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 Bonus(Scriptlet):

    def on_load(self):
        self.queue = None
        self.delay = DelayManager()
        self.bonus_lights = list()
        self.bonus_value = 0

        # register a hook into ball_ending
        self.machine.events.add_handler('ball_ending', self.prepare_bonus, 2)

    def prepare_bonus(self, queue):
        self.log.debug("Entering the Big Shot Bonus Sequence")

        if self.machine.tilted:
            self.log.debug("Ball has tilted. No bonus for you!")
            return

        self.set_bonus_value()

        # calculate the bonus value for this ball
        for target in self.machine.drop_target_banks['Solids'].drop_targets:
            if target.complete:
                self.log.debug("Drop Target '%s' is down", target.name)
                self.bonus_lights.append(target.config['light'])

        if self.machine.lights['ball8'].state['brightness']:
            self.log.debug("Eight Ball Target was hit")
            self.bonus_lights.append(self.machine.lights['ball8'])

        for target in self.machine.drop_target_banks['Stripes'].drop_targets:
            if target.complete:
                self.log.debug("Drop Target '%s' is down", target.name)
                self.bonus_lights.append(target.config['light'])

        # if we have bonus to do, register a ball_ending wait
        if self.bonus_lights:
            self.log.debug("Registering a wait since we have bonus light(s)")
            self.queue = queue
            self.queue.wait()

            reels = self.machine.score_reel_controller.active_scorereelgroup

            # Check to see if any of the score reels are busy
            if not reels.valid:
                self.log.debug("Found a score reel group that's not valid. "
                               "We'll wait...")
                self.machine.events.add_handler(
                    'scorereelgroup_' + reels.name + '_valid',
                    self.start_bonus)
            else:
                self.log.debug("Reels are valid. Starting now")
                # If they're not busy, start the bonus now
                self.start_bonus()

    def start_bonus(self, **kwargs):
        # remove the handler that we used to wait for the score reels to be done
        self.machine.events.remove_handler(self.start_bonus)

        reels = self.machine.score_reel_controller.active_scorereelgroup

        # add the handler that will advance through these bonus steps
        self.machine.events.add_handler('scorereelgroup_' + reels.name +
                                        '_valid', self.bonus_step)

        # do the first bonus step to kick off this process
        self.bonus_step()

    def bonus_step(self, **kwargs):
        # automatically called when the score reels are valid

        if self.bonus_lights:
            # sets the "pause" between bonus scores, then does the bonus step
            self.delay.add('bonus', 200, self.do_bonus_step)

        else:
            self.bonus_done()

    def do_bonus_step(self):
        this_light = self.bonus_lights.pop()
        self.machine.score.add(self.bonus_value, force=True)
        this_light.off()

        # if this is the 8 ball, also turn off the top 8 ball lane light
        if this_light.name == 'ball8':
            self.machine.lights['eightBall500'].off()

    def set_bonus_value(self):
        # Figures out what the bonus score value is based on what ball this is
        # and how many balls the game is set to.

        balls_remaining = (self.machine.config['Game']['Balls per game'] -
                           self.machine.game.player.vars['ball'])

        if balls_remaining > 1:
            self.bonus_value = (self.machine.config['Scoring']
                                ['bonusValue']['Score'])
        elif balls_remaining == 1:
            self.bonus_value = (self.machine.config['Scoring']
                                ['secondToLastBonusValue']['Score'])
        else:
            self.bonus_value = (self.machine.config['Scoring']
                                ['lastBonusValue']['Score'])

    def bonus_done(self):
        self.log.debug("Bonus is done. Clearing queue")
        # Remove any event handlers we were waiting for
        self.machine.events.remove_handler(self.bonus_step)

        self.queue.clear()

    def stop(self):
        pass
Exemplo n.º 3
0
class Mode(object):
    """Parent class for in-game mode code."""

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

        self.log = logging.getLogger('Mode.' + name)

        self.delay = DelayManager()

        self.priority = 0
        self._active = False
        self._mode_start_wait_queue = None
        self.stop_methods = list()
        self.timers = dict()
        self.start_callback = None
        self.stop_callback = None
        self.event_handlers = set()
        self.switch_handlers = list()
        self.mode_start_kwargs = dict()
        self.mode_stop_kwargs = dict()
        self.mode_devices = set()

        self.player = None
        '''Reference to the current player object.'''

        self._validate_mode_config()

        self.configure_mode_settings(config.get('mode', dict()))

        self.auto_stop_on_ball_end = self.config['mode']['stop_on_ball_end']
        '''Controls whether this mode is stopped when the ball ends,
        regardless of its stop_events settings.
        '''

        self.restart_on_next_ball = self.config['mode']['restart_on_next_ball']
        '''Controls whether this mode will restart on the next ball. This only
        works if the mode was running when the ball ended. It's tracked per-
        player in the '_restart_modes_on_next_ball' untracked player variable.
        '''

        for asset_manager in self.machine.asset_managers.values():

            config_data = self.config.get(asset_manager.config_section, dict())

            self.config[asset_manager.config_section] = (
                asset_manager.register_assets(config=config_data,
                                              mode_path=self.path))

        # Call registered remote loader methods
        for item in self.machine.mode_controller.loader_methods:
            if (item.config_section and
                    item.config_section in self.config and
                    self.config[item.config_section]):
                item.method(config=self.config[item.config_section],
                            mode_path=self.path,
                            **item.kwargs)
            elif not item.config_section:
                item.method(config=self.config, mode_path=self.path,
                            **item.kwargs)

        self.mode_init()

    def __repr__(self):
        return '<Mode.{}>'.format(self.name)

    @property
    def active(self):
        return self._active

    @active.setter
    def active(self, active):
        if self._active != active:
            self._active = active
            self.machine.mode_controller._active_change(self, self._active)

    def configure_mode_settings(self, config):
        """Processes this mode's configuration settings from a config
        dictionary.
        """

        self.config['mode'] = self.machine.config_processor.process_config2(
            config_spec='mode', source=config, section_name='mode')

        for event in self.config['mode']['start_events']:
            self.machine.events.add_handler(event=event, handler=self.start,
                priority=self.config['mode']['priority'] +
                self.config['mode']['start_priority'])

    def _validate_mode_config(self):
        for section in self.machine.config['mpf']['mode_config_sections']:
            this_section = self.config.get(section, None)

            if this_section:
                if type(this_section) is dict:
                    for device, settings in this_section.iteritems():
                        self.config[section][device] = (
                            self.machine.config_processor.process_config2(
                                section, settings))

                else:
                    self.config[section] = (
                        self.machine.config_processor.process_config2(section,
                        this_section))

    def _get_merged_settings(self, section_name):
        # Returns a dict_merged dict of a config section from the machine-wide
        # config with the mode-specific config merged in.

        if section_name in self.machine.config:
            return_dict = copy.deepcopy(self.machine.config[section_name])
        else:
            return_dict = CaseInsensitiveDict()

        if section_name in self.config:
            return_dict = Util.dict_merge(return_dict,
                                          self.config[section_name],
                                          combine_lists=False)

        return return_dict

    def start(self, priority=None, callback=None, **kwargs):
        """Starts this mode.

        Args:
            priority: Integer value of what you want this mode to run at. If you
                don't specify one, it will use the "Mode: priority" setting from
                this mode's configuration file.
            **kwargs: Catch-all since this mode might start from events with
                who-knows-what keyword arguments.

        Warning: You can safely call this method, but do not override it in your
        mode code. If you want to write your own mode code by subclassing Mode,
        put whatever code you want to run when this mode starts in the
        mode_start method which will be called automatically.
        """

        self.log.debug("Received request to start")

        if self._active:
            self.log.debug("Mode is already active. Aborting start")
            return

        if self.config['mode']['use_wait_queue'] and 'queue' in kwargs:

            self.log.debug("Registering a mode start wait queue")

            self._mode_start_wait_queue = kwargs['queue']
            self._mode_start_wait_queue.wait()

        if type(priority) is int:
            self.priority = priority
        else:
            self.priority = self.config['mode']['priority']

        self.start_event_kwargs = kwargs

        self.log.info('Mode Starting. Priority: %s', self.priority)

        self._create_mode_devices()

        self.log.debug("Registering mode_stop handlers")

        # register mode stop events
        if 'stop_events' in self.config['mode']:

            for event in self.config['mode']['stop_events']:
                # stop priority is +1 so if two modes of the same priority
                # start and stop on the same event, the one will stop before
                # the other starts
                self.add_mode_event_handler(event=event, handler=self.stop,
                    priority=self.priority + 1 +
                    self.config['mode']['stop_priority'])

        self.start_callback = callback

        if 'timers' in self.config:
            self._setup_timers()

        self.log.debug("Calling mode_start handlers")

        for item in self.machine.mode_controller.start_methods:
            if item.config_section in self.config or not item.config_section:
                self.stop_methods.append(
                    item.method(config=self.config.get(item.config_section,
                                                       self.config),
                                priority=self.priority,
                                mode=self,
                                **item.kwargs))

        self._setup_device_control_events()

        self.machine.events.post_queue(event='mode_' + self.name + '_starting',
                                       callback=self._started)

    def _started(self):
        # Called after the mode_<name>_starting queue event has finished.

        self.log.debug('Mode Started. Priority: %s', self.priority)

        self.active = True

        self._start_timers()

        self.machine.events.post('mode_' + self.name + '_started',
                                 callback=self._mode_started_callback)

    def _mode_started_callback(self, **kwargs):
        # Called after the mode_<name>_started queue event has finished.
        self.mode_start(**self.start_event_kwargs)

        self.start_event_kwargs = dict()

        if self.start_callback:
            self.start_callback()

        self.log.debug('Mode Start process complete.')

    def stop(self, callback=None, **kwargs):
        """Stops this mode.

        Args:
            **kwargs: Catch-all since this mode might start from events with
                who-knows-what keyword arguments.

        Warning: You can safely call this method, but do not override it in your
        mode code. If you want to write your own mode code by subclassing Mode,
        put whatever code you want to run when this mode stops in the
        mode_stop method which will be called automatically.
        """

        if not self._active:
            return

        self.mode_stop_kwargs = kwargs

        self.log.debug('Mode Stopping.')

        self._remove_mode_switch_handlers()

        self.stop_callback = callback

        self._kill_timers()
        self.delay.clear()

        # self.machine.events.remove_handler(self.stop)
        # todo is this ok here? Or should we only remove ones that we know this
        # mode added?

        self.machine.events.post_queue(event='mode_' + self.name + '_stopping',
                                       callback=self._stopped)

    def _stopped(self):
        self.log.debug('Mode Stopped.')

        self.priority = 0
        self.active = False

        for item in self.stop_methods:
            try:
                item[0](item[1])
            except TypeError:
                try:
                    item()
                except TypeError:
                    pass

        self.stop_methods = list()

        self.machine.events.post('mode_' + self.name + '_stopped',
                                 callback=self._mode_stopped_callback)

        if self._mode_start_wait_queue:

            self.log.debug("Clearing wait queue")

            self._mode_start_wait_queue.clear()
            self._mode_start_wait_queue = None

    def _mode_stopped_callback(self, **kwargs):
        self._remove_mode_event_handlers()
        self._remove_mode_devices()

        self.mode_stop(**self.mode_stop_kwargs)

        self.mode_stop_kwargs = dict()

        if self.stop_callback:
            self.stop_callback()

    def _create_mode_devices(self):
        # Creates new devices that are specified in a mode config that haven't
        # been created in the machine-wide config

        self.log.debug("Scanning config for mode-based devices")

        for collection_name, device_class in (
                self.machine.device_manager.device_classes.iteritems()):
            if device_class.config_section in self.config:
                for device, settings in (
                        self.config[device_class.config_section].iteritems()):

                    collection = getattr(self.machine, collection_name)

                    if device not in collection:  # no existing device, create

                        self.log.debug("Creating mode-based device: %s",
                                       device)

                        # TODO this config is already validated, so add
                        # something so it doesn't validate it again?

                        self.machine.device_manager.create_devices(
                            collection.name, {device: settings},
                            validate=False)

                        # change device from str to object
                        device = collection[device]

                        # Track that this device was added via this mode so we
                        # can remove it when the mode ends.
                        self.mode_devices.add(device)

                        # This lets the device know it was created by a mode
                        # instead of machine-wide, as some devices want to do
                        # certain things here. We also pass the player object
                        # in case this device wants to do something with that
                        # too.
                        device.device_added_to_mode(mode=self,
                                                    player=self.player)

    def _remove_mode_devices(self):

        for device in self.mode_devices:
            device.remove()

        self.mode_devices = set()

    def _setup_device_control_events(self):
        # registers mode handlers for control events for all devices specified
        # in this mode's config (not just newly-created devices)

        self.log.debug("Scanning mode-based config for device control_events")

        device_list = set()

        for event, method, delay, device in (
                self.machine.device_manager.get_device_control_events(
                self.config)):

            try:
                event, priority = event.split('|')
            except ValueError:
                priority = 0

            self.add_mode_event_handler(
                event=event,
                handler=self._control_event_handler,
                priority=self.priority + 2 + int(priority),
                callback=method,
                ms_delay=delay)

            device_list.add(device)

        for device in device_list:
            device.control_events_in_mode(self)

    def _control_event_handler(self, callback, ms_delay=0, **kwargs):

        self.log.debug("_control_event_handler: callback: %s,", callback)

        if ms_delay:
            self.delay.add(name=callback, ms=ms_delay, callback=callback,
                           mode=self)
        else:
            callback(mode=self)

    def add_mode_event_handler(self, event, handler, priority=1, **kwargs):
        """Registers an event handler which is automatically removed when this
        mode stops.

        This method is similar to the Event Manager's add_handler() method,
        except this method automatically unregisters the handlers when the mode
        ends.

        Args:
            event: String name of the event you're adding a handler for. Since
                events are text strings, they don't have to be pre-defined.
            handler: The method that will be called when the event is fired.
            priority: An arbitrary integer value that defines what order the
                handlers will be called in. The default is 1, so if you have a
                handler that you want to be called first, add it here with a
                priority of 2. (Or 3 or 10 or 100000.) The numbers don't matter.
                They're called from highest to lowest. (i.e. priority 100 is
                called before priority 1.)
            **kwargs: Any any additional keyword/argument pairs entered here
                will be attached to the handler and called whenever that handler
                is called. Note these are in addition to kwargs that could be
                passed as part of the event post. If there's a conflict, the
                event-level ones will win.

        Returns:
            A GUID reference to the handler which you can use to later remove
            the handler via ``remove_handler_by_key``. Though you don't need to
            remove the handler since the whole point of this method is they're
            automatically removed when the mode stops.

        Note that if you do add a handler via this method and then remove it
        manually, that's ok too.
        """

        key = self.machine.events.add_handler(event, handler, priority,
                                              mode=self, **kwargs)

        self.event_handlers.add(key)

        return key

    def _remove_mode_event_handlers(self):
        for key in self.event_handlers:
            self.machine.events.remove_handler_by_key(key)
        self.event_handlers = set()

    def _remove_mode_switch_handlers(self):
        for handler in self.switch_handlers:
            self.machine.switch_controller.remove_switch_handler(
                switch_name=handler['switch_name'],
                callback=handler['callback'],
                state=handler['state'],
                ms=handler['ms'])
        self.switch_handlers = list()

    def _setup_timers(self):
        # config is localized

        for timer, settings in self.config['timers'].iteritems():

            self.timers[timer] = ModeTimer(machine=self.machine, mode=self,
                                           name=timer, config=settings)

        return self._kill_timers

    def _start_timers(self):
        for timer in self.timers.values():
            if timer.running:
                timer.start()

    def _kill_timers(self, ):
        for timer in self.timers.values():
            timer.kill()

        self.timers = dict()

    def mode_init(self):
        """User-overrideable method which will be called when this mode
        initializes as part of the MPF boot process.
        """
        pass

    def mode_start(self, **kwargs):
        """User-overrideable method which will be called whenever this mode
        starts (i.e. whenever it becomes active).
        """
        pass

    def mode_stop(self, **kwargs):
        """User-overrideable method which will be called whenever this mode
        stops (i.e. whenever it becomes inactive).
        """
        pass
