def __init__(self, config): super(DanceSurface, self).__init__() # Create the floor communication object self._init_comms(config) # Create the layout object and calculate floor size self.layout = DisplayLayout(config["modules"]) (self.width, self.height) = self.layout.calculate_floor_size() self.total_pixels = self.width * self.height # Initialise all the pixels to black self.pixels = [ 0 for n in range(0, 3*self.total_pixels) ] self.text_writer = TextWriter()
def run_ddrpi(self): self.logger.info("Starting DDRPi running at %s" % (time.strftime('%H:%M:%S %d/%m/%Y %Z'))) config = self.parse_commandline_arguments() self.logger.info("%s" % config) # Initialise pygame, which we use for the GUI, controllers, rate limiting etc... pygame.init() # Set up a list of output endpoints, and input adapaters output_devices = [] input_adapters = [] # Parse the floor layout layout = DisplayLayout(config) converter = layout.get_converter() # Create a suitably sized canvas for the given config canvas = FloorCanvas(layout.size_x, layout.size_y) # Create a menu object to handle user input menu = Menu() output_filters = [] if ("system" in config and "filters" in config["system"]): print ("Found filters config: %s" % config["system"]["filters"]) for filter_number, filter_config in sorted(config["system"]["filters"].items()): if ("name" in filter_config and filter_config["name"] == "ClearFilter"): output_filters.append(ClearFilter(filter_config)) elif ("name" in filter_config and filter_config["name"] == "NegativeFilter"): output_filters.append(NegativeFilter(filter_config)) elif ("name" in filter_config and filter_config["name"] == "NeutralDensityFilter"): output_filters.append(NeutralDensityFilter(filter_config)) # Set up the various outputs defined in the config. # Known types are the moment are "gui", "serial" and "pipe" if ("outputs" in config): for output_number, details in config["outputs"].items(): self.logger.info("%d - %s" % (output_number, details)) if "enabled" in details: if details["enabled"] is False: self.logger.info("Skipping disabled output %d" % output_number) self.logger.info("%s" % details) continue self.logger.info("Configuring output %d" % output_number) if details["type"] == "serial": self.logger.info("Creating a SerialOutput class") serial_output = SerialOutput(details) serial_output.set_name("SerialOutput-#%d" % output_number) serial_output.set_output_converter(converter) for output_filter in output_filters: serial_output.append_filter(output_filter) output_devices.append(serial_output) elif details["type"] == "gui": # Skip the gui if headless has been specified if "headless" in config["system"] and config["system"]["headless"] is True: continue self.logger.info("Creating a GuiOutput class [There can be only one]") gui_output = GuiOutput() output_devices.append(gui_output) self.gui = gui_output elif details["type"] == "pipe": self.logger.info("Creating a PipeOutput class") pipe_output = PipeOutput(details) pipe_output.set_output_converter(converter) output_devices.append(pipe_output) else: self.logger.warn("I don't know how to handle an output of type '%s'" % (details["type"])) # Initialise any connected joypads/joysticks controllers = self.init_joysticks() # There can be many plugin directories, and can either be relative (from the directory in which this # script resides), or absolute. All directories are scanned, and the plugins found are added to the # master list. If there are duplicate class names, the last one encountered probably wins silently. visualisation_plugin_dirs = ["visualisation_plugins", "game_plugins"] # We will store a dict of classname > class object in here available_plugins = self.load_plugins(visualisation_plugin_dirs) # Add all the available plugins to the menu #menu.add_available_plugins(available_plugins) # Create a data model that we can use here, and pass to the # menu class to change what is active # We should also be able to provide this model to a webservice # class if we chose to add in HTTP control of the floor too plugin_model = PluginModel((layout.size_x, layout.size_y)) # Populate the data model plugin_model.add_plugins(available_plugins) # Link it up to the menu menu.set_plugin_model(plugin_model) # If we have defined a specific plugin to run indefinitely on the command line, use that specific_plugin = False if "plugin" in config["system"]: only_plugin = config["system"]["plugin"] if plugin_model.set_current_plugin(only_plugin) is not None: self.logger.info("Running requested plugin %s" % only_plugin) specific_plugin = True else: self.logger.info("Unable to find requested plugin: %s" % (only_plugin)) self.logger.info("Available plugins are:") for available_plugin in available_plugins: self.logger.info(" %s" % (available_plugin)) # This is intended as a development debug option, or at the very least the # person using it should know what they are doing, so if it isn't available # for whatever reason, exit. exit() if "playlist" in config["system"]: # Retrieve the list of playlists specified on the command line # or config file playlist_list = config["system"]["playlist"] self.logger.info(playlist_list) if len(playlist_list) > 0: for playlist in playlist_list: # Make the playlist an absolute path if it isn't already if not os.path.isabs(playlist): root_directory = os.path.dirname(os.path.realpath(__file__)) playlist = os.path.join(root_directory, playlist) # Load the playlist plugin_model.add_playlist_from_file(playlist) # Start the first one we added self.logger.info("Setting current playlist to the first one we added") plugin_model.set_current_playlist_by_index(1) if "twitter" in config: if "enabled" in config["twitter"] and config["twitter"]["enabled"] is not True: # Don't do anything, we don't want it at the moment pass else: # Only bother importing this module if we are going to use it, hence not requiring # an extra dependency on a Twitter library if people don't want to use that from lib.twitter import TwitterPlaylist # Get the twitter credentials from the configuration file: twitter_details = config["twitter"] # If we want to limit which plugins can be selected by Twitter, do so here plugin_whitelist = available_plugins # Create the twitter plugin playlist twitter_playlist = TwitterPlaylist(plugins=plugin_whitelist, details=twitter_details) playlist_index = plugin_model.add_playlist(twitter_playlist) if plugin_model.get_current_playlist() is None: plugin_model.set_current_playlist_by_index(playlist_index) # When there is no plugin specified and no user playlists either, make the first # playlist active, and select either the first plugin, or, if it is available, # any of the plugins listed in DEFAULT_STARTUP_PLUGIN # .set_current_plugin() is a convenience method to start the all_plugins playlist # with the given plugin name self.logger.info("Current playlist: %s" % plugin_model.get_current_playlist()) have_started_a_plugin = False if plugin_model.get_current_playlist() is None: for specified_plugin in self.DEFAULT_STARTUP_PLUGIN: if plugin_model.set_current_plugin(specified_plugin) is not None: have_start_a_plugin = True self.logger.info("Started default plugin %s" % specified_plugin) break # If we still haven't started anything, just start the all_plugins playlist if have_started_a_plugin is False: plugin_model.set_current_playlist_by_index(0) print (plugin_model) # if self.gui is not None: #playlistModel.add_model_changed_listener(self.gui) # self.gui.set_playlist_model(playlistModel) self.gui.set_plugin_model(plugin_model) # Create an object that can map key events to joystick events self.controller_mapper = ControllerInput() # The main loop is an event loop, with each part # non-blocking and yields after doing a short bit. # Each 'bit' is a frame # Check for pygame events, primarily coming from # gamepads and the keyboard running = True while running: current_playlist = plugin_model.get_current_playlist() current_plugin = None if current_playlist is not None: current_plugin = current_playlist.get_current_plugin() for e in pygame.event.get(): if e.type == pygame.QUIT: running = False e = self.controller_mapper.map_event(e) # Each consumer should return None if the # event has been consumed, or return # the event if something else should # act upon it # Check first if the framework is going to # consume this event. This includes heading # into menus, possibly modifying the plugin # model (previous/next plugin) #e = self.handle_event(e) #if e is None: # continue # See if the menu wants to consume this event e = menu.handle_event(e) if e is None: continue #self.print_input_event(e) # Next pass it on to the current plugin, if # there is one if current_plugin is not None: # e = current_plugin['instance'].handle_event(e) e = current_plugin.instance.handle_event(e) if e is None: continue # Ask the framework if it thinks it is displaying something # display_frame = self.draw_frame(canvas) # Ask the menu if it wants to draw something display_frame = menu.draw_frame(canvas) if display_frame is None: # If there is nothing to draw from the framework, ask # the current plugin to do something is there is one if current_plugin is not None: # display_frame = current_plugin['instance'].draw_frame(canvas) # There is every chance that the plugin might throw an # exception as we have no control over the quality of # external code (or some in-house code!), so catch any # exception try: display_frame = current_plugin.instance.draw_frame(canvas) except Exception as e: self.logger.warn("Current plugin threw an error whilst running draw_frame()") self.logger.warn(e) display_frame = self.draw_error(canvas) else: # If there is no plugin, then the framework should # do something pass # Send the data to all configured outputs if there is any to send # TODO: If the plugin doesn't return a canvas for some reason, # then the gui should be updated with a canvas, but the output # plugins need not be informed. The problem is that if the plugin # happens to never return anything, then the gui is never updated if display_frame is None: canvas.set_colour((0, 0, 0)) display_frame = canvas for output_device in output_devices: output_device.send_data(display_frame) # Limit the framerate, we need not do it in the plugins - they really shouldn't # mind that we are running at a max of 25fps self.clock.tick(25) pygame.quit() exit()
class DanceSurface(object): """ The class representing the drawable dance floor. This is a wrapper around an internal representation of the dance floor, so that plugins need only write images using (x,y) coordinates and don't have to worry about the configuration of dance floor tiles. The dance surface is passed to the display plugins, and reacts to any changes made by sending the the appropriate updates to the dance floor through the serial port. """ def __init__(self, config): super(DanceSurface, self).__init__() # Create the floor communication object self._init_comms(config) # Create the layout object and calculate floor size self.layout = DisplayLayout(config["modules"]) (self.width, self.height) = self.layout.calculate_floor_size() self.total_pixels = self.width * self.height # Initialise all the pixels to black self.pixels = [ 0 for n in range(0, 3*self.total_pixels) ] self.text_writer = TextWriter() def _init_comms(self, config): """ Initialise the comms to the floor or simulator """ self.comms = ComboComms(config) def blit(self): """ Draw the updated floor to the serial port """ i = 0 sanitised_pixels = [] while i < len(self.pixels): p = self.pixels[i] if p < 0: logging.error("DanceSurface: Tried to send a pixel component with a negative value") sanitised_pixels.append(0) elif p > 255: logging.error("DanceSurface: Tried to send a pixel component with a value > 255") sanitised_pixels.append(255) else: sanitised_pixels.append(p) i += 1 self.pixels = sanitised_pixels self.comms.send_data(self.pixels) def clear_hex(self, colour): """ Clear the surface to a single colour """ # Make sure we never send a 1 by mistake and screw up the frames (r,g,b) = [ v if not v == 1 else 0 for v in ColourUtils.hexToTuple(colour) ] for x in range(0,self.total_pixels): self.pixels[x*3:(x+1)*3] = [r,g,b] def clear_tuple(self, colour): """ Clear the surface to a single colour """ # Make sure we never send a 1 by mistake and screw up the frames (r,g,b) = [ v if not v == 1 else 0 for v in colour ] for x in range(0,self.total_pixels): self.pixels[x*3:(x+1)*3] = [r,g,b] def draw_hex_pixel(self, x, y, colour): """ Set the value of the pixel at (x,y) to colour(#RRGGBB") """ # Make sure we never send a 1 by mistake and screw up the frames (r,g,b) = [ v if not v == 1 else 0 for v in ColourUtils.hexToTuple(colour) ] pos = self.layout.get_position(x,y) if pos is not None: mapped_pixel = 3 * pos self.pixels[mapped_pixel:mapped_pixel+3] = [r,g,b] def draw_tuple_pixel(self, x, y, colour): """ Set the value of the pixel at (x,y) to colour((r,g,b)) """ # Make sure we never send a 1 by mistake and screw up the frames (r,g,b) = [ v if not v == 1 else 0 for v in colour ] pos = self.layout.get_position(x,y) if pos is not None: mapped_pixel = 3 * pos self.pixels[mapped_pixel:mapped_pixel+3] = [r,g,b] def draw_float_tuple_pixel(self, x, y, colour): """ Set the value of the pixel at (x,y) to colour((r,g,b)) where r g and b are floats """ (floatR, floatG, floatB) = colour intR = int(floatR*255) intG = int(floatG*255) intB = int(floatB*255) self.draw_tuple_pixel(x, y, (intR, intG, intB)) def draw_tuple_box(self, top_left, bottom_right, colour): """ Fill the box from top left to bottom right with the given colour """ (tlx,tly) = top_left (brx,bry) = bottom_right if tlx <= brx and tly <= bry: y = tly while y <= bry: x = tlx while x <= brx: self.draw_tuple_pixel(x, y, colour) x += 1 y += 1 def draw_text(self, text, colour, x_pos, y_pos): if (self.text_writer == None): return (0,0) # Returns the text size as a (width, height) tuple for reference text_size = self.text_writer.draw_text(self, text, colour, x_pos, y_pos) return text_size def get_text_size(self, text): if (self.text_writer == None): return (0,0) # Returns the text size as a (width, height) tuple for reference, # but doesn't actually draw anything because it doesn't pass a surface through return self.text_writer.draw_text(None, text, (0,0,0), 0, 0)
def run_ddrpi(self): self.logger.info("Starting DDRPi running at %s" % (time.strftime('%H:%M:%S %d/%m/%Y %Z'))) config = self.parse_commandline_arguments() self.logger.info("%s" % config) # Initialise pygame, which we use for the GUI, controllers, rate limiting etc... pygame.init() # Set up a list of output endpoints, and input adapaters output_devices = [] input_adapters = [] # Parse the floor layout layout = DisplayLayout(config) converter = layout.get_converter() # Create a suitably sized canvas for the given config canvas = FloorCanvas(layout.size_x, layout.size_y) # Create a menu object to handle user input menu = Menu() output_filters = [] if ("system" in config and "filters" in config["system"]): print("Found filters config: %s" % config["system"]["filters"]) for filter_number, filter_config in sorted( config["system"]["filters"].items()): if ("name" in filter_config and filter_config["name"] == "ClearFilter"): output_filters.append(ClearFilter(filter_config)) elif ("name" in filter_config and filter_config["name"] == "NegativeFilter"): output_filters.append(NegativeFilter(filter_config)) elif ("name" in filter_config and filter_config["name"] == "NeutralDensityFilter"): output_filters.append(NeutralDensityFilter(filter_config)) # Set up the various outputs defined in the config. # Known types are the moment are "gui", "serial" and "pipe" if ("outputs" in config): for output_number, details in config["outputs"].items(): self.logger.info("%d - %s" % (output_number, details)) if "enabled" in details: if details["enabled"] is False: self.logger.info("Skipping disabled output %d" % output_number) self.logger.info("%s" % details) continue self.logger.info("Configuring output %d" % output_number) if details["type"] == "serial": self.logger.info("Creating a SerialOutput class") serial_output = SerialOutput(details) serial_output.set_name("SerialOutput-#%d" % output_number) serial_output.set_output_converter(converter) for output_filter in output_filters: serial_output.append_filter(output_filter) output_devices.append(serial_output) elif details["type"] == "gui": # Skip the gui if headless has been specified if "headless" in config[ "system"] and config["system"]["headless"] is True: continue self.logger.info( "Creating a GuiOutput class [There can be only one]") gui_output = GuiOutput() output_devices.append(gui_output) self.gui = gui_output elif details["type"] == "pipe": self.logger.info("Creating a PipeOutput class") pipe_output = PipeOutput(details) pipe_output.set_output_converter(converter) output_devices.append(pipe_output) else: self.logger.warn( "I don't know how to handle an output of type '%s'" % (details["type"])) # Initialise any connected joypads/joysticks controllers = self.init_joysticks() # There can be many plugin directories, and can either be relative (from the directory in which this # script resides), or absolute. All directories are scanned, and the plugins found are added to the # master list. If there are duplicate class names, the last one encountered probably wins silently. visualisation_plugin_dirs = ["visualisation_plugins", "game_plugins"] # We will store a dict of classname > class object in here available_plugins = self.load_plugins(visualisation_plugin_dirs) # Add all the available plugins to the menu #menu.add_available_plugins(available_plugins) # Create a data model that we can use here, and pass to the # menu class to change what is active # We should also be able to provide this model to a webservice # class if we chose to add in HTTP control of the floor too plugin_model = PluginModel((layout.size_x, layout.size_y)) # Populate the data model plugin_model.add_plugins(available_plugins) # Link it up to the menu menu.set_plugin_model(plugin_model) # If we have defined a specific plugin to run indefinitely on the command line, use that specific_plugin = False if "plugin" in config["system"]: only_plugin = config["system"]["plugin"] if plugin_model.set_current_plugin(only_plugin) is not None: self.logger.info("Running requested plugin %s" % only_plugin) specific_plugin = True else: self.logger.info("Unable to find requested plugin: %s" % (only_plugin)) self.logger.info("Available plugins are:") for available_plugin in available_plugins: self.logger.info(" %s" % (available_plugin)) # This is intended as a development debug option, or at the very least the # person using it should know what they are doing, so if it isn't available # for whatever reason, exit. exit() if "playlist" in config["system"]: # Retrieve the list of playlists specified on the command line # or config file playlist_list = config["system"]["playlist"] self.logger.info(playlist_list) if len(playlist_list) > 0: for playlist in playlist_list: # Make the playlist an absolute path if it isn't already if not os.path.isabs(playlist): root_directory = os.path.dirname( os.path.realpath(__file__)) playlist = os.path.join(root_directory, playlist) # Load the playlist plugin_model.add_playlist_from_file(playlist) # Start the first one we added self.logger.info( "Setting current playlist to the first one we added") plugin_model.set_current_playlist_by_index(1) if "twitter" in config: if "enabled" in config["twitter"] and config["twitter"][ "enabled"] is not True: # Don't do anything, we don't want it at the moment pass else: # Only bother importing this module if we are going to use it, hence not requiring # an extra dependency on a Twitter library if people don't want to use that from lib.twitter import TwitterPlaylist # Get the twitter credentials from the configuration file: twitter_details = config["twitter"] # If we want to limit which plugins can be selected by Twitter, do so here plugin_whitelist = available_plugins # Create the twitter plugin playlist twitter_playlist = TwitterPlaylist(plugins=plugin_whitelist, details=twitter_details) playlist_index = plugin_model.add_playlist(twitter_playlist) if plugin_model.get_current_playlist() is None: plugin_model.set_current_playlist_by_index(playlist_index) # When there is no plugin specified and no user playlists either, make the first # playlist active, and select either the first plugin, or, if it is available, # any of the plugins listed in DEFAULT_STARTUP_PLUGIN # .set_current_plugin() is a convenience method to start the all_plugins playlist # with the given plugin name self.logger.info("Current playlist: %s" % plugin_model.get_current_playlist()) have_started_a_plugin = False if plugin_model.get_current_playlist() is None: for specified_plugin in self.DEFAULT_STARTUP_PLUGIN: if plugin_model.set_current_plugin( specified_plugin) is not None: have_start_a_plugin = True self.logger.info("Started default plugin %s" % specified_plugin) break # If we still haven't started anything, just start the all_plugins playlist if have_started_a_plugin is False: plugin_model.set_current_playlist_by_index(0) print(plugin_model) # if self.gui is not None: #playlistModel.add_model_changed_listener(self.gui) # self.gui.set_playlist_model(playlistModel) self.gui.set_plugin_model(plugin_model) # Create an object that can map key events to joystick events self.controller_mapper = ControllerInput() # The main loop is an event loop, with each part # non-blocking and yields after doing a short bit. # Each 'bit' is a frame # Check for pygame events, primarily coming from # gamepads and the keyboard running = True while running: current_playlist = plugin_model.get_current_playlist() current_plugin = None if current_playlist is not None: current_plugin = current_playlist.get_current_plugin() for e in pygame.event.get(): if e.type == pygame.QUIT: running = False e = self.controller_mapper.map_event(e) # Each consumer should return None if the # event has been consumed, or return # the event if something else should # act upon it # Check first if the framework is going to # consume this event. This includes heading # into menus, possibly modifying the plugin # model (previous/next plugin) #e = self.handle_event(e) #if e is None: # continue # See if the menu wants to consume this event e = menu.handle_event(e) if e is None: continue #self.print_input_event(e) # Next pass it on to the current plugin, if # there is one if current_plugin is not None: # e = current_plugin['instance'].handle_event(e) e = current_plugin.instance.handle_event(e) if e is None: continue # Ask the framework if it thinks it is displaying something # display_frame = self.draw_frame(canvas) # Ask the menu if it wants to draw something display_frame = menu.draw_frame(canvas) if display_frame is None: # If there is nothing to draw from the framework, ask # the current plugin to do something is there is one if current_plugin is not None: # display_frame = current_plugin['instance'].draw_frame(canvas) # There is every chance that the plugin might throw an # exception as we have no control over the quality of # external code (or some in-house code!), so catch any # exception try: display_frame = current_plugin.instance.draw_frame( canvas) except Exception as e: self.logger.warn( "Current plugin threw an error whilst running draw_frame()" ) self.logger.warn(e) display_frame = self.draw_error(canvas) else: # If there is no plugin, then the framework should # do something pass # Send the data to all configured outputs if there is any to send # TODO: If the plugin doesn't return a canvas for some reason, # then the gui should be updated with a canvas, but the output # plugins need not be informed. The problem is that if the plugin # happens to never return anything, then the gui is never updated if display_frame is None: canvas.set_colour((0, 0, 0)) display_frame = canvas for output_device in output_devices: output_device.send_data(display_frame) # Limit the framerate, we need not do it in the plugins - they really shouldn't # mind that we are running at a max of 25fps self.clock.tick(25) pygame.quit() exit()