예제 #1
0
    def __init__(self, machine: MachineController) -> None:
        """Initialise switch controller."""
        super().__init__(machine)
        self.registered_switches = CaseInsensitiveDict()        # type: Dict[str, List[RegisteredSwitch]]
        # Dictionary of switches and states that have been registered for
        # callbacks.

        self._timed_switch_handler_delay = None                 # type: Any

        self.active_timed_switches = defaultdict(list)          # type: Dict[float, List[TimedSwitchHandler]]
        # Dictionary of switches that are currently in a state counting ms
        # waiting to notify their handlers. In other words, this is the dict
        # that tracks current switches for things like "do foo() if switch bar
        # is active for 100ms."

        self.switches = CaseInsensitiveDict()                   # type: Dict[str, SwitchState]
        # Dictionary which holds the master list of switches as well as their
        # current states. State here does factor in whether a switch is NO or
        # NC so 1 = active and 0 = inactive.

        # register for events
        self.machine.events.add_async_handler('init_phase_2', self._initialize_switches, 1000)
        # priority 1000 so this fires first

        self.machine.events.add_handler('machine_reset_phase_3', self.log_active_switches)

        self.monitors = list()      # type: List[Callable[[MonitoredSwitchChange], None]]
예제 #2
0
    def create_machine_var(self,
                           name,
                           value=0,
                           persist=False,
                           expire_secs=None,
                           silent=False):
        """Create a new machine variable.

        Args:
            name: String name of the variable.
            value: The value of the variable. This can be any Type.
            persist: Boolean as to whether this variable should be saved to
                disk so it's available the next time MPF boots.
            expire_secs: Optional number of seconds you'd like this variable
                to persist on disk for. When MPF boots, if the expiration time
                of the variable is in the past, it will be loaded with a value
                of 0. For example, this lets you write the number of credits on
                the machine to disk to persist even during power off, but you
                could set it so that those only stay persisted for an hour.
        """
        var = CaseInsensitiveDict()

        var['value'] = value
        var['persist'] = persist
        var['expire_secs'] = expire_secs

        self.machine_vars[name] = var

        if not silent:
            self.set_machine_var(name, value, force_events=True)
예제 #3
0
    def configure_machine_var(self,
                              name: str,
                              persist: bool,
                              expire_secs: int = None) -> None:
        """Create a new machine variable.

        Args:
            name: String name of the variable.
            persist: Boolean as to whether this variable should be saved to
                disk so it's available the next time MPF boots.
            expire_secs: Optional number of seconds you'd like this variable
                to persist on disk for. When MPF boots, if the expiration time
                of the variable is in the past, it will be loaded with a value
                of 0. For example, this lets you write the number of credits on
                the machine to disk to persist even during power off, but you
                could set it so that those only stay persisted for an hour.
        """
        if name not in self.machine_vars:
            var = CaseInsensitiveDict()

            var['value'] = None
            var['persist'] = persist
            var['expire_secs'] = expire_secs
            self.machine_vars[name] = var
        else:
            self.machine_vars[name]['persist'] = persist
            self.machine_vars[name]['expire_sec'] = expire_secs
