Esempio n. 1
0
class BallSave(SystemWideDevice, ModeDevice):
    """Ball save device which will give back the ball within a certain time."""

    config_section = 'ball_saves'
    collection = 'ball_saves'
    class_label = 'ball_save'

    def __init__(self, machine: "MachineController", name: str) -> None:
        """Initialise ball save."""
        self.unlimited_saves = None  # type: bool
        self.source_playfield = None  # type: Playfield
        super().__init__(machine, name)

        self.delay = DelayManager(machine.delayRegistry)
        self.enabled = False
        self.timer_started = False
        self.saves_remaining = 0
        self.early_saved = 0
        self.state = 'disabled'
        self._scheduled_balls = 0

    @asyncio.coroutine
    def _initialize(self) -> None:
        yield from super()._initialize()
        self.unlimited_saves = self.config['balls_to_save'] == -1
        self.source_playfield = self.config['source_playfield']

    @property
    def can_exist_outside_of_game(self) -> bool:
        """Return true if this device can exist outside of a game."""
        return True

    def validate_and_parse_config(self,
                                  config: dict,
                                  is_mode_config: bool,
                                  debug_prefix: str = None) -> dict:
        """Make sure timer_start_events are not in enable_events."""
        config = super().validate_and_parse_config(config, is_mode_config,
                                                   debug_prefix)

        for event in config['timer_start_events']:
            if event in config['enable_events']:
                raise AssertionError(
                    "{}: event {} in timer_start_events will not work because it is also in "
                    "enable_events. Omit it!".format(event, str(self)))

        if config['delayed_eject_events'] and config['eject_delay']:
            raise AssertionError(
                "cannot use delayed_eject_events and eject_delay at the same time."
            )

        return config

    @event_handler(10)
    def enable(self, **kwargs) -> None:
        """Enable ball save."""
        del kwargs
        if self.enabled:
            return

        self.saves_remaining = self.config['balls_to_save']
        self.early_saved = 0
        self.enabled = True
        self.state = 'enabled'
        self.debug_log("Enabling. Auto launch: %s, Balls to save: %s",
                       self.config['auto_launch'],
                       self.config['balls_to_save'])

        # Enable shoot again
        self.machine.events.add_handler('ball_drain',
                                        self._ball_drain_while_active,
                                        priority=1000)

        if (self.config['active_time'] > 0
                and not self.config['timer_start_events']):
            self.timer_start()

        self.machine.events.post('ball_save_{}_enabled'.format(self.name))
        '''event: ball_save_(name)_enabled
        desc: The ball save called (name) has just been enabled.
        '''

    @event_handler(1)
    def disable(self, **kwargs) -> None:
        """Disable ball save."""
        del kwargs
        if not self.enabled:
            return

        self.enabled = False
        self.state = 'disabled'
        self.timer_started = False
        self.debug_log("Disabling...")
        self.machine.events.remove_handler(self._ball_drain_while_active)
        self.delay.remove('disable')
        self.delay.remove('hurry_up')
        self.delay.remove('grace_period')

        self.machine.events.post('ball_save_{}_disabled'.format(self.name))
        '''event: ball_save_(name)_disabled
        desc: The ball save called (name) has just been disabled.
        '''

    @event_handler(9)
    def timer_start(self, **kwargs) -> None:
        """Start the timer.

        This is usually called after the ball was ejected while the ball save may have been enabled earlier.
        """
        del kwargs
        if self.timer_started or not self.enabled:
            return

        self.timer_started = True

        self.machine.events.post('ball_save_{}_timer_start'.format(self.name))
        '''event: ball_save_(name)_timer_start
        desc: The ball save called (name) has just start its countdown timer.
        '''

        if self.config['active_time'] > 0:
            self.debug_log('Starting ball save timer: %ss',
                           self.config['active_time'] / 1000.0)

            self.delay.add(name='disable',
                           ms=(self.config['active_time'] +
                               self.config['grace_period']),
                           callback=self.disable)
            self.delay.add(name='grace_period',
                           ms=self.config['active_time'],
                           callback=self._grace_period)
            self.delay.add(name='hurry_up',
                           ms=(self.config['active_time'] -
                               self.config['hurry_up_time']),
                           callback=self._hurry_up)

    def _hurry_up(self) -> None:
        self.debug_log("Starting Hurry Up")

        self.state = 'hurry_up'

        self.machine.events.post('ball_save_{}_hurry_up'.format(self.name))
        '''event: ball_save_(name)_hurry_up
        desc: The ball save called (name) has just entered its hurry up mode.
        '''

    def _grace_period(self) -> None:
        self.debug_log("Starting Grace Period")

        self.state = 'grace_period'

        self.machine.events.post('ball_save_{}_grace_period'.format(self.name))
        '''event: ball_save_(name)_grace_period
        desc: The ball save called (name) has just entered its grace period
            time.
        '''

    def _get_number_of_balls_to_save(self, available_balls: int) -> int:
        no_balls_in_play = False

        try:
            if not self.machine.game.balls_in_play:
                no_balls_in_play = True

            if self.config[
                    'only_last_ball'] and self.machine.game.balls_in_play > 1:
                self.debug_log("Will only save last ball but %s are in play.",
                               self.machine.game.balls_in_play)
                return 0
        except AttributeError:
            no_balls_in_play = True

        if no_balls_in_play:
            self.debug_log("Received request to save ball, but no balls are in"
                           " play. Discarding request.")
            return 0

        balls_to_save = available_balls

        if self.config['only_last_ball'] and balls_to_save > 1:
            balls_to_save = 1

        if balls_to_save > self.machine.game.balls_in_play:
            balls_to_save = self.machine.game.balls_in_play

        if balls_to_save > self.saves_remaining and not self.unlimited_saves:
            balls_to_save = self.saves_remaining

        return balls_to_save

    def _reduce_remaining_saves_and_disable_if_zero(
            self, balls_to_save: int) -> None:
        if not self.unlimited_saves:
            self.saves_remaining -= balls_to_save
            self.debug_log("Saves remaining: %s", self.saves_remaining)
        else:
            self.debug_log("Unlimited saves remaining")

        if self.saves_remaining <= 0 and not self.unlimited_saves:
            self.debug_log("Disabling since there are no saves remaining")
            self.disable()

    def _ball_drain_while_active(self, balls: int, **kwargs) -> Optional[dict]:
        del kwargs
        if balls <= 0:
            return {}

        balls_to_save = self._get_number_of_balls_to_save(balls)

        self.debug_log(
            "Ball(s) drained while active. Requesting new one(s). "
            "Autolaunch: %s", self.config['auto_launch'])

        self.machine.events.post('ball_save_{}_saving_ball'.format(self.name),
                                 balls=balls_to_save,
                                 early_save=False)
        '''event: ball_save_(name)_saving_ball
        desc: The ball save called (name) has just saved one (or more) balls.

        args:
            balls: The number of balls this ball saver is saving.
            early_save: True if this is an early ball save.
        '''

        self._schedule_balls(balls_to_save)

        self._reduce_remaining_saves_and_disable_if_zero(balls_to_save)

        return {'balls': balls - balls_to_save}

    @event_handler(8)
    def early_ball_save(self, **kwargs) -> None:
        """Perform early ball save if enabled."""
        del kwargs
        if not self.enabled:
            return

        if not self._get_number_of_balls_to_save(1):
            return

        if self.early_saved > 0:
            self.debug_log(
                "Already performed an early ball save. Ball needs to drain first."
            )
            return

        self.machine.events.post('ball_save_{}_saving_ball'.format(self.name),
                                 balls=1,
                                 early_save=True)
        # doc block above

        self.debug_log("Performing early ball save.")
        self.early_saved += 1
        self._schedule_balls(1)
        self.machine.events.add_handler('ball_drain',
                                        self._early_ball_save_drain_handler,
                                        priority=1001)

        self._reduce_remaining_saves_and_disable_if_zero(1)

    def _early_ball_save_drain_handler(self, balls: int, **kwargs) -> dict:
        del kwargs
        if self.early_saved and balls > 0:
            balls -= 1
            self.early_saved -= 1
            self.debug_log("Early saved ball drained.")
            self.machine.events.remove_handler(
                self._early_ball_save_drain_handler)
            return {'balls': balls}
        else:
            return {}

    def _schedule_balls(self, balls_to_save: int) -> None:
        if self.config['eject_delay']:
            # schedule after delay. to add some drama
            self.delay.add(self.config['eject_delay'],
                           self._add_balls,
                           balls_to_save=balls_to_save)
        elif self.config['delayed_eject_events']:
            # unlimited delay. wait for event
            self._scheduled_balls += balls_to_save
        else:
            # default: no delay. just eject balls right now
            self._add_balls(balls_to_save)

    @event_handler(4)
    def delayed_eject(self, **kwargs):
        """Trigger eject of all scheduled balls."""
        del kwargs
        self._add_balls(self._scheduled_balls)
        self._scheduled_balls = 0

    def _add_balls(self, balls_to_save, **kwargs):
        del kwargs
        self.source_playfield.add_ball(
            balls=balls_to_save,
            player_controlled=self.config['auto_launch'] ^ 1)

    def device_removed_from_mode(self, mode: Mode) -> None:
        """Disable ball save when mode ends."""
        del mode
        self.debug_log("Removing...")

        self.disable()

        if self.config['delayed_eject_events']:
            self.debug_log("Triggering delayed eject because mode ended.")
            self.delayed_eject()
Esempio n. 2
0
class Stepper(SystemWideDevice):

    """Represents an stepper motor based axis in a pinball machine.

    Args: Same as the Device parent class.
    """

    config_section = 'steppers'
    collection = 'steppers'
    class_label = 'stepper'

    def __init__(self, machine, name):
        """Initialise stepper."""
        self.hw_stepper = None
        self.platform = None        # type: Stepper
        self._cachedPosition = 0    # in user units
        self._ball_search_started = False
        self._min_pos = 0
        self._max_pos = 1
        self.positionMode = False
        self._cachedVelocity = 0
        self._isHomed = False
        self._isMoving = False
        self._move_complete_pollrate = 100  # ms
        self._resetPosition = 0
        self._position = None
        self._max_velocity = None

        self.delay = DelayManager(machine.delayRegistry)
        super().__init__(machine, name)

    def _initialize(self):
        self.platform = self.machine.get_platform_sections('stepper_controllers', self.config['platform'])

        for position in self.config['named_positions']:
            self.machine.events.add_handler(self.config['named_positions'][position],
                                            self._position_event,
                                            position=position)

        self.hw_stepper = self.platform.configure_stepper(self.config['number'], self.config)
        self._position = self.config['reset_position']
        self._max_pos = self.config['pos_max']
        self._min_pos = self.config['pos_min']
        self._max_velocity = self.config['velocity_limit']
        self._resetPosition = self.config['reset_position']

        mode = self.config['mode']
        if mode == 'position':
            self.positionMode = True
        elif mode == 'velocity':
            self.positionMode = False
        else:
            raise AssertionError("Operating Mode not defined")

        if self.config['include_in_ball_search']:
            self.machine.events.add_handler("ball_search_started",
                                            self._ball_search_start)
            self.machine.events.add_handler("ball_search_stopped",
                                            self._ball_search_stop)

    def current_position(self):
        """Return position in user units (vs microsteps)."""
        return self.hw_stepper.current_position()

    def move_abs_pos(self, position):
        """Move servo to position."""
        if self._ball_search_started:
            return
        if not self.positionMode:
            raise RuntimeError("Cannot do a position move in velocity mode")
        if self._min_pos <= position <= self._max_pos:
            self.hw_stepper.move_abs_pos(position)
            if self._isMoving is False:     # already moving, don't re-kickoff polling
                self._isMoving = True
                self._schedule_move_complete_check()
        else:
            raise ValueError("move_abs: position argument beyond limits")

    def home(self):
        """Home an axis, resetting 0 position."""
        if self.positionMode:
            self.hw_stepper.home()
            self._isHomed = False
            if self._isMoving is False:     # already moving, don't re-kickoff polling
                self._isMoving = True
                self._schedule_home_complete_check()
        else:
            raise RuntimeError("Cannot home in velocity mode")

    def move_rel_pos(self, delta):
        """Move axis to a relative position."""
        start = self.current_position()
        self.move_abs_pos(start + delta)

    def move_vel_mode(self, velocity):
        """Move at a specific velocity indefinitely."""
        if self.positionMode:
            raise RuntimeError("Cannot do a velocity move in position mode")
        if velocity <= self._max_velocity:
            self.hw_stepper.move_vel_mode(velocity)
            self._cachedVelocity = velocity
        else:
            raise ValueError("move_vel_mode: velocity argument is above limit")

    def stop(self):
        """Stop motor."""
        self.hw_stepper.stop()
        self._isMoving = False
        self._cachedVelocity = 0.0
        self.delay.remove('stepper_move_complete_check')
        self.delay.remove('stepper_home_complete_check')

    def _schedule_move_complete_check(self):
        self.delay.add(name='stepper_move_complete_check',
                       ms=self._move_complete_pollrate,
                       callback=self._check_mv_complete)

    def _check_mv_complete(self):
        # TODO add timeout that stops this with error event if it hasn't made it in some amount of time
        if not self._isMoving:
            return
        if self.hw_stepper.is_move_complete():
            self._isMoving = False
            self._cachedPosition = self.current_position()
            self.machine.events.post('stepper_' + self.name + "_ready")
            '''event: stepper_(name)_ready'''
        else:
            # reschedule
            self._schedule_move_complete_check()

    def _schedule_home_complete_check(self):
        self.delay.add(name='stepper_home_complete_check',
                       ms=self._move_complete_pollrate,
                       callback=self._check_home_complete)

    def _check_home_complete(self):
        # TODO add timeout that stops this with error event if it hasn't made it in some amount of time
        if self._isHomed:
            return
        if self.hw_stepper.is_move_complete():
            self._isMoving = False
            self._isHomed = True
            self.machine.events.post('stepper_' + self.name + "_ready")
            '''event: stepper_(name)_ready'''
        else:
            # reschedule
            self._schedule_home_complete_check()

    @event_handler(1)
    def reset(self, **kwargs):
        """Stop Motor."""
        del kwargs
        self.stop()
        if self.positionMode:
            self.home()
            self.move_abs_pos(self._resetPosition)

    @event_handler(5)
    def _position_event(self, position, **kwargs):
        del kwargs
        self.move_abs_pos(position)

    def _ball_search_start(self, **kwargs):
        del kwargs
        # we do not touch self._position during ball search so we can reset to
        # it later
        self._ball_search_started = True
        self._ball_search_go_to_min()

    def _ball_search_go_to_min(self):
        self._move_abs_pos(self.config['ball_search_min'])
        self.delay.add(name="ball_search", callback=self._ball_search_go_to_max, ms=self.config['ball_search_wait'])

    def _ball_search_go_to_max(self):
        self._move_abs_pos(self.config['ball_search_max'])
        self.delay.add(name="ball_search", callback=self._ball_search_go_to_min, ms=self.config['ball_search_wait'])

    def _ball_search_stop(self, **kwargs):
        del kwargs
        # stop delay
        self.delay.remove("ball_search")
        self._ball_search_started = False

        # move to last commanded
        if self.positionMode:
            self.move_abs_pos(self._cachedPosition)
        else:
            self.move_vel_mode(self._cachedVelocity)
