Exemplo n.º 1
0
class MediaController(object):

    def __init__(self, options):
        self.options = options

        self.log = logging.getLogger("MediaController")
        self.log.info("Media Controller Version %s", version.__version__)
        self.log.info("Backbox Control Protocol Version %s",
                      version.__bcp_version__)
        self.log.info("Config File Version %s",
                      version.__config_version__)

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

        self.config = dict()
        self.done = False  # todo
        self.machine_path = None
        self.asset_managers = dict()
        self.num_assets_to_load = 0
        self.window = None
        self.window_manager = None
        self.pygame = False
        self.pygame_requested = False
        self.registered_pygame_handlers = dict()
        self.pygame_allowed_events = list()
        self.socket_thread = None
        self.receive_queue = Queue.Queue()
        self.sending_queue = Queue.Queue()
        self.crash_queue = Queue.Queue()
        self.game_modes = CaseInsensitiveDict()
        self.player_list = list()
        self.player = None
        self.HZ = 0
        self.next_tick_time = 0
        self.secs_per_tick = 0

        Task.Create(self._check_crash_queue)

        self.bcp_commands = {'hello': self.bcp_hello,
                             'goodbye': self.bcp_goodbye,
                             'reset': self.reset,
                             'mode_start': self.bcp_mode_start,
                             'mode_stop': self.bcp_mode_stop,
                             'error': self.bcp_error,
                             'ball_start': self.bcp_ball_start,
                             'ball_end': self.bcp_ball_end,
                             'game_start': self.bcp_game_start,
                             'game_end': self.bcp_game_end,
                             'player_added': self.bcp_player_add,
                             'player_variable': self.bcp_player_variable,
                             'player_score': self.bcp_player_score,
                             'player_turn_start': self.bcp_player_turn_start,
                             'attract_start': self.bcp_attract_start,
                             'attract_stop': self.bcp_attract_stop,
                             'trigger': self.bcp_trigger,
                             'switch': self.bcp_switch,
                             'get': self.bcp_get,
                             'set': self.bcp_set,
                             'config': self.bcp_config,
                             'timer': self.bcp_timer
                            }

        # load the MPF config & machine defaults
        self.config = (
            Config.load_config_yaml(config=self.config,
                                    yaml_file=self.options['mcconfigfile']))

        # Find the machine_files location. If it starts with a forward or
        # backward slash, then we assume it's from the mpf root. Otherwise we
        # assume it's from the subfolder location specified in the
        # mpfconfigfile location

        if (options['machinepath'].startswith('/') or
                options['machinepath'].startswith('\\')):
            machine_path = options['machinepath']
        else:
            machine_path = os.path.join(self.config['mediacontroller']['paths']
                                        ['machine_files'],
                                        options['machinepath'])

        self.machine_path = os.path.abspath(machine_path)

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

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

        # Now find the config file location. Same as machine_file with the
        # slash uses to specify an absolute path

        if (options['configfile'].startswith('/') or
                options['configfile'].startswith('\\')):
            config_file = options['configfile']
        else:

            if not options['configfile'].endswith('.yaml'):
                options['configfile'] += '.yaml'

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

        self.log.info("Base machine config file: %s", config_file)

        # Load the machine-specific config
        self.config = Config.load_config_yaml(config=self.config,
                                              yaml_file=config_file)

        mediacontroller_config_spec = '''
                        exit_on_disconnect: boolean|True
                        port: int|5050
                        '''

        self.config['mediacontroller'] = (
            Config.process_config(mediacontroller_config_spec,
                                  self.config['mediacontroller']))

        self.events = EventManager(self)
        self.timing = Timing(self)

        # Load the media controller modules
        self.config['mediacontroller']['modules'] = (
            self.config['mediacontroller']['modules'].split(' '))
        for module in self.config['mediacontroller']['modules']:
            self.log.info("Loading module: %s", module)
            module_parts = module.split('.')
            exec('self.' + module_parts[0] + '=' + module + '(self)')

            # todo there's probably a more pythonic way to do this, and I know
            # exec() is supposedly unsafe, but meh, if you have access to put
            # malicious files in the system folder then you have access to this
            # code too.

        self.start_socket_thread()

        self.events.post("init_phase_1")
        self.events.post("init_phase_2")
        self.events.post("init_phase_3")
        self.events.post("init_phase_4")
        self.events.post("init_phase_5")

        self.reset()

    def _check_crash_queue(self):
        try:
            crash = self.crash_queue.get(block=False)
        except Queue.Empty:
            yield 1000
        else:
            self.log.critical("MPF Shutting down due to child thread crash")
            self.log.critical("Crash details: %s", crash)
            self.done = True

    def reset(self, **kwargs):
        """Processes an incoming BCP 'reset' command."""
        self.player = None
        self.player_list = list()

        self.events.post('mc_reset_phase_1')
        self.events.post('mc_reset_phase_2')
        self.events.post('mc_reset_phase_3')

    def get_window(self):
        """ Returns a reference to the onscreen display window.

        This method will set up a window if one doesn't exist yet. This method
        exists because there are several different modules and plugins which
        may want to use a window, but we don't know which combinations might
        be used, so we centralize the creation and management of an onscreen
        window here.
        """

        if not self.window:
            self.window_manager = window.WindowManager(self)
            self.window = self.window_manager.window

        return self.window

    def request_pygame(self):
        """Called by a module to let the system know it would like to use
        Pygame. We centralize the requests instead of letting each module do
        their own pygame.init() so we get it in one place and can get everthing
        initialized in the right order.

        Returns: True or False, depending on whether pygame is available or not.
        """

        if pygame and not self.pygame_requested:
            self.events.add_handler('init_phase_3', self._pygame_init)
            self.pygame_requested = True
            return True

        else:
            return False

    def _pygame_init(self):
        # performs the actual pygame initialization

        if not pygame:
            self.log.critical("Pygame is needed but not available. Please "
                              "install Pygame and try again.")
            raise Exception("Pygame is needed but not available. Please install"
                            " Pygame and try again.")

        if not self.pygame:
            self.log.debug("Initializing Pygame, version %s",
                           pygame.version.ver)

            pygame.init()
            self.pygame = True

            self.events.add_handler('timer_tick', self.get_pygame_events,
                                    priority=1000)

            self.events.post('pygame_initialized')

    def register_pygame_handler(self, event, handler):
        """Registers a method to be a handler for a certain type of Pygame
        event.

        Args:
            event: A string of the Pygame event name you're registering this
            handler for.
            handler: A method that will be called when this Pygame event is
            posted.
        """
        if event not in self.registered_pygame_handlers:
            self.registered_pygame_handlers[event] = set()

        self.registered_pygame_handlers[event].add(handler)
        self.pygame_allowed_events.append(event)

        self.log.debug("Adding Window event handler. Event:%s, Handler:%s",
                       event, handler)

        pygame.event.set_allowed(self.pygame_allowed_events)

    def get_pygame_events(self):
        """Gets (and dispatches) Pygame events. Automatically called every
        machine loop via the timer_tick event.
        """
        for event in pygame.event.get():
            if event.type in self.registered_pygame_handlers:
                for handler in self.registered_pygame_handlers[event.type]:

                    if (event.type == pygame.KEYDOWN or
                            event.type == pygame.KEYUP):
                        handler(event.key, event.mod)
                    else:
                        handler()

    def _process_command(self, bcp_command, **kwargs):
        self.log.debug("Processing command: %s %s", bcp_command, kwargs)

        try:
            self.bcp_commands[bcp_command](**kwargs)
        except KeyError:
            self.log.warning("Received invalid BCP command: %s", bcp_command)
            self.send('error', message='invalid command', command=bcp_command)

    def send(self, bcp_command, callback=None, **kwargs):
        """Sends a BCP command to the connected pinball controller.

        Args:
            bcp_command: String of the BCP command name.
            callback: Optional callback method that will be called when the
                command is sent.
            **kwargs: Optional additional kwargs will be added to the BCP
                command string.

        """
        self.sending_queue.put(bcp.encode_command_string(bcp_command,
                                                          **kwargs))
        if callback:
            callback()

    def send_dmd_frame(self, data):
        """Sends a DMD frame to the BCP client.

        Args:
            data: A 4096-length raw byte string.
        """

        dmd_string = 'dmd_frame?' + data
        self.sending_queue.put(dmd_string)

    def _timer_init(self):
        self.HZ = 30
        self.next_tick_time = time.time()
        self.secs_per_tick = 1.0 / self.HZ

    def timer_tick(self):
        """Called by the platform each machine tick based on self.HZ"""
        self.timing.timer_tick()  # notifies the timing module
        self.events.post('timer_tick')  # sends the timer_tick system event
        Task.timer_tick()  # notifies tasks
        DelayManager.timer_tick()

    def run(self):
        """Main run loop."""
        self._timer_init()

        self.log.info("Starting the run loop at %sHz", self.HZ)

        start_time = time.time()
        loops = 0

        secs_per_tick = self.secs_per_tick

        self.next_tick_time = time.time()

        try:
            while self.done is False:
                time.sleep(0.001)

                self.get_from_queue()

                if self.next_tick_time <= time.time():  # todo change this
                    self.timer_tick()
                    self.next_tick_time += secs_per_tick
                    loops += 1

            self._do_shutdown()
            self.log.info("Target loop rate: %s Hz", self.HZ)
            self.log.info("Actual loop rate: %s Hz",
                          loops / (time.time() - start_time))

        except KeyboardInterrupt:
            self.shutdown()

    def shutdown(self):
        """Shuts down and exits the media controller.

        This method will also send the BCP 'goodbye' command to any connected
        clients.
        """
        self.socket_thread.stop()

    def _do_shutdown(self):
        if self.pygame:
            pygame.quit()

    def socket_thread_stopped(self):
        """Notifies the media controller that the socket thread has stopped."""
        self.done = True

    def start_socket_thread(self):
        """Starts the BCPServer socket thread."""
        self.socket_thread = BCPServer(self, self.receive_queue,
                                       self.sending_queue)
        self.socket_thread.daemon = True
        self.socket_thread.start()

    def get_from_queue(self):
        """Gets and processes all queued up incoming BCP commands."""
        while not self.receive_queue.empty():
            cmd, kwargs = bcp.decode_command_string(
                self.receive_queue.get(False))
            self._process_command(cmd, **kwargs)

    def bcp_hello(self, **kwargs):
        """Processes an incoming BCP 'hello' command."""
        try:
            if LooseVersion(kwargs['version']) == (
                    LooseVersion(version.__bcp_version__)):
                self.send('hello', version=version.__bcp_version__)
            else:
                self.send('hello', version='unknown protocol version')
        except:
            self.log.warning("Received invalid 'version' parameter with "
                             "'hello'")

    def bcp_goodbye(self, **kwargs):
        """Processes an incoming BCP 'goodbye' command."""
        if self.config['mediacontroller']['exit_on_disconnect']:
            self.socket_thread.sending_thread.stop()
            sys.exit()

    def bcp_mode_start(self, name=None, priority=0, **kwargs):
        """Processes an incoming BCP 'mode_start' command."""
        if not name:
            return
            #todo raise error

        if name in self.game_modes:
            self.game_modes[name].start(priority=priority)

    def bcp_mode_stop(self, name, **kwargs):
        """Processes an incoming BCP 'mode_stop' command."""
        if not name:
            return
            #todo raise error

        if name in self.game_modes:
            self.game_modes[name].stop()

    def bcp_error(self, **kwargs):
        """Processes an incoming BCP 'error' command."""
        self.log.warning('Received error command from client')

    def bcp_ball_start(self, **kwargs):
        """Processes an incoming BCP 'ball_start' command."""
        self.events.post('ball_started', **kwargs)

    def bcp_ball_end(self, **kwargs):
        """Processes an incoming BCP 'ball_end' command."""
        self.events.post('ball_ended', **kwargs)

    def bcp_game_start(self, **kargs):
        """Processes an incoming BCP 'game_start' command."""
        self.bcp_player_add(number=1)
        self.bcp_player_turn_start(player=1)
        self.events.post('game_started', **kargs)

    def bcp_game_end(self, **kwargs):
        """Processes an incoming BCP 'game_end' command."""
        self.player = None
        self.events.post('game_ended', **kwargs)

    def bcp_player_add(self, number, **kwargs):
        """Processes an incoming BCP 'player_add' command."""

        if number > len(self.player_list):
            new_player = Player(self)
            self.player_list.append(new_player)
            new_player.score = 0

            self.events.post('player_add_success', num=number)

    def bcp_player_variable(self, name, value, prev_value, change, **kwargs):
        """Processes an incoming BCP 'player_variable' command."""

        if self.player:
            self.player[name] = value

    def bcp_player_score(self, value, prev_value, change, **kwargs):
        """Processes an incoming BCP 'player_score' command."""

        if self.player:
            self.player['score'] = int(value)

    def bcp_attract_start(self, **kwargs):
        """Processes an incoming BCP 'attract_start' command."""
        self.events.post('machineflow_Attract_start')

    def bcp_attract_stop(self, **kwargs):
        self.events.post('machineflow_Attract_stop')
        """Processes an incoming BCP 'attract_stop' command."""

    def bcp_player_turn_start(self, player, **kwargs):
        """Processes an incoming BCP 'player_turn_start' command."""

        if ((self.player and self.player.number != player) or
                not self.player):

            self.player = self.player_list[int(player)-1]

    def bcp_trigger(self, name, **kwargs):
        """Processes an incoming BCP 'trigger' command."""

        blocked_event_prefixes = ('player_',
                                  'machinemode_',
                                 )

        blocked_events = ('ball_started',
                          'ball_ended',
                          'game_started',
                          'game_ended',
                         )

        if not (name.startswith(blocked_event_prefixes) and
                name in blocked_events):

            self.events.post(name, **kwargs)

    def bcp_switch(self, name, state, **kwargs):
        """Processes an incoming BCP 'switch' command."""
        if int(state):
            self.events.post('switch_' + name + '_active')
        else:
            self.events.post('switch_' + name + '_inactive')

    def bcp_get(self, **kwargs):
        """Processes an incoming BCP 'get' command.

        Note that this media controller doesn't implement the 'get' command at
        this time, but it's included here for completeness since the 'get'
        command is part of the BCP 1.0 specification so we don't want to return
        an error if we receive an incoming 'get' command.

        """
        pass

    def bcp_set(self, **kwargs):
        """Processes an incoming BCP 'set' command.

        Note that this media controller doesn't implement the 'set' command at
        this time, but it's included here for completeness since the 'set'
        command is part of the BCP 1.0 specification so we don't want to return
        an error if we receive an incoming 'set' command.

        """
        pass

    def bcp_config(self, **kwargs):
        """Processes an incoming BCP 'config' command."""
        for k, v in kwargs.iteritems():
            if k.startswith('volume_'):
                self.bcp_set_volume(track=k.split('volume_')[1], value=v)

    def bcp_timer(self, name, action, **kwargs):
        """Processes an incoming BCP 'config' command."""

        self.events.post('timer_' + name + '_' + action, **kwargs)

    def bcp_set_volume(self, track, value):
        """Sets the volume based on an incoming BCP 'config' command.

        Args:
            track: String name of the track the volume will set.
            value: Float between 0 and 1 which represents the volume level to
                set.

        Note: At this time only the master volume can be set with this method.

        """

        if track == 'master':
            self.sound.set_volume(value)