Exemplo n.º 4
0
class Snux(object):
    def __init__(self, machine, platform):
        self.log = logging.getLogger('Platform.Snux')
        self.delay = DelayManager()

        self.machine = machine
        self.platform = platform

        self.system11_config = None
        self.snux_config = None
        self.ac_relay_delay_ms = 100
        self.special_drivers = set()

        self.diag_led = None
        '''Diagnostics LED (LED 3) on the Snux board turns on solid when MPF
        first connects, then starts flashing once the MPF init is done.'''
        self.ac_relay = None
        self.flipper_relay = None
        self.ac_relay_enabled = False  # disabled = A, enabled = C

        self.a_side_queue = set()
        self.c_side_queue = set()

        self.a_drivers = set()
        self.c_drivers = set()

        self.a_side_done_time = 0
        self.c_side_done_time = 0
        self.drivers_holding_a_side = set()
        self.drivers_holding_c_side = set()
        # self.a_side_busy = False  # This is a property
        # self.c_side_active = False  # This is a property
        self.a_side_enabled = True
        self.c_side_enabled = False

        self.ac_relay_in_transition = False

        self._morph()

    @property
    def a_side_busy(self):
        if (self.drivers_holding_a_side or self.a_side_done_time > time.time()
                or self.a_side_queue):
            return True
        else:
            return False

    @property
    def c_side_active(self):
        if self.drivers_holding_c_side or self.c_side_done_time > time.time():
            return True
        else:
            return False

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

    def _morph(self):
        self.platform_configure_driver = self.platform.configure_driver
        self.platform.configure_driver = self.configure_driver

        self.platform_write_hw_rule = self.platform.write_hw_rule
        self.platform.write_hw_rule = self.write_hw_rule

    def initialize(self):
        """Automatically called by the Platform class after all the system
        modules are loaded.

        """
        self._validate_config()

        self.log.debug("Configuring Snux Diag LED for driver %s",
                       self.snux_config['diag_led_driver_number'])

        self.diag_led, _ = self.platform_configure_driver({
            'number':
            self.snux_config['diag_led_driver_number'],
            'allow_enable':
            True
        })

        self.diag_led.log.info = self.null_log_handler
        self.diag_led.log.debug = self.null_log_handler

        self.diag_led.enable()

        self.special_drivers.add(
            self.snux_config['diag_led_driver_number'].lower())

        self.log.debug("Configuring A/C Select Relay for driver %s",
                       self.system11_config['ac_relay_driver_number'])

        self.ac_relay, _ = self.platform_configure_driver({
            'number':
            self.system11_config['ac_relay_driver_number'],
            'allow_enable':
            True
        })

        self.special_drivers.add(
            self.system11_config['ac_relay_driver_number'].lower())

        self.log.debug(
            "Configuring A/C Select Relay transition delay for "
            "%sms", self.system11_config['ac_relay_delay_ms'])

        self.ac_relay_delay_ms = self.system11_config['ac_relay_delay_ms']

        self.flipper_relay, _ = self.platform_configure_driver({
            'number':
            self.snux_config['flipper_enable_driver_number'],
            'allow_enable':
            True
        })

        self.log.debug("Configuring Flipper Enable for driver %s",
                       self.snux_config['flipper_enable_driver_number'])

        self.machine.events.add_handler('init_phase_5',
                                        self._initialize_phase_2)

    def _initialize_phase_2(self):
        self.machine.timing.add(
            Timer(callback=self.flash_diag_led, frequency=0.5))

        self.machine.events.add_handler('timer_tick', self._tick)

    def _validate_config(self):
        self.system11_config = self.machine.config_processor.process_config2(
            'system11', self.machine.config['system11'])

        snux = self.machine.config.get('snux', dict())
        self.snux_config = self.machine.config_processor.process_config2(
            'snux', snux)

    def _tick(self):
        # Called based on the timer_tick event
        if self.a_side_queue:
            self._service_a_side()
        elif self.c_side_queue:
            self._service_c_side()
        elif self.c_side_enabled and not self.c_side_active:
            self._enable_a_side()

    def flash_diag_led(self):
        self.diag_led.pulse(250)

    def configure_driver(self, config, device_type='coil'):
        # If the user has configured one of the special drivers in their
        # machine config, don't set it up since that could let them do weird
        # things.
        if config['number'].lower() in self.special_drivers:
            return

        orig_number = config['number']

        if (config['number'].lower().endswith('a')
                or config['number'].lower().endswith('c')):

            config['number'] = config['number'][:-1]

            platform_driver, _ = (self.platform_configure_driver(
                config, device_type))

            snux_driver = SnuxDriver(orig_number, platform_driver, self)

            if orig_number.lower().endswith('a'):
                self._add_a_driver(snux_driver.platform_driver)
            elif orig_number.lower().endswith('c'):
                self._add_c_driver(snux_driver.platform_driver)

            return snux_driver, orig_number

        else:
            return self.platform_configure_driver(config, device_type)

    def write_hw_rule(self, switch_obj, sw_activity, driver_obj, driver_action,
                      disable_on_release, drive_now,
                      **driver_settings_overrides):
        """On system 11 machines, Switched drivers cannot be configured with
        autofire hardware rules.
        
        """
        if driver_obj in self.a_drivers or driver_obj in self.c_drivers:
            self.log.warning("Received a request to set a hardware rule for a"
                             "switched driver. Ignoring")
        else:
            self.platform_write_hw_rule(switch_obj, sw_activity, driver_obj,
                                        driver_action, disable_on_release,
                                        drive_now, **driver_settings_overrides)

    def driver_action(self, driver, milliseconds):
        """Adds a driver action for a switched driver to the queue (for either
        the A-side or C-side queue).

        Args:
            driver: A reference to the original platform class Driver instance.
            milliseconds: Integer of the number of milliseconds this action is
                for. 0 = pulse, -1 = enable (hold), any other value is a timed
                action (either pulse or long_pulse)

        This action will be serviced immediately if it can, or ASAP otherwise.

        """
        if driver in self.a_drivers:
            self.a_side_queue.add((driver, milliseconds))
            self._service_a_side()
        elif driver in self.c_drivers:
            self.c_side_queue.add((driver, milliseconds))
            if not self.ac_relay_in_transition and not self.a_side_busy:
                self._service_c_side()

    def _enable_ac_relay(self):
        self.ac_relay.enable()
        self.ac_relay_in_transition = True
        self.a_side_enabled = False
        self.c_side_enabled = False
        self.delay.add(ms=self.ac_relay_delay_ms,
                       callback=self._c_side_enabled,
                       name='enable_ac_relay')

    def _disable_ac_relay(self):
        self.ac_relay.disable()
        self.ac_relay_in_transition = True
        self.a_side_enabled = False
        self.c_side_enabled = False
        self.delay.add(ms=self.ac_relay_delay_ms,
                       callback=self._a_side_enabled,
                       name='disable_ac_relay')

    # -------------------------------- A SIDE ---------------------------------

    def _enable_a_side(self):
        if not self.a_side_enabled and not self.ac_relay_in_transition:

            if self.c_side_active:
                self._disable_all_c_side_drivers()
                self.delay.add(ms=self.ac_relay_delay_ms,
                               callback=self._enable_a_side,
                               name='enable_a_side')
                return

            elif self.c_side_enabled:
                self._disable_ac_relay()

            else:
                self._a_side_enabled()

    def _a_side_enabled(self):
        self.ac_relay_in_transition = False
        self.a_side_enabled = True
        self.c_side_enabled = False
        self._service_a_side()

    def _service_a_side(self):
        if not self.a_side_queue:
            return

        elif not self.a_side_enabled:
            self._enable_a_side()
            return

        while self.a_side_queue:
            driver, ms = self.a_side_queue.pop()

            if ms > 0:
                driver.pulse(ms)
                self.a_side_done_time = max(self.a_side_done_time,
                                            time.time() + (ms / 1000.0))

            elif ms == -1:
                driver.enable()
                self.drivers_holding_a_side.add(driver)

            else:  # ms == 0
                driver.disable()
                try:
                    self.drivers_holding_a_side.remove(driver)
                except KeyError:
                    pass

    def _add_a_driver(self, driver):
        self.a_drivers.add(driver)

    # -------------------------------- C SIDE ---------------------------------

    def _enable_c_side(self):
        if (not self.ac_relay_in_transition and not self.c_side_enabled
                and not self.a_side_busy):
            self._enable_ac_relay()

        elif self.c_side_enabled and self.c_side_queue:
            self._service_c_side()

    def _c_side_enabled(self):
        self.ac_relay_in_transition = False

        if self.a_side_queue:
            self._enable_a_side()
            return

        self.a_side_enabled = False
        self.c_side_enabled = True
        self._service_c_side()

    def _service_c_side(self):
        if not self.c_side_queue:
            return

        if self.ac_relay_in_transition or self.a_side_busy:
            return

        elif not self.c_side_enabled:
            self._enable_c_side()
            return

        while self.c_side_queue:
            driver, ms = self.c_side_queue.pop()

            if ms > 0:
                driver.pulse(ms)
                self.c_side_done_time = max(self.c_side_done_time,
                                            time.time() + (ms / 1000.))
            elif ms == -1:
                driver.enable()
                self.drivers_holding_c_side.add(driver)

            else:  # ms == 0
                driver.disable()
                try:
                    self.drivers_holding_c_side.remove(driver)
                except KeyError:
                    pass

    def _add_c_driver(self, driver):
        self.c_drivers.add(driver)

    def _disable_all_c_side_drivers(self):
        if self.c_side_active:
            for driver in self.c_drivers:
                driver.disable()
            self.drivers_holding_c_side = set()
            self.c_side_done_time = 0
            self.c_side_enabled = False
Exemplo n.º 5
0
class SwitchPlayer(object):
    def __init__(self, machine):
        self.log = logging.getLogger('switch_player')

        if 'switch_player' not in machine.config:
            machine.log.debug('"switch_player:" section not found in '
                              'machine configuration, so the Switch Player'
                              'plugin will not be used.')
            return

        self.machine = machine
        self.delay = DelayManager()
        self.current_step = 0

        config_spec = '''
                        start_event: string|machine_reset_phase_3
                        start_delay: secs|0
                        '''

        self.config = Config.process_config(
            config_spec, self.machine.config['switch_player'])

        self.machine.events.add_handler(self.config['start_event'],
                                        self._start_event_callback)

        self.step_list = self.config['steps']
        self.start_delay = self.config['start_delay']

    def __repr__(self):
        return '<SwitchPlayer>'

    def _start_event_callback(self):

        if ('time' in self.step_list[self.current_step]
                and self.step_list[self.current_step]['time'] > 0):

            self.delay.add(name='switch_player_next_step',
                           ms=Timing.string_to_ms(
                               self.step_list[self.current_step]['time']),
                           callback=self._do_step)

    def _do_step(self):

        this_step = self.step_list[self.current_step]

        self.log.debug("Switch: %s, Action: %s", this_step['switch'],
                       this_step['action'])

        # send this step's switches
        if this_step['action'] == 'activate':
            self.machine.switch_controller.process_switch(this_step['switch'],
                                                          state=1,
                                                          logical=True)
        elif this_step['action'] == 'deactivate':
            self.machine.switch_controller.process_switch(this_step['switch'],
                                                          state=0,
                                                          logical=True)
        elif this_step['action'] == 'hit':
            self._hit(this_step['switch'])

        # inc counter
        if self.current_step < len(self.step_list) - 1:
            self.current_step += 1

            # schedule next step
            self.delay.add(name='switch_player_next_step',
                           ms=Timing.string_to_ms(
                               self.step_list[self.current_step]['time']),
                           callback=self._do_step)

    def _hit(self, switch):
        self.machine.switch_controller.process_switch(switch,
                                                      state=1,
                                                      logical=True)
        self.machine.switch_controller.process_switch(switch,
                                                      state=0,
                                                      logical=True)
