Beispiel #1
0
class FlasherPlayer(DeviceConfigPlayer):
    """Triggers flashers based on config."""

    config_file_section = 'flasher_player'
    show_section = 'flashers'

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

    def play(self, settings, context, calling_context, priority=0, **kwargs):
        """Flash flashers."""
        del kwargs

        for flasher, s in settings.items():
            if isinstance(flasher, str):
                self._flash(self.machine.lights[flasher],
                            duration_ms=s['ms'],
                            key=context)
            else:
                self._flash(flasher, duration_ms=s['ms'], key=context)

    def _flash(self, light, duration_ms, key):
        light.color("white", fade_ms=0, key=key)
        self.delay.add(duration_ms, self._remove_flash, light=light, key=key)

    @staticmethod
    def _remove_flash(light, key):
        light.remove_from_stack_by_key(key=key)

    def get_express_config(self, value):
        """Parse express config."""
        return dict(ms=value)
Beispiel #2
0
class EventPlayer(FlatConfigPlayer):

    """Posts events based on config."""

    config_file_section = 'event_player'
    show_section = 'events'

    __slots__ = ["delay"]

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

    def play(self, settings, context, calling_context, priority=0, **kwargs):
        """Post (delayed) events."""
        for event, s in settings.items():
            s = deepcopy(s)
            event_dict = self.machine.placeholder_manager.parse_conditional_template(event)

            if event_dict['condition'] and not event_dict['condition'].evaluate(kwargs):
                continue

            if event_dict['number']:
                delay = Util.string_to_ms(event_dict['number'])
                self.delay.add(callback=self._post_event, ms=delay,
                               event=event_dict['name'], s=s, **kwargs)
            else:
                self._post_event(event_dict['name'], s, **kwargs)

    def _post_event(self, event, s, **kwargs):
        event_name_placeholder = TextTemplate(self.machine, event.replace("(", "{").replace(")", "}"))
        for key, param in s.items():
            if isinstance(param, dict):
                s[key] = self._evaluate_event_param(param, kwargs)
        self.machine.events.post(event_name_placeholder.evaluate(kwargs), **s)

    def _evaluate_event_param(self, param, kwargs):
        if param.get("type") == "float":
            placeholder = self.machine.placeholder_manager.build_float_template(param["value"])
        elif param.get("type") == "int":
            placeholder = self.machine.placeholder_manager.build_int_template(param["value"])
        elif param.get("type") == "bool":
            placeholder = self.machine.placeholder_manager.build_bool_template(param["value"])
        else:
            placeholder = self.machine.placeholder_manager.build_string_template(param["value"])
        return placeholder.evaluate(kwargs)

    def get_list_config(self, value):
        """Parse list."""
        result = {}
        for event in value:
            result[event] = {}
        return result

    def get_express_config(self, value):
        """Parse short config."""
        return self.get_list_config(Util.string_to_list(value))
Beispiel #3
0
class EventPlayer(FlatConfigPlayer):
    """Posts events based on config."""

    config_file_section = 'event_player'
    show_section = 'events'

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

    def play(self, settings, context, calling_context, priority=0, **kwargs):
        """Post (delayed) events."""
        for event, s in settings.items():
            s = deepcopy(s)
            event_dict = self.machine.placeholder_manager.parse_conditional_template(
                event)

            if event_dict['condition'] and not event_dict[
                    'condition'].evaluate(kwargs):
                continue

            if event_dict['number']:
                delay = Util.string_to_ms(event_dict['number'])
                self.delay.add(callback=self._post_event,
                               ms=delay,
                               event=event_dict['name'],
                               s=s)
            else:
                self._post_event(event_dict['name'], s)

    def _post_event(self, event, s):
        event_name_placeholder = TextTemplate(
            self.machine,
            event.replace("(", "{").replace(")", "}"))
        self.machine.events.post(event_name_placeholder.evaluate({}), **s)

    def get_list_config(self, value):
        """Parse list."""
        result = {}
        for event in value:
            result[event] = {}
        return result

    def get_express_config(self, value):
        """Parse short config."""
        return self.get_list_config(Util.string_to_list(value))
Beispiel #4
0
class EventPlayer(FlatConfigPlayer):
    """Posts events based on config."""

    config_file_section = 'event_player'
    show_section = 'events'
    device_collection = None

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

    def play(self, settings, context, calling_context, priority=0, **kwargs):
        """Post (delayed) events."""
        del kwargs
        for event, s in settings.items():
            s = deepcopy(s)
            if '|' in event:
                event, delay = event.split("|")
                delay = Util.string_to_ms(delay)
                self.delay.add(callback=self._post_event,
                               ms=delay,
                               event=event,
                               s=s)
            elif ':' in event:
                event, delay = event.split(":")
                delay = Util.string_to_ms(delay)
                self.delay.add(callback=self._post_event,
                               ms=delay,
                               event=event,
                               s=s)
            else:
                self._post_event(event, s)

    def _post_event(self, event, s):
        self.machine.events.post(event, **s)

    def get_list_config(self, value):
        """Parse list."""
        result = {}
        for event in value:
            result[event] = {}
        return result

    def get_express_config(self, value):
        """Parse short config."""
        return self.get_list_config(Util.string_to_list(value))
Beispiel #5
0
class RpiDriver(DriverPlatformInterface):
    """An output on a Rasoberry Pi."""
    def __init__(self, number, config, platform):
        """Initialise output."""
        super().__init__(config, number)
        self.platform = platform  # type: RaspberryPiHardwarePlatform
        self.gpio = int(self.number)
        self.delay = DelayManager(self.platform.machine.delayRegistry)

    def get_board_name(self):
        """Return name."""
        return "Raspberry Pi"

    def pulse(self, pulse_settings: PulseSettings):
        """Pulse output."""
        self.enable(pulse_settings, None)

    def enable(self, pulse_settings: PulseSettings,
               hold_settings: Optional[HoldSettings]):
        """Enable output."""
        self.platform.send_command(self.platform.pi.write(self.gpio, 1))
        if hold_settings and hold_settings.power == 1:
            # do nothing. just keep driver enabled
            pass
        elif hold_settings and hold_settings.power > 0:
            # schedule pwm
            self.delay.add(pulse_settings.duration,
                           self._pwm,
                           hold_power=hold_settings.power)
        else:
            # no hold. disable after pulse
            self.delay.add(pulse_settings.duration, self.disable)

    def _pwm(self, hold_power):
        """Set up pwm."""
        self.platform.send_command(
            self.platform.pi.set_PWM_dutycycle(self.gpio, hold_power * 255))

    def disable(self):
        """Disable output."""
        self.platform.send_command(self.platform.pi.write(self.gpio, 0))
        # clear all delays
        self.delay.clear()
Beispiel #6
0
class sbone_servoactivation(Mode):
    def mode_init(self):
        self.log.info('sbone_servoactivation_mode_init')
        self.delay = DelayManager(self.machine.delayRegistry)

    def mode_start(self, **kwargs):
        self.log.info('sbone_servoactivation mode_start')

        self.add_mode_event_handler('balldevice_bd_sbone_ball_eject_attempt',
                                    self.eject_a_ball)

    def eject_a_ball(self, **kwargs):
        self.machine.events.post('sbone_ejecting_a_ball')
        self.machine.events.post('sbone_lock_servo_open')

        if self.machine.ball_devices.bd_sbone.available_balls == 3:
            self.log.info('activating delay for 3 balls')
            delay = 100
        elif self.machine.ball_devices.bd_sbone.available_balls == 2:
            self.log.info('activating delay for 2 balls')
            delay = 120
        elif self.machine.ball_devices.bd_sbone.available_balls == 1:
            self.log.info('activating delay for 1 balls')
            delay = 140
        else:
            self.log.info('activating delay for unknown balls')
            delay = 140

        self.delay.add(callback=self._post_event,
                       ms=delay,
                       event='sbone_lock_servo_closed')
        self.delay.add(callback=self._post_event,
                       ms=600,
                       event='sbone_ball_has_probably_been_released')

    def _post_event(self, event):
        self.machine.events.post(event)

    def mode_stop(self, **kwargs):
        self.machine.events.post('sbone_servoactivation_mode_ended')
        self.log.info('sbone_servoactivation_mode_stop')
Beispiel #7
0
class bonus_award(Mode):
    def mode_init(self):
        self.log.info('bonus award python code initalized')

    def mode_start(self, **kwargs):
        #initalize the delay manager
        self.delay = DelayManager(self.machine.delayRegistry)
        #The way this mode works, it watches for this slide to be posted:
        self.add_mode_event_handler("slide_bonus_award_1_slide_active",
                                    self.bonus_start_1)
#Then the mode runs through three different delay managers

#this is the first step, it displays the bonus award slide for 1.5 seconds before
#handing the mode over to the next step

    def bonus_start_1(self, **kwargs):
        self.log.info('bonus start 1 called')
        #this varable is needed to hold the bonus value in the next step
        #it was just convient to put it here in this method
        self.player.bonus_holder = self.player.bonus
        self.delay.add(name='bonus_1', ms=1500, callback=self.bonus_addup)

#this is the 2nd step in the bonus award.  It counts down the bonus, and
#adds the bonus to the score.  Because we are using player.bonus, and
#mode bonus is still running, the bonus lights will count down with the
#award of the bonus points, just like in the orignal game.

    def bonus_addup(self, **kwargs):
        if self.player.bonus_x > 0:
            # if we still have bonus x, we keep counting down bonus
            if self.player.bonus > 0:
                self.player.score += 1000
                self.player.bonus -= 1000

                # when we run out of bonus, we lower bonus x, and reset bonus total
            else:
                self.player.bonus_x -= 1
                if self.player.bonus_x > 0:
                    self.player.bonus = self.player.bonus_holder

        #we are basically running this in a loop until we run out of bonux X
            self.delay.add(name='bonus_addup',
                           ms=50,
                           callback=self.bonus_addup)
        #when we are out of bonus X, we stop the count down, and pause before
        #moving on..
        else:
            self.delay.add(name='last_bonus_delay',
                           ms=1000,
                           callback=self.last_bonus)


#this is the last step.  when this is called after the last_bonus_delay pause
#to allow the player to see the score before we pull the slide.  The event
#listed below triggers the slide player in the yaml file to remove the slide

    def last_bonus(self, **kwargs):
        self.machine.events.post('remove_bonus_award_1')