Esempio n. 3
0
class Diverter(SystemWideDevice):

    """Represents a diverter in a pinball machine.

    Args: Same as the Device parent class.
    """

    config_section = 'diverters'
    collection = 'diverters'
    class_label = 'diverter'

    def __init__(self, machine, name):
        """Initialise diverter."""
        super().__init__(machine, name)

        self.delay = DelayManager(machine.delayRegistry)

        # Attributes
        self.active = False
        self.enabled = False
        self.platform = None

        self.diverting_ejects_count = 0
        self.eject_state = False
        self.eject_attempt_queue = deque()

    def _initialize(self):
        # register for feeder device eject events
        for feeder_device in self.config['feeder_devices']:
            self.machine.events.add_handler(
                'balldevice_' + feeder_device.name +
                '_ball_eject_attempt',
                self._feeder_eject_attempt)

            self.machine.events.add_handler(
                'balldevice_' + feeder_device.name +
                '_ejecting_ball',
                self._feeder_ejecting)

            self.machine.events.add_handler(
                'balldevice_' + feeder_device.name +
                '_ball_eject_failed',
                self._feeder_eject_count_decrease)

            self.machine.events.add_handler(
                'balldevice_' + feeder_device.name +
                '_ball_eject_success',
                self._feeder_eject_count_decrease)

        self.machine.events.add_handler('init_phase_3',
                                        self._register_switches)

        self.platform = self.config['activation_coil'].platform

        if self.config['ball_search_order']:
            self.config['playfield'].ball_search.register(
                self.config['ball_search_order'], self._ball_search, self.name)

    def _register_switches(self, **kwargs):
        del kwargs
        # register for deactivation switches
        for switch in self.config['deactivation_switches']:
            self.machine.switch_controller.add_switch_handler(
                switch.name, self.deactivate)

        # register for disable switches:
        for switch in self.config['disable_switches']:
            self.machine.switch_controller.add_switch_handler(
                switch.name, self.disable)

    def reset(self, **kwargs):
        """Reset and deactivate the diverter."""
        del kwargs
        self.deactivate()

    def enable(self, auto=False, **kwargs):
        """Enable this diverter.

        Args:
            auto: Boolean value which is used to indicate whether this
                diverter enabled itself automatically. This is passed to the
                event which is posted.
            **kwargs: unused

        If an 'activation_switches' is configured, then this method writes a
        hardware autofire rule to the pinball controller which fires the
        diverter coil when the switch is activated.

        If no `activation_switches` is specified, then the diverter is activated
        immediately.
        """
        del kwargs
        self.enabled = True

        self.machine.events.post('diverter_' + self.name + '_enabling',
                                 auto=auto)
        '''event: diverter_(name)_enabling
        desc: The diverter called (name) is enabling itself. Note that if this
            diverter has ``activation_switches:`` configured, it will not
            physically activate until one of those switches is hit. Otherwise
            this diverter will activate immediately.

        args:
            auto: Boolean which indicates whether this diverter enabled itself
                automatically for the purpose of routing balls to their proper
                location(s).
        '''

        if self.config['activation_switches']:
            self._enable_switches()
        else:
            self.activate()

    def disable(self, auto=False, **kwargs):
        """Disable this diverter.

        This method will remove the hardware rule if this diverter is activated
        via a hardware switch.

        Args:
            auto: Boolean value which is used to indicate whether this
                diverter disabled itself automatically. This is passed to the
                event which is posted.
            **kwargs: This is here because this disable method is called by
                whatever event the game programmer specifies in their machine
                configuration file, so we don't know what event that might be
                or whether it has random kwargs attached to it.
        """
        del kwargs
        self.enabled = False

        self.machine.events.post('diverter_' + self.name + '_disabling',
                                 auto=auto)
        '''event: diverter_(name)_disabling
        desc: The diverter called (name) is disabling itself. Note that if this
            diverter has ``activation_switches:`` configured, it will not
            physically deactivate now, instead deactivating based on switch
            hits and timing. Otherwise this diverter will deactivate immediately.

        args:
            auto: Boolean which indicates whether this diverter disabled itself
                automatically for the purpose of routing balls to their proper
                location(s).
        '''

        self.debug_log("Disabling Diverter")
        if self.config['activation_switches']:
            self._disable_switches()
        # if there is no deactivation way
        if not (self.config['activation_time'] or self.config['deactivation_switches'] or
           self.config['deactivate_events']):
            self.deactivate()

    def activate(self, **kwargs):
        """Physically activate this diverter's coil."""
        del kwargs
        self.debug_log("Activating Diverter")
        self.active = True

        self.machine.events.post('diverter_' + self.name + '_activating')
        '''event: diverter_(name)_activating
        desc: The diverter called (name) is activating itself, which means
            it's physically pulsing or holding the coil to move.

        '''
        if self.config['type'] == 'pulse':
            self.config['activation_coil'].pulse()
        elif self.config['type'] == 'hold':
            self.config['activation_coil'].enable()
        self.schedule_deactivation()

    def deactivate(self, **kwargs):
        """Deactivate this diverter.

        This method will disable the activation_coil, and (optionally) if it's
        configured with a deactivation coil, it will pulse it.
        """
        del kwargs
        self.debug_log("Deactivating Diverter")
        self.active = False

        if self.config['activation_time']:
            self.delay.remove('deactivate_timed')

        self.machine.events.post('diverter_' + self.name + '_deactivating')
        '''event: diverter_(name)_deactivating
        desc: The diverter called (name) is deativating itself.

        '''
        self.config['activation_coil'].disable()

        if self.config['deactivation_coil']:
            self.config['deactivation_coil'].pulse()

    def schedule_deactivation(self):
        """Schedule a delay to deactivate this diverter."""
        if self.config['activation_time']:
            self.delay.add(name='deactivate_timed', ms=self.config['activation_time'],
                           callback=self.deactivate)

    def _enable_switches(self):
        """Register switch handler on activation switches."""
        self.debug_log("Enabling Diverter sw switches: %s",
                       self.config['activation_switches'])

        for switch in self.config['activation_switches']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=switch.name, callback=self.activate)

    def _disable_switches(self):
        """Deregister switch handlers for activation switches."""
        self.debug_log("Disabling Diverter sw switches: %s",
                       self.config['activation_switches'])

        for switch in self.config['activation_switches']:
            self.machine.switch_controller.remove_switch_handler(
                switch_name=switch.name, callback=self.activate)

    def _feeder_eject_count_decrease(self, target, **kwargs):
        del target
        del kwargs
        self.diverting_ejects_count -= 1
        if self.diverting_ejects_count <= 0:
            self.diverting_ejects_count = 0

            # If there are ejects waiting for the other target switch diverter
            if len(self.eject_attempt_queue) > 0:
                if not self.eject_state:
                    self.eject_state = True
                    self.debug_log(
                        "Enabling diverter since eject target is on the "
                        "active target list")
                    self.enable()
                elif self.eject_state:
                    self.eject_state = False
                    self.debug_log(
                        "Enabling diverter since eject target is on the "
                        "inactive target list")
                    self.disable()
                # And perform those ejects
                while len(self.eject_attempt_queue) > 0:
                    self.diverting_ejects_count += 1
                    queue = self.eject_attempt_queue.pop()
                    queue.clear()
            elif self.active and not self.config['activation_time']:
                # if diverter is active and no more ejects are ongoing
                self.deactivate()

    def _get_desired_state(self, target):
        desired_state = None
        if target in self.config['targets_when_active']:
            desired_state = True

        elif target in self.config['targets_when_inactive']:
            desired_state = False
        return desired_state

    def _feeder_eject_attempt(self, queue, target, **kwargs):
        # Event handler which is called when one of this diverter's feeder
        # devices attempts to eject a ball. This is what allows this diverter
        # to get itself in the right position to send the ball to where it needs
        # to go.

        # Since the 'target' kwarg is going to be an object, not a name, we need
        # to figure out if this object is one of the targets of this diverter.
        del kwargs
        self.debug_log("Feeder device eject attempt for target: %s", target)

        desired_state = self._get_desired_state(target)

        if desired_state is None:
            self.debug_log("Feeder device ejects to an unknown target: %s. "
                           "Ignoring!", target.name)
            return

        if self.diverting_ejects_count > 0 and self.eject_state != desired_state:
            self.debug_log("Feeder devices tries to eject to a target which "
                           "would require a state change. Postponing that "
                           "because we have an eject to the other side")
            queue.wait()
            self.eject_attempt_queue.append(queue)
            return

        self.diverting_ejects_count += 1
        self.eject_state = desired_state

    def _feeder_ejecting(self, target, **kwargs):
        """Enable or disable diverter on eject."""
        del kwargs
        self.debug_log("Feeder device is ejecting for target: %s", target)

        desired_state = self._get_desired_state(target)

        if desired_state is None:
            self.debug_log("Feeder device ejects to an unknown target: %s. "
                           "Ignoring!", target.name)
            return

        if desired_state:
            self.debug_log("Enabling diverter since eject target is on the "
                           "active target list")
            self.enable()
        elif not desired_state:
            self.debug_log("Enabling diverter since eject target is on the "
                           "inactive target list")
            self.disable()

    def _ball_search(self, phase, iteration):
        del phase
        del iteration
        self.activate()
        self.machine.delay.add(self.config['ball_search_hold_time'],
                               self.deactivate,
                               'diverter_{}_ball_search'.format(self.name))
        return True
Esempio n. 4
0
class LogicBlock(SystemWideDevice, ModeDevice):
    """Parent class for each of the logic block classes."""

    __slots__ = ["delay", "_state", "_start_enabled", "player_state_variable"]

    def __init__(self, machine: MachineController, name: str) -> None:
        """Initialize logic block."""
        super().__init__(machine, name)
        self.delay = DelayManager(self.machine)
        self._state = None  # type: Optional[LogicBlockState]
        self._start_enabled = None  # type: Optional[bool]

        self.player_state_variable = "{}_state".format(self.name)
        '''player_var: (logic_block)_state
        config_section: counters, accruals, sequences

        desc: A dictionary that stores the internal state of the logic block
        with the name (logic_block). (In other words, a logic block called
        *mode1_hit_counter* will store its state in a player variable called
        ``mode1_hit_counter_state``).

        The state that's stored in this variable include whether the logic
        block is enabled and whether it's complete.
        '''

    async def _initialize(self):
        await super()._initialize()
        if self.config['start_enabled'] is not None:
            self._start_enabled = self.config['start_enabled']
        else:
            self._start_enabled = not self.config['enable_events']

    def add_control_events_in_mode(self, mode: Mode) -> None:
        """Do not auto enable this device in modes."""

    def validate_and_parse_config(self,
                                  config: dict,
                                  is_mode_config: bool,
                                  debug_prefix: str = None) -> dict:
        """Validate logic block config."""
        del is_mode_config
        del debug_prefix
        if 'events_when_complete' not in config:
            config['events_when_complete'] = [
                'logicblock_' + self.name + '_complete'
            ]

        if 'events_when_hit' not in config:
            config['events_when_hit'] = ['logicblock_' + self.name + '_hit']

        self.machine.config_validator.validate_config(
            self.config_section, config, self.name,
            ("device", "logic_blocks_common"))

        self._configure_device_logging(config)
        return config

    @property
    def can_exist_outside_of_game(self) -> bool:
        """Return true if persist_state is not set."""
        return not bool(self.config['persist_state'])

    def get_start_value(self) -> Any:
        """Return the start value for this block."""
        raise NotImplementedError("implement")

    async def device_added_system_wide(self):
        """Initialise internal state."""
        self._state = LogicBlockState()
        self.value = self.get_start_value()
        await super().device_added_system_wide()
        if not self.config['enable_events']:
            self.enable()

        if self.config['persist_state']:
            self.raise_config_error(
                "Cannot set persist_state for system-wide logic_blocks", 1)

        self.post_update_event()

    def device_loaded_in_mode(self, mode: Mode, player: Player):
        """Restore internal state from player if persist_state is set or create new state."""
        super().device_loaded_in_mode(mode, player)
        if self.config['persist_state']:
            if not player.is_player_var(self.player_state_variable):
                player[self.player_state_variable] = LogicBlockState()
                # enable device ONLY when we create a new entry in the player
                if self._start_enabled:
                    mode.add_mode_event_handler(
                        MODE_STARTING_EVENT_TEMPLATE.format(mode.name),
                        self.event_enable,
                        priority=mode.priority + 1)

                self._state = player[self.player_state_variable]
                self.value = self.get_start_value()
            else:
                self._state = player[self.player_state_variable]
        else:
            self._state = LogicBlockState()
            self.value = self.get_start_value()
            if self._start_enabled:
                mode.add_mode_event_handler(
                    MODE_STARTING_EVENT_TEMPLATE.format(mode.name),
                    self.event_enable,
                    priority=mode.priority + 1)

        mode.add_mode_event_handler("mode_{}_starting".format(mode.name),
                                    self.post_update_event)

    def device_removed_from_mode(self, mode: Mode):
        """Unset internal state to prevent leakage."""
        super().device_removed_from_mode(mode)
        self._state = None

    @property
    def value(self):
        """Return value or None if that is currently not possible."""
        if self._state:
            return self._state.value

        return None

    @value.setter
    def value(self, value):
        """Set the value."""
        self._state.value = value

    @property
    def enabled(self):
        """Return if enabled."""
        return self._state and self._state.enabled

    @enabled.setter
    def enabled(self, value):
        """Set enable."""
        self._state.enabled = value

    @property
    def completed(self):
        """Return if completed."""
        return self._state and self._state.completed

    @completed.setter
    def completed(self, value):
        """Set if completed."""
        self._state.completed = value

    def post_update_event(self, **kwargs):
        """Post an event to notify about changes."""
        del kwargs
        value = self._state.value
        enabled = self._state.enabled
        self.machine.events.post("logicblock_{}_updated".format(self.name),
                                 value=value,
                                 enabled=enabled)
        '''event: logicblock_(name)_updated
        config_section: counters, accruals, sequences

        desc: The logic block called "name" has changed.

        This might happen when the block advanced, it was resetted or restored.

        args:
        value: The current value of this block.
        enabled: Whatever this block is enabled or not.
        '''

    def enable(self):
        """Enable this logic block.

        Automatically called when one of the
        enable_event events is posted. Can also manually be called.
        """
        super().enable()
        self.debug_log("Enabling")
        self.enabled = True
        self.post_update_event()
        self._logic_block_timer_start()

    def _post_hit_events(self, **kwargs):
        self.post_update_event()
        for event in self.config['events_when_hit']:
            self.machine.events.post(event, **kwargs)
            '''event: logicblock_(name)_hit
            config_section: counters, accruals, sequences

            desc: The logic block "name" was just hit.

            Note that this is the default hit event for logic blocks,
            but this can be changed in a logic block's "events_when_hit:"
            setting, so this might not be the actual event that's posted for
            all logic blocks in your machine.

            args: depend on the type
            '''

    @event_handler(0)
    def event_disable(self, **kwargs):
        """Event handler for disable event."""
        del kwargs
        self.disable()

    def disable(self):
        """Disable this logic block.

        Automatically called when one of the
        disable_event events is posted. Can also manually be called.
        """
        self.debug_log("Disabling")
        self.enabled = False
        self.post_update_event()
        self.delay.remove("timeout")

    @event_handler(4)
    def event_reset(self, **kwargs):
        """Event handler for reset event."""
        del kwargs
        self.reset()

    def reset(self):
        """Reset the progress towards completion of this logic block.

        Automatically called when one of the reset_event events is called.
        Can also be manually called.
        """
        self.completed = False
        self.value = self.get_start_value()
        self.debug_log("Resetting")
        self.post_update_event()
        self._logic_block_timer_start()

    def _logic_block_timer_start(self):
        if self.config['logic_block_timeout']:
            self.debug_log("Setting up a logic block timer for %sms",
                           self.config['logic_block_timeout'])

            self.delay.reset(name="timeout",
                             ms=self.config['logic_block_timeout'],
                             callback=self._logic_block_timeout)

    def _logic_block_timeout(self):
        """Reset the progress towards completion of this logic block when timer expires.

        Automatically called when one of the logic_block_timer_complete
        events is called.
        """
        self.info_log("Logic Block timeouted")
        self.machine.events.post("{}_timeout".format(self.name))
        '''event: (name)_timeout
        config_section: counters, accruals, sequences

        desc: The logic block called "name" has just timeouted.

        Timeouts are disabled by default but you can set logic_block_timeout to
        enable them. They will run from start of your logic block until it is
        stopped.
        '''
        self.reset()

    @event_handler(5)
    def event_restart(self, **kwargs):
        """Event handler for restart event."""
        del kwargs
        self.restart()

    def restart(self):
        """Restart this logic block by calling reset() and enable().

        Automatically called when one of the restart_event events is called.
        Can also be manually called.
        """
        self.debug_log("Restarting (resetting then enabling)")
        self.reset()
        self.enable()

    def complete(self):
        """Mark this logic block as complete.

        Posts the 'events_when_complete'
        events and optionally restarts this logic block or disables it,
        depending on this block's configuration settings.
        """
        # if already completed do not complete again
        if self.completed:
            return

        # otherwise mark as completed
        self.completed = True
        self.delay.remove("timeout")

        self.debug_log("Complete")
        if self.config['events_when_complete']:
            for event in self.config['events_when_complete']:
                self.machine.events.post(event)
        '''event: logicblock_(name)_complete
        config_section: counters, accruals, sequences

        desc: The logic block called "name" has just been completed.

        Note that this is the default completion event for logic blocks, but
        this can be changed in a logic block's "events_when_complete:" setting,
        so this might not be the actual event that's posted for all logic
        blocks in your machine.
        '''

        # call reset to reset completion
        if self.config['reset_on_complete']:
            self.reset()

        # disable block
        if self.config['disable_on_complete']:
            self.disable()