Exemplo n.º 2
0
class MediaController(object):

    def __init__(self, options):
        self.options = options

        self.log = logging.getLogger("MediaController")
        self.log.debug("Command line arguments: {}".format(self.options))
        self.log.info("Media Controller Version %s", version.__version__)
        self.log.debug("Backbox Control Protocol Version %s",
                      version.__bcp_version__)
        self.log.debug("Config File Version %s",
                      version.__config_version__)

        python_version = sys.version_info

        if python_version[0] != 2 or python_version[1] != 7:
            self.log.error("Incorrect Python version. MPF requires Python 2.7."
                           "x. You have Python %s.%s.%s.", python_version[0],
                           python_version[1], python_version[2])
            sys.exit()

        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)

        self.active_debugger = dict()

        self.config = dict()
        self.done = False  # todo
        self.machine_path = None
        self.asset_managers = dict()
        self.window = None
        self.window_manager = None
        self.pygame = False
        self.pygame_requested = False
        self.registered_pygame_handlers = dict()
        self.pygame_allowed_events = list()
        self.socket_thread = None
        self.receive_queue = Queue.Queue()
        self.sending_queue = Queue.Queue()
        self.crash_queue = Queue.Queue()
        self.modes = CaseInsensitiveDict()
        self.player_list = list()
        self.player = None
        self.HZ = 0
        self.next_tick_time = 0
        self.secs_per_tick = 0
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.tick_num = 0
        self.delay = DelayManager()

        self._pc_assets_to_load = 0
        self._pc_total_assets = 0
        self.pc_connected = False

        Task.create(self._check_crash_queue)

        self.bcp_commands = {'ball_start': self.bcp_ball_start,
                             'ball_end': self.bcp_ball_end,
                             'config': self.bcp_config,
                             'error': self.bcp_error,
                             'get': self.bcp_get,
                             'goodbye': self.bcp_goodbye,
                             'hello': self.bcp_hello,
                             'machine_variable': self.bcp_machine_variable,
                             'mode_start': self.bcp_mode_start,
                             'mode_stop': self.bcp_mode_stop,
                             'player_added': self.bcp_player_add,
                             'player_score': self.bcp_player_score,
                             'player_turn_start': self.bcp_player_turn_start,
                             'player_variable': self.bcp_player_variable,
                             'reset': self.reset,
                             'set': self.bcp_set,
                             'shot': self.bcp_shot,
                             'switch': self.bcp_switch,
                             'timer': self.bcp_timer,
                             'trigger': self.bcp_trigger,
                            }

        FileManager.init()
        self.config = dict()
        self._load_mc_config()
        self._set_machine_path()
        self._load_machine_config()

        # Find the machine_files location. If it starts with a forward or
        # backward slash, then we assume it's from the mpf root. Otherwise we
        # assume it's from the subfolder location specified in the
        # mpfconfig file location

        if (options['machine_path'].startswith('/') or
                options['machine_path'].startswith('\\')):
            machine_path = options['machine_path']
        else:
            machine_path = os.path.join(self.config['media_controller']['paths']
                                        ['machine_files'],
                                        options['machine_path'])

        self.machine_path = os.path.abspath(machine_path)

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

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

        mediacontroller_config_spec = '''
                        exit_on_disconnect: boolean|True
                        port: int|5050
                        '''

        self.config['media_controller'] = (
            Config.process_config(mediacontroller_config_spec,
                                  self.config['media_controller']))

        self.events = EventManager(self, setup_event_player=False)
        self.timing = Timing(self)

        # Load the media controller modules
        self.config['media_controller']['modules'] = (
            self.config['media_controller']['modules'].split(' '))
        self.log.info("Loading Modules...")
        for module in self.config['media_controller']['modules']:
            self.log.debug("Loading module: %s", module)
            module_parts = module.split('.')
            exec('self.' + module_parts[0] + '=' + module + '(self)')

            # todo there's probably a more pythonic way to do this, and I know
            # exec() is supposedly unsafe, but meh, if you have access to put
            # malicious files in the system folder then you have access to this
            # code too.

        self.start_socket_thread()

        self.events.post("init_phase_1")
        self.events._process_event_queue()
        self.events.post("init_phase_2")
        self.events._process_event_queue()
        self.events.post("init_phase_3")
        self.events._process_event_queue()
        self.events.post("init_phase_4")
        self.events._process_event_queue()
        self.events.post("init_phase_5")
        self.events._process_event_queue()

        self.reset()

    def _load_mc_config(self):
        self.config = Config.load_config_file(self.options['mcconfigfile'])

        # Find the machine_files location. If it starts with a forward or
        # backward slash, then we assume it's from the mpf root. Otherwise we
        # assume it's from the subfolder location specified in the
        # mpfconfigfile location

    def _set_machine_path(self):
        if (self.options['machine_path'].startswith('/') or
                self.options['machine_path'].startswith('\\')):
            machine_path = self.options['machine_path']
        else:
            machine_path = os.path.join(self.config['media_controller']['paths']
                                        ['machine_files'],
                                        self.options['machine_path'])

        self.machine_path = os.path.abspath(machine_path)
        self.log.debug("Machine path: {}".format(self.machine_path))

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

    def _load_machine_config(self):
        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['media_controller']['paths']['config'], config_file)

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

            self.config = Util.dict_merge(self.config,
                Config.load_config_file(config_file))

    def _check_crash_queue(self):
        try:
            crash = self.crash_queue.get(block=False)
        except Queue.Empty:
            yield 1000
        else:
            self.log.critical("MPF Shutting down due to child thread crash")
            self.log.critical("Crash details: %s", crash)
            self.done = True

    def reset(self, **kwargs):
        """Processes an incoming BCP 'reset' command."""
        self.player = None
        self.player_list = list()

        self.events.add_handler('assets_to_load',
                                self._bcp_client_asset_loader_tick)
        self.events.replace_handler('timer_tick', self.asset_loading_counter)

        self.events.post('mc_reset_phase_1')
        self.events._process_event_queue()
        self.events.post('mc_reset_phase_2')
        self.events._process_event_queue()
        self.events.post('mc_reset_phase_3')
        self.events._process_event_queue()

    def get_window(self):
        """ Returns a reference to the onscreen display window.

        This method will set up a window if one doesn't exist yet. This method
        exists because there are several different modules and plugins which
        may want to use a window, but we don't know which combinations might
        be used, so we centralize the creation and management of an onscreen
        window here.

        """
        if not self.window:
            self.window_manager = window.WindowManager(self)
            self.window = self.window_manager.window

        return self.window

    def request_pygame(self):
        """Called by a module to let the system know it would like to use
        Pygame. We centralize the requests instead of letting each module do
        their own pygame.init() so we get it in one place and can get everthing
        initialized in the right order.

        Returns: True or False, depending on whether pygame is available or not.
        """

        if pygame and not self.pygame_requested:
            self.events.add_handler('init_phase_3', self._pygame_init)
            self.pygame_requested = True
            return True

        else:
            return False

    def _pygame_init(self):
        # performs the actual pygame initialization

        if not pygame:
            self.log.critical("Pygame is needed but not available. Please "
                              "install Pygame and try again.")
            raise Exception("Pygame is needed but not available. Please install"
                            " Pygame and try again.")

        if not self.pygame:
            self.log.debug("Initializing Pygame, version %s",
                           pygame.version.ver)

            pygame.init()
            self.pygame = True

            self.events.add_handler('timer_tick', self.get_pygame_events,
                                    priority=1000)
            self.events.add_handler('timer_tick', self.asset_loading_counter)

            self.events.post('pygame_initialized')
            self.events._process_event_queue()

    def register_pygame_handler(self, event, handler):
        """Registers a method to be a handler for a certain type of Pygame
        event.

        Args:
            event: A string of the Pygame event name you're registering this
            handler for.
            handler: A method that will be called when this Pygame event is
            posted.
        """
        if event not in self.registered_pygame_handlers:
            self.registered_pygame_handlers[event] = set()

        self.registered_pygame_handlers[event].add(handler)
        self.pygame_allowed_events.append(event)

        self.log.debug("Adding Window event handler. Event:%s, Handler:%s",
                       event, handler)

        pygame.event.set_allowed(self.pygame_allowed_events)

    def get_pygame_events(self):
        """Gets (and dispatches) Pygame events. Automatically called every
        machine loop via the timer_tick event.
        """
        for event in pygame.event.get():
            if event.type in self.registered_pygame_handlers:
                for handler in self.registered_pygame_handlers[event.type]:

                    if (event.type == pygame.KEYDOWN or
                            event.type == pygame.KEYUP):
                        handler(event.key, event.mod)
                    else:
                        handler()

    def _process_command(self, bcp_command, **kwargs):
        self.log.debug("Processing command: %s %s", bcp_command, kwargs)


        # Can't use try/except KeyError here becasue there could be a KeyError
        # in the callback which we don't want it to swallow.
        if bcp_command in self.bcp_commands:
            self.bcp_commands[bcp_command](**kwargs)
        else:
            self.log.warning("Received invalid BCP command: %s", bcp_command)
            self.send('error', message='invalid command', command=bcp_command)


    def send(self, bcp_command, callback=None, **kwargs):
        """Sends a BCP command to the connected pinball controller.

        Args:
            bcp_command: String of the BCP command name.
            callback: Optional callback method that will be called when the
                command is sent.
            **kwargs: Optional additional kwargs will be added to the BCP
                command string.

        """
        self.sending_queue.put(bcp.encode_command_string(bcp_command,
                                                         **kwargs))
        if callback:
            callback()

    def send_dmd_frame(self, data):
        """Sends a DMD frame to the BCP client.

        Args:
            data: A 4096-length raw byte string.
        """

        dmd_string = 'dmd_frame?' + data
        self.sending_queue.put(dmd_string)

    def _timer_init(self):
        self.HZ = 30
        self.next_tick_time = time.time()
        self.secs_per_tick = 1.0 / self.HZ

    def timer_tick(self):
        """Called by the platform each machine tick based on self.HZ"""
        self.timing.timer_tick()  # notifies the timing module
        self.events.post('timer_tick')  # sends the timer_tick system event
        self.tick_num += 1
        Task.timer_tick()  # notifies tasks
        DelayManager.timer_tick(self)
        self.events._process_event_queue()

    def run(self):
        """Main run loop."""
        self._timer_init()

        self.log.info("Starting the run loop at %sHz", self.HZ)

        start_time = time.time()
        loops = 0

        secs_per_tick = self.secs_per_tick

        self.next_tick_time = time.time()

        try:
            while self.done is False:
                time.sleep(0.001)

                self.get_from_queue()

                if self.next_tick_time <= time.time():  # todo change this
                    self.timer_tick()
                    self.next_tick_time += secs_per_tick
                    loops += 1

            self._do_shutdown()
            self.log.info("Target loop rate: %s Hz", self.HZ)
            self.log.info("Actual loop rate: %s Hz",
                          loops / (time.time() - start_time))

        except KeyboardInterrupt:
            self.shutdown()

    def shutdown(self):
        """Shuts down and exits the media controller.

        This method will also send the BCP 'goodbye' command to any connected
        clients.
        """
        self.socket_thread.stop()

    def _do_shutdown(self):
        if self.pygame:
            pygame.quit()

    def socket_thread_stopped(self):
        """Notifies the media controller that the socket thread has stopped."""
        self.done = True

    def start_socket_thread(self):
        """Starts the BCPServer socket thread."""
        self.socket_thread = BCPServer(self, self.receive_queue,
                                       self.sending_queue)
        self.socket_thread.daemon = True
        self.socket_thread.start()

    def get_from_queue(self):
        """Gets and processes all queued up incoming BCP commands."""
        while not self.receive_queue.empty():
            cmd, kwargs = bcp.decode_command_string(
                self.receive_queue.get(False))
            self._process_command(cmd, **kwargs)

    def bcp_hello(self, **kwargs):
        """Processes an incoming BCP 'hello' command."""
        try:
            if LooseVersion(kwargs['version']) == (
                    LooseVersion(version.__bcp_version__)):
                self.send('hello', version=version.__bcp_version__)
            else:
                self.send('hello', version='unknown protocol version')
        except KeyError:
            self.log.warning("Received invalid 'version' parameter with "
                             "'hello'")

    def bcp_goodbye(self, **kwargs):
        """Processes an incoming BCP 'goodbye' command."""
        if self.config['media_controller']['exit_on_disconnect']:
            self.socket_thread.sending_thread.stop()
            sys.exit()

    def bcp_mode_start(self, name=None, priority=0, **kwargs):
        """Processes an incoming BCP 'mode_start' command."""
        if not name:
            return
            #todo raise error

        if name == 'game':
            self._game_start()

        if name in self.modes:
            self.modes[name].start(priority=priority)

    def bcp_mode_stop(self, name, **kwargs):
        """Processes an incoming BCP 'mode_stop' command."""
        if not name:
            return
            #todo raise error

        if name == 'game':
            self._game_end()

        if name in self.modes:
            self.modes[name].stop()

    def bcp_error(self, **kwargs):
        """Processes an incoming BCP 'error' command."""
        self.log.warning('Received error command from client')

    def bcp_ball_start(self, **kwargs):
        """Processes an incoming BCP 'ball_start' command."""
        kwargs['player'] = kwargs.pop('player_num')

        self.events.post('ball_started', **kwargs)

    def bcp_ball_end(self, **kwargs):
        """Processes an incoming BCP 'ball_end' command."""
        self.events.post('ball_ended', **kwargs)

    def _game_start(self, **kargs):
        """Processes an incoming BCP 'game_start' command."""
        self.player = None
        self.player_list = list()
        self.num_players = 0
        self.events.post('game_started', **kargs)

    def _game_end(self, **kwargs):
        """Processes an incoming BCP 'game_end' command."""
        self.player = None
        self.events.post('game_ended', **kwargs)

    def bcp_player_add(self, player_num, **kwargs):
        """Processes an incoming BCP 'player_add' command."""

        if player_num > len(self.player_list):
            new_player = Player(self, self.player_list)

            self.events.post('player_add_success', num=player_num)

    def bcp_player_variable(self, name, value, prev_value, change, player_num,
                            **kwargs):
        """Processes an incoming BCP 'player_variable' command."""

        try:
            self.player_list[int(player_num)-1][name] = value
        except (IndexError, KeyError):
            pass

    def bcp_machine_variable(self, name, value, **kwargs):
        """Processes an incoming BCP 'machine_variable' command."""

        self._set_machine_var(name, value)

    def bcp_player_score(self, value, prev_value, change, player_num,
                         **kwargs):
        """Processes an incoming BCP 'player_score' command."""

        try:
            self.player_list[int(player_num)-1]['score'] = int(value)
        except (IndexError, KeyError):
            pass

    def bcp_player_turn_start(self, player_num, **kwargs):
        """Processes an incoming BCP 'player_turn_start' command."""

        self.log.debug("bcp_player_turn_start")

        if ((self.player and self.player.number != player_num) or
                not self.player):

            try:
                self.player = self.player_list[int(player_num)-1]
            except IndexError:
                self.log.error('Received player turn start for player %s, but '
                               'only %s player(s) exist',
                               player_num, len(self.player_list))

    def bcp_trigger(self, name, **kwargs):
        """Processes an incoming BCP 'trigger' command."""
        self.events.post(name, **kwargs)

    def bcp_switch(self, name, state, **kwargs):
        """Processes an incoming BCP 'switch' command."""
        if int(state):
            self.events.post('switch_' + name + '_active')
        else:
            self.events.post('switch_' + name + '_inactive')

    def bcp_get(self, **kwargs):
        """Processes an incoming BCP 'get' command by posting an event
        'bcp_get_<name>'. It's up to an event handler to register for that
        event and to send the response BCP 'set' command.

        """
        for name in Util.string_to_list(names):
            self.events.post('bcp_get_{}'.format(name))

    def bcp_set(self, **kwargs):
        """Processes an incoming BCP 'set' command by posting an event
        'bcp_set_<name>' with a parameter value=<value>. It's up to an event
        handler to register for that event and to do something with it.

        Note that BCP set commands can contain multiple key/value pairs, and
        this method will post one event for each pair.

        """
        for k, v in kwargs.iteritems():
            self.events.post('bcp_set_{}'.format(k), value=v)

    def bcp_shot(self, name, profile, state):
        """The MPF media controller uses triggers instead of shots for its
        display events, so we don't need to pay attention here."""
        pass

    def bcp_config(self, **kwargs):
        """Processes an incoming BCP 'config' command."""
        for k, v in kwargs.iteritems():
            if k.startswith('volume_'):
                self.bcp_set_volume(track=k.split('volume_')[1], value=v)

    def bcp_timer(self, name, action, **kwargs):
        """Processes an incoming BCP 'timer' command."""
        pass

    def bcp_set_volume(self, track, value):
        """Sets the volume based on an incoming BCP 'config' command.

        Args:
            track: String name of the track the volume will set.
            value: Float between 0 and 1 which represents the volume level to
                set.

        Note: At this time only the master volume can be set with this method.

        """
        if track == 'master':
            self.sound.set_volume(value)

        #if track in self.sound.tracks:
            #self.sound.tracks[track]

            # todo add per-track volume support to sound system

    def get_debug_status(self, debug_path):
        if self.options['loglevel'] > 10 or self.options['consoleloglevel'] > 10:
            return True

        class_, module = debug_path.split('|')

        try:
            if module in self.active_debugger[class_]:
                return True
            else:
                return False
        except KeyError:
            return False

    def _set_machine_var(self, name, value):
        try:
            prev_value = self.machine_vars[name]
        except KeyError:
            prev_value = None

        self.machine_vars[name] = value

        try:
            change = value-prev_value
        except TypeError:
            if prev_value != value:
                change = True
            else:
                change = False

        if change:
            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)

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

    def _bcp_client_asset_loader_tick(self, total, remaining):
        self._pc_assets_to_load = int(remaining)
        self._pc_total_assets = int(total)

    def asset_loading_counter(self):

        if self.tick_num % 5 != 0:
            return

        if AssetManager.total_assets or self._pc_total_assets:
            # max because this could go negative at first
            percent = max(0, int(float(AssetManager.total_assets -
                                       self._pc_assets_to_load -
                                       AssetManager.loader_queue.qsize()) /
                                       AssetManager.total_assets * 100))
        else:
            percent = 100

        self.log.debug("Asset Loading Counter. PC remaining:{}, MC remaining:"
                       "{}, Percent Complete: {}".format(
                       self._pc_assets_to_load, AssetManager.loader_queue.qsize(),
                       percent))

        self.events.post('asset_loader',
                         total=AssetManager.loader_queue.qsize() +
                               self._pc_assets_to_load,
                         pc=self._pc_assets_to_load,
                         mc=AssetManager.loader_queue.qsize(),
                         percent=percent)

        if not AssetManager.loader_queue.qsize():

            if not self.pc_connected:
                self.events.post("waiting_for_client_connection")
                self.events.remove_handler(self.asset_loading_counter)

            elif not self._pc_assets_to_load:
                self.log.debug("Asset Loading Complete")
                self.events.post("asset_loading_complete")
                self.send('reset_complete')

                self.events.remove_handler(self.asset_loading_counter)