Beispiel #8
0
class Timer(LogMixin):
    """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):
        """Initialise mode timer."""
        self.machine = machine
        self.mode = mode
        self.name = name
        self.config = config

        self.running = False
        self.start_value = self.config['start_value'].evaluate([])
        self.restart_on_complete = self.config['restart_on_complete']
        self._ticks = 0
        self.tick_var = '{}_{}_tick'.format(self.mode.name, self.name)

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

        self.ticks_remaining = 0
        self.max_value = self.config['max_value']
        self.direction = self.config['direction'].lower()
        self.tick_secs = self.config['tick_interval'] / 1000.0
        self.timer = None
        self.event_keys = set()
        self.delay = DelayManager(self.machine.delayRegistry)

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

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

        self.ticks = self.start_value

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

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

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

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

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

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

        except TypeError:
            pass

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

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

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

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

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

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

            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):
        self.debug_log("Removing control events")

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

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

        Does not start or stop the timer.

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

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

        self.jump(self.start_value)

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

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

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

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

        self.info_log("Starting Timer.")

        if self._check_for_done():
            return ()

        self.running = True

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

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

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

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

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

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

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

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

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

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

        self.info_log("Stopping Timer")

        self.delay.remove('pause')

        self.running = False
        self._remove_system_timer()

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

        desc: The timer named (name) has stopped.

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

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

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

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

        if not timer_value:
            timer_value = 0  # make sure it's not None, etc.

        self.info_log("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.ticks,
                                 ticks_remaining=self.ticks_remaining)
        '''event: timer_(name)_paused

        desc: The timer named (name) has paused.

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

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

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

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

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

        self.info_log("Timer Complete")

        self.stop()

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

        desc: The timer named (name) has completed.

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

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

        if self.restart_on_complete:

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

            self.reset()
            self.start()

    def _timer_tick(self, dt):
        # Automatically called by the core clock each tick
        del dt

        self.debug_log("Timer Tick")

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

            self._remove_system_timer()
            return

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

        self._post_tick_events()

    def _post_tick_events(self):

        if not self._check_for_done():
            self.machine.events.post('timer_' + self.name + '_tick',
                                     ticks=self.ticks,
                                     ticks_remaining=self.ticks_remaining)
            '''event: timer_(name)_tick

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

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

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

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

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

        ticks_added = timer_value

        new_value = self.ticks + ticks_added

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

        self.ticks = new_value
        ticks_added = new_value - timer_value

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

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

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

        self._check_for_done()

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

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

        ticks_subtracted = timer_value

        self.ticks -= ticks_subtracted

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

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

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

        self._check_for_done()

    def _check_for_done(self):
        # Checks to see if this timer is done. Automatically called anytime the
        # timer's value changes.

        self.debug_log(
            "Checking to see if timer is done. Ticks: %s, End "
            "Value: %s, Direction: %s", self.ticks, self.end_value,
            self.direction)

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

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

        self.debug_log("Timer is not done")

        return False

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

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

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

        Args:
            change: Float or int of the change you want to make to this timer's
                tick rate. Note this value is 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.
        """
        del kwargs

        self.tick_secs *= change
        self._create_system_timer()

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

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

        Args:
            timer_value: The new number of seconds between each tick of this
                timer. This value should always be positive.
            **kwargs: Not used in this method. Only exists since this method is
                often registered as an event handler which may contain
                additional keyword arguments.
        """
        del kwargs

        self.tick_secs = abs(timer_value)
        self._create_system_timer()

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

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

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

        self.ticks = int(timer_value)

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

        self._check_for_done()

    def kill(self):
        """Stop this timer and also removes all the control events."""
        self.stop()
        self._remove_control_events()
Beispiel #9
0
class TestEventManager(MpfFakeGameTestCase, MpfTestCase):
    def __init__(self, test_map):
        super().__init__(test_map)
        self._handler1_args = tuple()
        self._handler1_kwargs = dict()
        self._handler1_called = 0
        self._handler2_args = tuple()
        self._handler2_kwargs = dict()
        self._handler2_called = 0
        self._handler3_args = tuple()
        self._handler3_kwargs = dict()
        self._handler3_called = 0
        self._handlers_called = list()
        self._handler_returns_false_args = tuple()
        self._handler_returns_false_kwargs = dict()
        self._handler_returns_false_called = 0
        self._relay1_called = 0
        self._relay2_called = 0
        self._relay_callback_args = tuple()
        self._relay_callback_kwargs = dict()
        self._relay_callback_called = 0
        self._callback_args = tuple()
        self._callback_kwargs = dict()
        self._callback_called = 0
        self._queue = None
        self._queue_callback_args = tuple()
        self._queue_callback_kwargs = dict()
        self._queue_callback_called = 0

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

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

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

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

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

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

        return False

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

        return {'relay_test': relay_test}

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

        return {'relay_test': relay_test - 1}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.assertEqual(1, self._callback_called)

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

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

        self.advance_time_and_run(1)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.machine.events.remove_handler_by_key(key)

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

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

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

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

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

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

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

        self.machine.events.remove_handlers_by_keys(keys)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.advance_time_and_run(1)

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

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

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

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

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

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

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

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

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

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

        self.advance_time_and_run(1)

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

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

        self.event_handler_clear_queue()
        self.advance_time_and_run()

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

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

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

        self.advance_time_and_run(1)

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

        self.advance_time_and_run(1)

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

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

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

        self.advance_time_and_run(1)

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

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

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

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

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

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

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

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

        self.advance_time_and_run(1)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.assertTrue(self.correct)

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

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

        raise AssertionError("this should never be called")

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

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

        self.advance_time_and_run(10)

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

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

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

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

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

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

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

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

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

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

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

        # reset exception
        self._exception = None

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.assertEventNotCalled("out3")
        self.assertEventCalled("out4")
Beispiel #10
0
class Multiball(SystemWideDevice, ModeDevice):

    """Multiball device for MPF."""

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

    def __init__(self, machine, name):
        """Initialise multiball."""
        self.ball_locks = None
        self.source_playfield = None
        super().__init__(machine, name)

        self.delay = DelayManager(machine.delayRegistry)
        self.balls_added_live = 0
        self.balls_live_target = 0
        self.enabled = False
        self.shoot_again = False

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

    def device_removed_from_mode(self, mode):
        """Disable and stop mb when mode stops."""
        del mode
        # disable mb when mode ends
        self.disable()

        # also stop mb if no shoot again is specified
        self.stop()

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

    @classmethod
    def prepare_config(cls, config, is_mode_config):
        """Add default enable_events and disable_events outside mode."""
        if not is_mode_config:
            if 'enable_events' not in config:
                config['enable_events'] = 'ball_started'
            if 'disable_events' not in config:
                config['disable_events'] = 'ball_will_end'
        return super().prepare_config(config, is_mode_config)

    def _handle_balls_in_play_and_balls_live(self):
        ball_count = self.config['ball_count'].evaluate([])

        if self.config['ball_count_type'] == "total":
            # policy: total balls
            if ball_count > self.machine.game.balls_in_play:
                self.balls_added_live = ball_count - self.machine.game.balls_in_play
                self.machine.game.balls_in_play = ball_count
            self.balls_live_target = ball_count
        else:
            # policy: add balls
            self.balls_added_live = ball_count
            self.machine.game.balls_in_play += self.balls_added_live
            self.balls_live_target = self.machine.game.balls_in_play

    @event_handler(10)
    def start(self, **kwargs):
        """Start multiball."""
        del kwargs
        if not self.enabled:
            return

        if self.balls_live_target > 0:
            self.debug_log("Cannot start MB because %s are still in play",
                           self.balls_added_live)
            return

        self.shoot_again = True

        self._handle_balls_in_play_and_balls_live()
        self.debug_log("Starting multiball with %s balls (added %s)", self.balls_live_target, self.balls_added_live)

        balls_added = 0

        # eject balls from locks
        for device in self.ball_locks:
            balls_to_release = max(min(device.available_balls, self.balls_added_live - balls_added), 0)
            device.eject(balls_to_release)
            balls_added += balls_to_release

        # request remaining balls
        if self.balls_added_live - balls_added > 0:
            self.source_playfield.add_ball(balls=self.balls_added_live - 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 self.config['shoot_again'] > 0:
                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.balls_live_target)
        '''event: multiball_(name)_started
        desc: The multiball called (name) has just started.
        args:
            balls: The number of balls in this multiball
        '''

    def _ball_drain_shoot_again(self, balls, **kwargs):
        del kwargs

        balls_to_safe = self.balls_live_target - self.machine.game.balls_in_play + balls

        if balls_to_safe <= 0:
            return {'balls': balls}

        if balls_to_safe > balls:
            balls_to_safe = balls

        self.machine.events.post("multiball_" + self.name + "_shoot_again", balls=balls_to_safe)
        '''event: multiball_(name)_shoot_again
        desc: A ball has drained during the multiball called (name) while the
        ball save timer for that multiball was running, so a ball (or balls)
        will be saved and re-added into play.

        args:
            balls: The number of balls that are being saved.
        '''

        self.debug_log("Ball drained during MB. Requesting a new one")
        self.source_playfield.add_ball(balls=balls_to_safe)
        return {'balls': balls - balls_to_safe}

    def _ball_drain_count_balls(self, balls, **kwargs):
        del kwargs
        self.machine.events.post("multiball_{}_ball_lost".format(self.name))
        '''event: multiball_(name)_lost_ball
        desc: The multiball called (name) has lost a ball after ball save expired.
        '''

        if not self.machine.game or self.machine.game.balls_in_play - balls < 1:
            self.balls_added_live = 0
            self.balls_live_target = 0
            self.machine.events.remove_handler(self._ball_drain_count_balls)
            self.machine.events.post("multiball_{}_ended".format(self.name))
            '''event: multiball_(name)_ended
            desc: The multiball called (name) has just ended.
            '''
            self.debug_log("Ball drained. MB ended.")

    @event_handler(5)
    def stop(self, **kwargs):
        """Stop shoot again."""
        del kwargs
        self.debug_log("Stopping shoot again of multiball")
        self.shoot_again = False

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

        self.machine.events.post("multiball_" + self.name + "_shoot_again_ended")
        '''event: multiball_(name)_shoot_again_ended
        desc: Shoot again for multiball (name) has ended.
        '''

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

    @event_handler(8)
    def add_a_ball(self, **kwargs):
        """Add a ball if multiball has started."""
        del kwargs
        if self.balls_live_target > 0:
            self.debug_log("Adding a ball.")
            self.balls_live_target += 1
            self.balls_added_live += 1
            self.machine.game.balls_in_play += 1
            self.source_playfield.add_ball(balls=1)

    @event_handler(9)
    def start_or_add_a_ball(self, **kwargs):
        """Start multiball or add a ball if multiball has started."""
        del kwargs
        if self.balls_live_target > 0:
            self.add_a_ball()
        else:
            self.start()

    @event_handler(20)
    def enable(self, **kwargs):
        """Enable the multiball.

        If the multiball is not enabled, it cannot start.

        Args:
            **kwargs: unused
        """
        del kwargs
        self.debug_log("Enabling...")
        self.enabled = True

    @event_handler(1)
    def disable(self, **kwargs):
        """Disable the multiball.

        If the multiball is not enabled, it cannot start. Will not stop a running multiball.

        Args:
            **kwargs: unused
        """
        del kwargs
        self.debug_log("Disabling...")
        self.enabled = False

    @event_handler(2)
    def reset(self, **kwargs):
        """Reset the multiball and disable it.

        Args:
            **kwargs: unused
        """
        del kwargs
        self.enabled = False
        self.shoot_again = False
        self.balls_added_live = 0
Beispiel #11
0
class System11OverlayPlatform(DriverPlatform):
    """Overlay platform to drive system11 machines using a WPC controller."""

    __slots__ = [
        "delay", "platform", "system11_config", "a_side_queue", "c_side_queue",
        "a_side_done_time", "c_side_done_time", "drivers_holding_a_side",
        "drivers_holding_c_side", "a_side_enabled", "c_side_enabled",
        "ac_relay_in_transition", "prefer_a_side", "drivers"
    ]

    def __init__(self, machine: MachineController) -> None:
        """Initialise the board."""
        super().__init__(machine)

        self.delay = DelayManager(machine)

        self.platform = None  # type: Optional[DriverPlatform]

        self.system11_config = None  # type: Any

        self.a_side_queue = \
            set()   # type: Set[Tuple[DriverPlatformInterface, Optional[PulseSettings], Optional[HoldSettings]]]
        self.c_side_queue = \
            set()   # type: Set[Tuple[DriverPlatformInterface, Optional[PulseSettings], Optional[HoldSettings]]]

        self.a_side_done_time = 0
        self.c_side_done_time = 0
        self.drivers_holding_a_side = set(
        )  # type: Set[DriverPlatformInterface]
        self.drivers_holding_c_side = set(
        )  # type: Set[DriverPlatformInterface]
        self.a_side_enabled = True
        self.c_side_enabled = False
        self.drivers = {}  # type: Dict[str, DriverPlatformInterface]

        self.ac_relay_in_transition = False
        # Specify whether the AC relay should favour the A or C side when at rest.
        # Typically during a game the 'C' side should be preferred, since that is
        # normally where the flashers are which need a quick response without having to wait on the relay.
        # At game over though, it should prefer the 'A' side so that the relay isn't permanently energised.
        self.prefer_a_side = True

    def stop(self):
        """Stop the overlay. Nothing to do here because stop is also called on parent platform."""

    @property
    def a_side_busy(self):
        """Return if A side cannot be switches off right away."""
        return self.drivers_holding_a_side or self.a_side_done_time > self.machine.clock.get_time(
        ) or self.a_side_queue

    @property
    def c_side_active(self):
        """Return if C side cannot be switches off right away."""
        return self.drivers_holding_c_side or self.c_side_done_time > self.machine.clock.get_time(
        )

    @property
    def c_side_busy(self):
        """Return if C side cannot be switches off right away."""
        return self.drivers_holding_c_side or self.c_side_done_time > self.machine.clock.get_time(
        ) or self.c_side_queue

    @property
    def a_side_active(self):
        """Return if A side cannot be switches off right away."""
        return self.drivers_holding_a_side or self.a_side_done_time > self.machine.clock.get_time(
        )

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

    async def initialize(self):
        """Automatically called by the Platform class after all the core modules are loaded."""
        # load coil platform
        self.platform = self.machine.get_platform_sections(
            "platform",
            getattr(self.machine.config.get('system11', {}), 'platform', None))

        # we have to wait for coils to be initialized
        self.machine.events.add_handler("init_phase_1", self._initialize)

    def _initialize(self, **kwargs):
        del kwargs
        self._validate_config()

        self.configure_logging('Platform.System11',
                               self.system11_config['console_log'],
                               self.system11_config['file_log'])

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

        self.system11_config['ac_relay_driver'].get_and_verify_hold_power(1.0)

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

        self.machine.events.add_handler(
            self.system11_config['prefer_a_side_event'], self._prefer_a_side)
        self.log.info(
            "Configuring System11 driver to prefer A side on event %s",
            self.system11_config['prefer_a_side_event'])

        self.machine.events.add_handler(
            self.system11_config['prefer_c_side_event'], self._prefer_c_side)
        self.log.info(
            "Configuring System11 driver to prefer C side on event %s",
            self.system11_config['prefer_c_side_event'])

    def _prefer_a_side(self, **kwargs):
        del kwargs
        self.prefer_a_side = True
        self._enable_a_side()

    def _prefer_c_side(self, **kwargs):
        del kwargs
        self.prefer_a_side = False
        self._enable_c_side()

    def _validate_config(self):
        self.system11_config = self.machine.config_validator.validate_config(
            'system11', self.machine.config.get('system11', {}))

    def tick(self):
        """System11 main loop.

        Called based on the timer_tick event.
        """
        if self.prefer_a_side:
            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()
        else:
            if self.c_side_queue:
                self._service_c_side()
            elif self.a_side_queue:
                self._service_a_side()
            elif self.a_side_enabled and not self.a_side_active:
                self._enable_c_side()

    def configure_driver(self, config: DriverConfig, number: str,
                         platform_settings: dict):
        """Configure a driver on the system11 overlay.

        Args:
            config: Driver config dict
            number: Number of the driver.
            platform_settings: Platform specific config.
        """
        assert self.platform is not None
        orig_number = number

        if number and (number.lower().endswith('a')
                       or number.lower().endswith('c')):

            side = number[-1:].upper()
            number = number[:-1]

            # only configure driver once
            if number not in self.drivers:
                self.drivers[number] = self.platform.configure_driver(
                    config, number, platform_settings)

            system11_driver = System11Driver(orig_number, self.drivers[number],
                                             self, side)

            return system11_driver

        return self.platform.configure_driver(config, number,
                                              platform_settings)

    def set_pulse_on_hit_and_release_rule(self, enable_switch, coil):
        """Configure a rule for a driver on the system11 overlay.

        Will pass the call onto the parent platform if the driver is not on A/C relay.
        """
        if coil.hw_driver in self.drivers.values():
            raise AssertionError(
                "Received a request to set a hardware rule for a System11 driver {}. "
                "This is not supported.".format(coil))

        self.platform.set_pulse_on_hit_and_release_rule(enable_switch, coil)

    def set_pulse_on_hit_and_enable_and_release_rule(self, enable_switch,
                                                     coil):
        """Configure a rule for a driver on the system11 overlay.

        Will pass the call onto the parent platform if the driver is not on A/C relay.
        """
        if coil.hw_driver in self.drivers.values():
            raise AssertionError(
                "Received a request to set a hardware rule for a System11 driver {}. "
                "This is not supported.".format(coil))

        self.platform.set_pulse_on_hit_and_enable_and_release_rule(
            enable_switch, coil)

    def set_pulse_on_hit_and_release_and_disable_rule(
            self, enable_switch: SwitchSettings, eos_switch: SwitchSettings,
            coil: DriverSettings, repulse_settings: Optional[RepulseSettings]):
        """Configure a rule for a driver on the system11 overlay.

        Will pass the call onto the parent platform if the driver is not on A/C relay.
        """
        if coil.hw_driver in self.drivers.values():
            raise AssertionError(
                "Received a request to set a hardware rule for a System11 driver {}. "
                "This is not supported.".format(coil))

        self.platform.set_pulse_on_hit_and_release_and_disable_rule(
            enable_switch, eos_switch, coil, repulse_settings)

    def set_pulse_on_hit_and_enable_and_release_and_disable_rule(
            self, enable_switch: SwitchSettings, eos_switch: SwitchSettings,
            coil: DriverSettings, repulse_settings: Optional[RepulseSettings]):
        """Configure a rule for a driver on the system11 overlay.

        Will pass the call onto the parent platform if the driver is not on A/C relay.
        """
        if coil.hw_driver in self.drivers.values():
            raise AssertionError(
                "Received a request to set a hardware rule for a System11 driver {}. "
                "This is not supported.".format(coil))

        self.platform.set_pulse_on_hit_and_enable_and_release_and_disable_rule(
            enable_switch, eos_switch, coil, repulse_settings)

    def set_pulse_on_hit_rule(self, enable_switch, coil):
        """Configure a rule on the system11 overlay.

        Will pass the call onto the parent platform if the driver is not on A/C relay.
        """
        if coil.hw_driver in self.drivers.values():
            raise AssertionError(
                "Received a request to set a hardware rule for a System11 driver {}. "
                "This is not supported.".format(coil))

        self.platform.set_pulse_on_hit_rule(enable_switch, coil)

    def clear_hw_rule(self, switch, coil):
        """Clear a rule for a driver on the system11 overlay."""
        if coil.hw_driver in self.drivers.values():
            raise AssertionError(
                "Received a request to clear a hardware rule for a System11 driver {}. "
                "This is not supported.".format(coil))

        self.platform.clear_hw_rule(switch, coil)

    def driver_action(self, driver, pulse_settings: Optional[PulseSettings],
                      hold_settings: Optional[HoldSettings], side: str):
        """Add 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.
            pulse_settings: Settings for the pulse or None
            hold_settings:Settings for hold or None
            side: Whatever the driver is on A or C side.

        This action will be serviced immediately if it can, or ASAP otherwise.
        """
        if self.prefer_a_side:
            if side == "A":
                self.a_side_queue.add((driver, pulse_settings, hold_settings))
                self._service_a_side()
            elif side == "C":
                self.c_side_queue.add((driver, pulse_settings, hold_settings))
                if not self.ac_relay_in_transition and not self.a_side_busy:
                    self._service_c_side()
            else:
                raise AssertionError("Invalid side {}".format(side))
        else:
            if side == "C":
                self.c_side_queue.add((driver, pulse_settings, hold_settings))
                self._service_c_side()
            elif side == "A":
                self.a_side_queue.add((driver, pulse_settings, hold_settings))
                if not self.ac_relay_in_transition and not self.c_side_busy:
                    self._service_a_side()
            else:
                raise AssertionError("Invalid side {}".format(side))

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

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

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

    def _enable_a_side(self):
        if self.prefer_a_side:
            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._disable_ac_relay()
                    self.delay.add(
                        ms=self.system11_config['ac_relay_delay_ms'],
                        callback=self._enable_a_side,
                        name='enable_a_side')
                    return

                if self.c_side_enabled:
                    self._disable_ac_relay()

                else:
                    self._a_side_enabled()
        else:
            if (not self.ac_relay_in_transition and not self.a_side_enabled
                    and not self.c_side_busy):
                self._disable_ac_relay()

            elif self.a_side_enabled and self.a_side_queue:
                self._service_a_side()

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

            if self.c_side_queue:
                self._enable_c_side()
                return

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

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

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

        while self.a_side_queue:
            driver, pulse_settings, hold_settings = self.a_side_queue.pop()

            if hold_settings is None and pulse_settings:
                driver.pulse(pulse_settings)
                self.a_side_done_time = max(
                    self.a_side_done_time,
                    self.machine.clock.get_time() +
                    (pulse_settings.duration / 1000.0))

            elif hold_settings and pulse_settings:
                driver.enable(pulse_settings, hold_settings)
                self.drivers_holding_a_side.add(driver)

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

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

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

                if self.a_side_active:
                    self._disable_all_a_side_drivers()
                    self._enable_ac_relay()
                    self.delay.add(
                        ms=self.system11_config['ac_relay_delay_ms'],
                        callback=self._enable_c_side,
                        name='enable_c_side')
                    return

                if self.a_side_enabled:
                    self._enable_ac_relay()

                else:
                    self._c_side_enabled()
        else:
            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.prefer_a_side:
            self.c_side_enabled = True
            self.a_side_enabled = False
            self._service_c_side()
        else:

            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

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

        while self.c_side_queue:
            driver, pulse_settings, hold_settings = self.c_side_queue.pop()

            if hold_settings is None and pulse_settings:
                driver.pulse(pulse_settings)
                self.c_side_done_time = max(
                    self.c_side_done_time,
                    self.machine.clock.get_time() +
                    (pulse_settings.duration / 1000.0))
            elif hold_settings and pulse_settings:
                driver.enable(pulse_settings, hold_settings)
                self.drivers_holding_c_side.add(driver)

            else:
                driver.disable()
                try:
                    self.drivers_holding_c_side.remove(driver)
                except KeyError:
                    pass

    def _disable_all_c_side_drivers(self):
        if self.c_side_active:
            for driver in self.drivers_holding_c_side:
                driver.disable()
            self.drivers_holding_c_side = set()
            self.c_side_done_time = 0
            self.c_side_enabled = False

    def _disable_all_a_side_drivers(self):
        if self.a_side_active:
            for driver in self.drivers_holding_a_side:
                driver.disable()
            self.drivers_holding_a_side = set()
            self.a_side_done_time = 0
            self.a_side_enabled = False

    def validate_coil_section(self, driver, config):
        """Validate coil config for platform."""
        return self.platform.validate_coil_section(driver, config)
Beispiel #12
0
class Stepper(SystemWideDevice):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def _check_home_complete(self):
        # TODO add timeout that stops this with error event if it hasn't made it in some amount of time
        if self._isHomed:
            return
        if self.hw_stepper.is_move_complete():
            self._isMoving = False
            self._isHomed = True
            # now move to reset position
            self.move_abs_pos(self._resetPosition)
        else:
            # reschedule
            self._schedule_home_complete_check()

    @event_handler(1)
    def reset(self, **kwargs):
        self.log.debug('Resetting')
        del kwargs
        self.stop()

        """ If position mode, home """
        if self.positionMode:
            self.home()            

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

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

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

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

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

        # move to last commanded
        if self.positionMode:
            self.move_abs_pos(self._cachedPosition)
        else:
            self.move_vel_mode(self._cachedVelocity)
Beispiel #13
0
class Stepper(SystemWideDevice):
    """Represents an stepper motor based axis in a pinball machine.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.move_to_position(position)

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

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

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

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

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

        # move to last position
        self._target_position = self._ball_search_old_target
        self._move_to_absolute_position(self._target_position)