Exemplo n.º 6
0
class SwitchPlayer(object):
    def __init__(self, machine):
        self.log = logging.getLogger("switch_player")

        if "switch_player" not in machine.config:
            machine.log.debug(
                '"switch_player:" section not found in '
                "machine configuration, so the Switch Player"
                "plugin will not be used."
            )
            return

        self.machine = machine
        self.delay = DelayManager()
        self.current_step = 0

        config_spec = """
                        start_event: string|machine_reset_phase_3
                        start_delay: secs|0
                        """

        self.config = Config.process_config(config_spec, self.machine.config["switch_player"])

        self.machine.events.add_handler(self.config["start_event"], self._start_event_callback)

        self.step_list = self.config["steps"]
        self.start_delay = self.config["start_delay"]

    def __repr__(self):
        return "<SwitchPlayer>"

    def _start_event_callback(self):

        if "time" in self.step_list[self.current_step] and self.step_list[self.current_step]["time"] > 0:

            self.delay.add(
                name="switch_player_next_step",
                ms=Timing.string_to_ms(self.step_list[self.current_step]["time"]),
                callback=self._do_step,
            )

    def _do_step(self):

        this_step = self.step_list[self.current_step]

        self.log.debug("Switch: %s, Action: %s", this_step["switch"], this_step["action"])

        # send this step's switches
        if this_step["action"] == "activate":
            self.machine.switch_controller.process_switch(this_step["switch"], state=1, logical=True)
        elif this_step["action"] == "deactivate":
            self.machine.switch_controller.process_switch(this_step["switch"], state=0, logical=True)
        elif this_step["action"] == "hit":
            self._hit(this_step["switch"])

        # inc counter
        if self.current_step < len(self.step_list) - 1:
            self.current_step += 1

            # schedule next step
            self.delay.add(
                name="switch_player_next_step",
                ms=Timing.string_to_ms(self.step_list[self.current_step]["time"]),
                callback=self._do_step,
            )

    def _hit(self, switch):
        self.machine.switch_controller.process_switch(switch, state=1, logical=True)
        self.machine.switch_controller.process_switch(switch, state=0, logical=True)
Exemplo n.º 7
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.º 8
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.º 9
0
class Counter(LogicBlock):
    """A type of LogicBlock that tracks multiple hits of a single event.

    This counter can be configured to track hits towards a specific end-goal
    (like number of tilt hits to tilt), or it can be an open-ended count (like
    total number of ramp shots).

    It can also be configured to count up or to count down, and can have a
    configurable counting interval.
    """

    # todo settle time

    def __init__(self, machine, name, player, config):
        self.log = logging.getLogger("Counter." + name)
        self.log.debug("Creating Counter LogicBlock")

        super(Counter, self).__init__(machine, name, player, config)

        self.delay = DelayManager()

        self.ignore_hits = False
        self.hit_value = -1

        config_spec = """
                        count_events: list|None
                        count_complete_value: int|0
                        multiple_hit_window: ms|0
                        count_interval: int|1
                        direction: string|up
                        starting_count: int|0
                      """

        self.config = Config.process_config(config_spec=config_spec, source=self.config)

        if "event_when_hit" not in self.config:
            self.config["event_when_hit"] = "counter_" + self.name + "_hit"

        if "player_variable" not in self.config:
            self.config["player_variable"] = self.name + "_count"

        self.hit_value = self.config["count_interval"]

        if self.config["direction"] == "down" and self.hit_value > 0:
            self.hit_value *= -1
        elif self.config["direction"] == "up" and self.hit_value < 0:
            self.hit_value *= -1

        self.player[self.config["player_variable"]] = self.config["starting_count"]

    def enable(self, **kwargs):
        """Enables this counter. Automatically called when one of the
        'enable_event's is posted. Can also manually be called.
        """
        super(Counter, self).enable()
        self.machine.events.remove_handler(self.hit)  # prevents multiples

        for event in self.config["count_events"]:
            self.handler_keys.add(self.machine.events.add_handler(event, self.hit))

    def reset(self, **kwargs):
        """Resets the hit progress towards completion"""
        super(Counter, self).reset(**kwargs)
        self.player[self.config["player_variable"]] = self.config["starting_count"]

    def hit(self, **kwargs):
        """Increases the hit progress towards completion. Automatically called
        when one of the `count_events`s is posted. Can also manually be
        called.
        """
        if not self.ignore_hits:
            self.player[self.config["player_variable"]] += self.hit_value
            self.log.debug("Processing Count change. Total: %s", self.player[self.config["player_variable"]])

            if self.config["count_complete_value"] is not None:

                if (
                    self.config["direction"] == "up"
                    and self.player[self.config["player_variable"]] >= self.config["count_complete_value"]
                ):
                    self.complete()

                elif (
                    self.config["direction"] == "down"
                    and self.player[self.config["player_variable"]] <= self.config["count_complete_value"]
                ):
                    self.complete()

            if self.config["event_when_hit"]:
                self.machine.events.post(
                    self.config["event_when_hit"], count=self.player[self.config["player_variable"]]
                )

            if self.config["multiple_hit_window"]:
                self.log.debug("Beginning Ignore Hits")
                self.ignore_hits = True
                self.delay.add("ignore_hits_within_window", self.config["multiple_hit_window"], self.stop_ignoring_hits)

    def stop_ignoring_hits(self, **kwargs):
        """Causes the Counter to stop ignoring subsequent hits that occur
        within the 'multiple_hit_window'. Automatically called when the window
        time expires. Can safely be manually called.
        """
        self.log.debug("Ending Ignore hits")
        self.ignore_hits = False
Exemplo n.º 10
0
Arquivo: snux.py Projeto: HarryXS/mpf
class Snux(object):

    def __init__(self, machine, platform):
        self.log = logging.getLogger('Platform.Snux')
        self.delay = DelayManager()

        self.machine = machine
        self.platform = platform

        self.system11_config = None
        self.snux_config = None
        self.ac_relay_delay_ms = 100
        self.special_drivers = set()

        self.diag_led = None
        '''Diagnostics LED (LED 3) on the Snux board turns on solid when MPF
        first connects, then starts flashing once the MPF init is done.'''
        self.ac_relay = None
        self.flipper_relay = None
        self.ac_relay_enabled = False  # disabled = A, enabled = C

        self.a_side_queue = set()
        self.c_side_queue = set()

        self.a_drivers = set()
        self.c_drivers = set()

        self.a_side_done_time = 0
        self.c_side_done_time = 0
        self.drivers_holding_a_side = set()
        self.drivers_holding_c_side = set()
        # self.a_side_busy = False  # This is a property
        # self.c_side_active = False  # This is a property
        self.a_side_enabled = True
        self.c_side_enabled = False

        self.ac_relay_in_transition = False

        self._morph()

    @property
    def a_side_busy(self):
        if (self.drivers_holding_a_side or
                    self.a_side_done_time > time.time() or
                    self.a_side_queue):
            return True
        else:
            return False

    @property
    def c_side_active(self):
        if self.drivers_holding_c_side or self.c_side_done_time > time.time():
            return True
        else:
            return False

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

    def _morph(self):
        self.platform_configure_driver = self.platform.configure_driver
        self.platform.configure_driver = self.configure_driver

        self.platform_write_hw_rule = self.platform.write_hw_rule
        self.platform.write_hw_rule = self.write_hw_rule

    def initialize(self):
        """Automatically called by the Platform class after all the system
        modules are loaded.

        """
        self._validate_config()

        self.log.debug("Configuring Snux Diag LED for driver %s",
                       self.snux_config['diag_led_driver_number'])

        self.diag_led, _ = self.platform_configure_driver(
            {'number': self.snux_config['diag_led_driver_number'],
             'allow_enable': True})

        self.diag_led.log.info = self.null_log_handler
        self.diag_led.log.debug = self.null_log_handler

        self.diag_led.enable()

        self.special_drivers.add(
            self.snux_config['diag_led_driver_number'].lower())

        self.log.debug("Configuring A/C Select Relay for driver %s",
                       self.system11_config['ac_relay_driver_number'])

        self.ac_relay, _ = self.platform_configure_driver(
            {'number': self.system11_config['ac_relay_driver_number'],
             'allow_enable': True})

        self.special_drivers.add(
            self.system11_config['ac_relay_driver_number'].lower())

        self.log.debug("Configuring A/C Select Relay transition delay for "
                       "%sms", self.system11_config['ac_relay_delay_ms'])

        self.ac_relay_delay_ms = self.system11_config['ac_relay_delay_ms']

        self.flipper_relay, _ = self.platform_configure_driver(
            {'number': self.snux_config['flipper_enable_driver_number'],
             'allow_enable': True})

        self.log.debug("Configuring Flipper Enable for driver %s",
                       self.snux_config['flipper_enable_driver_number'])

        self.machine.events.add_handler('init_phase_5',
                                        self._initialize_phase_2)

    def _initialize_phase_2(self):
        self.machine.timing.add(
            Timer(callback=self.flash_diag_led, frequency=0.5))

        self.machine.events.add_handler('timer_tick', self._tick)

    def _validate_config(self):
        self.system11_config = self.machine.config_processor.process_config2(
            'system11', self.machine.config['system11'])

        snux = self.machine.config.get('snux', dict())
        self.snux_config = self.machine.config_processor.process_config2(
            'snux', snux)

    def _tick(self):
        # Called based on the timer_tick event
        if self.a_side_queue:
            self._service_a_side()
        elif self.c_side_queue:
            self._service_c_side()
        elif self.c_side_enabled and not self.c_side_active:
            self._enable_a_side()

    def flash_diag_led(self):
        self.diag_led.pulse(250)

    def configure_driver(self, config, device_type='coil'):
        # If the user has configured one of the special drivers in their
        # machine config, don't set it up since that could let them do weird
        # things.
        if config['number'].lower() in self.special_drivers:
            return

        orig_number = config['number']

        if (config['number'].lower().endswith('a') or
                config['number'].lower().endswith('c')):

            config['number'] = config['number'][:-1]

            platform_driver, _ = (
                self.platform_configure_driver(config, device_type))

            snux_driver = SnuxDriver(orig_number, platform_driver, self)

            if orig_number.lower().endswith('a'):
                self._add_a_driver(snux_driver.platform_driver)
            elif orig_number.lower().endswith('c'):
                self._add_c_driver(snux_driver.platform_driver)

            return snux_driver, orig_number

        else:
            return self.platform_configure_driver(config, device_type)

    def write_hw_rule(self, switch_obj, sw_activity, driver_obj, driver_action,
                      disable_on_release, drive_now,
                      **driver_settings_overrides):
        """On system 11 machines, Switched drivers cannot be configured with
        autofire hardware rules.
        
        """
        if driver_obj in self.a_drivers or driver_obj in self.c_drivers:
            self.log.warning("Received a request to set a hardware rule for a"
                             "switched driver. Ignoring")
        else:
            self.platform_write_hw_rule(switch_obj, sw_activity, driver_obj,
                                        driver_action, disable_on_release,
                                        drive_now,
                                        **driver_settings_overrides)

    def driver_action(self, driver, milliseconds):
        """Adds a driver action for a switched driver to the queue (for either
        the A-side or C-side queue).

        Args:
            driver: A reference to the original platform class Driver instance.
            milliseconds: Integer of the number of milliseconds this action is
                for. 0 = pulse, -1 = enable (hold), any other value is a timed
                action (either pulse or long_pulse)

        This action will be serviced immediately if it can, or ASAP otherwise.

        """
        if driver in self.a_drivers:
            self.a_side_queue.add((driver, milliseconds))
            self._service_a_side()
        elif driver in self.c_drivers:
            self.c_side_queue.add((driver, milliseconds))
            if not self.ac_relay_in_transition and not self.a_side_busy:
                self._service_c_side()

    def _enable_ac_relay(self):
        self.ac_relay.enable()
        self.ac_relay_in_transition = True
        self.a_side_enabled = False
        self.c_side_enabled = False
        self.delay.add(ms=self.ac_relay_delay_ms,
                       callback=self._c_side_enabled,
                       name='enable_ac_relay')

    def _disable_ac_relay(self):
        self.ac_relay.disable()
        self.ac_relay_in_transition = True
        self.a_side_enabled = False
        self.c_side_enabled = False
        self.delay.add(ms=self.ac_relay_delay_ms,
                       callback=self._a_side_enabled,
                       name='disable_ac_relay')

    # -------------------------------- A SIDE ---------------------------------

    def _enable_a_side(self):
        if not self.a_side_enabled and not self.ac_relay_in_transition:

            if self.c_side_active:
                self._disable_all_c_side_drivers()
                self.delay.add(ms=self.ac_relay_delay_ms,
                               callback=self._enable_a_side,
                               name='enable_a_side')
                return

            elif self.c_side_enabled:
                self._disable_ac_relay()

            else:
                self._a_side_enabled()

    def _a_side_enabled(self):
        self.ac_relay_in_transition = False
        self.a_side_enabled = True
        self.c_side_enabled = False
        self._service_a_side()

    def _service_a_side(self):
        if not self.a_side_queue:
            return

        elif not self.a_side_enabled:
            self._enable_a_side()
            return

        while self.a_side_queue:
            driver, ms = self.a_side_queue.pop()

            if ms > 0:
                driver.pulse(ms)
                self.a_side_done_time = max(self.a_side_done_time,
                    time.time() + (ms / 1000.0))

            elif ms == -1:
                driver.enable()
                self.drivers_holding_a_side.add(driver)

            else:  # ms == 0
                driver.disable()
                try:
                    self.drivers_holding_a_side.remove(driver)
                except KeyError:
                    pass

    def _add_a_driver(self, driver):
        self.a_drivers.add(driver)

    # -------------------------------- C SIDE ---------------------------------

    def _enable_c_side(self):
        if (not self.ac_relay_in_transition and
                not self.c_side_enabled and
                not self.a_side_busy):
            self._enable_ac_relay()

        elif self.c_side_enabled and self.c_side_queue:
            self._service_c_side()

    def _c_side_enabled(self):
        self.ac_relay_in_transition = False

        if self.a_side_queue:
            self._enable_a_side()
            return

        self.a_side_enabled = False
        self.c_side_enabled = True
        self._service_c_side()

    def _service_c_side(self):
        if not self.c_side_queue:
            return

        if self.ac_relay_in_transition or self.a_side_busy:
            return

        elif not self.c_side_enabled:
            self._enable_c_side()
            return

        while self.c_side_queue:
            driver, ms = self.c_side_queue.pop()

            if ms > 0:
                driver.pulse(ms)
                self.c_side_done_time = max(self.c_side_done_time,
                    time.time() + (ms / 1000.))
            elif ms == -1:
                driver.enable()
                self.drivers_holding_c_side.add(driver)

            else:  # ms == 0
                driver.disable()
                try:
                    self.drivers_holding_c_side.remove(driver)
                except KeyError:
                    pass

    def _add_c_driver(self, driver):
        self.c_drivers.add(driver)

    def _disable_all_c_side_drivers(self):
        if self.c_side_active:
            for driver in self.c_drivers:
                driver.disable()
            self.drivers_holding_c_side = set()
            self.c_side_done_time = 0
            self.c_side_enabled = False
