Exemplo n.º 1
0
class BallSave(Device):

    config_section = "ball_saves"
    collection = "ball_saves"
    class_label = "ball_save"

    def __init__(self, machine, name, config, collection=None, validate=True):
        super(BallSave, self).__init__(machine, name, config, collection, validate=validate)

        self.delay = DelayManager()

        self.source_playfield = self.config["source_playfield"]

    def enable(self, **kwargs):
        self.log.debug("Enabling...")

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

        if self.config["auto_disable_time"] > 0:
            self.delay.add("disable_shoot_again", self.config["auto_disable_time"], self.disable)

        self.machine.events.post("ball_save_" + self.name + "_enabled")

    def disable(self, **kwargs):
        self.log.debug("Disabling...")
        self.machine.events.remove_handler(self._ball_drain_shoot_again)
        self.delay.remove("disable_shoot_again")

        self.machine.events.post("ball_save_" + self.name + "_disabled")

    def _ball_drain_shoot_again(self, balls, **kwargs):
        if balls <= 0:
            return {"balls": balls}

        self.machine.events.post("ball_save_" + self.name + "_shoot_again", balls=balls)

        self.log.debug("Ball drained during ball save. Requesting a new one.")
        self.source_playfield.add_ball(balls=balls)
        return {"balls": 0}
Exemplo n.º 2
0
class BallDevice(Device):
    """Base class for a 'Ball Device' in a pinball machine.

    A ball device is anything that can hold one or more balls, such as a
    trough, an eject hole, a VUK, a catapult, etc.

    Args: Same as Device.
    """

    config_section = 'balldevices'
    collection = 'balldevices'

    def __init__(self, machine, name, config, collection=None):
        self.log = logging.getLogger('BallDevice.' + name)
        super(BallDevice, self).__init__(machine, name, config, collection)

        self.delay = DelayManager()

        # set config defaults
        if 'exit_count_delay' not in self.config:
            self.config['exit_count_delay'] = ".5s"  # todo make optional
        if 'entrance_count_delay' not in self.config:
            self.config['entrance_count_delay'] = "0.5s"
        if 'eject_coil' not in self.config:
            self.config['eject_coil'] = None
        if 'eject_switch' not in self.config:
            self.config['eject_switch'] = None
        if 'entrance_switch' not in self.config:
            self.config['entrance_switch'] = None
        if 'jam_switch' not in self.config:
            self.config['jam_switch'] = None
        if 'eject_coil_hold_times' not in self.config:
            self.config['eject_coil_hold_times'] = list()
        if 'confirm_eject_type' not in self.config:
            self.config['confirm_eject_type'] = 'count'  # todo make optional?
        if 'eject_targets' not in self.config:
            self.config['eject_targets'] = ['playfield']
        else:
            self.config['eject_targets'] = Config.string_to_list(
                self.config['eject_targets'])

        if 'eject_timeouts' not in self.config:
            self.config['eject_timeouts'] = list()
        else:
            self.config['eject_timeouts'] = Config.string_to_list(
                self.config['eject_timeouts'])

        if 'confirm_eject_switch' not in self.config:
            self.config['confirm_eject_switch'] = None
        if 'confirm_eject_event' not in self.config:
            self.config['confirm_eject_event'] = None
        if 'balls_per_eject' not in self.config:
            self.config['balls_per_eject'] = 1
        if 'max_eject_attempts' not in self.config:
            self.config['max_eject_attempts'] = 0

        if 'ball_switches' in self.config:
            self.config['ball_switches'] = Config.string_to_list(
                self.config['ball_switches'])
        else:
            self.config['ball_switches'] = []

        if 'ball_capacity' not in self.config:
            self.config['ball_capacity'] = len(self.config['ball_switches'])

        if 'debug' not in self.config:
            self.config['debug'] = False

        # initialize variables

        self.balls = 0
        # Number of balls currently contained (held) in this device..

        self.eject_queue = deque()
        # Queue of the list of eject targets (ball devices) for the balls this
        # device is trying to eject.

        self.num_eject_attempts = 0
        # Counter of how many attempts to eject the current ball this device
        # has tried. Eventually it will give up.
        # todo log attemps more than one?

        self.eject_in_progress_target = None
        # The ball device this device is currently trying to eject to

        self.num_balls_requested = 0
        # The number of balls this device is in the process of trying to get.

        self.num_jam_switch_count = 0
        # How many times the jam switch has been activated since the last
        # successful eject.

        self.machine.events.add_handler('machine_reset_phase_1',
                                        self._initialize)

        self.num_balls_ejecting = 0
        # The number of balls that are currently in the process of being
        # ejected. This is either 0, 1, or whatever the balls was
        # for devices that eject all their balls at once.

        self.flag_confirm_eject_via_count = False
        # Notifies the count_balls() method that it should confirm an eject if
        # it finds a ball missing. We need this to be a standalone variable
        # since sometimes other eject methods will have to "fall back" on count
        #-based confirmations.

        self.valid = False
        self.need_first_time_count = True

        # Now configure the device
        self.configure()

    @property
    def num_balls_ejectable(self):
        return self.balls

        # todo look at upstream devices

    def configure(self, config=None):
        """Performs the actual configuration of the ball device based on the
        dictionary that was passed to it.

        Args:
            config: Python dictionary which holds the configuration settings.
        """

        # Merge in any new changes that were just passed
        if config:
            self.config.update(config)

        self.log.debug("Configuring device with: %s", self.config)

        # convert delay strings to ms ints
        if self.config['exit_count_delay']:
            self.config['exit_count_delay'] = \
                Timing.string_to_ms(self.config['exit_count_delay'])

        if self.config['entrance_count_delay']:
            self.config['entrance_count_delay'] = \
                Timing.string_to_ms(self.config['entrance_count_delay'])

        # Register for events

        # Look for eject requests for this device
        self.machine.events.add_handler(
            'balldevice_' + self.name + '_ball_eject_request', self.eject)
        # todo change event name to action_balldevice_name_eject_request

    def _initialize(self):
        # convert names to objects

        if self.config['ball_switches']:
            for i in range(len(self.config['ball_switches'])):
                self.config['ball_switches'][i] = (
                    self.machine.switches[self.config['ball_switches'][i]])

        if self.config['eject_coil']:
            self.config['eject_coil'] = (
                self.machine.coils[self.config['eject_coil']])

        if self.config['eject_switch']:
            self.config['eject_switch'] = (
                self.machine.switches[self.config['eject_switch']])

        if self.config['entrance_switch']:
            self.config['entrance_switch'] = (
                self.machine.switches[self.config['entrance_switch']])

        if self.config['jam_switch']:
            self.config['jam_switch'] = (
                self.machine.switches[self.config['jam_switch']])

        if self.config['confirm_eject_type'] == 'switch' and (
                self.config['confirm_eject_target']):
            self.config['confirm_eject_switch'] = (
                self.machine.switches[self.config['confirm_eject_switch']])

        if self.config['eject_targets']:
            for i in range(len(self.config['eject_targets'])):
                self.config['eject_targets'][i] = (
                    self.machine.balldevices[self.config['eject_targets'][i]])

        # make sure the eject timeouts list matches the length of the eject targets
        if (len(self.config['eject_timeouts']) < len(
                self.config['eject_targets'])):
            self.config['eject_timeouts'] += [None] * (
                len(self.config['eject_targets']) -
                len(self.config['eject_timeouts']))

        timeouts_list = self.config['eject_timeouts']
        self.config['eject_timeouts'] = dict()

        for i in range(len(self.config['eject_targets'])):
            self.config['eject_timeouts'][self.config['eject_targets'][i]] = (
                Timing.string_to_ms(timeouts_list[i]))
        # End code to create timeouts list -------------------------------------

        # Register switch handlers with delays for entrance & exit counts
        for switch in self.config['ball_switches']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=switch.name,
                state=1,
                ms=self.config['entrance_count_delay'],
                callback=self.count_balls)
        for switch in self.config['ball_switches']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=switch.name,
                state=0,
                ms=self.config['exit_count_delay'],
                callback=self.count_balls)
        for switch in self.config['ball_switches']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=switch.name,
                state=1,
                ms=0,
                callback=self._invalidate)
        for switch in self.config['ball_switches']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=switch.name,
                state=0,
                ms=0,
                callback=self._invalidate)

        # Configure switch handlers for jam switch activity
        if self.config['jam_switch']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=self.config['jam_switch'].name,
                state=1,
                ms=0,
                callback=self._jam_switch_handler)
            # todo do we also need to add inactive and make a smarter
            # handler?

        # Configure switch handlers for entrance switch activity
        if self.config['entrance_switch']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=self.config['entrance_switch'].name,
                state=1,
                ms=0,
                callback=self._entrance_switch_handler)
            # todo do we also need to add inactive and make a smarter
            # handler?

        # Configure event handlers to watch for target device status changes
        for target in self.config['eject_targets']:
            # Target device is requesting a ball
            self.machine.events.add_handler('balldevice_' + target.name +
                                            '_ball_request',
                                            self.eject,
                                            target=target,
                                            get_ball=True)

            # Target device is now able to receive a ball
            self.machine.events.add_handler(
                'balldevice_' + target.name + '_ok_to_receive', self._do_eject)

        # Get an initial ball count
        self.count_balls(stealth=True)

    def get_status(self, request=None):
        """Returns a dictionary of current status of this ball device.

        Args:
            request: A string of what status item you'd like to request.
                Default will return all status items.
                Options include:
                * balls
                * eject_in_progress_target
                * eject_queue

        Returns:
            A dictionary with the following keys:
                * balls
                * eject_in_progress_target
                * eject_queue
        """
        if request == 'balls':
            return self.balls
        elif request == 'eject_in_progress_target':
            return self.eject_in_progress_target
        elif request == 'eject_queue':
            return self.eject_queue,
        else:
            return {
                'balls': self.balls,
                'eject_in_progress_target': self.eject_in_progress_target,
                'eject_queue': self.eject_queue,
            }

    def status_dump(self):
        """Dumps the full current status of the device to the log."""

        if self.eject_in_progress_target:
            eject_name = self.eject_in_progress_target.name
        else:
            eject_name = 'None'

        self.log.debug("+-----------------------------------------+")
        self.log.debug("| balls: %s                  |", self.balls)
        self.log.debug("| eject_in_progress_target: %s", eject_name)
        self.log.debug("| num_balls_ejecting: %s                   |",
                       self.num_balls_ejecting)
        self.log.debug("| num_jam_switch_count: %s                     |",
                       self.num_jam_switch_count)
        self.log.debug("| num_eject_attempts: %s                   |",
                       self.num_eject_attempts)
        self.log.debug("| num_balls_requested: %s                  |",
                       self.num_balls_requested)
        self.log.debug("| eject queue: %s", self.eject_queue)
        self.log.debug("+-----------------------------------------+")

    def _invalidate(self):
        self.valid = False

    def count_balls(self, stealth=False, **kwargs):
        """Counts the balls in the device and processes any new balls that came
        in or balls that have gone out.

        Args:
            stealth: Boolean value that controls whether any events will be
                posted based on any ball count change info. If True, results
                will not be posted. If False, they will. Default is False.
            **kwargs: Catches unexpected args since this method is used as an
                event handler.
        """
        self.log.debug("Counting balls")

        self.valid = True

        if self.config['ball_switches']:

            ball_count = 0
            ball_change = 0
            previous_balls = self.balls
            self.log.debug("Previous number of balls: %s", previous_balls)

            for switch in self.config['ball_switches']:
                valid = False
                if self.machine.switch_controller.is_active(
                        switch.name, ms=self.config['entrance_count_delay']):
                    ball_count += 1
                    valid = True
                    self.log.debug("Confirmed active switch: %s", switch.name)
                elif self.machine.switch_controller.is_inactive(
                        switch.name, ms=self.config['exit_count_delay']):
                    self.log.debug("Confirmed inactive switch: %s",
                                   switch.name)
                    valid = True

                if not valid:  # one of our switches wasn't valid long enough
                    # recount will happen automatically after the time passes
                    # via the switch handler for count
                    self.log.debug(
                        "Switch '%s' changed too recently. Aborting "
                        "count & returning previous count value", switch.name)
                    self.valid = False
                    return previous_balls

            self.log.debug("Counted %s balls", ball_count)
            self.balls = ball_count

            # Figure out if we gained or lost any balls since last count?
            if self.need_first_time_count:
                self.log.debug("This is a first time count. Don't know if we "
                               "gained or lost anything.")
                # No "real" change since we didn't know previous value
                ball_change = 0
            else:
                ball_change = ball_count - previous_balls
                self.log.debug("Ball count change: %s", ball_change)

            # If we were waiting for a count-based eject confirmation, let's
            # confirm it now
            if (not ball_change and self.flag_confirm_eject_via_count
                    and self.eject_in_progress_target):
                self._eject_success()
                # todo I think this is ok with `not ball_change`. If ball_change
                # is positive that means the ball fell back in or a new one came
                # in. We can't tell the difference, but hey, we're using count-
                # based eject confirmation which sucks anyway, so them's the
                # ropes. If ball_change is negative then I don't know what the
                # heck happened.

            self.status_dump()

            if ball_change > 0:
                self._balls_added(ball_change)
            elif ball_change < 0:
                self._balls_missing(ball_change)

        else:  # this device doesn't have any ball switches
            self.log.debug("Received request to count balls, but we don't have"
                           " any ball switches. So we're just returning the"
                           " old count.")
            if self.need_first_time_count:
                self.balls = 0
            # todo add support for virtual balls

        self.need_first_time_count = False

        if self.balls < 0:
            self.balls = 0
            self.log.warning("Number of balls contained is negative (%s).",
                             self.balls)
            # This should never happen

        return self.balls

    def _balls_added(self, balls):
        # Called when ball_count finds new balls in this device

        # If this device received a new ball while a current eject was in
        # progress, let's try to figure out whether an actual new ball entered
        # or whether the current ball it was trying to eject fell back in.
        # Note we can only do this for devices that have a jam switch.

        if (self.eject_in_progress_target and self.config['jam_switch']
                and self.num_jam_switch_count > 1):
            # If the jam switch count is more than 1, we assume the ball it was
            # trying to eject fell back in.
            self.log.debug("Jam switch count: %s. Assuming eject failed.",
                           self.num_jam_switch_count)
            self.eject_failed()
            return

        elif ((self.eject_in_progress_target and self.config['jam_switch']
               and self.num_jam_switch_count == 1)
              or not self.eject_in_progress_target):
            # If there's an eject in progress with a jam switch count of only 1,
            # or no eject in progress, we assume this was a valid new ball.

            # If this device is not expecting any balls, we assuming this one
            # came from the playfield. Post this event so the playfield can keep
            # track of how many balls are out.
            if not self.num_balls_requested:
                self.machine.events.post('balldevice_captured_from_playfield',
                                         balls=balls)

            # Post the relay event as other handlers might be looking for to act
            # on the ball entering this device.
            self.machine.events.post_relay('balldevice_' + self.name +
                                           '_ball_enter',
                                           balls=balls,
                                           callback=self._balls_added_callback)

    def _balls_added_callback(self, balls, **kwargs):
        # Callback event for the balldevice_<name>_ball_enter relay event

        # If we still have balls here, that means that no one claimed them, so
        # essentially they're "stuck." So we just eject them... unless this
        # device is tagged 'trough' in which case we let it keep them.
        if balls and 'trough' not in self.tags:
            self.eject(balls)

        self.log.debug("In the balls added callback")
        self.log.debug("Eject queue: %s", self.eject_queue)

        #todo we should call the ball controller in case it wants to eject a
        # ball from a different device?

        if self.eject_queue:
            self.log.debug("A ball was added and we have an eject_queue, so "
                           "we're going to process that eject now.")
            self._do_eject()

    def _balls_missing(self, balls):
        # Called when ball_count finds that balls are missing from this device

        self.log.warning("%s ball(s) missing from device", abs(balls))

        # todo dunno if there's any action we should take here? This should
        # never happen unless someone takes the glass off and steals a ball or
        # unless there's a ball switch or a ball randomly falls out of a device?

    def is_full(self):
        """Checks to see if this device is full, meaning it is holding either
        the max number of balls it can hold, or it's holding all the known balls
        in the machine.

        Returns: True or False

        """
        if self.balls == self.ball_capacity:
            return True
        elif self.balls == self.machine.ball_controller.num_balls_known:
            return True
        else:
            return False

    def _jam_switch_handler(self):
        # The device's jam switch was just activated.
        # This method is typically used with trough devices to figure out if
        # balls fell back in.

        self.num_jam_switch_count += 1
        self.log.debug("Ball device %s jam switch hit. New count: %s",
                       self.name, self.num_jam_switch_count)

    def _entrance_switch_handler(self):
        # A ball has triggered this device's entrance switch

        if not self.config['ball_switches']:
            self.balls += 1
            self._balls_added(1)

    def get_additional_ball_capacity(self):
        """Returns an integer value of the number of balls this device can
            receive. A return value of 0 means that this device is full and/or
            that it's not able to receive any balls at this time due to a
            current eject_in_progress.
        """

        if self.num_balls_ejecting:
            # This device is in the process of ejecting a ball, so it shouldn't
            # receive any now.

            return 0

        if self.config['ball_capacity'] - self.balls < 0:
            self.log.warning("Device reporting more balls contained than its "
                             "capacity.")

        return self.config['ball_capacity'] - self.balls

    def request_ball(self, balls=1):
        """Request that one or more balls is added to this device.

        Args:
            balls: Integer of the number of balls that should be added to this
                device. A value of -1 will cause this device to try to fill
                itself.

        Note that a device will never request more balls than it can hold. Also,
        only devices that are fed by other ball devices (or a combination of
        ball devices and diverters) can make this request. e.g. if this device
        is fed from the playfield, then this request won't work.
        """

        self.log.debug("In request_ball. balls: %s", balls)

        if self.eject_in_progress_target:
            self.log.debug("Received request to request a ball, but we can't "
                           "since there's an eject in progress.")
            return False

        if not self.get_additional_ball_capacity():
            self.log.debug("Received request to request a ball, but we can't "
                           "since it's not ok to receive.")
            return False

        # How many balls are we requesting?
        remaining_capacity = (self.config['ball_capacity'] - self.balls -
                              self.num_balls_requested)

        if remaining_capacity < 0:
            remaining_capacity = 0

        # Figure out how many balls we can request
        if balls == -1 or balls > remaining_capacity:
            balls = remaining_capacity

        if not balls:
            return 0

        self.num_balls_requested += balls

        self.log.debug("Requesting Ball(s). Balls=%s", balls)

        # set event handler to watch for receiving a ball
        self.machine.events.add_handler(
            'balldevice_' + self.name + '_ball_enter',
            self._requested_ball_received)

        self.machine.events.post('balldevice_' + self.name + '_ball_request',
                                 balls=balls)

        return balls

    def _requested_ball_received(self, balls):
        # Responds to its own balldevice_<name>_ball_enter relay event
        # We do this since we need something to act on the balls being received,
        # otherwise it would think they were unexpected and eject them.

        # Figure out how many of the new balls were requested
        unexpected_balls = balls - self.num_balls_requested
        if unexpected_balls < 0:
            unexpected_balls = 0

        # Figure out how many outstanding ball requests we have
        self.num_balls_requested -= balls

        if self.num_balls_requested <= 0:
            self.num_balls_requested = 0
            self.machine.events.remove_handler(self._requested_ball_received)

        return {'balls': unexpected_balls}

    def _cancel_request_ball(self):
        self.machine.events.post('balldevice_' + self.name +
                                 '_cancel_ball_request')

    def _eject_event_handler(self):
        # We received the event that should eject this ball.

        if not self.balls:
            self.request_ball()

    def stop(self):
        """Stops all activity in this device.

        Cancels all pending eject requests. Cancels eject confirmation checks.
        """
        self.log.debug("Stopping all activity via stop()")
        self.eject_in_progress_target = None
        self.eject_queue = deque()
        self.num_jam_switch_count = 0

        # todo jan19 anything to add here?

        self._cancel_eject_confirmation()
        self.count_balls()  # need this since we're canceling the eject conf

    def eject(self, balls=1, target=None, timeout=None, get_ball=False):
        """Ejects one or more balls from the device.

        Args:
            balls: Integer of the number of balls to eject. Default is 1.
            target: Optional target that should receive the ejected ball(s),
                either a string name of the ball device or a ball device
                object. Default is None which means this device will eject this
                ball to the first entry in the eject_targets list.
            timeout: How long (in ms) to wait for the ball to make it into the
                target device after the ball is ejected. A value of ``None``
                means the default timeout from the config file will be used. A
                value of 0 means there is no timeout.
            get_ball: Boolean as to whether this device should attempt to get
                a ball to eject if it doesn't have one. Default is False.

        Note that if this device's 'balls_per_eject' configuration is more than
        1, then it will eject the nearest number of balls it can.
        """

        # Figure out the eject target

        if balls < 1:
            self.log.warning(
                "Received request to eject %s balls, which doesn't"
                " make sense. Ignoring...")
            return False

        if not target:
            target = self.config['eject_targets'][0]

        elif type(target) is str:
            target = self.machine.balldevices[target]

        self.log.debug("Received request to eject %s ball(s) to target '%s'",
                       balls, target.name)

        if not self.config['eject_coil']:
            self.log.debug("This device has no eject coil, so we're assuming "
                           "a manual eject by the player.")
            timeout = 0  # unlimited since who knows how long the player waits

        # Set the timeout for this eject
        if timeout is None:
            timeout = self.config['eject_timeouts'][target]

        # Set the number of balls to eject

        balls_to_eject = balls

        if balls_to_eject > self.balls and not get_ball:
            balls_to_eject = self.balls

        # Add one entry to the eject queue for each ball that's ejecting
        self.log.debug('Adding %s ball(s) to the eject_queue.', balls_to_eject)
        for i in range(balls_to_eject):
            self.eject_queue.append((target, timeout))

        self._do_eject()

    def eject_all(self, target=None):
        """Ejects all the balls from this device

        Args:
            target: The string or BallDevice target for this eject. Default of
                None means `playfield`.

        Returns:
            True if there are balls to eject. False if this device is empty.
        """
        self.log.debug("Ejecting all balls")
        if self.balls > 0:
            self.eject(balls=self.balls, target=target)
            return True
        else:
            return False

    def _do_eject(self, **kwargs):
        # Performs the actual eject attempts and sets up eject confirmations
        # **kwargs just because this method is registered for various events
        # which might pass them.

        self.log.debug(
            "Entering _do_eject(). Current in progress target: %s. "
            "Eject queue: %s", self.eject_in_progress_target, self.eject_queue)

        if self.eject_in_progress_target:
            self.log.debug(
                "Current eject in progress with target: %s. Aborting"
                " eject.", self.eject_in_progress_target)
            return False  # Don't want to get in the way of a current eject

        if not self.balls and not self.num_balls_requested:
            self.log.debug("Don't have any balls. Requesting one.")
            self.request_ball()
            # Once the ball is delivered then the presence of the eject_queue
            # will re-start this _do_eject() process
            return False

        elif self.eject_queue and self.balls:
            self.log.debug("We have an eject queue: %s", self.eject_queue)

            target = self.eject_queue[0][0]  # first item, first part of tuple

            if not target.get_additional_ball_capacity():
                self.log.debug(
                    "Target device '%s' is not able to receive now. "
                    "Aborting eject. Will retry when target can "
                    "receive.", target.name)
                return False

            else:
                self.log.debug("Proceeding with the eject")

                self.log.debug("Setting eject_in_progress_target, timeout")
                self.eject_in_progress_target, timeout = (
                    self.eject_queue.popleft())

                self.num_eject_attempts += 1

                if self.config['jam_switch']:
                    self.num_jam_switch_count = 0
                    if self.machine.switch_controller.is_active(
                            self.config['jam_switch'].name):
                        self.num_jam_switch_count += 1
                        # catches if the ball is blocking the switch to
                        # begin with, todo we have to get smart here

                if self.config['balls_per_eject'] == 1:
                    self.num_balls_ejecting = 1
                else:
                    self.num_balls_ejecting = self.balls

                self.machine.events.post('balldevice_' + self.name +
                                         '_ball_eject_attempt',
                                         balls=self.num_balls_ejecting,
                                         target=self.eject_in_progress_target,
                                         timeout=timeout,
                                         callback=self._fire_eject_coil)
                # Fire the coil via a callback in case there are events in the
                # queue. This ensures that the coil pulse happens when this
                # event is posted instead of firing right away.

    def _eject_status(self):
        if self.config['debug']:

            self.log.info("DEBUG: Eject duration: %ss. Target: %s",
                          round(time.time() - self.eject_start_time, 2),
                          self.eject_in_progress_target.name)

    def _fire_eject_coil(self, target, timeout=None, **kwargs):

        self._setup_eject_confirmation(self.eject_in_progress_target, timeout)

        self.balls -= self.num_balls_ejecting

        try:
            self.config['eject_coil'].pulse()
            # todo add support for hold coils with variable release times
            self.log.debug(
                "Firing eject coil. num_balls_ejecting: %s. New "
                "balls: %s.", self.num_balls_ejecting, self.balls)
        except AttributeError:
            self.log.debug("Waiting for player to manually eject ball(s)")

    def _setup_eject_confirmation(self, target=None, timeout=0):
        # Called after an eject request to confirm the eject. The exact method
        # of confirmation depends on how this ball device has been configured
        # and what target it's ejecting to

        # args are target device and timeout in ms

        self.log.debug("Setting up eject confirmation.")

        if self.config['debug']:
            self.eject_start_time = time.time()
            self.log.info("DEBUG: Eject start time: %s", self.eject_start_time)
            self.machine.events.add_handler('timer_tick', self._eject_status)

        self.flag_confirm_eject_via_count = False

        if self.config['confirm_eject_type'] == 'target':

            if not target:
                self.log.error("we got an eject confirmation request with no "
                               "target. This shouldn't happen. Post to the "
                               "forum if you see this.")
                raise Exception("we got an eject confirmation request with no "
                                "target. This shouldn't happen. Post to the "
                                "forum if you see this.")

            if timeout:
                # set up the delay to check for the failed the eject
                self.delay.add(name='target_eject_confirmation_timeout',
                               ms=timeout,
                               callback=self.eject_failed)

            self.log.debug(
                "Will confirm eject via ball entry into '%s' with a "
                "confirmation timeout of %sms", target.name, timeout)

            # watch for ball entry event on the target device
            # Note this must be higher priority than the failed eject handler
            self.machine.events.add_handler('balldevice_' + target.name +
                                            '_ball_enter',
                                            self._eject_success,
                                            priority=1000)

        elif self.config['confirm_eject_type'] == 'switch':
            self.log.debug("Will confirm eject via activation of switch '%s'",
                           self.config['confirm_eject_switch'].name)
            # watch for that switch to activate momentarily
            # todo add support for a timed switch here
            self.machine.switch_controller.add_switch_handler(
                switch_name=self.config['confirm_eject_switch'].name,
                callback=self._eject_success,
                state=1,
                ms=0)

        elif self.config['confirm_eject_type'] == 'event':
            self.log.debug("Will confirm eject via posting of event '%s'",
                           self.config['confirm_eject_event'])
            # watch for that event
            self.machine.events.add_handler(self.config['confirm_eject_event'],
                                            self._eject_success)

        elif self.config['confirm_eject_type'] == 'count':
            # todo I think we need to set a delay to recount? Because if the
            # ball re-enters in less time than the exit delay, then the switch
            # handler won't have time to reregister it.
            self.log.debug("Will confirm eject via recount of ball switches.")
            self.flag_confirm_eject_via_count = True

        elif (self.config['confirm_eject_type'] == 'playfield'
              or target == self.machine.balldevices['playfield']):

            if self.machine.playfield.ok_to_confirm_ball_via_playfield_switch(
            ):
                self.log.debug("Will confirm eject when a playfield switch is "
                               "hit")
                self.machine.events.add_handler('sw_playfield_active',
                                                self._eject_success)
            else:
                self.log.debug("Will confirm eject via recount of ball "
                               "switches.")
                self.flag_confirm_eject_via_count = True

        else:
            # If there's no confirm eject type specified, then we'll just
            # confirm it right away.
            self.log.debug("No eject confirmation configured. Confirming now.")
            self._eject_success()
            return

    def _cancel_eject_confirmation(self):
        self.log.debug("Canceling eject confirmations")
        self.eject_in_progress_target = None
        self.num_eject_attempts = 0

        # Remove any event watching for success
        self.machine.events.remove_handler(self._eject_success)

        # Remove any switch handlers
        if self.config['confirm_eject_type'] == 'switch':
            self.machine.switch_controller.remove_switch_handler(
                switch_name=self.config['confirm_eject_switch'].name,
                callback=self._eject_success,
                state=1,
                ms=0)

        # Remove any delays that were watching for failures
        self.delay.remove('target_eject_confirmation_timeout')

    def _eject_success(self, **kwargs):
        # We got an eject success for this device.
        # **kwargs because there are many ways to get here, some with kwargs and
        # some without. Also, since there are many ways we can get here, let's
        # first make sure we actually had an eject in progress

        self.log.debug("In _eject_success. Eject target: %s",
                       self.eject_in_progress_target)

        if self.config['debug']:
            self.log.info("DEBUG: Eject duration: %ss",
                          time.time() - self.eject_start_time)
            self.machine.events.remove_handler(self._eject_status)

        # Reset flags for next time
        self.flag_confirm_eject_via_count = False
        self.flag_pending_playfield_confirmation = False

        if self.eject_in_progress_target:
            self.log.debug("Confirmed successful eject")

            # Creat a temp attribute here so the real one is None when the
            # event is posted.
            eject_target = self.eject_in_progress_target
            self.num_jam_switch_count = 0
            self.num_eject_attempts = 0
            self.eject_in_progress_target = None
            balls_ejected = self.num_balls_ejecting
            self.num_balls_ejecting = 0

            # todo cancel post eject check delay

            self.machine.events.post('balldevice_' + self.name +
                                     '_ball_eject_success',
                                     balls=balls_ejected,
                                     target=eject_target)

        else:  # this should never happen
            self.log.warning(
                "We got to '_eject_success()' but no eject was in "
                "progress. Just FYI that something's weird.")

        self._cancel_eject_confirmation()

        if self.eject_queue:
            self._do_eject()

    def eject_failed(self, retry=True, force_retry=False):
        """Marks the current eject in progress as 'failed.'

        Note this is not typically a method that would be called manually. It's
        called automatically based on ejects timing out or balls falling back
        into devices while they're in the process of ejecting. But you can call
        it manually if you want to if you have some other way of knowing that
        the eject failed that the system can't figure out on it's own.

        Args:
            retry: Boolean as to whether this eject should be retried. If True,
                the ball device will retry the eject again as long as the
                'max_eject_attempts' has not been exceeded. Default is True.
            force_retry: Boolean that forces a retry even if the
                'max_eject_attempts' has been exceeded. Default is False.

        """

        self.log.debug("Eject Failed")

        # Put the current target back in the queue so we can try again
        # This sets up the timeout back to the default. Wonder if we should
        # add some intelligence to make this longer or shorter?
        self.eject_queue.appendleft(
            (self.eject_in_progress_target,
             self.config['eject_timeouts'][self.eject_in_progress_target]))

        # Reset the stuff that showed a current eject in progress
        self.eject_in_progress_target = None
        self.num_balls_ejecting = 0
        self.num_eject_attempts += 1

        if self.config['debug']:
            self.log.info("DEBUG: Eject duration: %ss",
                          time.time() - self.eject_start_time)

        self.machine.events.post('balldevice_' + self.name +
                                 '_ball_eject_failed',
                                 num_attempts=self.num_eject_attempts)

        self._cancel_eject_confirmation()

        if (retry and
            (not self.config['max_eject_attempts']
             or self.num_eject_attempts < self.config['max_eject_attempts'])):
            self._do_eject()

        elif force_retry:
            self._do_eject()

        else:
            self._eject_permanently_failed()

    def _eject_permanently_failed(self):
        self.log.warning("Eject failed %s times. Permanently giving up.",
                         self.config['max_eject_attempts'])
        self.machine.events.post('balldevice_' + self.name +
                                 'ball_eject_permanent_failure')

    def _ok_to_receive(self):
        # Post an event announcing that it's ok for this device to receive a
        # ball

        self.machine.events.post('balldevice_' + self.name + '_ok_to_receive',
                                 balls=self.get_additional_ball_capacity())
