class TextUi(MpfController): """Handles the text-based UI.""" config_name = "text_ui" __slots__ = [ "start_time", "machine", "_tick_task", "screen", "mpf_process", "ball_devices", "switches", "config", "_pending_bcp_connection", "_asset_percent", "_player_widgets", "_machine_widgets", "_bcp_status", "frame", "layout", "scene", "footer_memory", "switch_widgets", "mode_widgets", "ball_device_widgets", "footer_cpu", "footer_mc_cpu", "footer_uptime", "delay", "_layout_change" ] def __init__(self, machine: "MachineController") -> None: """Initialize TextUi.""" super().__init__(machine) self.delay = DelayManager(machine) self.config = machine.config.get('text_ui', {}) self.screen = None if not machine.options['text_ui'] or not Scene: return # hack to add themes until https://github.com/peterbrittain/asciimatics/issues/207 is implemented THEMES["mpf_theme"] = defaultdict( lambda: (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK), { "active_switch": (Screen.COLOUR_BLACK, Screen.A_NORMAL, Screen.COLOUR_GREEN), "pf_active": (Screen.COLOUR_GREEN, Screen.A_NORMAL, Screen.COLOUR_BLACK), "pf_inactive": (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK), "label": (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK), "title": (Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_RED), "title_exit": (Screen.COLOUR_BLACK, Screen.A_NORMAL, Screen.COLOUR_RED), "footer_cpu": (Screen.COLOUR_CYAN, Screen.A_NORMAL, Screen.COLOUR_BLACK), "footer_path": (Screen.COLOUR_YELLOW, Screen.A_NORMAL, Screen.COLOUR_BLACK), "footer_memory": (Screen.COLOUR_GREEN, Screen.A_NORMAL, Screen.COLOUR_BLACK), "footer_mc_cpu": (Screen.COLOUR_MAGENTA, Screen.A_NORMAL, Screen.COLOUR_BLACK), }) self.start_time = datetime.now() self.machine = machine self.mpf_process = Process() self.ball_devices = list() # type: List[BallDevice] self.switches = {} # type: Dict[str, Switch] self.machine.events.add_handler('init_phase_2', self._init) # self.machine.events.add_handler('init_phase_3', self._init2) self.machine.events.add_handler('loading_assets', self._asset_load_change) self.machine.events.add_handler('bcp_connection_attempt', self._bcp_connection_attempt) self.machine.events.add_handler('asset_loading_complete', self._asset_load_complete) self.machine.events.add_handler('bcp_clients_connected', self._bcp_connected) self.machine.events.add_handler('shutdown', self.stop) self.machine.add_crash_handler(self.stop) self.machine.events.add_handler('player_number', self._update_player) self.machine.events.add_handler('player_ball', self._update_player) self.machine.events.add_handler('player_score', self._update_player) self.machine.events.add_handler('ball_ended', self._update_player) self._pending_bcp_connection = False self._asset_percent = 0 self._bcp_status = (0, 0, 0) # type: Tuple[float, int, int] self.switch_widgets = [] # type: List[Widget] self.mode_widgets = [] # type: List[Widget] self.ball_device_widgets = [] # type: List[Widget] self._machine_widgets = [] # type: List[Widget] self._player_widgets = [] # type: List[Widget] self.footer_memory = None self.footer_cpu = None self.footer_mc_cpu = None self.footer_uptime = None self._layout_change = True self._tick_task = self.machine.clock.schedule_interval(self._tick, 1) self._create_window() self._draw_screen() def _init(self, **kwargs): del kwargs for mode in self.machine.modes.values(): self.machine.events.add_handler( "mode_{}_started".format(mode.name), self._mode_change) self.machine.events.add_handler( "mode_{}_stopped".format(mode.name), self._mode_change) self.machine.switch_controller.add_monitor(self._update_switches) self.machine.register_monitor("machine_vars", self._update_machine_vars) self.machine.variables.machine_var_monitor = True self.machine.bcp.interface.register_command_callback( "status_report", self._bcp_status_report) for bd in [ x for x in self.machine.ball_devices.values() if not x.is_playfield() ]: self.ball_devices.append(bd) self.ball_devices.sort() self._update_switch_layout() self._schedule_draw_screen() async def _bcp_status_report(self, client, cpu, rss, vms): del client self._bcp_status = cpu, rss, vms def _update_stats(self): # Runtime rt = (datetime.now() - self.start_time) mins, sec = divmod(rt.seconds + rt.days * 86400, 60) hours, mins = divmod(mins, 60) self.footer_uptime.text = 'RUNNING {:d}:{:02d}:{:02d}'.format( hours, mins, sec) # System Stats self.footer_memory.text = 'Free Memory (MB): {} CPU:{:3d}%'.format( round(virtual_memory().available / 1048576), round(cpu_percent(interval=None, percpu=False))) # MPF process stats self.footer_cpu.text = 'MPF (CPU RSS/VMS): {}% {}/{} MB '.format( round(self.mpf_process.cpu_percent()), round(self.mpf_process.memory_info().rss / 1048576), round(self.mpf_process.memory_info().vms / 1048576)) # MC process stats if self._bcp_status != (0, 0, 0): self.footer_mc_cpu.text = 'MC (CPU RSS/VMS) {}% {}/{} MB '.format( round(self._bcp_status[0]), round(self._bcp_status[1] / 1048576), round(self._bcp_status[2] / 1048576)) else: self.footer_mc_cpu.text = "" def _update_switch_layout(self): num = 0 self.switch_widgets = [] self.switches = {} self.switch_widgets.append((Label("SWITCHES"), 1)) self.switch_widgets.append((Divider(), 1)) self.switch_widgets.append((Label(""), 2)) self.switch_widgets.append((Divider(), 2)) for sw in sorted(self.machine.switches.values()): if sw.invert: name = sw.name + '*' else: name = sw.name col = 1 if num <= int(len(self.machine.switches) / 2) else 2 switch_widget = Label(name) if sw.state: switch_widget.custom_colour = "active_switch" self.switch_widgets.append((switch_widget, col)) self.switches[sw.name] = (sw, switch_widget) num += 1 self._schedule_draw_screen() def _update_switches(self, change, *args, **kwargs): del args del kwargs try: sw, switch_widget = self.switches[change.name] except KeyError: return if sw.state: switch_widget.custom_colour = "active_switch" else: switch_widget.custom_colour = "label" self._schedule_draw_screen() def _draw_switches(self): """Draw all switches.""" for widget, column in self.switch_widgets: self.layout.add_widget(widget, column) def _mode_change(self, *args, **kwargs): # Have to call this on the next frame since the mode controller's # active list isn't updated yet del args del kwargs self.mode_widgets = [] self.mode_widgets.append(Label("ACTIVE MODES")) self.mode_widgets.append(Divider()) try: modes = self.machine.mode_controller.active_modes except AttributeError: modes = None if modes: for mode in modes: self.mode_widgets.append( Label('{} ({})'.format(mode.name, mode.priority))) else: self.mode_widgets.append(Label("No active modes")) # empty line at the end self.mode_widgets.append(Label("")) self._layout_change = True self._schedule_draw_screen() def _draw_modes(self): for widget in self.mode_widgets: self.layout.add_widget(widget, 0) def _draw_ball_devices(self): for widget in self.ball_device_widgets: self.layout.add_widget(widget, 3) def _update_ball_devices(self, **kwargs): del kwargs # TODO: do not create widgets. just update contents self.ball_device_widgets = [] self.ball_device_widgets.append(Label("BALL COUNTS")) self.ball_device_widgets.append(Divider()) try: for pf in self.machine.playfields.values(): widget = Label('{}: {} '.format(pf.name, pf.balls)) if pf.balls: widget.custom_colour = "pf_active" else: widget.custom_colour = "pf_inactive" self.ball_device_widgets.append(widget) except AttributeError: pass for bd in self.ball_devices: widget = Label('{}: {} ({})'.format(bd.name, bd.balls, bd.state)) if bd.balls: widget.custom_colour = "pf_active" else: widget.custom_colour = "pf_inactive" self.ball_device_widgets.append(widget) self.ball_device_widgets.append(Label("")) self._layout_change = True self._schedule_draw_screen() def _update_player(self, **kwargs): del kwargs self._player_widgets = [] self._player_widgets.append(Label("CURRENT PLAYER")) self._player_widgets.append(Divider()) try: player = self.machine.game.player self._player_widgets.append( Label('PLAYER: {}'.format(player.number))) self._player_widgets.append(Label('BALL: {}'.format(player.ball))) self._player_widgets.append( Label('SCORE: {:,}'.format(player.score))) except AttributeError: self._player_widgets.append(Label("NO GAME IN PROGRESS")) return player_vars = player.vars.copy() player_vars.pop('score', None) player_vars.pop('number', None) player_vars.pop('ball', None) names = self.config.get('player_vars', player_vars.keys()) for name in names: self._player_widgets.append( Label("{}: {}".format(name, player_vars[name]))) self._layout_change = True self._schedule_draw_screen() def _draw_player(self, **kwargs): del kwargs for widget in self._player_widgets: self.layout.add_widget(widget, 3) def _update_machine_vars(self, **kwargs): """Update machine vars.""" del kwargs self._machine_widgets = [] self._machine_widgets.append(Label("MACHINE VARIABLES")) self._machine_widgets.append(Divider()) machine_vars = self.machine.variables.machine_vars # If config defines explict vars to show, only show those. Otherwise, all names = self.config.get('machine_vars', machine_vars.keys()) for name in names: self._machine_widgets.append( Label("{}: {}".format(name, machine_vars[name]['value']))) self._layout_change = True self._schedule_draw_screen() def _draw_machine_variables(self): """Draw machine vars.""" for widget in self._machine_widgets: self.layout.add_widget(widget, 0) def _create_window(self): self.screen = Screen.open() self.frame = Frame(self.screen, self.screen.height, self.screen.width, has_border=False, title="Test") self.frame.set_theme("mpf_theme") title_layout = Layout([1, 5, 1]) self.frame.add_layout(title_layout) title_left = Label("") title_left.custom_colour = "title" title_layout.add_widget(title_left, 0) title = 'Mission Pinball Framework v{}'.format( mpf._version.__version__) # noqa title_text = Label(title, align="^") title_text.custom_colour = "title" title_layout.add_widget(title_text, 1) exit_label = Label("< CTRL + C > TO EXIT", align=">") exit_label.custom_colour = "title_exit" title_layout.add_widget(exit_label, 2) self.layout = MpfLayout([1, 1, 1, 1], fill_frame=True) self.frame.add_layout(self.layout) footer_layout = Layout([1, 1, 1]) self.frame.add_layout(footer_layout) self.footer_memory = Label("", align=">") self.footer_memory.custom_colour = "footer_memory" self.footer_uptime = Label("", align=">") self.footer_uptime.custom_colour = "footer_memory" self.footer_mc_cpu = Label("") self.footer_mc_cpu.custom_colour = "footer_mc_cpu" self.footer_cpu = Label("") self.footer_cpu.custom_colour = "footer_cpu" footer_path = Label(self.machine.machine_path) footer_path.custom_colour = "footer_path" footer_empty = Label("") footer_empty.custom_colour = "footer_memory" footer_layout.add_widget(footer_path, 0) footer_layout.add_widget(self.footer_cpu, 0) footer_layout.add_widget(footer_empty, 1) footer_layout.add_widget(self.footer_mc_cpu, 1) footer_layout.add_widget(self.footer_uptime, 2) footer_layout.add_widget(self.footer_memory, 2) self.scene = Scene([self.frame], -1) self.screen.set_scenes([self.scene], start_scene=self.scene) # prevent main from scrolling out the footer self.layout.set_max_height(self.screen.height - 2) def _schedule_draw_screen(self): # schedule the draw in 10ms if it is not scheduled self.delay.add_if_doesnt_exist(10, self._draw_screen, "draw_screen") def _draw_screen(self): if not self.screen: # probably drawing during game end return if self._layout_change: self.layout.clear_columns() self._draw_modes() self._draw_machine_variables() self._draw_switches() self._draw_ball_devices() self._draw_player() self.frame.fix() self._layout_change = False self.screen.force_update() self.screen.draw_next_frame() def _tick(self): if self.screen.has_resized(): self._create_window() self._update_ball_devices() self._update_stats() self._schedule_draw_screen() self.machine.bcp.transport.send_to_clients_with_handler( handler="_status_request", bcp_command="status_request") def _bcp_connection_attempt(self, name, host, port, **kwargs): del name del kwargs self._pending_bcp_connection = PopUpDialog( self.screen, 'WAITING FOR MEDIA CONTROLLER {}:{}'.format(host, port), []) self.scene.add_effect(self._pending_bcp_connection) self._schedule_draw_screen() def _bcp_connected(self, **kwargs): del kwargs self.scene.remove_effect(self._pending_bcp_connection) self._schedule_draw_screen() def _asset_load_change(self, percent, **kwargs): del kwargs if self._asset_percent: self.scene.remove_effect(self._asset_percent) self._asset_percent = PopUpDialog( self.screen, 'LOADING ASSETS: {}%'.format(percent), []) self.scene.add_effect(self._asset_percent) self._schedule_draw_screen() def _asset_load_complete(self, **kwargs): del kwargs self.scene.remove_effect(self._asset_percent) self._schedule_draw_screen() def stop(self, **kwargs): """Stop the Text UI and restore the original console screen.""" del kwargs if self.screen: self.machine.clock.unschedule(self._tick_task) self.screen.close(True) self.screen = None
class ComboSwitch(SystemWideDevice, ModeDevice): """Combo Switch device.""" config_section = 'combo_switches' collection = 'combo_switches' class_label = 'combo_switch' def __init__(self, machine, name): """Initialize Combo Switch.""" super().__init__(machine, name) self.states = ['inactive', 'both', 'one'] self._state = 'inactive' self._switches_1_active = False self._switches_2_active = False self.delay_registry = DelayManagerRegistry(self.machine) self.delay = DelayManager(self.delay_registry) def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str=None) -> dict: """Validate and parse config.""" config = super().validate_and_parse_config(config, is_mode_config, debug_prefix) for state in self.states: if not config['events_when_{}'.format(state)]: config['events_when_{}'.format(state)] = [ "{}_{}".format(self.name, state)] return config def device_added_system_wide(self): """Add event handlers.""" super().device_added_system_wide() self._add_switch_handlers() def device_loaded_in_mode(self, mode: Mode, player: Player): """Add event handlers.""" self._add_switch_handlers() def _add_switch_handlers(self): if self.config['tag_1']: for tag in self.config['tag_1']: for switch in self.machine.switches.items_tagged(tag): self.config['switches_1'].add(switch) if self.config['tag_2']: for tag in self.config['tag_2']: for switch in self.machine.switches.items_tagged(tag): self.config['switches_2'].add(switch) self._register_switch_handlers() @property def state(self): """Return current state.""" return self._state @property def can_exist_outside_of_game(self): """Return true if this device can exist outside of a game.""" return True def device_removed_from_mode(self, mode): """Mode ended. Args: mode: mode which stopped """ del mode self._remove_switch_handlers() self._kill_delays() def _register_switch_handlers(self): for switch in self.config['switches_1']: switch.add_handler(self._switch_1_went_active, state=1) switch.add_handler(self._switch_1_went_inactive, state=0) for switch in self.config['switches_2']: switch.add_handler(self._switch_2_went_active, state=1) switch.add_handler(self._switch_2_went_inactive, state=0) def _remove_switch_handlers(self): for switch in self.config['switches_1']: switch.remove_handler(self._switch_1_went_active, state=1) switch.remove_handler(self._switch_1_went_inactive, state=0) for switch in self.config['switches_2']: switch.remove_handler(self._switch_2_went_active, state=1) switch.remove_handler(self._switch_2_went_inactive, state=0) def _kill_delays(self): self.delay.clear() def _switch_1_went_active(self): self.debug_log('A switch from switches_1 just went active') self.delay.remove('switch_1_inactive') if self._switches_1_active: return if not self.config['hold_time']: self._activate_switches_1() else: self.delay.add_if_doesnt_exist(self.config['hold_time'], self._activate_switches_1, 'switch_1_active') def _switch_2_went_active(self): self.debug_log('A switch from switches_2 just went active') self.delay.remove('switch_2_inactive') if self._switches_2_active: return if not self.config['hold_time']: self._activate_switches_2() else: self.delay.add_if_doesnt_exist(self.config['hold_time'], self._activate_switches_2, 'switch_2_active') def _switch_1_went_inactive(self): self.debug_log('A switch from switches_1 just went inactive') for switch in self.config['switches_1']: if switch.state: # at least one switch is still active return self.delay.remove('switch_1_active') if not self.config['release_time']: self._release_switches_1() else: self.delay.add_if_doesnt_exist(self.config['release_time'], self._release_switches_1, 'switch_1_inactive') def _switch_2_went_inactive(self): self.debug_log('A switch from switches_2 just went inactive') for switch in self.config['switches_2']: if switch.state: # at least one switch is still active return self.delay.remove('switch_2_active') if not self.config['release_time']: self._release_switches_2() else: self.delay.add_if_doesnt_exist(self.config['release_time'], self._release_switches_2, 'switch_2_inactive') def _activate_switches_1(self): self.debug_log('Switches_1 has passed the hold time and is now ' 'active') self._switches_1_active = self.machine.clock.get_time() if self._switches_2_active: if (self.config['max_offset_time'] >= 0 and (self._switches_1_active - self._switches_2_active > self.config['max_offset_time'])): self.debug_log("Switches_2 is active, but the " "max_offset_time=%s which is largest than when " "a Switches_2 switch was first activated, so " "the state will not switch to 'both'", self.config['max_offset_time']) return self._switch_state('both') def _activate_switches_2(self): self.debug_log('Switches_2 has passed the hold time and is now ' 'active') self._switches_2_active = self.machine.clock.get_time() if self._switches_1_active: if (self.config['max_offset_time'] >= 0 and (self._switches_2_active - self._switches_1_active > self.config['max_offset_time'])): self.debug_log("Switches_2 is active, but the " "max_offset_time=%s which is largest than when " "a Switches_2 switch was first activated, so " "the state will not switch to 'both'", self.config['max_offset_time']) return self._switch_state('both') def _release_switches_1(self): self.debug_log('Switches_1 has passed the release time and is now ' 'releases') self._switches_1_active = None if self._switches_2_active and self._state == 'both': self._switch_state('one') elif self._state == 'one': self._switch_state('inactive') def _release_switches_2(self): self.debug_log('Switches_2 has passed the release time and is now ' 'releases') self._switches_2_active = None if self._switches_1_active and self._state == 'both': self._switch_state('one') elif self._state == 'one': self._switch_state('inactive') def _switch_state(self, state): """Post events for current step.""" if state not in self.states: raise ValueError("Received invalid state: {}".format(state)) if state == self.state: return self._state = state self.debug_log("New State: %s", state) for event in self.config['events_when_{}'.format(state)]: self.machine.events.post(event) '''event: (combo_switch)_(state) desc: Combo switch (name) changed to state (state). Note that these events can be overridden in a combo switch's config. Valid states are: *inactive*, *both*, or *one*. ..rubric:: both A switch from group 1 and group 2 are both active at the same time, having been pressed within the ``max_offset_time:`` and being active for at least the ``hold_time:``. ..rubric:: one Either switch 1 or switch 2 has been released for at least the ``release_time:`` but the other switch is still active. ..rubric:: inactive Both switches are inactive. ''' '''event: flipper_cancel
class Driver(SystemWideDevice): """Generic class that holds driver objects. A 'driver' is any device controlled from a driver board which is typically the high-voltage stuff like coils and flashers. This class exposes the methods you should use on these driver types of devices. Each platform module (i.e. P-ROC, FAST, etc.) subclasses this class to actually communicate with the physical hardware and perform the actions. Args: Same as the Device parent class """ config_section = 'coils' collection = 'coils' class_label = 'coil' __slots__ = ["hw_driver", "delay", "platform", "__dict__"] def __init__(self, machine: MachineController, name: str) -> None: """Initialise driver.""" self.hw_driver = None # type: Optional[DriverPlatformInterface] super().__init__(machine, name) self.delay = DelayManager(self.machine) self.platform = None # type: Optional[DriverPlatform] @classmethod def device_class_init(cls, machine: MachineController): """Register handler for duplicate coil number checks.""" machine.events.add_handler("init_phase_4", cls._check_duplicate_coil_numbers, machine=machine) @staticmethod def _check_duplicate_coil_numbers(machine, **kwargs): del kwargs check_set = set() for coil in machine.coils.values(): if not hasattr(coil, "hw_driver"): # skip dual wound and other special devices continue key = (coil.config['platform'], coil.hw_driver.number) if key in check_set: raise AssertionError( "Duplicate coil number {} for coil {}".format( coil.hw_driver.number, coil)) check_set.add(key) def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str = None) -> dict: """Return the parsed and validated config. Args: ---- config: Config of device is_mode_config: Whether this device is loaded in a mode or system-wide debug_prefix: Prefix to use when logging. Returns: Validated config """ config = super().validate_and_parse_config(config, is_mode_config, debug_prefix) platform = self.machine.get_platform_sections( 'coils', getattr(config, "platform", None)) platform.assert_has_feature("drivers") config['platform_settings'] = platform.validate_coil_section( self, config.get('platform_settings', None)) return config async def _initialize(self): await super()._initialize() self.platform = self.machine.get_platform_sections( 'coils', self.config['platform']) config = DriverConfig( name=self.name, default_pulse_ms=self.get_and_verify_pulse_ms(None), default_pulse_power=self.get_and_verify_pulse_power(None), default_hold_power=self.get_and_verify_hold_power(None), default_recycle=self.config['default_recycle'], max_pulse_ms=self.config['max_pulse_ms'], max_pulse_power=self.config['max_pulse_power'], max_hold_power=self.config['max_hold_power']) platform_settings = dict( self.config['platform_settings'] ) if self.config['platform_settings'] else dict() if not self.platform.features[ 'allow_empty_numbers'] and self.config['number'] is None: self.raise_config_error("Driver must have a number.", 1) try: self.hw_driver = self.platform.configure_driver( config, self.config['number'], platform_settings) except AssertionError as e: raise AssertionError( "Failed to configure driver {} in platform. See error above". format(self.name)) from e def get_and_verify_pulse_power(self, pulse_power: Optional[float]) -> float: """Return the pulse power to use. If pulse_power is None it will use the default_pulse_power. Additionally it will verify the limits. """ if pulse_power is None: pulse_power = self.config['default_pulse_power'] if self.config[ 'default_pulse_power'] is not None else 1.0 if pulse_power and 0 > pulse_power > 1: raise AssertionError( "Pulse power has to be between 0 and 1 but is {}".format( pulse_power)) max_pulse_power = 0 if self.config['max_pulse_power']: max_pulse_power = self.config['max_pulse_power'] elif self.config['default_pulse_power']: max_pulse_power = self.config['default_pulse_power'] if pulse_power > max_pulse_power: raise DriverLimitsError( "Driver may {} not be pulsed with pulse_power {} because max_pulse_power is {}" .format(self.name, pulse_power, max_pulse_power)) return pulse_power def get_and_verify_hold_power(self, hold_power: Optional[float]) -> float: """Return the hold power to use. If hold_power is None it will use the default_hold_power. Additionally it will verify the limits. """ if hold_power is None and self.config['default_hold_power']: hold_power = self.config['default_hold_power'] if hold_power is None and self.config['max_hold_power']: hold_power = self.config['max_hold_power'] if hold_power is None and self.config['allow_enable']: hold_power = 1.0 if hold_power is None: hold_power = 0.0 if hold_power and 0 > hold_power > 1: raise AssertionError( "Hold_power has to be between 0 and 1 but is {}".format( hold_power)) max_hold_power = 0 # type: float if self.config['max_hold_power']: max_hold_power = self.config['max_hold_power'] elif self.config['allow_enable']: max_hold_power = 1.0 elif self.config['default_hold_power']: max_hold_power = self.config['default_hold_power'] if hold_power > max_hold_power: raise DriverLimitsError( "Driver {} may not be enabled with hold_power {} because max_hold_power is {}" .format(self.name, hold_power, max_hold_power)) return hold_power def get_and_verify_pulse_ms(self, pulse_ms: Optional[int]) -> int: """Return and verify pulse_ms to use. If pulse_ms is None return the default. """ assert self.platform is not None if pulse_ms is None: if self.config['default_pulse_ms'] is not None: pulse_ms = self.config['default_pulse_ms'] else: pulse_ms = self.machine.config['mpf']['default_pulse_ms'] if not isinstance(pulse_ms, int): raise AssertionError("Wrong type {}".format(pulse_ms)) if 0 > pulse_ms > self.platform.features['max_pulse']: raise AssertionError("Pulse_ms {} is not valid.".format(pulse_ms)) if self.config[ 'max_pulse_ms'] and pulse_ms > self.config['max_pulse_ms']: raise DriverLimitsError( "Driver {} may not be pulsed with pulse_ms {} because max_pulse_ms is {}" .format(self.name, pulse_ms, self.config['max_pulse_ms'])) return pulse_ms @event_handler(2) def event_enable(self, pulse_ms: int = None, pulse_power: float = None, hold_power: float = None, **kwargs): """Event handler for control enable.""" del kwargs self.enable(pulse_ms, pulse_power, hold_power) def enable(self, pulse_ms: int = None, pulse_power: float = None, hold_power: float = None): """Enable a driver by holding it 'on'. Args: ---- pulse_ms: The number of milliseconds the driver should be enabled for. If no value is provided, the driver will be enabled for the value specified in the config dictionary. pulse_power: The pulse power. A float between 0.0 and 1.0. hold_power: The pulse power. A float between 0.0 and 1.0. If this driver is configured with a holdpatter, then this method will use that holdpatter to pwm pulse the driver. If not, then this method will just enable the driver. As a safety precaution, if you want to enable() this driver without pwm, then you have to add the following option to this driver in your machine configuration files: allow_enable: True """ assert self.hw_driver is not None pulse_ms = self.get_and_verify_pulse_ms(pulse_ms) pulse_power = self.get_and_verify_pulse_power(pulse_power) hold_power = self.get_and_verify_hold_power(hold_power) if hold_power == 0.0: raise DriverLimitsError("Cannot enable driver with hold_power 0.0") self.info_log( "Enabling Driver with power %s (pulse_ms %sms and pulse_power %s)", hold_power, pulse_ms, pulse_power) self.hw_driver.enable( PulseSettings(power=pulse_power, duration=pulse_ms), HoldSettings(power=hold_power)) if self.config['max_hold_duration']: self.delay.add_if_doesnt_exist( self.config['max_hold_duration'] * 1000, self._enable_limit_reached, "enable_limit_reached") # inform bcp clients self.machine.bcp.interface.send_driver_event( action="enable", name=self.name, number=self.config['number'], pulse_ms=pulse_ms, pulse_power=pulse_power, hold_power=hold_power) def _enable_limit_reached(self): """Disable driver and report service alert if max_hold_duration has been reached.""" self.log.warning( "Reached max_hold_duration for this coil. Will disable driver now to prevent damage!" ) self.disable() self.machine.service.add_technical_alert( self, "Reached max_hold_duration. Driver disabled to prevent damage!") @event_handler(1) def event_disable(self, **kwargs): """Event handler for disable control event.""" del kwargs self.disable() def disable(self): """Disable this driver.""" self.info_log("Disabling Driver") self.hw_driver.disable() self.delay.remove("enable_limit_reached") # inform bcp clients self.machine.bcp.interface.send_driver_event( action="disable", name=self.name, number=self.config['number']) def _get_wait_ms(self, pulse_ms: int, max_wait_ms: Optional[int]) -> int: """Determine if this pulse should be delayed.""" if max_wait_ms is None: self.config['psu'].notify_about_instant_pulse(pulse_ms=pulse_ms) return 0 return self.config['psu'].get_wait_time_for_pulse( pulse_ms=pulse_ms, max_wait_ms=max_wait_ms) def _pulse_now(self, pulse_ms: int, pulse_power: float) -> None: """Pulse this driver now.""" assert self.hw_driver is not None assert self.platform is not None if 0 < pulse_ms <= self.platform.features['max_pulse']: self.info_log("Pulsing Driver for %sms (%s pulse_power)", pulse_ms, pulse_power) self.hw_driver.pulse( PulseSettings(power=pulse_power, duration=pulse_ms)) else: self.info_log("Enabling Driver for %sms (%s pulse_power)", pulse_ms, pulse_power) self.delay.reset(name='timed_disable', ms=pulse_ms, callback=self.disable) self.hw_driver.enable(PulseSettings(power=pulse_power, duration=0), HoldSettings(power=pulse_power)) # inform bcp clients self.machine.bcp.interface.send_driver_event( action="pulse", name=self.name, number=self.config['number'], pulse_ms=pulse_ms, pulse_power=pulse_power) @event_handler(3) def event_pulse(self, pulse_ms: int = None, pulse_power: float = None, max_wait_ms: int = None, **kwargs) -> None: """Event handler for pulse control events.""" del kwargs self.pulse(pulse_ms, pulse_power, max_wait_ms) def pulse(self, pulse_ms: int = None, pulse_power: float = None, max_wait_ms: int = None) -> int: """Pulse this driver. Args: ---- pulse_ms: The number of milliseconds the driver should be enabled for. If no value is provided, the driver will be enabled for the value specified in the config dictionary. pulse_power: The pulse power. A float between 0.0 and 1.0. max_wait_ms: Maximum time this pulse may be delayed for PSU optimization. """ pulse_ms = self.get_and_verify_pulse_ms(pulse_ms) pulse_power = self.get_and_verify_pulse_power(pulse_power) wait_ms = self._get_wait_ms(pulse_ms, max_wait_ms) if wait_ms > 0: self.debug_log( "Delaying pulse by %sms pulse_ms: %sms (%s pulse_power)", wait_ms, pulse_ms, pulse_power) self.delay.add(wait_ms, self._pulse_now, pulse_ms=pulse_ms, pulse_power=pulse_power) else: self._pulse_now(pulse_ms, pulse_power) return wait_ms
class ComboSwitch(SystemWideDevice, ModeDevice): """Combo Switch device.""" config_section = 'combo_switches' collection = 'combo_switches' class_label = 'combo_switch' __slots__ = [ "states", "_state", "_switches_1_active", "_switches_2_active", "delay", "_switch_handlers" ] def __init__(self, machine, name): """Initialize Combo Switch.""" super().__init__(machine, name) self.states = ['inactive', 'both', 'one'] self._state = 'inactive' self._switches_1_active = False self._switches_2_active = False self.delay = DelayManager(self.machine) self._switch_handlers = [] def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str = None) -> dict: """Validate and parse config.""" config = super().validate_and_parse_config(config, is_mode_config, debug_prefix) for state in self.states + ["switches_1", "switches_2"]: if not config['events_when_{}'.format(state)]: config['events_when_{}'.format(state)] = [ "{}_{}".format(self.name, state) ] return config async def device_added_system_wide(self): """Add event handlers.""" await super().device_added_system_wide() self._add_switch_handlers() def device_loaded_in_mode(self, mode: Mode, player: Player): """Add event handlers.""" self._add_switch_handlers() def _add_switch_handlers(self): if self.config['tag_1']: for tag in self.config['tag_1']: for switch in self.machine.switches.items_tagged(tag): self.config['switches_1'].add(switch) if self.config['tag_2']: for tag in self.config['tag_2']: for switch in self.machine.switches.items_tagged(tag): self.config['switches_2'].add(switch) self._register_switch_handlers() @property def state(self): """Return current state.""" return self._state @property def can_exist_outside_of_game(self): """Return true if this device can exist outside of a game.""" return True def device_removed_from_mode(self, mode): """Mode ended. Args: ---- mode: mode which stopped """ del mode self._remove_switch_handlers() self._kill_delays() def _register_switch_handlers(self): for switch in self.config['switches_1']: self._switch_handlers.append( switch.add_handler(self._switch_1_went_active, state=1, return_info=True)) self._switch_handlers.append( switch.add_handler(self._switch_1_went_inactive, state=0, return_info=True)) for switch in self.config['switches_2']: self._switch_handlers.append( switch.add_handler(self._switch_2_went_active, state=1, return_info=True)) self._switch_handlers.append( switch.add_handler(self._switch_2_went_inactive, state=0, return_info=True)) def _remove_switch_handlers(self): self.machine.switch_controller.remove_switch_handler_by_keys( self._switch_handlers) self._switch_handlers = [] def _kill_delays(self): self.delay.clear() def _switch_1_went_active(self, switch_name, **kwargs): del kwargs self.debug_log('A switch from switches_1 just went active') self.delay.remove('switch_1_inactive') if self._switches_1_active: return if not self.config['hold_time']: self._activate_switches_1(switch_name) else: self.delay.add_if_doesnt_exist( self.config['hold_time'], partial(self._activate_switches_1, switch_name), 'switch_1_active') def _switch_2_went_active(self, switch_name, **kwargs): del kwargs self.debug_log('A switch from switches_2 just went active') self.delay.remove('switch_2_inactive') if self._switches_2_active: return if not self.config['hold_time']: self._activate_switches_2(switch_name) else: self.delay.add_if_doesnt_exist( self.config['hold_time'], partial(self._activate_switches_2, switch_name), 'switch_2_active') def _switch_1_went_inactive(self, switch_name, **kwargs): del kwargs self.debug_log('A switch from switches_1 just went inactive') for switch in self.config['switches_1']: if switch.state: # at least one switch is still active return self.delay.remove('switch_1_active') if not self.config['release_time']: self._release_switches_1(switch_name) else: self.delay.add_if_doesnt_exist( self.config['release_time'], partial(self._release_switches_1, switch_name), 'switch_1_inactive') def _switch_2_went_inactive(self, switch_name, **kwargs): del kwargs self.debug_log('A switch from switches_2 just went inactive') for switch in self.config['switches_2']: if switch.state: # at least one switch is still active return self.delay.remove('switch_2_active') if not self.config['release_time']: self._release_switches_2(switch_name) else: self.delay.add_if_doesnt_exist( self.config['release_time'], partial(self._release_switches_2, switch_name), 'switch_2_inactive') def _activate_switches_1(self, switch_name): self.debug_log('Switches_1 has passed the hold time and is now ' 'active') self._switches_1_active = self.machine.clock.get_time() self.delay.remove("switch_2_only") if self._switches_2_active: if (self.config['max_offset_time'] >= 0 and (self._switches_1_active - self._switches_2_active > self.config['max_offset_time'])): self.debug_log( "Switches_2 is active, but the " "max_offset_time=%s which is largest than when " "a Switches_2 switch was first activated, so " "the state will not switch to 'both'", self.config['max_offset_time']) return self._switch_state('both', group=1, switch=switch_name) elif self.config['max_offset_time'] >= 0: self.delay.add_if_doesnt_exist(self.config['max_offset_time'] * 1000, self._post_only_one_active_event, "switch_1_only", number=1) def _activate_switches_2(self, switch_name): self.debug_log('Switches_2 has passed the hold time and is now ' 'active') self._switches_2_active = self.machine.clock.get_time() self.delay.remove("switch_1_only") if self._switches_1_active: if (self.config['max_offset_time'] >= 0 and (self._switches_2_active - self._switches_1_active > self.config['max_offset_time'])): self.debug_log( "Switches_2 is active, but the " "max_offset_time=%s which is largest than when " "a Switches_2 switch was first activated, so " "the state will not switch to 'both'", self.config['max_offset_time']) return self._switch_state('both', group=2, switch=switch_name) elif self.config['max_offset_time'] >= 0: self.delay.add_if_doesnt_exist(self.config['max_offset_time'] * 1000, self._post_only_one_active_event, "switch_2_only", number=2) def _post_only_one_active_event(self, number): for event in self.config['events_when_switches_{}'.format(number)]: self.machine.events.post(event) def _release_switches_1(self, switch_name): self.debug_log('Switches_1 has passed the release time and is now ' 'releases') self._switches_1_active = None if self._switches_2_active and self._state == 'both': self._switch_state('one', group=1, switch=switch_name) elif self._state == 'one': self._switch_state('inactive', group=1, switch=switch_name) def _release_switches_2(self, switch_name): self.debug_log('Switches_2 has passed the release time and is now ' 'releases') self._switches_2_active = None if self._switches_1_active and self._state == 'both': self._switch_state('one', group=2, switch=switch_name) elif self._state == 'one': self._switch_state('inactive', group=2, switch=switch_name) def _switch_state(self, state, group, switch): """Post events for current step.""" if state not in self.states: raise ValueError("Received invalid state: {}".format(state)) if state == self.state: return self._state = state self.debug_log("New State: %s", state) for event in self.config['events_when_{}'.format(state)]: self.machine.events.post(event, triggering_group=group, triggering_switch=switch) '''event: (name)_one config_attribute: events_when_one desc: Combo switch (name) changed to state one. Either switch 1 or switch 2 has been released for at least the ``release_time:`` but the other switch is still active. ''' '''event: (name)_both config_attribute: events_when_both desc: Combo switch (name) changed to state both. A switch from group 1 and group 2 are both active at the same time, having been pressed within the ``max_offset_time:`` and being active for at least the ``hold_time:``. ''' '''event: (name)_inactive config_attribute: events_when_inactive desc: Combo switch (name) changed to state inactive. Both switches are inactive. ''' '''event: (name)_switches_1 config_attribute: events_when_switches_1 desc: Combo switch (name) changed to state switches_1. Only switches_1 is active. max_offset_time has passed and this hit cannot become both later on. Only emited when ``max_offset_time:`` is defined. ''' '''event: (name)_switches_2