Exemplo n.º 11
0
class Diverter(Device):
    """Represents a diverter in a pinball machine.

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

    config_section = 'diverters'
    collection = 'diverters'

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

        self.delay = DelayManager()

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

        # configure defaults:
        if 'type' not in self.config:
            self.config['type'] = 'pulse'  # default to pulse to not fry coils
        if 'activation_time' not in self.config:
            self.config['activation_time'] = 0
        if 'activation_switches' in self.config:
            self.config['activation_switches'] = Config.string_to_list(
                self.config['activation_switches'])
        else:
            self.config['activation_switches'] = list()

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

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

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

        if 'deactivation_coil' in self.config:
            self.config['deactivation_coil'] = (
                self.machine.coils[self.config['deactivation_coil']])
        else:
            self.config['deactivation_coil'] = None

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

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

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

        # Create a list of ball device objects when active and inactive. We need
        # this because ball eject attempts pass the target device as an object
        # rather than by name.

        self.config['active_objects'] = list()
        self.config['inactive_objects'] = list()

        for target_device in self.config['targets_when_active']:
            if target_device == 'playfield':
                self.config['active_objects'].append('playfield')
            else:
                self.config['active_objects'].append(
                    self.machine.balldevices[target_device])

        for target_device in self.config['targets_when_inactive']:
            if target_device == 'playfield':
                self.config['inactive_objects'].append('playfield')
            else:
                self.config['inactive_objects'].append(
                    self.machine.balldevices[target_device])

        # convert the activation_time to ms
        self.config['activation_time'] = Timing.string_to_ms(
            self.config['activation_time'])

        # register for events
        for event in self.config['enable_events']:
            self.machine.events.add_handler(event, self.enable)

        for event in self.config['disable_events']:
            self.machine.events.add_handler(event, self.disable)

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

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

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

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

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

    def enable(self, auto=False, activations=-1, **kwargs):
        """Enables this diverter.

        Args:
            auto: Boolean value which is used to indicate whether this
                diverter enabled itself automatically. This is passed to the
                event which is posted.
            activations: Integer of how many times you'd like this diverter to
                activate before it will automatically disable itself. Default is
                -1 which is unlimited.

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

        If no `activation_switches` is specified, then the diverter is activated
        immediately.
        """

        self.enabled = True

        self.machine.events.post('diverter_' + self.name + '_enabling',
                                 auto=auto)

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

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

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

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

        self.enabled = False

        self.machine.events.post('diverter_' + self.name + '_disabling',
                                 auto=auto)

        self.log.debug("Disabling Diverter")
        if self.config['activation_switches']:
            self.disable_hw_switch()
        else:
            self.deactivate()

    def activate(self):
        """Physically activates this diverter's coil."""
        self.log.debug("Activating Diverter")

        self.active = True

        #if self.remaining_activations > 0:
        #    self.remaining_activations -= 1

        self.machine.events.post('diverter_' + self.name + '_activating')
        if self.config['type'] == 'pulse':
            self.config['activation_coil'].pulse()
        elif self.config['type'] == 'hold':
            self.config['activation_coil'].enable()
            self.schedule_deactivation()

    def deactivate(self):
        """Deactivates this diverter.

        This method will disable the activation_coil, and (optionally) if it's
        configured with a deactivation coil, it will pulse it.
        """
        self.log.debug("Deactivating Diverter")

        self.active = False

        self.machine.events.post('diverter_' + self.name + '_deactivating')
        self.config['activation_coil'].disable()

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

        #if self.remaining_activations != 0:
        #    self.enable()
        # todo this will be weird if the diverter is enabled without a hw
        # switch.. wonder if we should check for that here?

    def schedule_deactivation(self, time=None):
        """Schedules a delay to deactivate this diverter.

        Args:
            time: The MPF string time of how long you'd like the delay before
                deactivating the diverter. Default is None which means it uses
                the 'activation_time' setting configured for this diverter. If
                there is no 'activation_time' setting and no delay is passed,
                it will disable the diverter immediately.
        """

        if time is not None:
            delay = Timing.string_to_ms(time)

        elif self.config['activation_time']:
            delay = self.config['activation_time']

        if delay:
            self.delay.add('disable_held_coil', delay, self.disable_held_coil)
        else:
            self.disable_held_coil()

    def enable_hw_switches(self):
        """Enables the hardware switch rule which causes this diverter to
        activate when the switch is hit.

        This is typically used for diverters on loops and ramps where you don't
        want the diverter to phsyically activate until the ramp entry switch is
        activated.

        If this diverter is configured with a activation_time, this method will
        also set switch handlers which will set a delay to deactivate the
        diverter once the activation activation_time expires.

        If this diverter is configured with a deactivation switch, this method
        will set up the switch handlers to deactivate the diverter when the
        deactivation switch is activated.
        """
        self.log.debug("Enabling Diverter for hw switch: %s",
                       self.config['activation_switches'])

        if self.config['type'] == 'hold':

            for switch in self.config['activation_switches']:

                self.platform.set_hw_rule(
                    sw_name=switch,
                    sw_activity='active',
                    coil_name=self.config['activation_coil'].name,
                    coil_action_ms=-1,
                    pulse_ms=self.config['activation_coil'].config['pulse_ms'],
                    pwm_on=self.config['activation_coil'].config['pwm_on'],
                    pwm_off=self.config['activation_coil'].config['pwm_off'],
                    debounced=False)

                # If there's a activation_time then we need to watch for the hw
                # switch to be activated so we can disable the diverter

                if self.config['activation_time']:
                    self.machine.switch_controller.add_switch_handler(
                        switch, self.schedule_deactivation)

        elif self.config['type'] == 'pulse':

            for switch in self.config['activation_switches']:

                self.platform.set_hw_rule(
                    sw_name=switch,
                    sw_activity='active',
                    coil_name=self.config['activation_coil'].name,
                    coil_action_ms=self.config['activation_coil'].
                    config['pulse_ms'],
                    pulse_ms=self.config['activation_coil'].config['pulse_ms'],
                    debounced=False)

    def disable_hw_switch(self):
        """Removes the hardware rule to disable the hardware activation switch
        for this diverter.
        """

        for switch in self.config['activation_switches']:
            self.platform.clear_hw_rule(switch)

        # todo this should not clear all the rules for this switch

    def disable_held_coil(self):
        """Physically disables the coil holding this diverter open."""
        self.config['activation_coil'].disable()

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

        # Since the 'target' kwarg is going to be an object, not a name, we need
        # to figure out if this object is one of the targets of this diverter.

        self.log.debug("Feeder device eject attempt for target: %s", target)

        if target in self.config['active_objects']:
            self.log.debug("Enabling diverter since eject target is on the "
                           "active target list")
            self.enable()

        elif target in self.config['inactive_objects']:
            self.log.debug("Enabling diverter since eject target is on the "
                           "inactive target list")
            self.disable()
Exemplo n.º 12
0
class HitCounter(LogicBlock):
    """A type of LogicBlock that tracks multiple hits of a single event.

    Supports counting closely-spaced hits as one hit. This type of LogicBlock is
    used for things like counting the tilt hits.
    """

    # todo settle time

    def __init__(self, machine, name, config):
        self.log = logging.getLogger('HitCounter.' + name)
        self.log.debug("Creating HitCounter LogicBlock")

        super(HitCounter, self).__init__(machine, name, config)

        self.delay = DelayManager()

        self.num_hits = 0
        self.ignore_hits = False

        if 'trigger_events' not in self.config:
            return  # Not much point to continue here
        else:
            self.config['trigger_events'] = self.machine.string_to_list(
                self.config['trigger_events'])

        if 'event_when_hit' not in self.config:
            self.config['event_when_hit'] = ('eventtrigger_' + self.name +
                                             '_hit')

        if 'hits_to_complete' not in self.config:
            self.config['hits_to_complete'] = 1

        if 'multiple_hit_window' not in self.config:
            self.config['multiple_hit_window'] = None
        else:
            self.config['multiple_hit_window'] = Timing.string_to_ms(
                self.config['multiple_hit_window'])
        if 'settle_time' not in self.config:
            self.config['settle_time'] = None
        else:
            self.config['settle_time'] = Timing.string_to_ms(
                self.config['settle_time'])

    def enable(self, **kwargs):
        """Enables this trigger. Automatically called when one of the
        'enable_event's is posted. Can also manually be called.
        """
        super(HitCounter, self).enable(**kwargs)
        self.machine.events.remove_handler(self.hit)  # prevents multiples

        self.enabled = True

        for event in self.config['trigger_events']:
            self.machine.events.add_handler(event, self.hit)

    def reset(self, **kwargs):
        """Resets the hit progress towards completion"""
        super(HitCounter, self).reset(**kwargs)
        self.num_hits = 0

    def hit(self, **kwargs):
        """Increases the hit progress towards completion. Automatically called
        when one of the `trigger_events`s is posted. Can also manually be
        called.
        """
        if not self.ignore_hits:
            self.num_hits += 1
            self.log.debug("Processing Hit. Total: %s", self.num_hits)

            if self.num_hits >= self.config['hits_to_complete']:
                self.complete()

            if self.config['event_when_hit']:
                self.machine.events.post(self.config['event_when_hit'],
                                         hits=self.num_hits)

            if self.config['multiple_hit_window']:
                self.log.debug("Beginning Ignore Hits")
                self.ignore_hits = True
                self.delay.add('ignore_hits_within_window',
                               self.config['multiple_hit_window'],
                               self.stop_ignoring_hits)

    def stop_ignoring_hits(self, **kwargs):
        """Causes the EventTrigger to stop ignoring subsequent hits that occur
        within the 'multiple_hit_window'. Automatically called when the window
        time expires. Can safely be manually called.
        """
        self.log.debug("Ending Ignore hits")
        self.ignore_hits = False