Exemplo n.º 3
0
class Playfield(BallDevice):

    config_section = 'playfields'
    collection = 'playfields'
    class_label = 'playfield'

    # noinspection PyMissingConstructor
    def __init__(self, machine, name, config, collection=None, validate=True):
        self.log = logging.getLogger('playfield')

        self.machine = machine
        self.name = name.lower()
        self.tags = list()
        self.label = None
        self.debug = False
        self.config = dict()

        if validate:
            self.config = self.machine.config_processor.process_config2(
                self.config_section, config, self.name)
        else:
            self.config = config

        if self.config['debug']:
            self.debug = True
            self.log.debug("Enabling debug logging for this device")
            self.log.debug("Configuring device with settings: '%s'", config)

        self.tags = self.config['tags']
        self.label = self.config['label']

        self.delay = DelayManager()

        self.machine.ball_devices[name] = self

        if 'default' in self.config['tags']:
            self.machine.playfield = self

        # Attributes
        self._balls = 0
        self.num_balls_requested = 0
        self.queued_balls = list()
        self._playfield = True

        # Set up event handlers

        # Watch for balls added to the playfield
        for device in self.machine.ball_devices:
            for target in device.config['eject_targets']:
                if target == self.name:
                    self.machine.events.add_handler(
                        event='balldevice_' + device.name +
                        '_ball_eject_success',
                        handler=self._source_device_eject_success)
                    self.machine.events.add_handler(
                        event='balldevice_' + device.name +
                        '_ball_eject_failed',
                        handler=self._source_device_eject_failed)
                    self.machine.events.add_handler(
                        event='balldevice_' + device.name +
                        '_ball_eject_attempt',
                        handler=self._source_device_eject_attempt)
                    break

        # Watch for balls removed from the playfield
        self.machine.events.add_handler('balldevice_captured_from_' + self.name,
                                        self._ball_removed_handler)

        # Watch for any switch hit which indicates a ball on the playfield
        self.machine.events.add_handler('sw_' + self.name + '_active',
                                        self.playfield_switch_hit)

        self.machine.events.add_handler('init_phase_2',
                                        self._initialize)

    def _initialize(self):
        self.ball_controller = self.machine.ball_controller

        for device in self.machine.playfield_transfers:
            if device.config['eject_target'] == self.name:
                self.machine.events.add_handler(
                    event='balldevice_' + device.name +
                    '_ball_eject_success',
                    handler=self._source_device_eject_success)
                self.machine.events.add_handler(
                    event='balldevice_' + device.name +
                    '_ball_eject_attempt',
                    handler=self._source_device_eject_attempt)

    @property
    def balls(self):
        return self._balls

    @balls.setter
    def balls(self, balls):

        prior_balls = self._balls
        ball_change = balls - prior_balls

        if ball_change:
            self.log.debug("Ball count change. Prior: %s, Current: %s, Change:"
                           " %s", prior_balls, balls, ball_change)

        if balls > 0:
            self._balls = balls
            #self.ball_search_schedule()
        elif balls == 0:
            self._balls = 0
            #self.ball_search_disable()
        else:
            self.log.warning("Playfield balls went to %s. Resetting to 0, but "
                             "FYI that something's weird", balls)
            self._balls = 0
            #self.ball_search_disable()

        self.log.debug("New Ball Count: %s. (Prior count: %s)",
                       self._balls, prior_balls)

        if ball_change > 0:
            self.machine.events.post_relay('balldevice_' + self.name +
                                           '_ball_enter', balls=ball_change)

        if ball_change:
            self.machine.events.post(self.name + '_ball_count_change',
                                     balls=balls, change=ball_change)

    def count_balls(self, **kwargs):
        """Used to count the number of balls that are contained in a ball
        device. Since this is the playfield device, this method always returns
        zero.

        Returns: 0

        """
        return 0

    def get_additional_ball_capacity(self):
        """Used to find out how many more balls this device can hold. Since this
        is the playfield device, this method always returns 999.

        Returns: 999

        """
        return 999

    def add_ball(self, balls=1, source_name=None, source_device=None,
                 trigger_event=None, player_controlled=False):
        """Adds live ball(s) to the playfield.

        Args:
            balls: Integer of the number of balls you'd like to add.
            source_name: Optional string name of the ball device you'd like to
                add the ball(s) from.
            source_device: Optional ball device object you'd like to add the
                ball(s) from.
            trigger_event: The optional name of an event that MPF will wait for
                before adding the ball into play. Typically used with player-
                controlled eject tag events. If None, the ball will be added
                immediately.
            player_controlled: Boolean which specifies whether this event is
                player controlled. (See not below for details)

        Returns:
            True if it's able to process the add_ball() request, False if it
            cannot.

        Both source_name and source_device args are included to give you two
        options for specifying the source of the ball(s) to be added. You don't
        need to supply both. (it's an "either/or" thing.) Both of these args are
        optional, so if you don't supply them then MPF will look for a device
        tagged with 'ball_add_live'. If you don't provide a source and you don't
        have a device with the 'ball_add_live' tag, MPF will quit.

        This method does *not* increase the game controller's count of the
        number of balls in play. So if you want to add balls (like in a
        multiball scenario), you need to call this method along with
        ``self.machine.game.add_balls_in_play()``.)

        MPF tracks the number of balls in play separately from the actual balls
        on the playfield because there are numerous situations where the two
        counts are not the same. For example, if a ball is in a VUK while some
        animation is playing, there are no balls on the playfield but still one
        ball in play, or if the player has a two-ball multiball and they shoot
        them both into locks, there are still two balls in play even though
        there are no balls on the playfield. The opposite can also be true,
        like when the player tilts then there are still balls on the playfield
        but no balls in play.

        Explanation of the player_controlled parameter:

        Set player_controlled to True to indicate that MPF should wait for the
        player to eject the ball from the source_device rather than firing a
        coil. The logic works like this:

        If the source_device does not have an eject_coil defined, then it's
        assumed that player_controlled is the only option. (e.g. this is a
        traditional plunger.) If the source_device does have an eject_coil
        defined, then there are two ways the eject could work. (1) there could
        be a "launch" button of some kind that's used to fire the eject coil,
        or (2) the device could be the auto/manual combo style where there's a
        mechanical plunger but also a coil which can eject the ball.

        If player_controlled is true and the device has an eject_coil, MPF will
        look for the player_controlled_eject_tag and eject the ball when a
        switch with that tag is activated.

        If there is no player_controlled_eject_tag, MPF assumes it's a manual
        plunger and will wait for the ball to disappear from the device based
        on the device's ball count decreasing.

        """
        if balls < 1:
            self.log.error("Received request to add %s balls, which doesn't "
                           "make sense. Not adding any balls...")
            return False

        # Figure out which device we'll get a ball from

        if source_device:
            pass
        elif source_name and source_name in self.machine.ball_devices:
            source_device = self.machine.ball_devices[source_name]
        else:
            for device in self.machine.ball_devices.items_tagged('ball_add_live'):
                if self in device.config['eject_targets']:
                    source_device = device
                    break

        if not source_device:
            self.log.critical("Received request to add a ball to the playfield"
                              ", but no source device was passed and no ball "
                              "devices are tagged with 'ball_add_live'. Cannot"
                              " add a ball.")
            return False

        self.log.debug("Received request to add %s ball(s). Source device: %s."
                       " Wait for event: %s. Player-controlled: %s", balls,
                       source_device.name, trigger_event, player_controlled)

        if player_controlled:
            source_device.setup_player_controlled_eject(balls=balls,
                target=self, trigger_event=trigger_event)
        else:
            source_device.eject(balls=balls, target=self, get_ball=True)

        return True

    def playfield_switch_hit(self):
        """A switch tagged with '<this playfield name>_active' was just hit,
        indicating that there is at least one ball on the playfield.

        """
        if not self.balls:

            if not self.num_balls_requested:
                if self.machine.config['machine']['glass_off_mode']:
                    self.log.debug("Playfield_active switch hit with no balls "
                                   "expected. glass_off_mode is enabled, so "
                                   "this will be ignored.")
                else:
                    self.log.debug("Playfield_active switch hit with no balls "
                                   "expected. glass_off_mode is not enabled, "
                                   "setting playfield ball count to 1")

                    self.balls = 1
                    self.machine.events.post('unexpected_ball_on_' + self.name)

    def _ball_added_handler(self, balls):
        self.log.debug("%s ball(s) added to the playfield", balls)
        self.balls += balls

    def _ball_removed_handler(self, balls):
        self.log.debug("%s ball(s) removed from the playfield", balls)
        self.balls -= balls

    def _source_device_eject_attempt(self, balls, target, **kwargs):
        # A source device is attempting to eject a ball. We need to know if it's
        # headed to the playfield.
        if target == self:
            self.log.debug("A source device is attempting to eject %s ball(s)"
                           " to the playfield.", balls)
            self.num_balls_requested += balls

    def _source_device_eject_failed(self, balls, target, **kwargs):
        # A source device failed to eject a ball. We need to know if it was
        # headed to the playfield.
        if target == self:
            self.log.debug("A source device has failed to eject %s ball(s)"
                           " to the playfield.", balls)
            self.num_balls_requested -= balls

    def _source_device_eject_success(self, balls, target):
        # A source device has just confirmed that it has successfully ejected a
        # ball. Note that we don't care what type of confirmation it used.
        # (Playfield switch hit, count of its ball switches, etc.)

        if target == self:
            self.log.debug("A source device has confirmed it's ejected %s "
                           "ball(s) to the playfield.", balls)
            self.balls += balls
            self.num_balls_requested -= balls

            if self.num_balls_requested < 0:
                self.log.critical("num_balls_requested is %s, which doesn't "
                                  "make sense. Quitting...",
                                  self.num_balls_requested)
                raise Exception("num_balls_requested is %s, which doesn't make "
                                "sense. Quitting...", self.num_balls_requested)


    def ok_to_confirm_ball_via_playfield_switch(self):
        """Used to check whether it's ok for a ball device which ejects to the
        playfield to confirm its eject via a playfield switch being hit.

        Returns: True or False

        Right now this is simple. If there are no playfield balls, then any
        playfield switch hit is assumed to be from the newly-ejected ball. If
        there are other balls on the playfield, then we can't use this
        confirmation method since we don't know whether a playfield switch hit
        is from the newly-ejected ball(s) or a current previously-live
        playfield ball.

        """
        if not self.balls:
            return True
        else:
            return False

        # todo look for other incoming balls?

    # BALL SEARCH --------------------------------------------------------------

    # todo make ball search work with plunger lanes with no switches. i.e. we
    # don't want ball search to start until a switch is hit?

    def ball_search_schedule(self, secs=None, force=False):
        """Schedules a ball search to start. By default it will schedule it
        based on the time configured in the machine configuration files.

        If a ball search is already scheduled, this method will reset that
        schedule to the new time passed.

        Args:
            secs: Schedules the ball search that many secs from now.
            force : Boolean to force a ball search. Set True to force a ball
                search. Otherwise it will only schedule it if
                self.flag_no_ball_search is False. Default is False

        """
        if self.machine.config['ball_search']:
            if not self.flag_no_ball_search or force is True:
                if secs is not None:
                    start_ms = secs * 1000
                else:
                    start_ms = (self.machine.config['ball_search']
                        ['secs until ball search start'] * 1000)
                self.log.debug("Scheduling a ball search for %s secs from now",
                               start_ms / 1000.0)
                self.delay.reset("ball_search_start",
                                 ms=start_ms,
                                 callback=self.ball_search_begin)

    def ball_search_disable(self):
        """Disables ball search.

        Note this is used to prevent a future ball search from happening (like
        when all balls become contained). This method is not used to cancel an
        existing ball search. (Use `ball_search_end` for that.)

        """
        self.log.debug("Disabling Ball Search")
        self.delay.remove('ball_search_start')

    def ball_search_begin(self, force=False):
        """Begin the ball search process"""
        if not self.flag_no_ball_search:
            self.log.debug("Received request to start ball search")
            # ball search should only start if we have uncontained balls
            if self.balls or force:
                # todo add audit
                self.flag_ball_search_in_progress = True
                self.machine.events.post("ball_search_begin_phase1")
                # todo set delay to start phase 2

            else:
                self.log.debug("We got request to start ball search, but we "
                               "have no balls uncontained. WTF??")

    def ball_search_failed(self):
        """Ball Search did not find the ball."""
        self.log.debug("Ball Search failed to find a ball. Disabling.")
        self.ball_search_end()
        self.ball_lost()

    def ball_search_end(self):
        """End the ball search, either because we found the ball or
        are giving up."""
        self.log.debug("Ball search ending")
        self.flag_ball_search_in_progress = False
        self.machine.events.post("ball_search_end")
        # todo cancel the delay for phase 2 if we had one

    def ball_lost(self):
        """Mark a ball as lost"""
        self.num_balls_known = self.balls
        self.num_balls_missing = (self.machine.config['machine']
                                  ['balls installed'] - self.balls)
        self.num_balls_live = 0
        # since desired count doesn't change, this will relaunch them
        self.log.debug("Ball(s) Marked Lost. Known: %s, Missing: %s",
                       self.num_balls_known, self.num_balls_missing)

        # todo audit balls lost

    def ball_found(self, num=1):
        """Used when a previously missing ball is found. Updates the balls
        known and balls missing variables.

        Parameters
        ----------

        num : int
            Specifies how many balls have been found. Default is 1.

        """
        self.log.debug("HEY!! We just found %s lost ball(s).", num)
        self.num_balls_known += num
        self.num_balls_missing -= num
        self.log.debug("New ball counts. Known: %s, Missing: %s",
                       self.num_balls_known, self.num_balls_missing)
        self.ball_update_all_counts()  # is this necessary? todo

    def eject(self, *args, **kwargs):
        pass

    def eject_all(self, *args, **kwargs):
        pass