Esempio n. 5
0
class BallSearch(object):
    """Ball search controller."""
    def __init__(self, machine, playfield):
        """Initialise ball search."""
        self.machine = machine
        self.playfield = playfield
        self.log = logging.getLogger("BallSearch " + playfield.name)
        self.delay = DelayManager(self.machine.delayRegistry)

        self.started = False
        self.enabled = False
        self.callbacks = []

        self.iteration = False
        self.iterator = False
        self.phase = False

        # register for events
        self.machine.events.add_handler('request_to_start_game',
                                        self.request_to_start_game)

        self.machine.events.add_handler('cancel_ball_search',
                                        self.cancel_ball_search)
        '''event: cancel_ball_search
        desc: This event will cancel all running ball searches and mark the balls as lost. This is only a handler
        so all you have to do is to post the event.'''

    def request_to_start_game(self, **kwargs):
        """Method registered for the *request_to_start_game* event.

        Returns false if the ball search is running.
        """
        del kwargs
        if self.started:
            return False
        else:
            return

    def register(self, priority, callback):
        """Register a callback for sequential ballsearch.

        Callbacks are called by priority. Ball search only waits if the callback returns true.

        Args:
            priority: priority of this callback in the ball search procedure
            callback: callback to call. ball search will wait before the next callback, if it returns true
        """
        self.callbacks.append((priority, callback))
        # sort by priority
        self.callbacks = sorted(self.callbacks, key=lambda entry: entry[0])

    def enable(self):
        """Enable but do not start ball search.

        Ball search is started by a timeout. Enable also resets that timer.
        """
        if self.playfield.config['enable_ball_search'] is False or (
                not self.playfield.config['enable_ball_search']
                and not self.machine.config['mpf']['default_ball_search']):
            return

        if not self.callbacks:
            raise AssertionError("No callbacks registered")

        self.log.debug("Enabling Ball Search")

        self.enabled = True
        self.reset_timer()

    def disable(self):
        """Disable ball search.

        Will stop the ball search if it is running.
        """
        if self.started:
            self.machine.events.post('ball_search_stopped')
        '''event: ball_search_stopped

        desc: The ball search process has been disabled. This event is posted
            any time ball search stops, regardless of whether it found a ball
            or gave up. (If the ball search failed to find the ball, it will
            also post the *ball_search_failed* event.)
        '''

        self.started = False
        self.enabled = False
        self.delay.remove('start')
        self.delay.remove('run')

    def reset_timer(self):
        """Reset the start timer.

        Called by playfield.
        """
        if self.enabled and not self.started:
            self.delay.reset(name='start',
                             callback=self.start,
                             ms=self.playfield.config['ball_search_timeout'])

    def start(self):
        """Actually start ball search."""
        if not self.enabled or self.started or not self.callbacks:
            return
        self.started = True
        self.iteration = 1
        self.phase = 1
        self.iterator = iter(self.callbacks)
        self.log.debug("Starting ball search")
        self.machine.events.post('ball_search_started')
        '''event: ball_search_started

        desc: The ball search process has been begun.
        '''
        self.run()

    def run(self):
        """Run one iteration of the ball search.

        Will schedule itself for the next run.
        """
        timeout = self.playfield.config['ball_search_interval']
        # iterate until we are done with all callbacks
        while True:
            try:
                element = next(self.iterator)
            except StopIteration:
                self.iteration += 1
                # give up at some point
                if self.iteration > self.playfield.config[
                        'ball_search_phase_' + str(self.phase) + '_searches']:
                    self.phase += 1
                    self.iteration = 1
                    if self.phase > 3:
                        self.give_up()
                        return

                self.log.debug("Ball Search Phase %s Iteratio %s", self.phase,
                               self.iteration)
                self.iterator = iter(self.callbacks)
                element = next(self.iterator)
                timeout = self.playfield.config[
                    'ball_search_wait_after_iteration']

            (dummy_priority, callback) = element
            # if a callback returns True we wait for the next one
            if callback(self.phase, self.iteration):
                self.delay.add(name='run', callback=self.run, ms=timeout)
                return

    def cancel_ball_search(self, **kwargs):
        """Cancel the current ballsearch and mark the ball as missing."""
        del kwargs
        if self.started:
            self.give_up()

    def give_up(self):
        """Give up the ball search.

        Did not find the missing ball. Execute the failed action which either adds a replacement ball or ends the game.
        """
        self.log.warning("Ball Search failed to find ball. Giving up!")
        self.disable()
        self.machine.events.post('ball_search_failed')
        '''event: ball_search_failed

        desc: The ball search process has failed to locate a missing or stuck
            ball and has given up. This event will be posted immediately after
            the *ball_search_stopped* event.
        '''

        lost_balls = self.playfield.balls
        self.machine.ball_controller.num_balls_known -= lost_balls
        self.playfield.balls = 0
        self.playfield.available_balls = 0

        self._compensate_lost_balls(lost_balls)

    def _compensate_lost_balls(self, lost_balls):
        if not self.machine.game:
            return

        if self.playfield.config['ball_search_failed_action'] == "new_ball":
            if self.machine.ball_controller.num_balls_known > 0:
                # we have at least one ball remaining
                self.log.debug("Adding %s replacement ball", lost_balls)
                for dummy_iterator in range(lost_balls):
                    self.playfield.add_ball()
            else:
                self.log.debug("No more balls left. Ending game!")
                self.machine.game.game_ending()

        elif self.playfield.config['ball_search_failed_action'] == "end_game":
            if self.machine.game:
                self.log.debug("Ending the game")
                self.machine.game.game_ending()
            else:
                self.log.warning("There is no game. Doing nothing!")
        else:
            raise AssertionError(
                "Unknown action " +
                self.playfield.config['ball_search_failed_action'])
Esempio n. 6
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"
    ]

    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)

    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)
                ]
        for state in ["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']:
            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()
        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')
        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):
        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')
        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):
        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:: switches_1

            Only switches_1 is active. max_offset_time has passed and this hit
            cannot become both later on. Only emmited when ``max_offset_time:``
            is defined.

            ..rubric:: switches_2

            Only switches_2 is active. max_offset_time has passed and this hit
            cannot become both later on. Only emmited when ``max_offset_time:``
            is defined.

            ..rubric:: inactive

            Both switches are inactive.

            '''
            '''event: flipper_cancel
Esempio n. 7
0
class DropTargetBank(SystemWideDevice, ModeDevice):

    """A bank of drop targets in a pinball machine by grouping together multiple `DropTarget` class devices."""

    config_section = 'drop_target_banks'
    collection = 'drop_target_banks'
    class_label = 'drop_target_bank'

    def __init__(self, machine: "MachineController", name: str) -> None:
        """Initialise drop target bank."""
        super().__init__(machine, name)

        self.drop_targets = list()          # type: List[DropTarget]
        self.reset_coil = None              # type: Optional[Driver]
        self.reset_coils = set()            # type: Set[Driver]
        self.complete = False
        self.down = 0
        self.up = 0
        self.delay = DelayManager(machine)
        self._ignore_switch_hits = False

    @property
    def can_exist_outside_of_game(self):
        """Return true if this device can exist outside of a game."""
        return True

    async def _initialize(self):
        await super()._initialize()
        self.drop_targets = self.config['drop_targets']
        self.reset_coil = self.config['reset_coil']
        self.reset_coils = self.config['reset_coils']

    def device_loaded_in_mode(self, mode: Mode, player: Player):
        """Add targets."""
        self._add_targets_to_bank()

    async def device_added_system_wide(self):
        """Add targets."""
        await super().device_added_system_wide()
        self._add_targets_to_bank()

    def _add_targets_to_bank(self):
        """Add targets to bank."""
        for target in self.drop_targets:
            target.add_to_bank(self)

        self.member_target_change()

        self.debug_log('Drop Targets: %s', self.drop_targets)

    @event_handler(5)
    def event_reset(self, **kwargs):
        """Handle reset control event."""
        del kwargs
        self.reset()

    def reset(self):
        """Reset this bank of drop targets.

        This method has some intelligence to figure out what coil(s) it should
        fire. It builds up a set by looking at its own reset_coil and
        reset_coils settings, and also scanning through all the member drop
        targets and collecting their coils. Then it pulses each of them. (This
        coil list is a "set" which means it only sends a single pulse to each
        coil, even if each drop target is configured with its own coil.)
        """
        self.debug_log('Resetting')

        if self.down == 0:
            self.info_log('All targets are already up. Will not reset bank.')
            return

        self.info_log('%s targets are down. Will reset those.', self.down)

        # figure out all the coils we need to pulse
        coils = set()       # type: Set[Driver]

        for drop_target in self.drop_targets:
            # add all reset coil for targets which are down
            if drop_target.reset_coil and drop_target.complete:
                coils.add(drop_target.reset_coil)

            # tell the drop target that we are going to reset it physically
            drop_target.external_reset_from_bank()

        for coil in self.reset_coils:
            coils.add(coil)

        if self.reset_coil:
            coils.add(self.reset_coil)

        if self.config['ignore_switch_ms']:
            self._ignore_switch_hits = True
            self.delay.add(ms=self.config['ignore_switch_ms'],
                           callback=self._restore_switch_hits,
                           name='ignore_hits')

        # now pulse them
        for coil in coils:
            self.debug_log('Pulsing reset coils: %s', coils)
            coil.pulse(max_wait_ms=self.config['reset_coil_max_wait_ms'])

    def _restore_switch_hits(self):
        self.machine.events.post('restore')
        self._ignore_switch_hits = False
        self.member_target_change()

    def member_target_change(self):
        """Handle that a member drop target has changed state.

        This method causes this group to update its down and up counts and
        complete status.
        """
        if self._ignore_switch_hits:
            return

        self.down = 0
        self.up = 0

        for target in self.drop_targets:
            if target.complete:
                self.down += 1
            else:
                self.up += 1

        self.debug_log(
            'Member drop target status change: Up: %s, Down: %s,'
            ' Total: %s', self.up, self.down,
            len(self.drop_targets))

        if self.down == len(self.drop_targets):
            self._bank_down()
        elif not self.down:
            self._bank_up()
        else:
            self._bank_mixed()

    def _bank_down(self):
        self.complete = True
        self.debug_log('All targets are down')

        if self.config['reset_on_complete']:
            self.debug_log("Reset on complete after %s", self.config['reset_on_complete'])
            self.delay.add(self.config['reset_on_complete'], self.reset)

        self.machine.events.post('drop_target_bank_' + self.name + '_down')
        '''event: drop_target_bank_(name)_down
        desc: Every drop target in the drop target bank called
        (name) is now in the "down" state. This event is
        only posted once, when all the drop targets are down.'''

    def _bank_up(self):
        self.complete = False
        self.debug_log('All targets are up')
        self.machine.events.post('drop_target_bank_' + self.name + '_up')
        '''event: drop_target_bank_(name)_up
        desc: Every drop target in the drop target bank called
        (name) is now in the "up" state. This event is
        only posted once, when all the drop targets are up.'''

    def _bank_mixed(self):
        self.complete = False
        self.machine.events.post('drop_target_bank_' + self.name + '_mixed',
                                 down=self.down)
        '''event: drop_target_bank_(name)_mixed
        desc: The drop targets in the drop target bank
        (name) are in a "mixed" state, meaning that they're
        not all down or not all up. This event is posted every time a member
        drop target changes but the overall bank is not not complete.'''

    def device_removed_from_mode(self, mode):
        """Remove targets which were added in this mode."""
        self.delay.remove('ignore_hits')

        for target in self.drop_targets:
            target.remove_from_bank(self)
Esempio n. 8
0
class AutofireCoil(SystemWideDevice):
    """Autofire coils which fire based on switch hits with a hardware rule.

    Coils in the pinball machine which should fire automatically based on
    switch hits using defined hardware switch rules.

    Autofire coils work with rules written to the hardware pinball controller
    that allow them to respond "instantly" to switch hits versus waiting for
    the lag of USB and the host computer.

    Examples of Autofire Coils are pop bumpers, slingshots, and kicking
    targets. (Flippers use the same autofire rules under the hood, but flipper
    devices have their own device type in MPF.

    """

    config_section = 'autofire_coils'
    collection = 'autofires'
    class_label = 'autofire'

    __slots__ = [
        "_enabled", "_rule", "delay", "_ball_search_in_progress",
        "_timeout_watch_time", "_timeout_max_hits", "_timeout_disable_time",
        "_timeout_hits"
    ]

    def __init__(self, machine: "MachineController", name: str) -> None:
        """Initialise autofire."""
        self._enabled = False
        self._rule = None  # type: HardwareRule
        super().__init__(machine, name)
        self.delay = DelayManager(self.machine)
        self._ball_search_in_progress = False
        self._timeout_watch_time = None
        self._timeout_max_hits = None
        self._timeout_disable_time = None
        self._timeout_hits = []  # type: List[float]

    @asyncio.coroutine
    def _initialize(self):
        yield from super()._initialize()
        if self.config['ball_search_order']:
            self.config['playfield'].ball_search.register(
                self.config['ball_search_order'], self._ball_search, self.name)
        # pulse is handled via rule but add a handler so that we take notice anyway
        self.config['switch'].add_handler(self._hit)
        if self.config['timeout_watch_time']:
            self._timeout_watch_time = self.config['timeout_watch_time'] / 1000
            self._timeout_max_hits = self.config['timeout_max_hits']
            self._timeout_disable_time = self.config['timeout_disable_time']

        if '{}_active'.format(
                self.config['playfield'].name) in self.config['switch'].tags:
            self.raise_config_error(
                "Autofire device '{}' uses switch '{}' which has a "
                "'{}_active' tag. This is handled internally by the device. Remove the "
                "redundant '{}_active' tag from that switch.".format(
                    self.name, self.config['switch'].name,
                    self.config['playfield'].name,
                    self.config['playfield'].name), 1)

    @event_handler(1)
    def event_enable(self, **kwargs):
        """Handle enable control event.

        To prevent multiple rules at the same time we prioritize disable > enable.
        """
        del kwargs
        self.enable()

    def enable(self):
        """Enable the autofire device.

        This causes the coil to respond to the switch hits. This is typically
        called when a ball starts to enable the slingshots, pops, etc.

        Note that there are several options for both the coil and the switch
        which can be incorporated into this rule, including recycle times,
        switch debounce, reversing the switch (fire the coil when the switch
        goes inactive), etc. These rules vary by hardware platform. See the
        user documentation for the hardware platform for details.

        Args:
            **kwargs: Not used, just included so this method can be used as an
                event callback.

        """
        if self._enabled:
            return
        self._enabled = True

        self.debug_log("Enabling")

        recycle = True if self.config['coil_overwrite'].get(
            'recycle', None) in (True, None) else False
        debounce = False if self.config['switch_overwrite'].get(
            'debounce', None) in (None, "quick") else True

        self._rule = self.machine.platform_controller.set_pulse_on_hit_rule(
            SwitchRuleSettings(switch=self.config['switch'],
                               debounce=debounce,
                               invert=self.config['reverse_switch']),
            DriverRuleSettings(driver=self.config['coil'], recycle=recycle),
            PulseRuleSettings(
                duration=self.config['coil_overwrite'].get('pulse_ms', None),
                power=self.config['coil_overwrite'].get('pulse_power', None)))

    @event_handler(10)
    def event_disable(self, **kwargs):
        """Handle disable control event.

        To prevent multiple rules at the same time we prioritize disable > enable.
        """
        del kwargs
        self.disable()

    def disable(self):
        """Disable the autofire device.

        This is typically called at the end of a ball and when a tilt event
        happens.

        Args:
            **kwargs: Not used, just included so this method can be used as an
                event callback.

        """
        self.delay.remove("_timeout_enable_delay")

        if not self._enabled:
            return
        self._enabled = False

        self.debug_log("Disabling")
        self.machine.platform_controller.clear_hw_rule(self._rule)

    def _hit(self):
        """Rule was triggered."""
        if not self._enabled:
            return
        if not self._ball_search_in_progress:
            self.config['playfield'].mark_playfield_active_from_device_action()
        if self._timeout_watch_time:
            current_time = self.machine.clock.get_time()
            self._timeout_hits = [
                t for t in self._timeout_hits
                if t > current_time - self._timeout_watch_time / 1000.0
            ]
            self._timeout_hits.append(current_time)

            if len(self._timeout_hits) >= self._timeout_max_hits:
                self.disable()
                self.delay.add(self._timeout_disable_time, self.enable,
                               "_timeout_enable_delay")

    def _ball_search(self, phase, iteration):
        del phase
        del iteration
        self.delay.reset(ms=200,
                         callback=self._ball_search_ignore_done,
                         name="ball_search_ignore_done")
        self._ball_search_in_progress = True
        self.config['coil'].pulse()
        return True

    def _ball_search_ignore_done(self):
        """We no longer expect any fake hits."""
        self._ball_search_in_progress = False