Exemplo n.º 13
0
Arquivo: diverter.py Projeto: xsdk/mpf
class Diverter(Device):
    """Represents a diverter in a pinball machine.

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

    config_section = 'Diverters'
    collection = 'diverter'

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

        self.delay = DelayManager()

        # configure defaults:
        if 'type' not in self.config:
            self.config['type'] = 'pulse'  # default to pulse to not fry coils
        if 'timeout' not in self.config:
            self.config['timeout'] = 0
        if 'activation_switch' not in self.config:
            self.config['activation_switch'] = None
        if 'disable_switch' not in self.config:
            self.config['disable_switch'] = None
        if 'target_when_enabled' not in self.config:
            self.config['target_when_enabled'] = None  # todo
        if 'target_when_disabled' not in self.config:
            self.config['target_when_disabled'] = None  # todo

        # convert the timeout to ms
        self.config['timeout'] = Timing.string_to_ms(self.config['timeout'])

        # register for events
        for event in self.config['enable_events']:
            self.machine.events.add_handler(event, self.enable)

        for event in self.config['disable_events']:
            self.machine.events.add_handler(event, self.disable)

    def enable(self):
        """Enables this diverter.

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

        If no `activation_switch` is specified, then the diverter is activated
        immediately.
        """

        if self.config['activation_switch']:
            self.enable_hw_switch()
        else:
            self.activate()

        if self.config['disable_switch']:
            self.machine.switch_controller.add_switch_handler(
                self.config['disable_switch'], self.disable)

    def disable(self, deactivate=True, **kwargs):
        """Disables this diverter.

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

        Args:
            deactivate: A boolean value which specifies whether this diverter
                should be immediately deactived.
            **kwargs: This is here because this disable method is called by
                whatever event the game programmer specifies in their machine
                configuration file, so we don't know what event that might be
                or whether it has random kwargs attached to it.
        """
        self.log.debug("Disabling Diverter")
        if self.config['activation_switch']:
            self.disable_hw_switch()
        if deactivate:
            self.deactivate()

    def activate(self):
        """Physically activates this diverter."""

        self.log.debug("Activating Diverter")
        self.machine.events.post('diverter_' + self.name + '_activating')
        if self.config['type'] == 'pulse':
            self.machine.coils[self.config['coil']].pulse()
        elif self.config['type'] == 'hold':
            self.machine.coils[self.config['coil']].enable()
            self.schedule_disable()

    def deactivate(self):
        """Physically deactivates this diverter."""
        self.log.debug("Deactivating Diverter")
        self.machine.coils[self.config['coil']].disable()

    def schedule_disable(self):
        """Schedules a delay to deactivate this diverter based on the configured
        timeout.
        """
        if self.config['timeout']:
            self.delay.add('disable_held_coil', self.config['timeout'],
                           self.disable_held_coil)

    def enable_hw_switch(self):
        """Enables the hardware switch rule which causes this diverter to
        activate when the switch is hit.

        This is typically used for diverters on loops and ramps where you don't
        want the diverter to phsyically activate until the ramp entry switch is
        activated.

        If this diverter is configured with a timeout, this method will also
        set switch handlers which will set a delay to deactivate the diverter
        once the activation timeout expires.

        If this diverter is configured with a deactivation switch, this method
        will set up the switch handlers to deactivate the diverter when the
        deactivation switch is activated.
        """
        self.log.debug("Enabling Diverter for hw switch: %s",
                       self.config['activation_switch'])

        if self.config['type'] == 'hold':

            self.machine.platform.set_hw_rule(
                sw_name=self.config['activation_switch'],
                sw_activity='active',
                coil_name=self.config['coil'],
                coil_action_ms=-1,
                pulse_ms=self.machine.coils[
                    self.config['coil']].config['pulse_ms'],
                pwm_on=self.machine.coils[
                    self.config['coil']].config['pwm_on'],
                pwm_off=self.machine.coils[
                    self.config['coil']].config['pwm_off'],
                debounced=False)

            # If there's a timeout then we need to watch for the hw switch to
            # be activated so we can disable the diverter

            if self.config['timeout']:
                self.machine.switch_controller.add_switch_handler(
                    self.config['activation_switch'], self.schedule_disable)

        elif self.config['type'] == 'pulse':

            self.machine.platform.set_hw_rule(
                sw_name=self.config['activation_switch'],
                sw_activity='active',
                coil_name=self.config['coil'],
                coil_action_ms=1,
                pulse_ms=self.machine.coils[
                    self.config['main_coil']].config['pulse_ms'],
                debounced=False)

    def disable_hw_switch(self):
        """Removes the hardware rule to disable the hardware activation switch
        for this diverter.
        """
        self.machine.platform.clear_hw_rule(self.config['activation_switch'])

    def disable_held_coil(self):
        """Physically disables the coil holding this diverter open."""
        self.machine.coils[self.config['coil']].disable()
Exemplo n.º 14
0
class Multiball(Device):

    config_section = 'multiballs'
    collection = 'multiballs'
    class_label = 'multiball'

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

        self.delay = DelayManager()

        # let ball devices initialise first
        self.machine.events.add_handler('init_phase_3',
                                        self._initialize)

    def _initialize(self):
        self.ball_locks = self.config['ball_locks']
        self.shoot_again = False
        self.enabled = False
        self.source_playfield = self.config['source_playfield']

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

        if self.balls_ejected > 0:
            self.log.debug("Cannot start MB because %s are still in play",
                           self.balls_ejected)
            return

        self.shoot_again = True
        self.log.debug("Starting multiball with %s balls",
                       self.config['ball_count'])

        self.balls_ejected = self.config['ball_count'] - 1

        self.machine.game.add_balls_in_play(balls=self.balls_ejected)

        balls_added = 0

        # use lock_devices first
        for device in self.ball_locks:
            balls_added += device.release_balls(self.balls_ejected - balls_added)

            if self.balls_ejected - balls_added <= 0:
                break

        # request remaining balls
        if self.balls_ejected - balls_added > 0:
            self.source_playfield.add_ball(balls=self.balls_ejected - balls_added)

        if not self.config['shoot_again']:
            # No shoot again. Just stop multiball right away
            self.stop()
        else:
            # Enable shoot again
            self.machine.events.add_handler('ball_drain',
                                            self._ball_drain_shoot_again,
                                            priority=1000)
            # Register stop handler
            if not isinstance(self.config['shoot_again'], bool):
                self.delay.add('disable_shoot_again',
                               self.config['shoot_again'], self.stop)

        self.machine.events.post("multiball_" + self.name + "_started",
                         balls=self.config['ball_count'])

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

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

        self.log.debug("Ball drained during MB. Requesting a new one")
        self.source_playfield.add_ball(balls=balls)
        return {'balls': 0}


    def _ball_drain_count_balls(self, balls, **kwargs):
        self.balls_ejected -= balls
        if self.balls_ejected <= 0:
            self.balls_ejected = 0
            self.machine.events.remove_handler(self._ball_drain_count_balls)
            self.machine.events.post("multiball_" + self.name + "_ended")
            self.log.debug("Ball drained. MB ended.")
        else:
            self.log.debug("Ball drained. %s balls remain until MB ends",
                           self.balls_ejected)

        # TODO: we are _not_ claiming the balls because we want it to drain.
        # However this may result in wrong results with multiple MBs at the
        # same time. May be we should claim and remove balls manually?

        return {'balls': balls}

    def stop(self, **kwargs):
        self.log.debug("Stopping shoot again of multiball")
        self.shoot_again = False

        # disable shoot again
        self.machine.events.remove_handler(self._ball_drain_shoot_again)

        # add handler for ball_drain until self.balls_ejected are drained
        self.machine.events.add_handler('ball_drain',
                                        self._ball_drain_count_balls)

    def enable(self, **kwargs):
        """ Enables the multiball. If the multiball is not enabled, it cannot
        start.
        """
        self.log.debug("Enabling...")
        self.enabled = True

    def disable(self, **kwargs):
        """ Disabless the multiball. If the multiball is not enabled, it cannot
        start.
        """
        self.log.debug("Disabling...")
        self.enabled = False

    def reset(self, **kwargs):
        """Resets the multiball and disables it.
        """
        self.enabled = False
        self.shoot_again = False
        self.balls_ejected = 0
Exemplo n.º 15
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

        self._num_balls_known = -999

        self.num_balls_missing = 0
        # Balls lost and/or not installed.

        # register for events
        self.machine.events.add_handler('request_to_start_game',
                                        self.request_to_start_game)
        self.machine.events.add_handler('machine_reset_phase_2',
                                        self._initialize)
        # self.machine.events.add_handler('init_phase_2',
        #                                 self.create_playfield_device, 2)

    def _count_balls(self):
        self.log.debug("Counting Balls")
        balls = 0
        for device in self.machine.ball_devices:
            if not device._count_consistent:
                self.log.debug("Device %s is inconsistent", device.name)
                return -999
            self.log.debug("Found %s ball(s) in %s", device.balls, device.name)
            balls += device.balls

        if balls > self._num_balls_known:
            self.log.debug("Setting known balls to %s", balls)
            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_known(self):
        self._update_num_balls_known()

        return self._num_balls_known

    def _update_num_balls_known(self):
        balls = self._count_balls()

        if balls < 0:
            self.delay.add(callback=self._update_num_balls_known, ms=10)

        if balls > self._num_balls_known:
            self._num_balls_known = balls

    @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 _initialize(self):

        # 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, 'ball_devices'):
            return

        self._update_num_balls_known()

        for device in self.machine.ball_devices:
            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)

        # todo
        if 'Allow start with loose balls' not in self.machine.config['game']:
            self.machine.config['game']['Allow start with loose balls'] = False

    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.
        """
        balls = self._count_balls()
        self.log.debug("Received request to start game.")
        self.log.debug("Balls contained: %s, Min balls needed: %s", balls,
                       self.machine.config['machine']['min_balls'])
        if balls < self.machine.config['machine']['min_balls']:
            self.log.warning("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_collected(['home', 'trough']):
            self.collect_balls('home')
            self.log.warning("BallController denies game start. Balls are not "
                             "in their home positions.")
            return False

    def are_balls_collected(self, target=None, antitarget=None):
        """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 or list of strings of the tags you'd like to
                collect the balls to. Default of None will be replaced with
                'home' and 'trough'.

        """
        if not target:
            target = ['home', 'trough']

        self.log.debug(
            "Checking to see if all the balls are in devices tagged"
            " with '%s'", target)

        if type(target) is str:
            target = Util.string_to_list(target)

        count = 0
        devices = set()

        for tag in target:
            for device in self.machine.ball_devices.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('balls')
            self.log.debug('Found %s ball(s) in %s. Found %s total',
                           device.get_status('balls'), device.name, count)

        if count == self.machine.ball_controller.num_balls_known:
            self.log.debug("Yes, all balls are collected")
            return True
        else:
            self.log.debug(
                "No, all balls are not collected. Balls Counted: %s. "
                "Total balls known: %s", count,
                self.machine.ball_controller.num_balls_known)
            return False

    def collect_balls(self, target='home, trough'):
        """Used to ensure that all balls are in contained in ball devices with
        the tag or list of tags 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' and/or 'trough'.

        Args:
            target: A string of the tag name or a list of tags names of the
                ball devices you want all the balls to end up in. Default is
                ['home', 'trough'].

        """
        # I'm embarrassed at how ugly this code is. But meh, it works...

        tag_list = Util.string_to_list(target)

        self.log.debug("Collecting all balls to devices with tags '%s'",
                       tag_list)

        target_devices = set()
        source_devices = set()
        balls_to_collect = False

        for tag in tag_list:
            for device in self.machine.ball_devices.items_tagged(tag):
                target_devices.add(device)

        for device in self.machine.ball_devices:
            if device not in target_devices:
                if device.balls:
                    source_devices.add(device)
                    balls_to_collect = True

        self.log.debug("Ejecting all balls from: %s", source_devices)

        if balls_to_collect:
            self.machine.events.post('collecting_balls')

            for device in target_devices:
                self.machine.events.replace_handler(
                    'balldevice_{}_ball_enter'.format(device.name),
                    self._collecting_balls_entered_callback,
                    target=target)

            for device in source_devices:
                device.eject_all()
        else:
            self.log.debug("All balls are collected")

    def _collecting_balls_entered_callback(self, target, new_balls,
                                           unclaimed_balls, **kwargs):
        if self.are_balls_collected(target=target):
            self._collecting_balls_complete()

        return {'unclaimed_balls': unclaimed_balls}

    def _collecting_balls_complete(self):
        self.machine.events.remove_handler(self._collecting_balls_complete)
        self.machine.events.post('collecting_balls_complete')

    def _ball_drained_handler(self, new_balls, unclaimed_balls, device,
                              **kwargs):
        self.machine.events.post_relay('ball_drain',
                                       callback=self._process_ball_drained,
                                       device=device,
                                       balls=unclaimed_balls)

        # What happens if the ball enters the trough but the ball_add_live
        # event hasn't confirmed its eject? todo

    def _process_ball_drained(self, balls=None, ev_result=None, **kwargs):
        # 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
Exemplo n.º 16
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

        self._num_balls_known = -999

        self.num_balls_missing = 0
        # Balls lost and/or not installed.

        # register for events
        self.machine.events.add_handler('request_to_start_game',
                                        self.request_to_start_game)
        self.machine.events.add_handler('machine_reset_phase_2',
                                        self._initialize)
        # self.machine.events.add_handler('init_phase_2',
        #                                 self.create_playfield_device, 2)

    def _count_balls(self):
        self.log.debug("Counting Balls")
        balls = 0
        for device in self.machine.ball_devices:
            if not device._count_consistent:
                self.log.debug("Device %s is inconsistent", device.name)
                return -999
            self.log.debug("Found %s ball(s) in %s", device.balls, device.name)
            balls += device.balls

        if balls > self._num_balls_known:
            self.log.debug("Setting known balls to %s", balls)
            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_known(self):
        self._update_num_balls_known()

        return self._num_balls_known

    def _update_num_balls_known(self):
        balls = self._count_balls() 

        if balls < 0:
            self.delay.add(callback=self._update_num_balls_known, ms=10)

        if balls > self._num_balls_known:
            self._num_balls_known = balls


    @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 _initialize(self):

        # 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, 'ball_devices'):
            return

        self._update_num_balls_known()

        for device in self.machine.ball_devices:
            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)

        # todo
        if 'Allow start with loose balls' not in self.machine.config['game']:
            self.machine.config['game']['Allow start with loose balls'] = False

    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.
        """
        balls = self._count_balls()
        self.log.debug("Received request to start game.")
        self.log.debug("Balls contained: %s, Min balls needed: %s",
                       balls,
                       self.machine.config['machine']['min_balls'])
        if balls < self.machine.config['machine']['min_balls']:
            self.log.warning("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_collected(['home', 'trough']):
            self.collect_balls('home')
            self.log.warning("BallController denies game start. Balls are not "
                             "in their home positions.")
            return False

    def are_balls_collected(self, target=None, antitarget=None):
        """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 or list of strings of the tags you'd like to
                collect the balls to. Default of None will be replaced with
                'home' and 'trough'.

        """
        if not target:
            target = ['home', 'trough']

        self.log.debug("Checking to see if all the balls are in devices tagged"
                       " with '%s'", target)

        if type(target) is str:
            target = Util.string_to_list(target)

        count = 0
        devices = set()

        for tag in target:
            for device in self.machine.ball_devices.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('balls')
            self.log.debug('Found %s ball(s) in %s. Found %s total',
                           device.get_status('balls'), device.name, count)

        if count == self.machine.ball_controller.num_balls_known:
            self.log.debug("Yes, all balls are collected")
            return True
        else:
            self.log.debug("No, all balls are not collected. Balls Counted: %s. "
                           "Total balls known: %s", count,
                           self.machine.ball_controller.num_balls_known)
            return False

    def collect_balls(self, target='home, trough'):
        """Used to ensure that all balls are in contained in ball devices with
        the tag or list of tags 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' and/or 'trough'.

        Args:
            target: A string of the tag name or a list of tags names of the
                ball devices you want all the balls to end up in. Default is
                ['home', 'trough'].

        """
        # I'm embarrassed at how ugly this code is. But meh, it works...

        tag_list = Util.string_to_list(target)

        self.log.debug("Collecting all balls to devices with tags '%s'",
                       tag_list)

        target_devices = set()
        source_devices = set()
        balls_to_collect = False

        for tag in tag_list:
            for device in self.machine.ball_devices.items_tagged(tag):
                target_devices.add(device)

        for device in self.machine.ball_devices:
            if device not in target_devices:
                if device.balls:
                    source_devices.add(device)
                    balls_to_collect = True

        self.log.debug("Ejecting all balls from: %s", source_devices)

        if balls_to_collect:
            self.machine.events.post('collecting_balls')

            for device in target_devices:
                self.machine.events.replace_handler(
                    'balldevice_{}_ball_enter'.format(device.name),
                    self._collecting_balls_entered_callback,
                    target=target)

            for device in source_devices:
                device.eject_all()
        else:
            self.log.debug("All balls are collected")

    def _collecting_balls_entered_callback(self, target, new_balls,
                                           unclaimed_balls, **kwargs):
        if self.are_balls_collected(target=target):
            self._collecting_balls_complete()

        return {'unclaimed_balls': unclaimed_balls}

    def _collecting_balls_complete(self):
        self.machine.events.remove_handler(self._collecting_balls_complete)
        self.machine.events.post('collecting_balls_complete')

    def _ball_drained_handler(self, new_balls, unclaimed_balls, device,
                              **kwargs):
        self.machine.events.post_relay('ball_drain',
                                       callback=self._process_ball_drained,
                                       device=device,
                                       balls=unclaimed_balls)

        # What happens if the ball enters the trough but the ball_add_live
        # event hasn't confirmed its eject? todo

    def _process_ball_drained(self, balls=None, ev_result=None, **kwargs):
        # 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
Exemplo n.º 17
0
class SwitchPlayer(object):

    def __init__(self, machine):
        self.log = logging.getLogger('switchplayer')

        if 'switchplayer' not in machine.config:
            machine.log.debug('"switchplayer:" section not found in '
                                   'machine configuration, so the Switch Player'
                                   'plugin will not be used.')
            return

        self.machine = machine
        self.delay = DelayManager()
        self.current_step = 0

        config_spec = '''
                        start_event: string|machine_reset_phase_3
                        start_delay: secs|0
                        '''

        self.config = Config.process_config(config_spec,
                                            self.machine.config['switchplayer'])

        self.machine.events.add_handler(self.config['start_event'],
                                        self._start_event_callback)

        self.step_list = self.config['steps']
        self.start_delay = self.config['start_delay']

    def _start_event_callback(self):

        if ('time' in self.step_list[self.current_step] and
                self.step_list[self.current_step]['time'] > 0):

            self.delay.add(name='switch_player_next_step',
                           ms=Timing.string_to_ms(self.step_list[self.current_step]['time']),
                           callback=self._do_step)

    def _do_step(self):

            this_step = self.step_list[self.current_step]

            self.log.info("Switch: %s, Action: %s", this_step['switch'],
                          this_step['action'])

            # send this step's switches
            if this_step['action'] == 'activate':
                self.machine.switch_controller.process_switch(
                    this_step['switch'],
                    state=1,
                    logical=True)
            elif this_step['action'] == 'deactivate':
                self.machine.switch_controller.process_switch(
                    this_step['switch'],
                    state=0,
                    logical=True)
            elif this_step['action'] == 'hit':
                self._hit(this_step['switch'])

            # inc counter
            if self.current_step < len(self.step_list)-1:
                self.current_step += 1

                # schedule next step
                self.delay.add(name='switch_player_next_step',
                               ms=Timing.string_to_ms(self.step_list[self.current_step]['time']),
                               callback=self._do_step)

    def _hit(self, switch):
        self.machine.switch_controller.process_switch(
                    switch,
                    state=1,
                    logical=True)
        self.machine.switch_controller.process_switch(
                    switch,
                    state=0,
                    logical=True)
Exemplo n.º 18
0
Arquivo: mode.py Projeto: qcapen/mpf
class Mode(object):
    """Parent class for in-game mode code."""

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

        self.log = logging.getLogger("Mode." + name)

        self.delay = DelayManager()

        self.priority = 0
        self._active = False
        self._mode_start_wait_queue = None
        self.stop_methods = list()
        self.timers = dict()
        self.start_callback = None
        self.stop_callback = None
        self.event_handlers = set()
        self.switch_handlers = list()
        self.mode_start_kwargs = dict()
        self.mode_stop_kwargs = dict()
        self.mode_devices = set()

        self.player = None
        """Reference to the current player object."""

        self._validate_mode_config()

        self.configure_mode_settings(config.get("mode", dict()))

        self.auto_stop_on_ball_end = self.config["mode"]["stop_on_ball_end"]
        """Controls whether this mode is stopped when the ball ends,
        regardless of its stop_events settings.
        """

        self.restart_on_next_ball = self.config["mode"]["restart_on_next_ball"]
        """Controls whether this mode will restart on the next ball. This only
        works if the mode was running when the ball ended. It's tracked per-
        player in the '_restart_modes_on_next_ball' untracked player variable.
        """

        for asset_manager in self.machine.asset_managers.values():

            config_data = self.config.get(asset_manager.config_section, dict())

            self.config[asset_manager.config_section] = asset_manager.register_assets(
                config=config_data, mode_path=self.path
            )

        # Call registered remote loader methods
        for item in self.machine.mode_controller.loader_methods:
            if item.config_section and item.config_section in self.config and self.config[item.config_section]:
                item.method(config=self.config[item.config_section], mode_path=self.path, **item.kwargs)
            elif not item.config_section:
                item.method(config=self.config, mode_path=self.path, **item.kwargs)

        self.mode_init()

    def __repr__(self):
        return "<Mode.{}>".format(self.name)

    @property
    def active(self):
        return self._active

    @active.setter
    def active(self, active):
        if self._active != active:
            self._active = active
            self.machine.mode_controller._active_change(self, self._active)

    def configure_mode_settings(self, config):
        """Processes this mode's configuration settings from a config
        dictionary.
        """

        self.config["mode"] = self.machine.config_processor.process_config2(
            config_spec="mode", source=config, section_name="mode"
        )

        for event in self.config["mode"]["start_events"]:
            self.machine.events.add_handler(
                event=event,
                handler=self.start,
                priority=self.config["mode"]["priority"] + self.config["mode"]["start_priority"],
            )

    def _validate_mode_config(self):
        for section in self.machine.config["mpf"]["mode_config_sections"]:
            this_section = self.config.get(section, None)

            if this_section:
                if type(this_section) is dict:
                    for device, settings in this_section.iteritems():
                        self.config[section][device] = self.machine.config_processor.process_config2(section, settings)

                else:
                    self.config[section] = self.machine.config_processor.process_config2(section, this_section)

    def start(self, priority=None, callback=None, **kwargs):
        """Starts this mode.

        Args:
            priority: Integer value of what you want this mode to run at. If you
                don't specify one, it will use the "Mode: priority" setting from
                this mode's configuration file.
            **kwargs: Catch-all since this mode might start from events with
                who-knows-what keyword arguments.

        Warning: You can safely call this method, but do not override it in your
        mode code. If you want to write your own mode code by subclassing Mode,
        put whatever code you want to run when this mode starts in the
        mode_start method which will be called automatically.
        """

        self.log.debug("Received request to start")

        if self._active:
            self.log.debug("Mode is already active. Aborting start")
            return

        if self.config["mode"]["use_wait_queue"] and "queue" in kwargs:

            self.log.debug("Registering a mode start wait queue")

            self._mode_start_wait_queue = kwargs["queue"]
            self._mode_start_wait_queue.wait()

        if type(priority) is int:
            self.priority = priority
        else:
            self.priority = self.config["mode"]["priority"]

        self.start_event_kwargs = kwargs

        self.log.info("Mode Starting. Priority: %s", self.priority)

        self._create_mode_devices()

        self.log.debug("Registering mode_stop handlers")

        # register mode stop events
        if "stop_events" in self.config["mode"]:

            for event in self.config["mode"]["stop_events"]:
                # stop priority is +1 so if two modes of the same priority
                # start and stop on the same event, the one will stop before the
                # other starts
                self.add_mode_event_handler(
                    event=event, handler=self.stop, priority=self.priority + 1 + self.config["mode"]["stop_priority"]
                )

        self.start_callback = callback

        if "timers" in self.config:
            self._setup_timers()

        self.log.debug("Calling mode_start handlers")

        for item in self.machine.mode_controller.start_methods:
            if item.config_section in self.config or not item.config_section:
                self.stop_methods.append(
                    item.method(
                        config=self.config.get(item.config_section, self.config),
                        priority=self.priority,
                        mode=self,
                        **item.kwargs
                    )
                )

        self._setup_device_control_events()

        self.machine.events.post_queue(event="mode_" + self.name + "_starting", callback=self._started)

    def _started(self):
        # Called after the mode_<name>_starting queue event has finished.

        self.log.debug("Mode Started. Priority: %s", self.priority)

        self.active = True

        self._start_timers()

        self.machine.events.post("mode_" + self.name + "_started", callback=self._mode_started_callback)

    def _mode_started_callback(self, **kwargs):
        # Called after the mode_<name>_started queue event has finished.
        self.mode_start(**self.start_event_kwargs)

        self.start_event_kwargs = dict()

        if self.start_callback:
            self.start_callback()

        self.log.debug("Mode Start process complete.")

    def stop(self, callback=None, **kwargs):
        """Stops this mode.

        Args:
            **kwargs: Catch-all since this mode might start from events with
                who-knows-what keyword arguments.

        Warning: You can safely call this method, but do not override it in your
        mode code. If you want to write your own mode code by subclassing Mode,
        put whatever code you want to run when this mode stops in the
        mode_stop method which will be called automatically.
        """

        if not self._active:
            return

        self.mode_stop_kwargs = kwargs

        self.log.debug("Mode Stopping.")

        self._remove_mode_switch_handlers()

        self.stop_callback = callback

        self._kill_timers()
        self.delay.clear()

        # self.machine.events.remove_handler(self.stop)
        # todo is this ok here? Or should we only remove ones that we know this
        # mode added?

        self.machine.events.post_queue(event="mode_" + self.name + "_stopping", callback=self._stopped)

    def _stopped(self):
        self.log.debug("Mode Stopped.")

        self.priority = 0
        self.active = False

        for item in self.stop_methods:
            try:
                item[0](item[1])
            except TypeError:
                try:
                    item()
                except TypeError:
                    pass

        self.stop_methods = list()

        self.machine.events.post("mode_" + self.name + "_stopped", callback=self._mode_stopped_callback)

        if self._mode_start_wait_queue:

            self.log.debug("Clearing wait queue")

            self._mode_start_wait_queue.clear()
            self._mode_start_wait_queue = None

    def _mode_stopped_callback(self, **kwargs):
        self._remove_mode_event_handlers()
        self._remove_mode_devices()

        self.mode_stop(**self.mode_stop_kwargs)

        self.mode_stop_kwargs = dict()

        if self.stop_callback:
            self.stop_callback()

    def _create_mode_devices(self):
        # Creates new devices that are specified in a mode config that haven't
        # been created in the machine-wide config

        self.log.debug("Scanning config for mode-based devices")

        for collection_name, device_class in self.machine.device_manager.device_classes.iteritems():
            if device_class.config_section in self.config:
                for device, settings in self.config[device_class.config_section].iteritems():

                    collection = getattr(self.machine, collection_name)

                    if device not in collection:  # no existing device, create now

                        self.log.debug("Creating mode-based device: %s", device)

                        # TODO this config is already validated, so add something
                        # so it doesn't validate it again?

                        self.machine.device_manager.create_devices(collection.name, {device: settings}, validate=False)

                        # change device from str to object
                        device = collection[device]

                        # Track that this device was added via this mode so we
                        # can remove it when the mode ends.
                        self.mode_devices.add(device)

                        # This lets the device know it was created by a mode
                        # instead of machine-wide, as some devices want to do
                        # certain things here. We also pass the player object in
                        # case this device wants to do something with that too.
                        device.device_added_to_mode(mode=self, player=self.player)

    def _remove_mode_devices(self):

        for device in self.mode_devices:
            device.remove()

        self.mode_devices = set()

    def _setup_device_control_events(self):
        # registers mode handlers for control events for all devices specified
        # in this mode's config (not just newly-created devices)

        self.log.debug("Scanning mode-based config for device control_events")

        device_list = set()

        for event, method, delay, device in self.machine.device_manager.get_device_control_events(self.config):

            try:
                event, priority = event.split("|")
            except ValueError:
                priority = 0

            self.add_mode_event_handler(
                event=event,
                handler=self._control_event_handler,
                priority=self.priority + 2 + int(priority),
                callback=method,
                ms_delay=delay,
            )

            device_list.add(device)

        for device in device_list:
            device.control_events_in_mode(self)

    def _control_event_handler(self, callback, ms_delay=0, **kwargs):

        self.log.debug("_control_event_handler: callback: %s,", callback)

        if ms_delay:
            self.delay.add(name=callback, ms=ms_delay, callback=callback, mode=self)
        else:
            callback(mode=self)

    def add_mode_event_handler(self, event, handler, priority=1, **kwargs):
        """Registers an event handler which is automatically removed when this
        mode stops.

        This method is similar to the Event Manager's add_handler() method,
        except this method automatically unregisters the handlers when the mode
        ends.

        Args:
            event: String name of the event you're adding a handler for. Since
                events are text strings, they don't have to be pre-defined.
            handler: The method that will be called when the event is fired.
            priority: An arbitrary integer value that defines what order the
                handlers will be called in. The default is 1, so if you have a
                handler that you want to be called first, add it here with a
                priority of 2. (Or 3 or 10 or 100000.) The numbers don't matter.
                They're called from highest to lowest. (i.e. priority 100 is
                called before priority 1.)
            **kwargs: Any any additional keyword/argument pairs entered here
                will be attached to the handler and called whenever that handler
                is called. Note these are in addition to kwargs that could be
                passed as part of the event post. If there's a conflict, the
                event-level ones will win.

        Returns:
            A GUID reference to the handler which you can use to later remove
            the handler via ``remove_handler_by_key``. Though you don't need to
            remove the handler since the whole point of this method is they're
            automatically removed when the mode stops.

        Note that if you do add a handler via this method and then remove it
        manually, that's ok too.
        """

        key = self.machine.events.add_handler(event, handler, priority, mode=self, **kwargs)

        self.event_handlers.add(key)

        return key

    def _remove_mode_event_handlers(self):
        for key in self.event_handlers:
            self.machine.events.remove_handler_by_key(key)
        self.event_handlers = set()

    def _remove_mode_switch_handlers(self):
        for handler in self.switch_handlers:
            self.machine.switch_controller.remove_switch_handler(
                switch_name=handler["switch_name"],
                callback=handler["callback"],
                state=handler["state"],
                ms=handler["ms"],
            )
        self.switch_handlers = list()

    def _setup_timers(self):
        # config is localized

        for timer, settings in self.config["timers"].iteritems():

            self.timers[timer] = ModeTimer(machine=self.machine, mode=self, name=timer, config=settings)

        return self._kill_timers

    def _start_timers(self):
        for timer in self.timers.values():
            if timer.running:
                timer.start()

    def _kill_timers(self,):
        for timer in self.timers.values():
            timer.kill()

        self.timers = dict()

    def mode_init(self):
        """User-overrideable method which will be called when this mode
        initializes as part of the MPF boot process.
        """
        pass

    def mode_start(self, **kwargs):
        """User-overrideable method which will be called whenever this mode
        starts (i.e. whenever it becomes active).
        """
        pass

    def mode_stop(self, **kwargs):
        """User-overrideable method which will be called whenever this mode
        stops (i.e. whenever it becomes inactive).
        """
        pass
Exemplo n.º 19
0
class Diverter(Device):
    """Represents a diverter in a pinball machine.

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

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

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

        self.delay = DelayManager()

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

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

        self.trigger_type = 'software'  # 'software' or 'hardware'

        # Create a list of ball device objects when active and inactive. We need
        # this because ball eject attempts pass the target device as an object
        # rather than by name.

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

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

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

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

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

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

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

    def enable(self, auto=False, activations=-1, **kwargs):
        """Enables this diverter.

        Args:
            auto: Boolean value which is used to indicate whether this
                diverter enabled itself automatically. This is passed to the
                event which is posted.
            activations: Integer of how many times you'd like this diverter to
                activate before it will automatically disable itself. Default is
                -1 which is unlimited.

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

        If no `activation_switches` is specified, then the diverter is activated
        immediately.

        """
        self.enabled = True

        self.machine.events.post('diverter_' + self.name + '_enabling',
                                 auto=auto)

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

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

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

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

        self.machine.events.post('diverter_' + self.name + '_disabling',
                                 auto=auto)

        self.log.debug("Disabling Diverter")
        if self.config['activation_switches']:
            self.disable_switches()
        else:
            self.deactivate()

    def activate(self):
        """Physically activates this diverter's coil."""
        self.log.debug("Activating Diverter")
        self.active = True

        #if self.remaining_activations > 0:
        #    self.remaining_activations -= 1

        self.machine.events.post('diverter_' + self.name + '_activating')
        if self.config['type'] == 'pulse':
            self.config['activation_coil'].pulse()
        elif self.config['type'] == 'hold':
            self.config['activation_coil'].enable()
            self.schedule_deactivation()

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

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

        self.machine.events.post('diverter_' + self.name + '_deactivating')
        self.config['activation_coil'].disable()

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

        #if self.remaining_activations != 0:
        #    self.enable()
        # todo this will be weird if the diverter is enabled without a hw
        # switch.. wonder if we should check for that here?

    def schedule_deactivation(self, time=None):
        """Schedules a delay to deactivate this diverter.

        Args:
            time: The MPF string time of how long you'd like the delay before
                deactivating the diverter. Default is None which means it uses
                the 'activation_time' setting configured for this diverter. If
                there is no 'activation_time' setting and no delay is passed,
                it will disable the diverter immediately.
        """
        if time is not None:
            delay = Timing.string_to_ms(time)
        elif self.config['activation_time']:
            delay = self.config['activation_time']
        else:
            delay = False

        if delay:
            self.delay.add(name='disable_held_coil',
                           ms=delay,
                           callback=self.disable_held_coil)
        else:
            self.disable_held_coil()

    def enable_switches(self):
        if self.trigger_type == 'hardware':
            self.enable_hw_switches()
        else:
            self.enable_sw_switches()

    def disable_switches(self):
        if self.trigger_type == 'hardware':
            self.disable_hw_switches()
        else:
            self.disable_sw_switches()

    def enable_hw_switches(self):
        """Enables the hardware switch rule which causes this diverter to
        activate when the switch is hit.

        This is typically used for diverters on loops and ramps where you don't
        want the diverter to phsyically activate until the ramp entry switch is
        activated.

        If this diverter is configured with a activation_time, this method will
        also set switch handlers which will set a delay to deactivate the
        diverter once the activation activation_time expires.

        If this diverter is configured with a deactivation switch, this method
        will set up the switch handlers to deactivate the diverter when the
        deactivation switch is activated.
        """
        self.log.debug("Enabling Diverter for hw switch: %s",
                       self.config['activation_switches'])
        if self.config['type'] == 'hold':

            for switch in self.config['activation_switches']:

                self.platform.set_hw_rule(
                    sw_name=switch.name,
                    sw_activity=1,
                    driver_name=self.config['activation_coil'].name,
                    driver_action='hold',
                    disable_on_release=False,
                    **self.config)

                # If there's a activation_time then we need to watch for the hw
                # switch to be activated so we can disable the diverter

                if self.config['activation_time']:
                    self.machine.switch_controller.add_switch_handler(
                        switch.name, self.schedule_deactivation)

        elif self.config['type'] == 'pulse':

            for switch in self.config['activation_switches']:

                self.platform.set_hw_rule(
                    sw_name=switch.name,
                    sw_activity=1,
                    driver_name=self.config['activation_coil'].name,
                    driver_action='pulse',
                    disable_on_release=False,
                    **self.config)

    def enable_sw_switches(self):
        self.log.debug("Enabling Diverter sw switches: %s",
                       self.config['activation_switches'])

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

    def disable_sw_switches(self):
        self.log.debug("Disabling Diverter sw switches: %s",
                       self.config['activation_switches'])

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

    def disable_hw_switches(self):
        """Removes the hardware rule to disable the hardware activation switch
        for this diverter.
        """
        for switch in self.config['activation_switches']:
            self.platform.clear_hw_rule(switch.name)

        # todo this should not clear all the rules for this switch

    def disable_held_coil(self):
        """Physically disables the coil holding this diverter open."""
        self.log.debug("Disabling Activation Coil")
        self.config['activation_coil'].disable()

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

            # If there are ejects waiting for the other target switch diverter
            if len(self.eject_attempt_queue) > 0:
                if self.eject_state == False:
                    self.eject_state = True
                    self.log.debug(
                        "Enabling diverter since eject target is on the "
                        "active target list")
                    self.enable()
                elif self.eject_state == True:
                    self.eject_state = False
                    self.log.debug(
                        "Enabling diverter since eject target is on the "
                        "inactive target list")
                    self.disable()
            # And perform those ejects
            while len(self.eject_attempt_queue) > 0:
                self.diverting_ejects_count += 1
                queue = self.eject_attempt_queue.pop()
                queue.clear()

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

        # Since the 'target' kwarg is going to be an object, not a name, we need
        # to figure out if this object is one of the targets of this diverter.

        self.log.debug("Feeder device eject attempt for target: %s", target)

        desired_state = None
        if target in self.config['targets_when_active']:
            desired_state = True

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

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

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

        self.diverting_ejects_count += 1

        if desired_state == True:
            self.log.debug("Enabling diverter since eject target is on the "
                           "active target list")
            self.eject_state = desired_state
            self.enable()
        elif desired_state == False:
            self.log.debug("Enabling diverter since eject target is on the "
                           "inactive target list")
            self.eject_state = desired_state
            self.disable()