Exemplo n.º 4
0
Arquivo: shots.py Projeto: xsdk/mpf
class SequenceShot(Shot):

    def __init__(self, machine, name, config):
        """SequenceShot is where you need certain switches to be hit in the
        right order, possibly within a time limit.

        Subclass of `Shot`

        Parameters
        ----------

        machine : object
            The MachineController object

        name : str
            The name of this shot

        config : dict
            The dictionary that holds the configuration for this shot.

        """
        super(SequenceShot, self).__init__(machine, name, config)

        self.delay = DelayManager()

        self.progress_index = 0
        """Tracks how far along through this sequence the current shot is."""

        self.configure()
        self.enable()
        self.active_delay = False

    def configure(self):
        """Configures the shot."""

        # convert our switches config to a list
        if 'Switches' in self.config:
            self.config['Switches'] = \
                self.machine.string_to_list(self.config['Switches'])

        # convert our timout to ms
        if 'Time' in self.config:
            self.config['Time'] = Timing.string_to_ms(self.config['Time'])
        else:
            self.config['Time'] = 0

    def enable(self):
        """Enables the shot. If it's not enabled, the switch handlers aren't
        active and the shot event will not be posted."""
        self.log.debug("Enabling")
        # create the switch handlers
        for switch in self.config['Switches']:
            self.machine.switch_controller.add_switch_handler(
                switch, self._switch_handler, return_info=True)
        self.progress_index = 0
        self.active = True

    def disable(self):
        """Disables the shot. If it's disabled, the switch handlers aren't
        active and the shot event will not be posted."""
        self.log.debug("Disabling")
        self.active = False
        for switch in self.config['Switches']:
            self.machine.switch_controller.remove_switch_handler(
                switch, self.switch_handler)
        self.progress_index = 0

    def _switch_handler(self, switch_name, state, ms):
        # does this current switch meet the next switch in the progress index?
        if switch_name == self.config['Switches'][self.progress_index]:

            # are we at the end?
            if self.progress_index == len(self.config['Switches']) - 1:
                self.confirm_shot()
            else:
                # does this shot specific a time limit?
                if self.config['Time']:
                    # do we need to set a delay?
                    if not self.active_delay:
                        self.delay.reset(name='shot_timer',
                                         ms=self.config['Time'],
                                         callback=self.reset)
                        self.active_delay = True

                # advance the progress index
                self.progress_index += 1

    def confirm_shot(self):
        """Called when the shot is complete to confirm and reset it."""
        # kill the delay
        self.delay.remove('shot_timer')
        # reset our shot
        self.reset()
        # post the success event
        if self.machine.auditor.enabled:
                self.machine.auditor.audit('Shots', self.name)
        self.machine.events.post('shot_' + self.name)

    def reset(self):
        """Resets the progress without disabling the shot."""
        self.log.debug("Resetting this shot")
        self.progress_index = 0
        self.active_delay = False