Beispiel #14
0
class Diverter(SystemWideDevice):
    """Represents a diverter in a pinball machine.

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

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

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

        self.delay = DelayManager(machine)

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

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

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

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

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

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

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

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

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

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

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

    def reset(self):
        """Reset and deactivate the diverter."""
        self.deactivate()

    @event_handler(10)
    def event_enable(self, auto=False, **kwargs):
        """Handle enable control event."""
        del kwargs
        self.enable(auto)

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

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

        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)
        '''event: diverter_(name)_enabling
        desc: The diverter called (name) is enabling itself. Note that if this
            diverter has ``activation_switches:`` configured, it will not
            physically activate until one of those switches is hit. Otherwise
            this diverter will activate immediately.

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

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

    @event_handler(0)
    def event_disable(self, auto=False, **kwargs):
        """Handle disable control event."""
        del kwargs
        self.disable(auto)

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

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

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

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

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

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

    def _coil_activate(self):
        """Activate the coil."""
        if self.config['activation_coil']:
            if self.config['type'] == 'pulse':
                self.config['activation_coil'].pulse()
            elif self.config['type'] == 'hold':
                self.config['activation_coil'].enable()

    def _coil_deactivate(self):
        """Deactivate the coil."""
        if self.config['activation_coil']:
            self.config['activation_coil'].disable()

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

    @event_handler(9)
    def event_activate(self, **kwargs):
        """Handle activate control event."""
        del kwargs
        self.activate()

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

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

        '''
        self._coil_activate()
        self.schedule_deactivation()

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

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

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

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

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

        '''
        self._coil_deactivate()

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

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

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

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

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

    def _feeder_eject_count_decrease(self, target, **kwargs):
        del target
        del kwargs
        self.debug_log("Source reported success")
        if self.config['cool_down_time']:
            self.delay.add(self.config['cool_down_time'],
                           self._reduce_eject_count)
        else:
            self._reduce_eject_count()

    def _reduce_eject_count(self):
        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 self.eject_attempt_queue:
                # And perform those ejects
                if self.config[
                        'allow_multiple_concurrent_ejects_to_same_side']:
                    while self.eject_attempt_queue:
                        self.diverting_ejects_count += 1
                        queue = self.eject_attempt_queue.pop()
                        queue.clear()
                else:
                    if self.eject_attempt_queue:
                        self.diverting_ejects_count += 1
                        queue = self.eject_attempt_queue.pop()
                        queue.clear()
            elif self.active and not self.config['activation_time']:
                # if diverter is active and no more ejects are ongoing
                self.deactivate()

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

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

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

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

        desired_state = self._get_desired_state(target)

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

        if self.diverting_ejects_count > 0:
            if self.config[
                    'allow_multiple_concurrent_ejects_to_same_side'] and self.eject_state != desired_state:
                self.debug_log(
                    "Feeder devices tries to eject to a target which "
                    "would require a state change. Postponing that "
                    "because we have an eject to the other side")
                queue.wait()
                self.eject_attempt_queue.append(queue)
                return
            if not self.config['allow_multiple_concurrent_ejects_to_same_side']:
                self.debug_log(
                    "More than one eject and allow_multiple_concurrent_ejects_to_same_side is false"
                )
                queue.wait()
                self.eject_attempt_queue.append(queue)
                return

        self.diverting_ejects_count += 1
        self.eject_state = desired_state

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

        desired_state = self._get_desired_state(target)

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

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

    def _ball_search(self, phase, iteration):
        del phase
        del iteration
        self._coil_activate()
        self.machine.delay.add(self.config['ball_search_hold_time'],
                               self._coil_deactivate,
                               'diverter_{}_ball_search'.format(self.name))
        return True

    def _ball_search_restore(self):
        """Restore state after ball search ended."""
        if self.active:
            self._coil_activate()
        else:
            self._coil_deactivate()
Beispiel #15
0
class SnuxHardwarePlatform(DriverPlatform):
    """Overlay platform for the snux hardware board."""
    def __init__(self, machine: MachineController) -> None:
        """Initalize the board."""
        super().__init__(machine)

        self.log = logging.getLogger('Platform.Snux')
        self.delay = DelayManager(machine.delayRegistry)

        self.platform = None  # type: DriverPlatform

        self.system11_config = None  # type: Any
        self.snux_config = None  # type: Any

        self.a_side_queue = set(
        )  # type: Set[Tuple[DriverPlatformInterface, PulseSettings, HoldSettings]]
        self.c_side_queue = set(
        )  # type: Set[Tuple[DriverPlatformInterface, PulseSettings, HoldSettings]]

        self.a_drivers = set()  # type: Set[DriverPlatformInterface]
        self.c_drivers = set()  # type: Set[DriverPlatformInterface]

        self.a_side_done_time = 0
        self.c_side_done_time = 0
        self.drivers_holding_a_side = set(
        )  # type: Set[DriverPlatformInterface]
        self.drivers_holding_c_side = set(
        )  # type: Set[DriverPlatformInterface]
        self.a_side_enabled = True
        self.c_side_enabled = False

        self.ac_relay_in_transition = False

    def stop(self):
        """Stop the overlay. Nothing to do here because stop is also called on parent platform."""
        pass

    @property
    def a_side_busy(self):
        """True when A side cannot be switches off right away."""
        return self.drivers_holding_a_side or self.a_side_done_time > self.machine.clock.get_time(
        ) or self.a_side_queue

    @property
    def c_side_active(self):
        """True when C side cannot be switches off right away."""
        return self.drivers_holding_c_side or self.c_side_done_time > self.machine.clock.get_time(
        )

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

    @asyncio.coroutine
    def initialize(self):
        """Automatically called by the Platform class after all the core modules are loaded."""
        # load coil platform
        self.platform = self.machine.get_platform_sections(
            "platform", getattr(self.machine.config['snux'], 'platform', None))

        # we have to wait for coils to be initialized
        self.machine.events.add_handler("init_phase_1", self._initialize)

    def _initialize(self, **kwargs):
        del kwargs
        self._validate_config()

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

        # Hack to silence logging of P_ROC
        # TODO: clean this up
        self.snux_config[
            'diag_led_driver'].hw_driver.log.info = self._null_log_handler
        self.snux_config[
            'diag_led_driver'].hw_driver.log.debug = self._null_log_handler

        self.snux_config['diag_led_driver'].enable()

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

        self.system11_config['ac_relay_driver'].get_and_verify_hold_power(1.0)

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

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

        self.snux_config['flipper_enable_driver'].get_and_verify_hold_power(
            1.0)

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

    def _initialize_phase_2(self, **kwargs):
        del kwargs
        self.machine.clock.schedule_interval(self._flash_diag_led, 0.5)

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

        self.snux_config = self.machine.config_validator.validate_config(
            'snux', self.machine.config['snux'])

    def tick(self):
        """Snux main loop.

        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):
        """Flash diagnosis LED."""
        self.snux_config['diag_led_driver'].pulse(250)

    def configure_driver(self, config: DriverConfig, number: str,
                         platform_settings: dict):
        """Configure a driver on the snux board.

        Args:
            config: Driver config dict
        """
        orig_number = number

        if number and (number.endswith('a') or number.lower().endswith('c')):

            number = number[:-1]

            platform_driver = self.platform.configure_driver(
                config, number, platform_settings)

            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

        else:
            return self.platform.configure_driver(config, number,
                                                  platform_settings)

    def set_pulse_on_hit_and_release_rule(self, enable_switch, coil):
        """Configure a rule for a driver on the snux board.

        Will pass the call onto the parent platform if the driver is not on A/C relay.
        """
        if coil.hw_driver in self.a_drivers or coil.hw_driver in self.c_drivers:
            self.log.warning("Received a request to set a hardware rule for a"
                             "switched driver. Ignoring")
        else:
            self.platform.set_pulse_on_hit_and_release_rule(
                enable_switch, coil)

    def set_pulse_on_hit_and_enable_and_release_rule(self, enable_switch,
                                                     coil):
        """Configure a rule for a driver on the snux board.

        Will pass the call onto the parent platform if the driver is not on A/C relay.
        """
        if coil.hw_driver in self.a_drivers or coil.hw_driver in self.c_drivers:
            self.log.warning("Received a request to set a hardware rule for a"
                             "switched driver. Ignoring")
        else:
            self.platform.set_pulse_on_hit_and_enable_and_release_rule(
                enable_switch, coil)

    def set_pulse_on_hit_and_enable_and_release_and_disable_rule(
            self, enable_switch, disable_switch, coil):
        """Configure a rule for a driver on the snux board.

        Will pass the call onto the parent platform if the driver is not on A/C relay.
        """
        if coil.hw_driver in self.a_drivers or coil.hw_driver in self.c_drivers:
            self.log.warning("Received a request to set a hardware rule for a"
                             "switched driver. Ignoring")
        else:
            self.platform.set_pulse_on_hit_and_enable_and_release_and_disable_rule(
                enable_switch, disable_switch, coil)

    def set_pulse_on_hit_rule(self, enable_switch, coil):
        """Configure a rule on the snux board.

        Will pass the call onto the parent platform if the driver is not on A/C relay.
        """
        if coil.hw_driver in self.a_drivers or coil.hw_driver in self.c_drivers:
            self.log.warning("Received a request to set a hardware rule for a"
                             "switched driver. Ignoring")
        else:
            self.platform.set_pulse_on_hit_rule(enable_switch, coil)

    def clear_hw_rule(self, switch, coil):
        """Clear a rule for a driver on the snux board."""
        self.platform.clear_hw_rule(switch, coil)

    def driver_action(self, driver, pulse_settings: Optional[PulseSettings],
                      hold_settings: Optional[HoldSettings]):
        """Add 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.
            pulse_settings: Settings for the pulse or None
            hold_settings:Settings for hold or None

        This action will be serviced immediately if it can, or ASAP otherwise.
        """
        if driver in self.a_drivers:
            self.a_side_queue.add((driver, pulse_settings, hold_settings))
            self._service_a_side()
        elif driver in self.c_drivers:
            self.c_side_queue.add((driver, pulse_settings, hold_settings))
            if not self.ac_relay_in_transition and not self.a_side_busy:
                self._service_c_side()

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

    def _disable_ac_relay(self):
        self.system11_config['ac_relay_driver'].disable()
        self.ac_relay_in_transition = True
        self.a_side_enabled = False
        self.c_side_enabled = False
        self.delay.add(ms=self.system11_config['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._disable_ac_relay()
                self.delay.add(ms=self.system11_config['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, pulse_settings, hold_settings = self.a_side_queue.pop()

            if hold_settings is None and pulse_settings:
                driver.pulse(pulse_settings)
                self.a_side_done_time = max(
                    self.a_side_done_time,
                    self.machine.clock.get_time() +
                    (pulse_settings.duration / 1000.0))

            elif hold_settings and pulse_settings:
                driver.enable(pulse_settings, hold_settings)
                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, pulse_settings, hold_settings = self.c_side_queue.pop()

            if hold_settings is None and pulse_settings:
                driver.pulse(pulse_settings)
                self.c_side_done_time = max(
                    self.c_side_done_time,
                    self.machine.clock.get_time() +
                    (pulse_settings.duration / 1000.0))
            elif hold_settings and pulse_settings:
                driver.enable(pulse_settings, hold_settings)
                self.drivers_holding_c_side.add(driver)

            else:
                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.drivers_holding_c_side:
                driver.disable()
            self.drivers_holding_c_side = set()
            self.c_side_done_time = 0
            self.c_side_enabled = False

    def validate_coil_section(self, driver, config):
        """Validate coil config for platform."""
        return self.platform.validate_coil_section(driver, config)
Beispiel #16
0
class DropTarget(SystemWideDevice):
    """Represents a single drop target in a pinball machine.

    Args: Same as the `Target` parent class
    """

    config_section = 'drop_targets'
    collection = 'drop_targets'
    class_label = 'drop_target'

    def __init__(self, machine: "MachineController", name: str) -> None:
        """Initialise drop target."""
        self.reset_coil = None  # type: Driver
        self.knockdown_coil = None  # type: Driver
        self.banks = None  # type: Set[DropTargetBank]
        super().__init__(machine, name)

        self._in_ball_search = False
        self.complete = False
        self.delay = DelayManager(machine.delayRegistry)

        self._ignore_switch_hits = False

    def _initialize(self) -> None:
        self.reset_coil = self.config['reset_coil']
        self.knockdown_coil = self.config['knockdown_coil']
        self.banks = set()

        # can't read the switch until the switch controller is set up
        self.machine.events.add_handler('init_phase_4',
                                        self._update_state_from_switch,
                                        priority=2)
        self.machine.events.add_handler('init_phase_4',
                                        self._register_switch_handlers,
                                        priority=1)

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

    def _ignore_switch_hits_for(self, ms):
        """Ignore switch hits for ms."""
        self._ignore_switch_hits = True
        self.delay.reset(name="ignore_switch",
                         callback=self._restore_switch_hits,
                         ms=ms)

    def _restore_switch_hits(self):
        self._ignore_switch_hits = False
        self._update_state_from_switch(reconcile=True)

    def _ball_search_phase1(self):
        if not self.complete and self.reset_coil:
            self.reset_coil.pulse()
            return True
        # if down. knock down again
        elif self.complete and self.knockdown_coil:
            self.knockdown_coil.pulse()
            return True

    def _ball_search_phase2(self):
        if self.reset_coil and self.knockdown_coil:
            if self.complete:
                self._in_ball_search = True
                self.reset_coil.pulse()
                self.delay.add(100, self._ball_search_knockdown)
                return True
            else:
                self._in_ball_search = True
                self.knockdown_coil.pulse()
                self.delay.add(100, self._ball_search_reset)
                return True
        else:
            # fall back to phase1
            return self._ball_search_phase1()

    def _ball_search_phase3(self):
        if self.complete:
            if self.reset_coil:
                self.reset_coil.pulse()
                if self.knockdown_coil:
                    self._in_ball_search = True
                    self.delay.add(100, self._ball_search_knockdown)
                return True
            else:
                return self._ball_search_phase1()
        else:
            if self.knockdown_coil:
                self.knockdown_coil.pulse()
                if self.reset_coil:
                    self._in_ball_search = True
                    self.delay.add(100, self._ball_search_reset)
                return True
            else:
                return self._ball_search_phase1()

    def _ball_search_iteration_finish(self):
        self._in_ball_search = False

    def _ball_search_knockdown(self):
        self.knockdown_coil.pulse()
        self.delay.add(100, self._ball_search_iteration_finish)

    def _ball_search_reset(self):
        self.reset_coil.pulse()
        self.delay.add(100, self._ball_search_iteration_finish)

    def _ball_search(self, phase, iteration):
        del iteration
        if phase == 1:
            # phase 1: do not change state.
            # if up. reset again
            return self._ball_search_phase1()
        elif phase == 2:
            # phase 2: if we can reset and knockdown the target we will do that
            return self._ball_search_phase2()
        else:
            # phase3: reset no matter what
            return self._ball_search_phase3()

    def _register_switch_handlers(self, **kwargs):
        del kwargs
        # register for notification of switch state
        # this is in addition to the parent since drop targets track
        # self.complete in separately

        self.machine.switch_controller.add_switch_handler(
            self.config['switch'].name, self._update_state_from_switch, 0)
        self.machine.switch_controller.add_switch_handler(
            self.config['switch'].name, self._update_state_from_switch, 1)

    @event_handler(6)
    def enable_keep_up(self, **kwargs):
        """Keep the target up by enabling the coil."""
        del kwargs
        if self.reset_coil:
            self.reset_coil.enable()

    @event_handler(5)
    def disable_keep_up(self, **kwargs):
        """No longer keep up the target up."""
        del kwargs
        if self.reset_coil:
            self.reset_coil.disable()

    @event_handler(7)
    def knockdown(self, **kwargs):
        """Pulse the knockdown coil to knock down this drop target."""
        del kwargs
        if self.knockdown_coil and not self.machine.switch_controller.is_active(
                self.config['switch'].name):
            self._ignore_switch_hits_for(ms=self.config['ignore_switch_ms'])
            self.knockdown_coil.pulse(
                self.config['knockdown_coil_max_wait_ms'])

    def _update_state_from_switch(self, reconcile=False, **kwargs):
        del kwargs

        is_complete = self.machine.switch_controller.is_active(
            self.config['switch'].name)

        if (self._in_ball_search or self._ignore_switch_hits
                or is_complete == self.complete):
            return

        if not reconcile:
            self.config['playfield'].mark_playfield_active_from_device_action()

        if is_complete != self.complete:

            if is_complete:
                self._down()
            else:
                self._up()

            self._update_banks()

    def _down(self):
        self.complete = True
        self.machine.events.post('drop_target_' + self.name + '_down',
                                 device=self)
        '''event: drop_target_(name)_down
        desc: The drop target with the (name) has just changed to the "down"
        state.'''

    def _up(self):
        self.complete = False
        self.machine.events.post('drop_target_' + self.name + '_up',
                                 device=self)
        '''event: drop_target_(name)_up
        desc: The drop target (name) has just changed to the "up" state.'''

    def _update_banks(self):
        for bank in self.banks:
            bank.member_target_change()

    def add_to_bank(self, bank):
        """Add this drop target to a drop target bank.

         This allows the bank to update its status based on state changes to this drop target.

        Args:
            bank: DropTargetBank object to add this drop target to.
        """
        self.banks.add(bank)

    def remove_from_bank(self, bank):
        """Remove the DropTarget from a bank.

        Args:
            bank: DropTargetBank object to remove
        """
        self.banks.remove(bank)

    @event_handler(1)
    def reset(self, **kwargs):
        """Reset this drop target.

        If this drop target is configured with a reset coil, then this method
        will pulse that coil. If not, then it checks to see if this drop target
        is part of a drop target bank, and if so, it calls the reset() method of
        the drop target bank.

        This method does not reset the target profile, however, the switch event
        handler should reset the target profile on its own when the drop target
        physically moves back to the up position.
        """
        del kwargs

        if self.reset_coil and self.machine.switch_controller.is_active(
                self.config['switch'].name):
            self._ignore_switch_hits_for(ms=self.config['ignore_switch_ms'])
            self.reset_coil.pulse(self.config['reset_coil_max_wait_ms'])
Beispiel #17
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.
    """

    config_section = 'counters'
    collection = 'counters'
    class_label = 'counter'

    def __init__(self, machine: MachineController, name: str) -> None:
        """Initialise counter."""
        super().__init__(machine, name)
        self.debug_log("Creating Counter LogicBlock")

        self.delay = DelayManager(self.machine.delayRegistry)

        self.ignore_hits = False
        self.hit_value = -1

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

    def get_start_value(self) -> int:
        """Return start count."""
        return self.config['starting_count'].evaluate([])

    def validate_and_parse_config(self,
                                  config: dict,
                                  is_mode_config: bool,
                                  debug_prefix: str = None) -> dict:
        """Validate logic block config."""
        if 'events_when_hit' not in config:
            # for compatibility post the same default as previously for
            # counters. This one is deprecated.
            config['events_when_hit'] = ['counter_' + self.name + '_hit']

            # this is the one moving forward
            config['events_when_hit'].append('logicblock_' + self.name +
                                             '_hit')

        return super().validate_and_parse_config(config, is_mode_config,
                                                 debug_prefix)

    def count(self, **kwargs):
        """Increase the hit progress towards completion.

        This method is also automatically called when one of the
        ``count_events`` is posted.

        """
        del kwargs
        if not self.enabled:
            return

        count_complete_value = self.config['count_complete_value'].evaluate([]) if self.config['count_complete_value']\
            is not None else None

        if not self.ignore_hits:
            self._state.value += self.hit_value
            self.debug_log("Processing Count change. Total: %s",
                           self._state.value)

            args = {"count": self._state.value}
            if count_complete_value is not None:
                args['remaining'] = count_complete_value - self._state.value

            self._post_hit_events(**args)

            if count_complete_value is not None:

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

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

            if self.config['multiple_hit_window']:
                self.debug_log("Beginning Ignore Hits")
                self.ignore_hits = True
                self.delay.add(name='ignore_hits_within_window',
                               ms=self.config['multiple_hit_window'],
                               callback=self.stop_ignoring_hits)

    def stop_ignoring_hits(self, **kwargs):
        """Cause 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.
        """
        del kwargs
        self.debug_log("Ending Ignore hits")
        self.ignore_hits = False
Beispiel #18
0
class Driver(SystemWideDevice):
    """Generic class that holds driver objects.

    A 'driver' is any device controlled from a driver board which is typically
    the high-voltage stuff like coils and flashers.

    This class exposes the methods you should use on these driver types of
    devices. Each platform module (i.e. P-ROC, FAST, etc.) subclasses this
    class to actually communicate with the physical hardware and perform the
    actions.

    Args: Same as the Device parent class
    """

    config_section = 'coils'
    collection = 'coils'
    class_label = 'coil'

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

    def __init__(self, machine: MachineController, name: str) -> None:
        """Initialise driver."""
        self.hw_driver = None  # type: DriverPlatformInterface
        super().__init__(machine, name)
        self.delay = DelayManager(self.machine)
        self.platform = None  # type: DriverPlatform

    @classmethod
    def device_class_init(cls, machine: MachineController):
        """Register handler for duplicate coil number checks."""
        machine.events.add_handler("init_phase_4",
                                   cls._check_duplicate_coil_numbers,
                                   machine=machine)

    @staticmethod
    def _check_duplicate_coil_numbers(machine, **kwargs):
        del kwargs
        check_set = set()
        for coil in machine.coils:
            if not hasattr(coil, "hw_driver"):
                # skip dual wound and other special devices
                continue
            key = (coil.platform, coil.hw_driver.number)
            if key in check_set:
                raise AssertionError(
                    "Duplicate coil number {} for coil {}".format(
                        coil.hw_driver.number, coil))

            check_set.add(key)

    def validate_and_parse_config(self,
                                  config: dict,
                                  is_mode_config: bool,
                                  debug_prefix: str = None) -> dict:
        """Return the parsed and validated config.

        Args:
            config: Config of device
            is_mode_config: Whether this device is loaded in a mode or system-wide

        Returns: Validated config
        """
        config = super().validate_and_parse_config(config, is_mode_config,
                                                   debug_prefix)
        platform = self.machine.get_platform_sections(
            'coils', getattr(config, "platform", None))
        config['platform_settings'] = platform.validate_coil_section(
            self, config.get('platform_settings', None))
        self._configure_device_logging(config)
        return config

    @asyncio.coroutine
    def _initialize(self):
        yield from super()._initialize()
        self.platform = self.machine.get_platform_sections(
            'coils', self.config['platform'])

        config = DriverConfig(
            default_pulse_ms=self.get_and_verify_pulse_ms(None),
            default_pulse_power=self.get_and_verify_pulse_power(None),
            default_hold_power=self.get_and_verify_hold_power(None),
            default_recycle=self.config['default_recycle'],
            max_pulse_ms=self.config['max_pulse_ms'],
            max_pulse_power=self.config['max_pulse_power'],
            max_hold_power=self.config['max_hold_power'])
        platform_settings = dict(
            self.config['platform_settings']
        ) if self.config['platform_settings'] else dict()

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

    def get_and_verify_pulse_power(self,
                                   pulse_power: Optional[float]) -> float:
        """Return the pulse power to use.

        If pulse_power is None it will use the default_pulse_power. Additionally it will verify the limits.
        """
        if pulse_power is None:
            pulse_power = self.config['default_pulse_power'] if self.config[
                'default_pulse_power'] is not None else 1.0

        if pulse_power and 0 > pulse_power > 1:
            raise AssertionError(
                "Pulse power has to be between 0 and 1 but is {}".format(
                    pulse_power))

        max_pulse_power = 0
        if self.config['max_pulse_power']:
            max_pulse_power = self.config['max_pulse_power']
        elif self.config['default_pulse_power']:
            max_pulse_power = self.config['default_pulse_power']

        if pulse_power > max_pulse_power:
            raise DriverLimitsError(
                "Driver may {} not be pulsed with pulse_power {} because max_pulse_power is {}"
                .format(self.name, pulse_power, max_pulse_power))
        return pulse_power

    def get_and_verify_hold_power(self, hold_power: Optional[float]) -> float:
        """Return the hold power to use.

        If hold_power is None it will use the default_hold_power. Additionally it will verify the limits.
        """
        if hold_power is None and self.config['default_hold_power']:
            hold_power = self.config['default_hold_power']

        if hold_power is None and self.config['max_hold_power']:
            hold_power = self.config['max_hold_power']

        if hold_power is None and self.config['allow_enable']:
            hold_power = 1.0

        if hold_power is None:
            hold_power = 0.0

        if hold_power and 0 > hold_power > 1:
            raise AssertionError(
                "Hold_power has to be between 0 and 1 but is {}".format(
                    hold_power))

        max_hold_power = 0  # type: float
        if self.config['max_hold_power']:
            max_hold_power = self.config['max_hold_power']
        elif self.config['allow_enable']:
            max_hold_power = 1.0
        elif self.config['default_hold_power']:
            max_hold_power = self.config['default_hold_power']

        if hold_power > max_hold_power:
            raise DriverLimitsError(
                "Driver {} may not be enabled with hold_power {} because max_hold_power is {}"
                .format(self.name, hold_power, max_hold_power))
        return hold_power

    def get_and_verify_pulse_ms(self, pulse_ms: Optional[int]) -> int:
        """Return and verify pulse_ms to use.

        If pulse_ms is None return the default.
        """
        if pulse_ms is None:
            if self.config['default_pulse_ms'] is not None:
                pulse_ms = self.config['default_pulse_ms']
            else:
                pulse_ms = self.machine.config['mpf']['default_pulse_ms']

        if not isinstance(pulse_ms, int):
            raise AssertionError("Wrong type {}".format(pulse_ms))

        if 0 > pulse_ms > self.platform.features['max_pulse']:
            raise AssertionError("Pulse_ms {} is not valid.".format(pulse_ms))

        if self.config[
                'max_pulse_ms'] and pulse_ms > self.config['max_pulse_ms']:
            raise DriverLimitsError(
                "Driver {} may not be pulsed with pulse_ms {} because max_pulse_ms is {}"
                .format(self.name, pulse_ms, self.config['max_pulse_ms']))

        return pulse_ms

    @event_handler(2)
    def event_enable(self,
                     pulse_ms: int = None,
                     pulse_power: float = None,
                     hold_power: float = None,
                     **kwargs):
        """Event handler for control enable."""
        del kwargs
        self.enable(pulse_ms, pulse_power, hold_power)

    def enable(self,
               pulse_ms: int = None,
               pulse_power: float = None,
               hold_power: float = None):
        """Enable a driver by holding it 'on'.

        Args:
            pulse_ms: The number of milliseconds the driver should be
                enabled for. If no value is provided, the driver will be
                enabled for the value specified in the config dictionary.
            pulse_power: The pulse power. A float between 0.0 and 1.0.
            hold_power: The pulse power. A float between 0.0 and 1.0.

        If this driver is configured with a holdpatter, then this method will use
        that holdpatter to pwm pulse the driver.

        If not, then this method will just enable the driver. As a safety
        precaution, if you want to enable() this driver without pwm, then you
        have to add the following option to this driver in your machine
        configuration files:

        allow_enable: True
        """
        pulse_ms = self.get_and_verify_pulse_ms(pulse_ms)

        pulse_power = self.get_and_verify_pulse_power(pulse_power)
        hold_power = self.get_and_verify_hold_power(hold_power)

        if hold_power == 0.0:
            raise DriverLimitsError("Cannot enable driver with hold_power 0.0")

        self.info_log(
            "Enabling Driver with power %s (pulse_ms %sms and pulse_power %s)",
            hold_power, pulse_ms, pulse_power)
        self.hw_driver.enable(
            PulseSettings(power=pulse_power, duration=pulse_ms),
            HoldSettings(power=hold_power))
        # inform bcp clients
        self.machine.bcp.interface.send_driver_event(
            action="enable",
            name=self.name,
            number=self.config['number'],
            pulse_ms=pulse_ms,
            pulse_power=pulse_power,
            hold_power=hold_power)

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

    def disable(self):
        """Disable this driver."""
        self.info_log("Disabling Driver")
        self.machine.delay.remove(name='{}_timed_enable'.format(self.name))
        self.hw_driver.disable()
        # inform bcp clients
        self.machine.bcp.interface.send_driver_event(
            action="disable", name=self.name, number=self.config['number'])

    def _get_wait_ms(self, pulse_ms: int, max_wait_ms: Optional[int]) -> int:
        """Determine if this pulse should be delayed."""
        if max_wait_ms is None:
            self.config['psu'].notify_about_instant_pulse(pulse_ms=pulse_ms)
            return 0
        else:
            return self.config['psu'].get_wait_time_for_pulse(
                pulse_ms=pulse_ms, max_wait_ms=max_wait_ms)

    def _pulse_now(self, pulse_ms: int, pulse_power: float) -> None:
        """Pulse this driver now."""
        if 0 < pulse_ms <= self.platform.features['max_pulse']:
            self.info_log("Pulsing Driver for %sms (%s pulse_power)", pulse_ms,
                          pulse_power)
            self.hw_driver.pulse(
                PulseSettings(power=pulse_power, duration=pulse_ms))
        else:
            self.info_log("Enabling Driver for %sms (%s pulse_power)",
                          pulse_ms, pulse_power)
            self.delay.reset(name='timed_disable',
                             ms=pulse_ms,
                             callback=self.disable)
            self.hw_driver.enable(PulseSettings(power=pulse_power, duration=0),
                                  HoldSettings(power=pulse_power))
        # inform bcp clients
        self.machine.bcp.interface.send_driver_event(
            action="pulse",
            name=self.name,
            number=self.config['number'],
            pulse_ms=pulse_ms,
            pulse_power=pulse_power)

    @event_handler(3)
    def event_pulse(self,
                    pulse_ms: int = None,
                    pulse_power: float = None,
                    max_wait_ms: int = None,
                    **kwargs) -> None:
        """Event handler for pulse control events."""
        del kwargs
        self.pulse(pulse_ms, pulse_power, max_wait_ms)

    def pulse(self,
              pulse_ms: int = None,
              pulse_power: float = None,
              max_wait_ms: int = None) -> int:
        """Pulse this driver.

        Args:
            pulse_ms: The number of milliseconds the driver should be
                enabled for. If no value is provided, the driver will be
                enabled for the value specified in the config dictionary.
            pulse_power: The pulse power. A float between 0.0 and 1.0.
        """
        pulse_ms = self.get_and_verify_pulse_ms(pulse_ms)
        pulse_power = self.get_and_verify_pulse_power(pulse_power)
        wait_ms = self._get_wait_ms(pulse_ms, max_wait_ms)

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

        return wait_ms
Beispiel #19
0
class Servo(SystemWideDevice):
    """Represents a servo in a pinball machine.

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

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

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

    @asyncio.coroutine
    def _initialize(self):
        yield from super()._initialize()
        self.platform = self.machine.get_platform_sections(
            'servo_controllers', self.config['platform'])

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        # move to last position set
        self._go_to_position(self._position)
Beispiel #20
0
class AutofireCoil(SystemWideDevice):
    """Autofire coils which fire based on switch hits with a hardware rule.

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

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

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

    """

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

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

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

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

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

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

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

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

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

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

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

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

        self.debug_log("Enabling")

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

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

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

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

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

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

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

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

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

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

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

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

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

    def _ball_search_ignore_done(self):
        """We no longer expect any fake hits."""
        self._ball_search_in_progress = False
Beispiel #21
0
class ScoreReel(SystemWideDevice):
    """Represents an individual electro-mechanical score reel in a pinball machine.

    Multiples reels of this class can be grouped together into ScoreReelGroups
    which collectively make up a display like "Player 1 Score" or "Player 2
    card value", etc.

    This device class is used for all types of mechanical number reels in a
    machine, including reels that have more than ten numbers and that can move
    in multiple directions (such as the credit reel).
    """

    config_section = 'score_reels'
    collection = 'score_reels'
    class_label = 'score_reel'

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

        self.rollover_reel_advanced = False
        # True when a rollover pulse has been ordered

        self.value_switches = []
        # This is a list with each element corresponding to a value on the
        # reel. An entry of None means there's no value switch there. An entry
        # of a reference to a switch object (todo or switch name?) means there
        # is a switch there.
        self.num_values = 0
        # The number of values on this wheel. This starts with zero, so a
        # wheel with 10 values will have this value set to 9. (This actually
        # makes sense since most (all?) score reels also have a zero value.)

        self.physical_value = -999
        # The physical confirmed value of this reel. This will always be the
        # value of whichever switch is active or -999. This differs from
        # `self.assumed_value` in that assumed value will make assumptions about
        # where the reel is as it pulses through values with no swithces,
        # whereas this physical value will always be -999 if there is no switch
        # telling it otherwise.

        # Note this value will be initialized via self.check_hw_switches()
        # below.

        self.hw_sync = False
        # Specifies whether this reel has verified it's positions via the
        # switches since it was last advanced."""

        self.ready = True
        # Whether this reel is ready to advance. Typically used to make sure
        # it's not trying to re-fire a stuck position.

        self.assumed_value = -999
        # The assumed value the machine thinks this reel is showing. A value
        # of -999 indicates that the value is unknown.

        self.next_pulse_time = 0
        # The time when this reel next wants to be pulsed. The reel will set
        # this on its own (based on its own attribute of how fast pulses can
        # happen). If the ScoreReelController is ready to pulse this reel and
        # the value is in the past, it will do a pulse. A value of 0 means this
        # reel does not currently need to be pulsed.

        self.rollover_reel = None
        # A reference to the ScoreReel object of the next higher reel in the
        # group. This is used so the reel can notify its neighbor that it needs
        # to advance too when this reel rolls over.

        self._destination_index = 0
        # Holds the index of the destination the reel is trying to advance to.

        # todo add some kind of status for broken?

    def _initialize(self):
        self.debug_log("Configuring score reel with: %s", self.config)

        self.assumed_value = self.check_hw_switches()

        # figure out how many values we have
        # Add 1 so range is inclusive of the lower limit
        self.num_values = self.config['limit_hi'] - self.config['limit_lo'] + 1

        self.debug_log("Total reel values: %s", self.num_values)

        for value in range(self.num_values):
            self.value_switches.append(self.config.get('switch_' + str(value)))

    def set_rollover_reel(self, reel):
        """Set this reels' rollover_reel to the object of the next higher reel."""
        self.debug_log("Setting rollover reel: %s", reel.name)
        self.rollover_reel = reel

    def advance(self):
        """Perform the coil firing to advance this reel one position (up or down).

        This method also schedules delays to post the following events:

        `reel_<name>_ready`: When the config['repeat_pulse_time'] time is up
        `reel_<name>_hw_value`: When the config['hw_confirm_time'] time is up

        Args:

        Returns: If this method is unable to advance the reel (either because
            it's not ready, because it's at its maximum value and does not have
            rollover capabilities, or because you're trying to advance it in a
            direction but it doesn't have a coil for that direction), it will
            return `False`. If it's able to pulse the advance coil, it returns
            `True`.
        """
        self.debug_log("Received command advance Reel")

        self.set_destination_value()
        # above line also sets self._destination_index

        if self.next_pulse_time > self.machine.clock.get_time():
            # This reel is not ready to pulse again
            # Note we don't allow this to be overridden. Figure the
            # recycle time is there for a reason and we don't want to
            # potentially break an old delicate mechanism
            self.debug_log("Received advance request but this reel is not "
                           "ready")
            return False  # since we didn't advance...in case anyone cares?

        # Ensure we're not at the limit of a reel that can't roll over
        if not ((self.physical_value == self.config['limit_hi'])
                and not self.config['rollover']):
            self.debug_log("Ok to advance")

            # Since we're firing, assume we're going to make it
            self.assumed_value = self._destination_index
            self.debug_log("+++Setting assumed value to: %s",
                           self.assumed_value)

            # Reset our statuses (stati?) :)
            self.ready = False
            self.hw_sync = False

            # fire the coil
            self.config['coil_inc'].pulse()

            # set delay to notify when this reel can be fired again
            self.delay.add(name='ready_to_fire',
                           ms=self.config['repeat_pulse_time'],
                           callback=self._ready_to_fire)

            self.next_pulse_time = (
                self.machine.clock.get_time() +
                (self.config['repeat_pulse_time'] / 1000.0))
            self.debug_log("@@@ New Next pulse ready time: %s",
                           self.next_pulse_time)

            # set delay to check the hw switches
            self.delay.add(name='hw_switch_check',
                           ms=self.config['hw_confirm_time'],
                           callback=self.check_hw_switches)

            return True

        else:
            self.log.warning("Received command to increment reel, but "
                             "we're at the max limit and this reel "
                             "cannot roll over")
            return False

    def _ready_to_fire(self):
        # automatically called (via a delay) after the reel fires to post an
        # event that the reel is ready to fire again
        self.ready = True
        self.machine.events.post('reel_' + self.name + "_ready")
        '''event: reel_(name)_ready
        desc: The score real (name) is ready to be pulsed again.'''

    def check_hw_switches(self, no_event=False):
        """Check all the value switches for this score reel.

        This check only happens if `self.ready` is `True`. If the reel is not
        ready, it means another advance request has come in after the initial
        one. In that case then the subsequent advance will call this method
        again when after that advance is done.

        If this method finds an active switch, it sets `self.physical_value` to
        that. Otherwise it sets it to -999. It will also update
        `self.assumed_value` if it finds an active switch. Otherwise it leaves
        that value unchanged.

        This method is automatically called (via a delay) after the reel
        advances. The delay is based on the config value
        `self.config['hw_confirm_time']`.

        TODO: What happens if there are multiple active switches? Currently it
        will return the highest one. Is that ok?

        Args:
            no_event: A boolean switch that allows you to suppress the event
                posting from this call if you just want to update the values.

        Returns: The hardware value of the switch, either the position or -999.
            If the reel is not ready, it returns `False`.
        """
        # check to make sure the 'hw_confirm_time' time has passed. If not then
        # we cannot trust any value we read from the switches
        if (self.config['coil_inc'].time_last_changed +
            (self.config['hw_confirm_time'] / 1000.0) <=
                self.machine.clock.get_time()):
            self.debug_log("Checking hw switches to determine reel value")
            value = -999
            for i in range(len(self.value_switches)):
                if self.value_switches[i]:  # not all values have a switch
                    if self.machine.switch_controller.is_active(
                            self.value_switches[i].name):
                        value = i

            self.debug_log("+++Setting hw value to: %s", value)
            self.physical_value = value
            self.hw_sync = True
            # only change this if we know where we are or can confirm that
            # we're not in the right position
            if value != -999:
                if value != self.assumed_value:
                    self.log.info(
                        "Setting value to %s because that switch is active.",
                        value)
                    self.assumed_value = value

            # if value is -999, but we have a switch for the assumed value,
            # then we're in the wrong position because our hw_value should be
            # at the assumed value
            elif self.assumed_value != -999 and self.value_switches[
                    self.assumed_value]:
                self.log.warning(
                    "Assumed value %s but the switch for that value is not active",
                    self.assumed_value)
                self.assumed_value = -999

            if not no_event:
                self.machine.events.post('reel_' + self.name + "_hw_value",
                                         value=value)
                '''event: reel_(name)_hw_value
                desc: The score reel (name) has checked its hardware switches.
                args:
                value: The physical confirmed value of the real. (Will be -999
                if the reel is not at a position with a switch.'''
            return value

        else:
            return False

    def set_destination_value(self):
        """Return the integer value of the destination this reel is moving to.

        Args:

        Returns: The value of the destination. If the current
            `self.assumed_value` is -999, this method will always return -999
            since it doesn't know where the reel is and therefore doesn't know
            what the destination value would be.
        """
        # We can only know if we have a destination if we know where we are
        self.debug_log("@@@ set_destination_value")
        self.debug_log("@@@ old destination_index: %s",
                       self._destination_index)
        if self.assumed_value != -999:
            self._destination_index = self.assumed_value + 1
            if self._destination_index > (self.num_values - 1):
                self._destination_index = 0
            if self._destination_index == 1:
                self.rollover_reel_advanced = False
            self.debug_log("@@@ new destination_index: %s",
                           self._destination_index)
            return self._destination_index
        else:
            self.debug_log("@@@ new destination_index: -999")
            self._destination_index = -999
            return -999
Beispiel #22
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):
        """Initialise ball controller."""
        self.machine = machine
        self.log = logging.getLogger("BallController")
        self.log.debug("Loading the BallController")
        self.delay = DelayManager(self.machine.delayRegistry)

        self.num_balls_known = -999

        # 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._init2)

    def _init2(self, **kwargs):
        del kwargs
        # register a handler for all switches
        for device in self.machine.ball_devices:
            if 'ball_switches' not in device.config:
                continue
            for switch in device.config['ball_switches']:
                self.machine.switch_controller.add_switch_handler(
                    switch.name,
                    self._update_num_balls_known,
                    ms=device.config['entrance_count_delay'],
                    state=1)
                self.machine.switch_controller.add_switch_handler(
                    switch.name,
                    self._update_num_balls_known,
                    ms=device.config['exit_count_delay'],
                    state=0)
                self.machine.switch_controller.add_switch_handler(
                    switch.name,
                    self._correct_playfield_count,
                    ms=device.config['entrance_count_delay'],
                    state=1)
                self.machine.switch_controller.add_switch_handler(
                    switch.name,
                    self._correct_playfield_count,
                    ms=device.config['exit_count_delay'],
                    state=0)

        for playfield in self.machine.playfields:
            self.machine.events.add_handler(
                '{}_ball_count_change'.format(playfield.name),
                self._correct_playfield_count)

        # run initial count
        self._update_num_balls_known()

    def _get_loose_balls(self):
        return self.num_balls_known - self._count_stable_balls()

    def _count_stable_balls(self):
        self.log.debug("Counting Balls")
        balls = 0

        for device in self.machine.ball_devices:
            if device.is_playfield():
                continue

            if not device.is_ball_count_stable():
                raise ValueError("devices not stable")

            # generally we do not count ball devices without switches
            if 'ball_switches' not in device.config:
                continue
            # special handling for troughs (needed for gottlieb)
            elif not device.config['ball_switches'] and 'trough' in device.tags:
                balls += device.balls
            else:
                for switch in device.config['ball_switches']:
                    if self.machine.switch_controller.is_active(
                            switch.name,
                            ms=device.config['entrance_count_delay']):
                        balls += 1
                    elif self.machine.switch_controller.is_inactive(
                            switch.name, ms=device.config['exit_count_delay']):
                        continue
                    else:
                        raise ValueError("switches not stable")

        return balls

    def _correct_playfield_count(self, **kwargs):
        del kwargs
        self.delay.reset(ms=1,
                         callback=self._correct_playfield_count2,
                         name="correct_playfield")

    def _correct_playfield_count2(self):
        try:
            loose_balls = self._get_loose_balls()
        except ValueError:
            self.delay.reset(ms=10000,
                             callback=self._correct_playfield_count2,
                             name="correct_playfield")
            return

        balls_on_pfs = 0

        for playfield in self.machine.playfields:
            balls_on_pfs += playfield.balls

        jump_sources = []
        jump_targets = []

        # fix too much balls and prefer playfields where balls and available_balls have the same value
        if balls_on_pfs > loose_balls:
            balls_on_pfs -= self._fix_jumped_balls(balls_on_pfs - loose_balls,
                                                   jump_sources)

        # fix too much balls and take the remaining playfields
        if balls_on_pfs > loose_balls:
            balls_on_pfs -= self._remove_balls_from_playfield_randomly(
                balls_on_pfs - loose_balls, jump_sources)

        if balls_on_pfs > loose_balls:
            self.log.warning(
                "Failed to remove enough balls from playfields. This is a bug!"
            )

        for playfield in self.machine.playfields:
            if playfield.balls != playfield.available_balls:
                self.log.warning(
                    "Correcting available_balls %s to %s on "
                    "playfield %s", playfield.available_balls, playfield.balls,
                    playfield.name)
                if playfield.balls > playfield.available_balls:
                    jump_targets.append(playfield)
                playfield.available_balls = playfield.balls

        for _ in range(min(len(jump_sources), len(jump_targets))):
            source = jump_sources.pop()
            target = jump_targets.pop()
            self.log.warning("Suspecting that ball jumped from %s to %s",
                             str(source), str(target))
            self.machine.events.post("playfield_jump",
                                     source=source,
                                     target=target)

    def _fix_jumped_balls(self, balls_to_remove, jump_sources):
        balls_removed = 0
        for dummy_i in range(balls_to_remove):
            for playfield in self.machine.playfields:
                self.log.warning(
                    "Correcting balls on pf from %s to %s on "
                    "playfield %s (preferred)", playfield.balls,
                    playfield.balls - 1, playfield.name)
                if playfield.available_balls == playfield.balls and playfield.balls > 0:
                    jump_sources.append(playfield)
                    if playfield.unexpected_balls > 0:
                        playfield.unexpected_balls -= 1
                    playfield.balls -= 1
                    playfield.available_balls -= 1
                    balls_removed += 1
                    break
        return balls_removed

    def _remove_balls_from_playfield_randomly(self, balls_to_remove,
                                              jump_sources):
        balls_removed = 0
        for dummy_i in range(balls_to_remove):
            for playfield in self.machine.playfields:
                self.log.warning(
                    "Correcting balls on pf from %s to %s on "
                    "playfield %s", playfield.balls, playfield.balls - 1,
                    playfield.name)
                if playfield.balls > 0:
                    jump_sources.append(playfield)
                    if playfield.unexpected_balls > 0:
                        playfield.unexpected_balls -= 1
                    playfield.balls -= 1
                    playfield.available_balls -= 1
                    balls_removed += 1
                    break
        return balls_removed

    def trigger_ball_count(self):
        """Count the balls now if possible."""
        self._update_num_balls_known()
        self._correct_playfield_count()

    def _update_num_balls_known(self):
        try:
            balls = self._count_balls()
        except ValueError:
            self.delay.reset(ms=100,
                             callback=self._update_num_balls_known,
                             name="update_num_balls_known")
            return

        if self.num_balls_known < 0:
            self.num_balls_known = 0
        if balls > self.num_balls_known:
            self.log.debug("Found new balls. Setting known balls to %s", balls)
            self.delay.add(1,
                           self._handle_new_balls,
                           new_balls=balls - self.num_balls_known)
            self.num_balls_known = balls

    def _handle_new_balls(self, new_balls):
        for dummy_i in range(new_balls):
            for playfield in self.machine.playfields:
                if playfield.unexpected_balls > 0:
                    playfield.unexpected_balls -= 1
                    playfield.available_balls += 1
                    break

    def _count_balls(self):
        self.log.debug("Counting Balls")
        balls = 0

        for device in self.machine.ball_devices:
            # generally we do not count ball devices without switches
            if 'ball_switches' not in device.config:
                continue
            # special handling for troughs (needed for gottlieb)
            elif not device.config['ball_switches'] and 'trough' in device.tags:
                balls += device.balls
            else:
                for switch in device.config['ball_switches']:
                    if self.machine.switch_controller.is_active(
                            switch.name,
                            ms=device.config['entrance_count_delay']):
                        balls += 1
                    elif self.machine.switch_controller.is_inactive(
                            switch.name, ms=device.config['exit_count_delay']):
                        continue
                    else:
                        raise ValueError("switches not stable")

        return balls

    def _initialize(self, **kwargs):

        # If there are no ball devices, then the ball controller has no work to
        # do and will create errors, so we just abort.
        del kwargs
        if not hasattr(self.machine, 'ball_devices'):
            return

        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)

    def dump_ball_counts(self):
        """Dump ball count of all devices."""
        for device in self.machine.ball_devices:
            self.log.info("%s contains %s balls. Tags %s", device.name,
                          device.balls, device.tags)

    def request_to_start_game(self, **kwargs):
        """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.
        """
        del kwargs
        try:
            balls = self._count_balls()
        except ValueError:
            balls = -1
        self.log.debug("Received request to start game.")
        if balls < self.machine.config['machine']['min_balls']:
            self.dump_ball_counts()
            self.log.warning(
                "BallController denies game start. Not enough "
                "balls. %s found. %s required", balls,
                self.machine.config['machine']['min_balls'])
            return False

        if self.machine.config['game']['allow_start_with_ball_in_drain']:
            allowed_positions = ['home', 'trough', 'drain']
        else:
            allowed_positions = ['home', 'trough']

        if self.machine.config['game']['allow_start_with_loose_balls']:
            return

        elif not self.are_balls_collected(allowed_positions):
            self.collect_balls('home')
            self.dump_ball_counts()
            self.log.warning("BallController denies game start. Balls are not "
                             "in their home positions.")
            return False

    def are_balls_collected(self, target):
        """Check 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'.
        """
        self.log.debug(
            "Checking to see if all the balls are in devices tagged"
            " with '%s'", target)

        if isinstance(target, 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'].

        """
        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.available_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')
            '''event: collecting_balls

            desc: Posted by the ball controller when it starts the collecting
                balls process.

            '''

            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:
                if not device.is_playfield():
                    if "drain" in device.tags:
                        device.eject_all(device.find_next_trough())
                    else:
                        device.eject_all()
        else:
            self.log.debug("All balls are collected")
            self._collecting_balls_complete()

    def _collecting_balls_entered_callback(self, target, new_balls,
                                           unclaimed_balls, **kwargs):
        del kwargs
        del new_balls
        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')
        '''event: collecting_balls_complete

        desc: Posted by the ball controller when it has finished the collecting
            balls process.

        '''

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

        desc: A ball (or balls) has just drained. (More specifically, ball(s)
        have entered a ball device tagged with "drain".)

        This is a relay event.

        args:

        device: The ball device object that received the ball(s)

        balls: The number of balls that have just drained. Any balls remaining
        after the relay will be processed as newly-drained 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
Beispiel #23
0
class SwitchPlayer:

    """Plays back switch sequences from a config file, used for testing."""

    def __init__(self, machine):
        """Initialise switch player."""
        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.machine.delayRegistry)
        self.current_step = 0

        self.config = self.machine.config['switch_player']
        self.machine.config_validator.validate_config("switch_player", self.config)

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

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

    def __repr__(self):
        """Return string representation."""
        return '<SwitchPlayer>'

    def _start_event_callback(self, **kwargs):
        del kwargs
        self.delay.add(name='switch_player_next_step',
                       ms=Util.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=Util.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)
Beispiel #24
0
class BallSave(SystemWideDevice, ModeDevice):
    """Ball save device which will give back the ball within a certain time."""

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

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

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

    def _initialize(self) -> None:
        self.unlimited_saves = self.config['balls_to_save'] == -1
        self.source_playfield = self.config['source_playfield']

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

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

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

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

        return config

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

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

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

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

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

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

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

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

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

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

        self.timer_started = True

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

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

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

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

        self.state = 'hurry_up'

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

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

        self.state = 'grace_period'

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

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

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

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

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

        balls_to_save = available_balls

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

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

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

        return balls_to_save

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

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

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

        balls_to_save = self._get_number_of_balls_to_save(balls)

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

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

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

        self._schedule_balls(balls_to_save)

        self._reduce_remaining_saves_and_disable_if_zero(balls_to_save)

        return {'balls': balls - balls_to_save}

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

        if not self._get_number_of_balls_to_save(1):
            return

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

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

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

        self._reduce_remaining_saves_and_disable_if_zero(1)

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

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

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

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

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

        self.disable()
Beispiel #25
0
class DropTargetBank(SystemWideDevice, ModeDevice):
    """A bank of drop targets in a pinball machine by grouping together multiple `DropTarget` class devices."""

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

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

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

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

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

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

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

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

        self.member_target_change()

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

    @event_handler(5)
    def reset(self, **kwargs):
        """Reset this bank of drop targets.

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

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

        for drop_target in self.drop_targets:
            if drop_target.reset_coil:
                coils.add(drop_target.reset_coil)

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

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

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

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

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

    def member_target_change(self):
        """A member drop target has changed state.

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

        self.down = 0
        self.up = 0

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

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

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

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

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

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

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

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

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

        for target in self.drop_targets:
            target.remove_from_bank(self)
Beispiel #26
0
class Magnet(SystemWideDevice):

    """Controls a playfield magnet in a pinball machine."""

    config_section = 'magnets'
    collection = 'magnets'
    class_label = 'magnet'

    def __init__(self, machine, name):
        """Initialise magnet."""
        super().__init__(machine, name)
        self.delay = DelayManager(machine.delayRegistry)
        self._enabled = False
        self._active = False
        self._release_in_progress = False

    @event_handler(10)
    def enable(self, **kwargs):
        """Enable magnet."""
        del kwargs
        if self._enabled:
            return

        self.debug_log("Enabling Magnet")
        self._enabled = True

        if self.config['grab_switch']:
            self.config['grab_switch'].add_handler(self.grab_ball)

    @event_handler(0)
    def disable(self, **kwargs):
        """Disable magnet."""
        del kwargs
        if not self._enabled:
            return

        self.debug_log("Disabling Magnet")
        self._enabled = False

        if self.config['grab_switch']:
            self.config['grab_switch'].remove_handler(self.grab_ball)

    @event_handler(1)
    def reset(self, **kwargs):
        """Release ball and disable magnet."""
        del kwargs
        self.debug_log("Resetting Magnet")
        self.release_ball()
        self.disable()

    @event_handler(9)
    def grab_ball(self, **kwargs):
        """Grab a ball."""
        del kwargs
        # mark the playfield active no matter what
        self.config['playfield'].mark_playfield_active_from_device_action()
        # check if magnet is enabled or already active
        if not self._enabled or self._active or self._release_in_progress:
            return
        self.debug_log("Grabbing a ball.")
        self._active = True
        self.config['magnet_coil'].enable()

        self.machine.events.post("magnet_{}_grabbing_ball".format(self.name))
        '''event: magnet_(name)_grabbing_ball

        desc: The magnet called (name) is attempting to grab a ball.
        '''
        self.delay.add(self.config['grab_time'], self._grabbing_done)

    def _grabbing_done(self):
        self.machine.events.post("magnet_{}_grabbed_ball".format(self.name))
        '''event: magnet_(name)_grabbed_ball

        desc: The magnet called (name) has completed grabbing the ball.
        Note that the magnet doesn't actually "know" whether it
        successfully grabbed a ball or not, so this even is saying that it
        things it did.
        to).
        '''

    @event_handler(8)
    def release_ball(self, **kwargs):
        """Release the grabbed ball."""
        del kwargs
        if not self._active or self._release_in_progress:
            return

        self._active = False
        self._release_in_progress = True
        self.debug_log("Releasing ball.")
        self.machine.events.post("magnet_{}_releasing_ball".format(self.name))
        '''event: magnet_(name)_releasing_ball

        desc: The magnet called (name) is in the process of releasing a ball.
        '''

        self.delay.add(self.config['release_time'], self._release_done)
        self.config['magnet_coil'].disable()

    def _release_done(self):
        self._release_in_progress = False
        self.machine.events.post("magnet_{}_released_ball".format(self.name))
        '''event: magnet_(name)_released_ball

        desc: The magnet called (name) has just released a ball.
        '''

    @event_handler(7)
    def fling_ball(self, **kwargs):
        """Fling the grabbed ball."""
        del kwargs
        if not self._active or self._release_in_progress:
            return

        self._active = False
        self._release_in_progress = True
        self.debug_log("Flinging ball.")
        self.machine.events.post("magnet_{}_flinging_ball".format(self.name))
        '''event: magnet_(name)_flinging_ball

        desc: The magnet called (name) is flinging a ball by disabling and
        enabling the magnet again for a short time.
        '''

        self.delay.add(self.config['fling_drop_time'], self._fling_reenable)
        self.config['magnet_coil'].disable()

    def _fling_reenable(self):
        self.delay.add(self.config['fling_regrab_time'], self._fling_done)
        self.config['magnet_coil'].enable()

    def _fling_done(self):
        self._release_in_progress = False
        self.config['magnet_coil'].disable()
        self.machine.events.post("magnet_{}_flinged_ball".format(self.name))
        '''event: magnet_(name)_flinged_ball
Beispiel #27
0
class Mode(object):
    """Parent class for in-game mode code."""
    def __init__(self, machine, config: dict, name: str, path):
        """Initialise mode.

        Args:
            machine(mpf.core.machine.MachineController): the machine controller
            config: config dict for mode
            name: name of mode
            path: path of mode

        Returns:

        """
        self.machine = machine
        self.config = config
        self.name = name.lower()
        self.path = path

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

        self.delay = DelayManager(self.machine.delayRegistry)

        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_stop_kwargs = dict()
        self.mode_devices = set()
        self.start_event_kwargs = None
        self.stopping = False

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

        self._create_mode_devices()

        self._validate_mode_config()

        self._initialise_mode_devices()

        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' player variable.
        '''

        # 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,
                            mode=self,
                            root_config_dict=self.config,
                            **item.kwargs)
            elif not item.config_section:
                item.method(config=self.config,
                            mode_path=self.path,
                            **item.kwargs)

        self.mode_init()

    @staticmethod
    def get_config_spec():
        """Return config spec for mode_settings."""
        return '''
                __valid_in__: mode
                __allow_others__:
                '''

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

    @property
    def active(self):
        """Return true if mode is active."""
        return self._active

    @active.setter
    def active(self, active):
        """Setter for _active."""
        if self._active != active:
            self._active = active
            self.machine.mode_controller.set_mode_state(self, self._active)

    def configure_mode_settings(self, config):
        """Process this mode's configuration settings from a config dictionary."""
        self.config['mode'] = self.machine.config_validator.validate_config(
            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):
        """Validate mode config."""
        for section in self.machine.config['mpf']['mode_config_sections']:
            this_section = self.config.get(section, None)

            # do not double validate devices
            if section in self.machine.device_manager.device_classes:
                continue

            if this_section:
                if isinstance(this_section, dict):
                    for device, settings in this_section.items():
                        self.config[section][device] = (
                            self.machine.config_validator.validate_config(
                                section, settings, "mode:" + self.name))

                else:
                    self.config[section] = (
                        self.machine.config_validator.validate_config(
                            section, this_section))

    def _get_merged_settings(self, section_name):
        """Return a 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, mode_priority=None, callback=None, **kwargs):
        """Start this mode.

        Args:
            mode_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.config['mode']['game_mode'] and not self.machine.game:
            raise AssertionError(
                "Can only start mode {} during a game.".format(self.name))

        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 isinstance(mode_priority, int):
            self.priority = mode_priority
        else:
            self.priority = self.config['mode']['priority']

        self.start_event_kwargs = kwargs

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

        self._add_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

        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)
        '''event: mode_(name)_starting

        desc: The mode called "name" is starting.

        This is a queue event. The mode will not fully start until the queue is
        cleared.
        '''

    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

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

        self._start_timers()

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

        desc: Posted when a mode has started. The "name" part is replaced
        with the actual name of the mode, so the actual event posted is
        something like *mode_attract_started*, *mode_base_started*, etc.

        This is posted after the "mode_(name)_starting" event.
        '''

    def _mode_started_callback(self, **kwargs):
        """Called after the mode_<name>_started queue event has finished."""
        del kwargs
        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):
        """Stop 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.stopping = True

        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)
        '''event: mode_(name)_stopping

        desc: The mode called "name" is stopping. This is a queue event. The
        mode won't actually stop until the queue is cleared.

        '''

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

        self.priority = 0
        self.active = False
        self.stopping = False

        for callback in self.machine.mode_controller.stop_methods:
            callback[0](self)

        for item in self.stop_methods:
            item[0](item[1])

        self.stop_methods = list()

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

        desc: Posted when a mode has stopped. The "name" part is replaced
        with the actual name of the mode, so the actual event posted is
        something like *mode_attract_stopped*, *mode_base_stopped*, etc.
        '''

        self.machine.events.post('clear', key=self.name)
        '''event: clear

        args:
            key: string name of the configs to clear

        desc: Posted to cause config players to clear whatever they're running
            based on the key passed. Typically posted when a show or mode ends.
        '''

        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):
        del 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 _add_mode_devices(self):
        # adds and initializes mode devices which get removed at the end of the mode

        for collection_name, device_class in (iter(
                self.machine.device_manager.device_classes.items())):

            # check if there is config for the device type
            if device_class.config_section in self.config:

                for device_name in self.config[device_class.config_section]:

                    collection = getattr(self.machine, collection_name)

                    # get device
                    device = collection[device_name]

                    # Track that this device was added via this mode so we
                    # can remove it when the mode ends.
                    self.mode_devices.add(device)
                    if not self.config['mode'][
                            'game_mode'] and not device.can_exist_outside_of_game:
                        raise AssertionError(
                            "Device {} cannot exist in non game-mode {}.".
                            format(device, self.name))

                    # This lets the device know it was added to a mode
                    device.device_added_to_mode(mode=self, player=self.player)

    def _create_mode_devices(self):
        """Create new devices that are specified in a mode config that haven't been created in the machine-wide."""
        self.log.debug("Scanning config for mode-based devices")

        for collection_name, device_class in iter(
                self.machine.device_manager.device_classes.items()):

            # check if there is config for the device type
            if device_class.config_section not in self.config:
                continue

            # check if it is supposed to be used in mode
            if collection_name not in self.machine.config['mpf'][
                    'mode_config_sections']:
                raise AssertionError(
                    "Found config for device {} in mode {} which may not be used in modes"
                    .format(collection_name, self.name))

            for device, settings in iter(
                    self.config[device_class.config_section].items()):

                collection = getattr(self.machine, collection_name)

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

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

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

    def _initialise_mode_devices(self):
        """Initialise new devices that are specified in a mode config."""
        for collection_name, device_class in iter(
                self.machine.device_manager.device_classes.items()):

            # check if there is config for the device type
            if device_class.config_section not in self.config:
                continue

            for device, settings in iter(
                    self.config[device_class.config_section].items()):

                collection = getattr(self.machine, collection_name)
                device = collection[device]
                settings = device.prepare_config(settings, True)
                settings = device.validate_and_parse_config(settings, True)

                if device.config:
                    self.log.debug("Overwrite mode-based device: %s", device)
                    # overload
                    device.overload_config_in_mode(self, settings)

                else:
                    self.log.debug("Initialising mode-based device: %s",
                                   device)
                    # load config
                    device.load_config(settings)

    def _remove_mode_devices(self):
        for device in self.mode_devices:
            device.device_removed_from_mode(self)

        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.add_control_events_in_mode(self)

    def _control_event_handler(self, callback, ms_delay=0, **kwargs):
        del 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):
        """Register 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'].items():

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

        return self._kill_timers

    def _start_timers(self):
        for timer in list(self.timers.values()):
            if timer.config['start_running']:
                timer.start()

    def _kill_timers(self, ):
        for timer in list(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."""
        pass