Exemplo n.º 20
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.º 21
0
class Multiball(Device):

    config_section = 'multiballs'
    collection = 'multiballs'
    class_label = 'multiball'

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

        self.delay = DelayManager()
        self.balls_ejected = 0

        # let ball devices initialise first
        self.machine.events.add_handler('init_phase_3', self._initialize)

    def _initialize(self):
        self.ball_locks = self.config['ball_locks']
        self.shoot_again = False
        self.enabled = False
        self.source_playfield = self.config['source_playfield']

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

        if self.balls_ejected > 0:
            self.log.debug("Cannot start MB because %s are still in play",
                           self.balls_ejected)
            return

        self.shoot_again = True
        self.log.debug("Starting multiball with %s balls",
                       self.config['ball_count'])

        self.balls_ejected = self.config['ball_count'] - 1

        self.machine.game.add_balls_in_play(balls=self.balls_ejected)

        balls_added = 0

        # use lock_devices first
        for device in self.ball_locks:
            balls_added += device.release_balls(self.balls_ejected -
                                                balls_added)

            if self.balls_ejected - balls_added <= 0:
                break

        # request remaining balls
        if self.balls_ejected - balls_added > 0:
            self.source_playfield.add_ball(balls=self.balls_ejected -
                                           balls_added)

        if not self.config['shoot_again']:
            # No shoot again. Just stop multiball right away
            self.stop()
        else:
            # Enable shoot again
            self.machine.events.add_handler('ball_drain',
                                            self._ball_drain_shoot_again,
                                            priority=1000)
            # Register stop handler
            if not isinstance(self.config['shoot_again'], bool):
                self.delay.add(name='disable_shoot_again',
                               ms=self.config['shoot_again'],
                               callback=self.stop)

        self.machine.events.post("multiball_" + self.name + "_started",
                                 balls=self.config['ball_count'])

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

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

        self.log.debug("Ball drained during MB. Requesting a new one")
        self.source_playfield.add_ball(balls=balls)
        return {'balls': 0}

    def _ball_drain_count_balls(self, balls, **kwargs):
        self.balls_ejected -= balls
        if self.balls_ejected <= 0:
            self.balls_ejected = 0
            self.machine.events.remove_handler(self._ball_drain_count_balls)
            self.machine.events.post("multiball_" + self.name + "_ended")
            self.log.debug("Ball drained. MB ended.")
        else:
            self.log.debug("Ball drained. %s balls remain until MB ends",
                           self.balls_ejected)

        # TODO: we are _not_ claiming the balls because we want it to drain.
        # However this may result in wrong results with multiple MBs at the
        # same time. May be we should claim and remove balls manually?

        return {'balls': balls}

    def stop(self, **kwargs):
        self.log.debug("Stopping shoot again of multiball")
        self.shoot_again = False

        # disable shoot again
        self.machine.events.remove_handler(self._ball_drain_shoot_again)

        # add handler for ball_drain until self.balls_ejected are drained
        self.machine.events.add_handler('ball_drain',
                                        self._ball_drain_count_balls)

    def enable(self, **kwargs):
        """ Enables the multiball. If the multiball is not enabled, it cannot
        start.
        """
        self.log.debug("Enabling...")
        self.enabled = True

    def disable(self, **kwargs):
        """ Disabless the multiball. If the multiball is not enabled, it cannot
        start.
        """
        self.log.debug("Disabling...")
        self.enabled = False

    def reset(self, **kwargs):
        """Resets the multiball and disables it.
        """
        self.enabled = False
        self.shoot_again = False
        self.balls_ejected = 0
