Beispiel #1
0
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
Beispiel #2
0
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
Beispiel #3
0
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
Beispiel #4
0
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