Beispiel #28
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.
    """

    config_section = 'counters'
    collection = 'counters'
    class_label = 'counter'

    __slots__ = ["delay", "ignore_hits", "hit_value"]

    def __init__(self, machine: MachineController, name: str) -> None:
        """Initialise counter."""
        super().__init__(machine, name)
        self.debug_log("Creating Counter LogicBlock")

        self.delay = DelayManager(self.machine)

        self.ignore_hits = False
        self.hit_value = -1

    async def _initialize(self):
        await super()._initialize()
        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
        # Add control events if included in the config
        if self.config['control_events']:
            self._setup_control_events(self.config['control_events'])

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

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

        kwargs = {}
        for entry in event_list:
            if entry['action'] in ('add', 'subtract', 'jump'):
                handler = getattr(self, "event_{}".format(entry['action']))
                kwargs = {'value': entry['value']}
            else:
                raise AssertionError(
                    "Invalid control_event action {} in mode".format(
                        entry['action']), self.name)
            self.machine.events.add_handler(entry['event'], handler, **kwargs)

    def check_complete(self, count_complete_value=None):
        """Check if counter is completed.

        Return true if the counter has reached or surpassed its specified
        completion value, return False if no completion criteria or is
        not complete.
        """
        # If count_complete_value was not passed, obtain it
        if count_complete_value is None and self.config.get(
                "count_complete_value"):
            count_complete_value = self.config[
                "count_complete_value"].evaluate([])

        if count_complete_value is not None:
            if self.config['direction'] == 'up':
                return self._state.value >= count_complete_value
            if self.config['direction'] == 'down':
                return self._state.value <= count_complete_value

        return False

    def event_add(self, value, **kwargs):
        """Add to the value of this counter.

        Args:
            kwargs: Used for the "value" member which contains how much to add
            to the counter.
        """
        evaluated_value = value.evaluate_or_none(kwargs)
        if evaluated_value is None:
            self.log.warning(
                "Placeholder %s for counter add did not evaluate with args %s",
                value, kwargs)
            return
        # Add to the counter the specified value
        self._state.value += evaluated_value
        # Check if count is complete given the updated value
        if self.check_complete():
            self.complete()

    def event_subtract(self, value, **kwargs):
        """Subtract from the value of this counter.

        Args:
            kwargs: Used for the "value" member which contains how much to
            subtract from the counter.
        """
        evaluated_value = value.evaluate_or_none(kwargs)
        if evaluated_value is None:
            self.log.warning(
                "Placeholder %s for counter substract did not evaluate with args %s",
                value, kwargs)
            return
        # Subtract from the counter the specified value
        self._state.value -= evaluated_value
        # Check if count is complete given the updated value
        if self.check_complete():
            self.complete()

    def event_jump(self, value, **kwargs):
        """Set the internal value of the counter.

        Args:
            kwargs: Used for the "value" member which contains what to set
            the counter value to.
        """
        evaluated_value = value.evaluate_or_none(kwargs)
        if evaluated_value is None:
            self.log.warning(
                "Placeholder %s for counter jump did not evaluate with args %s",
                value, kwargs)
            return
        # Set the internal value of the counter to the specified value
        self._state.value = evaluated_value
        # Check if count is complete given the updated value
        if self.check_complete():
            self.complete()

    def get_start_value(self) -> int:
        """Return start count."""
        return self.config['starting_count'].evaluate([])

    def validate_and_parse_config(self,
                                  config: dict,
                                  is_mode_config: bool,
                                  debug_prefix: str = None) -> dict:
        """Validate logic block config."""
        if 'events_when_hit' not in config:
            # for compatibility post the same default as previously for
            # counters. This one is deprecated.
            config['events_when_hit'] = ['counter_' + self.name + '_hit']

            # this is the one moving forward
            config['events_when_hit'].append('logicblock_' + self.name +
                                             '_hit')

        return super().validate_and_parse_config(config, is_mode_config,
                                                 debug_prefix)

    @event_handler(0)
    def event_count(self, **kwargs):
        """Event handler for count events."""
        del kwargs
        self.count()

    def count(self):
        """Increase the hit progress towards completion.

        This method is also automatically called when one of the
        ``count_events`` is posted.

        """
        if not self.enabled:
            return

        count_complete_value = self.config['count_complete_value'].evaluate([]) if self.config['count_complete_value']\
            is not None else None

        if not self.ignore_hits:
            self._state.value += self.hit_value
            self.debug_log("Processing Count change. Total: %s",
                           self._state.value)

            args = {"count": self._state.value}
            if count_complete_value is not None:
                args['remaining'] = count_complete_value - self._state.value

            self._post_hit_events(**args)

            if self.check_complete(count_complete_value):
                self.complete()

            if self.config['multiple_hit_window']:
                self.debug_log("Beginning Ignore Hits")
                self.ignore_hits = True
                self.delay.add(name='ignore_hits_within_window',
                               ms=self.config['multiple_hit_window'],
                               callback=self.stop_ignoring_hits)

    def stop_ignoring_hits(self, **kwargs):
        """Cause 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.
        """
        del kwargs
        self.debug_log("Ending Ignore hits")
        self.ignore_hits = False
Beispiel #29
0
class Mode(LogMixin):
    """Base class for a mode."""

    __slots__ = [
        "machine", "config", "name", "path", "priority", "_active",
        "_starting", "_mode_start_wait_queue", "stop_methods",
        "start_callback", "stop_callbacks", "event_handlers",
        "switch_handlers", "mode_stop_kwargs", "mode_devices",
        "start_event_kwargs", "stopping", "delay", "player",
        "auto_stop_on_ball_end", "restart_on_next_ball", "asset_paths"
    ]

    # pylint: disable-msg=too-many-arguments
    def __init__(self, machine: "MachineController", config, name: str, path,
                 asset_paths) -> None:
        """Initialise mode.

        Args:
        ----
            machine: the machine controller
            config: config dict for mode
            name: name of mode
            path: path of mode
            asset_paths: all paths to consider for assets in this mode
        """
        super().__init__()
        self.machine = machine  # type: MachineController
        self.config = config  # type: ignore
        self.name = name
        self.path = path
        self.asset_paths = asset_paths
        self.priority = 0
        self._active = False
        self._starting = False
        self._mode_start_wait_queue = None  # type: Optional[QueuedEvent]
        self.stop_methods = list(
        )  # type: List[Tuple[Callable[[Any], None], Any]]
        self.start_callback = None  # type: Optional[Callable[[], None]]
        self.stop_callbacks = []  # type: List[Callable[[], None]]
        self.event_handlers = set()  # type: Set[EventHandlerKey]
        self.switch_handlers = list()  # type: List[SwitchHandler]
        self.mode_stop_kwargs = dict()  # type: Dict[str, Any]
        self.mode_devices = set()  # type: Set[ModeDevice]
        self.start_event_kwargs = {}  # type: Dict[str, Any]
        self.stopping = False

        self.delay = DelayManager(self.machine)
        '''DelayManager instance for delays in this mode. Note that all delays
        scheduled here will be automatically canceled when the mode stops.'''

        self.player = None  # type: Optional[Player]
        '''Reference to the current player object.'''

        self.configure_logging('Mode.' + name,
                               self.config['mode']['console_log'],
                               self.config['mode']['file_log'])

        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' player variable.
        '''

        if self.config['mode'][
                'game_mode'] and not self.config['mode']['stop_on_ball_end']:
            self.raise_config_error(
                "All game modes need to stop at ball end. If you want to set stop_on_ball_end to "
                "False also set game_mode to False.", 1)

    @staticmethod
    def get_config_spec() -> str:
        """Return config spec for mode_settings."""
        return '''
                __valid_in__: mode
                __allow_others__:
                '''

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

    @property
    def active(self) -> bool:
        """Return *True* if this mode is active."""
        return self._active

    @active.setter
    def active(self, new_active: bool):
        """Setter for _active."""
        if self._active != new_active:
            self._active = new_active
            self.machine.mode_controller.set_mode_state(self, self._active)

    def configure_mode_settings(self, config: dict) -> None:
        """Process this mode's configuration settings from a config dictionary."""
        self.config['mode'] = self.machine.config_validator.validate_config(
            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'])

    @property
    def is_game_mode(self) -> bool:
        """Return true if this is a game mode."""
        return bool(self.config['mode']['game_mode'])

    def start(self, mode_priority=None, callback=None, **kwargs) -> None:
        """Start this mode.

        Args:
        ----
            mode_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.
            callback: Callback to call when this mode has been started.
            **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.
        """
        # remove argument so we do not repost this
        kwargs.pop('_from_bcp', None)
        self.debug_log("Received request to start")

        if self.config['mode']['game_mode'] and not (self.machine.game
                                                     and self.player):
            self.warning_log(
                "Can only start mode %s during a game. Aborting start.",
                self.name)
            return

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

        if self._starting:
            self.debug_log("Mode already starting. Aborting start.")
            return

        self._starting = True

        self.machine.events.post('mode_{}_will_start'.format(self.name),
                                 **kwargs)
        '''event: mode_(name)_will_start

        desc: Posted when a mode is about to start. The "name" part is replaced
        with the actual name of the mode, so the actual event posted is
        something like *mode_attract_will_start*, *mode_base_will_start*, etc.

        This is posted before the "mode_(name)_starting" event.
        '''

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

            self.debug_log("Registering a mode start wait queue")

            self._mode_start_wait_queue = kwargs['queue']
            assert isinstance(self._mode_start_wait_queue, QueuedEvent)
            self._mode_start_wait_queue.wait()

        if isinstance(mode_priority, int):
            self.priority = mode_priority
        else:
            self.priority = self.config['mode']['priority']

        self.start_event_kwargs = kwargs

        # hook for custom code. called before any mode devices are set up
        self.mode_will_start(**self.start_event_kwargs)

        self._add_mode_devices()

        self.debug_log("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.config['mode']['stop_priority'] + 1)

        self.start_callback = callback

        self.debug_log("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:
                result = item.method(config=self.config.get(
                    item.config_section, self.config),
                                     priority=self.priority,
                                     mode=self,
                                     **item.kwargs)
                if result:
                    self.stop_methods.append(result)

        self._setup_device_control_events()

        self.machine.events.post_queue(event='mode_{}_starting'.format(
            self.name),
                                       callback=self._started,
                                       **kwargs)
        '''event: mode_(name)_starting

        desc: The mode called "name" is starting.

        This is a queue event. The mode will not fully start until the queue is
        cleared.
        '''

    def _started(self, **kwargs) -> None:
        """Handle result of mode_<name>_starting queue event."""
        del kwargs
        if self.machine.is_shutting_down:
            self.info_log("Will not start because machine is shutting down.")
            return

        self.info_log('Started. Priority: %s', self.priority)

        self.active = True
        self._starting = False

        for event_name in self.config['mode']['events_when_started']:
            self.machine.events.post(event_name)

        self.machine.events.post(event='mode_{}_started'.format(self.name),
                                 callback=self._mode_started_callback,
                                 **self.start_event_kwargs)
        '''event: mode_(name)_started

        desc: Posted when a mode has started. The "name" part is replaced
        with the actual name of the mode, so the actual event posted is
        something like *mode_attract_started*, *mode_base_started*, etc.

        This is posted after the "mode_(name)_starting" event.
        '''

    def _mode_started_callback(self, **kwargs) -> None:
        """Handle result of mode_<name>_started queue event."""
        del kwargs
        self.mode_start(**self.start_event_kwargs)

        self.start_event_kwargs = dict()

        if self.start_callback:
            self.start_callback()

        self.debug_log('Mode Start process complete.')

    def stop(self, callback: Any = None, **kwargs) -> bool:
        """Stop this mode.

        Args:
        ----
            callback: Method which will be called once this mode has stopped. Will only be called when the mode is
                running (includes currently stopping)
            **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.

        Returns true if the mode is running. Otherwise false.
        """
        if not self._active:
            return False

        if callback:
            self.stop_callbacks.append(callback)

        # do not stop twice. only register callback in that case
        if self.stopping:
            # mode is still running
            return True

        self.machine.events.post('mode_' + self.name + '_will_stop')
        '''event: mode_(name)_will_stop

        desc: Posted when a mode is about to stop. The "name" part is replaced
        with the actual name of the mode, so the actual event posted is
        something like *mode_attract_will_stop*, *mode_base_will_stop*, etc.

        This is posted immediately before the "mode_(name)_stopping" event.
        '''

        self.stopping = True

        self.mode_stop_kwargs = kwargs

        self.debug_log('Mode Stopping.')

        self._remove_mode_switch_handlers()

        self.delay.clear()

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

        desc: The mode called "name" is stopping. This is a queue event. The
        mode won't actually stop until the queue is cleared.

        '''
        return True

    def _stopped(self) -> None:
        self.info_log('Stopped.')

        self.priority = 0
        self.active = False
        self.stopping = False

        for item in self.stop_methods:
            item[0](item[1])

        self.stop_methods = list()

        for event_name in self.config['mode']['events_when_stopped']:
            self.machine.events.post(event_name)

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

        desc: Posted when a mode has stopped. The "name" part is replaced
        with the actual name of the mode, so the actual event posted is
        something like *mode_attract_stopped*, *mode_base_stopped*, etc.
        '''

        self.machine.events.post('clear', key=self.name)
        '''event: clear

        args:
            key: string name of the configs to clear

        desc: Posted to cause config players to clear whatever they're running
            based on the key passed. Typically posted when a show or mode ends.
        '''

        if self._mode_start_wait_queue:

            self.debug_log("Clearing wait queue")

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

    def _mode_stopped_callback(self, **kwargs) -> None:
        del kwargs

        # Call the mode_stop() method before removing the devices
        self.mode_stop(**self.mode_stop_kwargs)
        self.mode_stop_kwargs = dict()

        # Clean up the mode handlers and devices
        self._remove_mode_event_handlers()
        self._remove_mode_devices()

        for callback in self.stop_callbacks:
            callback()

        self.stop_callbacks = []

    def _add_mode_devices(self) -> None:
        # adds and initializes mode devices which get removed at the end of the mode

        for collection_name, device_class in (iter(
                self.machine.device_manager.device_classes.items())):

            # check if there is config for the device type
            if device_class.config_section in self.config:

                for device_name in self.config[device_class.config_section]:

                    collection = getattr(self.machine, collection_name)

                    # get device
                    device = collection[device_name]

                    # Track that this device was added via this mode so we
                    # can remove it when the mode ends.
                    self.mode_devices.add(device)
                    if not self.config['mode'][
                            'game_mode'] and not device.can_exist_outside_of_game:
                        raise AssertionError(
                            "Device {} cannot exist in non game-mode {}.".
                            format(device, self.name))

                    # This lets the device know it was added to a mode
                    device.device_loaded_in_mode(mode=self, player=self.player)

    def create_mode_devices(self) -> None:
        """Create new devices that are specified in a mode config that haven't been created in the machine-wide."""
        self.debug_log("Scanning config for mode-based devices")

        for collection_name, device_class in iter(
                self.machine.device_manager.device_classes.items()):

            # check if there is config for the device type
            if device_class.config_section not in self.config:
                continue

            for device, settings in iter(
                    self.config[device_class.config_section].items()):

                collection = getattr(self.machine, collection_name)

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

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

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

    async def load_mode_devices(self) -> None:
        """Load config of mode devices."""
        for collection_name, device_class in iter(
                self.machine.device_manager.device_classes.items()):

            # check if there is config for the device type
            if device_class.config_section not in self.config:
                continue

            for device, settings in iter(
                    self.config[device_class.config_section].items()):

                collection = getattr(self.machine, collection_name)
                device = collection[device]
                settings = device.prepare_config(settings, True)
                settings = device.validate_and_parse_config(
                    settings, True, "mode:" + self.name)

                if device.config:
                    self.debug_log("Overwrite mode-based device: %s", device)
                    # overload
                    device.overload_config_in_mode(self, settings)

                else:
                    self.debug_log("Initializing mode-based device: %s",
                                   device)
                    # load config
                    device.load_config(settings)

        for collection_name, device_class in iter(
                self.machine.device_manager.device_classes.items()):
            # check if there is config for the device type
            if device_class.config_section not in self.config:
                continue

            for device, settings in iter(
                    self.config[device_class.config_section].items()):
                collection = getattr(self.machine, collection_name)
                device = collection[device]
                await device.device_added_to_mode(mode=self)

    def _remove_mode_devices(self) -> None:
        for device in self.mode_devices:
            device.device_removed_from_mode(self)

        self.mode_devices = set()

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

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

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

            if not delay:
                self.add_mode_event_handler(
                    event=event,
                    handler=method,
                    blocking_facility=device.class_label)
            else:
                self.add_mode_event_handler(
                    event=event,
                    handler=self._control_event_handler,
                    callback=method,
                    ms_delay=delay,
                    blocking_facility=device.class_label)

        # get all devices in the mode
        device_list = set()  # type: Set[ModeDevice]
        for collection in self.machine.device_manager.collections:
            if self.machine.device_manager.collections[
                    collection].config_section in self.config:
                for device, _ in \
                        iter(self.config[self.machine.device_manager.collections[collection].config_section].items()):
                    device_list.add(
                        self.machine.device_manager.collections[collection]
                        [device])

        for device in device_list:
            device.add_control_events_in_mode(self)

    def _control_event_handler(self,
                               callback: Callable[..., None],
                               ms_delay: int = 0,
                               **kwargs) -> None:
        del kwargs
        self.debug_log("_control_event_handler: callback: %s,", callback)

        self.delay.add(ms=ms_delay, callback=callback, mode=self)

    def add_mode_event_handler(self,
                               event: str,
                               handler: Callable,
                               priority: int = 0,
                               **kwargs) -> EventHandlerKey:
        """Register 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 EventHandlerKey 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,
                                              self.priority + priority,
                                              mode=self,
                                              **kwargs)

        self.event_handlers.add(key)

        return key

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

    def _remove_mode_switch_handlers(self) -> None:
        for handler in self.switch_handlers:
            self.machine.switch_controller.remove_switch_handler_by_key(
                handler)
        self.switch_handlers = list()

    def initialise_mode(self) -> None:
        """Initialise this mode."""
        self.mode_init()

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

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

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

    def mode_stop(self, **kwargs) -> None:
        """User-overrideable method which will be called whenever this mode stops."""
Beispiel #30
0
class HardwarePlatform(DriverPlatform):
    """Overlay platform for the snux hardware board."""
    def __init__(self, machine):
        """Initalize the board."""
        super().__init__(machine)

        self.log = logging.getLogger('Platform.Snux')
        self.delay = DelayManager(machine.delayRegistry)

        self.platform = None

        self.system11_config = None
        self.snux_config = None

        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_enabled = True
        self.c_side_enabled = False

        self.ac_relay_in_transition = False

    def stop(self):
        """Stop the overlay. Nothing to do here because stop is also called on parent platform."""
        pass

    @property
    def a_side_busy(self):
        """True when A side cannot be switches off right away."""
        return self.drivers_holding_a_side or self.a_side_done_time > self.machine.clock.get_time(
        ) or self.a_side_queue

    @property
    def c_side_active(self):
        """True when C side cannot be switches off right away."""
        return self.drivers_holding_c_side or self.c_side_done_time > self.machine.clock.get_time(
        )

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

    def initialize(self):
        """Automatically called by the Platform class after all the core modules are loaded."""
        # load coil platform
        self.platform = self.machine.get_platform_sections(
            "platform", getattr(self.machine.config['snux'], 'platform', None))

        # we have to wait for coils to be initialized
        self.machine.events.add_handler("init_phase_1", self._initialize)

    def _initialize(self, **kwargs):
        del kwargs
        self._validate_config()

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

        # Hack to silence logging of P_ROC
        # TODO: clean this up
        self.snux_config[
            'diag_led_driver'].hw_driver.log.info = self._null_log_handler
        self.snux_config[
            'diag_led_driver'].hw_driver.log.debug = self._null_log_handler

        self.snux_config['diag_led_driver'].enable()

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

        if not self.system11_config['ac_relay_driver'].config['allow_enable']:
            raise AssertionError(
                "AC Relay has to have allow_enable set to true")

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

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

        if not self.snux_config['flipper_enable_driver'].config['allow_enable']:
            raise AssertionError(
                "Flipper Relay has to have allow_enable set to true")

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

    def _initialize_phase_2(self, **kwargs):
        del kwargs
        self.machine.clock.schedule_interval(self._flash_diag_led, 0.5)

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

        self.snux_config = self.machine.config_validator.validate_config(
            'snux', self.machine.config['snux'])

    def tick(self, dt):
        """Snux main loop.

        Called based on the timer_tick event

        Args:
            dt: time since last call
        """
        del dt
        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, dt):
        del dt
        self.snux_config['diag_led_driver'].pulse(250)

    def configure_driver(self, config):
        """Configure a driver on the snux board.

        Args:
            config: Driver config dict
        """
        orig_number = config['number']

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

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

            platform_driver = self.platform.configure_driver(config)

            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

        else:
            return self.platform.configure_driver(config)

    def set_pulse_on_hit_and_release_rule(self, enable_switch, coil):
        """Configure a rule for a driver on the snux board.

        Will pass the call onto the parent platform if the driver is not on A/C relay.
        """
        if coil.hw_driver in self.a_drivers or coil.hw_driver in self.c_drivers:
            self.log.warning("Received a request to set a hardware rule for a"
                             "switched driver. Ignoring")
        else:
            self.platform.set_pulse_on_hit_and_release_rule(
                enable_switch, coil)

    def set_pulse_on_hit_and_enable_and_release_rule(self, enable_switch,
                                                     coil):
        """Configure a rule for a driver on the snux board.

        Will pass the call onto the parent platform if the driver is not on A/C relay.
        """
        if coil.hw_driver in self.a_drivers or coil.hw_driver in self.c_drivers:
            self.log.warning("Received a request to set a hardware rule for a"
                             "switched driver. Ignoring")
        else:
            self.platform.set_pulse_on_hit_and_enable_and_release_rule(
                enable_switch, coil)

    def set_pulse_on_hit_and_enable_and_release_and_disable_rule(
            self, enable_switch, disable_switch, coil):
        """Configure a rule for a driver on the snux board.

        Will pass the call onto the parent platform if the driver is not on A/C relay.
        """
        if coil.hw_driver in self.a_drivers or coil.hw_driver in self.c_drivers:
            self.log.warning("Received a request to set a hardware rule for a"
                             "switched driver. Ignoring")
        else:
            self.platform.set_pulse_on_hit_and_enable_and_release_and_disable_rule(
                enable_switch, disable_switch, coil)

    def set_pulse_on_hit_rule(self, enable_switch, coil):
        """Configure a rule on the snux board.

        Will pass the call onto the parent platform if the driver is not on A/C relay.
        """
        if coil.hw_driver in self.a_drivers or coil.hw_driver in self.c_drivers:
            self.log.warning("Received a request to set a hardware rule for a"
                             "switched driver. Ignoring")
        else:
            self.platform.set_pulse_on_hit_rule(enable_switch, coil)

    def clear_hw_rule(self, switch, coil):
        """Clear a rule for a driver on the snux board."""
        self.platform.clear_hw_rule(switch, coil)

    def driver_action(self, driver, coil, milliseconds):
        """Add 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, coil, milliseconds))
            self._service_a_side()
        elif driver in self.c_drivers:
            self.c_side_queue.add((driver, coil, milliseconds))
            if not self.ac_relay_in_transition and not self.a_side_busy:
                self._service_c_side()

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

    def _disable_ac_relay(self):
        self.system11_config['ac_relay_driver'].disable()
        self.ac_relay_in_transition = True
        self.a_side_enabled = False
        self.c_side_enabled = False
        self.delay.add(ms=self.system11_config['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._disable_ac_relay()
                self.delay.add(ms=self.system11_config['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, coil, ms = self.a_side_queue.pop()

            if ms > 0:
                driver.pulse(coil, ms)
                self.a_side_done_time = max(
                    self.a_side_done_time,
                    self.machine.clock.get_time() + (ms / 1000.0))

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

            else:  # ms == 0
                driver.disable(coil)
                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, coil, ms = self.c_side_queue.pop()

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

            else:  # ms == 0
                driver.disable(coil)
                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.drivers_holding_c_side:
                driver.disable(ConfiguredHwDriver(driver, {}))
            self.drivers_holding_c_side = set()
            self.c_side_done_time = 0
            self.c_side_enabled = False

    def validate_coil_section(self, driver, config):
        """Validate coil config for platform."""
        return self.platform.validate_coil_section(driver, config)