Esempio n. 9
0
class DigitalOutput(SystemWideDevice):
    """A digital output on either a light or driver platform."""

    config_section = 'digital_outputs'
    collection = 'digital_outputs'
    class_label = 'digital_output'

    __slots__ = ["hw_driver", "platform", "type", "__dict__"]

    def __init__(self, machine: MachineController, name: str) -> None:
        """Initialise digital output."""
        self.hw_driver = None  # type: Union[DriverPlatformInterface, LightPlatformInterface]
        self.platform = None  # type: Union[DriverPlatform, LightsPlatform]
        self.type = None  # type: str
        super().__init__(machine, name)
        self.delay = DelayManager(self.machine)

    @asyncio.coroutine
    def _initialize(self):
        """Initialise the hardware driver for this digital output."""
        yield from super()._initialize()
        if self.config['type'] == "driver":
            self._initialize_driver()
        elif self.config['type'] == "light":
            self._initialize_light()
        else:
            raise AssertionError("Invalid type {}".format(self.config['type']))

    def _initialize_light(self):
        """Configure a light as digital output."""
        self.platform = self.machine.get_platform_sections(
            'lights', self.config['platform'])
        self.type = "light"

        try:
            self.hw_driver = self.platform.configure_light(
                self.config['number'], self.config['light_subtype'], {})
        except AssertionError as e:
            raise AssertionError(
                "Failed to configure light {} in platform. See error above".
                format(self.name)) from e

    def _initialize_driver(self):
        """Configure a driver as digital output."""
        self.platform = self.machine.get_platform_sections(
            'coils', self.config['platform'])
        self.type = "driver"

        config = DriverConfig(default_pulse_ms=255,
                              default_pulse_power=1.0,
                              default_hold_power=1.0,
                              default_recycle=False,
                              max_pulse_ms=255,
                              max_pulse_power=1.0,
                              max_hold_power=1.0)

        try:
            self.hw_driver = self.platform.configure_driver(
                config, self.config['number'], {})
        except AssertionError as e:
            raise AssertionError(
                "Failed to configure driver {} in platform. See error above".
                format(self.name)) from e

    @staticmethod
    def _get_state(max_fade_ms: int, state: bool) -> Tuple[float, int, bool]:
        """Return the current state without any fade."""
        del max_fade_ms
        if state:
            return 1.0, -1, True
        else:
            return 0.0, -1, True

    @event_handler(3)
    def event_pulse(self, pulse_ms, **kwargs):
        """Handle pulse control event."""
        del kwargs
        self.pulse(pulse_ms)

    def pulse(self, pulse_ms):
        """Pulse digital output."""
        if self.type == "driver":
            self.hw_driver.pulse(PulseSettings(power=1.0, duration=pulse_ms))
        elif self.type == "light":
            self.hw_driver.set_fade(partial(self._get_state, state=True))
            self.platform.light_sync()
            self.delay.reset(name='timed_disable',
                             ms=pulse_ms,
                             callback=self.disable)
        else:
            raise AssertionError("Invalid type {}".format(self.type))

    @event_handler(2)
    def event_enable(self, **kwargs):
        """Handle enable control event."""
        del kwargs
        self.enable()

    def enable(self):
        """Enable digital output."""
        if self.type == "driver":
            self.hw_driver.enable(PulseSettings(power=1.0, duration=0),
                                  HoldSettings(power=1.0))
        elif self.type == "light":
            self.hw_driver.set_fade(partial(self._get_state, state=True))
            self.platform.light_sync()
            self.delay.remove(name='timed_disable')
        else:
            raise AssertionError("Invalid type {}".format(self.type))

    @event_handler(1)
    def event_disable(self, **kwargs):
        """Handle disable control event."""
        del kwargs
        self.disable()

    def disable(self):
        """Disable digital output."""
        if self.type == "driver":
            self.hw_driver.disable()
        elif self.type == "light":
            self.hw_driver.set_fade(partial(self._get_state, state=False))
            self.platform.light_sync()
            self.delay.remove(name='timed_disable')
        else:
            raise AssertionError("Invalid type {}".format(self.type))
Esempio n. 10
0
class Servo(SystemWideDevice):
    """Represents a servo in a pinball machine.

    Args: Same as the Device parent class.
    """

    config_section = 'servos'
    collection = 'servos'
    class_label = 'servo'

    def __init__(self, machine, name):
        """Initialise servo."""
        self.hw_servo = None
        self.platform = None  # type: ServoPlatform
        self._position = None
        self.speed_limit = None
        self.acceleration_limit = None
        self._ball_search_started = False
        self.delay = DelayManager(machine)
        super().__init__(machine, name)

    async def _initialize(self):
        await super()._initialize()
        self.platform = self.machine.get_platform_sections(
            'servo_controllers', self.config['platform'])

        for position in self.config['positions']:
            self.machine.events.add_handler(self.config['positions'][position],
                                            self._position_event,
                                            position=position)

        if not self.platform.features[
                'allow_empty_numbers'] and self.config['number'] is None:
            self.raise_config_error("Servo must have a number.", 1)

        self.hw_servo = await self.platform.configure_servo(
            self.config['number'])
        self._position = self.config['reset_position']
        self.speed_limit = self.config['speed_limit']
        self.acceleration_limit = self.config['acceleration_limit']

        if self.config['include_in_ball_search']:
            self.machine.events.add_handler("ball_search_started",
                                            self._ball_search_start)
            self.machine.events.add_handler("ball_search_stopped",
                                            self._ball_search_stop)

        self.set_speed_limit(self.speed_limit)
        self.set_acceleration_limit(self.acceleration_limit)

    @event_handler(1)
    def event_reset(self, **kwargs):
        """Event handler for reset event."""
        del kwargs
        self.reset()

    def reset(self):
        """Go to reset position."""
        self.go_to_position(self.config['reset_position'])

    @event_handler(5)
    def _position_event(self, position, **kwargs):
        del kwargs
        self.go_to_position(position)

    def go_to_position(self, position):
        """Move servo to position."""
        self._position = position
        if self._ball_search_started:
            return
        self._go_to_position(position)

    def _go_to_position(self, position):
        # linearly interpolate between servo limits
        corrected_position = self.config['servo_min'] + position * (
            self.config['servo_max'] - self.config['servo_min'])

        self.debug_log("Moving to position %s (corrected: %s)", position,
                       corrected_position)

        # call platform with calculated position
        self.hw_servo.go_to_position(corrected_position)

    def set_speed_limit(self, speed_limit):
        """Set speed parameter."""
        self.hw_servo.set_speed_limit(speed_limit)

    def set_acceleration_limit(self, acceleration_limit):
        """Set acceleration parameter."""
        self.hw_servo.set_acceleration_limit(acceleration_limit)

    def _ball_search_start(self, **kwargs):
        del kwargs
        # we do not touch self._position during ball search so we can reset to
        # it later
        self._ball_search_started = True
        self._ball_search_go_to_min()

    def _ball_search_go_to_min(self):
        self._go_to_position(self.config['ball_search_min'])
        self.delay.add(name="ball_search",
                       callback=self._ball_search_go_to_max,
                       ms=self.config['ball_search_wait'])

    def _ball_search_go_to_max(self):
        self._go_to_position(self.config['ball_search_max'])
        self.delay.add(name="ball_search",
                       callback=self._ball_search_go_to_min,
                       ms=self.config['ball_search_wait'])

    def _ball_search_stop(self, **kwargs):
        del kwargs
        # stop delay
        self.delay.remove("ball_search")
        self._ball_search_started = False

        # move to last position set
        self._go_to_position(self._position)
Esempio n. 11
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__", "_pulse_ms"]

    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]
        self._pulse_ms = None

    @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

    def _calculate_pulse_ms_placeholder(self, *args):
        del args
        if self.config['default_pulse_ms'] is not None:
            self._pulse_ms, future = self.config[
                'default_pulse_ms'].evaluate_and_subscribe({})
            future.add_done_callback(self._calculate_pulse_ms_placeholder)
        else:
            self._pulse_ms = self.machine.config['mpf']['default_pulse_ms']

    async def _initialize(self):
        await super()._initialize()
        self.platform = self.machine.get_platform_sections(
            'coils', self.config['platform'])

        self._calculate_pulse_ms_placeholder()

        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:
            pulse_ms = self._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,
               max_wait_ms: int = 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.
            max_wait_ms: Maximum time this pulse may be delayed for PSU optimization.

        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)
        wait_ms = self._notify_psu_and_get_wait_ms(pulse_ms, max_wait_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")

        if wait_ms > 0:
            self.debug_log(
                "Delaying enable by %sms pulse_ms: %sms (%s pulse_power %s hold_power)",
                wait_ms, pulse_ms, pulse_power, hold_power)
            self.delay.add(wait_ms,
                           self._enable_now,
                           pulse_ms=pulse_ms,
                           pulse_power=pulse_power,
                           hold_power=hold_power)
        else:
            self._enable_now(pulse_ms, pulse_power, hold_power)

    def _enable_now(self,
                    pulse_ms: int = None,
                    pulse_power: float = None,
                    hold_power: float = None):
        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 _notify_psu_and_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._notify_psu_and_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
Esempio n. 12
0
class Stepper(SystemWideDevice):

    """Represents an stepper motor based axis in a pinball machine.

    Args: Same as the Device parent class.
    """

    config_section = 'steppers'
    collection = 'steppers'
    class_label = 'stepper'

    __slots__ = ["hw_stepper", "platform", "_target_position", "_current_position", "_ball_search_started",
                 "_ball_search_old_target", "_is_homed", "_is_moving", "_move_task", "delay"]

    def __init__(self, machine, name):
        """Initialise stepper."""
        self.hw_stepper = None          # type: Optional[StepperPlatformInterface]
        self.platform = None            # type: Optional[Stepper]
        self._target_position = 0       # in user units
        self._current_position = 0      # in user units
        self._ball_search_started = False
        self._ball_search_old_target = 0
        self._is_homed = False
        self._is_moving = asyncio.Event(loop=machine.clock.loop)
        self._move_task = None          # type: Optional[asyncio.Task]
        self.delay = DelayManager(machine)
        super().__init__(machine, name)

    async def _initialize(self):
        await super()._initialize()
        self.platform = self.machine.get_platform_sections('stepper_controllers', self.config['platform'])

        # first target is the reset position but we might get an early target during startup via events
        self._target_position = self.config['reset_position']

        for position in self.config['named_positions']:
            self.machine.events.add_handler(self.config['named_positions'][position],
                                            self.event_move_to_position,
                                            position=position)

        if not self.platform.features['allow_empty_numbers'] and self.config['number'] is None:
            self.raise_config_error("Stepper must have a number.", 2)

        self.hw_stepper = await self.platform.configure_stepper(self.config['number'],
                                                                self.config['platform_settings'])

        if self.config['include_in_ball_search']:
            self.machine.events.add_handler("ball_search_started",
                                            self._ball_search_start)
            self.machine.events.add_handler("ball_search_stopped",
                                            self._ball_search_stop)

        if self.config['homing_mode'] == "switch" and not self.config['homing_switch']:
            self.raise_config_error("Cannot use homing_mode switch without a homing_switch. Please add homing_switch or"
                                    " use homing_mode hardware.", 1)

        self._move_task = self.machine.clock.loop.create_task(self._run())
        self._move_task.add_done_callback(self._done)

    @staticmethod
    def _done(future):
        try:
            future.result()
        except asyncio.CancelledError:
            pass

    def validate_and_parse_config(self, config, is_mode_config, debug_prefix: str = None):
        """Validate stepper config."""
        config = super().validate_and_parse_config(config, is_mode_config, debug_prefix)
        platform = self.machine.get_platform_sections(
            'stepper_controllers', getattr(config, "platform", None))
        config['platform_settings'] = platform.validate_stepper_section(
            self, config.get('platform_settings', None))
        self._configure_device_logging(config)
        return config

    async def _run(self):
        # wait for switches to be initialised
        await self.machine.events.wait_for_event("init_phase_3")

        # first home the stepper
        self.debug_log("Homing stepper")
        await self._home()

        # run the loop at least once
        self._is_moving.set()

        while True:
            # wait until we should be moving
            await self._is_moving.wait()
            self._is_moving.clear()
            # store target position in local variable since it may change in the meantime
            target_position = self._target_position
            delta = target_position - self._current_position
            if delta != 0:
                self.debug_log("Got move command. Current position: %s Target position: %s Delta: %s",
                               self._current_position, target_position, delta)
                # move stepper
                self.hw_stepper.move_rel_pos(delta)
                # wait for the move to complete
                await self.hw_stepper.wait_for_move_completed()
            else:
                self.debug_log("Got move command. Stepper already at target. Not moving.")
            # set current position
            self._current_position = target_position
            # post ready event
            self._post_ready_event()
            self.debug_log("Move completed")

    def _move_to_absolute_position(self, position):
        """Move servo to position."""
        self.debug_log("Moving to position %s", position)
        if self.config['pos_min'] <= position <= self.config['pos_max']:
            self._target_position = position
            self._is_moving.set()
        else:
            raise ValueError("_move_to_absolute_position: position argument beyond limits")

    async def _home(self):
        """Home an axis, resetting 0 position."""
        self._is_homed = False
        self._is_moving.set()
        if self.config['homing_mode'] == "hardware":
            self.hw_stepper.home(self.config['homing_direction'])
            await self.hw_stepper.wait_for_move_completed()
        else:
            # move the stepper manually
            if self.config['homing_direction'] == "clockwise":
                self.hw_stepper.move_vel_mode(1)
            else:
                self.hw_stepper.move_vel_mode(-1)

            # wait until home switch becomes active
            await self.machine.switch_controller.wait_for_switch(self.config['homing_switch'].name,
                                                                 only_on_change=False)
            self.hw_stepper.stop()
            self.hw_stepper.set_home_position()

        self._is_homed = True
        self._is_moving.clear()
        # home position is 0
        self._current_position = 0

    def _post_ready_event(self):
        if not self._ball_search_started:
            self.machine.events.post('stepper_' + self.name + "_ready", position=self._current_position)
            '''event: stepper_(name)_ready'''

    def stop(self):
        """Stop motor."""
        self.hw_stepper.stop()
        self._is_moving.clear()
        if self._move_task:
            self._move_task.cancel()
            self._move_task = None

    @event_handler(1)
    def event_reset(self, **kwargs):
        """Event handler for reset event."""
        del kwargs
        self.reset()

    def reset(self):
        """Move to reset position."""
        self._move_to_absolute_position(self.config['reset_position'])

    @event_handler(5)
    def event_move_to_position(self, position=None, **kwargs):
        """Event handler for move_to_position event."""
        del kwargs
        if position is None:
            raise AssertionError("move_to_position event is missing a position.")

        self.move_to_position(position)

    def move_to_position(self, position):
        """Move stepper to a position."""
        self._target_position = position
        if self._ball_search_started:
            return
        self._move_to_absolute_position(position)

    def _ball_search_start(self, **kwargs):
        del kwargs
        # we do not touch self._position during ball search so we can reset to
        # it later
        self._ball_search_old_target = self._target_position
        self._ball_search_started = True
        self._ball_search_go_to_min()

    def _ball_search_go_to_min(self):
        self._move_to_absolute_position(self.config['ball_search_min'])
        self.delay.add(name="ball_search", callback=self._ball_search_go_to_max, ms=self.config['ball_search_wait'])

    def _ball_search_go_to_max(self):
        self._move_to_absolute_position(self.config['ball_search_max'])
        self.delay.add(name="ball_search", callback=self._ball_search_go_to_min, ms=self.config['ball_search_wait'])

    def _ball_search_stop(self, **kwargs):
        del kwargs
        # stop delay
        self.delay.remove("ball_search")
        self._ball_search_started = False

        # move to last position
        self._target_position = self._ball_search_old_target
        self._move_to_absolute_position(self._target_position)
Esempio n. 13
0
class DigitalOutput(SystemWideDevice):
    """A digital output on either a light or driver platform."""

    config_section = 'digital_outputs'
    collection = 'digital_outputs'
    class_label = 'digital_output'

    __slots__ = ["hw_driver", "platform", "type", "__dict__"]

    def __init__(self, machine: MachineController, name: str) -> None:
        """Initialise digital output."""
        self.hw_driver = None  # type: Optional[Union[DriverPlatformInterface, LightPlatformInterface]]
        self.platform = None  # type: Optional[Union[DriverPlatform, LightsPlatform]]
        self.type = None  # type: Optional[str]
        super().__init__(machine, name)
        self.delay = DelayManager(self.machine)

    async def _initialize(self):
        """Initialise the hardware driver for this digital output."""
        await super()._initialize()
        if self.config['type'] == "driver":
            self._initialize_driver()
        elif self.config['type'] == "light":
            self._initialize_light()
        else:
            raise AssertionError("Invalid type {}".format(self.config['type']))

    def _initialize_light(self):
        """Configure a light as digital output."""
        self.platform = self.machine.get_platform_sections(
            'lights', self.config['platform'])
        self.platform.assert_has_feature("lights")
        self.type = "light"

        if not self.platform.features[
                'allow_empty_numbers'] and self.config['number'] is None:
            self.raise_config_error("Digital Output must have a number.", 1)

        try:
            self.hw_driver = self.platform.configure_light(
                self.config['number'], self.config['light_subtype'], {})
        except AssertionError as e:
            raise AssertionError(
                "Failed to configure light {} in platform. See error above".
                format(self.name)) from e

    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)
        if config['type'] == "driver":
            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))
        elif config['type'] == "light":
            pass
        else:
            raise AssertionError("Invalid type {}".format(config['type']))
        return config

    def _initialize_driver(self):
        """Configure a driver as digital output."""
        self.platform = self.machine.get_platform_sections(
            'coils', self.config['platform'])
        self.platform.assert_has_feature("drivers")
        self.type = "driver"

        config = DriverConfig(default_pulse_ms=255,
                              default_pulse_power=1.0,
                              default_hold_power=1.0,
                              default_recycle=False,
                              max_pulse_ms=255,
                              max_pulse_power=1.0,
                              max_hold_power=1.0)

        if not self.platform.features[
                'allow_empty_numbers'] and self.config['number'] is None:
            self.raise_config_error("Digital Output must have a number.", 2)

        try:
            self.hw_driver = self.platform.configure_driver(
                config, self.config['number'],
                self.config['platform_settings'])
        except AssertionError as e:
            raise AssertionError(
                "Failed to configure driver {} in platform. See error above".
                format(self.name)) from e

    @event_handler(3)
    def event_pulse(self, pulse_ms, **kwargs):
        """Handle pulse control event."""
        del kwargs
        self.pulse(pulse_ms)

    def pulse(self, pulse_ms):
        """Pulse digital output."""
        if self.type == "driver":
            self.hw_driver.pulse(PulseSettings(power=1.0, duration=pulse_ms))
        elif self.type == "light":
            self.hw_driver.set_fade(1.0, -1, 1.0, -1)
            self.platform.light_sync()
            self.delay.reset(name='timed_disable',
                             ms=pulse_ms,
                             callback=self.disable)
        else:
            raise AssertionError("Invalid type {}".format(self.type))

    @event_handler(2)
    def event_enable(self, **kwargs):
        """Handle enable control event."""
        del kwargs
        self.enable()

    def enable(self):
        """Enable digital output."""
        if self.type == "driver":
            self.hw_driver.enable(PulseSettings(power=1.0, duration=0),
                                  HoldSettings(power=1.0))
        elif self.type == "light":
            self.hw_driver.set_fade(1.0, -1, 1.0, -1)
            self.platform.light_sync()
            self.delay.remove(name='timed_disable')
        else:
            raise AssertionError("Invalid type {}".format(self.type))

    @event_handler(1)
    def event_disable(self, **kwargs):
        """Handle disable control event."""
        del kwargs
        self.disable()

    def disable(self):
        """Disable digital output."""
        if self.type == "driver":
            self.hw_driver.disable()
        elif self.type == "light":
            self.hw_driver.set_fade(0.0, -1, 0.0, -1)
            self.platform.light_sync()
            self.delay.remove(name='timed_disable')
        else:
            raise AssertionError("Invalid type {}".format(self.type))