Exemplo n.º 5
0
class BallDevice(Device):
    """Base class for a 'Ball Device' in a pinball machine.

    A ball device is anything that can hold one or more balls, such as a
    trough, an eject hole, a VUK, a catapult, etc.

    Args: Same as Device.
    """

    config_section = 'ball_devices'
    collection = 'ball_devices'
    class_label = 'ball_device'

    def __init__(self, machine, name, config, collection=None, validate=True):
        super(BallDevice, self).__init__(machine, name, config, collection,
                                         validate=validate)

        self.delay = DelayManager()

        if self.config['ball_capacity'] is None:
            self.config['ball_capacity'] = len(self.config['ball_switches'])

        # initialize variables

        self.balls = 0
        """Number of balls currently contained (held) in this device."""

        self.eject_queue = deque()
        """ Queue of the list of eject targets (ball devices) for the balls this
        device is trying to eject.
        """

        self.num_eject_attempts = 0
        """ Counter of how many attempts to eject the current ball this device
        has tried. Eventually it will give up.
        """
        # todo log attemps more than one?

        self.eject_in_progress_target = None
        """The ball device this device is currently trying to eject to."""

        self.num_balls_requested = 0
        """The number of balls this device is in the process of trying to get.
        """

        self.num_balls_in_transit = 0
        """The number of balls in transit to this device.
        """

        self.num_jam_switch_count = 0
        """How many times the jam switch has been activated since the last
        successful eject.
        """

        self.machine.events.add_handler('machine_reset_phase_1',
                                        self._initialize)

        self.machine.events.add_handler('machine_reset_phase_2',
                                        self._initialize2)

        self.num_balls_ejecting = 0
        """ The number of balls that are currently in the process of being
        ejected. This is either 0, 1, or whatever the balls was
        for devices that eject all their balls at once.
        """

        self.flag_confirm_eject_via_count = False
        """Notifies the count_balls() method that it should confirm an eject if
        it finds a ball missing. We need this to be a standalone variable
        since sometimes other eject methods will have to "fall back" on count
        -based confirmations.
        """

        self.valid = False
        self.need_first_time_count = True
        self._playfield = False

        self.machine.events.add_handler('balldevice_' + self.name +
                                        '_ball_eject_request',
                                        self.eject)

        self.machine.events.add_handler('init_phase_2',
                                        self.configure_eject_targets)

    @property
    def num_balls_ejectable(self):
        """How many balls are in this device that could be ejected."""
        return self.balls

        # todo look at upstream devices

    def configure_eject_targets(self, config=None):
        new_list = list()

        for target in self.config['eject_targets']:
            new_list.append(self.machine.ball_devices[target])

        self.config['eject_targets'] = new_list

    def _source_device_eject_attempt(self, balls, target, **kwargs):
        # A source device is attempting to eject a ball.
        if target == self:
            if self.debug:
                self.log.debug("Waiting for %s balls", balls)
            self.num_balls_in_transit += balls

            if self.num_balls_requested:
                # set event handler to watch for receiving a ball
                self.machine.events.add_handler('balldevice_' + self.name +
                                                '_ball_enter',
                                                self._requested_ball_received,
                                                priority=1000)

    def _source_device_eject_failed(self, balls, target, **kwargs):
        # A source device failed to eject a ball.
        if target == self:
            self.num_balls_in_transit -= balls

            if self.num_balls_in_transit <= 0:
                self.num_balls_in_transit = 0
                self.machine.events.remove_handler(self._requested_ball_received)

    def _initialize(self):
        # convert names to objects

        # make sure the eject timeouts list matches the length of the eject targets
        if (len(self.config['eject_timeouts']) <
                len(self.config['eject_targets'])):
            self.config['eject_timeouts'] += [None] * (
                len(self.config['eject_targets']) -
                len(self.config['eject_timeouts']))

        timeouts_list = self.config['eject_timeouts']
        self.config['eject_timeouts'] = dict()

        for i in range(len(self.config['eject_targets'])):
            self.config['eject_timeouts'][self.config['eject_targets'][i]] = (
                Timing.string_to_ms(timeouts_list[i]))
        # End code to create timeouts list -------------------------------------

        # Register switch handlers with delays for entrance & exit counts
        for switch in self.config['ball_switches']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=switch.name, state=1,
                ms=self.config['entrance_count_delay'],
                callback=self.count_balls)
        for switch in self.config['ball_switches']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=switch.name, state=0,
                ms=self.config['exit_count_delay'],
                callback=self.count_balls)
        for switch in self.config['ball_switches']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=switch.name, state=1,
                ms=0,
                callback=self._invalidate)
        for switch in self.config['ball_switches']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=switch.name, state=0,
                ms=0,
                callback=self._invalidate)

        # Configure switch handlers for jam switch activity
        if self.config['jam_switch']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=self.config['jam_switch'].name, state=1, ms=0,
                callback=self._jam_switch_handler)
            # todo do we also need to add inactive and make a smarter
            # handler?

        # Configure switch handlers for entrance switch activity
        if self.config['entrance_switch']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=self.config['entrance_switch'].name, state=1, ms=0,
                callback=self._entrance_switch_handler)
            # todo do we also need to add inactive and make a smarter
            # handler?

        # handle hold_coil activation when a ball hits a switch
        for switch in self.config['hold_switches']:
            self.machine.switch_controller.add_switch_handler(
                switch_name=switch.name, state=1,
                ms=0,
                callback=self._enable_hold_coil)



        # Configure event handlers to watch for target device status changes
        for target in self.config['eject_targets']:
            # Target device is requesting a ball
            self.machine.events.add_handler('balldevice_' +
                                            target.name
                                            + '_ball_request',
                                            self.eject,
                                            target=target,
                                            get_ball=True)

            # Target device is now able to receive a ball
            self.machine.events.add_handler('balldevice_' +
                                            target.name
                                            + '_ok_to_receive',
                                            self._do_eject)

        # Get an initial ball count
        self.count_balls(stealth=True)

    def _initialize2(self):
        # Watch for ejects targeted at us
        for device in self.machine.ball_devices:
            for target in device.config['eject_targets']:
                if target.name == self.name:
                    if self.debug:
                        self.log.debug("EVENT: %s to %s", device.name,
                                       target.name)
                    self.machine.events.add_handler(
                        event='balldevice_' + device.name +
                        '_ball_eject_failed',
                        handler=self._source_device_eject_failed)

                    self.machine.events.add_handler(
                        event='balldevice_' + device.name +
                        '_ball_eject_attempt',
                        handler=self._source_device_eject_attempt)
                    break

    def get_status(self, request=None):
        """Returns a dictionary of current status of this ball device.

        Args:
            request: A string of what status item you'd like to request.
                Default will return all status items.
                Options include:
                * balls
                * eject_in_progress_target
                * eject_queue

        Returns:
            A dictionary with the following keys:
                * balls
                * eject_in_progress_target
                * eject_queue
        """
        if request == 'balls':
            return self.balls
        elif request == 'eject_in_progress_target':
            return self.eject_in_progress_target
        elif request == 'eject_queue':
            return self.eject_queue,
        else:
            return {'balls': self.balls,
                    'eject_in_progress_target': self.eject_in_progress_target,
                    'eject_queue': self.eject_queue,
                    }

    def status_dump(self):
        """Dumps the full current status of the device to the log."""

        if self.eject_in_progress_target:
            eject_name = self.eject_in_progress_target.name
        else:
            eject_name = 'None'

        if self.debug:
            self.log.debug("+-----------------------------------------+")
            self.log.debug("| balls: %s                  |",
                           self.balls)
            self.log.debug("| eject_in_progress_target: %s",
                           eject_name)
            self.log.debug("| num_balls_ejecting: %s                   |",
                           self.num_balls_ejecting)
            self.log.debug("| num_jam_switch_count: %s                     |",
                           self.num_jam_switch_count)
            self.log.debug("| num_eject_attempts: %s                   |",
                           self.num_eject_attempts)
            self.log.debug("| num_balls_requested: %s                  |",
                           self.num_balls_requested)
            self.log.debug("| eject queue: %s",
                           self.eject_queue)
            self.log.debug("+-----------------------------------------+")

    def _invalidate(self):
        self.valid = False

    def count_balls(self, stealth=False, **kwargs):
        """Counts the balls in the device and processes any new balls that came
        in or balls that have gone out.

        Args:
            stealth: Boolean value that controls whether any events will be
                posted based on any ball count change info. If True, results
                will not be posted. If False, they will. Default is False.
            **kwargs: Catches unexpected args since this method is used as an
                event handler.

        """
        if self.debug:
            self.log.debug("Counting balls")

        self.valid = True

        if self.config['ball_switches']:

            ball_count = 0
            ball_change = 0
            previous_balls = self.balls
            if self.debug:
                self.log.debug("Previous number of balls: %s", previous_balls)

            for switch in self.config['ball_switches']:
                valid = False
                if self.machine.switch_controller.is_active(switch.name,
                        ms=self.config['entrance_count_delay']):
                    ball_count += 1
                    valid = True
                    if self.debug:
                        self.log.debug("Confirmed active switch: %s", switch.name)
                elif self.machine.switch_controller.is_inactive(switch.name,
                        ms=self.config['exit_count_delay']):
                    if self.debug:
                        self.log.debug("Confirmed inactive switch: %s", switch.name)
                    valid = True

                if not valid:  # one of our switches wasn't valid long enough
                    # recount will happen automatically after the time passes
                    # via the switch handler for count
                    if self.debug:
                        self.log.debug("Switch '%s' changed too recently. "
                                       "Aborting count & returning previous "
                                       "count value", switch.name)
                    self.valid = False
                    return previous_balls

            if self.debug:
                self.log.debug("Counted %s balls", ball_count)
            self.balls = ball_count

            # Figure out if we gained or lost any balls since last count?
            if self.need_first_time_count:
                if self.debug:
                    self.log.debug("This is a first time count. Don't know if "
                                   "we gained or lost anything.")
                # No "real" change since we didn't know previous value
                ball_change = 0
            else:
                ball_change = ball_count - previous_balls
                if self.debug:
                    self.log.debug("Ball count change: %s", ball_change)

            # If we were waiting for a count-based eject confirmation, let's
            # confirm it now
            if (not ball_change and self.flag_confirm_eject_via_count and
                    self.eject_in_progress_target):
                self._eject_success()
                # todo I think this is ok with `not ball_change`. If ball_change
                # is positive that means the ball fell back in or a new one came
                # in. We can't tell the difference, but hey, we're using count-
                # based eject confirmation which sucks anyway, so them's the
                # ropes. If ball_change is negative then I don't know what the
                # heck happened.

            self.status_dump()

            if ball_change > 0:
                self._balls_added(ball_change)
            elif ball_change < 0:
                    self._balls_missing(ball_change)

        else:  # this device doesn't have any ball switches
            if self.debug:
                self.log.debug("Received request to count balls, but we don't "
                               "have any ball switches. So we're just returning"
                               "the old count.")
            if self.need_first_time_count:
                self.balls = 0
            # todo add support for virtual balls

        self.need_first_time_count = False

        if self.balls < 0:
            self.balls = 0
            self.log.warning("Number of balls contained is negative (%s).",
                             self.balls)
            # This should never happen

        return self.balls

    def _balls_added(self, balls):
        # Called when ball_count finds new balls in this device

        # If this device received a new ball while a current eject was in
        # progress, let's try to figure out whether an actual new ball entered
        # or whether the current ball it was trying to eject fell back in.
        # Note we can only do this for devices that have a jam switch.

        if (self.eject_in_progress_target and self.config['jam_switch'] and
                self.num_jam_switch_count > 1):
            # If the jam switch count is more than 1, we assume the ball it was
            # trying to eject fell back in.
            if self.debug:
                self.log.debug("Jam switch count: %s. Assuming eject failed.",
                               self.num_jam_switch_count)
            self.eject_failed()
            return

        elif ((self.eject_in_progress_target and self.config['jam_switch'] and
                self.num_jam_switch_count == 1) or
                not self.eject_in_progress_target):
            # If there's an eject in progress with a jam switch count of only 1,
            # or no eject in progress, we assume this was a valid new ball.

            # If this device is not expecting any balls, we assuming this one
            # came from the playfield. Post this event so the playfield can keep
            # track of how many balls are out.
            if not self.num_balls_in_transit:
                self.machine.events.post('balldevice_captured_from_' +
                                         self.config['captures_from'],
                                         balls=balls)

            # Post the relay event as other handlers might be looking for to act
            # on the ball entering this device.
            self.machine.events.post_relay('balldevice_' + self.name +
                                           '_ball_enter',
                                            balls=balls,
                                            callback=self._balls_added_callback)

    def _balls_added_callback(self, balls, **kwargs):
        # Callback event for the balldevice_<name>_ball_enter relay event

        # If we still have balls here, that means that no one claimed them, so
        # essentially they're "stuck." So we just eject them... unless this
        # device is tagged 'trough' in which case we let it keep them.
        if balls and 'trough' not in self.tags:
            self.eject(balls)

        if self.debug:
            self.log.debug("In the balls added callback")
            self.log.debug("Eject queue: %s", self.eject_queue)

        #todo we should call the ball controller in case it wants to eject a
        # ball from a different device?

        if self.eject_queue:
            if self.debug:
                self.log.debug("A ball was added and we have an eject_queue, so"
                               " we're going to process that eject now.")
            self._do_eject()

    def _balls_missing(self, balls):
        # Called when ball_count finds that balls are missing from this device

        self.log.warning("%s ball(s) missing from device", abs(balls))

        # todo dunno if there's any action we should take here? This should
        # never happen unless someone takes the glass off and steals a ball or
        # unless there's a ball switch or a ball randomly falls out of a device?

    def is_full(self):
        """Checks to see if this device is full, meaning it is holding either
        the max number of balls it can hold, or it's holding all the known balls
        in the machine.

        Returns: True or False

        """
        if self.config['ball_capacity'] and self.balls >= self.config['ball_capacity']:
            return True
        elif self.balls >= self.machine.ball_controller.num_balls_known:
            return True
        else:
            return False

    def _jam_switch_handler(self):
        # The device's jam switch was just activated.
        # This method is typically used with trough devices to figure out if
        # balls fell back in.

        self.num_jam_switch_count += 1
        if self.debug:
            self.log.debug("Ball device %s jam switch hit. New count: %s",
                           self.name, self.num_jam_switch_count)

    def _entrance_switch_handler(self):
        # A ball has triggered this device's entrance switch

        if not self.config['ball_switches'] and not self.is_full():
            self.balls += 1
            self._balls_added(1)

    def get_additional_ball_capacity(self):
        """Returns an integer value of the number of balls this device can
            receive. A return value of 0 means that this device is full and/or
            that it's not able to receive any balls at this time due to a
            current eject_in_progress.

        """
        if self.num_balls_ejecting:
            # This device is in the process of ejecting a ball, so it shouldn't
            # receive any now.

            return 0

        if self.config['ball_capacity'] - self.balls < 0:
            self.log.warning("Device reporting more balls contained than its "
                             "capacity.")

        return self.config['ball_capacity'] - self.balls

    def request_ball(self, balls=1):
        """Request that one or more balls is added to this device.

        Args:
            balls: Integer of the number of balls that should be added to this
                device. A value of -1 will cause this device to try to fill
                itself.

        Note that a device will never request more balls than it can hold. Also,
        only devices that are fed by other ball devices (or a combination of
        ball devices and diverters) can make this request. e.g. if this device
        is fed from the playfield, then this request won't work.

        """
        if self.debug:
            self.log.debug("In request_ball. balls: %s", balls)

        if self.eject_in_progress_target:
            if self.debug:
                self.log.debug("Received request to request a ball, but we "
                               "can't since there's an eject in progress.")
            return False

        if not self.get_additional_ball_capacity():
            if self.debug:
                self.log.debug("Received request to request a ball, but we "
                               "can't since it's not ok to receive.")
            return False

        # How many balls are we requesting?
        remaining_capacity = (self.config['ball_capacity'] -
                              self.balls -
                              self.num_balls_requested)

        if remaining_capacity < 0:
            remaining_capacity = 0

        # Figure out how many balls we can request
        if balls == -1 or balls > remaining_capacity:
            balls = remaining_capacity

        if not balls:
            return 0

        self.num_balls_requested += balls

        if self.debug:
            self.log.debug("Requesting Ball(s). Balls=%s", balls)

        self.machine.events.post('balldevice_' + self.name + '_ball_request',
                                 balls=balls)

        return balls

    def _requested_ball_received(self, balls):
        # Responds to its own balldevice_<name>_ball_enter relay event
        # We do this since we need something to act on the balls being received,
        # otherwise it would think they were unexpected and eject them.

        # Figure out how many of the new balls were requested
        unexpected_balls = balls - self.num_balls_in_transit
        if unexpected_balls < 0:
            unexpected_balls = 0

        # Figure out how many outstanding ball requests we have
        self.num_balls_requested -= balls
        self.num_balls_in_transit -= balls

        if self.num_balls_requested <= 0:
            self.num_balls_requested = 0

        if self.num_balls_in_transit <= 0:
            self.machine.events.remove_handler(self._requested_ball_received)

        return {'balls': unexpected_balls}

    def _cancel_request_ball(self):
        self.machine.events.post('balldevice_' + self.name +
                                 '_cancel_ball_request')

    def _eject_event_handler(self):
        # We received the event that should eject this ball.

        if not self.balls:
            self.request_ball()

    def stop(self, **kwargs):
        """Stops all activity in this device.

        Cancels all pending eject requests. Cancels eject confirmation checks.

        """
        if self.debug:
            self.log.debug("Stopping all activity via stop()")
        self.eject_in_progress_target = None
        self.eject_queue = deque()
        self.num_jam_switch_count = 0

        # todo jan19 anything to add here?

        self._cancel_eject_confirmation()
        self.count_balls()  # need this since we're canceling the eject conf

    def eject(self, balls=1, target=None, timeout=None, get_ball=False,
              **kwargs):
        """Ejects one or more balls from the device.

        Args:
            balls: Integer of the number of balls to eject. Default is 1.
            target: Optional target that should receive the ejected ball(s),
                either a string name of the ball device or a ball device
                object. Default is None which means this device will eject this
                ball to the first entry in the eject_targets list.
            timeout: How long (in ms) to wait for the ball to make it into the
                target device after the ball is ejected. A value of ``None``
                means the default timeout from the config file will be used. A
                value of 0 means there is no timeout.
            get_ball: Boolean as to whether this device should attempt to get
                a ball to eject if it doesn't have one. Default is False.

        Note that if this device's 'balls_per_eject' configuration is more than
        1, then it will eject the nearest number of balls it can.

        """
        # Figure out the eject target

        if balls < 1:
            self.log.warning("Received request to eject %s balls, which doesn't"
                             " make sense. Ignoring...")
            return False

        if not target:
            target = self.config['eject_targets'][0]

        elif type(target) is str:
            target = self.machine.ball_devices[target]

        if self.debug:
            self.log.debug("Received request to eject %s ball(s) to target '%s'",
                           balls, target.name)

        if not self.config['eject_coil'] and not self.config['hold_coil']:
            if self.debug:
                self.log.debug("This device has no eject and hold coil, so "
                               "we're assuming a manual eject by the player.")
            timeout = 0  # unlimited since who knows how long the player waits

        # Set the timeout for this eject
        if timeout is None:
            timeout = self.config['eject_timeouts'][target]

        # Set the number of balls to eject

        balls_to_eject = balls

        if balls_to_eject > self.balls and not get_ball:
            balls_to_eject = self.balls

        # Add one entry to the eject queue for each ball that's ejecting
        if self.debug:
            self.log.debug('Adding %s ball(s) to the eject_queue.',
                           balls_to_eject)
        for i in range(balls_to_eject):
            self.eject_queue.append((target, timeout))

        self._do_eject()

    def eject_all(self, target=None):
        """Ejects all the balls from this device

        Args:
            target: The string or BallDevice target for this eject. Default of
                None means `playfield`.

        Returns:
            True if there are balls to eject. False if this device is empty.

        """
        if self.debug:
            self.log.debug("Ejecting all balls")
        if self.balls > 0:
            self.eject(balls=self.balls, target=target)
            return True
        else:
            return False

    def _do_eject(self, **kwargs):
        # Performs the actual eject attempts and sets up eject confirmations
        # **kwargs just because this method is registered for various events
        # which might pass them.

        if not self.eject_queue:
            return False  # No eject queue and therefore nothing to do

        if self.debug:
            self.log.debug("Entering _do_eject(). Current in progress target: %s. "
                           "Eject queue: %s",
                           self.eject_in_progress_target, self.eject_queue)

        if self.eject_in_progress_target:
            if self.debug:
                self.log.debug("Current eject in progress with target: %s. "
                               "Aborting eject.", self.eject_in_progress_target)
            return False  # Don't want to get in the way of a current eject

        if not self.balls and not self.num_balls_requested:
            if self.debug:
                self.log.debug("Don't have any balls. Requesting one.")
            self.request_ball()
            # Once the ball is delivered then the presence of the eject_queue
            # will re-start this _do_eject() process
            return False

        elif self.balls:
            if self.debug:
                self.log.debug("We have an eject queue: %s", self.eject_queue)

            target = self.eject_queue[0][0]  # first item, first part of tuple

            if not target.get_additional_ball_capacity():
                if self.debug:
                    self.log.debug("Target device '%s' is not able to receive now. "
                               "Aborting eject. Will retry when target can "
                               "receive.", target.name)
                return False

            else:
                if self.debug:
                    self.log.debug("Proceeding with the eject")

                self.eject_in_progress_target, timeout = (
                    self.eject_queue.popleft())
                if self.debug:
                    self.log.debug("Setting eject_in_progress_target: %s, "
                               "timeout %s", self.eject_in_progress_target.name,
                               timeout)

                self.num_eject_attempts += 1

                if self.config['jam_switch']:
                    self.num_jam_switch_count = 0
                    if self.machine.switch_controller.is_active(
                            self.config['jam_switch'].name):
                        self.num_jam_switch_count += 1
                        # catches if the ball is blocking the switch to
                        # begin with, todo we have to get smart here

                if self.config['balls_per_eject'] == 1:
                    self.num_balls_ejecting = 1
                else:
                    self.num_balls_ejecting = self.balls

                self.machine.events.post_queue('balldevice_' + self.name +
                                         '_ball_eject_attempt',
                                         balls=self.num_balls_ejecting,
                                         target=self.eject_in_progress_target,
                                         timeout=timeout,
                                         num_attempts=self.num_eject_attempts,
                                         callback=self._perform_eject)
                # Fire the coil via a callback in case there are events in the
                # queue. This ensures that the coil pulse happens when this
                # event is posted instead of firing right away.

    def _eject_status(self):
        if self.debug:

            try:
                self.log.debug("DEBUG: Eject duration: %ss. Target: %s",
                              round(time.time() - self.eject_start_time, 2),
                              self.eject_in_progress_target.name)
            except AttributeError:
                self.log.debug("DEBUG: Eject duration: %ss. Target: None",
                              round(time.time() - self.eject_start_time, 2))

    def _perform_eject(self, target, timeout=None, **kwargs):
        self._setup_eject_confirmation(target, timeout)

        self.balls -= self.num_balls_ejecting

        if self.config['eject_coil']:
            self._fire_eject_coil()
        elif self.config['hold_coil']:
            self._disable_hold_coil()
            # TODO: allow timed release of single balls and reenable coil after
            # release. Disable coil when device is empty
        else:
            if self.debug:
                self.log.debug("Waiting for player to manually eject ball(s)")

    def _disable_hold_coil(self):

        self.config['hold_coil'].disable()
        if self.debug:
            self.log.debug("Disabling hold coil. num_balls_ejecting: %s. New "
                           "balls: %s.", self.num_balls_ejecting, self.balls)

    def hold(self, **kwargs):
        self._enable_hold_coil()

    def _enable_hold_coil(self, **kwargs):

        # do not enable coil when we are ejecting. Currently, incoming balls
        # will also be ejected.

        # TODO: notice new balls during eject and do timed release for last
        # ball also in that case.
        if self.num_balls_ejecting:
            return

        self.config['hold_coil'].enable()
        if self.debug:
            self.log.debug("Disabling hold coil. num_balls_ejecting: %s. New "
                           "balls: %s.", self.num_balls_ejecting, self.balls)



    def _fire_eject_coil(self):
        self.config['eject_coil'].pulse()
        if self.debug:
            self.log.debug("Firing eject coil. num_balls_ejecting: %s. New "
                           "balls: %s.", self.num_balls_ejecting, self.balls)

    def _setup_eject_confirmation(self, target=None, timeout=0):
        # Called after an eject request to confirm the eject. The exact method
        # of confirmation depends on how this ball device has been configured
        # and what target it's ejecting to

        # args are target device and timeout in ms

        if self.debug:
            self.log.debug("Setting up eject confirmation.")

        if self.debug:
            self.eject_start_time = time.time()
            self.log.debug("Eject start time: %s", self.eject_start_time)
            self.machine.events.add_handler('timer_tick', self._eject_status)

        self.flag_confirm_eject_via_count = False

        if self.config['confirm_eject_type'] == 'target':

            if not target:
                self.log.error("we got an eject confirmation request with no "
                               "target. This shouldn't happen. Post to the "
                               "forum if you see this.")
                raise Exception("we got an eject confirmation request with no "
                               "target. This shouldn't happen. Post to the "
                               "forum if you see this.")

            if self.debug:
                self.log.debug("Will confirm eject via recount of ball "
                               "switches.")
            self.flag_confirm_eject_via_count = True

            if (target.is_playfield() and
                    target.ok_to_confirm_ball_via_playfield_switch()):
                if self.debug:
                    self.log.debug("Will confirm eject when a %s switch is "
                               "hit (additionally)", target.name)
                self.machine.events.add_handler('sw_' + target.name + '_active',
                                                self._eject_success)

            if timeout:
                # set up the delay to check for the failed the eject
                self.delay.add(name='target_eject_confirmation_timeout',
                               ms=timeout,
                               callback=self.eject_failed)

            if self.debug:
                self.log.debug("Will confirm eject via ball entry into '%s' with a "
                           "confirmation timeout of %sms", target.name, timeout)

            # watch for ball entry event on the target device
            # Note this must be higher priority than the failed eject handler
            self.machine.events.add_handler(
                'balldevice_' + target.name +
                '_ball_enter', self._eject_success, priority=1000)

        elif self.config['confirm_eject_type'] == 'switch':
            if self.debug:
                self.log.debug("Will confirm eject via activation of switch '%s'",
                           self.config['confirm_eject_switch'].name)
            # watch for that switch to activate momentarily
            # todo add support for a timed switch here
            self.machine.switch_controller.add_switch_handler(
                switch_name=self.config['confirm_eject_switch'].name,
                callback=self._eject_success,
                state=1, ms=0)

        elif self.config['confirm_eject_type'] == 'event':
            if self.debug:
                self.log.debug("Will confirm eject via posting of event '%s'",
                           self.config['confirm_eject_event'])
            # watch for that event
            self.machine.events.add_handler(
                self.config['confirm_eject_event'], self._eject_success)

        elif self.config['confirm_eject_type'] == 'count':
            # todo I think we need to set a delay to recount? Because if the
            # ball re-enters in less time than the exit delay, then the switch
            # handler won't have time to reregister it.
            if self.debug:
                self.log.debug("Will confirm eject via recount of ball switches.")
            self.flag_confirm_eject_via_count = True

        elif self.config['confirm_eject_type'] == 'fake':
            # for all ball locks or captive balls which just release a ball
            # we use delay to keep the call order
            self.delay.add(name='target_eject_confirmation_timeout',
                           ms=1, callback=self._eject_success)

        else:
            self.log.error("Invalid confirm_eject_type setting: '%s'",
                           self.config['confirm_eject_type'])
            sys.exit()

    def _cancel_eject_confirmation(self):
        if self.debug:
            self.log.debug("Canceling eject confirmations")
        self.eject_in_progress_target = None
        self.num_eject_attempts = 0

        # Remove any event watching for success
        self.machine.events.remove_handler(self._eject_success)

        # Remove any switch handlers
        if self.config['confirm_eject_type'] == 'switch':
            self.machine.switch_controller.remove_switch_handler(
                switch_name=self.config['confirm_eject_switch'].name,
                callback=self._eject_success,
                state=1, ms=0)

        # Remove any delays that were watching for failures
        self.delay.remove('target_eject_confirmation_timeout')

    def _eject_success(self, **kwargs):
        # We got an eject success for this device.
        # **kwargs because there are many ways to get here, some with kwargs and
        # some without. Also, since there are many ways we can get here, let's
        # first make sure we actually had an eject in progress

        if self.debug:
            self.log.debug("In _eject_success. Eject target: %s",
                           self.eject_in_progress_target)

        if self.debug:
            self.log.debug("Eject duration: %ss",
                           time.time() - self.eject_start_time)
            self.machine.events.remove_handler(self._eject_status)

        # Reset flags for next time
        self.flag_confirm_eject_via_count = False
        self.flag_pending_playfield_confirmation = False

        if self.eject_in_progress_target:
            if self.debug:
                self.log.debug("Confirmed successful eject")

            # Creat a temp attribute here so the real one is None when the
            # event is posted.
            eject_target = self.eject_in_progress_target
            self.num_jam_switch_count = 0
            self.num_eject_attempts = 0
            self.eject_in_progress_target = None
            balls_ejected = self.num_balls_ejecting
            self.num_balls_ejecting = 0

            # todo cancel post eject check delay

            self.machine.events.post('balldevice_' + self.name +
                                     '_ball_eject_success',
                                     balls=balls_ejected,
                                     target=eject_target)

        else:  # this should never happen
            self.log.warning("We got to '_eject_success()' but no eject was in "
                             "progress. Just FYI that something's weird.")

        self._cancel_eject_confirmation()

        if self.eject_queue:
            self._do_eject()
        elif self.get_additional_ball_capacity():
            self._ok_to_receive()

    def eject_failed(self, retry=True, force_retry=False):
        """Marks the current eject in progress as 'failed.'

        Note this is not typically a method that would be called manually. It's
        called automatically based on ejects timing out or balls falling back
        into devices while they're in the process of ejecting. But you can call
        it manually if you want to if you have some other way of knowing that
        the eject failed that the system can't figure out on it's own.

        Args:
            retry: Boolean as to whether this eject should be retried. If True,
                the ball device will retry the eject again as long as the
                'max_eject_attempts' has not been exceeded. Default is True.
            force_retry: Boolean that forces a retry even if the
                'max_eject_attempts' has been exceeded. Default is False.

        """
        if self.debug:
            self.log.debug("Eject Failed")

        # Put the current target back in the queue so we can try again
        # This sets up the timeout back to the default. Wonder if we should
        # add some intelligence to make this longer or shorter?
        self.eject_queue.appendleft((self.eject_in_progress_target,
            self.config['eject_timeouts'][self.eject_in_progress_target]))

        # Remember variables for event
        target = self.eject_in_progress_target
        balls = self.num_balls_ejecting

        # Reset the stuff that showed a current eject in progress
        self.eject_in_progress_target = None
        self.num_balls_ejecting = 0
        self.num_eject_attempts += 1

        if self.debug:
            self.log.debug("Eject duration: %ss",
                          time.time() - self.eject_start_time)

        self.machine.events.post('balldevice_' + self.name +
                                 '_ball_eject_failed',
                                 target=target,
                                 balls=balls,
                                 num_attempts=self.num_eject_attempts)

        self._cancel_eject_confirmation()

        if (retry and (not self.config['max_eject_attempts'] or
                self.num_eject_attempts < self.config['max_eject_attempts'])):
            self._do_eject()

        elif force_retry:
            self._do_eject()

        else:
            self._eject_permanently_failed()

    def _eject_permanently_failed(self):
        self.log.warning("Eject failed %s times. Permanently giving up.",
                         self.config['max_eject_attempts'])
        self.machine.events.post('balldevice_' + self.name +
                                 'ball_eject_permanent_failure')

    def _ok_to_receive(self):
        # Post an event announcing that it's ok for this device to receive a
        # ball

        self.machine.events.post('balldevice_' + self.name +
                                 '_ok_to_receive',
                                 balls=self.get_additional_ball_capacity())

    def is_playfield(self):
        """Returns True if this ball device is a Playfield-type device, False if
        it's a regular ball device.

        """
        return self._playfield
