def evaluate_and_subscribe_template(self, template, parameters, text=None): """Evaluate and subscribe template.""" try: value, subscriptions = self._eval(template, parameters, True) except TemplateEvalError as e: value = e subscriptions = e.subscriptions except ValueError as e: raise AssertionError( "Failed to evaluate and subscribe template {} with parameters {}. " "See error above.".format(text, parameters)) from e if not subscriptions: future = asyncio.Future() elif len(subscriptions) == 1: future = subscriptions[0] else: future = Util.any(subscriptions) future = asyncio.ensure_future(future) return value, future
def play(self, settings, context, calling_context, priority=0, **kwargs): """Post (delayed) events.""" for event, s in settings.items(): s = deepcopy(s) event_dict = self.machine.placeholder_manager.parse_conditional_template( event) if event_dict['condition'] and not event_dict[ 'condition'].evaluate(kwargs): continue if event_dict['number']: delay = Util.string_to_ms(event_dict['number']) self.delay.add(callback=self._post_event, ms=delay, event=event_dict['name'], s=s, **kwargs) else: self._post_event(event_dict['name'], s, **kwargs)
def get_complete_config(self): root_document = self.get_root_document() if self._cached_config: return self._cached_config config = self._load_document_and_subconfigs(root_document) if "modes" in config: for mode in config['modes']: path = os.path.join(self._root_path, "modes", mode, "config", "{}.yaml".format(mode)) if os.path.exists(path): mode_document = self.get_document(uris.from_fs_path(path)) mode_config = self._load_document_and_subconfigs( mode_document) mode_config.pop("mode", None) config = Util.dict_merge(config, mode_config) self._cached_config = config return config
def add_platform(self, name: str) -> None: """Make an additional hardware platform interface available to MPF. Args: name: String name of the platform to add. Must match the name of a platform file in the mpf/platforms folder (without the .py extension). """ if name not in self.hardware_platforms: if name not in self.config['mpf']['platforms']: raise AssertionError("Invalid platform {}".format(name)) try: hardware_platform = Util.string_to_class(self.config['mpf']['platforms'][name]) except ImportError: # pragma: no cover raise ImportError("Cannot add hardware platform {}. This is " "not a valid platform name".format(name)) self.hardware_platforms[name] = ( hardware_platform(self))
def test_control_events_arguments(self): for device_type in self.machine.config['mpf']['device_modules']: device_cls = Util.string_to_class(device_type) config_spec = self.machine.config_validator.config_spec[ device_cls.config_section] for k in config_spec: if not k.endswith( '_events' ) or k == "control_events" or config_spec[k] == "ignore": continue method_name = k[:-7] method = getattr(device_cls, "event_{}".format(method_name), None) self.assertIsNotNone( method, "Method {}.event_{} is missing for {}".format( device_type, method_name, k)) sig = inspect.signature(method) self.assertTrue( sig.parameters['self'], "Method {}.{} is missing self. Actual signature: {}". format(device_type, method_name, sig)) self.assertTrue( 'kwargs' in sig.parameters, "Method {}.{} is missing **kwargs. Actual signature: {}". format(device_type, method_name, sig)) self.assertEqual( sig.parameters['kwargs'].kind, inspect._VAR_KEYWORD, "Method {}.{} kwargs param is missing '**'".format( device_type, method_name)) self.assertTrue( hasattr(method, "relative_priority"), "Method {}.{} is missing a relative_priority. Did you apply the event_handler " "decorator?".format(device_type, method_name))
def _parse_driver_number(self, number): try: board_str, driver_str = number.split("-") except ValueError: total_drivers = 0 for board_obj in self.io_boards.values(): total_drivers += board_obj.driver_count try: index = self.convert_number_from_config(number) except ValueError: self.raise_config_error( "Could not parse driver number {}. Please verify the number format is either " "board-driver or driver. Driver should be an integer here." .format(number), 7) if int(index, 16) >= total_drivers: raise AssertionError( "Driver {} does not exist. Only {} drivers found. Driver number: {}" .format(int(index, 16), total_drivers, number)) return index board = int(board_str) driver = int(driver_str) if board not in self.io_boards: raise AssertionError( "Board {} does not exist for driver {}".format(board, number)) if self.io_boards[board].driver_count <= driver: raise AssertionError( "Board {} only has {} drivers. Driver: {}".format( board, self.io_boards[board].driver_count, number)) index = 0 for board_number, board_obj in self.io_boards.items(): if board_number >= board: continue index += board_obj.driver_count return Util.int_to_hex_string(index + driver)
def __init__(self, mc, name, config, member_cls): """Initialise asset pool.""" self.machine = mc self.priority = None self.name = name self.config = config self.member_cls = member_cls self.loading_members = set() self._callbacks = set() self.assets = list() self._last_asset = None self._asset_sequence = deque() self._assets_sent = set() self._total_weights = 0 if 'load' not in config: config['load'] = 'on_demand' if 'type' not in config: config['type'] = 'sequence' for asset in Util.string_to_list( self.config[self.member_cls.config_section]): try: name, number = asset.split('|') if not number: number = 1 else: number = int(number) except ValueError: name = asset number = 1 try: self.assets.append( (getattr(self.machine, self.member_cls.attribute)[name], number)) except KeyError: # pragma: no cover raise ValueError("No asset named {}".format(name)) self._configure_return_asset()
def __init__(self, mc, name, config, member_cls): """Initialise asset pool.""" self.machine = mc self.priority = None self.name = name self.config = config self.member_cls = member_cls self.loading_members = set() self._callbacks = set() self.assets = list() self._last_asset = None self._asset_sequence = deque() self._assets_sent = set() self._total_weights = 0 self._has_conditions = False if 'load' not in config: config['load'] = 'on_demand' if 'type' not in config: config['type'] = 'sequence' for asset in Util.string_to_list( self.config[self.member_cls.config_section]): asset_dict = self.machine.placeholder_manager.parse_conditional_template( asset, default_number=1) # For efficiency, track whether any assets have conditions if asset_dict['condition']: self._has_conditions = True try: self.assets.append( (getattr(self.machine, self.member_cls.attribute)[asset_dict['name']], asset_dict['number'], asset_dict['condition'])) except KeyError: # pragma: no cover raise ValueError("No asset named {}".format( asset_dict['name'])) self._configure_return_asset()
def setUp(self): # Most of the setup is done in run(). Explanation is there. Config._named_configs.pop('app', None) self._start_time = time() self._current_time = self._start_time Clock._start_tick = self._start_time Clock._last_tick = self._start_time Clock.time = self._mc_time # prevent sleep in clock Clock._max_fps = 0 Clock._events = [[] for i in range(256)] self._test_started = self._start_time from mpf.core.player import Player Player.monitor_enabled = False mpf_config = ConfigProcessor.load_config_file( os.path.abspath( os.path.join(mpfmc.__path__[0], os.pardir, self.get_options()['mcconfigfile'])), 'machine') machine_path = self.getAbsoluteMachinePath() mpf_config = load_machine_config( Util.string_to_list(self.get_config_file()), machine_path, mpf_config['mpf-mc']['paths']['config'], mpf_config) self.preprocess_config(mpf_config) self.mc = MpfMc(options=self.get_options(), config=mpf_config, machine_path=machine_path) self.patch_bcp() from kivy.core.window import Window Window.create_window() Window.canvas.clear() self._start_app_as_slave()
def _setup_bcp_connections(self, queue: QueuedEvent, **kwargs): """Connect to BCP servers from MPF config.""" del kwargs if ('connections' not in self.machine.config['bcp'] or not self.machine.config['bcp']['connections']): return client_connect_futures = [] for name, settings in self.machine.config['bcp']['connections'].items( ): settings = self.machine.config_validator.validate_config( "bcp:connections", settings) self.machine.events.post('bcp_connection_attempt', name=name, host=settings['host'], port=settings['port']) '''event: bcp_connection_attempt desc: MPF is attempting to make a BCP connection. args: name: The name of the connection. host: The host name MPF is attempting to connect to. port: The TCP port MPF is attempting to connect to''' client = Util.string_to_class(settings['type'])(self.machine, name, self.machine.bcp) client.exit_on_close = settings['exit_on_close'] connect_future = asyncio.ensure_future( client.connect(settings), loop=self.machine.clock.loop) connect_future.add_done_callback( partial(self.transport.register_transport, client)) client_connect_futures.append(connect_future) # block init until all clients are connected if client_connect_futures: queue.wait() future = asyncio.ensure_future(asyncio.wait( iter(client_connect_futures), loop=self.machine.clock.loop), loop=self.machine.clock.loop) future.add_done_callback(lambda x: queue.clear()) future.add_done_callback(self._bcp_clients_connected)
def parse_config(self, mode_config): """Parse a yaml config file and create mappings for required assets.""" if not mode_config.get('sounds'): return self for sound_pool in mode_config.get('sound_pools', {}).values(): for soundname in Util.string_to_list(sound_pool['sounds']): if soundname in self._pool_tracks and self._pool_tracks[ soundname] != sound_pool['track']: print( "ERROR: Sound {} exists in multiple pools/tracks in config {}" .format(soundname, self.name)) return try: self._pool_tracks[soundname] = sound_pool['track'] except KeyError: raise AttributeError( "Sound pool '{}'' has no track".format(soundname)) for soundname, sound in mode_config['sounds'].items(): self._add_sound(sound, pool_track=self._pool_tracks.get(soundname))
def enable(self, pulse_settings: PulseSettings, hold_settings: HoldSettings): """Enable (turn on) this driver.""" if self.autofire: # If this driver is also configured for an autofire rule, we just # manually trigger it with the trigger_cmd and manual on ('03') cmd = '{}{},03'.format(self.get_trigger_cmd(), self.number) else: # Otherwise we send a full config command, trigger C1 (logic triggered # and drive now) switch ID 00, mode 18 (latched) cmd = '{}{},C1,00,18,{},{},{},{}'.format( self.get_config_cmd(), self.number, Util.int_to_hex_string(pulse_settings.duration), self.get_pwm_for_cmd(pulse_settings.power), self.get_pwm_for_cmd(hold_settings.power), self.get_recycle_ms_for_cmd(self.config.default_recycle, pulse_settings.duration)) self.log.debug("Sending Enable Command: %s", cmd) self.send(cmd)
def _run(self): yield from self.machine.events.wait_for_event("init_phase_3") self.check_hw_switches() while True: # wait for either a new value or a switch change switch_change_future = self.machine.switch_controller.wait_for_any_switch( switch_names=[ switch.name for switch in self.value_switches if switch is not None ], state=2, ms=self.config['hw_confirm_time']) result = yield from Util.first( [switch_change_future, self._busy.wait()], loop=self.machine.clock.loop) if result == switch_change_future: self.check_hw_switches() continue # advance the reel until we reached our destination position yield from self._advance_reel_if_position_does_not_match()
def set_pulse_on_hit_rule(self, enable_switch: SwitchSettings, coil: DriverSettings): """Set pulse on hit rule on driver.""" self.debug_log("Setting Pulse on hit and release HW Rule. Switch: %s," "Driver: %s", enable_switch.hw_switch.number, coil.hw_driver.number) self._check_switch_coil_combincation(enable_switch, coil) driver = coil.hw_driver cmd = '{}{},{},{},10,{},{},00,00,{}'.format( driver.get_config_cmd(), coil.hw_driver.number, driver.get_control_for_cmd(enable_switch), enable_switch.hw_switch.number[0], Util.int_to_hex_string(coil.pulse_settings.duration), driver.get_pwm_for_cmd(coil.pulse_settings.power), driver.get_recycle_ms_for_cmd(coil.recycle, coil.pulse_settings.duration)) enable_switch.hw_switch.configure_debounce(enable_switch.debounce) driver.set_autofire(cmd, coil.pulse_settings.duration, coil.pulse_settings.power, 0)
def _game_starting(self, queue, **kwargs): """Reset the score reels when a new game starts. This is a queue event so it doesn't allow the game start to continue until it's done. Args: queue: A reference to the queue object for the game starting event. """ del kwargs # tell the game_starting event queue that we have stuff to do queue.wait() futures = [] for score_reel_group in self.machine.score_reel_groups: score_reel_group.set_value(0) futures.append(score_reel_group.wait_for_ready()) future = asyncio.wait(iter(futures), loop=self.machine.clock.loop) future = Util.ensure_future(future, loop=self.machine.clock.loop) future.add_done_callback(partial(self._reels_ready, queue=queue))
def add_platform(self, name: str) -> None: """Make an additional hardware platform interface available to MPF. Args: ---- name: String name of the platform to add. Must match the name of a platform file in the mpf/platforms folder (without the .py extension). """ if name not in self.hardware_platforms: if name in self.config['mpf']['platforms']: # if platform is in config load it try: hardware_platform = Util.string_to_class( self.config['mpf']['platforms'][name]) except ImportError as e: # pragma: no cover if e.name != name: # do not swallow unrelated errors raise raise ImportError( "Cannot add hardware platform {}. This is " "not a valid platform name".format(name)) else: # check entry points entry_points = list( iter_entry_points(group='mpf.platforms', name=name)) if entry_points: # load platform from entry point self.debug_log( "Loading platform %s from external entry_point", name) if len(entry_points) != 1: raise AssertionError( "Got more than one entry point for platform {}: {}" .format(name, entry_points)) hardware_platform = entry_points[0].load() else: raise AssertionError("Unknown platform {}".format(name)) self.hardware_platforms[name] = hardware_platform(self)
def _validate_dict_or_omap(self, item_type, validation, validation_failure_info, item): if ':' not in validation: self.validation_error(item, validation_failure_info, "Missing : in dict validator.") validators = validation.split(':') if item_type == "omap": item_dict = OrderedDict() if not isinstance(item, OrderedDict): self.validation_error( item, validation_failure_info, "Item is not an ordered dict. " "Did you forget to add !!omap to your entry?", 7) elif item_type == "dict": item_dict = dict() if item == "None" or item is None: item = {} if not isinstance(item, dict): self.validation_error(item, validation_failure_info, "Item is not a dict.", 12) elif item_type == "event_handler": # item could be str, list, or list of dicts item_dict = dict() try: item = Util.event_config_to_dict(item) except TypeError: self.validation_error(item, validation_failure_info, "Could not convert item to dict", 8) else: raise AssertionError("Invalid type {}".format(item_type)) for k, v in item.items(): item_dict[self.validate_item( k, validators[0], validation_failure_info)] = (self.validate_item( v, validators[1], ValidationPath(validation_failure_info, k))) return item_dict
def validate_item(self, item, validator, validation_failure_info): """Validate an item using a validator.""" try: if item.lower() == 'none': item = None except AttributeError: pass if ':' in validator: validator = validator.split(':') # item could be str, list, or list of dicts item = Util.event_config_to_dict(item) return_dict = dict() for k, v in item.items(): return_dict[self.validate_item( k, validator[0], validation_failure_info)] = (self.validate_item( v, validator[1], validation_failure_info)) return return_dict elif '(' in validator and ')' in validator[-1:] == ')': validator_parts = validator.split('(') validator = validator_parts[0] param = validator_parts[1][:-1] return self.validator_list[validator]( item, validation_failure_info=validation_failure_info, param=param) elif validator in self.validator_list: return self.validator_list[validator]( item, validation_failure_info=validation_failure_info) else: raise ConfigFileError( "Invalid Validator '{}' in config spec {}:{}".format( validator, validation_failure_info[0][0], validation_failure_info[1]), 4, self.log.name)
def play(self, settings, context, calling_context, priority=0, **kwargs): """Set light color based on config.""" instance_dict = self._get_instance_dict(context) full_context = self._get_full_context(context) del kwargs for light, s in settings.items(): s = deepcopy(s) try: s['priority'] += priority except KeyError: s['priority'] = priority if isinstance(light, str): light_names = Util.string_to_list(light) for light_name in light_names: # skip non-replaces placeholders if not light_name or light_name[0:1] == "(" and light_name[-1:] == ")": continue self._light_named_color(light_name, instance_dict, full_context, s['color'], s["fade"], s['priority']) else: self._light_color(light, instance_dict, full_context, s['color'], s["fade"], s['priority'])
def pulse(self, coil, milliseconds): """Pulse this driver.""" if not self.can_be_pulsed: if self.use_switch: raise AssertionError( "Cannot currently pulse driver {} because hw_rule needs hold_power" .format(self.number)) self.solCard.platform.reconfigure_driver(coil, False) if milliseconds and milliseconds != self.config['pulse_ms']: raise AssertionError( "OPP platform doesn't allow changing pulse width using pulse call. " "Tried {}, used {}".format(milliseconds, self.config['pulse_ms'])) _, _, solenoid = self.number.split("-") sol_int = int(solenoid) self.log.debug("Pulsing solenoid %s", self.number) self._kick_coil(sol_int, True) hex_ms_string = self.config['pulse_ms'] return Util.hex_string_to_int(hex_ms_string)
async def initialize_devices(self): """Initialise devices.""" futures = [] for device_type in self.machine.config['mpf']['device_modules']: device_cls = Util.string_to_class(device_type) collection_name, config_name = device_cls.get_config_info() if config_name not in self.machine.config: continue # Get the config section for these devices collection = getattr(self.machine, collection_name) config = self.machine.config[config_name] # add machine wide for device_name in config: futures.append( collection[device_name].device_added_system_wide()) await asyncio.wait(futures)
def _load_config_from_files(self) -> None: self.log.info("Loading config from original files") self.config = self._get_mpf_config() self.config['_mpf_version'] = __version__ for num, config_file in enumerate(self.options['configfile']): if not (config_file.startswith('/') or config_file.startswith('\\')): config_file = os.path.join(self.machine_path, self.config['mpf']['paths']['config'], config_file) self.log.info("Machine config file #%s: %s", num + 1, config_file) self.config = Util.dict_merge(self.config, ConfigProcessor.load_config_file( config_file, config_type='machine')) if self.options['create_config_cache']: self._cache_config()
def hex_to_rgb(_hex, default=None): """Convert a HEX color representation to an RGB color representation. Args: _hex: The 3- or 6-char hexadecimal string representing the color value. default: The default value to return if _hex is invalid. Returns: RGB representation of the input HEX value as a 3-item tuple with each item being an integer 0-255. """ if not Util.is_hex_string(_hex): return default _hex = str(_hex).strip('#') n = len(_hex) // 3 r = int(_hex[:n], 16) g = int(_hex[n:2 * n], 16) b = int(_hex[2 * n:3 * n], 16) return r, g, b
def process_animations(self, config): # config is localized to the slide's 'animations' section for event_name, event_settings in config.items(): # str means it's a list of named animations if isinstance(event_settings, str): event_settings = Util.string_to_list(event_settings) # dict means it's a single set of settings for one animation step elif isinstance(event_settings, dict): event_settings = [event_settings] # ultimately we're producing a list of dicts, so build that list # as we iterate new_list = list() for settings in event_settings: new_list.append(self.process_animation(settings)) config[event_name] = new_list return config
def _run(self): """Handle timeouts.""" while True: if not self._incoming_balls: self._has_incoming_balls.clear() self._has_no_incoming_balls.set() # sleep until we have incoming balls yield from self._has_incoming_balls.wait() # the incoming balls may have been removed in the meantime if not self._incoming_balls: continue # wait for timeouts on incoming balls futures = [ incoming_ball.wait_for_timeout() for incoming_ball in self._incoming_balls ] yield from Util.first(futures, loop=self.machine.clock.loop) yield from self._is_timeouting.acquire() # handle timeouts timeouts = [] for incoming_ball in self._incoming_balls: if incoming_ball.is_timeouted: timeouts.append(incoming_ball) for incoming_ball in timeouts: self.ball_device.log.warning( "Incoming ball from %s timeouted.", incoming_ball.source) self._incoming_balls.remove(incoming_ball) for incoming_ball in timeouts: yield from self.ball_device.lost_incoming_ball( source=incoming_ball.source) self._is_timeouting.release()
def _load_device_modules(self, **kwargs): del kwargs # step 1: create devices in machine collection self.debug_log("Creating devices...") for device_type in self.machine.config['mpf']['device_modules']: device_cls = Util.string_to_class(device_type) # type: Device collection_name, config = device_cls.get_config_info() self.device_classes[collection_name] = device_cls # create the collection collection = DeviceCollection(self.machine, collection_name, device_cls.config_section) self.collections[collection_name] = collection setattr(self.machine, collection_name, collection) # Get the config section for these devices config = self.machine.config.get(config, None) # create the devices if config: self.create_devices(collection_name, config) # create the default control events try: self._create_default_control_events(collection) except KeyError: pass self.machine.mode_controller.create_mode_devices() # step 2: load config and validate devices self.load_devices_config(validate=True) yield from self.machine.mode_controller.load_mode_devices() # step 3: initialise devices (mode devices will be initialised when mode is started) yield from self.initialize_devices()
def do_load(self): """Load the image.""" # This is the method that's actually called to load the asset from # disk. It's called by the loader thread so it's ok to block. However # since it's a separate thread, don't update any other attributes. # When you're done loading and return, the asset will be processed and # the various load status attributes will be updated automatically, # and anything that was waiting for it to load will be called. So # all you have to do here is load and return. if self.config.get('image_template'): try: template = self.machine.machine_config['image_templates'][self.config['image_template']] self.config = Util.dict_merge(template, self.config) except KeyError: raise KeyError("Image template '{}' was not found, referenced in image config {}".format( self.config['image_template'], self.config)) if self.machine.machine_config['mpf-mc']['zip_lazy_loading']: # lazy loading for zip file image sequences ImageLoader.zip_loader = KivyImageLoaderPatch.lazy_zip_loader self._image = Image(self.config['file'], keep_data=False, scale=1.0, mipmap=False, anim_delay=-1, nocache=True) self._image.anim_reset(False) if self.config.get('frame_skips'): # Frames are provided in 1-index values, but the image animates in zero-index values self.frame_skips = {s['from'] - 1: s['to'] - 1 for s in self.config['frame_skips']} # load first texture to speed up first display self._callbacks.add(lambda x: self._image.texture)
def parse_light_number_to_channels(self, number: str, subtype: str): """Parse light channels from number string.""" if subtype == "gi": if self.machine_type == 'wpc': # translate number to FAST GI number number = fast_defines.wpc_gi_map.get(str(number).upper()) else: number = self.convert_number_from_config(number) return [{"number": number}] elif subtype == "matrix": if self.machine_type == 'wpc': # translate number to FAST light num number = fast_defines.wpc_light_map.get(str(number).upper()) else: number = self.convert_number_from_config(number) return [{"number": number}] elif not subtype or subtype == "led": # if the LED number is in <channel> - <led> format, convert it to a # FAST hardware number if '-' in str(number): num = str(number).split('-') number = Util.int_to_hex_string((int(num[0]) * 64) + int(num[1])) else: number = self.convert_number_from_config(number) return [ { "number": number + "-0" }, { "number": number + "-1" }, { "number": number + "-2" }, ] else: raise AssertionError("Unknown subtype {}".format(subtype))
async def get_hw_switch_states(self): """Return hw switch states.""" if not self.initial_states_sent: if 'virtual_platform_start_active_switches' in self.machine.config: initial_active_switches = [] for switch in Util.string_to_list( self.machine. config['virtual_platform_start_active_switches']): if switch not in self.machine.switches: if " " in switch: self.raise_config_error( "MPF no longer supports lists separated by space in " "virtual_platform_start_active_switches. Please separate " "switches by comma: {}.".format(switch), 1) else: self.raise_config_error( "Switch {} used in virtual_platform_start_active_switches was not " "found in switches section.".format(switch), 1) initial_active_switches.append( self.machine.switches[switch].hw_switch.number) for k in self.hw_switches: if k in initial_active_switches: self.hw_switches[k] ^= 1 self.initial_states_sent = True else: switches = [ x for x in self.machine.switches.values() if x.platform == self ] for switch in switches: self.hw_switches[ switch.hw_switch.number] = switch.state ^ switch.invert return self.hw_switches
def _handle_confirm(self, eject_request: OutgoingBall, ball_eject_process: EjectTracker, incoming_ball_at_target: IncomingBall) -> Generator[int, None, bool]: # TODO: check double eject (two balls left). can only happen when not jammed timeout = eject_request.eject_timeout self.debug_log("Wait for confirm with timeout %s", timeout) confirm_future = incoming_ball_at_target.wait_for_confirm() try: yield from Util.first([confirm_future], timeout=timeout, loop=self.machine.clock.loop, cancel_others=False) except asyncio.TimeoutError: self.ball_device.set_eject_state("failed_confirm") self.debug_log("Got timeout before confirm") return (yield from self._handle_late_confirm_or_missing(eject_request, ball_eject_process, incoming_ball_at_target)) else: if not confirm_future.done(): raise AssertionError("Future not done") if confirm_future.cancelled(): raise AssertionError("Eject failed but should not") # eject successful self.debug_log("Got eject confirm") yield from self._handle_eject_success(ball_eject_process, eject_request) return True