Esempio n. 14
0
class TestEventManager(MpfFakeGameTestCase, MpfTestCase):
    def __init__(self, test_map):
        super().__init__(test_map)
        self._handler1_args = tuple()
        self._handler1_kwargs = dict()
        self._handler1_called = 0
        self._handler2_args = tuple()
        self._handler2_kwargs = dict()
        self._handler2_called = 0
        self._handler3_args = tuple()
        self._handler3_kwargs = dict()
        self._handler3_called = 0
        self._handlers_called = list()
        self._handler_returns_false_args = tuple()
        self._handler_returns_false_kwargs = dict()
        self._handler_returns_false_called = 0
        self._relay1_called = 0
        self._relay2_called = 0
        self._relay_callback_args = tuple()
        self._relay_callback_kwargs = dict()
        self._relay_callback_called = 0
        self._callback_args = tuple()
        self._callback_kwargs = dict()
        self._callback_called = 0
        self._queue = None
        self._queue_callback_args = tuple()
        self._queue_callback_kwargs = dict()
        self._queue_callback_called = 0

    def getConfigFile(self):
        return 'test_event_manager.yaml'

    def getMachinePath(self):
        return 'tests/machine_files/event_manager/'

    def event_handler1(self, *args, **kwargs):
        self._handler1_args = args
        self._handler1_kwargs = kwargs
        self._handler1_called += 1
        self._handlers_called.append(self.event_handler1)

    def event_handler2(self, *args, **kwargs):
        self._handler2_args = args
        self._handler2_kwargs = kwargs
        self._handler2_called += 1
        self._handlers_called.append(self.event_handler2)

    def event_handler3(self, *args, **kwargs):
        self._handler3_args = args
        self._handler3_kwargs = kwargs
        self._handler3_called += 1
        self._handlers_called.append(self.event_handler3)

    def event_handler_returns_false(self, *args, **kwargs):
        self._handler_returns_false_args = args
        self._handler_returns_false_kwargs = kwargs
        self._handler_returns_false_called += 1
        self._handlers_called.append(self.event_handler_returns_false)

        return False

    def event_handler_relay1(self, relay_test, **kwargs):
        del kwargs
        self._relay1_called += 1
        self._handlers_called.append(self.event_handler_relay1)

        return {'relay_test': relay_test}

    def event_handler_relay2(self, relay_test, **kwargs):
        del kwargs
        self._relay2_called += 1
        self._handlers_called.append(self.event_handler_relay2)

        return {'relay_test': relay_test - 1}

    def callback(self, *args, **kwargs):
        self._callback_args = args
        self._callback_kwargs = kwargs
        self._callback_called += 1
        self._handlers_called.append(self.callback)

    def relay_callback(self, *args, **kwargs):
        self._relay_callback_args = args
        self._relay_callback_kwargs = kwargs
        self._relay_callback_called += 1
        self._handlers_called.append(self.relay_callback)

    def event_handler_calls_second_event(self, **kwargs):
        del kwargs
        self.machine.events.post('second_event')
        self._handlers_called.append(self.event_handler_calls_second_event)

    def event_handler_add_queue(self, queue, **kwargs):
        del kwargs
        self._handlers_called.append(self.event_handler_add_queue)
        self._queue = queue
        self._queue.wait()

    def event_handler_add_quick_queue(self, queue, **kwargs):
        del kwargs
        self._handlers_called.append(self.event_handler_add_quick_queue)
        self._queue = queue
        self._queue.wait()
        self._queue.clear()

    def event_handler_clear_queue(self, **kwargs):
        del kwargs
        self._handlers_called.append(self.event_handler_clear_queue)
        self._queue.clear()

    def queue_callback(self, *args, **kwargs):
        self._queue_callback_args = args
        self._queue_callback_kwargs = kwargs
        self._queue_callback_called += 1
        self._handlers_called.append(self.queue_callback)

    def test_event(self):
        # tests that a handler responds to a regular event post
        self.machine.events.add_handler('test_event', self.event_handler1)
        self.advance_time_and_run(1)

        self.machine.events.post('test_event')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(), self._handler1_kwargs)

    def test_event_with_kwargs(self):
        # test that a kwarg can be passed to a handler which is registered for
        # a regular event post
        self.machine.events.add_handler('test_event', self.event_handler1)
        self.advance_time_and_run(1)

        self.machine.events.post('test_event', test1='test1')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual({'test1': 'test1'}, self._handler1_kwargs)

    def test_event_with_callback(self):
        # test that a callback is called when the event is done
        self.machine.events.add_handler('test_event', self.event_handler1)
        self.advance_time_and_run(1)

        self.machine.events.post('test_event',
                                 test1='test1',
                                 callback=self.callback)
        self.advance_time_and_run(1)

        self.assertEqual(1, self._callback_called)

    def test_nested_callbacks(self):
        # tests that an event handlers which posts another event has that event
        # handled before the first event's callback is called

        self.machine.events.add_handler('test_event',
                                        self.event_handler_calls_second_event)
        self.machine.events.add_handler('second_event', self.event_handler1)

        self.advance_time_and_run(1)

        self.machine.events.post('test_event', callback=self.callback)
        self.advance_time_and_run(1)

        self.assertEqual(self._handlers_called[0],
                         self.event_handler_calls_second_event)
        self.assertEqual(self._handlers_called[1], self.event_handler1)
        self.assertEqual(self._handlers_called[2], self.callback)

    def test_event_handler_priorities(self):
        # tests that handler priorities work. The second handler should be
        # called first because it's a higher priority even though it's
        # registered second
        self.machine.events.add_handler('test_event',
                                        self.event_handler1,
                                        priority=100)
        self.machine.events.add_handler('test_event',
                                        self.event_handler2,
                                        priority=200)
        self.advance_time_and_run(1)

        self.machine.events.post('test_event')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(), self._handler1_kwargs)
        self.assertEqual(1, self._handler2_called)
        self.assertEqual(tuple(), self._handler2_args)
        self.assertEqual(dict(), self._handler2_kwargs)

        self.assertEqual(self._handlers_called[0], self.event_handler2)
        self.assertEqual(self._handlers_called[1], self.event_handler1)

    def test_remove_handler_by_handler(self):
        # tests that a handler can be removed by passing the handler to remove
        self.machine.events.add_handler('test_event', self.event_handler1)
        self.advance_time_and_run(1)

        self.machine.events.post('test_event')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(), self._handler1_kwargs)

        self.machine.events.remove_handler(self.event_handler1)

        self.machine.events.post('test_event')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(), self._handler1_kwargs)

    def test_remove_handler_by_event(self):
        # tests that a handler can be removed by a handler/event combo, and
        # that only that handler/event combo is removed
        self.machine.events.add_handler('test_event1', self.event_handler1)
        self.machine.events.add_handler('test_event2', self.event_handler1)
        self.advance_time_and_run(1)

        self.machine.events.post('test_event1')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(), self._handler1_kwargs)

        # should not remove handler since this is the wrong event
        self.machine.events.remove_handler_by_event('test_event3',
                                                    self.event_handler1)

        self.machine.events.post('test_event1')
        self.advance_time_and_run(1)

        self.assertEqual(2, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(), self._handler1_kwargs)

        # remove handler for this event
        self.machine.events.remove_handler_by_event('test_event1',
                                                    self.event_handler1)

        self.machine.events.post('test_event1')
        self.advance_time_and_run(1)

        # results should be the same as above since this handler was removed
        self.assertEqual(2, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(), self._handler1_kwargs)

        self.machine.events.post('test_event2')
        self.advance_time_and_run(1)

        # results should be the same as above since this handler was removed
        self.assertEqual(3, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(), self._handler1_kwargs)

    def test_remove_handler_by_key(self):
        # tests that a handler responds to a regular event post
        key = self.machine.events.add_handler('test_event',
                                              self.event_handler1)
        self.advance_time_and_run(1)

        self.machine.events.post('test_event')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(), self._handler1_kwargs)

        self.machine.events.remove_handler_by_key(key)

        self.machine.events.post('test_event')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(), self._handler1_kwargs)

    def test_remove_handlers_by_keys(self):
        # tests that multiple handlers can be removed by an iterable keys list
        keys = list()

        keys.append(
            self.machine.events.add_handler('test_event1',
                                            self.event_handler1))
        keys.append(
            self.machine.events.add_handler('test_event2',
                                            self.event_handler2))
        self.machine.events.post('test_event1')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(), self._handler1_kwargs)

        self.machine.events.post('test_event2')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler2_called)
        self.assertEqual(tuple(), self._handler2_args)
        self.assertEqual(dict(), self._handler2_kwargs)

        self.machine.events.remove_handlers_by_keys(keys)

        # post events again and handlers should not be called again
        self.machine.events.post('test_event1')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(), self._handler1_kwargs)

        self.machine.events.post('test_event2')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler2_called)
        self.assertEqual(tuple(), self._handler2_args)
        self.assertEqual(dict(), self._handler2_kwargs)

    def test_does_event_exist(self):
        self.machine.events.add_handler('test_event', self.event_handler1)

        self.assertEqual(True,
                         self.machine.events.does_event_exist('test_event'))
        self.assertEqual(False,
                         self.machine.events.does_event_exist('test_event1'))

    def test_regular_event_with_false_return(self):
        # tests that regular events process all handlers even if one returns
        # False

        self.machine.events.add_handler('test_event',
                                        self.event_handler1,
                                        priority=100)
        self.machine.events.add_handler('test_event',
                                        self.event_handler_returns_false,
                                        priority=200)
        self.advance_time_and_run(1)

        self.machine.events.post('test_event')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(1, self._handler_returns_false_called)

        self.assertEqual(self._handlers_called[0],
                         self.event_handler_returns_false)
        self.assertEqual(self._handlers_called[1], self.event_handler1)

    def test_post_boolean(self):
        # tests that a boolean event works

        self.machine.events.add_handler('test_event',
                                        self.event_handler1,
                                        priority=100)
        self.machine.events.add_handler('test_event',
                                        self.event_handler2,
                                        priority=200)
        self.advance_time_and_run(1)

        self.machine.events.post_boolean('test_event')
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(), self._handler1_kwargs)
        self.assertEqual(1, self._handler2_called)
        self.assertEqual(tuple(), self._handler2_args)
        self.assertEqual(dict(), self._handler2_kwargs)

        self.assertEqual(self._handlers_called[0], self.event_handler2)
        self.assertEqual(self._handlers_called[1], self.event_handler1)

    def test_boolean_event_with_false_return(self):
        # tests that regular events process all handlers even if one returns
        # False

        self.machine.events.add_handler('test_event',
                                        self.event_handler1,
                                        priority=100)
        self.machine.events.add_handler('test_event',
                                        self.event_handler_returns_false,
                                        priority=200)
        self.advance_time_and_run(1)

        self.machine.events.post_boolean('test_event')
        self.advance_time_and_run(1)

        self.assertEqual(0, self._handler1_called)
        self.assertEqual(1, self._handler_returns_false_called)

        self.assertEqual(self._handlers_called[0],
                         self.event_handler_returns_false)

        self.assertEqual(1, len(self._handlers_called))

    def test_relay_event(self):
        # tests that a relay event works by passing a value

        self.machine.events.add_handler('test_event',
                                        self.event_handler_relay1,
                                        priority=200)

        self.advance_time_and_run(1)

        self.machine.events.post_relay('test_event',
                                       relay_test=1,
                                       callback=self.relay_callback)
        self.advance_time_and_run(1)

        self.assertEqual(1, self._relay1_called)
        self.assertEqual(1, self._relay_callback_called)

        assert 'relay_test' in self._relay_callback_kwargs
        assert self._relay_callback_kwargs['relay_test'] == 1

    def test_relay_event_handler_changes_value(self):
        # tests that a relay event works by passing a value to a handler that
        # changes that value

        self.machine.events.add_handler('test_event',
                                        self.event_handler_relay1,
                                        priority=200)
        self.machine.events.add_handler('test_event',
                                        self.event_handler_relay2,
                                        priority=100)
        self.advance_time_and_run(1)

        self.machine.events.post_relay('test_event',
                                       relay_test=1,
                                       callback=self.relay_callback)
        self.advance_time_and_run(1)

        self.assertEqual(1, self._relay1_called)
        self.assertEqual(1, self._relay2_called)
        self.assertEqual(1, self._relay_callback_called)

        assert 'relay_test' in self._relay_callback_kwargs
        self.assertEqual(self._relay_callback_kwargs['relay_test'], 0)

    def test_queue(self):
        # tests that a queue event works by registering and clearing a queue

        self.machine.events.add_handler('test_event',
                                        self.event_handler_add_queue)

        self.advance_time_and_run(1)

        self.machine.events.post_queue('test_event',
                                       callback=self.queue_callback)
        self.advance_time_and_run(1)

        self.assertEqual(
            self._handlers_called.count(self.event_handler_add_queue), 1)
        self.assertEqual(self._handlers_called.count(self.queue_callback), 0)
        self.assertEqual(False, self._queue.is_empty())

        self.event_handler_clear_queue()
        self.advance_time_and_run()

        self.assertEqual(self._handlers_called.count(self.queue_callback), 1)
        self.assertEqual(True, self._queue.is_empty())

    def test_queue_event_with_no_queue(self):
        # tests that a queue event works and the callback is called right away
        # if no handlers request a wait

        self.machine.events.add_handler('test_event', self.event_handler1)

        self.advance_time_and_run(1)

        self.machine.events.post_queue('test_event',
                                       callback=self.queue_callback)

        self.advance_time_and_run(1)

        self.assertEqual(self._handlers_called.count(self.event_handler1), 1)
        self.assertEqual(self._handlers_called.count(self.queue_callback), 1)

    def test_queue_event_with_quick_queue_clear(self):
        # tests that a queue event that quickly creates and clears a queue

        self.machine.events.add_handler('test_event',
                                        self.event_handler_add_quick_queue)

        self.advance_time_and_run(1)

        self.machine.events.post_queue('test_event',
                                       callback=self.queue_callback)
        self.advance_time_and_run(1)

        self.assertEqual(
            self._handlers_called.count(self.event_handler_add_quick_queue), 1)

        self.assertEqual(self._handlers_called.count(self.queue_callback), 1)
        self.assertEqual(True, self._queue.is_empty())

    def test_queue_event_with_no_registered_handlers(self):
        # tests that a queue event callback is called works even if there are
        # not registered handlers for that event

        self.machine.events.post_queue('test_event',
                                       callback=self.queue_callback)
        self.advance_time_and_run(1)

        self.assertEqual(self._handlers_called.count(self.queue_callback), 1)
        self.assertIsNone(self._queue)

    def test_queue_event_with_double_quick_queue_clear(self):
        # tests that a queue event that quickly creates and clears a queue

        self.machine.events.add_handler('test_event',
                                        self.event_handler_add_quick_queue,
                                        priority=1)
        self.machine.events.add_handler('test_event',
                                        self.event_handler_add_quick_queue,
                                        priority=2)

        self.advance_time_and_run(1)

        self.machine.events.post_queue('test_event',
                                       callback=self.queue_callback)
        self.advance_time_and_run(1)

        self.assertEqual(
            self._handlers_called.count(self.event_handler_add_quick_queue), 2)

        self.assertEqual(self._handlers_called.count(self.queue_callback), 1)
        self.assertEqual(True, self._queue.is_empty())

    def test_event_player(self):
        self.machine.events.add_handler('test_event_player1',
                                        self.event_handler1)
        self.machine.events.add_handler('test_event_player2',
                                        self.event_handler2)
        self.machine.events.add_handler('test_event_player3',
                                        self.event_handler3)
        self.advance_time_and_run(1)

        self.machine.events.post('test_event_player1', test="123")
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(1, self._handler2_called)
        self.assertEqual(1, self._handler3_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(test="123"), self._handler1_kwargs)
        self.assertEqual(tuple(), self._handler2_args)
        self.assertEqual(dict(priority=0), self._handler2_kwargs)
        self.assertEqual(tuple(), self._handler3_args)
        self.assertEqual(dict(priority=0), self._handler3_kwargs)

    def test_event_player_delay(self):
        self.mock_event('test_event_player2')
        self.mock_event('test_event_player3')

        self.machine.events.post('test_event_player_delayed')
        self.machine_run()
        self.assertEqual(0, self._events['test_event_player2'])
        self.assertEqual(0, self._events['test_event_player3'])
        self.advance_time_and_run(2)
        self.assertEqual(1, self._events['test_event_player2'])
        self.assertEqual(1, self._events['test_event_player3'])

    def test_random_event_player(self):
        self.machine.events.add_handler('test_random_event_player1',
                                        self.event_handler1)
        self.machine.events.add_handler('test_random_event_player2',
                                        self.event_handler2)
        self.machine.events.add_handler('test_random_event_player3',
                                        self.event_handler3)
        self.advance_time_and_run(1)

        with patch('random.randint', return_value=1) as mock_random:
            self.machine.events.post('test_random_event_player1', test="123")
            self.advance_time_and_run(1)
            mock_random.assert_called_once_with(1, 2)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(1, self._handler2_called)
        self.assertEqual(0, self._handler3_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(test="123"), self._handler1_kwargs)
        self.assertEqual(tuple(), self._handler2_args)
        self.assertEqual(dict(test="123"), self._handler2_kwargs)

        with patch('random.randint', return_value=1) as mock_random:
            self.machine.events.post('test_random_event_player1', test="123")
            self.advance_time_and_run(1)
            mock_random.assert_called_once_with(1, 1)

        self.assertEqual(2, self._handler1_called)
        self.assertEqual(1, self._handler2_called)
        self.assertEqual(1, self._handler3_called)
        self.assertEqual(tuple(), self._handler1_args)
        self.assertEqual(dict(test="123"), self._handler1_kwargs)
        self.assertEqual(tuple(), self._handler3_args)
        self.assertEqual(dict(test="123"), self._handler3_kwargs)

    def test_event_player_in_mode(self):
        self.machine.events.add_handler('test_event_player_mode1',
                                        self.event_handler1)
        self.machine.events.add_handler('test_event_player_mode2',
                                        self.event_handler2)
        self.machine.events.add_handler('test_event_player_mode3',
                                        self.event_handler3)
        self.advance_time_and_run(1)

        # mode not loaded. event should not be replayed
        self.machine.events.post('test_event_player_mode1', test="123")
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(0, self._handler2_called)
        self.assertEqual(0, self._handler3_called)

        # start mode
        self.machine.events.post('test_mode_start')
        self.advance_time_and_run(1)
        self.assertTrue(self.machine.mode_controller.is_active("test_mode"))

        # now the event should get replayed
        self.machine.events.post('test_event_player_mode1', test="123")
        self.advance_time_and_run(1)

        self.assertEqual(2, self._handler1_called)
        self.assertEqual(1, self._handler2_called)
        self.assertEqual(1, self._handler3_called)

        # stop mode
        self.machine.events.post('test_mode_end')
        self.advance_time_and_run(1)
        self.assertFalse(self.machine.mode_controller.is_active("test_mode"))

        # event should not longer get replayed
        self.machine.events.post('test_event_player_mode1', test="123")
        self.advance_time_and_run(1)

        self.assertEqual(3, self._handler1_called)
        self.assertEqual(1, self._handler2_called)
        self.assertEqual(1, self._handler3_called)

    def test_random_event_player_in_mode(self):
        self.machine.events.add_handler('test_random_event_player_mode1',
                                        self.event_handler1)
        self.machine.events.add_handler('test_random_event_player_mode2',
                                        self.event_handler2)
        self.machine.events.add_handler('test_random_event_player_mode3',
                                        self.event_handler3)
        self.advance_time_and_run(1)

        # mode not loaded. event should not be replayed
        self.machine.events.post('test_random_event_player_mode1', test="123")
        self.advance_time_and_run(1)

        self.assertEqual(1, self._handler1_called)
        self.assertEqual(0, self._handler2_called)
        self.assertEqual(0, self._handler3_called)

        # start mode
        self.machine.events.post('test_mode_start')
        self.advance_time_and_run(1)
        self.assertTrue(self.machine.mode_controller.is_active("test_mode"))

        # now the event should get replayed
        with patch('random.randint', return_value=1) as mock_random:
            self.machine.events.post('test_random_event_player_mode1',
                                     test="123")
            self.advance_time_and_run(1)
            mock_random.assert_called_once_with(1, 2)

        self.assertEqual(2, self._handler1_called)
        self.assertEqual(1, self._handler2_called)
        self.assertEqual(0, self._handler3_called)

        # stop mode
        self.machine.events.post('test_mode_end')
        self.advance_time_and_run(1)
        self.assertFalse(self.machine.mode_controller.is_active("test_mode"))

        # event should not longer get replayed
        self.machine.events.post('test_random_event_player_mode1', test="123")
        self.advance_time_and_run(1)

        self.assertEqual(3, self._handler1_called)
        self.assertEqual(1, self._handler2_called)
        self.assertEqual(0, self._handler3_called)

    def event_block(self, queue, **kwargs):
        del kwargs
        self.queue = queue
        queue.wait()

    def test_random_event_player_in_game_mode(self):
        self.start_game()
        self.machine.events.add_handler('out1', self.event_handler2)
        self.machine.events.add_handler('out2', self.event_handler3)
        self.advance_time_and_run(1)

        # mode not loaded. event should not be replayed
        self.machine.events.post('test_random_event_player_mode2', test="123")
        self.advance_time_and_run(1)

        self.assertEqual(0, self._handler2_called)
        self.assertEqual(0, self._handler3_called)

        # start mode
        self.machine.events.post('game_mode_start')
        self.advance_time_and_run(1)
        self.assertTrue(self.machine.mode_controller.is_active("game_mode"))

        # now the event should get replayed
        with patch('random.randint', return_value=1) as mock_random:
            self.machine.events.post('test_random_event_player_mode2',
                                     test="123")
            self.advance_time_and_run(1)
            mock_random.assert_called_once_with(1, 2)

        self.assertEqual(1, self._handler2_called)
        self.assertEqual(0, self._handler3_called)

        # block stop of mode
        self.machine.events.add_handler('mode_game_mode_stopping',
                                        self.event_block)

        # stop game
        self.machine.game.stop()
        self.advance_time_and_run()

        # event should still work (and not crash)
        self.machine.events.post('test_random_event_player_mode2', test="123")
        self.machine_run()
        self.assertEqual(1, self._handler2_called)
        self.assertEqual(1, self._handler3_called)

        # when the block ends game should end too
        self.queue.clear()
        self.machine_run()

        # but no longer after clear
        self.machine.events.post('test_random_event_player_mode2', test="123")
        self.machine_run()

        self.advance_time_and_run(1)
        self.assertFalse(self.machine.mode_controller.is_active("game_mode"))

        self.assertEqual(1, self._handler2_called)
        self.assertEqual(1, self._handler3_called)

    def delay1_cb(self, **kwargs):
        del kwargs
        self.machine.events.post("event1")

    def event1_cb(self, **kwargs):
        del kwargs
        self.delay.add(ms=100, callback=self.delay2_cb)

    def delay2_cb(self, **kwargs):
        del kwargs
        self.machine.events.post("event2")

    def event2_cb(self, **kwargs):
        del kwargs
        self.delay.add(ms=100, callback=self.delay3_cb)

    def delay3_cb(self, **kwargs):
        del kwargs
        self.machine.events.post("event3")

    def event3_cb(self, **kwargs):
        del kwargs
        self.correct = True

    def test_event_in_delay(self):
        self.machine.events.add_handler('event1', self.event1_cb)
        self.machine.events.add_handler('event2', self.event2_cb)
        self.machine.events.add_handler('event3', self.event3_cb)
        self.correct = False
        self.delay = DelayManager(self.machine.delayRegistry)

        self.machine.events.post("event1")
        self.advance_time_and_run(1)

        self.assertTrue(self.correct)

    def delay_first(self):
        self.called = True
        self.delay.remove("second")

    def delay_second(self):
        if not self.called:
            raise AssertionError("first has not been called")

        raise AssertionError("this should never be called")

    def test_delay_order(self):
        self.called = False
        self.delay = DelayManager(self.machine.delayRegistry)

        self.delay.add(ms=6001, name="second", callback=self.delay_second)
        self.delay.add(ms=6000, name="first", callback=self.delay_first)

        self.advance_time_and_run(10)

    def delay_zero_ms(self, start):
        self.delay.add(ms=0,
                       name="second",
                       callback=self.delay_zero_ms_next_frame,
                       start=start)

    def delay_zero_ms_next_frame(self, start):
        self.assertLessEqual(self.machine.clock.get_time(), start)

    def test_zero_ms_delay(self):
        self.called = False
        self.delay = DelayManager(self.machine.delayRegistry)

        self.delay.add(ms=0,
                       name="first",
                       callback=self.delay_zero_ms,
                       start=self.machine.clock.get_time())
        self.advance_time_and_run(10)

    def _handler(self, **kwargs):
        del kwargs
        self._called += 1

    def test_handler_with_condition(self):
        self._called = 0
        self.machine.events.add_handler("test{param > 1 and a == True}",
                                        self._handler)

        self.post_event("test")
        self.assertEqual(0, self._called)

        self.post_event_with_params("test", param=3, a=False)
        self.assertEqual(0, self._called)

        self.post_event_with_params("test", param=3, a=True)
        self.assertEqual(1, self._called)

    def test_handler_with_settings_condition_invalid_setting(self):
        self._called = 0
        self.machine.events.add_handler("test{settings.test == True}",
                                        self._handler)

        # invalid setting
        with self.assertRaises(AssertionError):
            self.post_event("test")
            self.assertEqual(0, self._called)

        # reset exception
        self._exception = None

    def test_handler_with_settings_condition(self):
        self._called = 0
        self.machine.events.add_handler("test{settings.test == True}",
                                        self._handler)

        self.machine.settings._settings = {}
        self.machine.settings.add_setting(
            SettingEntry("test", "Test", 1, "test", "a", {
                False: "A (default)",
                True: "B"
            }))

        # setting false
        self.post_event("test")
        self.assertEqual(0, self._called)

        self.machine.settings.set_setting_value("test", True)

        # settings true
        self.post_event("test")
        self.assertEqual(1, self._called)

    def test_weighted(self):
        self.mock_event("out3")
        self.mock_event("out4")

        self.post_event("test_mode_start")
        self.advance_time_and_run()

        with patch('random.randint', return_value=1) as mock_random:
            self.machine.events.post('test_random_event_player_weighted')
            self.advance_time_and_run(1)
            mock_random.assert_called_once_with(1, 1001)

        self.assertEventCalled("out3")
        self.assertEventNotCalled("out4")

        self.mock_event("out3")
        self.mock_event("out4")

        with patch('random.randint', return_value=2) as mock_random:
            self.machine.events.post('test_random_event_player_weighted')
            self.advance_time_and_run(1)
            mock_random.assert_called_once_with(1, 1001)

        self.assertEventNotCalled("out3")
        self.assertEventCalled("out4")

        self.mock_event("out3")
        self.mock_event("out4")

        with patch('random.randint', return_value=500) as mock_random:
            self.machine.events.post('test_random_event_player_weighted')
            self.advance_time_and_run(1)
            mock_random.assert_called_once_with(1, 1001)

        self.assertEventNotCalled("out3")
        self.assertEventCalled("out4")
