Esempio n. 1
0
File: rpi.py Progetto: unRARed/mpf
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()
Esempio n. 2
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."""
Esempio n. 3
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
Esempio n. 4
0
class ComboSwitch(SystemWideDevice, ModeDevice):

    """Combo Switch device."""

    config_section = 'combo_switches'
    collection = 'combo_switches'
    class_label = 'combo_switch'

    def __init__(self, machine, name):
        """Initialize Combo Switch."""
        super().__init__(machine, name)
        self.states = ['inactive', 'both', 'one']
        self._state = 'inactive'
        self._switches_1_active = False
        self._switches_2_active = False

        self.delay_registry = DelayManagerRegistry(self.machine)
        self.delay = DelayManager(self.delay_registry)

    def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str=None) -> dict:
        """Validate and parse config."""
        config = super().validate_and_parse_config(config, is_mode_config, debug_prefix)

        for state in self.states:
            if not config['events_when_{}'.format(state)]:
                config['events_when_{}'.format(state)] = [
                    "{}_{}".format(self.name, state)]

        return config

    def device_added_system_wide(self):
        """Add event handlers."""
        super().device_added_system_wide()
        self._add_switch_handlers()

    def device_loaded_in_mode(self, mode: Mode, player: Player):
        """Add event handlers."""
        self._add_switch_handlers()

    def _add_switch_handlers(self):
        if self.config['tag_1']:
            for tag in self.config['tag_1']:
                for switch in self.machine.switches.items_tagged(tag):
                    self.config['switches_1'].add(switch)

        if self.config['tag_2']:
            for tag in self.config['tag_2']:
                for switch in self.machine.switches.items_tagged(tag):
                    self.config['switches_2'].add(switch)

        self._register_switch_handlers()

    @property
    def state(self):
        """Return current state."""
        return self._state

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

    def device_removed_from_mode(self, mode):
        """Mode ended.

        Args:
            mode: mode which stopped
        """
        del mode

        self._remove_switch_handlers()
        self._kill_delays()

    def _register_switch_handlers(self):
        for switch in self.config['switches_1']:
            switch.add_handler(self._switch_1_went_active, state=1)
            switch.add_handler(self._switch_1_went_inactive, state=0)

        for switch in self.config['switches_2']:
            switch.add_handler(self._switch_2_went_active, state=1)
            switch.add_handler(self._switch_2_went_inactive, state=0)

    def _remove_switch_handlers(self):
        for switch in self.config['switches_1']:
            switch.remove_handler(self._switch_1_went_active, state=1)
            switch.remove_handler(self._switch_1_went_inactive, state=0)

        for switch in self.config['switches_2']:
            switch.remove_handler(self._switch_2_went_active, state=1)
            switch.remove_handler(self._switch_2_went_inactive, state=0)

    def _kill_delays(self):
        self.delay.clear()

    def _switch_1_went_active(self):
        self.debug_log('A switch from switches_1 just went active')
        self.delay.remove('switch_1_inactive')

        if self._switches_1_active:
            return

        if not self.config['hold_time']:
            self._activate_switches_1()
        else:
            self.delay.add_if_doesnt_exist(self.config['hold_time'],
                                           self._activate_switches_1,
                                           'switch_1_active')

    def _switch_2_went_active(self):
        self.debug_log('A switch from switches_2 just went active')
        self.delay.remove('switch_2_inactive')

        if self._switches_2_active:
            return

        if not self.config['hold_time']:
            self._activate_switches_2()
        else:
            self.delay.add_if_doesnt_exist(self.config['hold_time'],
                                           self._activate_switches_2,
                                           'switch_2_active')

    def _switch_1_went_inactive(self):
        self.debug_log('A switch from switches_1 just went inactive')
        for switch in self.config['switches_1']:
            if switch.state:
                # at least one switch is still active
                return

        self.delay.remove('switch_1_active')

        if not self.config['release_time']:
            self._release_switches_1()
        else:
            self.delay.add_if_doesnt_exist(self.config['release_time'],
                                           self._release_switches_1,
                                           'switch_1_inactive')

    def _switch_2_went_inactive(self):
        self.debug_log('A switch from switches_2 just went inactive')
        for switch in self.config['switches_2']:
            if switch.state:
                # at least one switch is still active
                return

        self.delay.remove('switch_2_active')

        if not self.config['release_time']:
            self._release_switches_2()
        else:
            self.delay.add_if_doesnt_exist(self.config['release_time'],
                                           self._release_switches_2,
                                           'switch_2_inactive')

    def _activate_switches_1(self):
        self.debug_log('Switches_1 has passed the hold time and is now '
                       'active')
        self._switches_1_active = self.machine.clock.get_time()

        if self._switches_2_active:
            if (self.config['max_offset_time'] >= 0 and
                    (self._switches_1_active - self._switches_2_active >
                        self.config['max_offset_time'])):

                self.debug_log("Switches_2 is active, but the "
                               "max_offset_time=%s which is largest than when "
                               "a Switches_2 switch was first activated, so "
                               "the state will not switch to 'both'",
                               self.config['max_offset_time'])

                return

            self._switch_state('both')

    def _activate_switches_2(self):
        self.debug_log('Switches_2 has passed the hold time and is now '
                       'active')
        self._switches_2_active = self.machine.clock.get_time()

        if self._switches_1_active:
            if (self.config['max_offset_time'] >= 0 and
                    (self._switches_2_active - self._switches_1_active >
                        self.config['max_offset_time'])):
                self.debug_log("Switches_2 is active, but the "
                               "max_offset_time=%s which is largest than when "
                               "a Switches_2 switch was first activated, so "
                               "the state will not switch to 'both'",
                               self.config['max_offset_time'])
                return

            self._switch_state('both')

    def _release_switches_1(self):
        self.debug_log('Switches_1 has passed the release time and is now '
                       'releases')
        self._switches_1_active = None
        if self._switches_2_active and self._state == 'both':
            self._switch_state('one')
        elif self._state == 'one':
            self._switch_state('inactive')

    def _release_switches_2(self):
        self.debug_log('Switches_2 has passed the release time and is now '
                       'releases')
        self._switches_2_active = None
        if self._switches_1_active and self._state == 'both':
            self._switch_state('one')
        elif self._state == 'one':
            self._switch_state('inactive')

    def _switch_state(self, state):
        """Post events for current step."""
        if state not in self.states:
            raise ValueError("Received invalid state: {}".format(state))

        if state == self.state:
            return

        self._state = state
        self.debug_log("New State: %s", state)

        for event in self.config['events_when_{}'.format(state)]:
            self.machine.events.post(event)
            '''event: (combo_switch)_(state)
            desc: Combo switch (name) changed to state (state).

            Note that these events can be overridden in a combo switch's
            config.

            Valid states are: *inactive*, *both*, or *one*.

            ..rubric:: both

            A switch from group 1 and group 2 are both active at the
            same time, having been pressed within the ``max_offset_time:`` and
            being active for at least the ``hold_time:``.

            ..rubric:: one

            Either switch 1 or switch 2 has been released for at
            least the ``release_time:`` but the other switch is still active.

            ..rubric:: inactive

            Both switches are inactive.

            '''

            '''event: flipper_cancel
Esempio n. 5
0
class Blinkenlight(SystemWideDevice):
    """A light that alternates between several different colors."""

    config_section = 'blinkenlights'
    collection = 'blinkenlights'
    class_label = 'blinkenlight'

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

        self._colors = []
        self.delay = DelayManager(machine)
        self._color_duration = None
        self._cycle_duration = None
        self.num_colors = 0
        self._light_key = 'blinkenlight_{}'.format(
            self.name)  # cache the key as string operations as expensive

    def load_config(self, config: dict):
        """Load config."""
        super().load_config(config)

        self._color_duration = self.config['color_duration']
        self._cycle_duration = self.config['cycle_duration']
        if (self._color_duration is None and self._cycle_duration is None) or \
           (self._color_duration is not None and self._cycle_duration is not None):
            self.raise_config_error(
                "Either color_duration or cycle_duration must be specified, but not both.",
                1)

    @property
    def light(self):
        """Return the light this blinkenlight controls."""
        return self.config['light']

    @property
    def num_colors_in_cycle(self):
        """Similar to num_colors, but adds 1 for the "off" color if there is one between cycles."""
        return self.num_colors + (1 if self._off_between_cycles() else 0)

    def add_color(self, color, key, priority):
        """Add a color to the blinkenlight."""
        # check if this key already exists. If it does, replace it with the incoming color/priority
        self._colors = [x for x in self._colors if x[1] != key]
        self._colors.append((color, key, priority))
        self.info_log('Color {} with key {} added'.format(color, key))
        self._update_light()

    def remove_all_colors(self):
        """Remove all colors from the blinkenlight."""
        self._colors.clear()
        self.info_log('All colors removed')
        self._update_light()

    def remove_color_with_key(self, key):
        """Remove a color with a given key from the blinkenlight."""
        old_len = len(self._colors)
        self._colors = [x for x in self._colors if x[1] != key]
        if len(self._colors) != old_len:
            self.info_log('Color removed with key {}'.format(key))
            self._update_light()

    def _update_light(self):
        """Update the underlying light."""
        self.delay.clear()
        self.num_colors = len(self._colors)
        if self._colors:
            self._sort_colors()
        self._perform_step()

    def _sort_colors(self):
        """Sort the blinkenlights colors by their priority."""
        self._colors = sorted(self._colors, key=itemgetter(2),
                              reverse=True)  # priority is item 2 of the tuple

    def _off_between_cycles(self):
        return (self.config['off_when_multiple']
                and len(self._colors) > 1) or (len(self._colors) == 1)

    def _get_time_between_colors_ms(self):
        if self._cycle_duration:
            delay = self._cycle_duration / self.num_colors_in_cycle
        else:
            delay = self._color_duration
        if delay < 1:
            delay = 1
        return delay

    def _get_total_cycle_ms(self):
        if self._cycle_duration:
            return self._cycle_duration
        return self._color_duration * self.num_colors_in_cycle

    def _get_current_color(self):
        now = self.machine.clock.get_time()
        offset_ms = (now * 1000) % self._get_total_cycle_ms()
        color_i = int(offset_ms / self._get_time_between_colors_ms() + 0.5)
        if color_i >= self.num_colors:
            return RGBColor("off")
        return self._colors[color_i][0]

    def _perform_step(self):
        if self.num_colors == 0:
            self.light.remove_from_stack_by_key(self._light_key)
            return
        current_color = self._get_current_color()
        color_duration_ms = self._get_time_between_colors_ms()
        cycle_ms = self._get_total_cycle_ms()
        self.light.color(current_color,
                         priority=self.config['priority'],
                         key=self._light_key)
        delay_ms = color_duration_ms - (
            (self.machine.clock.get_time() * 1000) %
            cycle_ms) % color_duration_ms
        self.delay.add(delay_ms, self._perform_step, name="perform_step")
Esempio n. 6
0
class Spinner(EnableDisableMixinSystemWideDevice, SystemWideDevice):

    """Represents a spinner or spinner group in a pinball machine."""

    config_section = 'spinners'
    collection = 'spinners'
    class_label = 'spinner'

    __slots__ = ["hits", "_active_ms", "_active", "_idle", "delay"]

    def __init__(self, machine: "MachineController", name: str) -> None:
        """Initialise spinner device."""
        super().__init__(machine, name)
        self._active = False
        self._idle = True
        self._active_ms = None
        self.hits = None
        self.delay = DelayManager(machine)
        self.enabled = True  # Default to enabled

    async def _initialize(self):
        await super()._initialize()
        self.hits = 0
        # Cache this value because it's used a lot in rapid succession
        self._active_ms = self.config['active_ms']
        # Can't read the switch until the switch controller is set up
        self.machine.events.add_handler('init_phase_4',
                                        self._register_switch_handlers, priority=1)

    def _register_switch_handlers(self, **kwargs):
        del kwargs

        labels = dict(zip(self.config['switches'], self.config['labels'])) if self.config['labels'] else None
        for switch in self.config['switches']:
            callback_kwargs = {"label": labels[switch]} if labels else None
            # register for notification of switch active state
            self.machine.switch_controller.add_switch_handler_obj(
                switch,
                self._update_state_from_switch, 1, callback_kwargs=callback_kwargs)

    def _update_state_from_switch(self, **kwargs):
        if not self.enabled:
            return
        label = kwargs.get("label")
        if not self._active:
            self.machine.events.post("spinner_{}_active".format(self.name), label=label)
            '''event: spinner_(name)_active
            desc: The idle spinner (name) was just hit and became active.

            This event will post whenever a spinner switch is hit and the spinner
            is not already active.

            args:
            label: The label of the switch that triggered the activation
            '''
            if label:
                self.machine.events.post("spinner_{}_{}_active".format(self.name, label))
                '''event: spinner_(name)_(label)_active
                desc: The idle spinner (name) was just hit and became active.

                This event will post whenever a spinner switch is hit and the spinner
                is not already active, but only if labels are defined for the spinner.
                '''
            self._active = True
            self._idle = False
        self.hits += 1
        self.machine.events.post("spinner_{}_hit".format(self.name), hits=self.hits, label=label)
        '''event: spinner_(name)_hit
        desc: The spinner (name) was just hit.

        This event will post whenever a spinner switch is hit.

        args:
        hits: The number of switch hits the spinner has had since it became active
        label: The label of the switch that was hit
        '''
        if label:
            self.machine.events.post("spinner_{}_{}_hit".format(self.name, label))
            '''event: spinner_(name)_(label)_hit
            desc: The spinner (name) was just hit on the switch labelled (label).

            This event will post whenever a spinner switch is hit and labels
            are defined for the spinner
            '''
        self.delay.clear()
        self.delay.add(self._active_ms, self._deactivate)

    def _deactivate(self, **kwargs):
        """Post an 'inactive' event after no switch hits for the active_ms duration."""
        del kwargs
        self.machine.events.post("spinner_{}_inactive".format(self.name), hits=self.hits)
        '''event: spinner_(name)_inactive
        desc: The spinner (name) is no longer receiving hits

        This event will post whenever a spinner has not received hits and
        its active_ms has timed out.

        args:
        hits: The number of switch hits the spinner had while it was active
        '''
        self._active = False
        if self.config['idle_ms']:
            self.delay.add(self.config['idle_ms'], self._on_idle)
            if self.config['reset_when_inactive']:
                self.hits = 0
        else:
            self._idle = True

    def _on_idle(self, **kwargs):
        """Post an 'idle' event if the spinner has been inactive for the idle_ms duration."""
        del kwargs
        self.machine.events.post("spinner_{}_idle".format(self.name), hits=self.hits)
        '''event: spinner_(name)_idle
        desc: The spinner (name) is now idle

        This event will post whenever a spinner has not received hits and
        its idle_ms has timed out. If no idle_ms is defined, this event
        will not post.

        args:
        hits: The number of switch hits the spinner had while it was active
        '''
        self.hits = 0
        self._idle = True

    def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str = None):
        """Validate and parse spinner config."""
        config = super().validate_and_parse_config(config, is_mode_config, debug_prefix)
        for switch in config['switch']:
            if switch not in config['switches']:
                config['switches'].append(switch)

        if config['labels'] and len(config['labels']) != len(config['switches']):
            raise ConfigFileError("Spinner labels must be the same number as switches", 1, self.name)

        return config

    @property
    def active(self):
        """Return whether the spinner is actively spinning."""
        return self._active

    @property
    def idle(self):
        """Return whether the spinner is idle."""
        return self._idle
Esempio n. 7
0
class ComboSwitch(SystemWideDevice, ModeDevice):
    """Combo Switch device."""

    config_section = 'combo_switches'
    collection = 'combo_switches'
    class_label = 'combo_switch'

    __slots__ = [
        "states", "_state", "_switches_1_active", "_switches_2_active",
        "delay", "_switch_handlers"
    ]

    def __init__(self, machine, name):
        """Initialize Combo Switch."""
        super().__init__(machine, name)
        self.states = ['inactive', 'both', 'one']
        self._state = 'inactive'
        self._switches_1_active = False
        self._switches_2_active = False

        self.delay = DelayManager(self.machine)
        self._switch_handlers = []

    def validate_and_parse_config(self,
                                  config: dict,
                                  is_mode_config: bool,
                                  debug_prefix: str = None) -> dict:
        """Validate and parse config."""
        config = super().validate_and_parse_config(config, is_mode_config,
                                                   debug_prefix)

        for state in self.states + ["switches_1", "switches_2"]:
            if not config['events_when_{}'.format(state)]:
                config['events_when_{}'.format(state)] = [
                    "{}_{}".format(self.name, state)
                ]

        return config

    async def device_added_system_wide(self):
        """Add event handlers."""
        await super().device_added_system_wide()
        self._add_switch_handlers()

    def device_loaded_in_mode(self, mode: Mode, player: Player):
        """Add event handlers."""
        self._add_switch_handlers()

    def _add_switch_handlers(self):
        if self.config['tag_1']:
            for tag in self.config['tag_1']:
                for switch in self.machine.switches.items_tagged(tag):
                    self.config['switches_1'].add(switch)

        if self.config['tag_2']:
            for tag in self.config['tag_2']:
                for switch in self.machine.switches.items_tagged(tag):
                    self.config['switches_2'].add(switch)

        self._register_switch_handlers()

    @property
    def state(self):
        """Return current state."""
        return self._state

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

    def device_removed_from_mode(self, mode):
        """Mode ended.

        Args:
        ----
            mode: mode which stopped
        """
        del mode

        self._remove_switch_handlers()
        self._kill_delays()

    def _register_switch_handlers(self):
        for switch in self.config['switches_1']:
            self._switch_handlers.append(
                switch.add_handler(self._switch_1_went_active,
                                   state=1,
                                   return_info=True))
            self._switch_handlers.append(
                switch.add_handler(self._switch_1_went_inactive,
                                   state=0,
                                   return_info=True))

        for switch in self.config['switches_2']:
            self._switch_handlers.append(
                switch.add_handler(self._switch_2_went_active,
                                   state=1,
                                   return_info=True))
            self._switch_handlers.append(
                switch.add_handler(self._switch_2_went_inactive,
                                   state=0,
                                   return_info=True))

    def _remove_switch_handlers(self):
        self.machine.switch_controller.remove_switch_handler_by_keys(
            self._switch_handlers)
        self._switch_handlers = []

    def _kill_delays(self):
        self.delay.clear()

    def _switch_1_went_active(self, switch_name, **kwargs):
        del kwargs
        self.debug_log('A switch from switches_1 just went active')
        self.delay.remove('switch_1_inactive')

        if self._switches_1_active:
            return

        if not self.config['hold_time']:
            self._activate_switches_1(switch_name)
        else:
            self.delay.add_if_doesnt_exist(
                self.config['hold_time'],
                partial(self._activate_switches_1, switch_name),
                'switch_1_active')

    def _switch_2_went_active(self, switch_name, **kwargs):
        del kwargs
        self.debug_log('A switch from switches_2 just went active')
        self.delay.remove('switch_2_inactive')

        if self._switches_2_active:
            return

        if not self.config['hold_time']:
            self._activate_switches_2(switch_name)
        else:
            self.delay.add_if_doesnt_exist(
                self.config['hold_time'],
                partial(self._activate_switches_2, switch_name),
                'switch_2_active')

    def _switch_1_went_inactive(self, switch_name, **kwargs):
        del kwargs
        self.debug_log('A switch from switches_1 just went inactive')
        for switch in self.config['switches_1']:
            if switch.state:
                # at least one switch is still active
                return

        self.delay.remove('switch_1_active')

        if not self.config['release_time']:
            self._release_switches_1(switch_name)
        else:
            self.delay.add_if_doesnt_exist(
                self.config['release_time'],
                partial(self._release_switches_1, switch_name),
                'switch_1_inactive')

    def _switch_2_went_inactive(self, switch_name, **kwargs):
        del kwargs
        self.debug_log('A switch from switches_2 just went inactive')
        for switch in self.config['switches_2']:
            if switch.state:
                # at least one switch is still active
                return

        self.delay.remove('switch_2_active')

        if not self.config['release_time']:
            self._release_switches_2(switch_name)
        else:
            self.delay.add_if_doesnt_exist(
                self.config['release_time'],
                partial(self._release_switches_2, switch_name),
                'switch_2_inactive')

    def _activate_switches_1(self, switch_name):
        self.debug_log('Switches_1 has passed the hold time and is now '
                       'active')
        self._switches_1_active = self.machine.clock.get_time()
        self.delay.remove("switch_2_only")

        if self._switches_2_active:
            if (self.config['max_offset_time'] >= 0
                    and (self._switches_1_active - self._switches_2_active >
                         self.config['max_offset_time'])):

                self.debug_log(
                    "Switches_2 is active, but the "
                    "max_offset_time=%s which is largest than when "
                    "a Switches_2 switch was first activated, so "
                    "the state will not switch to 'both'",
                    self.config['max_offset_time'])

                return

            self._switch_state('both', group=1, switch=switch_name)
        elif self.config['max_offset_time'] >= 0:
            self.delay.add_if_doesnt_exist(self.config['max_offset_time'] *
                                           1000,
                                           self._post_only_one_active_event,
                                           "switch_1_only",
                                           number=1)

    def _activate_switches_2(self, switch_name):
        self.debug_log('Switches_2 has passed the hold time and is now '
                       'active')
        self._switches_2_active = self.machine.clock.get_time()
        self.delay.remove("switch_1_only")

        if self._switches_1_active:
            if (self.config['max_offset_time'] >= 0
                    and (self._switches_2_active - self._switches_1_active >
                         self.config['max_offset_time'])):
                self.debug_log(
                    "Switches_2 is active, but the "
                    "max_offset_time=%s which is largest than when "
                    "a Switches_2 switch was first activated, so "
                    "the state will not switch to 'both'",
                    self.config['max_offset_time'])
                return

            self._switch_state('both', group=2, switch=switch_name)
        elif self.config['max_offset_time'] >= 0:
            self.delay.add_if_doesnt_exist(self.config['max_offset_time'] *
                                           1000,
                                           self._post_only_one_active_event,
                                           "switch_2_only",
                                           number=2)

    def _post_only_one_active_event(self, number):
        for event in self.config['events_when_switches_{}'.format(number)]:
            self.machine.events.post(event)

    def _release_switches_1(self, switch_name):
        self.debug_log('Switches_1 has passed the release time and is now '
                       'releases')
        self._switches_1_active = None
        if self._switches_2_active and self._state == 'both':
            self._switch_state('one', group=1, switch=switch_name)
        elif self._state == 'one':
            self._switch_state('inactive', group=1, switch=switch_name)

    def _release_switches_2(self, switch_name):
        self.debug_log('Switches_2 has passed the release time and is now '
                       'releases')
        self._switches_2_active = None
        if self._switches_1_active and self._state == 'both':
            self._switch_state('one', group=2, switch=switch_name)
        elif self._state == 'one':
            self._switch_state('inactive', group=2, switch=switch_name)

    def _switch_state(self, state, group, switch):
        """Post events for current step."""
        if state not in self.states:
            raise ValueError("Received invalid state: {}".format(state))

        if state == self.state:
            return

        self._state = state
        self.debug_log("New State: %s", state)

        for event in self.config['events_when_{}'.format(state)]:
            self.machine.events.post(event,
                                     triggering_group=group,
                                     triggering_switch=switch)
            '''event: (name)_one
            config_attribute: events_when_one

            desc: Combo switch (name) changed to state one.

            Either switch 1 or switch 2 has been released for at
            least the ``release_time:`` but the other switch is still active.
            '''
            '''event: (name)_both
            config_attribute: events_when_both

            desc: Combo switch (name) changed to state both.

            A switch from group 1 and group 2 are both active at the
            same time, having been pressed within the ``max_offset_time:`` and
            being active for at least the ``hold_time:``.
            '''
            '''event: (name)_inactive
            config_attribute: events_when_inactive

            desc: Combo switch (name) changed to state inactive.

            Both switches are inactive.
            '''
            '''event: (name)_switches_1
            config_attribute: events_when_switches_1

            desc: Combo switch (name) changed to state switches_1.

            Only switches_1 is active. max_offset_time has passed and this hit
            cannot become both later on. Only emited when ``max_offset_time:``
            is defined.
            '''
            '''event: (name)_switches_2