Exemplo n.º 6
0
Arquivo: mode.py Projeto: qcapen/mpf
class ModeTimer(object):
    """Parent class for a mode timer.

    Args:
        machine: The main MPF MachineController object.
        mode: The parent mode object that this timer belongs to.
        name: The string name of this timer.
        config: A Python dictionary which contains the configuration settings
            for this timer.

    """

    def __init__(self, machine, mode, name, config):
        self.machine = machine
        self.mode = mode
        self.name = name
        self.config = config

        self.tick_var = self.mode.name + "_" + self.name + "_tick"
        self.mode.player[self.tick_var] = 0

        self.running = False
        self.start_value = 0
        self.restart_on_complete = False
        self._ticks = 0
        self.end_value = None
        self.ticks_remaining = 0
        self.max_value = None
        self.direction = "up"
        self.tick_secs = 1
        self.timer = None
        self.bcp = False
        self.event_keys = set()
        self.delay = DelayManager()
        self.log = None
        self.debug = False

        if "start_value" in self.config:
            self.start_value = self.config["start_value"]
        else:
            self.start_value = 0

        if "start_running" in self.config and self.config["start_running"]:
            self.running = True

        if "end_value" in self.config:
            self.end_value = self.config["end_value"]

        if "control_events" in self.config and self.config["control_events"]:
            if type(self.config["control_events"]) is dict:
                self.config["control_events"] = [self.config["control_events"]]
        else:
            self.config["control_events"] = list()

        if "direction" in self.config and self.config["direction"].lower() == "down":
            self.direction = "down"

            if not self.end_value:
                self.end_value = 0  # need it to be 0 not None

        if "tick_interval" in self.config:
            self.tick_secs = Timing.string_to_secs(self.config["tick_interval"])

        if "max_value" in self.config:
            self.max_value = self.config["max_value"]

        if "restart_on_complete" in self.config and self.config["restart_on_complete"]:
            self.restart_on_complete = True

        if "bcp" in self.config and self.config["bcp"]:
            self.bcp = True

        if "debug" in self.config and self.config["debug"]:
            self.debug = True
            self.log.debug("Enabling Debug Logging")

        self.mode.player[self.tick_var] = self.start_value

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

        self._setup_control_events(self.config["control_events"])

    def _setup_control_events(self, event_list):

        if self.debug:
            self.log.debug("Setting up control events")

        kwargs = None

        for entry in event_list:
            if entry["action"] == "add":
                handler = self.add_time
                kwargs = {"timer_value": entry["value"]}

            elif entry["action"] == "subtract":
                handler = self.subtract_time
                kwargs = {"timer_value": entry["value"]}

            elif entry["action"] == "jump":
                handler = self.set_current_time
                kwargs = {"timer_value": entry["value"]}

            elif entry["action"] == "start":
                handler = self.start

            elif entry["action"] == "stop":
                handler = self.stop

            elif entry["action"] == "reset":
                handler = self.reset

            elif entry["action"] == "restart":
                handler = self.restart

            elif entry["action"] == "pause":
                handler = self.pause
                kwargs = {"timer_value": entry["value"]}

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

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

            if kwargs:
                self.event_keys.add(self.machine.events.add_handler(entry["event"], handler, **kwargs))
            else:
                self.event_keys.add(self.machine.events.add_handler(entry["event"], handler))

    def _remove_control_events(self):

        if self.debug:
            self.log.debug("Removing control events")

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

    def reset(self, **kwargs):
        """Resets 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.
        """

        if self.debug:
            self.log.debug("Resetting timer. New value: %s", self.start_value)

        self.set_current_time(self.start_value)

    def start(self, **kwargs):
        """Starts this timer based on the starting value that's already been
        configured. Use set_current_time() 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.
        """

        if self.debug:
            self.log.debug("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.mode.player[self.tick_var],
            ticks_remaining=self.ticks_remaining,
        )

        if self.bcp:
            self.machine.bcp.send(
                "timer",
                name=self.name,
                action="started",
                ticks=self.mode.player[self.tick_var],
                ticks_remaining=self.ticks_remaining,
            )

    def restart(self, **kwargs):
        """Restarts 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.

        """
        self.reset()
        self.start()

    def stop(self, **kwargs):
        """Stops 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.
        """

        if self.debug:
            self.log.debug("Stopping Timer")

        self.delay.remove("pause")

        self.running = False
        self._remove_system_timer()

        self.machine.events.post(
            "timer_" + self.name + "_stopped",
            ticks=self.mode.player[self.tick_var],
            ticks_remaining=self.ticks_remaining,
        )

        if self.bcp:
            self.machine.bcp.send(
                "timer",
                name=self.name,
                action="stopped",
                ticks=self.mode.player[self.tick_var],
                ticks_remaining=self.ticks_remaining,
            )

    def pause(self, timer_value=0, **kwargs):
        """Pauses 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.
        """

        if self.debug:
            self.log.debug("Pausing Timer for %s secs", timer_value)

        self.running = False

        pause_secs = timer_value

        self._remove_system_timer()
        self.machine.events.post(
            "timer_" + self.name + "_paused",
            ticks=self.mode.player[self.tick_var],
            ticks_remaining=self.ticks_remaining,
        )
        if self.bcp:
            self.machine.bcp.send(
                "timer",
                name=self.name,
                action="paused",
                ticks=self.mode.player[self.tick_var],
                ticks_remaining=self.ticks_remaining,
            )

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

    def timer_complete(self):
        """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.
        """

        if self.debug:
            self.log.debug("Timer Complete")

        self.stop()

        if self.bcp:  # must be before the event post in case it stops the mode
            self.machine.bcp.send(
                "timer",
                name=self.name,
                action="complete",
                ticks=self.mode.player[self.tick_var],
                ticks_remaining=self.ticks_remaining,
            )

        self.machine.events.post(
            "timer_" + self.name + "_complete",
            ticks=self.mode.player[self.tick_var],
            ticks_remaining=self.ticks_remaining,
        )

        if self.restart_on_complete:

            if self.debug:
                self.log.debug("Restart on complete: True")

            self.reset()
            self.start()

    def _timer_tick(self):
        # Automatically called by the sytem timer each tick

        if self.debug:
            self.log.debug("Timer Tick")

        if not self.running:

            if self.debug:
                self.log.debug("Timer is not running. Will remove.")

            self._remove_system_timer()
            return

        if self.direction == "down":
            self.mode.player[self.tick_var] -= 1
        else:
            self.mode.player[self.tick_var] += 1

        if not self._check_for_done():
            self.machine.events.post(
                "timer_" + self.name + "_tick",
                ticks=self.mode.player[self.tick_var],
                ticks_remaining=self.ticks_remaining,
            )

            if self.debug:
                self.log.debug("Ticks: %s, Remaining: %s", self.mode.player[self.tick_var], self.ticks_remaining)

            if self.bcp:
                self.machine.bcp.send(
                    "timer",
                    name=self.name,
                    action="tick",
                    ticks=self.mode.player[self.tick_var],
                    ticks_remaining=self.ticks_remaining,
                )

    def add_time(self, timer_value, **kwargs):
        """Adds ticks to this timer.

        Args:
            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.
        """

        ticks_added = timer_value

        new_value = self.mode.player[self.tick_var] + ticks_added

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

        self.mode.player[self.tick_var] = new_value
        ticks_added = new_value - timer_value

        self.machine.events.post(
            "timer_" + self.name + "_time_added",
            ticks=self.mode.player[self.tick_var],
            ticks_added=ticks_added,
            ticks_remaining=self.ticks_remaining,
        )

        if self.bcp:
            self.machine.bcp.send(
                "timer",
                name=self.name,
                action="time_added",
                ticks=self.mode.player[self.tick_var],
                ticks_added=ticks_added,
                ticks_remaining=self.ticks_remaining,
            )

        self._check_for_done()

    def subtract_time(self, timer_value, **kwargs):
        """Subracts ticks from this timer.

        Args:
            timer_value: The numebr 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.
        """

        ticks_subtracted = timer_value

        self.mode.player[self.tick_var] -= ticks_subtracted

        self.machine.events.post(
            "timer_" + self.name + "_time_subtracted",
            ticks=self.mode.player[self.tick_var],
            ticks_subtracted=ticks_subtracted,
            ticks_remaining=self.ticks_remaining,
        )

        if self.bcp:
            self.machine.bcp.send(
                "timer",
                name=self.name,
                action="time_subtracted",
                ticks=self.mode.player[self.tick_var],
                ticks_subtracted=ticks_subtracted,
                ticks_remaining=self.ticks_remaining,
            )

        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.log.debug(
                "Checking to see if timer is done. Ticks: %s, End " "Value: %s, Direction: %s",
                self.mode.player[self.tick_var],
                self.end_value,
                self.direction,
            )

        if self.direction == "up" and self.end_value is not None and self.mode.player[self.tick_var] >= self.end_value:
            self.timer_complete()
            return True
        elif self.direction == "down" and self.mode.player[self.tick_var] <= self.end_value:
            self.timer_complete()
            return True

        if self.end_value is not None:
            self.ticks_remaining = abs(self.end_value - self.mode.player[self.tick_var])

        if self.debug:
            self.log.debug("Timer is not done")

        return False

    def _create_system_timer(self):
        # Creates the system timer which drives this mode timer's tick method.
        self._remove_system_timer()
        self.timer = Timer(callback=self._timer_tick, frequency=self.tick_secs)
        self.machine.timing.add(self.timer)

    def _remove_system_timer(self):
        # Removes the system timer associated with this mode timer.
        if self.timer:
            self.machine.timing.remove(self.timer)
            self.timer = None

    def change_tick_interval(self, change=0.0, **kwargs):
        """Changes 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 added to the current tick
                interval. To set an absolute value, use the set_tick_interval()
                method. To shorten the tick rate, use a negative 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.
        """

        self.tick_secs *= change
        self._create_system_timer()

    def set_tick_interval(self, timer_value, **kwargs):
        """Sets 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(timer_value)
        self._create_system_timer()

    def set_current_time(self, timer_value, **kwargs):
        """Sets 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.
        """

        self.mode.player[self.tick_var] = int(timer_value)

        if self.max_value and self.mode.player[self.tick_var] > self.max_value:
            self.mode.player[self.tick_var] = self.max_value

    def kill(self):
        """Stops this timer and also removes all the control events.

        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.
        """

        self.stop()
        self._remove_control_events()
Exemplo n.º 7
0
Arquivo: modes.py Projeto: jabdoa2/mpf
class ModeTimer(object):
    """Parent class for a mode timer.

    Args:
        machine: The main MPF MachineController object.
        mode: The parent mode object that this timer belongs to.
        name: The string name of this timer.
        config: A Python dictionary which contains the configuration settings
            for this timer.

    """

    def __init__(self, machine, mode, name, config):
        self.machine = machine
        self.mode = mode
        self.name = name
        self.config = config

        self.tick_var = self.mode.name + '_' + self.name + '_tick'
        self.mode.player[self.tick_var] = 0

        self.running = False
        self.start_value = 0
        self.restart_on_complete = False
        self._ticks = 0
        self.end_value = 0
        self.max_value = None
        self.direction = 'up'
        self.tick_secs = 1
        self.timer = None
        self.bcp = False
        self.event_keys = set()
        self.delay = DelayManager()

        if 'start_value' in self.config:
            self.start_value = self.config['start_value']
        else:
            self.start_value = 0

        if 'start_running' in self.config and self.config['start_running']:
            self.running = True

        if 'end_value' in self.config:
            self.end_value = self.config['end_value']

        if 'control_events' in self.config and self.config['control_events']:
            if type(self.config['control_events']) is dict:
                self.config['control_events'] = [self.config['control_events']]
        else:
            self.config['control_events'] = list()

        if 'direction' in self.config and self.config['direction'] == 'down':
            self.direction = 'down'

        if 'tick_interval' in self.config:
            self.tick_secs = Timing.string_to_secs(self.config['tick_interval'])

        if 'max_value' in self.config:
            self.max_value = self.config['max_value']

        if ('restart_on_complete' in self.config and
                self.config['restart_on_complete']):
            self.restart_on_complete = True

        if 'bcp' in self.config and self.config['bcp']:
            self.bcp = True

        self.mode.player[self.tick_var] = self.start_value

        self._setup_control_events(self.config['control_events'])

    def _setup_control_events(self, event_list):

        kwargs = None

        for entry in event_list:
            if entry['action'] == 'add':
                handler = self.add_time
                kwargs = {'timer_value': entry['value']}

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

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

            elif entry['action'] == 'start':
                handler = self.start

            elif entry['action'] == 'stop':
                handler = self.stop

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

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

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

            if kwargs:
                self.event_keys.add(self.machine.events.add_handler(
                                    entry['event'], handler, **kwargs))
            else:
                self.event_keys.add(self.machine.events.add_handler(
                                    entry['event'], handler))

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

    def reset(self):
        self._ticks = self.start_value

    def start(self, **kwargs):
        """Starts this timer based on the starting value that's already been
        configured. Use set_current_time() 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.
        """

        self.running = True

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

        self.machine.events.post('timer_' + self.name + '_started',
                                 ticks=self.mode.player[self.tick_var])

        if self.bcp:
            self.machine.bcp.send('timer', name=self.name, action='started',
                                  ticks=self.mode.player[self.tick_var])

    def stop(self, **kwargs):
        """Stops 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.
        """

        self.delay.remove('pause')

        self.running = False
        self._remove_system_timer()

        self.machine.events.post('timer_' + self.name + '_stopped',
                                 ticks=self.mode.player[self.tick_var])

        if self.bcp:
            self.machine.bcp.send('timer', name=self.name, action='stopped',
                                  ticks=self.mode.player[self.tick_var])

    def pause(self, timer_value=0, **kwargs):
        """Pauses 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.
        """

        self.running = False

        pause_secs = timer_value

        self._remove_system_timer()
        self.machine.events.post('timer_' + self.name + '_paused',
                                 ticks=self.mode.player[self.tick_var])
        if self.bcp:
            self.machine.bcp.send('timer', name=self.name, action='paused',
                                  ticks=self.mode.player[self.tick_var])

        if pause_secs > 0:
            self.delay.add('pause', pause_secs, self.start)

    def timer_complete(self):
        """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.
        """

        self.stop()

        if self.bcp:  # must be before the event post in case it stops the mode
            self.machine.bcp.send('timer', name=self.name, action='complete',
                                  ticks=self.mode.player[self.tick_var])

        self.machine.events.post('timer_' + self.name + '_complete',
                                 ticks=self.mode.player[self.tick_var])

        if self.restart_on_complete:
            self.reset()
            self.start()

    def _timer_tick(self):
        # Automatically called by the sytem timer each tick

        if not self.running:
            self._remove_system_timer()
            return

        if self.direction == 'down':
            self.mode.player[self.tick_var] -= 1
        else:
            self.mode.player[self.tick_var] += 1

        if not self._check_for_done():
            self.machine.events.post('timer_' + self.name + '_tick',
                                     ticks=self.mode.player[self.tick_var])

            if self.bcp:
                self.machine.bcp.send('timer', name=self.name, action='tick',
                                      ticks=self.mode.player[self.tick_var])

    def add_time(self, timer_value, **kwargs):
        """Adds ticks to this timer.

        Args:
            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.
        """

        ticks_added = timer_value

        new_value = self.mode.player[self.tick_var] + ticks_added

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

        self.mode.player[self.tick_var] = new_value
        ticks_added = new_value - timer_value

        self.machine.events.post('timer_' + self.name + '_time_added',
                                 ticks=self.mode.player[self.tick_var],
                                 ticks_added=ticks_added)

        if self.bcp:
            self.machine.bcp.send('timer', name=self.name, action='time_added',
                                  ticks=self.mode.player[self.tick_var],
                                  ticks_added=ticks_added)

        self._check_for_done()

    def subtract_time(self, timer_value, **kwargs):
        """Subracts ticks from this timer.

        Args:
            timer_value: The numebr 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.
        """

        ticks_subtracted = timer_value

        self.mode.player[self.tick_var] -= ticks_subtracted

        self.machine.events.post('timer_' + self.name + '_time_subtracted',
                                 ticks=self.mode.player[self.tick_var],
                                 ticks_subtracted=ticks_subtracted)

        if self.bcp:
            self.machine.bcp.send('timer', name=self.name,
                                  action='time_subtracted',
                                  ticks=self.mode.player[self.tick_var],
                                  ticks_subtracted=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.direction == 'up' and
                self.mode.player[self.tick_var] >= self.end_value):
            self.timer_complete()
            return True
        elif self.mode.player[self.tick_var] <= self.end_value:
            self.timer_complete()
            return True

        return False

    def _create_system_timer(self):
        # Creates the system timer which drives this mode timer's tick method.
        self._remove_system_timer()
        self.timer = Timer(callback=self._timer_tick, frequency=self.tick_secs)
        self.machine.timing.add(self.timer)

    def _remove_system_timer(self):
        # Removes the system timer associated with this mode timer.
        if self.timer:
            self.machine.timing.remove(self.timer)
            self.timer = None

    def change_tick_interval(self, change=0.0, **kwargs):
        """Changes 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 added to the current tick
                interval. To set an absolute value, use the set_tick_interval()
                method. To shorten the tick rate, use a negative 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.
        """

        self.tick_secs *= change
        self._create_system_timer()

    def set_tick_interval(self, timer_value, **kwargs):
        """Sets 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(timer_value)
        self._create_system_timer()

    def set_current_time(self, timer_value, **kwargs):
        """Sets 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.
        """

        self.mode.player[self.tick_var] = int(timer_value)

        if self.max_value and self.mode.player[self.tick_var] > self.max_value:
            self.mode.player[self.tick_var] = self.max_value

    def kill(self):
        """Stops this timer and also removes all the control events.

        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.
        """

        self.stop()
        self._remove_control_events()