Esempio n. 15
0
class SegmentDisplayPlayer(DeviceConfigPlayer):
    """Generates texts on segment displays."""

    config_file_section = 'segment_display_player'
    show_section = 'segment_displays'
    machine_collection_name = 'segment_displays'

    __slots__ = ["delay"]

    def __init__(self, machine):
        """Initialise SegmentDisplayPlayer."""
        super().__init__(machine)
        self.delay = DelayManager(self.machine.delayRegistry)

    def play(self, settings, context, calling_context, priority=0, **kwargs):
        """Show text on display."""
        del kwargs
        instance_dict = self._get_instance_dict(
            context)  # type: Dict[str, SegmentDisplay]
        full_context = self._get_full_context(context)

        for display, s in settings.items():
            action = s['action']
            if display not in instance_dict:
                instance_dict[display] = {}

            key = full_context + "." + display.name

            if s['key']:
                key += s['key']

            if action == "add":
                # add text
                display.add_text(s['text'], priority + s['priority'], key)

                if s['expire']:
                    instance_dict[display][key] = self.delay.add(
                        s['expire'],
                        self._remove,
                        instance_dict=instance_dict,
                        key=key,
                        display=display)
                else:
                    instance_dict[display][key] = True
            elif action == "remove":
                self._remove(instance_dict=instance_dict,
                             key=key,
                             display=display)
            elif action == "flash":
                display.set_flashing(True)
            elif action == "no_flash":
                display.set_flashing(False)
            else:
                raise AssertionError("Invalid action {}".format(action))

    def _remove(self, instance_dict, key, display):
        if key in instance_dict[display]:
            display.remove_text_by_key(key)
            if instance_dict[display][key] is not True:
                self.delay.remove(instance_dict[display][key])
            del instance_dict[display][key]

    def clear_context(self, context):
        """Remove all texts."""
        instance_dict = self._get_instance_dict(context)
        for display, keys in instance_dict.items():
            for key in dict(keys).keys():
                self._remove(instance_dict=instance_dict,
                             key=key,
                             display=display)

        self._reset_instance_dict(context)

    def get_express_config(self, value):
        """Parse express config."""
        return dict(action="add", text=value)