Exemplo n.º 22
0
class Diverter(Device):
    """Represents a diverter in a pinball machine.

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

    config_section = 'diverters'
    collection = 'diverters'

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

        self.delay = DelayManager()

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

        # configure defaults:
        if 'type' not in self.config:
            self.config['type'] = 'pulse'  # default to pulse to not fry coils
        if 'activation_time' not in self.config:
            self.config['activation_time'] = 0
        if 'activation_switches' in self.config:
            self.config['activation_switches'] = Config.string_to_list(
                self.config['activation_switches'])
        else:
            self.config['activation_switches'] = list()

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

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

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

        if 'deactivation_coil' in self.config:
            self.config['deactivation_coil'] = (
                self.machine.coils[self.config['deactivation_coil']])
        else:
            self.config['deactivation_coil'] = None

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

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

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

        # Create a list of ball device objects when active and inactive. We need
        # this because ball eject attempts pass the target device as an object
        # rather than by name.

        self.config['active_objects'] = list()
        self.config['inactive_objects'] = list()

        for target_device in self.config['targets_when_active']:
            if target_device == 'playfield':
                self.config['active_objects'].append('playfield')
            else:
                self.config['active_objects'].append(
                    self.machine.balldevices[target_device])

        for target_device in self.config['targets_when_inactive']:
            if target_device == 'playfield':
                self.config['inactive_objects'].append('playfield')
            else:
                self.config['inactive_objects'].append(
                    self.machine.balldevices[target_device])

        # convert the activation_time to ms
        self.config['activation_time'] = Timing.string_to_ms(self.config['activation_time'])

        # register for events
        for event in self.config['enable_events']:
            self.machine.events.add_handler(event, self.enable)

        for event in self.config['disable_events']:
            self.machine.events.add_handler(event, self.disable)

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

        # register for deactivation switches
        for switch in self.config['deactivation_switches']:
            self.machine.switch_controller.add_switch_handler(
                switch, self.deactivate)

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

    def enable(self, auto=False, activations=-1, **kwargs):
        """Enables this diverter.

        Args:
            auto: Boolean value which is used to indicate whether this
                diverter enabled itself automatically. This is passed to the
                event which is posted.
            activations: Integer of how many times you'd like this diverter to
                activate before it will automatically disable itself. Default is
                -1 which is unlimited.

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

        If no `activation_switches` is specified, then the diverter is activated
        immediately.
        """

        self.enabled = True

        self.machine.events.post('diverter_' + self.name + '_enabling',
                                 auto=auto)

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

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

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

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

        self.enabled = False

        self.machine.events.post('diverter_' + self.name + '_disabling',
                                 auto=auto)

        self.log.debug("Disabling Diverter")
        if self.config['activation_switches']:
            self.disable_hw_switch()
        else:
            self.deactivate()

    def activate(self):
        """Physically activates this diverter's coil."""
        self.log.debug("Activating Diverter")

        self.active = True

        #if self.remaining_activations > 0:
        #    self.remaining_activations -= 1

        self.machine.events.post('diverter_' + self.name + '_activating')
        if self.config['type'] == 'pulse':
            self.config['activation_coil'].pulse()
        elif self.config['type'] == 'hold':
            self.config['activation_coil'].enable()
            self.schedule_deactivation()

    def deactivate(self):
        """Deactivates this diverter.

        This method will disable the activation_coil, and (optionally) if it's
        configured with a deactivation coil, it will pulse it.
        """
        self.log.debug("Deactivating Diverter")

        self.active = False

        self.machine.events.post('diverter_' + self.name + '_deactivating')
        self.config['activation_coil'].disable()

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

        #if self.remaining_activations != 0:
        #    self.enable()
            # todo this will be weird if the diverter is enabled without a hw
            # switch.. wonder if we should check for that here?

    def schedule_deactivation(self, time=None):
        """Schedules a delay to deactivate this diverter.

        Args:
            time: The MPF string time of how long you'd like the delay before
                deactivating the diverter. Default is None which means it uses
                the 'activation_time' setting configured for this diverter. If
                there is no 'activation_time' setting and no delay is passed,
                it will disable the diverter immediately.
        """

        if time is not None:
            delay = Timing.string_to_ms(time)

        elif self.config['activation_time']:
            delay = self.config['activation_time']

        if delay:
            self.delay.add('disable_held_coil', delay, self.disable_held_coil)
        else:
            self.disable_held_coil()

    def enable_hw_switches(self):
        """Enables the hardware switch rule which causes this diverter to
        activate when the switch is hit.

        This is typically used for diverters on loops and ramps where you don't
        want the diverter to phsyically activate until the ramp entry switch is
        activated.

        If this diverter is configured with a activation_time, this method will
        also set switch handlers which will set a delay to deactivate the
        diverter once the activation activation_time expires.

        If this diverter is configured with a deactivation switch, this method
        will set up the switch handlers to deactivate the diverter when the
        deactivation switch is activated.
        """
        self.log.debug("Enabling Diverter for hw switch: %s",
                       self.config['activation_switches'])

        if self.config['type'] == 'hold':

            for switch in self.config['activation_switches']:

                self.machine.platform.set_hw_rule(
                    sw_name=switch,
                    sw_activity='active',
                    coil_name=self.config['activation_coil'].name,
                    coil_action_ms=-1,
                    pulse_ms=self.config['activation_coil'].config['pulse_ms'],
                    pwm_on=self.config['activation_coil'].config['pwm_on'],
                    pwm_off=self.config['activation_coil'].config['pwm_off'],
                    debounced=False)

                # If there's a activation_time then we need to watch for the hw
                # switch to be activated so we can disable the diverter

                if self.config['activation_time']:
                    self.machine.switch_controller.add_switch_handler(
                        switch,
                        self.schedule_deactivation)

        elif self.config['type'] == 'pulse':

            for switch in self.config['activation_switches']:

                self.machine.platform.set_hw_rule(
                    sw_name=switch,
                    sw_activity='active',
                    coil_name=self.config['activation_coil'].name,
                    coil_action_ms=self.config['activation_coil'].config['pulse_ms'],
                    pulse_ms=self.config['activation_coil'].config['pulse_ms'],
                    debounced=False)

    def disable_hw_switch(self):
        """Removes the hardware rule to disable the hardware activation switch
        for this diverter.
        """

        for switch in self.config['activation_switches']:
            self.machine.platform.clear_hw_rule(switch)

        # todo this should not clear all the rules for this switch

    def disable_held_coil(self):
        """Physically disables the coil holding this diverter open."""
        self.config['activation_coil'].disable()

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

        # Since the 'target' kwarg is going to be an object, not a name, we need
        # to figure out if this object is one of the targets of this diverter.

        self.log.debug("Feeder device eject attempt for target: %s", target)

        if target in self.config['active_objects']:
            self.log.debug("Enabling diverter since eject target is on the "
                           "active target list")
            self.enable()

        elif target in self.config['inactive_objects']:
            self.log.debug("Enabling diverter since eject target is on the "
                           "inactive target list")
            self.disable()