Exemplo n.º 8
0
class BallSave(Device):

    config_section = "ball_saves"
    collection = "ball_saves"
    class_label = "ball_save"

    def __init__(self, machine, name, config, collection=None, validate=True):
        super(BallSave, self).__init__(machine, name, config, collection, validate=validate)

        self.delay = DelayManager()
        self.enabled = False
        self.saves_remaining = 0

        if self.config["balls_to_save"] == -1:
            self.unlimited_saves = True
        else:
            self.unlimited_saves = False

        self.source_playfield = self.config["source_playfield"]

        # todo change the delays to timers so we can add pause and extension
        # events, but that will require moving timers out of mode conde

    def enable(self, **kwargs):
        if self.enabled:
            return

        self.saves_remaining = self.config["balls_to_save"]
        self.enabled = True
        self.log.debug(
            "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))

    def disable(self, **kwargs):
        if not self.enabled:
            return

        self.enabled = False
        self.log.debug("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))

    def timer_start(self, **kwargs):
        if self.config["active_time"] > 0:
            if self.debug:
                self.log.debug("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):
        if self.debug:
            self.log.debug("Starting Hurry Up")

        self.machine.events.post("ball_save_{}_hurry_up".format(self.name))

    def _grace_period(self):
        if self.debug:
            self.log.debug("Starting Grace Period")

        self.machine.events.post("ball_save_{}_grace_period".format(self.name))

    def _ball_drain_while_active(self, balls, **kwargs):
        if balls <= 0:
            return {"balls": balls}

        no_balls_in_play = False

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

        if no_balls_in_play:
            self.log.debug("Received request to save ball, but no balls are in" " play. Discarding request.")
            return {"balls": balls}

        self.log.debug(
            "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)

        self.source_playfield.add_ball(balls=balls, player_controlled=self.config["auto_launch"] ^ 1)

        if not self.unlimited_saves:
            self.saves_remaining -= balls
            if self.debug:
                self.log.debug("Saves remaining: %s", self.saves_remaining)
        elif self.debug:
            self.log.debug("Unlimited Saves enabled")

        if self.saves_remaining <= 0:
            if self.debug:
                self.log.debug("Disabling since there are no saves remaining")
            self.disable()

        return {"balls": 0}

    def remove(self):
        if self.debug:
            self.log.debug("Removing...")

        self.disable()
Exemplo n.º 9
0
class Playfield(BallDevice):

    def __init__(self, machine, name, collection):
        self.log = logging.getLogger('Playfield')

        self.machine = machine
        self.name = name
        self.tags = list()
        self.config = defaultdict(lambda: None)
        self.config['eject_targets'] = list()

        self.ball_controller = self.machine.ball_controller

        self.delay = DelayManager()

        # Add the playfield ball device to the existing device collection
        collection_object = getattr(self.machine, collection)[name] = self

        # Attributes
        self._balls = 0
        self.num_balls_requested = 0
        self.player_controlled_eject_in_progress = None
        self.queued_balls = list()

        # Set up event handlers

        # Watch for balls added to the playfield
        for device in self.machine.balldevices:
            for target in device.config['eject_targets']:
                if target == self.name:
                    self.machine.events.add_handler(
                        event='balldevice_' + device.name +
                        '_ball_eject_success',
                        handler=self._source_device_eject_success)
                    self.machine.events.add_handler(
                        event='balldevice_' + device.name +
                        '_ball_eject_attempt',
                        handler=self._source_device_eject_attempt)
                break

        # Watch for balls removed from the playfield
        self.machine.events.add_handler('balldevice_captured_from_playfield',
                                        self._ball_removed_handler)

        # Watch for any switch hit which indicates a ball on the playfield
        self.machine.events.add_handler('sw_playfield_active',
                                        self.playfield_switch_hit)

    @property
    def balls(self):
        return self._balls

    @balls.setter
    def balls(self, balls):

        prior_balls = self._balls
        ball_change = balls - prior_balls

        if ball_change:
            self.log.debug("Ball count change. Prior: %s, Current: %s, Change: "
                           "%s", prior_balls, balls, ball_change)

        if balls > 0:
            self._balls = balls
            #self.ball_search_schedule()
        elif balls == 0:
            self._balls = 0
            #self.ball_search_disable()
        else:
            self.log.warning("Playfield balls went to %s. Resetting to 0, but "
                             "FYI that something's weird", balls)
            self._balls = 0
            #self.ball_search_disable()

        self.log.debug("New Ball Count: %s. (Prior count: %s)",
                       self._balls, prior_balls)

        if ball_change > 0:
            self.machine.events.post_relay('balldevice_' + self.name +
                                           '_ball_enter', balls=ball_change)

        if ball_change:
            self.machine.events.post('playfield_ball_count_change',
                                     balls=balls, change=ball_change)

    def count_balls(self, **kwargs):
        """Used to count the number of balls that are contained in a ball
        device. Since this is the playfield device, this method always returns
        zero.

        Returns: 0
        """
        return 0

    def get_additional_ball_capacity(self):
        """Used to find out how many more balls this device can hold. Since this
        is the playfield device, this method always returns 999.

        Returns: 999
        """
        return 999

    def add_ball(self, balls=1, source_name=None, source_device=None,
                 trigger_event=None):

        """Adds live ball(s) to the playfield.

        Args:
            balls: Integer of the number of balls you'd like to add.
            source_name: Optional string name of the ball device you'd like to
                add the ball(s) from.
            source_device: Optional ball device object you'd like to add the
                ball(s) from.
            trigger_event: The optional name of an event that MPF will wait for
                before adding the ball into play. Typically used with player-
                controlled eject tag events. If None, the ball will be added
                immediately.

        Returns:
            True if it's able to process the add_ball() request, False if it
            cannot.

        Both source_name and source_device args are included to give you two
        options for specifying the source of the ball(s) to be added. You don't
        need to supply both. (it's an "either/or" thing.) Both of these args are
        optional, so if you don't supply them then MPF will look for a device
        tagged with 'ball_add_live'. If you don't provide a source and you don't
        have a device with the 'ball_add_live' tag, MPF will quit.

        This method does *not* increase the game controller's count of the
        number of balls in play. So if you want to add balls (like in a
        ball scenario(, you need to call this method along with
        ``self.machine.game.add_balls_in_play()``.)

        MPF tracks the number of balls in play separately from the actual balls
        on the playfield because there are numerous situations where the two
        counts are not the same. For example, if a ball is in a VUK while some
        animation is playing, there are no balls on the playfield but still one
        ball in play, or if the player has a two-ball multiball and they shoot
        them both into locks, there are still two balls in play even though
        there are no balls on the playfield, or if the player tilts then there
        are still balls on the playfield but no balls in play.

        """

        if balls < 1:
            self.log.error("Received request to add %s balls, which doesn't "
                           "make sense. Not adding any balls...")
            return False

        # Figure out which device we'll get a ball from

        if source_device:
            pass
        elif source_name and source_name in self.machine.balldevices:
            source_device = self.machine.balldevices[source_name]
        else:
            for device in self.machine.balldevices.items_tagged('ball_add_live'):
                source_device = device
                break

        if not source_device:
            self.log.critical("Received request to add a ball to the playfield, "
                              "but no source device was passed and no ball "
                              "devices are tagged with 'ball_add_live'. Cannot "
                              "add a ball.")
            raise Exception("Received request to add a ball to the playfield, "
                            "but no source device was passed and no ball "
                            "devices are tagged with 'ball_add_live'. Cannot "
                            "add a ball.")

        # If there's a player controlled eject in progress for this device, we
        # hold this request until it's over.
        if self.player_controlled_eject_in_progress == source_device:
            self.queued_balls.append((balls, source_name, source_device,
                                      trigger_event))
            self.log.debug("An add_ball() request came in while there was a "
                           "current player-controlled eject in progress for the"
                            "same device. Will queue the eject request")
            return True

        self.log.debug("Received request to add %s ball(s). Source device: %s. "
                       "Wait for event: %s", balls, source_device.name,
                       trigger_event)

        # If we don't have a coil that's fired by the player, and we our source
        # device has the ability to eject, then we do the eject now.

        # Some examples:

        # Plunger lane w/ switch and coil: ball_add_live device is plunger lane,
        # we don't eject now since *not* player_controlled is true.

        # Plunger lane w/ switch. No coil: ball_add_live device is plunger lane,
        # we don't eject now since there's no eject_coil for that device.

        # Plunger lane has no switch: ball_add_live device is trough, we do
        # eject now since there's no player_controlled tag and the device has an
        # eject coil.

        if trigger_event and source_device.config['eject_coil']:
            self.setup_player_controlled_eject(balls, source_device,
                                               trigger_event)

        else:
            # if there's no trigger, eject right away
            # if there's no eject coil, that's ok. We still need to setup the
            # eject so the device will be expecting the ball to disappear
            source_device.eject(balls=balls, target=self, get_ball=True)

        return True

    def setup_player_controlled_eject(self, balls, device, trigger_event):
        """Used to set up an eject from a ball device which will eject a ball to
        the playfield.

        Args:
            balls: Integer of the number of balls this device should eject.
            device: The ball device object that will eject the ball(s) when a
                switch with the player-controlled eject tag is hit.
            trigger_event: The name of the MPF event that will trigger the
                eject.

        When this method it called, MPF will set up an event handler to look for
        the trigger_event.
        """

        self.log.debug("Setting up a player controlled eject. Balls: %s, Device"
                       ": %s, Trigger Event: %s", balls, device, trigger_event)

        if not device.balls:
            device.request_ball(balls=balls)

        self.machine.events.add_handler(trigger_event,
                                        self.player_eject_request,
                                        balls=balls, device=device)

        self.player_controlled_eject_in_progress = device

    def remove_player_controlled_eject(self):
        """Removed the player-controlled eject so a player hitting a switch
        no longer calls the device(s) to eject a ball.
        """

        self.log.debug("Removing player-controlled eject.")

        self.machine.events.remove_handler(self.player_eject_request)
        self.player_controlled_eject_in_progress = None

        # Need to do this in case one of these queued balls is also a player
        # controlled eject which would re-add it to the queue while iterating.
        # So we clear it and pass them all to add_ball() and then let the queue
        # rebuild with what's left if it needs to.
        ball_list = self.queued_balls
        self.queued_balls = list()

        for item in ball_list:
            self.add_ball(balls=item[0], source_name=item[1],
                          source_device=item[2], trigger_event=item[3])

    def player_eject_request(self, balls, device):
        """A player has hit a switch tagged with the player_eject_request_tag.

        Args:
            balls: Integer of the number of balls that will be ejected.
            device: The ball device object that will eject the ball(s).
        """

        self.log.debug("Received player eject request. Balls: %s, Device: %s",
                       balls, device.name)
        device.eject(balls, target=self)

    def playfield_switch_hit(self):
        """A switch tagged with 'playfield_active' was just hit, indicating that
        there is at least one ball on the playfield.
        """
        if not self.balls:

            if not self.num_balls_requested:
                self.log.debug("PF switch hit with no balls expected. Setting "
                               "pf balls to 1.")
                self.balls = 1
                self.machine.events.post('unexpected_ball_on_playfield')

    def _ball_added_handler(self, balls):
        self.log.debug("%s ball(s) added to the playfield", balls)
        self.balls += balls

    def _ball_removed_handler(self, balls):
        self.log.debug("%s ball(s) removed from the playfield", balls)
        self.balls -= balls

    def _source_device_eject_attempt(self, balls, target, **kwargs):
        # A source device is attempting to eject a ball. We need to know if it's
        # headed to the playfield.
        if target == self:
            self.log.debug("A source device is attempting to ejected %s ball(s)"
                           " to the playfield.", balls)
            self.num_balls_requested += balls

    def _source_device_eject_success(self, balls, target):
        # A source device has just confirmed that it has successfully ejected a
        # ball. Note that we don't care what type of confirmation it used.
        # (Playfield switch hit, count of its ball switches, etc.)

        if target == self:
            self.log.debug("A source device has confirmed it's ejected %s "
                           "ball(s) to the playfield.", balls)
            self.balls += balls
            self.num_balls_requested -= balls

            if self.num_balls_requested < 0:
                self.log.critical("num_balls_requested is %s, which doesn't "
                                  "make sense. Quitting...",
                                  self.num_balls_requested)
                raise Exception("num_balls_requested is %s, which doesn't make "
                                "sense. Quitting...", self.num_balls_requested)

            self.remove_player_controlled_eject()

    def ok_to_confirm_ball_via_playfield_switch(self):
        """Used to check whether it's ok for a ball device which ejects to the
        playfield to confirm its eject via a playfield switch being hit.

        Returns: True or False

        Right now this is simple. If there are no playfield balls, then any
        playfield switch hit is assumed to be from the newly-ejected ball. If
        there are other balls on the playfield, then we can't use this
        confirmation method since we don't know whether a playfield switch hit
        is from the newly-ejected ball(s) or a current previously-live
        playfield ball.
        """
        if not self.balls:
            return True
        else:
            return False

        # todo look for other incoming balls?

    # BALL SEARCH --------------------------------------------------------------

    # todo make ball search work with plunger lanes with no switches. i.e. we
    # don't want ball search to start until a switch is hit?

    def ball_search_schedule(self, secs=None, force=False):
        """Schedules a ball search to start. By default it will schedule it
        based on the time configured in the machine configuration files.

        If a ball search is already scheduled, this method will reset that
        schedule to the new time passed.

        Args:
            secs: Schedules the ball search that many secs from now.
            force : Boolean to force a ball search. Set True to force a ball
                search. Otherwise it will only schedule it if
                self.flag_no_ball_search is False. Default is False

        """
        if self.machine.config['ballsearch']:
            if not self.flag_no_ball_search or force is True:
                if secs is not None:
                    start_ms = secs * 1000
                else:
                    start_ms = (self.machine.config['ballsearch']
                        ['secs until ball search start'] * 1000)
                self.log.debug("Scheduling a ball search for %s secs from now",
                               start_ms / 1000.0)
                self.delay.reset("ball_search_start",
                                 ms=start_ms,
                                 callback=self.ball_search_begin)

    def ball_search_disable(self):
        """Disables ball search.

        Note this is used to prevent a future ball search from happening (like
        when all balls become contained). This method is not used to cancel an
        existing ball search. (Use `ball_search_end` for that.)

        """
        self.log.debug("Disabling Ball Search")
        self.delay.remove('ball_search_start')

    def ball_search_begin(self, force=False):
        """Begin the ball search process"""
        if not self.flag_no_ball_search:
            self.log.debug("Received request to start ball search")
            # ball search should only start if we have uncontained balls
            if self.balls or force:
                # todo add audit
                self.flag_ball_search_in_progress = True
                self.machine.events.post("ball_search_begin_phase1")
                # todo set delay to start phase 2

            else:
                self.log.debug("We got request to start ball search, but we "
                               "have no balls uncontained. WTF??")

    def ball_search_failed(self):
        """Ball Search did not find the ball."""
        self.log.debug("Ball Search failed to find a ball. Disabling.")
        self.ball_search_end()
        self.ball_lost()

    def ball_search_end(self):
        """End the ball search, either because we found the ball or
        are giving up."""
        self.log.debug("Ball search ending")
        self.flag_ball_search_in_progress = False
        self.machine.events.post("ball_search_end")
        # todo cancel the delay for phase 2 if we had one

    def ball_lost(self):
        """Mark a ball as lost"""
        self.num_balls_known = self.balls
        self.num_balls_missing = self.machine.config['machine']\
            ['balls installed'] - self.balls
        self.num_balls_live = 0
        # since desired count doesn't change, this will relaunch them
        self.log.debug("Ball(s) Marked Lost. Known: %s, Missing: %s",
                       self.num_balls_known, self.num_balls_missing)

        # todo audit balls lost

    def ball_found(self, num=1):
        """Used when a previously missing ball is found. Updates the balls
        known and balls missing variables.

        Parameters
        ----------

        num : int
            Specifies how many balls have been found. Default is 1.

        """
        self.log.debug("HEY!! We just found %s lost ball(s).", num)
        self.num_balls_known += num
        self.num_balls_missing -= num
        self.log.debug("New ball counts. Known: %s, Missing: %s",
                       self.num_balls_known, self.num_balls_missing)
        self.ball_update_all_counts()  # is this necessary? todo

    def eject(self, *args, **kwargs):
        pass

    def eject_all(self, *args, **kwargs):
        pass
Exemplo n.º 10
0
class Playfield(BallDevice):

    config_section = 'playfields'
    collection = 'playfields'
    class_label = 'playfield'

    # noinspection PyMissingConstructor
    def __init__(self, machine, name, config, collection=None, validate=True):
        self.log = logging.getLogger('playfield')

        self.machine = machine
        self.name = name.lower()
        self.tags = list()
        self.label = None
        self.debug = False
        self.config = dict()
        self._count_consistent = True
        self.unexpected_balls = 0

        if validate:
            self.config = self.machine.config_processor.process_config2(
                self.config_section, config, self.name)
        else:
            self.config = config

        if self.config['debug']:
            self.debug = True
            self.log.debug("Enabling debug logging for this device")
            self.log.debug("Configuring device with settings: '%s'", config)

        self.tags = self.config['tags']
        self.label = self.config['label']

        self.delay = DelayManager()

        self.machine.ball_devices[name] = self

        if 'default' in self.config['tags']:
            self.machine.playfield = self

        # Attributes
        self._balls = 0
        self.available_balls = 0
        self.num_balls_requested = 0
        self.queued_balls = list()
        self._playfield = True

        # Set up event handlers

        # Watch for balls added to the playfield
        for device in self.machine.ball_devices:
            for target in device.config['eject_targets']:
                if target == self.name:
                    self.machine.events.add_handler(
                        event='balldevice_' + device.name +
                        '_ball_eject_success',
                        handler=self._source_device_eject_success)
                    self.machine.events.add_handler(
                        event='balldevice_' + device.name +
                        '_ball_eject_failed',
                        handler=self._source_device_eject_failed)
                    self.machine.events.add_handler(
                        event='balldevice_' + device.name +
                        '_ball_eject_attempt',
                        handler=self._source_device_eject_attempt)
                    break

        # Watch for balls removed from the playfield
        self.machine.events.add_handler(
            'balldevice_captured_from_' + self.name,
            self._ball_removed_handler)

        # Watch for any switch hit which indicates a ball on the playfield
        self.machine.events.add_handler('sw_' + self.name + '_active',
                                        self.playfield_switch_hit)

        self.machine.events.add_handler('init_phase_2', self._initialize)

    def _initialize(self):
        self.ball_controller = self.machine.ball_controller

        for device in self.machine.playfield_transfers:
            if device.config['eject_target'] == self.name:
                self.machine.events.add_handler(
                    event='balldevice_' + device.name + '_ball_eject_success',
                    handler=self._source_device_eject_success)
                self.machine.events.add_handler(
                    event='balldevice_' + device.name + '_ball_eject_attempt',
                    handler=self._source_device_eject_attempt)

    def add_missing_balls(self, balls):
        # if we catched an unexpected balls before do not add a ball
        if self.unexpected_balls:
            self.unexpected_balls -= 1
            balls -= 1

        self.balls += balls

    @property
    def balls(self):
        return self._balls

    @balls.setter
    def balls(self, balls):

        prior_balls = self._balls
        ball_change = balls - prior_balls

        if ball_change:
            self.log.debug(
                "Ball count change. Prior: %s, Current: %s, Change:"
                " %s", prior_balls, balls, ball_change)

        if balls >= 0:
            self._balls = balls
        else:
            self.log.warning(
                "Playfield balls went to %s. Resetting to 0, but "
                "FYI that something's weird", balls)
            self._balls = 0
            self.unexpected_balls = 0

        self.log.debug("New Ball Count: %s. (Prior count: %s)", self._balls,
                       prior_balls)

        if ball_change > 0:
            self.machine.events.post_relay('balldevice_' + self.name +
                                           '_ball_enter',
                                           new_balls=ball_change,
                                           unclaimed_balls=ball_change)

        if ball_change:
            self.machine.events.post(self.name + '_ball_count_change',
                                     balls=balls,
                                     change=ball_change)

    def count_balls(self, **kwargs):
        """Used to count the number of balls that are contained in a ball
        device. Since this is the playfield device, this method always returns
        zero.

        Returns: 0

        """
        return 0

    def get_additional_ball_capacity(self):
        """Used to find out how many more balls this device can hold. Since this
        is the playfield device, this method always returns 999.

        Returns: 999

        """
        return 999

    def add_ball(self,
                 balls=1,
                 source_name=None,
                 source_device=None,
                 player_controlled=False,
                 reset=False):
        """Adds live ball(s) to the playfield.

        Args:
            balls: Integer of the number of balls you'd like to add.
            source_name: Optional string name of the ball device you'd like to
                add the ball(s) from.
            source_device: Optional ball device object you'd like to add the
                ball(s) from.
            player_controlled: Boolean which specifies whether this event is
                player controlled. (See not below for details)
            reset: Boolean which controls whether the source device should
                reset its state to idle

        Returns:
            True if it's able to process the add_ball() request, False if it
            cannot.

        Both source_name and source_device args are included to give you two
        options for specifying the source of the ball(s) to be added. You don't
        need to supply both. (it's an "either/or" thing.) Both of these args are
        optional, so if you don't supply them then MPF will look for a device
        tagged with 'ball_add_live'. If you don't provide a source and you don't
        have a device with the 'ball_add_live' tag, MPF will quit.

        This method does *not* increase the game controller's count of the
        number of balls in play. So if you want to add balls (like in a
        multiball scenario), you need to call this method along with
        ``self.machine.game.add_balls_in_play()``.)

        MPF tracks the number of balls in play separately from the actual balls
        on the playfield because there are numerous situations where the two
        counts are not the same. For example, if a ball is in a VUK while some
        animation is playing, there are no balls on the playfield but still one
        ball in play, or if the player has a two-ball multiball and they shoot
        them both into locks, there are still two balls in play even though
        there are no balls on the playfield. The opposite can also be true,
        like when the player tilts then there are still balls on the playfield
        but no balls in play.

        Explanation of the player_controlled parameter:

        Set player_controlled to True to indicate that MPF should wait for the
        player to eject the ball from the source_device rather than firing a
        coil. The logic works like this:

        If the source_device does not have an eject_coil defined, then it's
        assumed that player_controlled is the only option. (e.g. this is a
        traditional plunger.) If the source_device does have an eject_coil
        defined, then there are two ways the eject could work. (1) there could
        be a "launch" button of some kind that's used to fire the eject coil,
        or (2) the device could be the auto/manual combo style where there's a
        mechanical plunger but also a coil which can eject the ball.

        If player_controlled is true and the device has an eject_coil, MPF will
        look for the player_controlled_eject_tag and eject the ball when a
        switch with that tag is activated.

        If there is no player_controlled_eject_tag, MPF assumes it's a manual
        plunger and will wait for the ball to disappear from the device based
        on the device's ball count decreasing.

        """
        if balls < 1:
            self.log.error("Received request to add %s balls, which doesn't "
                           "make sense. Not adding any balls...")
            return False

        # Figure out which device we'll get a ball from

        if source_device:
            pass
        elif source_name and source_name in self.machine.ball_devices:
            source_device = self.machine.ball_devices[source_name]
        else:
            for device in self.machine.ball_devices.items_tagged(
                    'ball_add_live'):
                if self in device.config['eject_targets']:
                    source_device = device
                    break

        if not source_device:
            self.log.critical("Received request to add a ball to the playfield"
                              ", but no source device was passed and no ball "
                              "devices are tagged with 'ball_add_live'. Cannot"
                              " add a ball.")
            return False

        if reset:
            source_device.stop()

        self.log.debug(
            "Received request to add %s ball(s). Source device: %s."
            " Player-controlled: %s", balls, source_device.name,
            player_controlled)

        if player_controlled:
            source_device.setup_player_controlled_eject(balls=balls,
                                                        target=self)
        else:
            source_device.eject(balls=balls, target=self, get_ball=True)

        return True

    def mark_playfield_active(self):
        self.machine.events.post_boolean(self.name + "_active")

    def playfield_switch_hit(self, **kwargs):
        """A switch tagged with '<this playfield name>_active' was just hit,
        indicating that there is at least one ball on the playfield.

        """
        if (not self.balls
                or (kwargs.get('balls') and self.balls - kwargs['balls'] < 0)):
            self.mark_playfield_active()

            if not self.num_balls_requested:
                if self.machine.game:
                    self.unexpected_balls = 1

                if self.machine.config['machine']['glass_off_mode']:
                    self.log.debug("Playfield_active switch hit with no balls "
                                   "expected. glass_off_mode is enabled, so "
                                   "this will be ignored.")
                else:
                    self.log.debug("Playfield_active switch hit with no balls "
                                   "expected. glass_off_mode is not enabled, "
                                   "setting playfield ball count to 1")

                    self.balls = 1
                    self.machine.events.post('unexpected_ball_on_' + self.name)

    # def _ball_added_handler(self, balls):
    #     self.log.debug("%s ball(s) added to the playfield", balls)
    #     self.balls += balls

    def _ball_removed_handler(self, balls, **kwargs):
        self._count_consistent = False
        # somebody got a ball from us so we obviously had one
        self.machine.events.post('sw_' + self.name + "_active",
                                 callback=self._ball_removed_handler2,
                                 balls=balls)

    def _ball_removed_handler2(self, balls):
        self.log.debug("%s ball(s) removed from the playfield", balls)
        self.balls -= balls
        self._count_consistent = True

    def _source_device_eject_attempt(self, balls, target, **kwargs):
        # A source device is attempting to eject a ball. We need to know if it's
        # headed to the playfield.
        if target == self:
            self.log.debug(
                "A source device is attempting to eject %s ball(s)"
                " to the playfield.", balls)
            self.num_balls_requested += balls

    def _source_device_eject_failed(self, balls, target, **kwargs):
        # A source device failed to eject a ball. We need to know if it was
        # headed to the playfield.
        if target == self:
            self.log.debug(
                "A source device has failed to eject %s ball(s)"
                " to the playfield.", balls)
            self.num_balls_requested -= balls

    def _source_device_eject_success(self, balls, target):
        # A source device has just confirmed that it has successfully ejected a
        # ball. Note that we don't care what type of confirmation it used.
        # (Playfield switch hit, count of its ball switches, etc.)

        if target == self:
            self.log.debug(
                "A source device has confirmed it's ejected %s "
                "ball(s) to the playfield.", balls)
            self.balls += balls
            self.num_balls_requested -= balls

            if self.num_balls_requested < 0:
                self.log.critical(
                    "num_balls_requested is %s, which doesn't "
                    "make sense. Quitting...", self.num_balls_requested)
                raise Exception(
                    "num_balls_requested is %s, which doesn't make "
                    "sense. Quitting...", self.num_balls_requested)

    def ok_to_confirm_ball_via_playfield_switch(self):
        """Used to check whether it's ok for a ball device which ejects to the
        playfield to confirm its eject via a playfield switch being hit.

        Returns: True or False

        Right now this is simple. If there are no playfield balls, then any
        playfield switch hit is assumed to be from the newly-ejected ball. If
        there are other balls on the playfield, then we can't use this
        confirmation method since we don't know whether a playfield switch hit
        is from the newly-ejected ball(s) or a current previously-live
        playfield ball.

        """
        if not self.balls:
            return True
        else:
            return False

        # todo look for other incoming balls?

    # BALL SEARCH --------------------------------------------------------------

    # todo make ball search work with plunger lanes with no switches. i.e. we
    # don't want ball search to start until a switch is hit?

    def ball_search_schedule(self, secs=None, force=False):
        """Schedules a ball search to start. By default it will schedule it
        based on the time configured in the machine configuration files.

        If a ball search is already scheduled, this method will reset that
        schedule to the new time passed.

        Args:
            secs: Schedules the ball search that many secs from now.
            force : Boolean to force a ball search. Set True to force a ball
                search. Otherwise it will only schedule it if
                self.flag_no_ball_search is False. Default is False

        """
        if self.machine.config['ball_search']:
            if not self.flag_no_ball_search or force is True:
                if secs is not None:
                    start_ms = secs * 1000
                else:
                    start_ms = (self.machine.config['ball_search']
                                ['secs until ball search start'] * 1000)
                self.log.debug("Scheduling a ball search for %s secs from now",
                               start_ms / 1000.0)
                self.delay.reset("ball_search_start",
                                 ms=start_ms,
                                 callback=self.ball_search_begin)

    def ball_search_disable(self):
        """Disables ball search.

        Note this is used to prevent a future ball search from happening (like
        when all balls become contained). This method is not used to cancel an
        existing ball search. (Use `ball_search_end` for that.)

        """
        self.log.debug("Disabling Ball Search")
        self.delay.remove('ball_search_start')

    def ball_search_begin(self, force=False):
        """Begin the ball search process"""
        if not self.flag_no_ball_search:
            self.log.debug("Received request to start ball search")
            # ball search should only start if we have uncontained balls
            if self.balls or force:
                # todo add audit
                self.flag_ball_search_in_progress = True
                self.machine.events.post("ball_search_begin_phase1")
                # todo set delay to start phase 2

            else:
                self.log.debug("We got request to start ball search, but we "
                               "have no balls uncontained. WTF??")

    def ball_search_failed(self):
        """Ball Search did not find the ball."""
        self.log.debug("Ball Search failed to find a ball. Disabling.")
        self.ball_search_end()
        self.ball_lost()

    def ball_search_end(self):
        """End the ball search, either because we found the ball or
        are giving up."""
        self.log.debug("Ball search ending")
        self.flag_ball_search_in_progress = False
        self.machine.events.post("ball_search_end")
        # todo cancel the delay for phase 2 if we had one

    def ball_lost(self):
        """Mark a ball as lost"""
        self.num_balls_known = self.balls
        self.num_balls_missing = (
            self.machine.config['machine']['balls installed'] - self.balls)
        self.num_balls_live = 0
        # since desired count doesn't change, this will relaunch them
        self.log.debug("Ball(s) Marked Lost. Known: %s, Missing: %s",
                       self.num_balls_known, self.num_balls_missing)

        # todo audit balls lost

    def ball_found(self, num=1):
        """Used when a previously missing ball is found. Updates the balls
        known and balls missing variables.

        Parameters
        ----------

        num : int
            Specifies how many balls have been found. Default is 1.

        """
        self.log.debug("HEY!! We just found %s lost ball(s).", num)
        self.num_balls_known += num
        self.num_balls_missing -= num
        self.log.debug("New ball counts. Known: %s, Missing: %s",
                       self.num_balls_known, self.num_balls_missing)
        self.ball_update_all_counts()  # is this necessary? todo

    def eject(self, *args, **kwargs):
        pass

    def eject_all(self, *args, **kwargs):
        pass

    def is_playfield(self):
        return True

    def add_incoming_ball(self, source):
        pass

    def remove_incoming_ball(self, source):
        pass
Exemplo n.º 11
0
class BallSave(Device):

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

    def __init__(self, machine, name, config, collection=None, validate=True):
        super(BallSave, self).__init__(machine,
                                       name,
                                       config,
                                       collection,
                                       validate=validate)

        self.delay = DelayManager()
        self.enabled = False
        self.saves_remaining = 0

        if self.config['balls_to_save'] == -1:
            self.unlimited_saves = True
        else:
            self.unlimited_saves = False

        self.source_playfield = self.config['source_playfield']

        # todo change the delays to timers so we can add pause and extension
        # events, but that will require moving timers out of mode conde

    def enable(self, **kwargs):
        if self.enabled:
            return

        self.saves_remaining = self.config['balls_to_save']
        self.enabled = True
        self.log.debug("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))

    def disable(self, **kwargs):
        if not self.enabled:
            return

        self.enabled = False
        self.log.debug("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))

    def timer_start(self, **kwargs):
        if self.config['active_time'] > 0:
            if self.debug:
                self.log.debug('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):
        if self.debug:
            self.log.debug("Starting Hurry Up")

        self.machine.events.post('ball_save_{}_hurry_up'.format(self.name))

    def _grace_period(self):
        if self.debug:
            self.log.debug("Starting Grace Period")

        self.machine.events.post('ball_save_{}_grace_period'.format(self.name))

    def _ball_drain_while_active(self, balls, **kwargs):
        if balls <= 0:
            return {'balls': balls}

        no_balls_in_play = False

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

        if no_balls_in_play:
            self.log.debug("Received request to save ball, but no balls are in"
                           " play. Discarding request.")
            return {'balls': balls}

        self.log.debug(
            "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)

        self.source_playfield.add_ball(
            balls=balls, player_controlled=self.config['auto_launch'] ^ 1)

        if not self.unlimited_saves:
            self.saves_remaining -= balls
            if self.debug:
                self.log.debug("Saves remaining: %s", self.saves_remaining)
        elif self.debug:
            self.log.debug("Unlimited Saves enabled")

        if self.saves_remaining <= 0:
            if self.debug:
                self.log.debug("Disabling since there are no saves remaining")
            self.disable()

        return {'balls': 0}

    def remove(self):
        if self.debug:
            self.log.debug("Removing...")

        self.disable()
Exemplo n.º 12
0
class BallController(object):
    """Base class for the Ball Controller which is used to keep track of all
    the balls in a pinball machine.

    Parameters
    ----------

    machine : :class:`MachineController`
        A reference to the instance of the MachineController object.

    """
    def __init__(self, machine):
        self.machine = machine
        self.log = logging.getLogger("BallController")
        self.log.debug("Loading the BallController")
        self.delay = DelayManager()

        self.game = None

        # Properties:
        # self.num_balls_contained
        # self.num_balls_live
        # self.num_balls_desired_live

        self._num_balls_live = 0  # do not update this. Use the property
        self._num_balls_desired_live = 0  # do not update. Use the property
        self._num_balls_known = -999  # do not update. Use the property

        self.num_balls_in_transit = 0
        # Balls currently in transit from one ball device to another
        # Not currently implemented

        self.num_balls_missing = 0
        # Balls lost and/or not installed.

        self.flag_ball_search_in_progress = False
        #True if there's currently a ball search in progress.

        self.flag_no_ball_search = False
        #Ball search is enabled and disabled automatically based on whether
        #any balls are uncontained. Set this flag_no_ball_search to True if for
        #some reason you don't want the ball search to be enabled. BTW I can't
        #think of why you'd ever want this. The automatic stuff works great, and
        #you need to keep it enabled even after tilts and stuff. Maybe for some
        #kind of maintainance mode or something?

        # register for events
        self.machine.events.add_handler('request_to_start_game',
                                        self.request_to_start_game)
        self.machine.events.add_handler('sw_ballLive', self.ball_live_hit)
        self.machine.events.add_handler('machine_reset_phase_2', self.reset)
        self.machine.events.add_handler('timer_tick', self._tick)

    @property
    def num_balls_contained(self):
        balls = 0
        for device in self.machine.balldevices:
            balls += device.num_balls_contained
            if balls > self._num_balls_known:
                self.num_balls_known = balls
        if balls < 0:
            return -999
        else:
            return balls
        # todo figure out how to do this with a generator

    @property
    def num_balls_live(self):
        return self._num_balls_live

    @num_balls_live.setter
    def num_balls_live(self, balls):
        """The number of balls that are actually live (i.e. loose and bouncing
        around) at this moment.

        This is not necessarily the same as the number of balls in play that the
        Game object tracks. For example during a game if the player shoots a
        ball into a ball device, there will be no live balls during that moment
        even though there is still a ball in play. And if the player tilts,
        there will be no balls in play but we'll still have balls live until
        they all roll into the drain.
        """

        prior_count = self._num_balls_live

        if balls > 0:
            self._num_balls_live = balls
            self.ball_search_schedule()
        else:
            self._num_balls_live = 0
            self.ball_search_disable()

        self.log.debug("New Live Ball Count: %s. (Prior count: %s)",
                       self._num_balls_live, prior_count)

        # todo add support for finding missing balls

    @property
    def num_balls_desired_live(self):
        return self._num_balls_desired_live

    @num_balls_desired_live.setter
    def num_balls_desired_live(self, balls):
        """How many balls the ball controller will try to keep live. If this
        number is ever greater than the number live, the ball controller will
        automatically add more live. If it's less, then the ball controller will
        not automatically eject a ball if it enters the drain device.
        """

        prior_count = self._num_balls_desired_live

        if balls > 0:
            self._num_balls_desired_live = balls
        else:
            self._num_balls_desired_live = 0

        self.log.debug("New Desired Live Ball Count: %s. (Prior count: %s)",
                       self._num_balls_desired_live, prior_count)

        # todo should we ensure that this value never exceed the number of balls
        # known? Or is that a crutch that will obscure programming errors?
        # I think we should do it but then log it as a warning.

    @property
    def num_balls_known(self):
        if self.num_balls_contained > self._num_balls_known:
            self._num_balls_known = self.num_balls_contained

        return self._num_balls_known

    @num_balls_known.setter
    def num_balls_known(self, balls):
        """How many balls the machine knows about. Could vary from the number
        of balls installed based on how many are *actually* in the machine, or
        to compensate for balls that are lost or stuck.
        """
        self._num_balls_known = balls

    def reset(self):
        """Resets the BallController.

        Current this just gets an initial count of the balls and sends all the
        balls to their 'home' position.
        """

        # If there are no ball devices, then the ball controller has no work to
        # do and will create errors, so we just abort.
        if not hasattr(self.machine, 'balldevices'):
            return

        self.num_balls_known = self.num_balls_contained

        # remove any old handlers
        self.machine.events.remove_handler(self._ball_add_live_handler)

        # add handlers to watch for balls ejected to the playfield
        for device in self.machine.balldevices:

            if device.config['confirm_eject_type'] != 'device':
                # This device ejects to the playfield

                self.machine.events.add_handler(
                    'balldevice_' + device.name + '_ball_eject_attempt',
                    self._ball_add_live_handler)
                self.machine.events.add_handler(
                    'balldevice_' + device.name + '_ball_eject_failed',
                    self._ball_remove_live_handler)
            if 'drain' in device.tags:  # device is used to drain balls from pf
                self.machine.events.add_handler(
                    'balldevice_' + device.name + '_ball_enter',
                    self._ball_drained_handler)

            if not device.config['feeder_device']:
                # This device receives balls from the playfield
                self.machine.events.add_handler('balldevice_' + device.name +
                                                '_ball_enter',
                                                self._ball_remove_live_handler,
                                                priority=100)

        if 'Allow start with loose balls' not in self.machine.config['Game']:
            self.machine.config['Game']['Allow start with loose balls'] = False

        # todo where do we figure out balls missing?
        self.num_balls_live = 0
        self.num_balls_desired_live = 0

    def set_live_count(self,
                       balls=None,
                       from_tag='ball_add_live',
                       device=None):
        """Tells the ball controller how many balls you want live."""
        self.log.debug(
            "Setting desired live to: %s. (from_tag: %s, device: %s)", balls,
            from_tag, device)
        self.log.debug("Previous desired live count: %s",
                       self.num_balls_desired_live)

        if balls is not None:
            self.num_balls_desired_live = balls

        if self.num_balls_desired_live <= self.num_balls_live:
            # no live balls to add
            return

        balls_to_add = self.num_balls_desired_live - self.num_balls_live

        # set which ball device we're working with
        if not device:
            device = self.machine.balldevices.items_tagged('ball_add_live')[0]
            self.log.debug("Will add ball from device: %s", device.name)
            # todo what if there isn't one? Need a clean error

        # can we eject from this device? Grab a ball if not
        if not device.num_balls_contained:
            self.log.debug("Asking device %s to stage 1 ball", device.name)
            device.num_balls_desired = 1  # this will stage a ball

        self.log.debug("Subtracting 1 ball from %s's desired count",
                       device.name)
        device.num_balls_desired -= balls_to_add

        # todo need to check how many balls ejectable this device has, and go
        # to another device if this one can't serve them all

    def add_live(self, balls=1, from_tag='ball_add_live', device=None):
        """Tells the ball controller to add a live ball.

        This method ensures you're not adding more balls live than you have
        available.

        By default it will try to add the ball(s) from devices tagged with
        'ball_add_live'.

        This is a convenience method which calls set_live_count()
        """
        self.log.debug(
            "Received request to add %s live ball(s). Current "
            "desired live:  %s", balls, self.num_balls_desired_live)
        if (self.num_balls_desired_live < self.num_balls_known and
                self.num_balls_desired_live + balls <= self.num_balls_known):

            self.set_live_count(self.num_balls_desired_live + balls, from_tag,
                                device)
            return True

        elif self.num_balls_desired_live + balls > self.num_balls_known:
            self.log.warning("Live ball request exceeds number of known balls")
            self.set_live_count(self.num_balls_known, from_tag, device)
            # should we return something here? I guess None is ok?

        else:
            self.log.debug("Cannot set new live ball count.")
            return False

    def stage_ball(self, tag='ball_add_live'):
        """Makes sure that ball devices with the tag passed have a ball."""

        for device in self.machine.balldevices.items_tagged(tag):
            device.num_balls_desired = 1

    def _tick(self):
        # ticks once per game loop. Tries to keep the number of live balls
        # matching the number of balls in play

        if self.num_balls_desired_live < 0:
            self.log.debug("Warning. num_balls_desired_live is negative. "
                           "Resetting to 0.")
            # todo found a lost ball??
            self.num_balls_desired_live = 0
            # todo change num_balls_desired_live to a property?

        if self.num_balls_live != self.num_balls_desired_live:
            self.log.debug("(tick) Current Balls Live: %s, Balls Desired: %s",
                           self.num_balls_live, self.num_balls_desired_live)
            self.set_live_count()

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

        Checks to make sure that the balls are in all the right places and
        returns. If too many balls are missing (based on the config files 'Min
        Balls' setting), it will return False to reject the game start request.
        """
        self.log.debug("Received request to start game.")
        self.log.debug("Balls contained: %s, Min balls needed: %s",
                       self.num_balls_contained,
                       self.machine.config['Machine']['Min Balls'])
        if self.num_balls_contained < self.machine.config['Machine'][
                'Min Balls']:
            self.log.debug(
                "BallController denies game start. Not enough balls")
            return False

        if self.machine.config['Game']['Allow start with loose balls']:
            return

        elif not self.are_balls_gathered(['home', 'trough']):
            self.gather_balls('home')
            self.log.debug("BallController denies game start. Balls are not in"
                           " their home positions.")
            return False

    def are_balls_gathered(self, target=['home', 'trough']):
        """Checks to see if all the balls are contained in devices tagged with
        the parameter that was passed.

        Note if you pass a target that's not used in any ball devices, this
        method will return True. (Because you're asking if all balls are
        nowhere, and they always are. :)

        Args:
            target: String value of the tag you'd like to check. Default is
            'home'
        """

        self.log.debug(
            "Checking to see if all the balls are in devices tagged"
            " with '%s'", target)

        if type(target) is str:
            target = [target]

        count = 0
        devices = set()

        for tag in target:
            for device in self.machine.balldevices.items_tagged(tag):
                devices.add(device)

        if len(devices) == 0:
            # didn't find any devices matching that tag, so we return True
            return True

        for device in devices:
            count += device.get_status('num_balls_contained')

        if count == self.machine.ball_controller.num_balls_known:
            self.log.debug("Yes, all balls are gathered")
            return True
        else:
            self.log.debug("No, all balls are not gathered")
            return False

    def gather_balls(self, target='home', antitarget=None):
        """Used to ensure that all balls are in (or not in) ball devices with
        the tag you pass.

        Typically this would be used after a game ends, or when the machine is
        reset or first starts up, to ensure that all balls are in devices
        tagged with 'home'.

        Args:
            target: A string of the tag name of the ball devices you want all
                the balls to end up in. Default is 'home'.
            antitarget: The opposite of target. Will eject all balls from
                all devices with the string you pass. Default is None.

        Note you can't pass both a target and antitarget in the same call. (If
        you do it will just use the target and ignore the antitarget.)

        TODO: Add support to actually move balls into position. e.g. STTNG, the
        lock at the top of the playfield wants to hold a ball before a game
        starts, so when a game ends the machine will auto eject one from the
        plunger with the diverter set so it's held in the rear lock.
        """

        if not antitarget:
            # todo do we add the option of making the target a list?
            self.log.debug("Gathering all balls to devices tagged '%s'",
                           target)
            for device in self.machine.balldevices:
                if (target in device.tags):
                    device.num_balls_desired = device.config['ball_capacity']
                else:
                    device.num_balls_desired = 0

        elif antitarget:
            self.log.debug("Emptying balls from devices tagged '%s'",
                           antitarget)
            for device in self.machine.devices:
                if (target in device.tags):
                    device.num_balls_desired = 0
                else:
                    device.num_balls_desired = device.config['ball_capacity']

    def _ball_add_live_handler(self, balls):
        # Event handler which watches for device eject attempts to add
        # live balls

        if not balls:
            return

        # If our previous desired count was less or equal to our live count,
        # then this eject should increase the desired count. Why? Because
        # whatever caused this eject wants there to be more balls desired.

        # If the previous desired count was higher than this eject, then the
        # desired count shouldn't change, as these balls are fulfilling its
        # missing desired balls.

        # todo potential bug: What if prior desired was higher than prior live,
        # and we get a new live increase which takes it above the prior desired?
        # I *think* that should never happen since the ball controller would
        # try to launch a new ball if live fell below desired, but it's possible
        # we could get into this situation depending on staging times and stuff.

        # Let's log this as a warning for now and revisit this later.

        if ((self.num_balls_desired_live > self.num_balls_live) and balls >
            (self.num_balls_desired_live > self.num_balls_live)):
            self.log.warning("Ball add deficit warning. See note in "
                             "_ball_add_live_handler() in ball_controller.py")

        if self.num_balls_desired_live <= self.num_balls_live:
            self.num_balls_desired_live += balls

        self.num_balls_live += balls

        self.machine.events.post('ball_live_added',
                                 total_live=self.num_balls_live)

    def _ball_remove_live_handler(self, balls=1):
        # Event handler which watches for device ball entry events
        self.num_balls_live -= balls
        self.num_balls_desired_live -= balls

        self.machine.events.post('ball_live_removed',
                                 total_live=self.num_balls_live)

    def _ball_drained_handler(self, balls):
        # This is a special handler which is called when balls enter devices
        # tagged with drain. It posts a ball_drain event and automatically
        # decrements the desired_balls_live counter.
        self.log.debug(
            "Ball Drain Handler. Previous desired live: %s. Will "
            "decrement by 1 and post 'ball_drain' relay event.",
            self.num_balls_desired_live)

        if not self.machine.tilted:
            self.num_balls_desired_live -= balls

            self.machine.events.post('ball_drain',
                                     ev_type='relay',
                                     callback=self._process_ball_drained,
                                     balls=balls)

        else:  # received a drain while tilted
            self.machine.events.post('tilted_ball_drain')

    def _process_ball_drained(self, balls=None, ev_result=None):
        # We don't need to do anything here because other modules (ball save,
        # the game, etc. should jump in and do whatever they need to do when a
        # ball is drained.
        pass

    def ball_live_hit(self):
        """A ball just hit a playfield switch.

        This means we have a ball loose on the playfield. (It doesn't
        necessarily mean that ball is "live," as this could happen as a ball
        rolls towards the drain after a tilt.)

        This method is mainly used to continuously push out the start time of
        the ball search. If this method is called when a ball search is in
        progress, it will end the it. (Since this method means we found the
        stuck ball.)

        Note you shouldn't have to call this method manually. The switch
        controller will do it automatically each time a switch tagged with
        'ball_live' has been activated.

        """
        if self.num_balls_live:
            self.ball_search_schedule()
        if self.flag_ball_search_in_progress:
            self.log.debug("Just got a live playfield hit during ball search, "
                           "so we're ending ball search.")
            self.ball_search_end()

    '''
     ____        _ _    _____                     _
    |  _ \      | | |  / ____|                   | |
    | |_) | __ _| | | | (___   ___  __ _ _ __ ___| |__
    |  _ < / _` | | |  \___ \ / _ \/ _` | '__/ __| '_ \
    | |_) | (_| | | |  ____) |  __/ (_| | | | (__| | | |
    |____/ \__,_|_|_| |_____/ \___|\__,_|_|  \___|_| |_|

    The following code interfaces with the ball search module (which actually
    performs the ball search). Here's the interface if you want to write your
    own:

    MPF will post events when it wants certain things to happen, like
    "ball_search_begin_1"
    "ball_search_begin_2"
    "ball_search_end"

    You can use the following methods from our machine controller. (These
    examples assume it's in your ball search module as self.machine.)

    self.machine.get_balldevice_status()

    Returns a dictionary of ball devices, with the device object as the key
    and the number of balls it contains at the value.

    If you want to access a balldevice, they're accessible via:
    self.machine.balldevices[<device>].x

    Valid methods incude eject()
    With force=True to force it to fire the eject coil even if it thinks
    there's no ball.

    # todo that should trigger an eject in progress which the game can use to
    figure out where the ball came from.

    This ball search module is not reponsible for "finding" a ball. It just
    manages all the actions that take place during a search. The MPF Ball
    Controller will "find" any balls that are knocked loose and will then
    cancel the search.

    If you create your own, you should receive the instance of the machine_
    controller as an init paramter.

    You can fire coils via self.machine.coils[<coilname>].pulse()
    '''

    # todo need to think about soft delay switches (flippers)

    def ball_search_schedule(self, secs=None, force=False):
        """Schedules a ball search to start. By default it will schedule it
        based on the time configured in the machine configuration files.

        If a ball search is already scheduled, this method will reset that
        schedule to the new time passed.

        Parameters
        ----------

        secs : into
            Schedules the ball search that many secs from now.

        force : bool
            Set True to force a ball search. Otherwise it will only schedule it
            if self.flag_no_ball_search is False

        """
        if self.machine.config['BallSearch']:
            if not self.flag_no_ball_search or force is True:
                if secs is not None:
                    start_ms = secs * 1000
                else:
                    start_ms = (self.machine.config['BallSearch']
                                ['Secs until ball search start'] * 1000)
                self.log.debug("Scheduling a ball search for %s secs from now",
                               start_ms / 1000.0)
                self.delay.reset("ball_search_start",
                                 ms=start_ms,
                                 callback=self.ball_search_begin)

    def ball_search_disable(self):
        """Disables ball search.

        Note this is used to prevent a future ball
        search from happening (like when all balls become contained.) This
        method is not used to cancel an existing ball search. (Use
        ball_search_end for that.)

        """
        self.log.debug("Disabling Ball Search")
        self.delay.remove('ball_search_start')

    def ball_search_begin(self, force=False):
        """Begin the ball search process"""
        if not self.flag_no_ball_search:
            self.log.debug("Received request to start ball search")
            # ball search should only start if we have uncontained balls
            if self.num_balls_live or force:
                # todo add audit
                self.flag_ball_search_in_progress = True
                self.machine.events.post("ball_search_begin_phase1")
                # todo set delay to start phase 2

            else:
                self.log.debug("We got request to start ball search, but we "
                               "have no balls uncontained. WTF??")

    def ball_search_failed(self):
        """Ball Search did not find the ball."""
        self.log.debug("Ball Search failed to find a ball. Disabling.")
        self.ball_search_end()
        self.ball_lost()

    def ball_search_end(self):
        """End the ball search, either because we found the ball or
        are giving up."""
        self.log.debug("Ball search ending")
        self.flag_ball_search_in_progress = False
        self.machine.events.post("ball_search_end")
        # todo cancel the delay for phase 2 if we had one

    def ball_lost(self):
        """Mark a ball as lost"""
        self.num_balls_known = self.num_balls_contained
        self.num_balls_missing = self.machine.config['Machine']\
            ['Balls Installed'] - self.num_balls_contained
        self.num_balls_live = 0
        # since desired count doesn't change, this will relaunch them
        self.log.debug("Ball(s) Marked Lost. Known: %s, Missing: %s",
                       self.num_balls_known, self.num_balls_missing)

        # todo audit balls lost

    def ball_found(self, num=1):
        """Used when a previously missing ball is found. Updates the balls
        known and balls missing variables.

        Parameters
        ----------

        num : int
            Specifies how many balls have been found. Default is 1.

        """
        self.log.debug("HEY!! We just found %s lost ball(s).", num)
        self.num_balls_known += num
        self.num_balls_missing -= num
        self.log.debug("New ball counts. Known: %s, Missing: %s",
                       self.num_balls_known, self.num_balls_missing)
        self.ball_update_all_counts()  # is this necessary? todo
Exemplo n.º 13
0
Arquivo: shots.py Projeto: jabdoa2/mpf
class SequenceShot(Shot):

    def __init__(self, machine, name, config, priority):
        """SequenceShot is where you need certain switches to be hit in the
        right order, possibly within a time limit.

        Subclass of `Shot`

        Args:
            machine: The MachineController object
            name: String name of this shot.
            config: Dictionary that holds the configuration for this shot.

        """
        super(SequenceShot, self).__init__(machine, name, config, priority)

        self.delay = DelayManager()

        self.progress_index = 0
        """Tracks how far along through this sequence the current shot is."""

        # convert our switches config to a list
        if 'switches' in self.config:
            self.config['switches'] = \
                Config.string_to_list(self.config['switches'])

        # convert our timout to ms
        if 'time' in self.config:
            self.config['time'] = Timing.string_to_ms(self.config['time'])
        else:
            self.config['time'] = 0

        self.active_delay = False

        self.enable()

    def enable(self):
        """Enables the shot. If it's not enabled, the switch handlers aren't
        active and the shot event will not be posted."""

        super(SequenceShot, self).enable()

        # create the switch handlers
        for switch in self.config['switches']:
            self.machine.switch_controller.add_switch_handler(
                switch, self._switch_handler, return_info=True)
        self.progress_index = 0

    def disable(self):
        """Disables the shot. If it's disabled, the switch handlers aren't
        active and the shot event will not be posted."""

        super(SequenceShot, self).disable()

        for switch in self.config['switches']:
            self.machine.switch_controller.remove_switch_handler(
                switch, self.switch_handler)
        self.progress_index = 0

    def _switch_handler(self, switch_name, state, ms):
        # does this current switch meet the next switch in the progress index?
        if switch_name == self.config['switches'][self.progress_index]:

            # are we at the end?
            if self.progress_index == len(self.config['switches']) - 1:
                self.confirm_shot()
            else:
                # does this shot specific a time limit?
                if self.config['time']:
                    # do we need to set a delay?
                    if not self.active_delay:
                        self.delay.reset(name='shot_timer',
                                         ms=self.config['time'],
                                         callback=self.reset)
                        self.active_delay = True

                # advance the progress index
                self.progress_index += 1

    def confirm_shot(self):
        """Called when the shot is complete to confirm and reset it."""
        # kill the delay
        self.delay.remove('shot_timer')
        # reset our shot
        self.reset()

        self.shot_made()

    def reset(self):
        """Resets the progress without disabling the shot."""
        self.log.debug("Resetting this shot")
        self.progress_index = 0
        self.active_delay = False
Exemplo n.º 14
0
class Playfield(BallDevice):
    def __init__(self, machine, name, collection):
        self.log = logging.getLogger('Playfield')

        self.machine = machine
        self.name = name
        self.tags = list()
        self.config = defaultdict(lambda: None)
        self.config['eject_targets'] = list()

        self.ball_controller = self.machine.ball_controller

        self.delay = DelayManager()

        # Add the playfield ball device to the existing device collection
        collection_object = getattr(self.machine, collection)[name] = self

        # Attributes
        self._balls = 0
        self.num_balls_requested = 0
        self.player_controlled_eject_in_progress = None
        self.queued_balls = list()

        # Set up event handlers

        # Watch for balls added to the playfield
        for device in self.machine.balldevices:
            for target in device.config['eject_targets']:
                if target == self.name:
                    self.machine.events.add_handler(
                        event='balldevice_' + device.name +
                        '_ball_eject_success',
                        handler=self._source_device_eject_success)
                    self.machine.events.add_handler(
                        event='balldevice_' + device.name +
                        '_ball_eject_attempt',
                        handler=self._source_device_eject_attempt)
                break

        # Watch for balls removed from the playfield
        self.machine.events.add_handler('balldevice_captured_from_playfield',
                                        self._ball_removed_handler)

        # Watch for any switch hit which indicates a ball on the playfield
        self.machine.events.add_handler('sw_playfield_active',
                                        self.playfield_switch_hit)

    @property
    def balls(self):
        return self._balls

    @balls.setter
    def balls(self, balls):

        prior_balls = self._balls
        ball_change = balls - prior_balls

        if ball_change:
            self.log.debug(
                "Ball count change. Prior: %s, Current: %s, Change: "
                "%s", prior_balls, balls, ball_change)

        if balls > 0:
            self._balls = balls
            #self.ball_search_schedule()
        elif balls == 0:
            self._balls = 0
            #self.ball_search_disable()
        else:
            self.log.warning(
                "Playfield balls went to %s. Resetting to 0, but "
                "FYI that something's weird", balls)
            self._balls = 0
            #self.ball_search_disable()

        self.log.debug("New Ball Count: %s. (Prior count: %s)", self._balls,
                       prior_balls)

        if ball_change > 0:
            self.machine.events.post_relay('balldevice_' + self.name +
                                           '_ball_enter',
                                           balls=ball_change)

        if ball_change:
            self.machine.events.post('playfield_ball_count_change',
                                     balls=balls,
                                     change=ball_change)

    def count_balls(self, **kwargs):
        """Used to count the number of balls that are contained in a ball
        device. Since this is the playfield device, this method always returns
        zero.

        Returns: 0
        """
        return 0

    def get_additional_ball_capacity(self):
        """Used to find out how many more balls this device can hold. Since this
        is the playfield device, this method always returns 999.

        Returns: 999
        """
        return 999

    def add_ball(self,
                 balls=1,
                 source_name=None,
                 source_device=None,
                 trigger_event=None):
        """Adds live ball(s) to the playfield.

        Args:
            balls: Integer of the number of balls you'd like to add.
            source_name: Optional string name of the ball device you'd like to
                add the ball(s) from.
            source_device: Optional ball device object you'd like to add the
                ball(s) from.
            trigger_event: The optional name of an event that MPF will wait for
                before adding the ball into play. Typically used with player-
                controlled eject tag events. If None, the ball will be added
                immediately.

        Returns:
            True if it's able to process the add_ball() request, False if it
            cannot.

        Both source_name and source_device args are included to give you two
        options for specifying the source of the ball(s) to be added. You don't
        need to supply both. (it's an "either/or" thing.) Both of these args are
        optional, so if you don't supply them then MPF will look for a device
        tagged with 'ball_add_live'. If you don't provide a source and you don't
        have a device with the 'ball_add_live' tag, MPF will quit.

        This method does *not* increase the game controller's count of the
        number of balls in play. So if you want to add balls (like in a
        ball scenario(, you need to call this method along with
        ``self.machine.game.add_balls_in_play()``.)

        MPF tracks the number of balls in play separately from the actual balls
        on the playfield because there are numerous situations where the two
        counts are not the same. For example, if a ball is in a VUK while some
        animation is playing, there are no balls on the playfield but still one
        ball in play, or if the player has a two-ball multiball and they shoot
        them both into locks, there are still two balls in play even though
        there are no balls on the playfield, or if the player tilts then there
        are still balls on the playfield but no balls in play.

        """

        if balls < 1:
            self.log.error("Received request to add %s balls, which doesn't "
                           "make sense. Not adding any balls...")
            return False

        # Figure out which device we'll get a ball from

        if source_device:
            pass
        elif source_name and source_name in self.machine.balldevices:
            source_device = self.machine.balldevices[source_name]
        else:
            for device in self.machine.balldevices.items_tagged(
                    'ball_add_live'):
                source_device = device
                break

        if not source_device:
            self.log.critical(
                "Received request to add a ball to the playfield, "
                "but no source device was passed and no ball "
                "devices are tagged with 'ball_add_live'. Cannot "
                "add a ball.")
            raise Exception("Received request to add a ball to the playfield, "
                            "but no source device was passed and no ball "
                            "devices are tagged with 'ball_add_live'. Cannot "
                            "add a ball.")

        # If there's a player controlled eject in progress for this device, we
        # hold this request until it's over.
        if self.player_controlled_eject_in_progress == source_device:
            self.queued_balls.append(
                (balls, source_name, source_device, trigger_event))
            self.log.debug(
                "An add_ball() request came in while there was a "
                "current player-controlled eject in progress for the"
                "same device. Will queue the eject request")
            return True

        self.log.debug(
            "Received request to add %s ball(s). Source device: %s. "
            "Wait for event: %s", balls, source_device.name, trigger_event)

        # If we don't have a coil that's fired by the player, and we our source
        # device has the ability to eject, then we do the eject now.

        # Some examples:

        # Plunger lane w/ switch and coil: ball_add_live device is plunger lane,
        # we don't eject now since *not* player_controlled is true.

        # Plunger lane w/ switch. No coil: ball_add_live device is plunger lane,
        # we don't eject now since there's no eject_coil for that device.

        # Plunger lane has no switch: ball_add_live device is trough, we do
        # eject now since there's no player_controlled tag and the device has an
        # eject coil.

        if trigger_event and source_device.config['eject_coil']:
            self.setup_player_controlled_eject(balls, source_device,
                                               trigger_event)

        else:
            # if there's no trigger, eject right away
            # if there's no eject coil, that's ok. We still need to setup the
            # eject so the device will be expecting the ball to disappear
            source_device.eject(balls=balls, target=self, get_ball=True)

        return True

    def setup_player_controlled_eject(self, balls, device, trigger_event):
        """Used to set up an eject from a ball device which will eject a ball to
        the playfield.

        Args:
            balls: Integer of the number of balls this device should eject.
            device: The ball device object that will eject the ball(s) when a
                switch with the player-controlled eject tag is hit.
            trigger_event: The name of the MPF event that will trigger the
                eject.

        When this method it called, MPF will set up an event handler to look for
        the trigger_event.
        """

        self.log.debug(
            "Setting up a player controlled eject. Balls: %s, Device"
            ": %s, Trigger Event: %s", balls, device, trigger_event)

        if not device.balls:
            device.request_ball(balls=balls)

        self.machine.events.add_handler(trigger_event,
                                        self.player_eject_request,
                                        balls=balls,
                                        device=device)

        self.player_controlled_eject_in_progress = device

    def remove_player_controlled_eject(self):
        """Removed the player-controlled eject so a player hitting a switch
        no longer calls the device(s) to eject a ball.
        """

        self.log.debug("Removing player-controlled eject.")

        self.machine.events.remove_handler(self.player_eject_request)
        self.player_controlled_eject_in_progress = None

        # Need to do this in case one of these queued balls is also a player
        # controlled eject which would re-add it to the queue while iterating.
        # So we clear it and pass them all to add_ball() and then let the queue
        # rebuild with what's left if it needs to.
        ball_list = self.queued_balls
        self.queued_balls = list()

        for item in ball_list:
            self.add_ball(balls=item[0],
                          source_name=item[1],
                          source_device=item[2],
                          trigger_event=item[3])

    def player_eject_request(self, balls, device):
        """A player has hit a switch tagged with the player_eject_request_tag.

        Args:
            balls: Integer of the number of balls that will be ejected.
            device: The ball device object that will eject the ball(s).
        """

        self.log.debug("Received player eject request. Balls: %s, Device: %s",
                       balls, device.name)
        device.eject(balls, target=self)

    def playfield_switch_hit(self):
        """A switch tagged with 'playfield_active' was just hit, indicating that
        there is at least one ball on the playfield.
        """
        if not self.balls:

            if not self.num_balls_requested:
                self.log.debug("PF switch hit with no balls expected. Setting "
                               "pf balls to 1.")
                self.balls = 1
                self.machine.events.post('unexpected_ball_on_playfield')

    def _ball_added_handler(self, balls):
        self.log.debug("%s ball(s) added to the playfield", balls)
        self.balls += balls

    def _ball_removed_handler(self, balls):
        self.log.debug("%s ball(s) removed from the playfield", balls)
        self.balls -= balls

    def _source_device_eject_attempt(self, balls, target, **kwargs):
        # A source device is attempting to eject a ball. We need to know if it's
        # headed to the playfield.
        if target == self:
            self.log.debug(
                "A source device is attempting to ejected %s ball(s)"
                " to the playfield.", balls)
            self.num_balls_requested += balls

    def _source_device_eject_success(self, balls, target):
        # A source device has just confirmed that it has successfully ejected a
        # ball. Note that we don't care what type of confirmation it used.
        # (Playfield switch hit, count of its ball switches, etc.)

        if target == self:
            self.log.debug(
                "A source device has confirmed it's ejected %s "
                "ball(s) to the playfield.", balls)
            self.balls += balls
            self.num_balls_requested -= balls

            if self.num_balls_requested < 0:
                self.log.critical(
                    "num_balls_requested is %s, which doesn't "
                    "make sense. Quitting...", self.num_balls_requested)
                raise Exception(
                    "num_balls_requested is %s, which doesn't make "
                    "sense. Quitting...", self.num_balls_requested)

            self.remove_player_controlled_eject()

    def ok_to_confirm_ball_via_playfield_switch(self):
        """Used to check whether it's ok for a ball device which ejects to the
        playfield to confirm its eject via a playfield switch being hit.

        Returns: True or False

        Right now this is simple. If there are no playfield balls, then any
        playfield switch hit is assumed to be from the newly-ejected ball. If
        there are other balls on the playfield, then we can't use this
        confirmation method since we don't know whether a playfield switch hit
        is from the newly-ejected ball(s) or a current previously-live
        playfield ball.
        """
        if not self.balls:
            return True
        else:
            return False

        # todo look for other incoming balls?

    # BALL SEARCH --------------------------------------------------------------

    # todo make ball search work with plunger lanes with no switches. i.e. we
    # don't want ball search to start until a switch is hit?

    def ball_search_schedule(self, secs=None, force=False):
        """Schedules a ball search to start. By default it will schedule it
        based on the time configured in the machine configuration files.

        If a ball search is already scheduled, this method will reset that
        schedule to the new time passed.

        Args:
            secs: Schedules the ball search that many secs from now.
            force : Boolean to force a ball search. Set True to force a ball
                search. Otherwise it will only schedule it if
                self.flag_no_ball_search is False. Default is False

        """
        if self.machine.config['ballsearch']:
            if not self.flag_no_ball_search or force is True:
                if secs is not None:
                    start_ms = secs * 1000
                else:
                    start_ms = (self.machine.config['ballsearch']
                                ['secs until ball search start'] * 1000)
                self.log.debug("Scheduling a ball search for %s secs from now",
                               start_ms / 1000.0)
                self.delay.reset("ball_search_start",
                                 ms=start_ms,
                                 callback=self.ball_search_begin)

    def ball_search_disable(self):
        """Disables ball search.

        Note this is used to prevent a future ball search from happening (like
        when all balls become contained). This method is not used to cancel an
        existing ball search. (Use `ball_search_end` for that.)

        """
        self.log.debug("Disabling Ball Search")
        self.delay.remove('ball_search_start')

    def ball_search_begin(self, force=False):
        """Begin the ball search process"""
        if not self.flag_no_ball_search:
            self.log.debug("Received request to start ball search")
            # ball search should only start if we have uncontained balls
            if self.balls or force:
                # todo add audit
                self.flag_ball_search_in_progress = True
                self.machine.events.post("ball_search_begin_phase1")
                # todo set delay to start phase 2

            else:
                self.log.debug("We got request to start ball search, but we "
                               "have no balls uncontained. WTF??")

    def ball_search_failed(self):
        """Ball Search did not find the ball."""
        self.log.debug("Ball Search failed to find a ball. Disabling.")
        self.ball_search_end()
        self.ball_lost()

    def ball_search_end(self):
        """End the ball search, either because we found the ball or
        are giving up."""
        self.log.debug("Ball search ending")
        self.flag_ball_search_in_progress = False
        self.machine.events.post("ball_search_end")
        # todo cancel the delay for phase 2 if we had one

    def ball_lost(self):
        """Mark a ball as lost"""
        self.num_balls_known = self.balls
        self.num_balls_missing = self.machine.config['machine']\
            ['balls installed'] - self.balls
        self.num_balls_live = 0
        # since desired count doesn't change, this will relaunch them
        self.log.debug("Ball(s) Marked Lost. Known: %s, Missing: %s",
                       self.num_balls_known, self.num_balls_missing)

        # todo audit balls lost

    def ball_found(self, num=1):
        """Used when a previously missing ball is found. Updates the balls
        known and balls missing variables.

        Parameters
        ----------

        num : int
            Specifies how many balls have been found. Default is 1.

        """
        self.log.debug("HEY!! We just found %s lost ball(s).", num)
        self.num_balls_known += num
        self.num_balls_missing -= num
        self.log.debug("New ball counts. Known: %s, Missing: %s",
                       self.num_balls_known, self.num_balls_missing)
        self.ball_update_all_counts()  # is this necessary? todo

    def eject(self, *args, **kwargs):
        pass

    def eject_all(self, *args, **kwargs):
        pass