Esempio n. 16
0
class BallSearch(MpfController):
    """Implements Ball search for a playfield device.

    In MPF, the ball search functionality is attached to each playfield
    device, rather than being done at the global level. (In other words, each
    playfield is responsible for making sure no balls get stuck on it, and it
    leverages an instance of this BallSearch class to handle it.)

    """
    def __init__(self, machine: MachineController,
                 playfield: "Playfield") -> None:
        """Initialize ball search."""
        self.module_name = 'BallSearch.' + playfield.name
        self.config_name = 'ball_search'

        super().__init__(machine)

        self.playfield = playfield
        """The playfield device this ball search instance is attached to."""

        self.delay = DelayManager(self.machine)

        self.started = False
        """Is the ball search process started (running) now."""
        self.enabled = False
        """Is ball search enabled."""
        self.blocked = False
        """If True, ball search will be blocked and will not start."""
        self.callbacks = []  # type: List[BallSearchCallback]

        self.iteration = False
        """Current iteration of the ball search, or ``False`` if ball search
        is not started."""
        self.iterator = False
        self.phase = False
        """Current phase of the ball search, or ``False`` if ball search is not
        started."""

        # register for events
        self.machine.events.add_handler('request_to_start_game',
                                        self.request_to_start_game)

        self.machine.events.add_handler('cancel_ball_search',
                                        self.cancel_ball_search)
        '''event: cancel_ball_search
        desc: This event will cancel all running ball searches and mark the
        balls as lost. This is only a handler so all you have to do is to post
        the event.'''

    def request_to_start_game(self, **kwargs):
        """Handle result of the *request_to_start_game* event.

        If ball search is running, this method will return *False* to prevent
        the game from starting while ball search is running.

        This method also posts the *ball_search_prevents_game_start* event
        if ball search is started.

        """
        # todo we should enable ball search if a ball is missing on game start

        del kwargs
        if self.started:

            self.machine.events.post('ball_search_prevents_game_start')
            '''event: ball_search_prevents_game_start
            desc: A game start has been requested, but the ball search process
            is running and thus the game start has been blocked. This is a
            good event to use for a slide player to inform the player that the
            machine is looking for a missing ball.'''

            return False
        else:
            return True

    def register(self, priority, callback, name, *, restore_callback=None):
        """Register a callback for sequential ball search.

        Callbacks are called by priority. Ball search only waits if the
        callback returns true.

        Args:
            priority: priority of this callback in the ball search procedure
            callback: callback to call. ball search will wait before the next
                callback, if it returns true
            name: string name which is used for debugging & the logs
            restore_callback: optional callback to restore state of the device
                after ball search ended
        """
        self.debug_log("Registering callback: {} (priority: {})".format(
            name, priority))
        self.callbacks.append(
            BallSearchCallback(priority, callback, name, restore_callback))
        # sort by priority
        self.callbacks = sorted(self.callbacks,
                                key=lambda entry: entry.priority)

    def enable(self, **kwargs):
        """Enable the ball search for this playfield.

        Note that this method does *not* start the ball search process. Rather
        it just resets and starts the timeout timer, as well as resetting it
        when playfield switches are hit.

        """
        if self.blocked:
            return

        del kwargs
        if self.playfield.config['enable_ball_search'] is False or (
                not self.playfield.config['enable_ball_search']
                and not self.machine.config['mpf']['default_ball_search']):
            return

        if not self.callbacks:
            raise AssertionError("No callbacks registered")

        self.debug_log("Enabling Ball Search")

        self.enabled = True

        self.reset_timer()

    def disable(self, **kwargs):
        """Disable ball search.

        This method will also stop the ball search if it is running.
        """
        del kwargs
        self.stop()

        self.debug_log("Disabling Ball Search")
        self.enabled = False
        self.delay.remove('start')

    def block(self, **kwargs):
        """Block ball search for this playfield.

        Blocking will disable ball search if it's enabled or running, and will
        prevent ball search from enabling if it's disabled until
        ``ball_search_unblock()`` is called.
        """
        del kwargs
        self.debug_log("Blocking ball search")
        self.disable()
        self.blocked = True

    def unblock(self, **kwargs):
        """Unblock ball search for this playfield.

        This will check to see if there are balls on the playfield, and if so,
        enable ball search.
        """
        del kwargs
        self.debug_log("Unblocking ball search")
        self.blocked = False

        if self.playfield.balls:
            self.enable()

    def reset_timer(self):
        """Reset the timeout timer which starts ball search.

        This method will also cancel an actively running (started) ball search.

        This is called by the playfield anytime a playfield switch is hit.

        """
        if self.started:
            self.stop()

        if self.enabled:
            self.debug_log("Resetting ball search timer")
            self.delay.reset(name='start',
                             callback=self.start,
                             ms=self.playfield.config['ball_search_timeout'])

    def start(self):
        """Start ball search the ball search process."""
        if not self.enabled or self.started or not self.callbacks:
            return
        self.started = True
        self.iteration = 1
        self.phase = 1
        self.iterator = iter(self.callbacks)
        self.info_log("Starting ball search")
        self.machine.events.post('ball_search_started')
        '''event: ball_search_started

        desc: The ball search process has been begun.
        '''

        self.machine.events.post('ball_search_phase_1', iteration=1)
        # see description below

        self._run()

    def stop(self):
        """Stop an actively running ball search."""
        if not self.started:
            return

        self.info_log("Stopping ball search")

        self.started = False
        self.delay.remove('run')

        # restore all devices
        for callback in self.callbacks:
            if callback.restore_callback:
                callback.restore_callback()

        self.machine.events.post('ball_search_stopped')
        '''event: ball_search_stopped

        desc: The ball search process has been disabled. This event is posted
            any time ball search stops, regardless of whether it found a ball
            or gave up. (If the ball search failed to find the ball, it will
            also post the *ball_search_failed* event.)
        '''

    def _run(self):
        # Runs one iteration of the ball search.
        # Will schedule itself for the next run.

        # check if we should skip this phase
        if not self.playfield.config['ball_search_phase_{}_searches'.format(
                self.phase)]:
            self.phase += 1
            if self.phase > 3:
                # give up
                self.give_up()
            else:
                # go to the next phase
                self._run()
            return

        timeout = self.playfield.config['ball_search_interval']

        # iterate until we are done with all callbacks
        while True:
            try:
                element = next(self.iterator)
            except StopIteration:
                self.iteration += 1
                self.machine.events.post('ball_search_phase_{}'.format(
                    self.phase),
                                         iteration=self.iteration)
                '''event: ball_search_phase_(num)

                desc: The ball search phase (num) has started.
                args:
                    iteration: Current iteration of phase (num)
                '''
                # give up at some point
                if self.iteration > self.playfield.config[
                        'ball_search_phase_{}_searches'.format(self.phase)]:
                    self.phase += 1
                    self.iteration = 1
                    if self.phase > 3:
                        self.give_up()
                        return

                self.iterator = iter(self.callbacks)
                element = next(self.iterator)
                timeout = self.playfield.config[
                    'ball_search_wait_after_iteration']

            # if a callback returns True we wait for the next one
            self.debug_log("Ball search: {} (phase: {}  iteration: {})".format(
                element.name, self.phase, self.iteration))
            if element.callback(self.phase, self.iteration):
                self.delay.add(name='run', callback=self._run, ms=timeout)
                return

    def cancel_ball_search(self, **kwargs):
        """Cancel the current ball search and mark the ball as missing."""
        del kwargs
        if self.started:
            self.give_up()

    def give_up(self):
        """Give up the ball search.

        This method is called when the ball search process Did not find the
        missing ball. It executes the failed action which depending on the specification of *ball_search_failed_action*,
        either adds a replacement ball, ends the game, or ends the current ball.
        """
        self.info_log("Ball Search failed to find ball. Giving up!")
        self.disable()
        self.machine.events.post('ball_search_failed')
        '''event: ball_search_failed

        desc: The ball search process has failed to locate a missing or stuck
            ball and has given up. This event will be posted immediately after
            the *ball_search_stopped* event.
        '''

        lost_balls = self.playfield.balls
        self.machine.ball_controller.num_balls_known -= lost_balls
        self.playfield.balls = 0
        self.playfield.available_balls = 0

        self._compensate_lost_balls(lost_balls)

    def _compensate_lost_balls(self, lost_balls):
        if not self.machine.game:
            return

        if self.playfield.config['ball_search_failed_action'] == "new_ball":
            if self.machine.ball_controller.num_balls_known > 0:
                # we have at least one ball remaining
                self.info_log("Adding %s replacement ball", lost_balls)
                for dummy_iterator in range(lost_balls):
                    self.playfield.add_ball()
            else:
                self.info_log("No more balls left. Ending game!")
                self.machine.game.end_game()

        elif self.playfield.config['ball_search_failed_action'] == "end_game":
            if self.machine.game:
                self.info_log("Ending the game")
                self.machine.game.end_game()
            else:
                self.warning_log("There is no game. Doing nothing!")

        elif self.playfield.config['ball_search_failed_action'] == "end_ball":
            self.info_log("Ending current ball")
            self.machine.game.end_ball()

        else:
            raise AssertionError(
                "Unknown action " +
                self.playfield.config['ball_search_failed_action'])