Exemplo n.º 23
0
class Counter(LogicBlock):
    """A type of LogicBlock that tracks multiple hits of a single event.

    This counter can be configured to track hits towards a specific end-goal
    (like number of tilt hits to tilt), or it can be an open-ended count (like
    total number of ramp shots).

    It can also be configured to count up or to count down, and can have a
    configurable counting interval.
    """

    # todo settle time

    def __init__(self, machine, name, player, config):
        self.log = logging.getLogger('Counter.' + name)
        self.log.debug("Creating Counter LogicBlock")

        super(Counter, self).__init__(machine, name, player, config)

        self.delay = DelayManager()

        self.ignore_hits = False
        self.hit_value = -1

        config_spec = '''
                        count_events: list|None
                        count_complete_value: int|0
                        multiple_hit_window: ms|0
                        count_interval: int|1
                        direction: string|up
                        starting_count: int|0
                      '''

        self.config = Config.process_config(config_spec=config_spec,
                                            source=self.config)

        if 'event_when_hit' not in self.config:
            self.config['event_when_hit'] = ('counter_' + self.name + '_hit')

        if 'player_variable' not in self.config:
            self.config['player_variable'] = self.name + '_count'

        self.hit_value = self.config['count_interval']

        if self.config['direction'] == 'down' and self.hit_value > 0:
            self.hit_value *= -1
        elif self.config['direction'] == 'up' and self.hit_value < 0:
            self.hit_value *= -1

        self.player[self.config['player_variable']] = (
            self.config['starting_count'])

    def enable(self, **kwargs):
        """Enables this counter. Automatically called when one of the
        'enable_event's is posted. Can also manually be called.
        """
        super(Counter, self).enable()
        self.machine.events.remove_handler(self.hit)  # prevents multiples

        for event in self.config['count_events']:
            self.handler_keys.add(
                self.machine.events.add_handler(event, self.hit))

    def reset(self, **kwargs):
        """Resets the hit progress towards completion"""
        super(Counter, self).reset(**kwargs)
        self.player[self.config['player_variable']] = (
            self.config['starting_count'])

    def hit(self, **kwargs):
        """Increases the hit progress towards completion. Automatically called
        when one of the `count_events`s is posted. Can also manually be
        called.
        """
        if not self.ignore_hits:
            self.player[self.config['player_variable']] += self.hit_value
            self.log.debug("Processing Count change. Total: %s",
                           self.player[self.config['player_variable']])

            if self.config['count_complete_value'] is not None:

                if (self.config['direction'] == 'up'
                        and self.player[self.config['player_variable']] >=
                        self.config['count_complete_value']):
                    self.complete()

                elif (self.config['direction'] == 'down'
                      and self.player[self.config['player_variable']] <=
                      self.config['count_complete_value']):
                    self.complete()

            if self.config['event_when_hit']:
                self.machine.events.post(
                    self.config['event_when_hit'],
                    count=self.player[self.config['player_variable']])

            if self.config['multiple_hit_window']:
                self.log.debug("Beginning Ignore Hits")
                self.ignore_hits = True
                self.delay.add('ignore_hits_within_window',
                               self.config['multiple_hit_window'],
                               self.stop_ignoring_hits)

    def stop_ignoring_hits(self, **kwargs):
        """Causes the Counter to stop ignoring subsequent hits that occur
        within the 'multiple_hit_window'. Automatically called when the window
        time expires. Can safely be manually called.
        """
        self.log.debug("Ending Ignore hits")
        self.ignore_hits = False
Exemplo n.º 24
0
class Diverter(Device):
    """Represents a diverter in a pinball machine.

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

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

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

        self.delay = DelayManager()

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

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

        self.trigger_type = 'software'  # 'software' or 'hardware'

        # Create a list of ball device objects when active and inactive. We need
        # this because ball eject attempts pass the target device as an object
        # rather than by name.

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

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

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

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

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

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

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

    def enable(self, auto=False, activations=-1, **kwargs):
        """Enables this diverter.

        Args:
            auto: Boolean value which is used to indicate whether this
                diverter enabled itself automatically. This is passed to the
                event which is posted.
            activations: Integer of how many times you'd like this diverter to
                activate before it will automatically disable itself. Default is
                -1 which is unlimited.

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

        If no `activation_switches` is specified, then the diverter is activated
        immediately.

        """
        self.enabled = True

        self.machine.events.post('diverter_' + self.name + '_enabling',
                                 auto=auto)

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

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

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

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

        self.machine.events.post('diverter_' + self.name + '_disabling',
                                 auto=auto)

        self.log.debug("Disabling Diverter")
        if self.config['activation_switches']:
            self.disable_switches()
        else:
            self.deactivate()

    def activate(self):
        """Physically activates this diverter's coil."""
        self.log.debug("Activating Diverter")
        self.active = True

        #if self.remaining_activations > 0:
        #    self.remaining_activations -= 1

        self.machine.events.post('diverter_' + self.name + '_activating')
        if self.config['type'] == 'pulse':
            self.config['activation_coil'].pulse()
        elif self.config['type'] == 'hold':
            self.config['activation_coil'].enable()
            self.schedule_deactivation()

    def deactivate(self):
        """Deactivates this diverter.

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

        self.machine.events.post('diverter_' + self.name + '_deactivating')
        self.config['activation_coil'].disable()

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

        #if self.remaining_activations != 0:
        #    self.enable()
            # todo this will be weird if the diverter is enabled without a hw
            # switch.. wonder if we should check for that here?

    def schedule_deactivation(self, time=None):
        """Schedules a delay to deactivate this diverter.

        Args:
            time: The MPF string time of how long you'd like the delay before
                deactivating the diverter. Default is None which means it uses
                the 'activation_time' setting configured for this diverter. If
                there is no 'activation_time' setting and no delay is passed,
                it will disable the diverter immediately.
        """
        if time is not None:
            delay = Timing.string_to_ms(time)
        elif self.config['activation_time']:
            delay = self.config['activation_time']
        else:
            delay = False

        if delay:
            self.delay.add(name='disable_held_coil', ms=delay,
                           callback=self.disable_held_coil)
        else:
            self.disable_held_coil()

    def enable_switches(self):
        if self.trigger_type == 'hardware':
            self.enable_hw_switches()
        else:
            self.enable_sw_switches()

    def disable_switches(self):
        if self.trigger_type == 'hardware':
            self.disable_hw_switches()
        else:
            self.disable_sw_switches()

    def enable_hw_switches(self):
        """Enables the hardware switch rule which causes this diverter to
        activate when the switch is hit.

        This is typically used for diverters on loops and ramps where you don't
        want the diverter to phsyically activate until the ramp entry switch is
        activated.

        If this diverter is configured with a activation_time, this method will
        also set switch handlers which will set a delay to deactivate the
        diverter once the activation activation_time expires.

        If this diverter is configured with a deactivation switch, this method
        will set up the switch handlers to deactivate the diverter when the
        deactivation switch is activated.
        """
        self.log.debug("Enabling Diverter for hw switch: %s",
                       self.config['activation_switches'])
        if self.config['type'] == 'hold':

            for switch in self.config['activation_switches']:

                self.platform.set_hw_rule(
                    sw_name=switch.name,
                    sw_activity=1,
                    driver_name=self.config['activation_coil'].name,
                    driver_action='hold',
                    disable_on_release=False,
                    **self.config)

                # If there's a activation_time then we need to watch for the hw
                # switch to be activated so we can disable the diverter

                if self.config['activation_time']:
                    self.machine.switch_controller.add_switch_handler(
                        switch.name,
                        self.schedule_deactivation)

        elif self.config['type'] == 'pulse':

            for switch in self.config['activation_switches']:

                self.platform.set_hw_rule(
                    sw_name=switch.name,
                    sw_activity=1,
                    driver_name=self.config['activation_coil'].name,
                    driver_action='pulse',
                    disable_on_release=False,
                    **self.config)

    def enable_sw_switches(self):
        self.log.debug("Enabling Diverter sw switches: %s",
                       self.config['activation_switches'])

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

    def disable_sw_switches(self):
        self.log.debug("Disabling Diverter sw switches: %s",
                       self.config['activation_switches'])

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

    def disable_hw_switches(self):
        """Removes the hardware rule to disable the hardware activation switch
        for this diverter.
        """
        for switch in self.config['activation_switches']:
            self.platform.clear_hw_rule(switch.name)

        # todo this should not clear all the rules for this switch

    def disable_held_coil(self):
        """Physically disables the coil holding this diverter open."""
        self.log.debug("Disabling Activation Coil")
        self.config['activation_coil'].disable()

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

            # If there are ejects waiting for the other target switch diverter
            if len(self.eject_attempt_queue) > 0:
                if self.eject_state == False:
                    self.eject_state = True
                    self.log.debug("Enabling diverter since eject target is on the "
                                   "active target list")
                    self.enable()
                elif self.eject_state == True:
                    self.eject_state = False
                    self.log.debug("Enabling diverter since eject target is on the "
                                   "inactive target list")
                    self.disable()
            # And perform those ejects
            while len(self.eject_attempt_queue) > 0:
                self.diverting_ejects_count += 1
                queue = self.eject_attempt_queue.pop()
                queue.clear()

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

        # Since the 'target' kwarg is going to be an object, not a name, we need
        # to figure out if this object is one of the targets of this diverter.

        self.log.debug("Feeder device eject attempt for target: %s", target)

        desired_state = None
        if target in self.config['targets_when_active']:
            desired_state = True

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

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

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

        self.diverting_ejects_count += 1

        if desired_state == True:
            self.log.debug("Enabling diverter since eject target is on the "
                           "active target list")
            self.eject_state = desired_state
            self.enable()
        elif desired_state == False:
            self.log.debug("Enabling diverter since eject target is on the "
                           "inactive target list")
            self.eject_state = desired_state
            self.disable()
Exemplo n.º 25
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.º 26
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.º 27
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()