예제 #4
0
    def __init__(self, machine):
        """Initialize ExtraBallManager"""
        super().__init__(machine)

        self.extra_balls = CaseInsensitiveDict()

        self.config = self.machine.config_validator.validate_config(
            config_spec='global_extra_ball_settings',
            source=self.machine.config['global_extra_ball_settings'])

        self.enabled = self.config['enabled']

        self.events_only = self.config['events_only']

        self.machine.events.add_handler('player_add_success',
                                        self._player_added)
        self.machine.events.add_handler('player_turn_start',
                                        self._player_turn_start)
        self.machine.events.add_handler('player_turn_stop',
                                        self._player_turn_stop)
        self.machine.events.add_handler('award_extra_ball',
                                        self.award)
        '''event: award_extra_ball

        desc: This is an event you can post which will immediately award the
        player an extra ball (assuming they're within the limits of max
        extra balls, etc.). This event will in turn post the
        extra_ball_awarded event if the extra ball is able to be awarded.

        Note that if you want to just light the extra ball, but not award it
        right away, then use the :doc:`award_lit_extra_ball` event instead.

        Also note that if an extra ball is lit, this event will NOT unlight
        or decrement the lit extra ball count. If you want to do that, use the
        :doc:`award_lit_extra_ball` instead.

        '''
        self.machine.events.add_handler('award_lit_extra_ball',
                                        self.award_lit)
        '''event: award_lit_extra_ball

        desc: This event will award an extra ball if extra ball is lit. If the
        player has no lit extra balls, then this event will have no effect.

        This is a good event to use in your extra ball mode or shot to post
        to collect the lit extra ball. It will in turn post the
        :doc:`extra_ball_awarded` event (assuming the player has not
        exceeded any configured limits for max extra balls).

        If you just want to award an extra ball regardless of whether the
        player has one lit, use the :doc:`award_extra_ball` event instead.

        '''

        self.machine.events.add_handler('light_extra_ball',
                                        self.light)

        '''event: light_extra_ball
예제 #5
0
    def _write_machine_var_to_disk(self, name: str) -> None:
        """Write value to disk."""
        if self.machine_vars[name]['persist'] and self.config['mpf']['save_machine_vars_to_disk']:
            disk_var = CaseInsensitiveDict()
            disk_var['value'] = self.machine_vars[name]['value']

            if self.machine_vars[name]['expire_secs']:
                disk_var['expire'] = self.clock.get_time() + self.machine_vars[name]['expire_secs']

            self.machine_var_data_manager.save_key(name, disk_var)
예제 #6
0
파일: mode.py 프로젝트: ngksternhagen/mpf
    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
예제 #7
0
    def register_asset_class(self, asset_class: str, attribute: str,
                             config_section: str, disk_asset_section: str,
                             path_string: str, extensions: Iterable[str],
                             priority: int, pool_config_section: str) -> None:
        """Register a a type of assets to be controlled by the AssetManager.

        Args:
            asset_class: Reference to the class you want to register, based on
                mc.core.assets.Asset. e.g. mc.assets.images.ImageClass
            attribute: String of the name of the attribute dict that will be
                added to the main MpfMc instance. e.g. 'images' means that
                the dict of image names to image asset class instances will be
                at self.machine.images.
            config_section: String name of this assets section in the config
                files. e.g. 'images'
            disk_asset_section: String name of the section which holds settings
                for assets that are loaded from disk.
            path_string: String name of the setting from mpf-mc:paths: which
                controls the name of the folder that will hold this type of
                assets in the machine folder. e.g. 'images
            extensions: Tuple of strings, with no dots, of the types of file
                extensions that are valid for this type of asset. e.g. ('jpg',
                'gif', 'png')
            priority: Integer of the relative priority of this asset class as
                compared to other asset classes. This affects the order that
                asset objects are created and loaded (when there's a tie)
                because some asset classes depend on others to exist first.
                e.g. 'slide_shows' assets need 'images', 'videos', and 'sounds'
                to exist. Higher number is first.
            pool_config_section: String which specifies the config file
                section for associated asset groups.
        """
        if not hasattr(self.machine, attribute):
            # some assets of different classes use the same mc attribute, like
            # images and animated_images
            setattr(self.machine, attribute, CaseInsensitiveDict())

        ac = AssetClass(attribute=attribute,
                        cls=asset_class,
                        path_string=path_string,
                        config_section=config_section,
                        disk_asset_section=disk_asset_section,
                        extensions=extensions,
                        priority=priority,
                        pool_config_section=pool_config_section,
                        defaults=self._get_asset_class_defaults(
                            disk_asset_section, self.machine.machine_config))

        self._asset_classes.append(ac)
        self._asset_classes.sort(key=lambda x: x.priority, reverse=True)
예제 #8
0
    def __init__(self, machine, index):
        """Initialise player."""
        # use self.__dict__ below since __setattr__ would make these player vars
        self.__dict__['log'] = logging.getLogger("Player")
        self.__dict__['machine'] = machine
        self.__dict__['vars'] = CaseInsensitiveDict()

        number = index + 1

        self.log.debug("Creating new player: Player %s. (player index '%s')",
                       number, index)

        # Set these after the player_add_success event so any player monitors
        # get notification of the new player before they start seeing variable
        # changes for it.
        self.vars['index'] = index
        '''player_var: index

        desc: The index of this player, starting with 0. For example, Player
        1 has an index of 0, Player 2 has an index of 1, etc.

        If you want to get the player number, use the "number" player variable
        instead.
        '''

        self.vars['number'] = number
        '''player_var: number

        desc: The number of the player, beginning with 1. (e.g. Player 1 has
        a number of "1", Player 2 is "2", etc.
        '''

        self.machine.events.post('player_add_success',
                                 player=self,
                                 num=number,
                                 callback=self._player_add_done)
        '''event: player_add_success

        desc: A new player was just added to this game

        args:

        player: A reference to the instance of the Player() object.

        num: The number of the player that was just added. (e.g. Player 1 will
        have *num=1*, Player 4 will have *num=4*, etc.)

        '''
        self._load_initial_player_vars()
예제 #9
0
    def __init__(self, machine):
        """Initialise switch controller."""
        super().__init__(machine)
        self.registered_switches = CaseInsensitiveDict()
        # Dictionary of switches and states that have been registered for
        # callbacks.

        self._timed_switch_handler_delay = None

        self.active_timed_switches = defaultdict(list)
        # Dictionary of switches that are currently in a state counting ms
        # waiting to notify their handlers. In other words, this is the dict
        # that tracks current switches for things like "do foo() if switch bar
        # is active for 100ms."

        self.switches = CaseInsensitiveDict()
        # Dictionary which holds the master list of switches as well as their
        # current states. State here does factor in whether a switch is NO or
        # NC so 1 = active and 0 = inactive.

        self.switch_event_active = (
            self.machine.config['mpf']['switch_event_active'])
        self.switch_event_inactive = (
            self.machine.config['mpf']['switch_event_inactive'])
        self.switch_tag_event = (
            self.machine.config['mpf']['switch_tag_event'])

        # register for events
        self.machine.events.add_handler('init_phase_2',
                                        self._initialize_switches, 1000)
        # priority 1000 so this fires first

        self.machine.events.add_handler('machine_reset_phase_3',
                                        self.log_active_switches)

        self.monitors = list()
예제 #10
0
파일: utils.py 프로젝트: vgrillot/mpf-mc
def load_machine_config(config_file_list, machine_path,
                        config_path='config', existing_config=None):

    machine_config = dict()

    for num, config_file in enumerate(config_file_list):
        if not existing_config:
            machine_config = CaseInsensitiveDict()
        else:
            machine_config = existing_config

        if not (config_file.startswith('/') or
                config_file.startswith('\\')):
            config_file = os.path.join(machine_path, config_path,
                                       config_file)

        machine_config = Util.dict_merge(machine_config,
            ConfigProcessor.load_config_file(config_file, 'machine', ignore_unknown_sections=True))

    return machine_config
예제 #11
0
    def __init__(self, machine, index):
        """Initialise player."""
        # use self.__dict__ below since __setattr__ would make these player vars
        self.__dict__['log'] = logging.getLogger("Player")
        self.__dict__['machine'] = machine
        self.__dict__['vars'] = CaseInsensitiveDict()
        self.__dict__['_events_enabled'] = False

        number = index + 1

        self.log.debug("Creating new player: Player %s. (player index '%s')",
                       number, index)

        # Set these after the player_added event so any player monitors
        # get notification of the new player before they start seeing variable
        # changes for it.
        self.vars['index'] = index
        '''player_var: index

        desc: The index of this player, starting with 0. For example, Player
        1 has an index of 0, Player 2 has an index of 1, etc.

        If you want to get the player number, use the "number" player variable
        instead.
        '''

        self.vars['number'] = number
        '''player_var: number

        desc: The number of the player, beginning with 1. (e.g. Player 1 has
        a number of "1", Player 2 is "2", etc.
        '''

        self._load_initial_player_vars()

        # Set the initial player score to 0
        self.__setattr__("score", 0)
        '''player_var: score
예제 #12
0
    def __init__(self, options, config: MpfMcConfig, thread_stopper=None):

        self.log = logging.getLogger('mpfmc')
        self.log.info("Mission Pinball Framework Media Controller v%s",
                      __version__)
        self.log.info("Mission Pinball Framework Game Engine v%s",
                      __mpfversion__)

        if (__version__.split('.')[0] != __mpfversion__.split('.')[0]
                or __version__.split('.')[1] != __mpfversion__.split('.')[1]):

            self.log.error(
                "MPF MC and MPF Game engines must be same "
                "major.minor versions. You have MPF v%s and MPF-MC"
                " v%s", __mpfversion__, __version__)

            raise ValueError(
                "MPF MC and MPF Game engines must be same "
                "major.minor versions. You have MPF v{} and MPF-MC"
                " v{}".format(__mpfversion__, __version__))

        super().__init__()

        self.options = options
        self.machine_path = config.get_machine_path()
        self.log.info("Machine path: %s", self.machine_path)

        # load machine into path to load modules
        if self.machine_path not in sys.path:
            sys.path.append(self.machine_path)
        self.mc_config = config
        self.config_validator = ConfigValidator(self, config.get_config_spec())
        self.machine_config = self.mc_config.get_machine_config()
        self.config = self.machine_config

        self.clock = Clock
        # pylint: disable-msg=protected-access
        self.log.info("Starting clock at %sHz", Clock._max_fps)
        self._boot_holds = set()
        self.is_init_done = threading.Event()
        self.mpf_path = os.path.dirname(mpf.__file__)
        self.modes = CaseInsensitiveDict()
        self.player_list = list()
        self.player = None
        self.num_players = 0
        self.bcp_client_connected = False
        self.placeholder_manager = McPlaceholderManager(self)
        self.settings = McSettingsController(self)

        self.animation_configs = dict()
        self.active_slides = dict()
        self.custom_code = list()

        self.register_boot_hold('init')
        self.displays = DeviceCollection(self, "displays", "displays")
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.monitors = dict()
        self.targets = dict()
        """Dict which contains all the active slide frames in the machine that
        a slide can target. Will always contain an entry called 'default'
        which will be used if a slide doesn't specify targeting.
        """

        self.keyboard = None
        self.dmds = []
        self.rgb_dmds = []
        self.crash_queue = queue.Queue()
        self.ticks = 0
        self.start_time = 0
        self.debug_refs = []

        MYPY = False  # NOQA
        if MYPY:  # pragma: no cover
            self.videos = None  # type: Dict[str, VideoAsset]

        if thread_stopper:
            self.thread_stopper = thread_stopper
        else:
            self.thread_stopper = threading.Event()

        # Core components
        self.events = EventManager(self)
        self.mode_controller = ModeController(self)
        create_config_collections(
            self, self.machine_config['mpf-mc']['config_collections'])
        self._preprocess_config(self.config)

        self.config_processor = ConfigProcessor(self)
        self.transition_manager = TransitionManager(self)
        self.effects_manager = EffectsManager(self)

        self._set_machine_path()

        self._load_font_paths()

        # Initialize the sound system (must be done prior to creating the AssetManager).
        # If the sound system is not available, do not load any other sound-related modules.
        if SoundSystem is None or self.options.get("no_sound"):
            self.sound_system = None
        else:
            self.sound_system = SoundSystem(self)
            if self.sound_system.audio_interface is None:
                self.sound_system = None

        self.asset_manager = ThreadedAssetManager(self)
        self.bcp_processor = BcpProcessor(self)

        # Asset classes
        ImageAsset.initialize(self)
        VideoAsset.initialize(self)
        BitmapFontAsset.initialize(self)

        self._initialise_sound_system()

        self.clock.schedule_interval(self._check_crash_queue, 1)

        self.events.add_handler("client_connected", self._create_dmds)
        self.events.add_handler("player_turn_start", self.player_start_turn)

        self.create_machine_var('mpfmc_ver', __version__)
        # force setting it here so we have it before MPF connects
        self.receive_machine_var_update('mpfmc_ver', __version__, 0, True)
예제 #13
0
class MachineController(LogMixin):
    """Base class for the Machine Controller object.

    The machine controller is the main entity of the entire framework. It's the
    main part that's in charge and makes things happen.

    Args:
        options(dict): A dictionary of options built from the command line options
            used to launch mpf.py.
        machine_path: The root path of this machine_files folder
    """

    # pylint: disable-msg=too-many-statements
    def __init__(self, mpf_path: str, machine_path: str,
                 options: dict) -> None:
        """Initialize machine controller."""
        super().__init__()
        self.log = logging.getLogger("Machine")  # type: Logger
        self.log.info("Mission Pinball Framework Core Engine v%s", __version__)

        self.log.info("Command line arguments: %s", options)
        self.options = options
        self.config_processor = ConfigProcessor()

        self.log.info("MPF path: %s", mpf_path)
        self.mpf_path = mpf_path

        self.log.info("Machine path: %s", machine_path)
        self.machine_path = machine_path

        self.verify_system_info()
        self._exception = None  # type: Any
        self._boot_holds = set()  # type: Set[str]
        self.is_init_done = None  # type: asyncio.Event

        self._done = False
        self.monitors = dict()  # type: Dict[str, Set[Callable]]
        self.plugins = list()  # type: List[Any]
        self.scriptlets = list()  # type: List[Scriptlet]
        self.modes = DeviceCollection(self, 'modes',
                                      None)  # type: Dict[str, Mode]
        self.game = None  # type: Game
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.machine_var_data_manager = None  # type: DataManager
        self.thread_stopper = threading.Event()

        self.config = None  # type: Any

        # add some type hints
        MYPY = False  # noqa
        if MYPY:  # pragma: no cover
            # controllers
            self.events = None  # type: EventManager
            self.switch_controller = None  # type: SwitchController
            self.mode_controller = None  # type: ModeController
            self.settings = None  # type: SettingsController
            self.bcp = None  # type: Bcp
            self.asset_manager = None  # type: BaseAssetManager
            self.ball_controller = None  # type: BallController
            self.show_controller = None  # type: ShowController
            self.placeholder_manager = None  # type: PlaceholderManager
            self.device_manager = None  # type: DeviceManager
            self.auditor = None  # type: Auditor
            self.tui = None  # type: TextUi
            self.service = None  # type: ServiceController

            # devices
            self.autofires = None  # type: DeviceCollectionType[str, AutofireCoil]
            self.shows = None  # type: DeviceCollectionType[str, Show]
            self.shots = None  # type: DeviceCollectionType[str, Shot]
            self.shot_groups = None  # type: DeviceCollectionType[str, ShotGroup]
            self.switches = None  # type: DeviceCollectionType[str, Switch]
            self.coils = None  # type: DeviceCollectionType[str, Driver]
            self.lights = None  # type: DeviceCollectionType[str, Light]
            self.ball_devices = None  # type: DeviceCollectionType[str, BallDevice]
            self.accelerometers = None  # type: DeviceCollectionType[str, Accelerometer]
            self.playfield = None  # type: Playfield
            self.playfields = None  # type: DeviceCollectionType[str, Playfield]
            self.counters = None  # type: DeviceCollectionType[str, Counter]
            self.sequences = None  # type: DeviceCollectionType[str, Sequence]
            self.accruals = None  # type: DeviceCollectionType[str, Accrual]
            self.drop_targets = None  # type: DeviceCollectionType[str, DropTarget]
            self.servos = None  # type: DeviceCollectionType[str, Servo]
            self.segment_displays = None  # type: DeviceCollectionType[str, SegmentDisplay]

        self._set_machine_path()

        self.config_validator = ConfigValidator(self)

        self._load_config()
        self.machine_config = self.config  # type: Any
        self.configure_logging(
            'Machine', self.config['logging']['console']['machine_controller'],
            self.config['logging']['file']['machine_controller'])

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

        self.hardware_platforms = dict(
        )  # type: Dict[str, SmartVirtualHardwarePlatform]
        self.default_platform = None  # type: SmartVirtualHardwarePlatform

        self.clock = self._load_clock()
        self.stop_future = asyncio.Future(
            loop=self.clock.loop)  # type: asyncio.Future

    @asyncio.coroutine
    def initialise_core_and_hardware(self) -> Generator[int, None, None]:
        """Load core modules and hardware."""
        self._boot_holds = set()  # type: Set[str]
        self.is_init_done = asyncio.Event(loop=self.clock.loop)
        self.register_boot_hold('init')
        self._load_hardware_platforms()

        self._load_core_modules()
        # order is specified in mpfconfig.yaml

        self._validate_config()

        # This is called so hw platforms have a chance to register for events,
        # and/or anything else they need to do with core modules since
        # they're not set up yet when the hw platforms are constructed.
        yield from self._initialize_platforms()

    @asyncio.coroutine
    def initialise(self) -> Generator[int, None, None]:
        """Initialise machine."""
        yield from self.initialise_core_and_hardware()

        self._initialize_credit_string()

        self._register_config_players()
        self._register_system_events()
        self._load_machine_vars()
        yield from self._run_init_phases()
        self._init_phases_complete()

        yield from self._start_platforms()

        # wait until all boot holds were released
        yield from self.is_init_done.wait()
        yield from self.init_done()

    def _exception_handler(self, loop, context):  # pragma: no cover
        """Handle asyncio loop exceptions."""
        # call original exception handler
        loop.set_exception_handler(None)
        loop.call_exception_handler(context)

        # remember exception
        self._exception = context
        self.stop()

    # pylint: disable-msg=no-self-use
    def _load_clock(self) -> ClockBase:  # pragma: no cover
        """Load clock and loop."""
        clock = ClockBase(self)
        clock.loop.set_exception_handler(self._exception_handler)
        return clock

    @asyncio.coroutine
    def _run_init_phases(self) -> Generator[int, None, None]:
        """Run init phases."""
        yield from self.events.post_queue_async("init_phase_1")
        '''event: init_phase_1

        desc: Posted during the initial boot up of MPF.
        '''
        yield from self.events.post_queue_async("init_phase_2")
        '''event: init_phase_2

        desc: Posted during the initial boot up of MPF.
        '''
        self._load_plugins()
        yield from self.events.post_queue_async("init_phase_3")
        '''event: init_phase_3

        desc: Posted during the initial boot up of MPF.
        '''
        self._load_scriptlets()

        yield from self.events.post_queue_async("init_phase_4")
        '''event: init_phase_4

        desc: Posted during the initial boot up of MPF.
        '''

        yield from self.events.post_queue_async("init_phase_5")
        '''event: init_phase_5

        desc: Posted during the initial boot up of MPF.
        '''

    def _init_phases_complete(self, **kwargs) -> None:
        """Cleanup after init and remove boot holds."""
        del kwargs
        ConfigValidator.unload_config_spec()

        self.clear_boot_hold('init')

    @asyncio.coroutine
    def _initialize_platforms(self) -> Generator[int, None, None]:
        """Initialise all used hardware platforms."""
        init_done = []
        # collect all platform init futures
        for hardware_platform in list(self.hardware_platforms.values()):
            init_done.append(hardware_platform.initialize())

        # wait for all of them in parallel
        results = yield from asyncio.wait(init_done, loop=self.clock.loop)
        for result in results[0]:
            result.result()

    @asyncio.coroutine
    def _start_platforms(self) -> Generator[int, None, None]:
        """Start all used hardware platforms."""
        for hardware_platform in list(self.hardware_platforms.values()):
            yield from hardware_platform.start()
            if not hardware_platform.features['tickless']:
                self.clock.schedule_interval(
                    hardware_platform.tick,
                    1 / self.config['mpf']['default_platform_hz'])

    def _initialize_credit_string(self):
        """Set default credit string."""
        # Do this here so there's a credit_string var even if they're not using
        # the credits mode
        try:
            credit_string = self.config['credits']['free_play_string']
        except KeyError:
            credit_string = 'FREE PLAY'

        self.set_machine_var('credits_string', credit_string)
        '''machine_var: credits_string

        desc: Holds a displayable string which shows how many
        credits are on the machine. For example, "CREDITS: 1". If the machine
        is set to free play, the value of this string will be "FREE PLAY".

        You can change the format and value of this string in the ``credits:``
        section of the machine config file.
        '''

    def _validate_config(self) -> None:
        """Validate game and machine config."""
        self.validate_machine_config_section('machine')
        self.validate_machine_config_section('game')
        self.validate_machine_config_section('mpf')

    def validate_machine_config_section(self, section: str) -> None:
        """Validate a config section."""
        if section not in ConfigValidator.config_spec:
            return

        if section not in self.config:
            self.config[section] = dict()

        self.config[section] = self.config_validator.validate_config(
            section, self.config[section], section)

    def _register_system_events(self) -> None:
        """Register default event handlers."""
        self.events.add_handler('quit', self.stop)
        self.events.add_handler(
            self.config['mpf']['switch_tag_event'].replace('%', 'quit'),
            self.stop)

    def _register_config_players(self) -> None:
        """Register config players."""
        # todo move this to config_player module
        for name, module_class in self.config['mpf']['config_players'].items():
            config_player_class = Util.string_to_class(module_class)
            setattr(self, '{}_player'.format(name), config_player_class(self))

        self._register_plugin_config_players()

    def _register_plugin_config_players(self):
        """Register plugin config players."""
        self.debug_log("Registering Plugin Config Players")
        for entry_point in iter_entry_points(group='mpf.config_player',
                                             name=None):
            self.debug_log("Registering %s", entry_point)
            name, player = entry_point.load()(self)
            setattr(self, '{}_player'.format(name), player)

    def create_data_manager(
            self, config_name: str) -> DataManager:  # pragma: no cover
        """Return a new DataManager for a certain config.

        Args:
            config_name: Name of the config
        """
        return DataManager(self, config_name)

    def _load_machine_vars(self) -> None:
        """Load machine vars from data manager."""
        self.machine_var_data_manager = self.create_data_manager(
            'machine_vars')

        current_time = self.clock.get_time()

        for name, settings in (iter(
                self.machine_var_data_manager.get_data().items())):

            if not isinstance(settings, dict) or "value" not in settings:
                continue

            if ('expire' in settings and settings['expire']
                    and settings['expire'] < current_time):

                continue

            self.set_machine_var(name=name, value=settings['value'])

        self._load_initial_machine_vars()

        # Create basic system information machine variables
        self.set_machine_var(name="mpf_version", value=mpf_version)
        self.set_machine_var(name="mpf_extended_version",
                             value=mpf_extended_version)
        self.set_machine_var(name="python_version", value=python_version())
        self.set_machine_var(name="platform", value=platform(aliased=True))
        platform_info = system_alias(system(), release(), version())
        self.set_machine_var(name="platform_system", value=platform_info[0])
        self.set_machine_var(name="platform_release", value=platform_info[1])
        self.set_machine_var(name="platform_version", value=platform_info[2])
        self.set_machine_var(name="platform_machine", value=machine())

    def _load_initial_machine_vars(self) -> None:
        """Load initial machine var values from config if they did not get loaded from data."""
        if 'machine_vars' not in self.config:
            return

        config = self.config['machine_vars']
        for name, element in config.items():
            if name not in self.machine_vars:
                element = self.config_validator.validate_config(
                    "machine_vars", copy.deepcopy(element))
                self.set_machine_var(name=name,
                                     value=Util.convert_to_type(
                                         element['initial_value'],
                                         element['value_type']))
            self.configure_machine_var(name=name,
                                       persist=element.get('persist', False))

    def _set_machine_path(self) -> None:
        """Add the machine folder to sys.path so we can import modules from it."""
        sys.path.insert(0, self.machine_path)

    def _load_config(self) -> None:  # pragma: no cover
        config_files = [self.options['mpfconfigfile']]

        for num, config_file in enumerate(self.options['configfile']):

            if not (config_file.startswith('/')
                    or config_file.startswith('\\')):

                config_files.append(
                    os.path.join(self.machine_path, "config", config_file))

            self.log.info("Machine config file #%s: %s", num + 1, config_file)

        self.config = self.config_processor.load_config_files_with_cache(
            config_files,
            "machine",
            load_from_cache=not self.options['no_load_cache'],
            store_to_cache=self.options['create_config_cache'])

    def verify_system_info(self):
        """Dump information about the Python installation to the log.

        Information includes Python version, Python executable, platform, and
        core architecture.
        """
        python_version_info = sys.version_info

        if not (python_version_info[0] == 3
                and python_version_info[1] in (4, 5, 6)):
            raise AssertionError(
                "Incorrect Python version. MPF requires "
                "Python 3.4, 3.5 or 3.6. You have Python {}.{}.{}.".format(
                    python_version_info[0], python_version_info[1],
                    python_version_info[2]))

        self.log.info("Platform: %s", sys.platform)
        self.log.info("Python executable location: %s", sys.executable)

        if sys.maxsize < 2**32:
            self.log.info("Python version: %s.%s.%s (32-bit)",
                          python_version_info[0], python_version_info[1],
                          python_version_info[2])
        else:
            self.log.info("Python version: %s.%s.%s (64-bit)",
                          python_version_info[0], python_version_info[1],
                          python_version_info[2])

    def _load_core_modules(self) -> None:
        """Load core modules."""
        self.debug_log("Loading core modules...")
        for name, module_class in self.config['mpf']['core_modules'].items():
            self.debug_log("Loading '%s' core module", module_class)
            m = Util.string_to_class(module_class)(self)
            setattr(self, name, m)

    def _load_hardware_platforms(self) -> None:
        """Load all hardware platforms."""
        self.validate_machine_config_section('hardware')
        # if platform is forced use that one
        if self.options['force_platform']:
            self.add_platform(self.options['force_platform'])
            self.set_default_platform(self.options['force_platform'])
            return

        # otherwise load all platforms
        for section, platforms in self.config['hardware'].items():
            if section == 'driverboards':
                continue
            for hardware_platform in platforms:
                if hardware_platform.lower() != 'default':
                    self.add_platform(hardware_platform)

        # set default platform
        self.set_default_platform(self.config['hardware']['platform'][0])

    def _load_plugins(self) -> None:
        """Load plugins."""
        self.debug_log("Loading plugins...")

        # TODO: This should be cleaned up. Create a Plugins base class and
        # classmethods to determine if the plugins should be used.

        for plugin in Util.string_to_list(self.config['mpf']['plugins']):

            self.debug_log("Loading '%s' plugin", plugin)

            plugin_obj = Util.string_to_class(plugin)(self)
            self.plugins.append(plugin_obj)

    def _load_scriptlets(self) -> None:
        """Load scriptlets."""
        if 'scriptlets' in self.config:
            self.debug_log("Loading scriptlets...")

            for scriptlet in Util.string_to_list(self.config['scriptlets']):

                self.debug_log("Loading '%s' scriptlet", scriptlet)

                scriptlet_obj = Util.string_to_class(
                    self.config['mpf']['paths']['scriptlets'] + "." +
                    scriptlet)(machine=self, name=scriptlet.split('.')[1])

                self.scriptlets.append(scriptlet_obj)

    @asyncio.coroutine
    def reset(self) -> Generator[int, None, None]:
        """Reset the machine.

        This method is safe to call. It essentially sets up everything from
        scratch without reloading the config files and assets from disk. This
        method is called after a game ends and before attract mode begins.
        """
        self.debug_log('Resetting...')

        yield from self.events.post_queue_async('machine_reset_phase_1')
        '''Event: machine_reset_phase_1

        Desc: The first phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        This is a queue event. The machine reset phase 1 will not be complete
        until the queue is cleared.

        '''

        yield from self.events.post_queue_async('machine_reset_phase_2')
        '''Event: machine_reset_phase_2

        Desc: The second phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        This is a queue event. The machine reset phase 2 will not be complete
        until the queue is cleared.

        '''

        yield from self.events.post_queue_async('machine_reset_phase_3')
        '''Event: machine_reset_phase_3

        Desc: The third phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        This is a queue event. The machine reset phase 3 will not be complete
        until the queue is cleared.

        '''
        """Called when the machine reset process is complete."""
        self.debug_log('Reset Complete')
        yield from self.events.post_async('reset_complete')
        '''event: reset_complete

        desc: The machine reset process is complete

        '''

    def add_platform(self, name: str) -> None:
        """Make an additional hardware platform interface available to MPF.

        Args:
            name: String name of the platform to add. Must match the name of a
                platform file in the mpf/platforms folder (without the .py
                extension).
        """
        if name not in self.hardware_platforms:
            if name not in self.config['mpf']['platforms']:
                raise AssertionError("Invalid platform {}".format(name))

            try:
                hardware_platform = Util.string_to_class(
                    self.config['mpf']['platforms'][name])
            except ImportError as e:  # pragma: no cover
                if e.name != name:  # do not swallow unrelated errors
                    raise
                raise ImportError("Cannot add hardware platform {}. This is "
                                  "not a valid platform name".format(name))

            self.hardware_platforms[name] = (hardware_platform(self))

    def set_default_platform(self, name: str) -> None:
        """Set the default platform.

        It is used if a device class-specific or device-specific platform is not specified.

        Args:
            name: String name of the platform to set to default.
        """
        try:
            self.default_platform = self.hardware_platforms[name]
            self.debug_log("Setting default platform to '%s'", name)
        except KeyError:
            raise AssertionError(
                "Cannot set default platform to '{}', as that's not"
                " a currently active platform".format(name))

    def register_monitor(self, monitor_class: str,
                         monitor: Callable[..., Any]) -> None:
        """Register a monitor.

        Args:
            monitor_class: String name of the monitor class for this monitor
                that's being registered.
            monitor: Callback to notify

        MPF uses monitors to allow components to monitor certain internal
        elements of MPF.

        For example, a player variable monitor could be setup to be notified of
        any changes to a player variable, or a switch monitor could be used to
        allow a plugin to be notified of any changes to any switches.

        The MachineController's list of registered monitors doesn't actually
        do anything. Rather it's a dictionary of sets which the monitors
        themselves can reference when they need to do something. We just needed
        a central registry of monitors.

        """
        if monitor_class not in self.monitors:
            self.monitors[monitor_class] = set()

        self.monitors[monitor_class].add(monitor)

    def initialise_mpf(self):
        """Initialise MPF."""
        self.info_log("Initialise MPF.")
        timeout = 30 if self.options["production"] else None
        try:
            init = Util.ensure_future(self.initialise(), loop=self.clock.loop)
            self.clock.loop.run_until_complete(
                Util.first([init, self.stop_future],
                           cancel_others=False,
                           loop=self.clock.loop,
                           timeout=timeout))
        except asyncio.TimeoutError:
            self.shutdown()
            self.error_log(
                "MPF needed more than {}s for initialisation. Aborting!".
                format(timeout))
            return
        except RuntimeError:
            self.shutdown()
            # do not show a runtime useless runtime error
            self.error_log("Failed to initialise MPF")
            return
        if init.exception():
            self.shutdown()
            self.error_log("Failed to initialise MPF: %s", init.exception())
            traceback.print_tb(init.exception().__traceback__)  # noqa
            return

    def run(self) -> None:
        """Start the main machine run loop."""
        self.initialise_mpf()

        self.info_log("Starting the main run loop.")
        self._run_loop()

    def stop(self, **kwargs) -> None:
        """Perform a graceful exit of MPF."""
        del kwargs
        if self.stop_future.done():
            return
        self.stop_future.set_result(True)

    def _do_stop(self) -> None:
        self.log.info("Shutting down...")
        self.events.post('shutdown')
        '''event: shutdown
        desc: Posted when the machine is shutting down to give all modules a
        chance to shut down gracefully.

        '''

        self.events.process_event_queue()
        self.shutdown()

    def shutdown(self) -> None:
        """Shutdown the machine."""
        self.thread_stopper.set()
        if hasattr(self, "device_manager"):
            self.device_manager.stop_devices()
        self._platform_stop()

        self.clock.loop.stop()
        # this is needed to properly close all sockets
        self.clock.loop.run_forever()
        self.clock.loop.close()

    def _run_loop(self) -> None:  # pragma: no cover
        # Main machine run loop with when the default platform interface
        # specifies the MPF should control the main timer

        try:
            self.clock.run(self.stop_future)
        except KeyboardInterrupt:
            print("Shutdown because of keyboard interrupts")

        self._do_stop()

        if self._exception:
            print("Shutdown because of an exception:")
            raise self._exception['exception']

    def _platform_stop(self) -> None:
        """Stop all platforms."""
        for hardware_platform in list(self.hardware_platforms.values()):
            hardware_platform.stop()

    def _write_machine_var_to_disk(self, name: str) -> None:
        """Write value to disk."""
        if self.machine_vars[name]['persist'] and self.config['mpf'][
                'save_machine_vars_to_disk']:
            self._write_machine_vars_to_disk()

    def _write_machine_vars_to_disk(self):
        """Update machine vars on disk."""
        self.machine_var_data_manager.save_all({
            name: {
                "value": var["value"],
                "expire": var['expire_secs']
            }
            for name, var in self.machine_vars.items() if var["persist"]
        })

    def get_machine_var(self, name: str) -> Any:
        """Return the value of a machine variable.

        Args:
            name: String name of the variable you want to get that value for.

        Returns:
            The value of the variable if it exists, or None if the variable
            does not exist.

        """
        try:
            return self.machine_vars[name]['value']
        except KeyError:
            return None

    def is_machine_var(self, name: str) -> bool:
        """Return true if machine variable exists."""
        return name in self.machine_vars

    def configure_machine_var(self,
                              name: str,
                              persist: bool,
                              expire_secs: int = None) -> None:
        """Create a new machine variable.

        Args:
            name: String name of the variable.
            persist: Boolean as to whether this variable should be saved to
                disk so it's available the next time MPF boots.
            expire_secs: Optional number of seconds you'd like this variable
                to persist on disk for. When MPF boots, if the expiration time
                of the variable is in the past, it will not be loaded.
                For example, this lets you write the number of credits on
                the machine to disk to persist even during power off, but you
                could set it so that those only stay persisted for an hour.
        """
        if name not in self.machine_vars:
            self.machine_vars[name] = {
                'value': None,
                'persist': persist,
                'expire_secs': expire_secs
            }
        else:
            self.machine_vars[name]['persist'] = persist
            self.machine_vars[name]['expire_secs'] = expire_secs

    def set_machine_var(self, name: str, value: Any) -> None:
        """Set the value of a machine variable.

        Args:
            name: String name of the variable you're setting the value for.
            value: The value you're setting. This can be any Type.
        """
        if name not in self.machine_vars:
            self.configure_machine_var(name=name, persist=False)
            prev_value = None
            change = True
        else:
            prev_value = self.machine_vars[name]['value']
            try:
                change = value - prev_value
            except TypeError:
                change = prev_value != value

        # set value
        self.machine_vars[name]['value'] = value

        if change:
            self._write_machine_var_to_disk(name)

            self.debug_log(
                "Setting machine_var '%s' to: %s, (prior: %s, "
                "change: %s)", name, value, prev_value, change)
            self.events.post('machine_var_' + name,
                             value=value,
                             prev_value=prev_value,
                             change=change)
            '''event: machine_var_(name)

            desc: Posted when a machine variable is added or changes value.
            (Machine variables are like player variables, except they're
            maintained machine-wide instead of per-player or per-game.)

            args:

            value: The new value of this machine variable.

            prev_value: The previous value of this machine variable, e.g. what
            it was before the current value.

            change: If the machine variable just changed, this will be the
            amount of the change. If it's not possible to determine a numeric
            change (for example, if this machine variable is a list), then this
            *change* value will be set to the boolean *True*.
            '''

            if self.machine_var_monitor:
                for callback in self.monitors['machine_vars']:
                    callback(name=name,
                             value=value,
                             prev_value=prev_value,
                             change=change)

    def remove_machine_var(self, name: str) -> None:
        """Remove a machine variable by name.

        If this variable persists to disk, it will remove it from there too.

        Args:
            name: String name of the variable you want to remove.
        """
        try:
            del self.machine_vars[name]
            self._write_machine_vars_to_disk()
        except KeyError:
            pass

    def remove_machine_var_search(self,
                                  startswith: str = '',
                                  endswith: str = '') -> None:
        """Remove a machine variable by matching parts of its name.

        Args:
            startswith: Optional start of the variable name to match.
            endswith: Optional end of the variable name to match.

        For example, if you pass startswit='player' and endswith='score', this
        method will match and remove player1_score, player2_score, etc.
        """
        for var in list(self.machine_vars.keys()):
            if var.startswith(startswith) and var.endswith(endswith):
                del self.machine_vars[var]

        self._write_machine_vars_to_disk()

    def get_platform_sections(
            self, platform_section: str,
            overwrite: str) -> "SmartVirtualHardwarePlatform":
        """Return platform section."""
        if self.options['force_platform']:
            return self.default_platform

        if not overwrite:
            if self.config['hardware'][platform_section][0] != 'default':
                return self.hardware_platforms[self.config['hardware']
                                               [platform_section][0]]
            else:
                return self.default_platform
        else:
            try:
                return self.hardware_platforms[overwrite]
            except KeyError:
                raise AssertionError(
                    "Platform \"{}\" has not been loaded. Please add it to your \"hardware\" section."
                    .format(overwrite))

    def register_boot_hold(self, hold: str) -> None:
        """Register a boot hold."""
        if self.is_init_done.is_set():
            raise AssertionError("Register hold after init_done")
        self._boot_holds.add(hold)

    def clear_boot_hold(self, hold: str) -> None:
        """Clear a boot hold."""
        if self.is_init_done.is_set():
            raise AssertionError("Clearing hold after init_done")
        self._boot_holds.remove(hold)
        self.debug_log('Clearing boot hold %s. Holds remaining: %s', hold,
                       self._boot_holds)
        if not self._boot_holds:
            self.is_init_done.set()

    @asyncio.coroutine
    def init_done(self) -> Generator[int, None, None]:
        """Finish init.

        Called when init is done and all boot holds are cleared.
        """
        yield from self.events.post_async("init_done")
        '''event: init_done

        desc: Posted when the initial (one-time / boot) init phase is done. In
        other words, once this is posted, MPF is booted and ready to go.
        '''

        ConfigValidator.unload_config_spec()
        yield from self.reset()
예제 #14
0
    def __init__(self, mpf_path: str, machine_path: str,
                 options: dict) -> None:
        """Initialize machine controller."""
        super().__init__()
        self.log = logging.getLogger("Machine")  # type: Logger
        self.log.info("Mission Pinball Framework Core Engine v%s", __version__)

        self.log.info("Command line arguments: %s", options)
        self.options = options
        self.config_processor = ConfigProcessor()

        self.log.info("MPF path: %s", mpf_path)
        self.mpf_path = mpf_path

        self.log.info("Machine path: %s", machine_path)
        self.machine_path = machine_path

        self.verify_system_info()
        self._exception = None  # type: Any
        self._boot_holds = set()  # type: Set[str]
        self.is_init_done = None  # type: asyncio.Event

        self._done = False
        self.monitors = dict()  # type: Dict[str, Set[Callable]]
        self.plugins = list()  # type: List[Any]
        self.scriptlets = list()  # type: List[Scriptlet]
        self.modes = DeviceCollection(self, 'modes',
                                      None)  # type: Dict[str, Mode]
        self.game = None  # type: Game
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.machine_var_data_manager = None  # type: DataManager
        self.thread_stopper = threading.Event()

        self.config = None  # type: Any

        # add some type hints
        MYPY = False  # noqa
        if MYPY:  # pragma: no cover
            # controllers
            self.events = None  # type: EventManager
            self.switch_controller = None  # type: SwitchController
            self.mode_controller = None  # type: ModeController
            self.settings = None  # type: SettingsController
            self.bcp = None  # type: Bcp
            self.asset_manager = None  # type: BaseAssetManager
            self.ball_controller = None  # type: BallController
            self.show_controller = None  # type: ShowController
            self.placeholder_manager = None  # type: PlaceholderManager
            self.device_manager = None  # type: DeviceManager
            self.auditor = None  # type: Auditor
            self.tui = None  # type: TextUi
            self.service = None  # type: ServiceController

            # devices
            self.autofires = None  # type: DeviceCollectionType[str, AutofireCoil]
            self.shows = None  # type: DeviceCollectionType[str, Show]
            self.shots = None  # type: DeviceCollectionType[str, Shot]
            self.shot_groups = None  # type: DeviceCollectionType[str, ShotGroup]
            self.switches = None  # type: DeviceCollectionType[str, Switch]
            self.coils = None  # type: DeviceCollectionType[str, Driver]
            self.lights = None  # type: DeviceCollectionType[str, Light]
            self.ball_devices = None  # type: DeviceCollectionType[str, BallDevice]
            self.accelerometers = None  # type: DeviceCollectionType[str, Accelerometer]
            self.playfield = None  # type: Playfield
            self.playfields = None  # type: DeviceCollectionType[str, Playfield]
            self.counters = None  # type: DeviceCollectionType[str, Counter]
            self.sequences = None  # type: DeviceCollectionType[str, Sequence]
            self.accruals = None  # type: DeviceCollectionType[str, Accrual]
            self.drop_targets = None  # type: DeviceCollectionType[str, DropTarget]
            self.servos = None  # type: DeviceCollectionType[str, Servo]
            self.segment_displays = None  # type: DeviceCollectionType[str, SegmentDisplay]

        self._set_machine_path()

        self.config_validator = ConfigValidator(self)

        self._load_config()
        self.machine_config = self.config  # type: Any
        self.configure_logging(
            'Machine', self.config['logging']['console']['machine_controller'],
            self.config['logging']['file']['machine_controller'])

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

        self.hardware_platforms = dict(
        )  # type: Dict[str, SmartVirtualHardwarePlatform]
        self.default_platform = None  # type: SmartVirtualHardwarePlatform

        self.clock = self._load_clock()
        self.stop_future = asyncio.Future(
            loop=self.clock.loop)  # type: asyncio.Future
예제 #15
0
class WidgetCollection(ConfigCollection):

    config_section = 'widgets'
    collection = 'widgets'
    class_label = 'WidgetConfig'

    type_map = CaseInsensitiveDict()

    def _initialize(self) -> None:
        for cls_name, module in self.mc.machine_config['mpf-mc'][
                'widgets'].items():
            for widget_cls in import_module(module).widget_classes:
                self.type_map[cls_name] = widget_cls

    def process_config(self, config: Union[dict, list]) -> List["Widget"]:
        # config is localized to a specific widget section
        if isinstance(config, dict):
            config = [config]

        widget_list = list()

        for widget in config:
            widget_list.append(self.process_widget(widget))

        return widget_list

    def process_widget(self, config: dict) -> dict:
        # config is localized widget settings
        try:
            widget_cls = WidgetCollection.type_map[config['type']]
        except (KeyError, TypeError):
            try:
                raise ValueError(
                    '"{}" is not a valid MPF display widget type. Did you '
                    'misspell it, or forget to enable it in the "mpf-mc: '
                    'widgets" section of your machine config?'.format(
                        config['type']))
            except (KeyError, TypeError):
                raise ValueError("Invalid widget config: {}".format(config))

        config['_default_settings'] = list()

        for default_setting_name in widget_cls.merge_settings:
            if default_setting_name in config:
                config['_default_settings'].append(default_setting_name)

        self.mc.config_validator.validate_config('widgets:{}'.format(
            config['type']).lower(),
                                                 config,
                                                 base_spec='widgets:common')

        if 'effects' in config and config['type'] == 'display':
            config['effects'] = self.mc.effects_manager.validate_effects(
                config['effects'])

        if 'animations' in config:
            config['animations'] = (self.process_animations(
                config['animations']))

        else:
            config['animations'] = None

        if 'reset_animations_events' in config:
            for event_name in config['reset_animations_events']:
                if event_name not in magic_events:
                    self.mc.events.add_handler(
                        "client_connected",
                        partial(self._register_trigger, event_name))

        if config.get('z', 0) < 0:
            raise ValueError(
                "\nWidget with negative z value in config: {}.\n\nAs of MPF "
                "v0.30.3, negative z: "
                "values are no longer used to put widgets in 'parent' frames. "
                "Instead add a 'target:' setting to the 'widget_player:' entry"
                " and set that to the name of the display target (display or "
                "slide_frame) you want to add this widget to. Note that "
                "'target: default' is valid and will add the widget to the "
                "default display on top of any slides.\n".format(config))

        return config

    def _register_trigger(self, event_name: str, **kwargs) -> None:
        del kwargs
        self.mc.bcp_processor.register_trigger(event=event_name)

    def process_animations(self, config: dict) -> dict:
        # config is localized to the slide's 'animations' section

        for event_name, event_settings in config.items():

            # make sure the event_name is registered as a trigger event so MPF
            # will send those events as triggers via BCP. But we don't want
            # to register magic events since those aren't real MPF events.
            if event_name not in magic_events:
                self.mc.events.add_handler(
                    "client_connected",
                    partial(self._register_trigger, event_name))

            # str means it's a list of named animations
            if isinstance(event_settings, str):
                event_settings = Util.string_to_list(event_settings)

            # dict means it's a single set of settings for one animation step
            elif isinstance(event_settings, dict):
                event_settings = [event_settings]

            # ultimately we're producing a list of dicts, so build that list
            # as we iterate
            new_list = list()
            for settings in event_settings:
                new_list.append(self.mc.animations.process_animation(settings))

            config[event_name] = new_list

        return config
예제 #16
0
    def validate_config(self, config_spec, source, section_name=None,
                        base_spec=None, add_missing_keys=True, prefix=None):
        """Validate a config dict against spec."""
        # config_spec, str i.e. "device:shot"
        # source is dict
        # section_name is str used for logging failures

        if source is None:
            source = CaseInsensitiveDict()

        if not section_name:
            section_name = config_spec  # str

        if prefix:
            validation_failure_info = (prefix + ":" + config_spec, section_name)
        else:
            validation_failure_info = (config_spec, section_name)

        this_spec = self._build_spec(config_spec, base_spec)

        if '__allow_others__' not in this_spec:
            self.check_for_invalid_sections(this_spec, source,
                                            validation_failure_info)

        processed_config = source

        if not isinstance(source, (list, dict)):
            self.validation_error("", validation_failure_info, "Source should be list or dict but is {}".format(
                source.__class__
            ))

        for k in list(this_spec.keys()):
            if this_spec[k] == 'ignore' or k[0] == '_':
                continue

            elif k in source:  # validate the entry that exists

                if isinstance(this_spec[k], dict):
                    # This means we're looking for a list of dicts

                    final_list = list()
                    if k in source:
                        for i in source[k]:  # individual step
                            final_list.append(self.validate_config(
                                config_spec + ':' + k, source=i,
                                section_name=k))

                    processed_config[k] = final_list

                else:
                    processed_config[k] = self.validate_config_item(
                        this_spec[k], item=source[k],
                        validation_failure_info=(validation_failure_info, k))

            elif add_missing_keys:  # create the default entry

                if isinstance(this_spec[k], dict):
                    processed_config[k] = list()

                else:
                    processed_config[k] = self.validate_config_item(
                        this_spec[k],
                        validation_failure_info=(
                            validation_failure_info, k))

        return processed_config
예제 #17
0
파일: mc.py 프로젝트: elliotstarks/mpf-mc
    def __init__(self, options, config, machine_path,
                 thread_stopper=None, **kwargs):

        self.log = logging.getLogger('mpfmc')
        self.log.info("Mission Pinball Framework Media Controller v%s", __version__)
        self.log.info("Mission Pinball Framework Game Engine v%s", __mpfversion__)

        if (__version__.split('.')[0] != __mpfversion__.split('.')[0] or
                __version__.split('.')[1] != __mpfversion__.split('.')[1]):

            self.log.error("MPF MC and MPF Game engines must be same "
                           "major.minor versions. You have MPF v{} and MPF-MC"
                           " v{}".format(__mpfversion__, __version__))

            raise ValueError("MPF MC and MPF Game engines must be same "
                           "major.minor versions. You have MPF v{} and MPF-MC"
                           " v{}".format(__mpfversion__, __version__))

        super().__init__(**kwargs)

        self.options = options
        self.machine_config = config
        self.log.info("Machine path: %s", machine_path)
        self.machine_path = machine_path
        self.clock = Clock
        # pylint: disable-msg=protected-access
        self.log.info("Starting clock at %sHz", Clock._max_fps)
        self._boot_holds = set()
        self.mpf_path = os.path.dirname(mpf.__file__)
        self.modes = CaseInsensitiveDict()
        self.player_list = list()
        self.player = None
        self.num_players = 0
        self.bcp_client_connected = False
        self.placeholder_manager = McPlaceholderManager(self)
        self.settings = McSettingsController(self)

        self.animation_configs = dict()
        self.active_slides = dict()
        self.scriptlets = list()

        self.register_boot_hold('init')
        self.displays = CaseInsensitiveDict()
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.monitors = dict()
        self.targets = dict()
        """Dict which contains all the active slide frames in the machine that
        a slide can target. Will always contain an entry called 'default'
        which will be used if a slide doesn't specify targeting.
        """

        self.keyboard = None
        self.physical_dmds = []
        self.physical_rgb_dmds = []
        self.crash_queue = queue.Queue()
        self.ticks = 0
        self.start_time = 0
        self.is_init_done = False

        if thread_stopper:
            self.thread_stopper = thread_stopper
        else:
            self.thread_stopper = threading.Event()

        # Core components
        self.config_validator = ConfigValidator(self)
        self.events = EventManager(self)
        self.mode_controller = ModeController(self)
        create_config_collections(self, self.machine_config['mpf-mc']['config_collections'])
        ConfigValidator.load_config_spec()

        self.config_processor = ConfigProcessor(self)
        self.transition_manager = TransitionManager(self)

        self._set_machine_path()

        self._load_font_paths()

        # Initialize the sound system (must be done prior to creating the AssetManager).
        # If the sound system is not available, do not load any other sound-related modules.
        if SoundSystem is None:
            self.sound_system = None
        else:
            self.sound_system = SoundSystem(self)

        self.asset_manager = ThreadedAssetManager(self)
        self.bcp_processor = BcpProcessor(self)

        # Asset classes
        ImageAsset.initialize(self)
        VideoAsset.initialize(self)

        self._initialise_sound_system()

        self.clock.schedule_interval(self._check_crash_queue, 1)

        self.events.add_handler("client_connected", self._create_physical_dmds)
        self.events.add_handler("player_turn_start", self.player_start_turn)
예제 #18
0
class Widget(ConfigCollection):

    config_section = 'widgets'
    collection = 'widgets'
    class_label = 'WidgetConfig'

    type_map = CaseInsensitiveDict(text=Text,
                                   image=ImageWidget,
                                   video=VideoWidget,
                                   slide_frame=SlideFrame,
                                   bezier=Bezier,
                                   ellipse=Ellipse,
                                   line=Line,
                                   point=Point,
                                   points=Point,
                                   quad=Quad,
                                   rectangle=Rectangle,
                                   triangle=Triangle,
                                   dmd=Dmd,
                                   color_dmd=ColorDmd,
                                   text_input=MpfTextInput)

    def process_config(self, config):
        # config is localized to a specific widget section
        if isinstance(config, dict):
            config = [config]

        widget_list = list()

        for widget in config:
            widget_list.append(self.process_widget(widget))

        return widget_list

    def process_widget(self, config):
        # config is localized widget settings
        try:
            widget_cls = Widget.type_map[config['type']]
        except KeyError:
            try:
                raise ValueError(
                    '"{}" is not a valid MPF display widget type'.format(
                        config['type']))
            except KeyError:
                raise ValueError("Invalid widget config: {}".format(config))

        config['_default_settings'] = list()

        for default_setting_name in widget_cls.merge_settings:
            if default_setting_name in config:
                config['_default_settings'].append(default_setting_name)

        self.mc.config_validator.validate_config('widgets:{}'.format(
            config['type']).lower(),
                                                 config,
                                                 base_spec='widgets:common')

        if 'animations' in config:
            config['animations'] = (self.process_animations(
                config['animations']))

        else:
            config['animations'] = None

        if config.get('z', 0) < 0:
            raise ValueError(
                "\nWidget with negative z value in config: {}.\n\nAs of MPF "
                "v0.30.3, negative z: "
                "values are no longer used to put widgets in 'parent' frames. "
                "Instead add a 'target:' setting to the 'widget_player:' entry"
                " and set that to the name of the display target (display or "
                "slide_frame) you want to add this widget to. Note that "
                "'target: default' is valid and will add the widget to the "
                "default display on top of any slides.\n".format(config))

        return config

    def _register_trigger(self, event_name, **kwargs):
        del kwargs
        self.mc.bcp_processor.register_trigger(event=event_name)

    def process_animations(self, config):
        # config is localized to the slide's 'animations' section

        for event_name, event_settings in config.items():

            # make sure the event_name is registered as a trigger event so MPF
            # will send those events as triggers via BCP. But we don't want
            # to register magic events since those aren't real MPF events.
            if event_name not in magic_events:
                self.mc.events.add_handler(
                    "client_connected",
                    partial(self._register_trigger, event_name))

            # str means it's a list of named animations
            if isinstance(event_settings, str):
                event_settings = Util.string_to_list(event_settings)

            # dict means it's a single set of settings for one animation step
            elif isinstance(event_settings, dict):
                event_settings = [event_settings]

            # ultimately we're producing a list of dicts, so build that list
            # as we iterate
            new_list = list()
            for settings in event_settings:
                new_list.append(self.mc.animations.process_animation(settings))

            config[event_name] = new_list

        return config
예제 #19
0
class MachineController(object):
    """Base class for the Machine Controller object.

    The machine controller is the main entity of the entire framework. It's the
    main part that's in charge and makes things happen.

    Args:
        options: Dictionary of options the machine controller uses to configure
            itself.

    Attributes:
        options(dict): A dictionary of options built from the command line options
            used to launch mpf.py.
        config(dict): A dictionary of machine's configuration settings, merged from
            various sources.
        game(mpf.modes.game.code.game.Game): the current game
        machine_path: The root path of this machine_files folder
        plugins:
        scriptlets:
        hardware_platforms:
        events(mpf.core.events.EventManager):

    """
    def __init__(self, mpf_path: str, machine_path: str, options: dict):
        """Initialise machine controller."""
        self.log = logging.getLogger("Machine")
        self.log.info("Mission Pinball Framework Core Engine v%s", __version__)

        self.log.debug("Command line arguments: %s", options)
        self.options = options

        self.log.debug("MPF path: %s", mpf_path)
        self.mpf_path = mpf_path

        self.log.info("Machine path: %s", machine_path)
        self.machine_path = machine_path

        self.log.debug("Command line arguments: %s", self.options)
        self.verify_system_info()
        self._exception = None

        self._boot_holds = set()
        self.is_init_done = False
        self.register_boot_hold('init')

        self._done = False
        self.monitors = dict()
        self.plugins = list()
        self.scriptlets = list()
        self.modes = DeviceCollection(self, 'modes', None)
        self.game = None
        self.active_debugger = dict()
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.machine_var_data_manager = None
        self.thread_stopper = threading.Event()

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

        self.crash_queue = queue.Queue()

        self.config = None
        self.events = None
        self.machine_config = None
        self._set_machine_path()

        self.config_validator = ConfigValidator(self)

        self._load_config()

        self.clock = self._load_clock()
        self._crash_queue_checker = self.clock.schedule_interval(
            self._check_crash_queue, 1)

        self.hardware_platforms = dict()
        self.default_platform = None

        self._load_hardware_platforms()

        self._initialize_credit_string()

        self._load_core_modules()
        # order is specified in mpfconfig.yaml

        # This is called so hw platforms have a chance to register for events,
        # and/or anything else they need to do with core modules since
        # they're not set up yet when the hw platforms are constructed.
        self._initialize_platforms()

        self._validate_config()

        self._register_config_players()
        self._register_system_events()
        self._load_machine_vars()
        self._run_init_phases()

        ConfigValidator.unload_config_spec()

        self.clear_boot_hold('init')

    def _exception_handler(self, loop, context):  # pragma: no cover
        # stop machine
        self.stop()

        # call original exception handler
        loop.set_exception_handler(None)
        loop.call_exception_handler(context)

        # remember exception
        self._exception = context

    # pylint: disable-msg=no-self-use
    def _load_clock(self):  # pragma: no cover
        clock = ClockBase()
        clock.loop.set_exception_handler(self._exception_handler)
        return clock

    def _run_init_phases(self):
        self.events.post("init_phase_1")
        '''event: init_phase_1

        desc: Posted during the initial boot up of MPF.
        '''

        self.events.process_event_queue()
        self.events.post("init_phase_2")
        '''event: init_phase_2

        desc: Posted during the initial boot up of MPF.
        '''
        self.events.process_event_queue()
        self._load_plugins()
        self.events.post("init_phase_3")
        '''event: init_phase_3

        desc: Posted during the initial boot up of MPF.
        '''
        self.events.process_event_queue()
        self._load_scriptlets()
        self.events.post("init_phase_4")
        '''event: init_phase_4

        desc: Posted during the initial boot up of MPF.
        '''
        self.events.process_event_queue()
        self.events.post("init_phase_5")
        '''event: init_phase_5

        desc: Posted during the initial boot up of MPF.
        '''
        self.events.process_event_queue()

    def _initialize_platforms(self):
        for platform in list(self.hardware_platforms.values()):
            platform.initialize()
            if not platform.features['tickless']:
                self.clock.schedule_interval(
                    platform.tick,
                    1 / self.config['mpf']['default_platform_hz'])

    def _initialize_credit_string(self):
        # Do this here so there's a credit_string var even if they're not using
        # the credits mode
        try:
            credit_string = self.config['credits']['free_play_string']
        except KeyError:
            credit_string = 'FREE PLAY'

        self.create_machine_var('credits_string', credit_string, silent=True)

    def _validate_config(self):
        self.validate_machine_config_section('machine')
        self.validate_machine_config_section('hardware')
        self.validate_machine_config_section('game')

    def validate_machine_config_section(self, section):
        """Validate a config section."""
        if section not in ConfigValidator.config_spec:
            return

        if section not in self.config:
            self.config[section] = dict()

        self.config[section] = self.config_validator.validate_config(
            section, self.config[section], section)

    def _register_system_events(self):
        self.events.add_handler('shutdown', self.power_off)
        self.events.add_handler(
            self.config['mpf']['switch_tag_event'].replace('%', 'shutdown'),
            self.power_off)
        self.events.add_handler('quit', self.stop)
        self.events.add_handler(
            self.config['mpf']['switch_tag_event'].replace('%', 'quit'),
            self.stop)

    def _register_config_players(self):
        # todo move this to config_player module
        for name, module in self.config['mpf']['config_players'].items():
            imported_module = importlib.import_module(module)
            setattr(self, '{}_player'.format(name),
                    imported_module.player_cls(self))

        self._register_plugin_config_players()

    def _register_plugin_config_players(self):

        self.log.debug("Registering Plugin Config Players")
        for entry_point in iter_entry_points(group='mpf.config_player',
                                             name=None):
            self.log.debug("Registering %s", entry_point)
            entry_point.load()(self)

    def create_data_manager(self, config_name):  # pragma: no cover
        """Return a new DataManager for a certain config.

        Args:
            config_name: Name of the config
        """
        return DataManager(self, config_name)

    def _load_machine_vars(self):
        self.machine_var_data_manager = self.create_data_manager(
            'machine_vars')

        current_time = self.clock.get_time()

        for name, settings in (iter(
                self.machine_var_data_manager.get_data().items())):

            if not isinstance(settings, dict) or "value" not in settings:
                continue

            if ('expire' in settings and settings['expire']
                    and settings['expire'] < current_time):

                settings['value'] = 0

            self.create_machine_var(name=name, value=settings['value'])

    def _check_crash_queue(self, time):
        del time
        try:
            crash = self.crash_queue.get(block=False)
        except queue.Empty:
            pass
        else:
            print("MPF Shutting down due to child thread crash")
            print("Crash details: %s", crash)
            self.stop()

    def _set_machine_path(self):
        self.log.debug("Machine path: %s", self.machine_path)

        # Add the machine folder to sys.path so we can import modules from it
        sys.path.insert(0, self.machine_path)

    def _get_mpfcache_file_name(self):
        cache_dir = tempfile.gettempdir()
        path_hash = hashlib.md5(bytes(self.machine_path, 'UTF-8')).hexdigest()
        path_hash += '-'.join(self.options['configfile'])
        result = os.path.join(cache_dir, path_hash)
        return result

    def _load_config(self):  # pragma: no cover
        if self.options['no_load_cache']:
            load_from_cache = False
        else:
            try:
                if self._get_latest_config_mod_time() > os.path.getmtime(
                        self._get_mpfcache_file_name()):
                    load_from_cache = False  # config is newer
                else:
                    load_from_cache = True  # cache is newer

            except OSError as exception:
                if exception.errno != errno.ENOENT:
                    raise  # some unknown error?
                else:
                    load_from_cache = False  # cache file doesn't exist

        config_loaded = False
        if load_from_cache:
            config_loaded = self._load_config_from_cache()

        if not config_loaded:
            self._load_config_from_files()

    def _load_config_from_files(self):
        self.log.info("Loading config from original files")

        self.config = self._get_mpf_config()
        self.config['_mpf_version'] = __version__

        for num, config_file in enumerate(self.options['configfile']):

            if not (config_file.startswith('/')
                    or config_file.startswith('\\')):

                config_file = os.path.join(
                    self.machine_path, self.config['mpf']['paths']['config'],
                    config_file)

            self.log.info("Machine config file #%s: %s", num + 1, config_file)

            self.config = Util.dict_merge(
                self.config,
                ConfigProcessor.load_config_file(config_file,
                                                 config_type='machine'))
            self.machine_config = self.config

        if self.options['create_config_cache']:
            self._cache_config()

    def _get_mpf_config(self):
        return ConfigProcessor.load_config_file(self.options['mpfconfigfile'],
                                                config_type='machine')

    def _load_config_from_cache(self):
        self.log.info("Loading cached config: %s",
                      self._get_mpfcache_file_name())

        with open(self._get_mpfcache_file_name(), 'rb') as f:

            try:
                self.config = pickle.load(f)
                self.machine_config = self.config

            # unfortunately pickle can raise all kinds of exceptions and we dont want to crash on corrupted cache
            # pylint: disable-msg=broad-except
            except Exception:  # pragma: no cover
                self.log.warning("Could not load config from cache")
                return False

            if self.config.get('_mpf_version') != __version__:
                self.log.info(
                    "Cached config is from a different version of MPF.")
                return False

            return True

    def _get_latest_config_mod_time(self):

        latest_time = os.path.getmtime(self.options['mpfconfigfile'])

        for root, dirs, files in os.walk(
                os.path.join(self.machine_path, 'config')):
            for name in files:
                if not name.startswith('.'):
                    if os.path.getmtime(os.path.join(root,
                                                     name)) > latest_time:
                        latest_time = os.path.getmtime(os.path.join(
                            root, name))

            for name in dirs:
                if not name.startswith('.'):
                    if os.path.getmtime(os.path.join(root,
                                                     name)) > latest_time:
                        latest_time = os.path.getmtime(os.path.join(
                            root, name))

        return latest_time

    def _cache_config(self):  # pragma: no cover
        with open(self._get_mpfcache_file_name(), 'wb') as f:
            pickle.dump(self.config, f, protocol=4)
            self.log.info('Config file cache created: %s',
                          self._get_mpfcache_file_name())

    def verify_system_info(self):
        """Dump information about the Python installation to the log.

        Information includes Python version, Python executable, platform, and
        core architecture.
        """
        python_version = sys.version_info

        if not (python_version[0] == 3 and
                (python_version[1] == 4 or python_version[1] == 5)):
            raise AssertionError(
                "Incorrect Python version. MPF requires "
                "Python 3.4 or 3.5. You have Python {}.{}.{}.".format(
                    python_version[0], python_version[1], python_version[2]))

        self.log.debug("Python version: %s.%s.%s", python_version[0],
                       python_version[1], python_version[2])
        self.log.debug("Platform: %s", sys.platform)
        self.log.debug("Python executable location: %s", sys.executable)
        self.log.debug("32-bit Python? %s", sys.maxsize < 2**32)

    def _load_core_modules(self):
        self.log.debug("Loading core modules...")
        for name, module in self.config['mpf']['core_modules'].items():
            self.log.debug("Loading '%s' core module", module)
            m = Util.string_to_class(module)(self)
            setattr(self, name, m)

    def _load_hardware_platforms(self):
        if not self.options['force_platform']:
            for section, platform in self.config['hardware'].items():
                if platform.lower() != 'default' and section != 'driverboards':
                    self.add_platform(platform)
            self.set_default_platform(self.config['hardware']['platform'])

        else:
            self.add_platform(self.options['force_platform'])
            self.set_default_platform(self.options['force_platform'])

    def _load_plugins(self):
        self.log.debug("Loading plugins...")

        # TODO: This should be cleaned up. Create a Plugins base class and
        # classmethods to determine if the plugins should be used.

        for plugin in Util.string_to_list(self.config['mpf']['plugins']):

            self.log.debug("Loading '%s' plugin", plugin)

            plugin_obj = Util.string_to_class(plugin)(self)
            self.plugins.append(plugin_obj)

    def _load_scriptlets(self):
        if 'scriptlets' in self.config:
            self.config['scriptlets'] = self.config['scriptlets'].split(' ')

            self.log.debug("Loading scriptlets...")

            for scriptlet in self.config['scriptlets']:

                self.log.debug("Loading '%s' scriptlet", scriptlet)

                scriptlet_obj = Util.string_to_class(
                    self.config['mpf']['paths']['scriptlets'] + "." +
                    scriptlet)(machine=self, name=scriptlet.split('.')[1])

                self.scriptlets.append(scriptlet_obj)

    def reset(self):
        """Reset the machine.

        This method is safe to call. It essentially sets up everything from
        scratch without reloading the config files and assets from disk. This
        method is called after a game ends and before attract mode begins.

        Note: This method is not yet implemented.
        """
        self.log.debug('Resetting...')
        self.events.process_event_queue()
        self.events.post('machine_reset_phase_1')
        '''Event: machine_reset_phase_1

        Desc: The first phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        '''
        self.events.process_event_queue()
        self.events.post('machine_reset_phase_2')
        '''Event: machine_reset_phase_2

        Desc: The second phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        '''
        self.events.process_event_queue()
        self.events.post('machine_reset_phase_3')
        '''Event: machine_reset_phase_3

        Desc: The third phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        '''
        self.events.process_event_queue()
        self.log.debug('Reset Complete')
        self._reset_complete()

    def add_platform(self, name):
        """Make an additional hardware platform interface available to MPF.

        Args:
            name: String name of the platform to add. Must match the name of a
                platform file in the mpf/platforms folder (without the .py
                extension).
        """
        if name not in self.hardware_platforms:

            try:
                hardware_platform = Util.string_to_class(
                    self.config['mpf']['platforms'][name])
            except ImportError:  # pragma: no cover
                raise ImportError("Cannot add hardware platform {}. This is "
                                  "not a valid platform name".format(name))

            self.hardware_platforms[name] = (hardware_platform(self))

    def set_default_platform(self, name):
        """Set the default platform.

        It is used if a device class-specific or device-specific platform is not specified.

        Args:
            name: String name of the platform to set to default.
        """
        try:
            self.default_platform = self.hardware_platforms[name]
            self.log.debug("Setting default platform to '%s'", name)
        except KeyError:
            raise AssertionError(
                "Cannot set default platform to '{}', as that's not"
                " a currently active platform".format(name))

    def register_monitor(self, monitor_class, monitor):
        """Register a monitor.

        Args:
            monitor_class: String name of the monitor class for this monitor
                that's being registered.
            monitor: String name of the monitor.

        MPF uses monitors to allow components to monitor certain internal
        elements of MPF.

        For example, a player variable monitor could be setup to be notified of
        any changes to a player variable, or a switch monitor could be used to
        allow a plugin to be notified of any changes to any switches.

        The MachineController's list of registered monitors doesn't actually
        do anything. Rather it's a dictionary of sets which the monitors
        themselves can reference when they need to do something. We just needed
        a central registry of monitors.

        """
        if monitor_class not in self.monitors:
            self.monitors[monitor_class] = set()

        self.monitors[monitor_class].add(monitor)

    def run(self):
        """Start the main machine run loop."""
        self.log.debug("Starting the main run loop.")

        self._run_loop()

    def stop(self, **kwargs):
        """Perform a graceful exit of MPF."""
        del kwargs
        if self._done:
            return

        self.log.info("Shutting down...")
        self.events.post('shutdown')
        '''event: shutdown
        desc: Posted when the machine is shutting down to give all modules a
        chance to shut down gracefully.

        '''
        self.events.process_event_queue()
        self.thread_stopper.set()
        self._platform_stop()

        self.clock.loop.stop()

    def _do_stop(self):
        if self._done:
            return

        self._done = True
        self.clock.loop.stop()
        # this is needed to properly close all sockets
        self.clock.loop.run_forever()

    def _run_loop(self):  # pragma: no cover
        # Main machine run loop with when the default platform interface
        # specifies the MPF should control the main timer

        try:
            self.clock.run()
        except KeyboardInterrupt:
            self.stop()

        if self._exception:
            print("Shutdown because of an exception:")
            raise self._exception['exception']

        self._do_stop()
        self.clock.loop.close()

    def _platform_stop(self):
        for platform in list(self.hardware_platforms.values()):
            platform.stop()

    def power_off(self, **kwargs):
        """Attempt to perform a power down of the pinball machine and ends MPF.

        This method is not yet implemented.
        """
        pass

    def _reset_complete(self):
        self.log.debug('Reset Complete')
        self.events.post('reset_complete')
        '''event: reset_complete

        desc: The machine reset process is complete

        '''

    def set_machine_var(self, name, value, force_events=False):
        """Set the value of a machine variable.

        Args:
            name: String name of the variable you're setting the value for.
            value: The value you're setting. This can be any Type.
            force_events: Boolean which will force the event posting, the
                machine monitor callback, and writing the variable to disk (if
                it's set to persist). By default these things only happen if
                the new value is different from the old value.
        """
        if name not in self.machine_vars:
            self.log.warning(
                "Received request to set machine_var '%s', but "
                "that is not a valid machine_var.", name)
            return

        prev_value = self.machine_vars[name]['value']
        self.machine_vars[name]['value'] = value

        try:
            change = value - prev_value
        except TypeError:
            change = prev_value != value

        if change or force_events:

            if self.machine_vars[name]['persist'] and self.config['mpf'][
                    'save_machine_vars_to_disk']:
                disk_var = CaseInsensitiveDict()
                disk_var['value'] = value

                if self.machine_vars[name]['expire_secs']:
                    disk_var['expire'] = self.clock.get_time(
                    ) + self.machine_vars[name]['expire_secs']

                self.machine_var_data_manager.save_key(name, disk_var)

            self.log.debug(
                "Setting machine_var '%s' to: %s, (prior: %s, "
                "change: %s)", name, value, prev_value, change)
            self.events.post('machine_var_' + name,
                             value=value,
                             prev_value=prev_value,
                             change=change)
            '''event: machine_var_(name)

            desc: Posted when a machine variable is added or changes value.
            (Machine variables are like player variables, except they're
            maintained machine-wide instead of per-player or per-game.)

            args:

            value: The new value of this machine variable.

            prev_value: The previous value of this machine variable, e.g. what
            it was before the current value.

            change: If the machine variable just changed, this will be the
            amount of the change. If it's not possible to determine a numeric
            change (for example, if this machine variable is a list), then this
            *change* value will be set to the boolean *True*.
            '''

            if self.machine_var_monitor:
                for callback in self.monitors['machine_vars']:
                    callback(name=name,
                             value=value,
                             prev_value=prev_value,
                             change=change)

    def get_machine_var(self, name):
        """Return the value of a machine variable.

        Args:
            name: String name of the variable you want to get that value for.

        Returns:
            The value of the variable if it exists, or None if the variable
            does not exist.

        """
        try:
            return self.machine_vars[name]['value']
        except KeyError:
            return None

    def is_machine_var(self, name):
        """Return true if machine variable exists."""
        return name in self.machine_vars

    # pylint: disable-msg=too-many-arguments
    def create_machine_var(self,
                           name,
                           value=0,
                           persist=False,
                           expire_secs=None,
                           silent=False):
        """Create a new machine variable.

        Args:
            name: String name of the variable.
            value: The value of the variable. This can be any Type.
            persist: Boolean as to whether this variable should be saved to
                disk so it's available the next time MPF boots.
            expire_secs: Optional number of seconds you'd like this variable
                to persist on disk for. When MPF boots, if the expiration time
                of the variable is in the past, it will be loaded with a value
                of 0. For example, this lets you write the number of credits on
                the machine to disk to persist even during power off, but you
                could set it so that those only stay persisted for an hour.
        """
        var = CaseInsensitiveDict()

        var['value'] = value
        var['persist'] = persist
        var['expire_secs'] = expire_secs

        self.machine_vars[name] = var

        if not silent:
            self.set_machine_var(name, value, force_events=True)

    def remove_machine_var(self, name):
        """Remove a machine variable by name.

        If this variable persists to disk, it will remove it from there too.

        Args:
            name: String name of the variable you want to remove.
        """
        try:
            del self.machine_vars[name]
            self.machine_var_data_manager.remove_key(name)
        except KeyError:
            pass

    def remove_machine_var_search(self, startswith='', endswith=''):
        """Remove a machine variable by matching parts of its name.

        Args:
            startswith: Optional start of the variable name to match.
            endswith: Optional end of the variable name to match.

        For example, if you pass startswit='player' and endswith='score', this
        method will match and remove player1_score, player2_score, etc.
        """
        for var in list(self.machine_vars.keys()):
            if var.startswith(startswith) and var.endswith(endswith):
                del self.machine_vars[var]
                self.machine_var_data_manager.remove_key(var)

    def get_platform_sections(self, platform_section, overwrite):
        """Return platform section."""
        if not self.options['force_platform']:
            if not overwrite:
                if self.config['hardware'][platform_section] != 'default':
                    return self.hardware_platforms[self.config['hardware']
                                                   [platform_section]]
                else:
                    return self.default_platform
            else:
                try:
                    return self.hardware_platforms[overwrite]
                except KeyError:
                    self.add_platform(overwrite)
                    return self.hardware_platforms[overwrite]
        else:
            return self.default_platform

    def register_boot_hold(self, hold):
        """Register a boot hold."""
        if self.is_init_done:
            raise AssertionError("Register hold after init_done")
        self._boot_holds.add(hold)

    def clear_boot_hold(self, hold):
        """Clear a boot hold."""
        if self.is_init_done:
            raise AssertionError("Clearing hold after init_done")
        self._boot_holds.remove(hold)
        self.log.debug('Clearing boot hold %s. Holds remaining: %s', hold,
                       self._boot_holds)
        if not self._boot_holds:
            self.init_done()

    def init_done(self):
        """Finish init.

        Called when init is done and all boot holds are cleared.
        """
        self.is_init_done = True

        self.events.post("init_done")
        '''event: init_done

        desc: Posted when the initial (one-time / boot) init phase is done. In
        other words, once this is posted, MPF is booted and ready to go.
        '''
        self.events.process_event_queue()

        ConfigValidator.unload_config_spec()
        self.reset()
예제 #20
0
class SwitchController(MpfController):
    """Handles all switches in the machine.

    Base class for the switch controller, which is responsible for receiving
    all switch activity in the machine and converting them into events.

    More info:
    http://docs.missionpinball.org/en/latest/core/switch_controller.html

    """

    log = logging.getLogger('SwitchController')

    def __init__(self, machine):
        """Initialise switch controller."""
        super().__init__(machine)
        self.registered_switches = CaseInsensitiveDict()
        # Dictionary of switches and states that have been registered for
        # callbacks.

        self._timed_switch_handler_delay = None

        self.active_timed_switches = defaultdict(list)
        # Dictionary of switches that are currently in a state counting ms
        # waiting to notify their handlers. In other words, this is the dict
        # that tracks current switches for things like "do foo() if switch bar
        # is active for 100ms."

        self.switches = CaseInsensitiveDict()
        # Dictionary which holds the master list of switches as well as their
        # current states. State here does factor in whether a switch is NO or
        # NC so 1 = active and 0 = inactive.

        self.switch_event_active = (
            self.machine.config['mpf']['switch_event_active'])
        self.switch_event_inactive = (
            self.machine.config['mpf']['switch_event_inactive'])
        self.switch_tag_event = (
            self.machine.config['mpf']['switch_tag_event'])

        # register for events
        self.machine.events.add_handler('init_phase_2',
                                        self._initialize_switches, 1000)
        # priority 1000 so this fires first

        self.machine.events.add_handler('machine_reset_phase_3',
                                        self.log_active_switches)

        self.monitors = list()

    def register_switch(self, name):
        """Populate self.registered_switches.

        Args:
            name: Name of switch
        """
        self.registered_switches[name + '-0'] = list()
        self.registered_switches[name + '-1'] = list()

        self.set_state(name, 0, reset_time=True)

    def _initialize_switches(self, **kwargs):
        del kwargs
        self.update_switches_from_hw()

        for switch in self.machine.switches:
            # Populate self.switches
            self.set_state(switch.name, switch.state, reset_time=True)

            if self.machine.config['mpf']['auto_create_switch_events']:
                switch.activation_events.add(
                    self.machine.config['mpf']['switch_event_active'].replace(
                        '%', switch.name))

                switch.deactivation_events.add(
                    self.machine.config['mpf']
                    ['switch_event_inactive'].replace('%', switch.name))

            if 'events_when_activated' in switch.config:
                for event in Util.string_to_lowercase_list(
                        switch.config['events_when_activated']):

                    if "|" in event:
                        ev_name, ev_time = event.split("|")
                        self.add_switch_handler(
                            switch_name=switch.name,
                            callback=self.machine.events.post,
                            ms=Util.string_to_ms(ev_time),
                            callback_kwargs={'event': ev_name})
                    else:
                        switch.activation_events.add(event)

            if 'events_when_deactivated' in switch.config:
                for event in Util.string_to_lowercase_list(
                        switch.config['events_when_deactivated']):

                    if "|" in event:
                        ev_name, ev_time = event.split("|")
                        self.add_switch_handler(
                            switch_name=switch.name,
                            callback=self.machine.events.post,
                            state=0,
                            ms=Util.string_to_ms(ev_time),
                            callback_kwargs={'event': ev_name})
                    else:
                        switch.deactivation_events.add(event)

    def update_switches_from_hw(self):
        """Update the states of all the switches be re-reading the states from the hardware platform.

        This method works silently and does not post any events if any switches
        changed state.
        """
        # create a list of hw switch numbers, platforms, and switch objects
        platforms = set()
        switches = set()  # (switch_object, number)

        for switch in self.machine.switches:
            platforms.add(switch.platform)
            switches.add((switch, switch.hw_switch.number))

        for platform in platforms:
            switch_states = platform.get_hw_switch_states()

            for switch, number in switches:
                # if two platforms have the same number choose the right switch
                if switch.platform != platform:
                    continue
                try:
                    switch.state = switch_states[number] ^ switch.invert
                    switch.time = self.machine.clock.get_time()
                except (IndexError, KeyError):
                    raise AssertionError(
                        "Missing switch {} in update from hw.  Update from HW: {}, switches: {}"
                        .format(number, switch_states, switches))

    def verify_switches(self) -> bool:
        """Verify that switches states match the hardware.

        Loop through all the switches and queries their hardware states via
        their platform interfaces and them compares that to the state that MPF
        thinks the switches are in.

        Throws logging warnings if anything doesn't match.

        This method is notification only. It doesn't fix anything.
        """
        current_states = dict()

        for switch in self.machine.switches:
            current_states[switch] = switch.state

        self.update_switches_from_hw()

        ok = True

        for switch in self.machine.switches:
            if switch.state != current_states[switch]:  # pragma: no cover
                ok = False
                self.warning_log(
                    "Switch State Error! Switch: %s, HW State: "
                    "%s, MPF State: %s", switch.name, current_states[switch],
                    switch.state)

        return ok

    def is_state(self, switch_name, state, ms=0):
        """Check if switch is in state.

        Query whether a switch is in a given state and (optionally)
        whether it has been in that state for the specified number of ms.

        Returns True if the switch_name has been in the state for the given
        number of ms. If ms is not specified, returns True if the switch
        is in the state regardless of how long it's been in that state.
        """
        if not ms:
            ms = 0

        return self.switches[switch_name][
            'state'] == state and ms <= self.ms_since_change(switch_name)

    def is_active(self, switch_name, ms=None):
        """Query whether a switch is active.

        Returns True if the current switch is active. If optional arg ms
        is passed, will only return true if switch has been active for that
        many ms.

        Note this method does consider whether a switch is NO or NC. So an NC
        switch will show as active if it is open, rather than closed.
        """
        return self.is_state(switch_name=switch_name, state=1, ms=ms)

    def is_inactive(self, switch_name, ms=None):
        """Query whether a switch is inactive.

        Returns True if the current switch is inactive. If optional arg
        `ms` is passed, will only return true if switch has been inactive
        for that many ms.

        Note this method does consider whether a switch is NO or NC. So an NC
        switch will show as active if it is closed, rather than open.
        """
        return self.is_state(switch_name=switch_name, state=0, ms=ms)

    def ms_since_change(self, switch_name):
        """Return the number of ms that have elapsed since this switch last changed state."""
        return round((self.machine.clock.get_time() -
                      self.switches[switch_name]['time']) * 1000.0, 0)

    def set_state(self, switch_name, state=1, reset_time=False):
        """Set the state of a switch."""
        if reset_time:
            timestamp = -100000  # clock can be 0 at start
        else:
            timestamp = self.machine.clock.get_time()

        self.switches.update(
            {switch_name: {
                'state': state,
                'time': timestamp
            }})

        # todo this method does not set the switch device's state. Either get
        # rid of it, or move the switch device settings from process_switch()
        # to here.

    def process_switch_by_num(self, num, state, platform, logical=False):
        """Process a switch state change by switch number."""
        for switch in self.machine.switches:
            if switch.hw_switch.number == num and switch.platform == platform:
                self.process_switch_obj(obj=switch,
                                        state=state,
                                        logical=logical)
                return

        self.debug_log("Unknown switch %s change to state %s on platform %s",
                       num, state, platform)
        # if the switch is not configured still trigger the monitor
        for monitor in self.monitors:
            monitor(
                MonitoredSwitchChange(name=str(num),
                                      label="{}-{}".format(
                                          str(platform), str(num)),
                                      platform=platform,
                                      num=str(num),
                                      state=state))

    def process_switch(self, name, state=1, logical=False):
        """Process a new switch state change for a switch by name.

        Args:
            name: The string name of the switch.
            state: Boolean or int of state of the switch you're processing,
                True/1 is active, False/0 is inactive.
            logical: Boolean which specifies whether the 'state' argument
                represents the "physical" or "logical" state of the switch. If
                True, a 1 means this switch is active and a 0 means it's
                inactive, regardless of the NC/NO configuration of the switch.
                If False, then the state paramenter passed will be inverted if
                the switch is configured to be an 'NC' type. Typically the
                hardware will send switch states in their raw (logical=False)
                states, but other interfaces like the keyboard and OSC will use
                logical=True.

        This is the method that is called by the platform driver whenever a
        switch changes state. It's also used by the "other" modules that
        activate switches, including the keyboard and OSC interfaces.

        State 0 means the switch changed from active to inactive, and 1 means
        it changed from inactive to active. (The hardware & platform code
        handles NC versus NO switches and translates them to 'active' versus
        'inactive'.)
        """
        self.debug_log("Processing switch. Name: %s, state: %s, logical: %s,",
                       name, state, logical)

        try:
            obj = self.machine.switches[name]
        except KeyError:  # pragma: no cover
            raise AssertionError("Cannot process switch \"" + name + "\" as "
                                 "this is not a valid switch name.")

        self.process_switch_obj(obj, state, logical)

    def process_switch_obj(self, obj: Switch, state, logical):
        """Process a new switch state change for a switch by name.

        Args:
            obj: The switch object.
            state: Boolean or int of state of the switch you're processing,
                True/1 is active, False/0 is inactive.
            logical: Boolean which specifies whether the 'state' argument
                represents the "physical" or "logical" state of the switch. If
                True, a 1 means this switch is active and a 0 means it's
                inactive, regardless of the NC/NO configuration of the switch.
                If False, then the state paramenter passed will be inverted if
                the switch is configured to be an 'NC' type. Typically the
                hardware will send switch states in their raw (logical=False)
                states, but other interfaces like the keyboard and OSC will use
                logical=True.

        This is the method that is called by the platform driver whenever a
        switch changes state. It's also used by the "other" modules that
        activate switches, including the keyboard and OSC interfaces.

        State 0 means the switch changed from active to inactive, and 1 means
        it changed from inactive to active. (The hardware & platform code
        handles NC versus NO switches and translates them to 'active' versus
        'inactive'.)
        """
        # We need int, but this lets it come in as boolean also
        if state:
            state = 1
        else:
            state = 0

        # flip the logical & physical states for NC switches
        hw_state = state

        if obj.invert:
            if logical:  # NC + logical means hw_state is opposite of state
                hw_state ^= 1
            else:
                # NC w/o logical (i.e. hardware state was sent) means logical
                # state is the opposite
                state ^= 1

        # Update the hardware state since we always want this to match real hw
        obj.hw_state = hw_state

        # if the switch is active, check to see if it's recycle_time has passed
        if state and not self._check_recycle_time(obj, state):
            self.machine.clock.schedule_once(lambda dt: self._recycle_passed(
                obj, state, logical, obj.hw_state),
                                             timeout=obj.recycle_clear_time -
                                             self.machine.clock.get_time())
            return

        obj.state = state  # update the switch device

        if state:
            # update the switch's next recycle clear time
            obj.recycle_clear_time = (self.machine.clock.get_time() +
                                      obj.recycle_secs)

        # if the switch is already in this state, then abort
        if self.switches[obj.name]['state'] == state:

            if not obj.recycle_secs:
                self.warning_log(
                    "Received duplicate switch state, which means this switch "
                    "had some non-debounced state changes. This could be "
                    "nothing, but if it happens a lot it could indicate noise "
                    "or interference on the line. Switch: %s", obj.name)
            return

        if state:
            self.info_log("<<<<<<< '{}' active >>>>>>>".format(obj.name))
        else:
            self.info_log("<<<<<<< '{}' inactive >>>>>>>".format(obj.name))

        # Update the switch controller's logical state for this switch
        self.set_state(obj.name, state)

        self._call_handlers(obj.name, state)

        self._cancel_timed_handlers(obj.name, state)

        for monitor in self.monitors:
            monitor(
                MonitoredSwitchChange(name=obj.name,
                                      label=obj.label,
                                      platform=obj.platform,
                                      num=obj.hw_switch.number,
                                      state=state))

        self._post_switch_events(obj.name, state)

    def _recycle_passed(self, obj, state, logical, hw_state):
        if obj.hw_state == hw_state:
            self.process_switch(obj.name, state, logical)

    def wait_for_switch(self,
                        switch_name: str,
                        state: int = 1,
                        only_on_change=True,
                        ms=0):
        """Wait for a switch to change into state."""
        return self.wait_for_any_switch([switch_name], state, only_on_change,
                                        ms)

    def wait_for_any_switch(self,
                            switch_names: [str],
                            state: int = 1,
                            only_on_change=True,
                            ms=0):
        """Wait for the first switch in the list to change into state."""
        future = asyncio.Future(loop=self.machine.clock.loop)

        if not only_on_change:
            for switch_name in switch_names:
                if self.is_state(switch_name, state, ms):
                    future.set_result({
                        "switch_name": switch_name,
                        "state": state,
                        "ms": ms
                    })
                    return future

        handlers = []
        future.add_done_callback(partial(self._future_done, handlers))
        for switch_name in switch_names:
            handlers.append(
                self.add_switch_handler(switch_name,
                                        state=state,
                                        ms=ms,
                                        callback=partial(
                                            self._wait_handler,
                                            ms=ms,
                                            _future=future,
                                            switch_name=switch_name)))
        return future

    def _future_done(self, handlers, future):
        del future
        for handler in handlers:
            self.remove_switch_handler_by_key(handler)

    @staticmethod
    def _wait_handler(_future: asyncio.Future, **kwargs):
        if not _future.done():
            _future.set_result(result=kwargs)

    def _cancel_timed_handlers(self, name, state):
        # now check if the opposite state is in the active timed switches list
        # if so, remove it
        for k, v, in list(self.active_timed_switches.items()):
            # using items() instead of iteritems() since we might want to
            # delete while iterating
            for k2, item in enumerate(v):
                if item['switch_action'] == str(name) + '-' + str(state ^ 1):
                    # ^1 in above line invertes the state
                    if self.active_timed_switches[
                            k] and self.active_timed_switches[k][k2]:
                        del self.active_timed_switches[k][k2]

    def _add_timed_switch_handler(self, key, value):
        self.active_timed_switches[key].append(value)

        if self._timed_switch_handler_delay:
            self.machine.clock.unschedule(self._timed_switch_handler_delay)
        self._timed_switch_handler_delay = self.machine.clock.schedule_once(
            self._process_active_timed_switches,
            self.get_next_timed_switch_event() - self.machine.clock.get_time())

    def _call_handlers(self, name, state):
        # Combine name & state so we can look it up
        switch_key = str(name) + '-' + str(state)

        # Do we have any registered handlers for this switch/state combo?
        if switch_key in self.registered_switches:
            for entry in self.registered_switches[switch_key][:]:  # generator?
                # Found an entry.

                # skip if the handler has been removed in the meantime
                if entry not in self.registered_switches[switch_key]:
                    continue

                if entry['ms']:
                    # This entry is for a timed switch, so add it to our
                    # active timed switch list
                    key = self.machine.clock.get_time() + (entry['ms'] /
                                                           1000.0)
                    value = {
                        'switch_action': str(name) + '-' + str(state),
                        'callback': entry['callback'],
                        'switch_name': name,
                        'state': state,
                        'ms': entry['ms'],
                        'removed': False,
                        'return_info': entry['return_info'],
                        'callback_kwargs': entry['callback_kwargs']
                    }
                    self._add_timed_switch_handler(key, value)
                    self.debug_log(
                        "Found timed switch handler for k/v %s / %s", key,
                        value)
                else:
                    # This entry doesn't have a timed delay, so do the action
                    # now
                    if entry['return_info']:

                        entry['callback'](switch_name=name,
                                          state=state,
                                          ms=0,
                                          **entry['callback_kwargs'])
                    else:
                        entry['callback'](**entry['callback_kwargs'])

                        # todo need to add args and kwargs support to callback

    def add_monitor(self, monitor):
        """Add a monitor callback which is called on switch changes."""
        if monitor not in self.monitors:
            self.monitors.append(monitor)

    def remove_monitor(self, monitor):
        """Remove a monitor callback."""
        if monitor in self.monitors:
            self.monitors.remove(monitor)

    # pylint: disable-msg=too-many-arguments
    def add_switch_handler(self,
                           switch_name,
                           callback,
                           state=1,
                           ms=0,
                           return_info=False,
                           callback_kwargs=None) -> SwitchHandler:
        """Register a handler to take action on a switch event.

        Args:
            switch_name: String name of the switch you're adding this handler
                for.
            callback: The method you want called when this switch handler fires.
            state: Integer of the state transition you want to callback to be
                triggered on. Default is 1 which means it's called when the
                switch goes from inactive to active, but you can also use 0
                which means your callback will be called when the switch becomes
                inactive
            ms: Integer. If you specify a 'ms' parameter, the handler won't be
                called until the witch is in that state for that many
                milliseconds (rounded up to the nearst machine timer tick).
            return_info: If True, the switch controller will pass the
                parameters of the switch handler as arguments to the callback,
                including switch_name, state, and ms. If False (default), it
                just calls the callback with no parameters.
            callback_kwargs: Additional kwargs that will be passed with the
                callback.

        You can mix & match entries for the same switch here.
        """
        if not callback_kwargs:
            callback_kwargs = dict()

        self.debug_log(
            "Registering switch handler: %s, %s, state: %s, ms: %s"
            ", info: %s, cb_kwargs: %s", switch_name, callback, state, ms,
            return_info, callback_kwargs)

        entry_val = {
            'ms': ms,
            'callback': callback,
            'return_info': return_info,
            'callback_kwargs': callback_kwargs
        }
        entry_key = str(switch_name) + '-' + str(state)

        self.registered_switches[entry_key].append(entry_val)

        # If the switch handler that was just registered has a delay (i.e. ms>0,
        # then let's see if the switch is currently in the state that the
        # handler was registered for. If so, and if the switch has been in this
        # state for less time than the ms registered, then we need to add this
        # switch to our active_timed_switches list so this handler is called
        # when this switch's active time expires. (in other words, we're
        # catching delayed switches that were in progress when this handler was
        # registered.

        if ms:  # only do this for handlers that have delays
            if state == 1:
                if self.is_active(
                        switch_name,
                        0) and (self.ms_since_change(switch_name) < ms):
                    # figure out when this handler should fire based on the
                    # switch's original activation time.
                    key = self.machine.clock.get_time() + (
                        (ms - self.ms_since_change(switch_name)) / 1000.0)
                    value = {
                        'switch_action': entry_key,
                        'callback': callback,
                        'switch_name': switch_name,
                        'state': state,
                        'ms': ms,
                        'removed': False,
                        'return_info': return_info,
                        'callback_kwargs': callback_kwargs
                    }
                    self._add_timed_switch_handler(key, value)
            elif state == 0:
                if self.is_inactive(
                        switch_name,
                        0) and (self.ms_since_change(switch_name) < ms):
                    key = self.machine.clock.get_time() + (
                        (ms - self.ms_since_change(switch_name)) / 1000.0)
                    value = {
                        'switch_action': entry_key,
                        'callback': callback,
                        'switch_name': switch_name,
                        'state': state,
                        'ms': ms,
                        'removed': False,
                        'return_info': return_info,
                        'callback_kwargs': callback_kwargs
                    }
                    self._add_timed_switch_handler(key, value)

        # Return the args we used to setup this handler for easy removal later
        return SwitchHandler(switch_name, callback, state, ms)

    def remove_switch_handler_by_key(self, switch_handler: SwitchHandler):
        """Remove switch hander by key returned from add_switch_handler."""
        self.remove_switch_handler(switch_handler.switch_name,
                                   switch_handler.callback,
                                   switch_handler.state, switch_handler.ms)

    def remove_switch_handler(self, switch_name, callback, state=1, ms=0):
        """Remove a registered switch handler.

        Currently this only works if you specify everything exactly as you set
        it up. (Except for return_info, which doesn't matter if true or false, it
        will remove either / both.
        """
        self.debug_log(
            "Removing switch handler. Switch: %s, State: %s, ms: %s",
            switch_name, state, ms)

        entry_key = str(switch_name) + '-' + str(state)

        if entry_key in self.registered_switches:
            for dummy_index, settings in enumerate(
                    self.registered_switches[entry_key]):
                if settings['ms'] == ms and settings['callback'] == callback:
                    self.registered_switches[entry_key].remove(settings)

        for dummy_timed_key, timed_entry in self.active_timed_switches.items():
            for dummy_key, entry in enumerate(timed_entry):
                if entry['switch_action'] == entry_key and entry[
                        'ms'] == ms and entry['callback'] == callback:
                    entry['removed'] = True

    def log_active_switches(self, **kwargs):
        """Write out entries to the log file of all switches that are currently active.

        This is used to set the "initial" switch states of standalone testing
        tools, like our log file playback utility, but it might be useful in
        other scenarios when weird things are happening.

        This method dumps these events with logging level "INFO."
        """
        del kwargs
        for k, v in self.switches.items():
            if v['state']:
                self.info_log("Found active switch: %s", k)

    def _check_recycle_time(self, switch, state):
        # checks to see when a switch is ok to be activated again after it's
        # been last activated

        if self.machine.clock.get_time() >= switch.recycle_clear_time:
            return True

        else:
            if state:
                switch.recycle_jitter_count += 1
            return False

    @staticmethod
    def get_active_event_for_switch(switch_name):
        """Return the event name which is posted when switch_name becomes active."""
        return "{}_active".format(switch_name)

    def _post_switch_events(self, switch_name, state):
        """Post the game events based on this switch changing state."""
        # the following events all fire the moment a switch goes active
        if state == 1:

            for event in self.machine.switches[switch_name].activation_events:
                self.machine.events.post(event)

            for tag in self.machine.switches[switch_name].tags:
                self.machine.events.post(
                    self.switch_tag_event.replace('%', tag))
                '''event: sw_(tag_name)

                desc: A switch tagged with *tag_name* was just activated.

                For example, if in the ``switches:`` section of your config, you
                have a switch with ``tags: start, hello``, then the events
                *sw_start* and *sw_hello* will be posted when that switch is hit.

                Note that you can change the format of these events in your
                machine config file, but *sw_(tag_name)* is the default.

                '''

                self.machine.events.post(
                    self.switch_tag_event.replace('%', tag) + "_active")

        # the following events all fire the moment a switch becomes inactive
        elif state == 0:
            for event in self.machine.switches[
                    switch_name].deactivation_events:
                self.machine.events.post(event)

            for tag in self.machine.switches[switch_name].tags:
                self.machine.events.post(
                    self.switch_tag_event.replace('%', tag) + "_inactive")

    def get_next_timed_switch_event(self):
        """Return time of the next timed switch event."""
        if not self.active_timed_switches:
            raise AssertionError("No active timed switches")
        return min(self.active_timed_switches.keys())

    def _process_active_timed_switches(self, dt):
        """Process active times switches.

        Checks the current list of active timed switches to see if it's
        time to take action on any of them. If so, does the callback and then
        removes that entry from the list.
        """
        del dt
        next_event_time = False
        for k in list(self.active_timed_switches.keys()):
            if k <= self.machine.clock.get_time():  # change to generator?
                for entry in self.active_timed_switches[k]:
                    if entry['removed']:
                        continue
                    self.debug_log(
                        "Processing timed switch handler. Switch: %s "
                        " State: %s, ms: %s", entry['switch_name'],
                        entry['state'], entry['ms'])
                    if entry['return_info']:
                        entry['callback'](switch_name=entry['switch_name'],
                                          state=entry['state'],
                                          ms=entry['ms'],
                                          **entry['callback_kwargs'])
                    else:
                        entry['callback'](**entry['callback_kwargs'])
                del self.active_timed_switches[k]
            else:
                if not next_event_time or next_event_time > k:
                    next_event_time = k

        self.machine.events.process_event_queue()
        if next_event_time:
            if self._timed_switch_handler_delay:
                self.machine.clock.unschedule(self._timed_switch_handler_delay)
            self._timed_switch_handler_delay = self.machine.clock.schedule_once(
                self._process_active_timed_switches,
                next_event_time - self.machine.clock.get_time())
예제 #21
0
    def __init__(self, mpf_path: str, machine_path: str, options: dict):
        """Initialise machine controller."""
        self.log = logging.getLogger("Machine")
        self.log.info("Mission Pinball Framework Core Engine v%s", __version__)

        self.log.debug("Command line arguments: %s", options)
        self.options = options

        self.log.debug("MPF path: %s", mpf_path)
        self.mpf_path = mpf_path

        self.log.info("Machine path: %s", machine_path)
        self.machine_path = machine_path

        self.log.debug("Command line arguments: %s", self.options)
        self.verify_system_info()
        self._exception = None

        self._boot_holds = set()
        self.is_init_done = False
        self.register_boot_hold('init')

        self._done = False
        self.monitors = dict()
        self.plugins = list()
        self.scriptlets = list()
        self.modes = DeviceCollection(self, 'modes', None)
        self.game = None
        self.active_debugger = dict()
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.machine_var_data_manager = None
        self.thread_stopper = threading.Event()

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

        self.crash_queue = queue.Queue()

        self.config = None
        self.events = None
        self.machine_config = None
        self._set_machine_path()

        self.config_validator = ConfigValidator(self)

        self._load_config()

        self.clock = self._load_clock()
        self._crash_queue_checker = self.clock.schedule_interval(
            self._check_crash_queue, 1)

        self.hardware_platforms = dict()
        self.default_platform = None

        self._load_hardware_platforms()

        self._initialize_credit_string()

        self._load_core_modules()
        # order is specified in mpfconfig.yaml

        # This is called so hw platforms have a chance to register for events,
        # and/or anything else they need to do with core modules since
        # they're not set up yet when the hw platforms are constructed.
        self._initialize_platforms()

        self._validate_config()

        self._register_config_players()
        self._register_system_events()
        self._load_machine_vars()
        self._run_init_phases()

        ConfigValidator.unload_config_spec()

        self.clear_boot_hold('init')
예제 #22
0
    def set_machine_var(self, name, value, force_events=False):
        """Set the value of a machine variable.

        Args:
            name: String name of the variable you're setting the value for.
            value: The value you're setting. This can be any Type.
            force_events: Boolean which will force the event posting, the
                machine monitor callback, and writing the variable to disk (if
                it's set to persist). By default these things only happen if
                the new value is different from the old value.
        """
        if name not in self.machine_vars:
            self.log.warning(
                "Received request to set machine_var '%s', but "
                "that is not a valid machine_var.", name)
            return

        prev_value = self.machine_vars[name]['value']
        self.machine_vars[name]['value'] = value

        try:
            change = value - prev_value
        except TypeError:
            change = prev_value != value

        if change or force_events:

            if self.machine_vars[name]['persist'] and self.config['mpf'][
                    'save_machine_vars_to_disk']:
                disk_var = CaseInsensitiveDict()
                disk_var['value'] = value

                if self.machine_vars[name]['expire_secs']:
                    disk_var['expire'] = self.clock.get_time(
                    ) + self.machine_vars[name]['expire_secs']

                self.machine_var_data_manager.save_key(name, disk_var)

            self.log.debug(
                "Setting machine_var '%s' to: %s, (prior: %s, "
                "change: %s)", name, value, prev_value, change)
            self.events.post('machine_var_' + name,
                             value=value,
                             prev_value=prev_value,
                             change=change)
            '''event: machine_var_(name)

            desc: Posted when a machine variable is added or changes value.
            (Machine variables are like player variables, except they're
            maintained machine-wide instead of per-player or per-game.)

            args:

            value: The new value of this machine variable.

            prev_value: The previous value of this machine variable, e.g. what
            it was before the current value.

            change: If the machine variable just changed, this will be the
            amount of the change. If it's not possible to determine a numeric
            change (for example, if this machine variable is a list), then this
            *change* value will be set to the boolean *True*.
            '''

            if self.machine_var_monitor:
                for callback in self.monitors['machine_vars']:
                    callback(name=name,
                             value=value,
                             prev_value=prev_value,
                             change=change)
예제 #23
0
class SwitchController(MpfController):

    """Tracks all switches in the machine, receives switch activity, and converts switch changes into events."""

    log = logging.getLogger('SwitchController')
    config_name = "switch_controller"

    def __init__(self, machine: MachineController) -> None:
        """Initialise switch controller."""
        super().__init__(machine)
        self.registered_switches = CaseInsensitiveDict()        # type: Dict[str, List[RegisteredSwitch]]
        # Dictionary of switches and states that have been registered for
        # callbacks.

        self._timed_switch_handler_delay = None                 # type: Any

        self.active_timed_switches = defaultdict(list)          # type: Dict[float, List[TimedSwitchHandler]]
        # Dictionary of switches that are currently in a state counting ms
        # waiting to notify their handlers. In other words, this is the dict
        # that tracks current switches for things like "do foo() if switch bar
        # is active for 100ms."

        self.switches = CaseInsensitiveDict()                   # type: Dict[str, SwitchState]
        # Dictionary which holds the master list of switches as well as their
        # current states. State here does factor in whether a switch is NO or
        # NC so 1 = active and 0 = inactive.

        # register for events
        self.machine.events.add_async_handler('init_phase_2', self._initialize_switches, 1000)
        # priority 1000 so this fires first

        self.machine.events.add_handler('machine_reset_phase_3', self.log_active_switches)

        self.monitors = list()      # type: List[Callable[[MonitoredSwitchChange], None]]

    def register_switch(self, name):
        """Add the name of a switch to the switch controller for tracking.

        Args:
            name: String name of the switch to add
        """
        self.registered_switches[name + '-0'] = list()
        self.registered_switches[name + '-1'] = list()

        self.set_state(name, 0, reset_time=True)

    @asyncio.coroutine
    def _initialize_switches(self, **kwargs):
        del kwargs
        yield from self.update_switches_from_hw()

        for switch in self.machine.switches:
            # Populate self.switches
            self.set_state(switch.name, switch.state, reset_time=True)

    @asyncio.coroutine
    def update_switches_from_hw(self):
        """Update the states of all the switches be re-reading the states from the hardware platform.

        This method works silently and does not post any events if any switches
        changed state.
        """
        # create a list of hw switch numbers, platforms, and switch objects
        platforms = set()
        switches = set()  # (switch_object, number)

        for switch in self.machine.switches:
            platforms.add(switch.platform)
            switches.add((switch, switch.hw_switch.number))

        for platform in platforms:
            switch_states = yield from platform.get_hw_switch_states()

            for switch, number in switches:
                # if two platforms have the same number choose the right switch
                if switch.platform != platform:
                    continue
                try:
                    switch.state = switch_states[number] ^ switch.invert
                except (IndexError, KeyError):
                    raise AssertionError("Missing switch {} in update from hw.  Update from HW: {}, switches: {}".
                                         format(number, switch_states, switches))

    def verify_switches(self) -> bool:
        """Verify that switches states match the hardware.

        Loops through all the switches and queries their hardware states via
        their platform interfaces and then compares that to the state that MPF
        thinks the switches are in.

        Throws logging warnings if anything doesn't match.

        This method is notification only. It doesn't fix anything.
        """
        current_states = dict()

        for switch in self.machine.switches.values():
            current_states[switch] = switch.state

        self.update_switches_from_hw()

        ok = True

        for switch in self.machine.switches.values():
            if switch.state != current_states[switch]:  # pragma: no cover
                ok = False
                self.warning_log("Switch State Error! Switch: %s, HW State: "
                                 "%s, MPF State: %s", switch.name,
                                 current_states[switch],
                                 switch.state)

        return ok

    def is_state(self, switch_name, state, ms=0):
        """Check if switch is in state.

        Query whether a switch is in a given state and (optionally)
        whether it has been in that state for the specified number of ms.

        Args:
            switch_name: String name of the switch to check.
            state: Bool of the state to check. True is active and False is
                inactive.
            ms: Milliseconds that the switch has been in that state. If this
                is non-zero, then this method will only return True if the
                switch has been in that state for at least the number of ms
                specified.

        Returns: True if the switch_name has been in the state for the given
            number of ms. If ms is not specified, returns True if the switch
            is in the state regardless of how long it's been in that state.
        """
        if not ms:
            ms = 0

        return self.switches[switch_name].state == state and ms <= self.ms_since_change(switch_name)

    def is_active(self, switch_name, ms=None):
        """Query whether a switch is active.

        Args:
            switch_name: String name of the switch to check.
            ms: Milliseconds that the switch has been active. If this
                is non-zero, then this method will only return True if the
                switch has been in that state for at least the number of ms
                specified.

        Returns: True if the switch_name has been active for the given
            number of ms. If ms is not specified, returns True if the switch
            is in the state regardless of how long it's been in that state.
        """
        return self.is_state(switch_name=switch_name,
                             state=1,
                             ms=ms)

    def is_inactive(self, switch_name, ms=None):
        """Query whether a switch is inactive.

        Args:
            switch_name: String name of the switch to check.
            ms: Milliseconds that the switch has been inactive. If this
                is non-zero, then this method will only return True if the
                switch has been in that state for at least the number of ms
                specified.

        Returns: True if the switch_name has been inactive for the given
            number of ms. If ms is not specified, returns True if the switch
            is in the state regardless of how long it's been in that state.
        """
        return self.is_state(switch_name=switch_name,
                             state=0,
                             ms=ms)

    def ms_since_change(self, switch_name):
        """Return the number of ms that have elapsed since this switch last changed state.

        Args:
            switch_name: String name of the switch to check.

        Returns:
            Integer of milliseconds.
        """
        return round((self.machine.clock.get_time() - self.switches[switch_name].time) * 1000.0, 0)

    def set_state(self, switch_name, state=1, reset_time=False):
        """Set the state of a switch.

        Note that since this method just sets the logical state of the switch,
        weird things can happen if the state diverges from the physical state
        of the switch.

        It's mainly used with the virtual platforms to set the initial states
        of switches on MPF boot.

        Args:
            switch_name: String name of the switch to set.
            state: Logical state to set. 0 is inactive and 1 is active.
            reset_time: Sets the timestamp of the change to -100000 which
                indicates that this switch was in this state when the machine
                was powered on and therefore the various timed switch
                handlers will not be triggered.

        """
        if reset_time:
            timestamp = -100000     # clock can be 0 at start
        else:
            timestamp = self.machine.clock.get_time()

        self.switches[switch_name] = SwitchState(state=state, time=timestamp)

    def process_switch_by_num(self, num, state, platform, logical=False):
        """Process a switch state change by switch number.

        Args:
            num: The switch number (based on the platform number) for the
                switch you're setting.
            state: The state to set, either 0 or 1.
            platform: The platform this switch is on.
            logical: Whether the state you're setting is the logical or
                physical state of the switch. If a switch is NO (normally
                open), then the logical and physical states will be the same.
                NC (normally closed) switches will have physical and
                logical states that are inverted from each other.

        """
        for switch in self.machine.switches:
            if switch.hw_switch.number == num and switch.platform == platform:
                self.process_switch_obj(obj=switch, state=state, logical=logical)
                return

        self.debug_log("Unknown switch %s change to state %s on platform %s", num, state, platform)
        # if the switch is not configured still trigger the monitor
        for monitor in self.monitors:
            monitor(MonitoredSwitchChange(name=str(num), label="{}-{}".format(str(platform), str(num)),
                                          platform=platform, num=str(num), state=state))

    def process_switch(self, name, state=1, logical=False):
        """Process a new switch state change for a switch by name.

        This is the method that is called by the platform driver whenever a
        switch changes state. It's also used by the "other" modules that
        activate switches, including the keyboard and OSC interfaces.

        State 0 means the switch changed from active to inactive, and 1 means
        it changed from inactive to active. (The hardware & platform code
        handles NC versus NO switches and translates them to 'active' versus
        'inactive'.)

        Args:
            name: The string name of the switch.
            state: Boolean or int of state of the switch you're processing,
                True/1 is active, False/0 is inactive.
            logical: Boolean which specifies whether the 'state' argument
                represents the "physical" or "logical" state of the switch. If
                True, a 1 means this switch is active and a 0 means it's
                inactive, regardless of the NC/NO configuration of the switch.
                If False, then the state parameter passed will be inverted if
                the switch is configured to be an 'NC' type. Typically the
                hardware will send switch states in their raw (logical=False)
                states, but other interfaces like the keyboard and OSC will use
                logical=True.

        """
        self.debug_log("Processing switch. Name: %s, state: %s, logical: %s,", name, state, logical)

        try:
            obj = self.machine.switches[name]
        except KeyError:    # pragma: no cover
            raise AssertionError("Cannot process switch \"" + name + "\" as "
                                 "this is not a valid switch name.")

        self.process_switch_obj(obj, state, logical)

    def process_switch_obj(self, obj: Switch, state, logical):
        """Process a new switch state change for a switch by name.

        Args:
            obj: The switch object.
            state: Boolean or int of state of the switch you're processing,
                True/1 is active, False/0 is inactive.
            logical: Boolean which specifies whether the 'state' argument
                represents the "physical" or "logical" state of the switch. If
                True, a 1 means this switch is active and a 0 means it's
                inactive, regardless of the NC/NO configuration of the switch.
                If False, then the state parameter passed will be inverted if
                the switch is configured to be an 'NC' type. Typically the
                hardware will send switch states in their raw (logical=False)
                states, but other interfaces like the keyboard and OSC will use
                logical=True.

        This is the method that is called by the platform driver whenever a
        switch changes state. It's also used by the "other" modules that
        activate switches, including the keyboard and OSC interfaces.

        State 0 means the switch changed from active to inactive, and 1 means
        it changed from inactive to active. (The hardware & platform code
        handles NC versus NO switches and translates them to 'active' versus
        'inactive'.)
        """
        # We need int, but this lets it come in as boolean also
        if state:
            state = 1
        else:
            state = 0

        # flip the logical & physical states for NC switches
        hw_state = state

        if obj.invert:
            if logical:  # NC + logical means hw_state is opposite of state
                hw_state ^= 1
            else:
                # NC w/o logical (i.e. hardware state was sent) means logical
                # state is the opposite
                state ^= 1

        # Update the hardware state since we always want this to match real hw
        obj.hw_state = hw_state

        # if the switch is active, check to see if it's recycle_time has passed
        if state and not self._check_recycle_time(obj, state):
            self.machine.clock.schedule_once(partial(self._recycle_passed, obj, state, logical, obj.hw_state),
                                             timeout=obj.recycle_clear_time - self.machine.clock.get_time())
            return

        obj.state = state  # update the switch device

        if state:
            # update the switch's next recycle clear time
            obj.recycle_clear_time = (self.machine.clock.get_time() +
                                      obj.recycle_secs)

        # if the switch is already in this state, then abort
        if self.switches[obj.name].state == state:

            if not obj.recycle_secs:
                self.warning_log(
                    "Received duplicate switch state, which means this switch "
                    "had some non-debounced state changes. This could be "
                    "nothing, but if it happens a lot it could indicate noise "
                    "or interference on the line. Switch: %s", obj.name)
            return

        if state:
            self.info_log("<<<<<<< '{}' active >>>>>>>".format(obj.name))
        else:
            self.info_log("<<<<<<< '{}' inactive >>>>>>>".format(obj.name))

        # Update the switch controller's logical state for this switch
        self.set_state(obj.name, state)

        self._call_handlers(obj.name, state)

        self._cancel_timed_handlers(obj.name, state)

        for monitor in self.monitors:
            monitor(MonitoredSwitchChange(name=obj.name, label=obj.label, platform=obj.platform,
                                          num=obj.hw_switch.number, state=state))

    def _recycle_passed(self, obj, state, logical, hw_state):
        if obj.hw_state == hw_state:
            self.process_switch(obj.name, state, logical)

    def wait_for_switch(self, switch_name: str, state: int = 1, only_on_change=True, ms=0):
        """Wait for a switch to change into a state.

        Args:
            switch_name: String name of the switch to wait for.
            state: The state to wait for. 0 = inactive, 1 = active.
            only_on_change: Bool which controls whether this wait will be
                triggered now if the switch is already in the state, or
                whether it will wait until the switch changes into that state.
            ms: How long the switch needs to be in the new state to trigger
                the wait.

        """
        return self.wait_for_any_switch([switch_name], state, only_on_change, ms)

    def wait_for_any_switch(self, switch_names: List[str], state: int = 1, only_on_change=True, ms=0):
        """Wait for the first switch in the list to change into state.

        Args:
            switch_names: Iterable of strings of switch names. Whichever switch changes first will trigger this wait.
            state: The state to wait for. 0 = inactive, 1 = active.
            only_on_change: Bool which controls whether this wait will be
                triggered now if the switch is already in the state, or
                whether it will wait until the switch changes into that state.
            ms: How long the switch needs to be in the new state to trigger
                the wait.

        """
        future = asyncio.Future(loop=self.machine.clock.loop)   # type: asyncio.Future

        if not only_on_change:
            for switch_name in switch_names:
                if self.is_state(switch_name, state, ms):
                    future.set_result({"switch_name": switch_name, "state": state, "ms": ms})
                    return future

        handlers = []   # type: List[SwitchHandler]
        future.add_done_callback(partial(self._future_done, handlers))      # type: ignore
        for switch_name in switch_names:
            handlers.append(self.add_switch_handler(switch_name, state=state, ms=ms,
                                                    callback=partial(self._wait_handler,
                                                                     ms=ms,
                                                                     _future=future,
                                                                     switch_name=switch_name)))
        return future

    def _future_done(self, handlers: List[SwitchHandler], future: asyncio.Future):
        del future
        for handler in handlers:
            self.remove_switch_handler_by_key(handler)

    @staticmethod
    def _wait_handler(_future: asyncio.Future, **kwargs):
        if not _future.done():
            _future.set_result(kwargs)

    def _cancel_timed_handlers(self, name, state):
        # now check if the opposite state is in the active timed switches list
        # if so, remove it
        for k, v, in list(self.active_timed_switches.items()):
            # using items() instead of iteritems() since we might want to
            # delete while iterating
            for k2, item in enumerate(v):
                if item.switch_name == str(name) and item.state == state ^ 1:
                    # ^1 in above line invertes the state
                    if self.active_timed_switches[k] and self.active_timed_switches[k][k2]:
                        del self.active_timed_switches[k][k2]

    def _add_timed_switch_handler(self, time: float, timed_switch_handler: TimedSwitchHandler):
        self.active_timed_switches[time].append(timed_switch_handler)

        if self._timed_switch_handler_delay:
            self.machine.clock.unschedule(self._timed_switch_handler_delay)
        self._timed_switch_handler_delay = self.machine.clock.schedule_once(
            self._process_active_timed_switches,
            self.get_next_timed_switch_event() - self.machine.clock.get_time())

    def _call_handlers(self, name, state):
        # Combine name & state so we can look it up
        switch_key = str(name) + '-' + str(state)

        # Do we have any registered handlers for this switch/state combo?
        if switch_key in self.registered_switches:
            for entry in self.registered_switches[switch_key][:]:  # generator?
                # Found an entry.

                # skip if the handler has been removed in the meantime
                if entry not in self.registered_switches[switch_key]:
                    continue

                if entry.ms:
                    # This entry is for a timed switch, so add it to our
                    # active timed switch list
                    key = self.machine.clock.get_time() + (entry.ms / 1000.0)
                    value = TimedSwitchHandler(callback=entry.callback,
                                               switch_name=name,
                                               state=state,
                                               ms=entry.ms)
                    self._add_timed_switch_handler(key, value)
                    self.debug_log(
                        "Found timed switch handler for k/v %s / %s",
                        key, value)
                else:
                    # This entry doesn't have a timed delay, so do the action
                    # now
                    entry.callback()

    def add_monitor(self, monitor: Callable[[MonitoredSwitchChange], None]):
        """Add a monitor callback which is called on switch changes."""
        if monitor not in self.monitors:
            self.monitors.append(monitor)

    def remove_monitor(self, monitor: Callable[[MonitoredSwitchChange], None]):
        """Remove a monitor callback."""
        if monitor in self.monitors:
            self.monitors.remove(monitor)

    # pylint: disable-msg=too-many-arguments
    def add_switch_handler(self, switch_name, callback, state=1, ms=0,
                           return_info=False, callback_kwargs=None) -> SwitchHandler:
        """Register a handler to take action on a switch event.

        Args:
            switch_name: String name of the switch you're adding this handler
                for.
            callback: The method you want called when this switch handler fires.
            state: Integer of the state transition you want to callback to be
                triggered on. Default is 1 which means it's called when the
                switch goes from inactive to active, but you can also use 0
                which means your callback will be called when the switch becomes
                inactive
            ms: Integer. If you specify a 'ms' parameter, the handler won't be
                called until the witch is in that state for that many
                milliseconds.
            return_info: If True, the switch controller will pass the
                parameters of the switch handler as arguments to the callback,
                including switch_name, state, and ms. If False (default), it
                just calls the callback with no parameters.
            callback_kwargs: Additional kwargs that will be passed with the
                callback.

        You can mix & match entries for the same switch here.
        """
        if callback_kwargs and return_info:
            callback = partial(callback, switch_name=switch_name, state=state, ms=ms, **callback_kwargs)
        elif return_info:
            callback = partial(callback, switch_name=switch_name, state=state, ms=ms)
        elif callback_kwargs:
            callback = partial(callback, **callback_kwargs)

        self.debug_log("Registering switch handler: %s, %s, state: %s, ms: %s"
                       ", info: %s", switch_name, callback,
                       state, ms, return_info)

        entry_val = RegisteredSwitch(ms=ms, callback=callback)
        entry_key = str(switch_name) + '-' + str(state)

        self.registered_switches[entry_key].append(entry_val)

        # If the switch handler that was just registered has a delay (i.e. ms>0,
        # then let's see if the switch is currently in the state that the
        # handler was registered for. If so, and if the switch has been in this
        # state for less time than the ms registered, then we need to add this
        # switch to our active_timed_switches list so this handler is called
        # when this switch's active time expires. (in other words, we're
        # catching delayed switches that were in progress when this handler was
        # registered.

        if ms and self.ms_since_change(switch_name) < ms:  # only do this for handlers that have delays
            if (state == 1 and self.is_active(switch_name, 0)) or (state == 0 and self.is_inactive(switch_name, 0)):
                # figure out when this handler should fire based on the
                # switch's original activation time.
                key = self.machine.clock.get_time() + ((ms - self.ms_since_change(switch_name)) / 1000.0)
                value = TimedSwitchHandler(callback=callback,
                                           switch_name=switch_name,
                                           state=state,
                                           ms=ms)
                self._add_timed_switch_handler(key, value)

        # Return the args we used to setup this handler for easy removal later
        return SwitchHandler(switch_name, callback, state, ms)

    def remove_switch_handler_by_key(self, switch_handler: SwitchHandler):
        """Remove switch handler by key returned from add_switch_handler."""
        self.remove_switch_handler(switch_handler.switch_name, switch_handler.callback, switch_handler.state,
                                   switch_handler.ms)

    def remove_switch_handler(self, switch_name, callback, state=1, ms=0):
        """Remove a registered switch handler.

        Currently this only works if you specify everything exactly as you set
        it up. (Except for return_info, which doesn't matter if true or false,
        it will remove either / both.
        """
        self.debug_log(
            "Removing switch handler. Switch: %s, State: %s, ms: %s",
            switch_name, state, ms)

        entry_key = str(switch_name) + '-' + str(state)

        if entry_key in self.registered_switches.keys():
            for _, settings in enumerate(list(self.registered_switches[entry_key])):
                if settings.ms == ms and settings.callback == callback:
                    self.registered_switches[entry_key].remove(settings)

        for k in list(self.active_timed_switches.keys()):
            timed_entry = self.active_timed_switches[k]
            for dummy_key, entry in enumerate(timed_entry):
                if (entry.switch_name == switch_name and entry.state == state and entry.ms == ms and
                        entry.callback == callback):
                    del self.active_timed_switches[k][dummy_key]

    def log_active_switches(self, **kwargs):
        """Write out entries to the INFO log file of all switches that are currently active."""
        del kwargs
        for k, v in self.switches.items():
            if v.state:
                self.info_log("Found active switch: %s", k)

    def _check_recycle_time(self, switch, state):
        # checks to see when a switch is ok to be activated again after it's
        # been last activated

        if self.machine.clock.get_time() >= switch.recycle_clear_time:
            return True

        else:
            if state:
                switch.recycle_jitter_count += 1
            return False

    @staticmethod
    def get_active_event_for_switch(switch_name):
        """Return the event name which is posted when switch_name becomes active."""
        return "{}_active".format(switch_name)

    def get_next_timed_switch_event(self):
        """Return time of the next timed switch event."""
        if not self.active_timed_switches:
            raise AssertionError("No active timed switches")
        return min(self.active_timed_switches.keys())

    def _process_active_timed_switches(self):
        """Process active times switches.

        Checks the current list of active timed switches to see if it's
        time to take action on any of them. If so, does the callback and then
        removes that entry from the list.
        """
        next_event_time = False
        for k in list(self.active_timed_switches.keys()):
            if k <= self.machine.clock.get_time():  # change to generator?
                for entry in list(self.active_timed_switches[k]):
                    # check if removed by previous entry
                    if entry not in self.active_timed_switches[k]:
                        continue
                    self.debug_log(
                        "Processing timed switch handler. Switch: %s "
                        " State: %s, ms: %s", entry.switch_name,
                        entry.state, entry.ms)
                    entry.callback()
                del self.active_timed_switches[k]
            else:
                if not next_event_time or next_event_time > k:
                    next_event_time = k

        self.machine.events.process_event_queue()
        if next_event_time:
            if self._timed_switch_handler_delay:
                self.machine.clock.unschedule(self._timed_switch_handler_delay)
            self._timed_switch_handler_delay = self.machine.clock.schedule_once(
                self._process_active_timed_switches,
                next_event_time - self.machine.clock.get_time())
예제 #24
0
named_rgb_colors = CaseInsensitiveDict(
    off=(0, 0, 0),
    aliceblue=(240, 248, 255),
    antiquewhite=(250, 235, 215),
    aquamarine=(127, 255, 212),
    azure=(240, 255, 255),
    beige=(245, 245, 220),
    bisque=(255, 228, 196),
    black=(0, 0, 0),
    blanchedalmond=(255, 235, 205),
    blue=(0, 0, 255),
    blueviolet=(138, 43, 226),
    brown=(165, 42, 42),
    burlywood=(222, 184, 135),
    cadetblue=(95, 158, 160),
    chartreuse=(127, 255, 0),
    chocolate=(210, 105, 30),
    coral=(255, 127, 80),
    cornflowerblue=(100, 149, 237),
    cornsilk=(255, 248, 220),
    crimson=(220, 20, 60),
    cyan=(0, 255, 255),
    darkblue=(0, 0, 139),
    darkcyan=(0, 139, 139),
    darkgoldenrod=(184, 134, 11),
    darkgray=(169, 169, 169),
    darkgreen=(0, 100, 0),
    darkkhaki=(189, 183, 107),
    darkmagenta=(139, 0, 139),
    darkolivegreen=(85, 107, 47),
    darkorange=(255, 140, 0),
    darkorchid=(153, 50, 204),
    darkred=(139, 0, 0),
    darksalmon=(233, 150, 122),
    darkseagreen=(143, 188, 143),
    darkslateblue=(72, 61, 139),
    darkslategray=(47, 79, 79),
    darkturquoise=(0, 206, 209),
    darkviolet=(148, 0, 211),
    deeppink=(255, 20, 147),
    deepskyblue=(0, 191, 255),
    dimgray=(105, 105, 105),
    dodgerblue=(30, 144, 255),
    firebrick=(178, 34, 34),
    floralwhite=(255, 250, 240),
    forestgreen=(34, 139, 34),
    gainsboro=(220, 220, 220),
    ghostwhite=(248, 248, 255),
    gold=(255, 215, 0),
    goldenrod=(218, 165, 32),
    gray=(128, 128, 128),
    green=(0, 128, 0),
    greenyellow=(173, 255, 47),
    honeydew=(240, 255, 240),
    hotpink=(255, 105, 180),
    indianred=(205, 92, 92),
    indigo=(75, 0, 130),
    ivory=(255, 255, 240),
    khaki=(240, 230, 140),
    lavender=(230, 230, 250),
    lavenderblush=(255, 240, 245),
    lawngreen=(124, 252, 0),
    lemonchiffon=(255, 250, 205),
    lightblue=(173, 216, 230),
    lightcoral=(240, 128, 128),
    lightcyan=(224, 255, 255),
    lightgoldenrodyellow=(250, 250, 210),
    lightgreen=(144, 238, 144),
    lightgrey=(211, 211, 211),
    lightpink=(255, 182, 193),
    lightsalmon=(255, 160, 122),
    lightseagreen=(32, 178, 170),
    lightskyblue=(135, 206, 250),
    lightslategray=(119, 136, 153),
    lightsteelblue=(176, 196, 222),
    lightyellow=(255, 255, 224),
    lime=(0, 255, 0),
    limegreen=(50, 205, 50),
    linen=(250, 240, 230),
    magenta=(255, 0, 255),
    maroon=(128, 0, 0),
    mediumaquamarine=(102, 205, 170),
    mediumblue=(0, 0, 205),
    mediumorchid=(186, 85, 211),
    mediumpurple=(147, 112, 219),
    mediumseagreen=(60, 179, 113),
    mediumslateblue=(123, 104, 238),
    mediumspringgreen=(0, 250, 154),
    mediumturquoise=(72, 209, 204),
    mediumvioletred=(199, 21, 133),
    midnightblue=(25, 25, 112),
    mintcream=(245, 255, 250),
    mistyrose=(255, 228, 225),
    moccasin=(255, 228, 181),
    navajowhite=(255, 222, 173),
    navy=(0, 0, 128),
    oldlace=(253, 245, 230),
    olive=(128, 128, 0),
    olivedrab=(107, 142, 35),
    orange=(255, 165, 0),
    orangered=(255, 69, 0),
    orchid=(218, 112, 214),
    palegoldenrod=(238, 232, 170),
    palegreen=(152, 251, 152),
    paleturquoise=(175, 238, 238),
    palevioletred=(219, 112, 147),
    papayawhip=(255, 239, 213),
    peachpuff=(255, 218, 185),
    peru=(205, 133, 63),
    pink=(255, 192, 203),
    plum=(221, 160, 221),
    powderblue=(176, 224, 230),
    purple=(128, 0, 128),
    rebeccapurple=(102, 51, 153),
    red=(255, 0, 0),
    rosybrown=(188, 143, 143),
    royalblue=(65, 105, 225),
    saddlebrown=(139, 69, 19),
    salmon=(250, 128, 114),
    sandybrown=(244, 164, 96),
    seagreen=(46, 139, 87),
    seashell=(255, 245, 238),
    sienna=(160, 82, 45),
    silver=(192, 192, 192),
    skyblue=(135, 206, 235),
    slateblue=(106, 90, 205),
    slategray=(112, 128, 144),
    snow=(255, 250, 250),
    springgreen=(0, 255, 127),
    steelblue=(70, 130, 180),
    tan=(210, 180, 140),
    teal=(0, 128, 128),
    thistle=(216, 191, 216),
    tomato=(255, 99, 71),
    turquoise=(64, 224, 208),
    violet=(238, 130, 238),
    wheat=(245, 222, 179),
    white=(255, 255, 255),
    whitesmoke=(245, 245, 245),
    yellow=(255, 255, 0),
    yellowgreen=(154, 205, 50),
)
예제 #25
0
    def __init__(self, mc):
        """Initialise sound system."""
        self.mc = mc
        self.log = logging.getLogger('SoundSystem')
        self._initialized = False
        self.audio_interface = None
        self.config = dict()
        self.sound_events = dict()
        self.tracks = CaseInsensitiveDict()
        self.clock_event = None

        self.log.debug("Loading the Sound System")

        # Load configuration for sound system
        if 'sound_system' not in self.mc.machine_config:
            self.log.info("SoundSystem: Using default 'sound_system' settings")
            self.config = dict()
        else:
            self.config = self.mc.machine_config['sound_system']

        # TODO: Use config spec validator

        # Validate configuration and provide default values where needed
        if 'enabled' not in self.config:
            self.config['enabled'] = DEFAULT_AUDIO_ENABLED

        # If the sound system has been disabled, abort initialization
        if not self.config['enabled']:
            self.log.debug(
                "SoundSystem: The sound system has been disabled in "
                "the configuration file (enabled: False). No audio "
                "features will be available.")
            return

        if 'buffer' not in self.config or self.config['buffer'] == 'auto':
            self.config['buffer'] = DEFAULT_AUDIO_BUFFER_SAMPLE_SIZE
        elif not AudioInterface.power_of_two(self.config['buffer']):
            self.log.warning(
                "SoundSystem: The buffer setting is not a power of "
                "two. Default buffer size will be used.")
            self.config['buffer'] = DEFAULT_AUDIO_BUFFER_SAMPLE_SIZE

        if 'frequency' not in self.config or self.config['frequency'] == 'auto':
            self.config['frequency'] = DEFAULT_SAMPLE_RATE

        if 'channels' not in self.config:
            self.config['channels'] = DEFAULT_AUDIO_CHANNELS

        if 'master_volume' not in self.config:
            self.config['master_volume'] = DEFAULT_MASTER_VOLUME

        # Initialize audio interface library (get audio output)
        try:
            self.audio_interface = AudioInterface(
                rate=self.config['frequency'],
                channels=self.config['channels'],
                buffer_samples=self.config['buffer'])
        except AudioException:
            self.log.error("Could not initialize the audio interface. "
                           "Audio features will not be available.")
            self.audio_interface = None
            return

        # Setup tracks in audio system (including initial volume levels)
        if 'tracks' in self.config:
            for track_name, track_config in self.config['tracks'].items():
                self._create_track(track_name, track_config)
        else:
            self._create_track('default')
            self.log.info(
                "No audio tracks are specified in your machine config file. "
                "a track named 'default' has been created.")

        # Set initial master volume level
        self.master_volume = self.config['master_volume']

        # Establish machine tick function callback (will process internal audio events)
        self.clock_event = Clock.schedule_interval(self.tick, 0)

        # Start audio engine processing
        self.audio_interface.enable()
        self._initialized = True

        self.mc.events.add_handler("master_volume_increase",
                                   self.master_volume_increase)
        self.mc.events.add_handler("master_volume_decrease",
                                   self.master_volume_decrease)
        self.mc.events.add_handler("shutdown", self.shutdown)
        self.mc.events.add_handler("client_connected", self._send_volume, -1)