Esempio n. 17
0
class Timer(ModeDevice):
    """Parent class for a mode timer.

    Args:
    ----
        machine: The main MPF MachineController object.
        name: The string name of this timer.
    """

    config_section = 'timers'
    collection = 'timers'
    class_label = 'timer'

    def __init__(self, machine: "MachineController", name: str) -> None:
        """Initialise mode timer."""
        super().__init__(machine, name)
        self.machine = machine
        self.name = name

        self.running = False
        self.start_value = None  # type: Optional[int]
        self.restart_on_complete = None  # type: Optional[bool]
        self._ticks = 0
        self.tick_var = None  # type: Optional[str]
        self.tick_secs = None  # type: Optional[float]
        self.player = None  # type: Optional[Player]
        self.end_value = None  # type: Optional[int]
        self.max_value = None  # type: Optional[int]
        self.ticks_remaining = None  # type: Optional[int]
        self.direction = None  # type: Optional[str]
        self.timer = None  # type: Optional[PeriodicTask]
        self.event_keys = list()  # type: List[EventHandlerKey]
        self.delay = None  # type: Optional[DelayManager]

    async def device_added_to_mode(self, mode: Mode) -> None:
        """Device added in mode."""
        await super().device_added_to_mode(mode)
        self.tick_var = '{}_{}_tick'.format(mode.name, self.name)

    async def _initialize(self):
        await super()._initialize()
        self.ticks_remaining = 0
        self.max_value = self.config['max_value']
        self.direction = self.config['direction']
        self.tick_secs = None
        self.timer = None
        self.event_keys = list()
        self.delay = DelayManager(self.machine)

        self.restart_on_complete = self.config['restart_on_complete']
        self.end_value = None
        self.start_value = None
        self.ticks = None

        if self.config['debug']:
            self.configure_logging('Timer.' + self.name, 'full', 'full')
        else:
            self.configure_logging('Timer.' + self.name,
                                   self.config['console_log'],
                                   self.config['file_log'])

        self.debug_log("----------- Initial Values -----------")
        self.debug_log("running: %s", self.running)
        self.debug_log("start_value: %s", self.start_value)
        self.debug_log("restart_on_complete: %s", self.restart_on_complete)
        self.debug_log("_ticks: %s", self.ticks)
        self.debug_log("end_value: %s", self.end_value)
        self.debug_log("ticks_remaining: %s", self.ticks_remaining)
        self.debug_log("max_value: %s", self.max_value)
        self.debug_log("direction: %s", self.direction)
        self.debug_log("tick_secs: %s", self.tick_secs)
        self.debug_log("--------------------------------------")

    def device_loaded_in_mode(self, mode: Mode, player: Player):
        """Set up control events when mode is loaded."""
        del mode
        self.tick_secs = self.config['tick_interval'].evaluate([])

        try:
            self.end_value = self.config['end_value'].evaluate([])
        except AttributeError:
            self.end_value = None

        if self.direction == 'down' and not self.end_value:
            self.end_value = 0  # need it to be 0 not None

        self.start_value = self.config['start_value'].evaluate([])
        self.ticks = self.start_value

        self.player = player
        if self.config['control_events']:
            self._setup_control_events(self.config['control_events'])

        if self.config['start_running']:
            self.start()

    @property
    def ticks(self):
        """Return ticks."""
        return self._ticks

    @ticks.setter
    def ticks(self, value):
        self._ticks = value

        try:
            self.player[self.tick_var] = value
            '''player_var: (mode)_(timer)_tick

            desc: Stores the current tick value for the timer from the mode
            (mode) with the time name (timer). For example, a timer called
            "my_timer" which is in the config for "mode1" will store its tick
            value in the player variable ``mode1_my_timer_tick``.
            '''

        except TypeError:
            pass

    @property
    def can_exist_outside_of_game(self):
        """Timer can live outside of games."""
        return True

    def _setup_control_events(self, event_list):
        self.debug_log("Setting up control events")

        kwargs = {}
        for entry in event_list:
            if entry['action'] in ('add', 'subtract', 'jump', 'pause',
                                   'set_tick_interval'):
                handler = getattr(self, entry['action'])
                kwargs = {'timer_value': entry['value']}

            elif entry['action'] in ('start', 'stop', 'reset', 'restart'):
                handler = getattr(self, entry['action'])

            elif entry['action'] == 'change_tick_interval':
                handler = self.change_tick_interval
                kwargs = {'change': entry['value']}

            elif entry['action'] == 'set_tick_interval':
                handler = self.set_tick_interval
                kwargs = {'timer_value': entry['value']}

            elif entry['action'] == 'reset_tick_interval':
                handler = self.set_tick_interval
                kwargs = {'timer_value': self.config['tick_interval']}

            else:
                raise AssertionError(
                    "Invalid control_event action {} in mode".format(
                        entry['action']), self.name)

            self.event_keys.append(
                self.machine.events.add_handler(entry['event'], handler,
                                                **kwargs))

    def _remove_control_events(self):
        self.debug_log("Removing control events")

        for key in self.event_keys:
            self.machine.events.remove_handler_by_key(key)

    def reset(self, **kwargs):
        """Reset this timer based to the starting value that's already been configured.

        Does not start or stop the timer.

        Args:
        ----
            **kwargs: Not used in this method. Only exists since this method is
                often registered as an event handler which may contain
                additional keyword arguments.
        """
        del kwargs

        self.debug_log("Resetting timer. New value: %s", self.start_value)

        self.jump(self.start_value)

    def start(self, **kwargs):
        """Start this timer based on the starting value that's already been configured.

        Use jump() if you want to set the starting time value.

        Args:
        ----
            **kwargs: Not used in this method. Only exists since this method is
                often registered as an event handler which may contain
                additional keyword arguments.
        """
        del kwargs

        # do not start if timer is already running
        if self.running:
            return

        self.info_log("Starting Timer.")

        if self._check_for_done():
            return

        self.running = True

        self.delay.remove('pause')
        self._create_system_timer()

        self.machine.events.post('timer_' + self.name + '_started',
                                 ticks=self.ticks,
                                 ticks_remaining=self.ticks_remaining)
        '''event: timer_(name)_started

        desc: The timer named (name) has just started.

        args:
            ticks: The current tick number this timer is at.
            ticks_remaining: The number of ticks in this timer remaining.
        '''

        self._post_tick_events()
        # since lots of slides and stuff are tied to the timer tick, we want
        # to post an initial tick event also that represents the starting
        # timer value.

    def restart(self, **kwargs):
        """Restart the timer by resetting it and then starting it.

        Essentially this is just a reset() then a start().

        Args:
        ----
            **kwargs: Not used in this method. Only exists since this method is
                often registered as an event handler which may contain
                additional keyword arguments.
        """
        del kwargs
        self.reset()
        self.start()

    def stop(self, **kwargs):
        """Stop the timer and posts the 'timer_<name>_stopped' event.

        Args:
        ----
            **kwargs: Not used in this method. Only exists since this method is
                often registered as an event handler which may contain
                additional keyword arguments.
        """
        del kwargs

        self.info_log("Stopping Timer")

        self.delay.remove('pause')

        self.running = False
        self._remove_system_timer()

        self.machine.events.post('timer_' + self.name + '_stopped',
                                 ticks=self.ticks,
                                 ticks_remaining=self.ticks_remaining)
        '''event: timer_(name)_stopped

        desc: The timer named (name) has stopped.

        This event is posted any time the timer stops, whether it stops because
        it ended or because it was stopped early by some other event.

        args:
            ticks: The current tick number this timer is at.
            ticks_remaining: The number of ticks in this timer remaining.
        '''

    def pause(self, timer_value=0, **kwargs):
        """Pause the timer and posts the 'timer_<name>_paused' event.

        Args:
        ----
            timer_value: How many seconds you want to pause the timer for. Note
                that this pause time is real-world seconds and does not take
                into consideration this timer's tick interval.
            **kwargs: Not used in this method. Only exists since this method is
                often registered as an event handler which may contain
                additional keyword arguments.
        """
        del kwargs

        if not timer_value:
            pause_ms = 0  # make sure it's not None, etc.
        else:
            pause_ms = self._get_timer_value(
                timer_value) * 1000  # delays happen in ms

        self.info_log("Pausing Timer for %s ms", pause_ms)

        self.running = False

        self._remove_system_timer()
        self.machine.events.post('timer_' + self.name + '_paused',
                                 ticks=self.ticks,
                                 ticks_remaining=self.ticks_remaining)
        '''event: timer_(name)_paused

        desc: The timer named (name) has paused.

        args:
            ticks: The current tick number this timer is at.
            ticks_remaining: The number of ticks in this timer remaining.
        '''

        if pause_ms > 0:
            self.delay.add(name='pause', ms=pause_ms, callback=self.start)

    def timer_complete(self, **kwargs):
        """Automatically called when this timer completes.

        Posts the 'timer_<name>_complete' event. Can be manually called to mark
        this timer as complete.

        Args:
        ----
            **kwargs: Not used in this method. Only exists since this method is
                often registered as an event handler which may contain
                additional keyword arguments.
        """
        del kwargs

        self.info_log("Timer Complete")

        self.stop()

        self.machine.events.post('timer_' + self.name + '_complete',
                                 ticks=self.ticks,
                                 ticks_remaining=self.ticks_remaining)
        '''event: timer_(name)_complete

        desc: The timer named (name) has completed.

        Note that this timer may reset and start again after this event is
        posted, depending on its settings.

        args:
            ticks: The current tick number this timer is at.
            ticks_remaining: The number of ticks in this timer remaining.
        '''

        if self.restart_on_complete:

            self.debug_log("Restart on complete: True")

            self.reset()
            self.start()

    def _timer_tick(self):
        # Automatically called by the core clock each tick
        if self._debug:
            self.debug_log("Timer Tick")

        if not self.running:
            if self._debug:
                self.debug_log("Timer is not running. Will remove.")

            self._remove_system_timer()
            return

        if self.direction == 'down':
            self.ticks -= 1
        else:
            self.ticks += 1

        self._post_tick_events()

    def _post_tick_events(self):
        if not self._check_for_done():
            self.machine.events.post('timer_{}_tick'.format(self.name),
                                     ticks=self.ticks,
                                     ticks_remaining=self.ticks_remaining)
            '''event: timer_(name)_tick

            desc: The timer named (name) has just counted down (or up,
            depending on its settings).

            args:
                ticks: The new tick number this timer is at.
                ticks_remaining: The new number of ticks in this timer
                    remaining.
            '''

            if self._debug:
                self.debug_log("Ticks: %s, Remaining: %s", self.ticks,
                               self.ticks_remaining)

    def add(self, timer_value, **kwargs):
        """Add ticks to this timer.

        Args:
        ----
            timer_value: The number of ticks you want to add to this timer's
                current value.
            kwargs: Not used in this method. Only exists since this method is
                often registered as an event handler which may contain
                additional keyword arguments.
        """
        del kwargs

        timer_value = self._get_timer_value(timer_value)
        ticks_added = timer_value

        new_value = self.ticks + ticks_added

        if self.max_value and new_value > self.max_value:
            new_value = self.max_value

        self.ticks = new_value
        ticks_added = new_value - timer_value

        self.machine.events.post('timer_' + self.name + '_time_added',
                                 ticks=self.ticks,
                                 ticks_added=ticks_added,
                                 ticks_remaining=self.ticks_remaining)
        '''event: timer_(name)_time_added

        desc: The timer named (name) has just had time added to it.

        args:
            ticks: The new tick number this timer is at.
            ticks_remaining: The new number of ticks in this timer remaining.
            ticks_added: How many ticks were just added.
        '''

        self._check_for_done()

    def subtract(self, timer_value, **kwargs):
        """Subtract ticks from this timer.

        Args:
        ----
            timer_value: The number of ticks you want to subtract from this
                timer's current value.
            **kwargs: Not used in this method. Only exists since this method is
                often registered as an event handler which may contain
                additional keyword arguments.
        """
        del kwargs

        ticks_subtracted = self._get_timer_value(timer_value)

        self.ticks -= ticks_subtracted

        self.machine.events.post('timer_' + self.name + '_time_subtracted',
                                 ticks=self.ticks,
                                 ticks_subtracted=ticks_subtracted,
                                 ticks_remaining=self.ticks_remaining)
        '''event: timer_(name)_time_subtracted

        desc: The timer named (name) just had some ticks removed.

        args:
            ticks: The new current tick number this timer is at.
            ticks_remaining: The new number of ticks in this timer remaining.
            ticks_subtracted: How many ticks were just subtracted from this
                timer. (This number will be positive, indicating the ticks
                subtracted.)
        '''

        self._check_for_done()

    def _check_for_done(self):
        # Checks to see if this timer is done. Automatically called anytime the
        # timer's value changes.
        if self._debug:
            self.debug_log(
                "Checking to see if timer is done. Ticks: %s, End "
                "Value: %s, Direction: %s", self.ticks, self.end_value,
                self.direction)

        if (self.direction == 'up' and self.end_value is not None
                and self.ticks >= self.end_value):
            self.timer_complete()
            return True

        if (self.direction == 'down' and self.ticks <= self.end_value):
            self.timer_complete()
            return True

        if self.end_value is not None:
            self.ticks_remaining = abs(self.end_value - self.ticks)

        if self._debug:
            self.debug_log("Timer is not done")

        return False

    def _create_system_timer(self):
        # Creates the clock event which drives this mode timer's tick method.
        self._remove_system_timer()
        self.timer = self.machine.clock.schedule_interval(
            self._timer_tick, self.tick_secs)

    def _remove_system_timer(self):
        # Removes the clock event associated with this mode timer.
        if self.timer:
            self.machine.clock.unschedule(self.timer)
            self.timer = None

    @staticmethod
    def _get_timer_value(timer_value):
        if hasattr(timer_value, "evaluate"):
            # Convert to int for ticks; config_spec must be float for change_tick_interval
            return int(timer_value.evaluate([]))
        return timer_value

    def change_tick_interval(self, change=0.0, **kwargs):
        """Change the interval for each "tick" of this timer.

        Args:
        ----
            change: Float or int of the change you want to make to this timer's
                tick rate. Note this value is multiplied by the current tick
                interval: >1 will increase the tick interval (slow the timer) and
                <1 will decrease the tick interval (accelerate the timer).
                To set an absolute value, use the set_tick_interval() method.
            **kwargs: Not used in this method. Only exists since this method is
                often registered as an event handler which may contain
                additional keyword arguments.
        """
        del kwargs

        self.tick_secs *= change.evaluate([])
        self._create_system_timer()

    def set_tick_interval(self, timer_value, **kwargs):
        """Set the number of seconds between ticks for this timer.

        This is an absolute setting. To apply a change to the current value,
        use the change_tick_interval() method.

        Args:
        ----
            timer_value: The new number of seconds between each tick of this
                timer. This value should always be positive.
            **kwargs: Not used in this method. Only exists since this method is
                often registered as an event handler which may contain
                additional keyword arguments.
        """
        self.tick_secs = abs(
            self._get_timer_value(timer_value.evaluate(kwargs)))
        self._create_system_timer()

    def jump(self, timer_value, **kwargs):
        """Set the current amount of time of this timer.

        This value is expressed in "ticks" since the interval per tick can be
        something other than 1 second).

        Args:
        ----
            timer_value: Integer of the current value you want this timer to be.
            **kwargs: Not used in this method. Only exists since this method is
                often registered as an event handler which may contain
                additional keyword arguments.
        """
        del kwargs

        self.ticks = self._get_timer_value(timer_value)

        if self.max_value and self.ticks > self.max_value:
            self.ticks = self.max_value

        self._remove_system_timer()
        self._create_system_timer()

        self._check_for_done()

    def device_removed_from_mode(self, mode: Mode):
        """Stop this timer and also removes all the control events."""
        self.stop()
        self._remove_control_events()
Esempio n. 18
0
class Servo(SystemWideDevice):
    """Represents a servo in a pinball machine.

    Args: Same as the Device parent class.
    """

    config_section = 'servos'
    collection = 'servos'
    class_label = 'servo'

    def __init__(self, machine, name):
        """Initialise servo."""
        self.hw_servo = None
        self._position = None
        self._ball_search_started = False
        self.delay = DelayManager(machine.delayRegistry)
        super().__init__(machine, name)

    def _initialize(self):
        self.load_platform_section('servo_controllers')

        for position in self.config['positions']:
            self.machine.events.add_handler(self.config['positions'][position],
                                            self._position_event,
                                            position=position)

        self.hw_servo = self.platform.configure_servo(self.config)
        self._position = self.config['reset_position']

        if self.config['include_in_ball_search']:
            self.machine.events.add_handler("ball_search_started",
                                            self._ball_search_start)
            self.machine.events.add_handler("ball_search_stopped",
                                            self._ball_search_stop)

    def reset(self, **kwargs):
        """Go to reset position."""
        del kwargs
        self.go_to_position(self.config['reset_position'])

    def _position_event(self, position, **kwargs):
        del kwargs
        self.go_to_position(position)

    def go_to_position(self, position):
        """Move servo to position."""
        self._position = position
        if self._ball_search_started:
            return
        self._go_to_position(position)

    def _go_to_position(self, position):
        # linearly interpolate between servo limits
        position = self.config['servo_min'] + position * (
            self.config['servo_max'] - self.config['servo_min'])

        # call platform with calculated position
        self.hw_servo.go_to_position(position)

    def _ball_search_start(self, **kwargs):
        del kwargs
        # we do not touch self._position during ball search so we can reset to
        # it later
        self._ball_search_started = True
        self._ball_search_go_to_min()

    def _ball_search_go_to_min(self):
        self._go_to_position(self.config['ball_search_min'])
        self.delay.add(name="ball_search",
                       callback=self._ball_search_go_to_max,
                       ms=self.config['ball_search_wait'])

    def _ball_search_go_to_max(self):
        self._go_to_position(self.config['ball_search_max'])
        self.delay.add(name="ball_search",
                       callback=self._ball_search_go_to_min,
                       ms=self.config['ball_search_wait'])

    def _ball_search_stop(self, **kwargs):
        del kwargs
        # stop delay
        self.delay.remove("ball_search")
        self._ball_search_started = False

        # move to last position set
        self._go_to_position(self._position)
Esempio n. 19
0
class SegmentDisplayPlayer(DeviceConfigPlayer):
    """Generates texts on segment displays."""

    config_file_section = 'segment_display_player'
    show_section = 'segment_displays'
    machine_collection_name = 'segment_displays'

    __slots__ = ["delay"]

    def __init__(self, machine):
        """Initialise SegmentDisplayPlayer."""
        super().__init__(machine)
        self.delay = DelayManager(self.machine)

    # pylint: disable=too-many-branches
    def play(self, settings, context, calling_context, priority=0, **kwargs):
        """Show text on display."""
        del kwargs
        instance_dict = self._get_instance_dict(
            context)  # type: Dict[str, SegmentDisplay]
        full_context = self._get_full_context(context)

        for display, s in settings.items():
            action = s['action']
            if display not in instance_dict:
                instance_dict[display] = {}

            key = full_context + "." + display.name

            if s['key']:
                key += s['key']

            if action == "add":
                if key in instance_dict[display] and instance_dict[display][
                        key] is not True:
                    self.delay.remove(instance_dict[display][key])

                # add text
                s = TransitionManager.validate_config(
                    s, self.machine.config_validator)
                display.add_text_entry(text=s['text'],
                                       color=s['color'],
                                       flashing=self._get_flashing_type(s),
                                       flash_mask=s['flash_mask'],
                                       transition=s['transition'],
                                       transition_out=s['transition_out'],
                                       priority=priority + s['priority'],
                                       key=key)

                if s['expire']:
                    instance_dict[display][key] = self.delay.add(
                        s['expire'],
                        self._remove,
                        instance_dict=instance_dict,
                        key=key,
                        display=display)
                else:
                    instance_dict[display][key] = True
            elif action == "remove":
                self._remove(instance_dict=instance_dict,
                             key=key,
                             display=display)
            elif action == "flash":
                display.set_flashing(FlashingType.FLASH_ALL)
            elif action == "flash_match":
                display.set_flashing(FlashingType.FLASH_MATCH)
            elif action == "flash_mask":
                display.set_flashing(FlashingType.FLASH_MASK,
                                     s.get('flash_mask', ""))
            elif action == "no_flash":
                display.set_flashing(FlashingType.NO_FLASH)
            elif action == "set_color":
                if s['color']:
                    display.set_color(s['color'])
            else:
                raise AssertionError("Invalid action {}".format(action))

    @staticmethod
    def _get_flashing_type(config: dict):
        flashing = config.get('flashing', None)
        if flashing == "off":
            return FlashingType.NO_FLASH
        if flashing == "all":
            return FlashingType.FLASH_ALL
        if flashing == "match":
            return FlashingType.FLASH_MATCH
        if flashing == "mask":
            return FlashingType.FLASH_MASK

        return None

    def _remove(self, instance_dict, key, display):
        """Remove an instance by key."""
        if key in instance_dict[display]:
            display.remove_text_by_key(key)
            if instance_dict[display][key] is not True:
                self.delay.remove(instance_dict[display][key])
            del instance_dict[display][key]

    def clear_context(self, context):
        """Remove all texts."""
        instance_dict = self._get_instance_dict(context)
        for display, keys in instance_dict.items():
            for key in dict(keys).keys():
                self._remove(instance_dict=instance_dict,
                             key=key,
                             display=display)

        self._reset_instance_dict(context)

    def get_express_config(self, value):
        """Parse express config."""
        return dict(action="add", text=value)