class ResourceReader(object): config = None def __init__(self): self.mon = Monitor() self.mon.on() def read(self, pp_dir, pp_home, pp_profile): """ looks for resources.cfg in the profile, then in pp_home, then in the pi_presents directory. returns True if it finds the resources.cfg, otherwise returns False ::param pp_dir: the PiPresents directory ::param pp_home: the current pp_home directory ::param pp_profile: the current profile directory """ if not ResourceReader.config: profile_config = os.path.join(pp_profile, "resources.cfg") home_config = os.path.join(pp_home, "resources.cfg") pp_config = os.path.join(pp_dir, 'pp_home', "resources.cfg") # try inside profile if os.path.exists(profile_config): config_path = profile_config # try inside pp_home elif os.path.exists(home_config): config_path = home_config # try in the pi presents directory elif os.path.exists(pp_config): config_path = pp_config else: # throw an error if we can't find any config files self.mon.err(self, "resources.cfg not found at {0}, {1} or {2}".format(profile_config, home_config, pp_config)) return False ResourceReader.config = ConfigParser.ConfigParser() ResourceReader.config.read(config_path) self.mon.log(self, "resources.cfg read from " + config_path) return True def get(self, section, item): if not ResourceReader.config.has_option(section, item): return False else: return ResourceReader.config.get(section, item)
class KbdDriver(object): config = None def __init__(self): self.mon = Monitor() # sets up tkinter keyboard events such that any key press # does a callback to 'callback' with the event object and a symbolic name. def bind_keys(self, widget, callback): for option in KbdDriver.config.items("keys"): condition = option[0] symbolic_name = option[1] # print condition,symbolic_name widget.bind(condition, lambda event, name=symbolic_name: self.specific_key(callback, name)) # bind all the normal keys that return a printing character such that x produces pp-key-x widget.bind("<Key>", lambda event: self.normal_key(callback, event)) def specific_key(self, callback, name): callback(name, "KBD") # alphanumeric keys- convert to symbolic by adding pp-key- def normal_key(self, callback, event): key = event.char if key != "": callback("pp-key-" + key, "KBD") # read the key bindings from keys.cfg def read(self, pp_dir, pp_home, pp_profile): if KbdDriver.config is None: # try inside profile tryfile = pp_profile + os.sep + "pp_io_config" + os.sep + "keys.cfg" if os.path.exists(tryfile): self.mon.log(self, "Found keys.cfg in profile at: " + tryfile) filename = tryfile else: # try inside pipresents tryfile = pp_dir + os.sep + "pp_config" + os.sep + "keys.cfg" if os.path.exists(tryfile): filename = tryfile self.mon.log(self, "Fallback keys.cfg found at " + tryfile) else: self.mon.log(self, "keys.cfg not found at " + tryfile) self.mon.err(self, "keys.cfg not found in profile or fallback in Pi Presents") return False KbdDriver.config = ConfigParser.ConfigParser() KbdDriver.config.optionxform = str KbdDriver.config.read(filename) self.mon.log(self, "keys.cfg read from " + filename) if KbdDriver.config.has_section("keys") is False: self.mon.err(self, "no [keys] section in keys.cfg") return False return True def has_section(self, section): if KbdDriver.config.has_section(section) is False: return False
class ResourceReader: config = None def __init__(self): self.mon = Monitor() self.mon.on() def read(self, pp_dir, pp_home, pp_profile): if ResourceReader.config == None: # try inside profile tryfile = pp_profile + os.sep + "resources.cfg" # self.mon.log(self,"Trying resources.cfg in profile at: "+ tryfile) if os.path.exists(tryfile): filename = tryfile else: # try inside pp_home # self.mon.log(self,"resources.cfg not found at "+ tryfile+ " trying pp_home") tryfile = pp_home + os.sep + "resources.cfg" if os.path.exists(tryfile): filename = tryfile else: # try inside pipresents # self.mon.log(self,"resources.cfg not found at "+ tryfile + " trying inside pipresents") tryfile = pp_dir + os.sep + 'pp_home' + os.sep + "resources.cfg" if os.path.exists(tryfile): filename = tryfile else: self.mon.log(self, "resources.cfg not found at " + tryfile) self.mon.err(self, "resources.cfg not found") return False ResourceReader.config = ConfigParser.ConfigParser() ResourceReader.config.read(filename) self.mon.log(self, "resources.cfg read from " + filename) return True def get(self, section, item): if ResourceReader.config.has_option(section, item) == False: return False else: return ResourceReader.config.get(section, item)
class ResourceReader: config=None def __init__(self): self.mon=Monitor() self.mon.on() def read(self,pp_dir,pp_home,pp_profile): if ResourceReader.config==None: # try inside profile tryfile=pp_profile+os.sep+"resources.cfg" # self.mon.log(self,"Trying resources.cfg in profile at: "+ tryfile) if os.path.exists(tryfile): filename=tryfile else: # try inside pp_home # self.mon.log(self,"resources.cfg not found at "+ tryfile+ " trying pp_home") tryfile=pp_home+os.sep+"resources.cfg" if os.path.exists(tryfile): filename=tryfile else: # try inside pipresents # self.mon.log(self,"resources.cfg not found at "+ tryfile + " trying inside pipresents") tryfile=pp_dir+os.sep+'pp_home'+os.sep+"resources.cfg" if os.path.exists(tryfile): filename=tryfile else: self.mon.log(self,"resources.cfg not found at "+ tryfile) self.mon.err(self,"resources.cfg not found") return False ResourceReader.config = ConfigParser.ConfigParser() ResourceReader.config.read(filename) self.mon.log(self,"resources.cfg read from "+ filename) return True def get(self,section,item): if ResourceReader.config.has_option(section,item)==False: return False else: return ResourceReader.config.get(section,item)
class ResourceReader: config=None def __init__(self): self.mon=Monitor() self.mon.on() def read(self,pp_dir,pp_home): if ResourceReader.config==None: tryfile=pp_home+os.sep+"resources.cfg" if os.path.exists(tryfile): filename=tryfile else: self.mon.log(self,"Resources not found at "+ tryfile) tryfile=pp_dir+os.sep+'pp_home'+os.sep+"resources.cfg" if os.path.exists(tryfile): filename=tryfile else: self.mon.log(self,"Resources not found at "+ tryfile) self.mon.err(self,"resources.cfg not found") return False ResourceReader.config = ConfigParser.ConfigParser() ResourceReader.config.read(filename) self.mon.log(self,"Read resources from "+ filename) return True def get(self,section,item): if ResourceReader.config.has_option(section,item)==False: return False else: return ResourceReader.config.get(section,item)
class ResourceReader: config = None def __init__(self): self.mon = Monitor() self.mon.on() def read(self, pp_dir, pp_home): if ResourceReader.config == None: tryfile = pp_home + os.sep + "resources.cfg" if os.path.exists(tryfile): filename = tryfile else: self.mon.log(self, "Resources not found at " + tryfile) tryfile = pp_dir + os.sep + 'pp_home' + os.sep + "resources.cfg" if os.path.exists(tryfile): filename = tryfile else: self.mon.log(self, "Resources not found at " + tryfile) self.mon.err(self, "resources.cfg not found") return False ResourceReader.config = ConfigParser.ConfigParser() ResourceReader.config.read(filename) self.mon.log(self, "Read resources from " + filename) return True def get(self, section, item): if ResourceReader.config.has_option(section, item) == False: return False else: return ResourceReader.config.get(section, item)
class VideoPlayer: """ plays a track using omxplayer See pp_imageplayer for common software design description """ _CLOSED = "omx_closed" #probably will not exist _STARTING = "omx_starting" #track is being prepared _PLAYING = "omx_playing" #track is playing to the screen, may be paused _ENDING = "omx_ending" #track is in the process of ending due to quit or end of track # *************************************** # EXTERNAL COMMANDS # *************************************** def __init__(self, show_id, root, canvas, show_params, track_params , pp_dir, pp_home, pp_profile): self.mon=Monitor() self.mon.on() #instantiate arguments self.show_id=show_id self.root=root self.canvas = canvas self.show_params=show_params self.track_params=track_params self.pp_dir=pp_dir self.pp_home=pp_home self.pp_profile=pp_profile # get config from medialist if there. if self.track_params['omx-audio']<>"": self.omx_audio= self.track_params['omx-audio'] else: self.omx_audio= self.show_params['omx-audio'] if self.omx_audio<>"": self.omx_audio= "-o "+ self.omx_audio if self.track_params['omx-volume']<>"": self.omx_volume= self.track_params['omx-volume'] else: self.omx_volume= self.show_params['omx-volume'] if self.omx_volume<>"": self.omx_volume= "--vol "+ str(int(self.omx_volume)*100) + ' ' if self.track_params['omx-window']<>'': self.omx_window= self.track_params['omx-window'] else: self.omx_window= self.show_params['omx-window'] # get background image from profile. self.background_file='' if self.track_params['background-image']<>"": self.background_file= self.track_params['background-image'] else: if self.track_params['display-show-background']=='yes': self.background_file= self.show_params['background-image'] # get background colour from profile. if self.track_params['background-colour']<>"": self.background_colour= self.track_params['background-colour'] else: self.background_colour= self.show_params['background-colour'] self.centre_x = int(self.canvas['width'])/2 self.centre_y = int(self.canvas['height'])/2 #get animation instructions from profile self.animate_begin_text=self.track_params['animate-begin'] self.animate_end_text=self.track_params['animate-end'] # open the plugin Manager self.pim=PluginManager(self.show_id,self.root,self.canvas,self.show_params,self.track_params,self.pp_dir,self.pp_home,self.pp_profile) #create an instance of PPIO so we can create gpio events self.ppio = PPIO() # could put instance generation in play, not sure which is better. self.omx=OMXDriver(self.canvas) self.tick_timer=None self.init_play_state_machine() def play(self, track, showlist, end_callback, ready_callback, enable_menu=False): #instantiate arguments self.track=track self.showlist=showlist self.ready_callback=ready_callback #callback when ready to play self.end_callback=end_callback # callback when finished self.enable_menu = enable_menu # callback to the calling object to e.g remove egg timer and enable click areas. if self.ready_callback<>None: self.ready_callback() # create an instance of showmanager so we can control concurrent shows self.show_manager=ShowManager(self.show_id,self.showlist,self.show_params,self.root,self.canvas,self.pp_dir,self.pp_profile,self.pp_home) #set up video window reason,message,comand,has_window,x1,y1,x2,y2= self.parse_window(self.omx_window) if reason =='error': self.mon.err(self,'omx window error: ' + message + ' in ' + self.omx_window) self.end_callback(reason,message) else: if has_window==True: self.omx_window= '--win " '+ str(x1) + ' ' + str(y1) + ' ' + str(x2) + ' ' + str(y2) + ' " ' else: self.omx_window='' # Control other shows at beginning reason,message=self.show_manager.show_control(self.track_params['show-control-begin']) if reason in ('error','killed'): self.end_callback(reason,message) self=None else: #display content reason,message=self.display_content() if reason == 'error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # create animation events reason,message=self.ppio.animate(self.animate_begin_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # start playing the video. if self.play_state == VideoPlayer._CLOSED: self.mon.log(self,">play track received") self.start_play_state_machine(self.track) else: self.mon.err(self,'play track rejected') self.end_callback('error','play track rejected') self=None def terminate(self,reason): # circumvents state machine and does not wait for omxplayer to close if self.omx<>None: self.mon.log(self,"sent terminate to omxdriver") self.omx.terminate(reason) self.end('killed',' end without waiting for omxplayer to finish') # end without waiting else: self.mon.log(self,"terminate, omxdriver not running") self.end('killed','terminate, mplayerdriver not running') def input_pressed(self,symbol): if symbol[0:4]=='omx-': self.control(symbol[4]) elif symbol =='pause': self.pause() elif symbol=='stop': self.stop() else: pass def get_links(self): return self.track_params['links'] # *************************************** # INTERNAL FUNCTIONS # *************************************** # respond to normal stop def stop(self): # send signal to stop the track to the state machine self.mon.log(self,">stop received") self.quit_signal=True #toggle pause def pause(self): if self.play_state in (VideoPlayer._PLAYING,VideoPlayer._ENDING): self.omx.pause() return True else: self.mon.log(self,"!<pause rejected") return False # other control when playing def control(self,char): if self.play_state==VideoPlayer._PLAYING and char not in ('q'): self.mon.log(self,"> send control to omx: "+ char) self.omx.control(char) return True else: self.mon.log(self,"!<control rejected") return False # *********************** # sequencing # ********************** """self. play_state controls the playing sequence, it has the following values. I am not entirely sure the starting and ending states are required. - _closed - the omx process is not running, omx process can be initiated - _starting - omx process is running but is not yet able to receive controls - _playing - playing a track, controls can be sent - _ending - omx is doing its termination, controls cannot be sent """ def init_play_state_machine(self): self.quit_signal=False self.play_state=VideoPlayer._CLOSED def start_play_state_machine(self,track): #initialise all the state machine variables #self.iteration = 0 # for debugging self.quit_signal=False # signal that user has pressed stop self.play_state=VideoPlayer._STARTING #play the selected track options=self.omx_audio+ " " + self.omx_volume + ' ' + self.omx_window + ' ' + self.show_params['omx-other-options']+" " self.omx.play(track,options) self.mon.log (self,'Playing track from show Id: '+ str(self.show_id)) # and start polling for state changes self.tick_timer=self.canvas.after(50, self.play_state_machine) def play_state_machine(self): if self.play_state == VideoPlayer._CLOSED: self.mon.log(self," State machine: " + self.play_state) return elif self.play_state == VideoPlayer._STARTING: self.mon.log(self," State machine: " + self.play_state) # if omxplayer is playing the track change to play state if self.omx.start_play_signal==True: self.mon.log(self," <start play signal received from omx") self.omx.start_play_signal=False self.play_state=VideoPlayer._PLAYING self.mon.log(self," State machine: omx_playing started") self.tick_timer=self.canvas.after(50, self.play_state_machine) elif self.play_state == VideoPlayer._PLAYING: # self.mon.log(self," State machine: " + self.play_state) # service any queued stop signals if self.quit_signal==True: self.mon.log(self," Service stop required signal") self.stop_omx() self.quit_signal=False # self.play_state = VideoPlayer._ENDING # omxplayer reports it is terminating so change to ending state if self.omx.end_play_signal: self.mon.log(self," <end play signal received") self.mon.log(self," <end detected at: " + str(self.omx.video_position)) if self.omx.end_play_reason<>'nice_day': # deal with omxplayer not sending 'have a nice day' self.mon.warn(self," <end detected at: " + str(self.omx.video_position)) self.mon.warn(self," <pexpect reports: "+self.omx.end_play_reason) self.mon.warn(self,'pexpect.before is'+self.omx.xbefore) self.play_state = VideoPlayer._ENDING self.ending_count=0 self.tick_timer=self.canvas.after(200, self.play_state_machine) elif self.play_state == VideoPlayer._ENDING: self.mon.log(self," State machine: " + self.play_state) # if spawned process has closed can change to closed state self.mon.log (self," State machine : is omx process running? - " + str(self.omx.is_running())) if self.omx.is_running() ==False: self.mon.log(self," <omx process is dead") self.play_state = VideoPlayer._CLOSED self.end('normal','quit by user or system') else: self.ending_count+=1 if self.ending_count>10: # deal with omxplayer not terminating at the end of a track self.mon.warn(self," <omxplayer failed to close at: " + str(self.omx.video_position)) self.mon.warn(self,'pexpect.before is'+self.omx.xbefore) self.omx.kill() self.mon.warn(self,'omxplayer now terminated ') self.play_state = VideoPlayer._CLOSED self.end('normal','end from omxplayer failed to terminate') else: self.tick_timer=self.canvas.after(200, self.play_state_machine) def stop_omx(self): # send signal to stop the track to the state machine self.mon.log(self," >stop omx received from state machine") if self.play_state==VideoPlayer._PLAYING: self.omx.stop() return True else: self.mon.log(self,"!<stop rejected") return False # ***************** # ending the player # ***************** def end(self,reason,message): # stop the plugin if self.track_params['plugin']<>'': self.pim.stop_plugin() # os.system("xrefresh -display :0") # abort the timer if self.tick_timer<>None: self.canvas.after_cancel(self.tick_timer) self.tick_timer=None if reason in ('error','killed'): self.end_callback(reason,message) self=None else: # normal end so do show control and animation # Control concurrent shows at end reason,message=self.show_manager.show_control(self.track_params['show-control-end']) if reason =='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # clear events list for this track if self.track_params['animate-clear']=='yes': self.ppio.clear_events_list(id(self)) # create animation events for ending reason,message=self.ppio.animate(self.animate_begin_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: self.end_callback('normal',"track has terminated or quit") self=None # ***************** # displaying things # ***************** def display_content(self): #background colour if self.background_colour<>'': self.canvas.config(bg=self.background_colour) # delete previous content self.canvas.delete('pp-content') # background image if self.background_file<>'': self.background_img_file = self.complete_path(self.background_file) if not os.path.exists(self.background_img_file): self.mon.err(self,"Video background file not found: "+ self.background_img_file) self.end('error',"Video background file not found") else: pil_background_img=PIL.Image.open(self.background_img_file) self.background = PIL.ImageTk.PhotoImage(pil_background_img) self.drawn = self.canvas.create_image(int(self.canvas['width'])/2, int(self.canvas['height'])/2, image=self.background, anchor=CENTER, tag='pp-content') # execute the plugin if required if self.track_params['plugin']<>'': reason,message,self.track = self.pim.do_plugin(self.track,self.track_params['plugin'],) if reason <> 'normal': return reason,message # display show text if enabled if self.show_params['show-text']<> '' and self.track_params['display-show-text']=='yes': self.canvas.create_text(int(self.show_params['show-text-x']),int(self.show_params['show-text-y']), anchor=NW, text=self.show_params['show-text'], fill=self.show_params['show-text-colour'], font=self.show_params['show-text-font'], tag='pp-content') # display track text if enabled if self.track_params['track-text']<> '': self.canvas.create_text(int(self.track_params['track-text-x']),int(self.track_params['track-text-y']), anchor=NW, text=self.track_params['track-text'], fill=self.track_params['track-text-colour'], font=self.track_params['track-text-font'], tag='pp-content') # display instructions if enabled if self.enable_menu== True: self.canvas.create_text(int(self.show_params['hint-x']), int(self.show_params['hint-y']), text=self.show_params['hint-text'], fill=self.show_params['hint-colour'], font=self.show_params['hint-font'], anchor=NW, tag='pp-content') self.canvas.tag_raise('pp-click-area') self.canvas.update_idletasks( ) return 'normal','' # **************** # utilities # ***************** def complete_path(self,track_file): # complete path of the filename of the selected entry if track_file[0]=="+": track_file=self.pp_home+track_file[1:] self.mon.log(self,"Background image is "+ track_file) return track_file # original _ # warp _ or xy2 def parse_window(self,line): fields = line.split() # check there is a command field if len(fields) < 1: return 'error','no type field','',False,0,0,0,0 # deal with original which has 1 if fields[0]=='original': if len(fields) <> 1: return 'error','number of fields for original','',False,0,0,0,0 return 'normal','',fields[0],False,0,0,0,0 #deal with warp which has 1 or 5 arguments # check basic syntax if fields[0] <>'warp': return 'error','not a valid type','',False,0,0,0,0 if len(fields) not in (1,5): return 'error','wrong number of coordinates for warp','',False,0,0,0,0 # deal with window coordinates if len(fields) == 5: #window is specified if not (fields[1].isdigit() and fields[2].isdigit() and fields[3].isdigit() and fields[4].isdigit()): return 'error','coordinates are not positive integers','',False,0,0,0,0 has_window=True return 'normal','',fields[0],has_window,int(fields[1]),int(fields[2]),int(fields[3]),int(fields[4]) else: # fullscreen has_window=True return 'normal','',fields[0],has_window,0,0,self.canvas['width'],self.canvas['height']
class BrowserPlayer: #state constants _CLOSED = "player_closed" #probably will not exist _STARTING = "player_starting" #uzbl beinf loaded and fifo created _WAITING = "wait for timeout" # waiting for browser to appear on the screen _PLAYING = "player_playing" #track is playing to the screen _ENDING = "player_ending" #track is in the process of ending due to quit or duration exceeded # *************************************** # EXTERNAL COMMANDS # *************************************** def __init__(self, show_id, root, canvas, show_params, track_params, pp_dir, pp_home, pp_profile): self.mon=Monitor() self.mon.on() #instantiate arguments self.show_id=show_id self.root=root, self.canvas = canvas self.show_params=show_params self.track_params=track_params self.pp_dir=pp_dir self.pp_home=pp_home self.pp_profile=pp_profile # get duration limit (secs ) from profile if self.track_params['duration']<>"": self.duration= int(self.track_params['duration']) else: self.duration= int(self.show_params['duration']) self.duration_limit=20*self.duration # get background image from profile. self.background_file='' if self.track_params['background-image']<>"": self.background_file= self.track_params['background-image'] else: if self.track_params['display-show-background']=='yes': self.background_file= self.show_params['background-image'] # get background colour from profile. if self.track_params['background-colour']<>"": self.background_colour= self.track_params['background-colour'] else: self.background_colour= self.show_params['background-colour'] #get animation instructions from profile self.animate_begin_text=self.track_params['animate-begin'] self.animate_end_text=self.track_params['animate-end'] # open the plugin Manager self.pim=PluginManager(self.show_id,self.root,self.canvas,self.show_params,self.track_params,self.pp_dir,self.pp_home,self.pp_profile) #create an instance of PPIO so we can create gpio events self.ppio = PPIO() # could put instance generation in play, not sure which is better. self.bplayer=uzblDriver(self.canvas) self.command_timer=None self.tick_timer=None self.init_play_state_machine() def play(self, track, showlist, end_callback, ready_callback, enable_menu=False): #instantiate arguments self.track=track self.showlist=showlist self.end_callback=end_callback # callback when finished self.ready_callback=ready_callback #callback when ready to play self.enable_menu=enable_menu # callback to the calling object to e.g remove egg timer. if self.ready_callback<>None: self.ready_callback() # create an instance of showmanager so we can control concurrent shows self.show_manager=ShowManager(self.show_id,self.showlist,self.show_params,self.root,self.canvas,self.pp_dir,self.pp_profile,self.pp_home) #web window if self.track_params['web-window']<>'': self.web_window= self.track_params['web-window'] else: self.web_window= self.show_params['web-window'] reason,message,command,has_window,x1,y1,x2,y2=self.parse_window(self.web_window) if reason =='error': self.mon.err(self,'web window error: '+' ' + message + ' in ' + self.web_window) self.end_callback(reason,message) self=None else: #deal with web_window if has_window==False: self.geometry = ' --geometry=maximized ' else: width=x2-x1 height=y2-y1 self.geometry = "--geometry=%dx%d%+d%+d " % (width,height,x1,y1) # get browser commands reason,message=self.parse_commands(self.track_params['browser-commands']) if reason != 'normal': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # Control other shows at beginning reason,message=self.show_manager.show_control(self.track_params['show-control-begin']) if reason == 'error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: #display content reason,message=self.display_content() if reason == 'error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # create animation events reason,message=self.ppio.animate(self.animate_begin_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # start playing the track. self.start_play_state_machine() def terminate(self,reason): """ terminate the player in special circumstances normal user termination if by key_pressed 'stop' reason will be killed or error """ # circumvents state machine to terminate lower level and then itself. if self.bplayer<>None: self.mon.log(self,"sent terminate to uzbldriver") self.bplayer.terminate(reason) self.end('killed',' end without waiting for uzbl to finish') # end without waiting else: self.mon.log(self,"terminate, uzbldriver not running") self.end('killed','terminate, uzbldriver not running') def get_links(self): return self.track_params['links'] def input_pressed(self,symbol): # print symbol, symbol[0:5] if symbol[0:5]=='uzbl-': self.control(symbol[5:]) elif symbol == 'pause': self.pause() elif symbol=='stop': self.stop() # *************************************** # INTERNAL FUNCTIONS # *************************************** #browser do not do pause def pause(self): self.mon.log(self,"!<pause rejected") return False # other control when playing, not currently used def control(self,char): if self.play_state==BrowserPlayer._PLAYING and char not in ('exit'): self.mon.log(self,"> send control to uzbl:"+ char) self.bplayer.control(char) return True else: self.mon.log(self,"!<control rejected") return False # respond to normal stop def stop(self): # send signal to stop the track to the state machine self.mon.log(self,">stop received") self.quit_signal=True # *************************************** # sequencing # *************************************** """self. play_state controls the playing sequence, it has the following values. I am not entirely sure the starting and ending states are required. - _closed - the mplayer process is not running, mplayer process can be initiated - _starting - mplayer process is running but is not yet able to receive controls - _playing - playing a track, controls can be sent - _ending - mplayer is doing its termination, controls cannot be sent """ def init_play_state_machine(self): self.quit_signal=False self.play_state=BrowserPlayer._CLOSED def start_play_state_machine(self): #initialise all the state machine variables self.duration_count = 0 self.quit_signal=False # signal that user has pressed stop #play the track self.bplayer.play(self.track,self.geometry) self.mon.log (self,'Playing track from show Id: '+ str(self.show_id)) self.play_state=BrowserPlayer._STARTING # and start polling for state changes and count duration self.tick_timer=self.canvas.after(50, self.play_state_machine) def play_state_machine(self): if self.play_state == BrowserPlayer._CLOSED: self.mon.log(self," State machine: " + self.play_state) return elif self.play_state == BrowserPlayer._STARTING: # self.mon.log(self," State machine: " + self.play_state) # if uzbl fifo is available can send comands to uzbl but change to wait state to wait for it to appear on screen if self.bplayer.start_play_signal==True: self.mon.log(self," <fifo available signal received from uzbl") self.bplayer.start_play_signal=False self.play_state=BrowserPlayer._WAITING # get rid of status bar self.bplayer.control('set show_status = 0') # and get ready to wait for browser to appear self.wait_count= 50 # 10 seconds at 200mS steps self.mon.log(self," State machine: uzbl process alive") self.tick_timer=self.canvas.after(200, self.play_state_machine) elif self.play_state == BrowserPlayer._WAITING: if self.wait_count==0: # set state to playing self.play_state = BrowserPlayer._PLAYING # and start executing the browser commands self.play_commands() self.mon.log(self," State machine: uzbl_playing started") self.wait_count -=1 self.tick_timer=self.canvas.after(200, self.play_state_machine) elif self.play_state == BrowserPlayer._PLAYING: self.duration_count+=1 # self.mon.log(self," State machine: " + self.play_state) # service any queued stop signals and test duration count if self.quit_signal==True or (self.duration_limit>0 and self.duration_count>self.duration_limit): self.mon.log(self," Service stop required signal or timeout") # self.quit_signal=False self.stop_bplayer() self.play_state = BrowserPlayer._ENDING # uzbl reports it is terminating so change to ending state if self.bplayer.end_play_signal: self.mon.log(self," <end play signal received") self.play_state = BrowserPlayer._ENDING self.tick_timer=self.canvas.after(50, self.play_state_machine) elif self.play_state == BrowserPlayer._ENDING: # self.mon.log(self," State machine: " + self.play_state) # if spawned process has closed can change to closed state # self.mon.log (self," State machine : is luakit process running? - " + str(self.bplayer.is_running())) if self.bplayer.is_running() ==False: self.mon.log(self," <uzbl process is dead") if self.quit_signal==True: self.quit_signal=False self.play_state = BrowserPlayer._CLOSED self.end('normal','quit required or timeout') else: self.tick_timer=self.canvas.after(50, self.play_state_machine) def stop_bplayer(self): # send signal to stop the track to the state machine self.mon.log(self," >send stop to uzbl driver") if self.play_state==BrowserPlayer._PLAYING: self.bplayer.stop() return True else: self.mon.log(self,"!<stop rejected") return False # ***************** # ending the player # ***************** def end(self,reason,message): # stop the plugin if self.pim<>None: self.pim.stop_plugin() # abort the timers if self.tick_timer<>None: self.canvas.after_cancel(self.tick_timer) self.tick_timer=None if self.command_timer<>None: self.canvas.after_cancel(self.command_timer) self.tick_timer=None # clean up and fifos and sockets left by uzbl os.system('rm -f /tmp/uzbl_*') if reason in ('error','killed'): self.end_callback(reason,message) self=None else: # normal end so do show control and animation # Control concurrent shows at end reason,message=self.show_manager.show_control(self.track_params['show-control-end']) if reason =='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # clear events list for this track if self.track_params['animate-clear']=='yes': self.ppio.clear_events_list(id(self)) # create animation events for ending reason,message=self.ppio.animate(self.animate_end_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: self.end_callback('normal',"track has terminated or quit") self=None # ***************** # displaying things # ***************** def display_content(self): self.canvas.delete('pp-content') #background colour if self.background_colour<>'': self.canvas.config(bg=self.background_colour) if self.background_file<>'': self.background_img_file = self.complete_path(self.background_file) if not os.path.exists(self.background_img_file): self.mon.err(self,"Audio background file not found: "+ self.background_img_file) self.end('error',"Audio background file not found") else: pil_background_img=PIL.Image.open(self.background_img_file) self.background = PIL.ImageTk.PhotoImage(pil_background_img) self.drawn = self.canvas.create_image(int(self.canvas['width'])/2, int(self.canvas['height'])/2, image=self.background, anchor=CENTER, tag='pp-content') # execute the plugin if required if self.track_params['plugin']<>'': reason,message,self.track = self.pim.do_plugin(self.track,self.track_params['plugin'],) if reason <> 'normal': return reason,message # display hint text if enabled if self.enable_menu== True: self.canvas.create_text(int(self.show_params['hint-x']), int(self.show_params['hint-y']), text=self.show_params['hint-text'], fill=self.show_params['hint-colour'], font=self.show_params['hint-font'], anchor=NW, tag='pp-content') # display show text if enabled if self.show_params['show-text']<> ''and self.track_params['display-show-text']=='yes': self.canvas.create_text(int(self.show_params['show-text-x']),int(self.show_params['show-text-y']), anchor=NW, text=self.show_params['show-text'], fill=self.show_params['show-text-colour'], font=self.show_params['show-text-font'], tag='pp-content') # display track text if enabled if self.track_params['track-text']<> '': self.canvas.create_text(int(self.track_params['track-text-x']),int(self.track_params['track-text-y']), anchor=NW, text=self.track_params['track-text'], fill=self.track_params['track-text-colour'], font=self.track_params['track-text-font'], tag='pp-content') self.mon.log(self,"Displayed background and text ") self.canvas.tag_raise('pp-click-area') self.canvas.update_idletasks( ) return 'normal','' # ******************* # browser commands # *********************** def parse_commands(self,command_text): self.command_list=[] lines = command_text.split('\n') for line in lines: if line.strip()=="": continue reason,entry=self.parse_command(line) if reason != 'normal': return 'error',entry self.command_list.append(copy.deepcopy(entry)) # print self.command_list return 'normal','' def parse_command(self,line): fields = line.split() if fields[0]=='uzbl': # print fields[0], line[4:] return 'normal',[fields[0],line[4:]] if len(fields) not in (1,2): return 'error',"incorrect number of fields in command: " + line command=fields[0] arg='' if command not in ('load','refresh','wait','exit','loop'): return 'error','unknown command: '+ command if command in ('refresh','exit','loop') and len(fields)<>1: return 'error','incorrect number of fields for '+ command + 'in: ' + line if command == 'load': if len(fields)<>2: return 'error','incorrect number of fields for '+ command + 'in: ' + line else: arg = fields[1] if command == 'wait': if len(fields)<>2: return 'error','incorrect number of fields for '+ command + 'in: ' + line else: arg = fields[1] if not arg.isdigit():return 'error','Argument for Wait is not 0 or positive number in: ' + line return 'normal',[command,arg] def play_commands(self): if len(self.command_list)==0: return self.loop=0 self.command_index=0 self.canvas.after(100,self.execute_command) def execute_command(self): entry=self.command_list[self.command_index] command=entry[0] arg=entry[1] if self.command_index==len(self.command_list)-1: self.command_index=self.loop else: self.command_index+=1 # execute command if command == 'load': #self.canvas.focus_force() #self.root.lower() file=self.complete_path(arg) self.bplayer.control('uri '+ file) self.command_timer=self.canvas.after(10,self.execute_command) elif command == 'refresh': self.bplayer.control('reload_ign_cache') self.command_timer=self.canvas.after(10,self.execute_command) elif command == 'wait': self.command_timer=self.canvas.after(1000*int(arg),self.execute_command) elif command=='exit': self.quit_signal=True elif command=='loop': self.loop=self.command_index self.command_timer=self.canvas.after(10,self.execute_command) elif command=='uzbl': self.bplayer.control(arg) self.command_timer=self.canvas.after(10,self.execute_command) # ***************** # utilities # ***************** def complete_path(self,track_file): # complete path of the filename of the selected entry if track_file[0]=="+": track_file=self.pp_home+track_file[1:] self.mon.log(self,"Background image is "+ track_file) return track_file def parse_window(self,line): # parses warp _ or xy2 fields = line.split() # check there is a command field if len(fields) < 1: return 'error','no type field','',False,0,0,0,0 #deal with warp which has 1 or 5 arguments # check basic syntax if fields[0] <>'warp': return 'error','not a valid type','',False,0,0,0,0 if len(fields) not in (1,5): return 'error','wrong number of coordinates for warp','',False,0,0,0,0 # deal with window coordinates if len(fields) == 5: #window is specified if not (fields[1].isdigit() and fields[2].isdigit() and fields[3].isdigit() and fields[4].isdigit()): return 'error','coordinates are not positive integers','',False,0,0,0,0 has_window=True return 'normal','',fields[0],has_window,int(fields[1]),int(fields[2]),int(fields[3]),int(fields[4]) else: # fullscreen has_window=False return 'normal','',fields[0],has_window,0,0,0,0
class OSCDriver(object): # executed by main program def init(self,pp_profile,show_command_callback,input_event_callback,output_event_callback): self.pp_profile=pp_profile self.show_command_callback=show_command_callback self.input_event_callback=input_event_callback self.output_event_callback=output_event_callback self.mon=Monitor() config_file=self.pp_profile + os.sep +'pp_io_config'+os.sep+ 'osc.cfg' if not os.path.exists(config_file): self.mon.err(self, 'OSC Configuration file nof found: '+config_file) return'error','OSC Configuration file nof found: '+config_file self.mon.log(self, 'OSC Configuration file found at: '+config_file) self.options=OSCConfig() # only reads the data for required unit_type if self.options.read(config_file) ==False: return 'error','failed to read osc.cfg' self.prefix='/pipresents' self.this_unit='/' + self.options.this_unit_name self.this_unit_type = self.options.this_unit_type self.reply_client=None self.command_client=None self.client=None self.server=None if self.this_unit_type not in ('master','slave','master+slave'): return 'error','this unit type not known: '+self.this_unit_type if self.this_unit_type in('slave','master+slave'): #start the client that sends replies to controlling unit self.reply_client=OSC.OSCClient() self.mon.log(self, 'sending replies to controller at: '+self.options.controlled_by_ip+':'+self.options.controlled_by_port) self.reply_client.connect((self.options.controlled_by_ip,int(self.options.controlled_by_port))) self.mon.log(self,'sending repiles to: '+ str(self.reply_client)) self.client=self.reply_client if self.this_unit_type in ('master','master+slave'): #start the client that sends commands to the controlled unit self.command_client=OSC.OSCClient() self.command_client.connect((self.options.controlled_unit_1_ip,int(self.options.controlled_unit_1_port))) self.mon.log(self, 'sending commands to controled unit at: '+self.options.controlled_unit_1_ip+':'+self.options.controlled_unit_1_port) self.mon.log(self,'sending commands to: '+str(self.command_client)) self.client=self.command_client #start the listener's server self.mon.log(self, 'listen to commands from controlled by unit and replies from controlled units on: ' + self.options.this_unit_ip+':'+self.options.this_unit_port) self.server=myOSCServer((self.options.this_unit_ip,int(self.options.this_unit_port)),self.client) # return_port=int(self.options.controlled_by_port) self.mon.log(self,'listening on: '+str(self.server)) self.add_initial_handlers() return 'normal','osc.cfg read' def terminate(self): if self.server != None: self.server.close() self.mon.log(self, 'Waiting for Server-thread to finish') if self.st != None: self.st.join() ##!!! self.mon.log(self,'server thread closed') self.client.close() def start_server(self): # Start OSCServer self.mon.log(self,'Starting OSCServer') self.st = threading.Thread( target = self.server.serve_forever ) self.st.start() def add_initial_handlers(self): self.server.addMsgHandler('default', self.no_match_handler) self.server.addMsgHandler(self.prefix+self.this_unit+"/system/server-info", self.server_info_handler) self.server.addMsgHandler(self.prefix+self.this_unit+"/system/loopback", self.loopback_handler) self.server.addMsgHandler(self.prefix+ self.this_unit+'/core/open', self.open_show_handler) self.server.addMsgHandler(self.prefix+ self.this_unit+'/core/close', self.close_show_handler) self.server.addMsgHandler(self.prefix+ self.this_unit+'/core/exitpipresents', self.exitpipresents_handler) self.server.addMsgHandler(self.prefix+ self.this_unit+'/core/shutdownnow', self.shutdownnow_handler) self.server.addMsgHandler(self.prefix+ self.this_unit+'/core/event', self.input_event_handler) self.server.addMsgHandler(self.prefix+ self.this_unit+'/core/output', self.output_event_handler) def no_match_handler(self,addr, tags, stuff, source): self.mon.warn(self,"no match for osc msg with addr : %s" % addr) return None def server_info_handler(self,addr, tags, stuff, source): msg = OSC.OSCMessage(self.prefix+'/'+self.options.controlled_by_name+'/system/server-info-reply') msg.append('Unit: '+ self.options.this_unit_name) return msg def loopback_handler(self,addr, tags, stuff, source): # send a reply to the client. msg = OSC.OSCMessage(self.prefix+'/'+self.options.controlled_by_name+'/system/loopback-reply') return msg def open_show_handler(self,address, tags, args, source): self.prepare_show_command_callback('open ',args,1) def close_show_handler(self,address, tags, args, source): self.prepare_show_command_callback('close ', args,1) def exitpipresents_handler(self,address, tags, args, source): self.prepare_show_command_callback('exitpipresents',args,0) def shutdownnow_handler(self,address, tags, args, source): self.prepare_show_command_callback('shutdownnow',args,0) def prepare_show_command_callback(self,command,args,limit): if len(args) == limit: if limit !=0: self.show_command_callback(command+args[0]) else: self.show_command_callback(command) else: self.mon.warn(self,'OSC show command does not have '+limit +' argument - ignoring') def input_event_handler(self,address, tags, args, source): if len(args) == 1: self.input_event_callback(args[0],'OSC') else: self.mon.warn(self,'OSC input event does not have 1 argument - ignoring') def output_event_handler(self,address, tags, args, source): if len(args) !=0: # delay symbol,param_type,param_values,req_time as a string text='0 ' for arg in args: text= text+ arg + ' ' text = text + '0' self.output_event_callback(text) else: self.mon.warn(self,'OSC output event has no arguments - ignoring') #send messages to controlled units # parses the message string into fields and sends - NO error checking def send_command(self,text): self.mon.log(self,'send OSC Command: ' + text ) if self.this_unit_type not in ('master','remote','master+slave'): self.mon.warn(self,'Unit is not an OSC Master, ignoring command') return fields=text.split() address = fields[0] # print 'ADDRESS'+address address_fields=address[1:].split('/') if address_fields[0] != 'pipresents': self.mon.warn(self,'prefix is not pipresents: '+address_fields[0]) if address_fields[1] != self.options.controlled_unit_1_name: self.mon.warn(self,'not sending OSC to the controlled unit: ' +self.options.controlled_unit_1_name + ' is '+ address_fields[1]) arg_list=fields[1:] self.send(address,arg_list) def send(self,address,arg_list): # print self.command_client msg = OSC.OSCMessage() # print address msg.setAddress(address) for arg in arg_list: # print arg msg.append(arg) self.command_client.send(msg)
class ShowManager: # ShowManager manages PiPresents' concurrent shows. It does not manage sub-shows or child-shows. # concurrent shows are always 'top level' shows: # They can be started by the start show or by 'myshow start' in the Show Control field of players # They can be stopped either by 'myshow stop' in the Show Control field in players # or in the case of mediashows by making them single-run in its Repeat field # a show with the same reference should not be run twice as there is no way to reference an individual instance when stopping # ??? this could be changed as there is single-run to stop them, the stop command could stop all instances. # Declare class variables shows=[] shutdown_required=False SHOW_TEMPLATE=['',None] SHOW_REF= 0 # show-reference - name of the show as in editor SHOW_OBJ = 1 # the python object def __init__(self,show_id,showlist,show_params,root,canvas,pp_dir,pp_profile,pp_home): self.showlist=showlist self.show_params=show_params self.root=root self.canvas=canvas self.pp_dir=pp_dir self.pp_profile=pp_profile self.pp_home=pp_home self.show_id=show_id self.mon=Monitor() self.mon.on() # Initialise, first time through only in pipresents.py def init(self,all_shows_ended_callback): ShowManager.all_shows_ended_callback=all_shows_ended_callback ShowManager.shows=[] ShowManager.shutdown_required=False # ************************************** # functions to manipulate show register # ************************************** #adds a new concurrent show to the register if not already there, returns an index for use by start and stop def register_show(self,ref): registered=self.show_registered(ref) if registered==-1: ShowManager.shows.append(copy.deepcopy(ShowManager.SHOW_TEMPLATE)) index=len(ShowManager.shows)-1 ShowManager.shows[index][ShowManager.SHOW_REF]=ref ShowManager.shows[index][ShowManager.SHOW_OBJ]=None return index else: return registered # is the show registered? # can be used to return the index to the show def show_registered(self,show_ref): index=0 for show in ShowManager.shows: if show[ShowManager.SHOW_REF]==show_ref: return index index+=1 return -1 # needs calling program to check that the show is not already running def set_running(self,index,show_obj): ShowManager.shows[index][ShowManager.SHOW_OBJ]=show_obj # print 'running', ShowManager.shows # print "started show ", ShowManager.shows[index][ShowManager.SHOW_REF] # is the show running? def show_running(self,index): if ShowManager.shows[index][ShowManager.SHOW_OBJ]<>None: return ShowManager.shows[index][ShowManager.SHOW_OBJ] else: return None def set_stopped(self,index): ShowManager.shows[index][ShowManager.SHOW_OBJ]=None # print "stopped show ", ShowManager.shows[index][ShowManager.SHOW_REF] #print 'stopping', ShowManager.shows # are all shows stopped? def all_shows_stopped(self): all_stopped=True for show in ShowManager.shows: if show[ShowManager.SHOW_OBJ]<>None: all_stopped=False return all_stopped # ********************************* # show control # ********************************* # control initial shows from PiPresents so command is always start def start_initial_shows(self,start_shows_text): show_refs= start_shows_text.split() fields=['',''] for show_ref in show_refs: fields[0]=show_ref fields[1]='start' reason,message=self.control_a_show(fields) if reason<>'normal': return reason,message #no shows started return 'normal','' # Control shows from Players so need to handle start and stop commands def show_control(self,show_control_text): lines = show_control_text.split('\n') for line in lines: if line.strip()=="": continue fields= line.split() #control a show and return its ending reason # print 'show control fields: ',fields reason,message=self.control_a_show(fields) if reason<>'normal': return reason,message #all commands done OK return 'normal','' def control_a_show(self,fields): show_ref=fields[0] show_command=fields[1] if show_command=='start': return self.start_show(show_ref) elif show_command =='stop': return self.stop_show(show_ref) elif show_command=='exit': return self.stop_all_shows() elif show_command=='shutdownnow': ShowManager.shutdown_required=True return self.stop_all_shows() else: return 'error','command not recognised '+ show_command def stop_show(self,show_ref): index=self.show_registered(show_ref) self.mon.log(self,self.show_params['show-ref']+ ' '+ str(self.show_id)+ ": Stopping show "+ show_ref + ' ' + str(index)) show_obj=self.show_running(index) if show_obj<>None: show_obj.managed_stop() return 'normal','stopped a concurrent show' def stop_all_shows(self): for show in ShowManager.shows: self.stop_show(show[ShowManager.SHOW_REF]) return 'normal','stopped all shows' def start_show(self,show_ref): show_index = self.showlist.index_of_show(show_ref) if show_index <0: return 'error',"Show not found in showlist: "+ show_ref show=self.showlist.show(show_index) index=self.register_show(show_ref) self.mon.log(self,self.show_params['show-ref']+ ' '+ str(self.show_id)+ ": Starting show "+ show_ref + ' ' + str(index)) if self.show_running(index): self.mon.log(self,"show already running "+show_ref) return 'normal','this concurrent show already running' if show['type']=="mediashow": show_obj = MediaShow(show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.set_running(index,show_obj) show_obj.play(index,self._end_play_show,None,top=True,command='nil') return 'normal','concurrent show started' if show['type']=="radiobuttonshow": show_obj = RadioButtonShow(show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.set_running(index,show_obj) show_obj.play(index,self._end_play_show,None,top=True,command='nil') return 'normal','concurrent show started' if show['type']=="hyperlinkshow": show_obj = HyperlinkShow(show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.set_running(index,show_obj) show_obj.play(index,self._end_play_show,None,top=True,command='nil') return 'normal','concurrent show started' elif show['type']=="menu": show_obj = MenuShow(show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.set_running(index,show_obj) show_obj.play(index,self._end_play_show,None,top=True,command='nil') return 'normal','concurrent show started' elif show['type']=="liveshow": show_obj= LiveShow(show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.set_running(index,show_obj) show_obj.play(index,self._end_play_show,None,top=True,command='nil') return 'normal','concurrent show started' else: return 'error',"unknown show type in start concurrent show - "+ show['type'] def _end_play_show(self,index,reason,message): self.mon.log(self,self.show_params['show-ref']+ ' '+ str(self.show_id)+ ": Returned from show with message: "+ message) # print 'returned to showplayer' self.set_stopped(index) if self.all_shows_stopped()==True: ShowManager.all_shows_ended_callback('normal','All shows ended',ShowManager.shutdown_required) return reason,message
class VideoPlayer: """ plays a track using omxplayer See pp_imageplayer for common software design description """ _CLOSED = "omx_closed" #probably will not exist _STARTING = "omx_starting" #track is being prepared _PLAYING = "omx_playing" #track is playing to the screen, may be paused _ENDING = "omx_ending" #track is in the process of ending due to quit or end of track # *************************************** # EXTERNAL COMMANDS # *************************************** def __init__(self, show_id, root, canvas, show_params, track_params , pp_dir, pp_home, pp_profile): self.mon=Monitor() self.mon.on() #instantiate arguments self.show_id=show_id self.root=root self.canvas = canvas self.show_params=show_params self.track_params=track_params self.pp_dir=pp_dir self.pp_home=pp_home self.pp_profile=pp_profile # get config from medialist if there. if self.track_params['omx-audio']<>"": self.omx_audio= self.track_params['omx-audio'] else: self.omx_audio= self.show_params['omx-audio'] if self.omx_audio<>"": self.omx_audio= "-o "+ self.omx_audio if self.track_params['omx-volume']<>"": self.omx_volume= self.track_params['omx-volume'] else: self.omx_volume= self.show_params['omx-volume'] if self.omx_volume<>"": self.omx_volume= "--vol "+ str(int(self.omx_volume)*100) + ' ' if self.track_params['omx-window']<>'': self.omx_window= self.track_params['omx-window'] else: self.omx_window= self.show_params['omx-window'] # get background image from profile. self.background_file='' if self.track_params['background-image']<>"": self.background_file= self.track_params['background-image'] else: if self.track_params['display-show-background']=='yes': self.background_file= self.show_params['background-image'] # get background colour from profile. if self.track_params['background-colour']<>"": self.background_colour= self.track_params['background-colour'] else: self.background_colour= self.show_params['background-colour'] self.centre_x = int(self.canvas['width'])/2 self.centre_y = int(self.canvas['height'])/2 #get animation instructions from profile self.animate_begin_text=self.track_params['animate-begin'] self.animate_end_text=self.track_params['animate-end'] # open the plugin Manager self.pim=PluginManager(self.show_id,self.root,self.canvas,self.show_params,self.track_params,self.pp_dir,self.pp_home,self.pp_profile) #create an instance of PPIO so we can create gpio events self.ppio = PPIO() # could put instance generation in play, not sure which is better. self.omx=OMXDriver(self.canvas) self.tick_timer=None self.init_play_state_machine() def play(self, track, showlist, end_callback, ready_callback, enable_menu=False): #instantiate arguments self.track=track self.showlist=showlist self.ready_callback=ready_callback #callback when ready to play self.end_callback=end_callback # callback when finished self.enable_menu = enable_menu # callback to the calling object to e.g remove egg timer and enable click areas. if self.ready_callback<>None: self.ready_callback() # create an instance of showmanager so we can control concurrent shows self.show_manager=ShowManager(self.show_id,self.showlist,self.show_params,self.root,self.canvas,self.pp_dir,self.pp_profile,self.pp_home) #set up video window reason,message,comand,has_window,x1,y1,x2,y2= self.parse_window(self.omx_window) if reason =='error': self.mon.err(self,'omx window error: ' + message + ' in ' + self.omx_window) self.end_callback(reason,message) else: if has_window==True: self.omx_window= '--win " '+ str(x1) + ' ' + str(y1) + ' ' + str(x2) + ' ' + str(y2) + ' " ' else: self.omx_window='' # Control other shows at beginning reason,message=self.show_manager.show_control(self.track_params['show-control-begin']) if reason in ('error','killed'): self.end_callback(reason,message) self=None else: #display content reason,message=self.display_content() if reason == 'error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # create animation events reason,message=self.ppio.animate(self.animate_begin_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # start playing the video. if self.play_state == VideoPlayer._CLOSED: self.mon.log(self,">play track received") self.start_play_state_machine(self.track) else: self.mon.err(self,'play track rejected') self.end_callback('error','play track rejected') self=None def terminate(self,reason): # circumvents state machine and does not wait for omxplayer to close if self.omx<>None: self.mon.log(self,"sent terminate to omxdriver") self.omx.terminate(reason) self.end('killed',' end without waiting for omxplayer to finish') # end without waiting else: self.mon.log(self,"terminate, omxdriver not running") self.end('killed','terminate, mplayerdriver not running') def input_pressed(self,symbol): if symbol[0:4]=='omx-': self.control(symbol[4]) elif symbol =='pause': self.pause() elif symbol=='stop': self.stop() else: pass def get_links(self): return self.track_params['links'] # *************************************** # INTERNAL FUNCTIONS # *************************************** # respond to normal stop def stop(self): # send signal to stop the track to the state machine self.mon.log(self,">stop received") self.quit_signal=True #toggle pause def pause(self): if self.play_state in (VideoPlayer._PLAYING,VideoPlayer._ENDING): self.omx.pause() return True #NIK # for pausing video after first frame elif self.play_state == VideoPlayer._STARTING: self.omx.delayed_pause = True return True #NIK else: self.mon.log(self,"!<pause rejected") return False # other control when playing def control(self,char): if self.play_state==VideoPlayer._PLAYING and char not in ('q'): self.mon.log(self,"> send control to omx: "+ char) self.omx.control(char) return True else: self.mon.log(self,"!<control rejected") return False # *********************** # sequencing # ********************** """self. play_state controls the playing sequence, it has the following values. I am not entirely sure the starting and ending states are required. - _closed - the omx process is not running, omx process can be initiated - _starting - omx process is running but is not yet able to receive controls - _playing - playing a track, controls can be sent - _ending - omx is doing its termination, controls cannot be sent """ def init_play_state_machine(self): self.quit_signal=False self.play_state=VideoPlayer._CLOSED def start_play_state_machine(self,track): #initialise all the state machine variables #self.iteration = 0 # for debugging self.quit_signal=False # signal that user has pressed stop self.play_state=VideoPlayer._STARTING #play the selected track options=self.omx_audio+ " " + self.omx_volume + ' ' + self.omx_window + ' ' + self.show_params['omx-other-options']+" " # NIK ADDITION # adding subtitles file for video if 'omx-subtitles' in self.track_params and self.track_params['omx-subtitles'] <> '': subtitles_full_path = self.complete_path(self.track_params['omx-subtitles']) if os.path.exists (subtitles_full_path): options += '--font-size 40 --subtitles "' + subtitles_full_path + '" ' if 'omx-subtitles-numlines' in self.track_params and self.track_params['omx-subtitles-numlines'] <> '': options += '--lines ' + self.track_params['omx-subtitles-numlines'] + ' ' # END NIK ADDITION self.omx.play(track,options) self.mon.log (self,'Playing track from show Id: '+ str(self.show_id)) # and start polling for state changes self.tick_timer=self.canvas.after(50, self.play_state_machine) def play_state_machine(self): if self.play_state == VideoPlayer._CLOSED: self.mon.log(self," State machine: " + self.play_state) return elif self.play_state == VideoPlayer._STARTING: self.mon.log(self," State machine: " + self.play_state) # if omxplayer is playing the track change to play state if self.omx.start_play_signal==True: self.mon.log(self," <start play signal received from omx") self.omx.start_play_signal=False self.play_state=VideoPlayer._PLAYING self.mon.log(self," State machine: omx_playing started") self.tick_timer=self.canvas.after(50, self.play_state_machine) elif self.play_state == VideoPlayer._PLAYING: # self.mon.log(self," State machine: " + self.play_state) # service any queued stop signals if self.quit_signal==True: self.mon.log(self," Service stop required signal") self.stop_omx() self.quit_signal=False # self.play_state = VideoPlayer._ENDING # omxplayer reports it is terminating so change to ending state if self.omx.end_play_signal: self.mon.log(self," <end play signal received") self.mon.log(self," <end detected at: " + str(self.omx.video_position)) if self.omx.end_play_reason<>'nice_day': # deal with omxplayer not sending 'have a nice day' self.mon.warn(self," <end detected at: " + str(self.omx.video_position)) self.mon.warn(self," <pexpect reports: "+self.omx.end_play_reason) self.mon.warn(self,'pexpect.before is'+self.omx.xbefore) self.play_state = VideoPlayer._ENDING self.ending_count=0 self.tick_timer=self.canvas.after(200, self.play_state_machine) elif self.play_state == VideoPlayer._ENDING: self.mon.log(self," State machine: " + self.play_state) # if spawned process has closed can change to closed state self.mon.log (self," State machine : is omx process running? - " + str(self.omx.is_running())) if self.omx.is_running() ==False: self.mon.log(self," <omx process is dead") self.play_state = VideoPlayer._CLOSED self.end('normal','quit by user or system') else: self.ending_count+=1 if self.ending_count>10: # deal with omxplayer not terminating at the end of a track self.mon.warn(self," <omxplayer failed to close at: " + str(self.omx.video_position)) self.mon.warn(self,'pexpect.before is'+self.omx.xbefore) self.omx.kill() self.mon.warn(self,'omxplayer now terminated ') self.play_state = VideoPlayer._CLOSED self.end('normal','end from omxplayer failed to terminate') else: self.tick_timer=self.canvas.after(200, self.play_state_machine) def stop_omx(self): # send signal to stop the track to the state machine self.mon.log(self," >stop omx received from state machine") if self.play_state==VideoPlayer._PLAYING: self.omx.stop() return True else: self.mon.log(self,"!<stop rejected") return False # ***************** # ending the player # ***************** def end(self,reason,message): # stop the plugin if self.track_params['plugin']<>'': self.pim.stop_plugin() # os.system("xrefresh -display :0") # abort the timer if self.tick_timer<>None: self.canvas.after_cancel(self.tick_timer) self.tick_timer=None if reason in ('error','killed'): self.end_callback(reason,message) self=None else: # normal end so do show control and animation # Control concurrent shows at end reason,message=self.show_manager.show_control(self.track_params['show-control-end']) if reason =='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: # clear events list for this track if self.track_params['animate-clear']=='yes': self.ppio.clear_events_list(id(self)) # create animation events for ending reason,message=self.ppio.animate(self.animate_end_text,id(self)) if reason=='error': self.mon.err(self,message) self.end_callback(reason,message) self=None else: self.end_callback('normal',"track has terminated or quit") self=None # ***************** # displaying things # ***************** def display_content(self): #background colour if self.background_colour<>'': self.canvas.config(bg=self.background_colour) # delete previous content self.canvas.delete('pp-content') # background image if self.background_file<>'': self.background_img_file = self.complete_path(self.background_file) if not os.path.exists(self.background_img_file): self.mon.err(self,"Video background file not found: "+ self.background_img_file) self.end('error',"Video background file not found") else: pil_background_img=PIL.Image.open(self.background_img_file) self.background = PIL.ImageTk.PhotoImage(pil_background_img) self.drawn = self.canvas.create_image(int(self.canvas['width'])/2, int(self.canvas['height'])/2, image=self.background, anchor=CENTER, tag='pp-content') # execute the plugin if required if self.track_params['plugin']<>'': reason,message,self.track = self.pim.do_plugin(self.track,self.track_params['plugin'],) if reason <> 'normal': return reason,message # display show text if enabled if self.show_params['show-text']<> '' and self.track_params['display-show-text']=='yes': self.canvas.create_text(int(self.show_params['show-text-x']),int(self.show_params['show-text-y']), anchor=NW, text=self.show_params['show-text'], fill=self.show_params['show-text-colour'], font=self.show_params['show-text-font'], tag='pp-content') # display track text if enabled if self.track_params['track-text']<> '': self.canvas.create_text(int(self.track_params['track-text-x']),int(self.track_params['track-text-y']), anchor=NW, text=self.track_params['track-text'], fill=self.track_params['track-text-colour'], font=self.track_params['track-text-font'], tag='pp-content') # display instructions if enabled if self.enable_menu== True: self.canvas.create_text(int(self.show_params['hint-x']), int(self.show_params['hint-y']), text=self.show_params['hint-text'], fill=self.show_params['hint-colour'], font=self.show_params['hint-font'], anchor=NW, tag='pp-content') self.canvas.tag_raise('pp-click-area') self.canvas.update_idletasks( ) return 'normal','' # **************** # utilities # ***************** def complete_path(self,track_file): # complete path of the filename of the selected entry if track_file[0]=="+": track_file=self.pp_home+track_file[1:] self.mon.log(self,"Background image is "+ track_file) return track_file # original _ # warp _ or xy2 def parse_window(self,line): fields = line.split() # check there is a command field if len(fields) < 1: return 'error','no type field','',False,0,0,0,0 # deal with original which has 1 if fields[0]=='original': if len(fields) <> 1: return 'error','number of fields for original','',False,0,0,0,0 return 'normal','',fields[0],False,0,0,0,0 #deal with warp which has 1 or 5 arguments # check basic syntax if fields[0] <>'warp': return 'error','not a valid type','',False,0,0,0,0 if len(fields) not in (1,5): return 'error','wrong number of coordinates for warp','',False,0,0,0,0 # deal with window coordinates if len(fields) == 5: #window is specified if not (fields[1].isdigit() and fields[2].isdigit() and fields[3].isdigit() and fields[4].isdigit()): return 'error','coordinates are not positive integers','',False,0,0,0,0 has_window=True return 'normal','',fields[0],has_window,int(fields[1]),int(fields[2]),int(fields[3]),int(fields[4]) else: # fullscreen has_window=True return 'normal','',fields[0],has_window,0,0,self.canvas['width'],self.canvas['height']
class MediaList: """ manages a media list of tracks and the track selected from the medialist """ def __init__(self): self.clear() self.mon = Monitor() self.mon.on() # Functions for the editor dealing with complete list def clear(self): self._tracks = [] #MediaList, stored as a list of dicts self._num_tracks = 0 self._selected_track_index = -1 # index of currently selected track def print_list(self): print self._tracks def first(self): self.select(0) def length(self): return self._num_tracks def append(self, track_dict): """appends a track dictionary to the end of the medialist store""" self._tracks.append(copy.deepcopy(track_dict)) self._num_tracks += 1 def update(self, index, values): self._tracks[index].update(values) def remove(self, index): self._tracks.pop(index) self._num_tracks -= 1 # deselect any track, saves worrying about whether index needs changing self._selected_track_index = -1 def move_up(self): if self._selected_track_index <> 0: self._tracks.insert(self._selected_track_index - 1, self._tracks.pop(self._selected_track_index)) self.select(self._selected_track_index - 1) def move_down(self): if self._selected_track_index <> self._num_tracks - 1: self._tracks.insert(self._selected_track_index + 1, self._tracks.pop(self._selected_track_index)) self.select(self._selected_track_index + 1) def replace(self, index, replacement): self._tracks[index] = replacement # Common functions work for anything def track_is_selected(self): if self._selected_track_index >= 0: return True else: return False def selected_track_index(self): return self._selected_track_index def track(self, index): return self._tracks[index] def selected_track(self): """returns a dictionary containing all fields in the selected track """ return self._selected_track def select(self, index): """does housekeeping necessary when a track is selected""" if self._num_tracks > 0 and index >= 0 and index < self._num_tracks: self._selected_track_index = index self._selected_track = self._tracks[index] return True else: return False # Dealing with anonymous tracks for use and display def at_end(self): # true is selected track is last anon index = self._num_tracks - 1 while index >= 0: if self._tracks[index]['track-ref'] == "": end = index if self._selected_track_index == end: return True else: return False index -= 1 return False def index_of_end(self): index = self._num_tracks - 1 while index >= 0: if self._tracks[index]['track-ref'] == "": return index index -= 1 return -1 def at_start(self): index = 0 while index < self._num_tracks: if self._tracks[index]['track-ref'] == "": start = index if self._selected_track_index == start: return True else: return False index += 1 return False def index_of_start(self): index = 0 while index < self._num_tracks: if self._tracks[index]['track-ref'] == "": return index index += 1 return False def display_length(self): count = 0 index = 0 while index < self._num_tracks: if self._tracks[index]['track-ref'] == "": count += 1 index += 1 return count def start(self): # select first anymous track in the list index = 0 while index < self._num_tracks: if self._tracks[index]['track-ref'] == "": self.select(index) return True index += 1 return False def finish(self): # select first anymous track in the list index = self._num_tracks - 1 while index >= 0: if self._tracks[index]['track-ref'] == "": self.select(index) return True index -= 1 return False def next(self): if self._selected_track_index == self._num_tracks - 1: index = 0 else: index = self._selected_track_index + 1 end = self._selected_track_index while index <> end: if self._tracks[index]['track-ref'] == "": self.select(index) return True if index == self._num_tracks - 1: index = 0 else: index = index + 1 return False def previous(self): if self._selected_track_index == 0: index = self._num_tracks - 1 else: index = self._selected_track_index - 1 end = self._selected_track_index while index <> end: if self._tracks[index]['track-ref'] == "": self.select(index) return True if index == 0: index = self._num_tracks - 1 else: index = index - 1 return False # Lookup for labelled tracks def index_of_track(self, wanted_track): index = 0 for track in self._tracks: if track['track-ref'] == wanted_track: return index index += 1 return -1 # open and save def open_list(self, filename, showlist_issue): """ opens a saved medialist medialists are stored as json arrays. """ ifile = open(filename, 'rb') mdict = json.load(ifile) ifile.close() self._tracks = mdict['tracks'] if 'issue' in mdict: self.issue = mdict['issue'] else: self.issue = "1.0" if self.issue == showlist_issue: self._num_tracks = len(self._tracks) self._selected_track_index = -1 return True else: return False def issue(self): return self.issue def save_list(self, filename): """ save a medialist """ if filename == "": return False dic = {'issue': self.issue, 'tracks': self._tracks} filename = str(filename) filename = string.replace(filename, '\\', '/') tries = 1 while tries <= 10: # print "save medialist ",filename try: ofile = open(filename, "wb") json.dump(dic, ofile, sort_keys=True, indent=1) ofile.close() self.mon.log(self, "Saved medialist " + filename) break except IOError: self.mon.err( self, "failed to save medialist, trying again " + str(tries)) tries += 1 return # for the future def open_csv(self, filename): """ opens a saved csv medialist """ if filename != "" and os.path.exists(filename): ifile = open(filename, 'rb') pl = csv.reader(ifile) for pl_row in pl: if len(pl_row) != 0: entry = dict([('type', pl_row[2]), ('location', pl_row[0]), ('title', pl_row[1])]) self.append(copy.deepcopy(entry)) ifile.close() return True else: return False
class OSCMonitor(object): def __init__(self): self.editor_issue="1.3" # get command options self.command_options=remote_options() # get directory holding the code self.pp_dir=sys.path[0] if not os.path.exists(self.pp_dir+os.sep+"pipresents.py"): tkMessageBox.showwarning("Pi Presents","Bad Application Directory") exit() # Initialise logging Monitor.log_path=self.pp_dir self.mon=Monitor() self.mon.init() Monitor.classes = ['OSCMonitor','OSCConfig','OSCEditor'] Monitor.log_level = int(self.command_options['debug']) self.mon.log (self, "Pi Presents Monitor is starting") self.mon.log (self," OS and separator " + os.name +' ' + os.sep) self.mon.log(self,"sys.path[0] - location of code: code "+sys.path[0]) self.setup_gui() # initialise OSC config class self.osc_config=OSCConfig() self.init() #and start the system self.root.after(1000,self.run_app) self.root.mainloop() def init(self): # read the options and allow their editing self.osc_config_file = self.pp_dir + os.sep + 'pp_config' + os.sep + 'pp_oscmonitor.cfg' self.read_create_osc() def add_status(self,text): self.status_display.insert(END,text+'\n') self.status_display.see(END) def run_app(self): self.client=None self.server=None self.st=None # initialise OSC variables self.prefix='/pipresents' self.this_unit='/' + self.osc_config.this_unit_name self.add_status('this unit is: '+self.this_unit) self.controlled_by_unit='/'+self.osc_config.controlled_by_name self.add_status('controlled by unit : '+self.controlled_by_unit) #connect client for replies then start server to listen for commands self.client = OSC.OSCClient() self.add_status('connecting to controlled by unit: '+self.osc_config.controlled_by_ip+':'+self.osc_config.controlled_by_port +' '+self.osc_config.controlled_by_name) self.client.connect((self.osc_config.controlled_by_ip,int(self.osc_config.controlled_by_port))) self.add_status('listening for commands on:'+self.osc_config.this_unit_ip+':'+self.osc_config.this_unit_port) self.init_server(self.osc_config.this_unit_ip,self.osc_config.this_unit_port,self.client) self.add_initial_handlers() self.start_server() # *************************************** # OSC CLIENT TO SEND REPLIES # *************************************** def disconnect_client(self): if self.client != None: self.client.close() return # *************************************** # OSC SERVER TO LISTEN TO COMMANDS # *************************************** def init_server(self,ip,port_text,client): self.add_status('Init Server: '+ip+':'+port_text) self.server = myOSCServer((ip,int(port_text)),client) def start_server(self): self.add_status('Start Server') self.st = threading.Thread( target = self.server.serve_forever ) self.st.start() def close_server(self): if self.server != None: self.server.close() self.mon.log(self, 'Waiting for Server-thread to finish') if self.st != None: self.st.join() ##!!! self.mon.log(self,'server thread closed') def add_initial_handlers(self): pass self.server.addMsgHandler('default', self.no_match_handler) self.server.addMsgHandler(self.prefix+self.this_unit+"/system/server-info", self.server_info_handler) self.server.addMsgHandler(self.prefix+self.this_unit+"/system/loopback", self.loopback_handler) def no_match_handler(self,addr, tags, stuff, source): text= "Message from %s" % OSC.getUrlStr(source)+'\n' text+= " %s" % addr+ self.pretty_list(stuff) self.add_status(text+'\n') def server_info_handler(self,addr, tags, stuff, source): msg = OSC.OSCMessage(self.prefix+self.controlled_by_unit+'/system/server-info-reply') msg.append('Unit: '+ self.osc_config.this_unit_name) self.add_status('Server Info Request from %s:' % OSC.getUrlStr(source)) return msg def loopback_handler(self,addr, tags, stuff, source): # send a reply to the client. msg = OSC.OSCMessage(self.prefix+self.controlled_by_unit+'/system/loopback-reply') self.add_status('Loopback Request from %s:' % OSC.getUrlStr(source)) return msg def pretty_list(self,fields): text=' ' for field in fields: text += str(field) + ' ' return text # *************************************** # INIT EXIT MISC # *************************************** def e_edit_osc(self): self.disconnect_client() self.close_server() self.edit_osc() self.init() self.add_status('\n\n\nRESTART') self.run_app() def app_exit(self): self.disconnect_client() self.close_server() if self.root is not None: self.root.destroy() self.mon.finish() sys.exit() def show_help (self): tkMessageBox.showinfo("Help","Read 'manual.pdf'") def about (self): tkMessageBox.showinfo("About","Simple Remote Monitor for Pi Presents\n" +"Author: Ken Thompson" +"\nWebsite: http://pipresents.wordpress.com/") def setup_gui(self): # set up the gui # root is the Tkinter root widget self.root = Tk() self.root.title("Remote Monitor for Pi Presents") # self.root.configure(background='grey') self.root.resizable(False,False) # define response to main window closing self.root.protocol ("WM_DELETE_WINDOW", self.app_exit) # bind some display fields self.filename = StringVar() self.display_show = StringVar() self.results = StringVar() self.status = StringVar() # define menu menubar = Menu(self.root) toolsmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") menubar.add_cascade(label='Tools', menu = toolsmenu) osc_configmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") menubar.add_cascade(label='Options', menu = osc_configmenu) osc_configmenu.add_command(label='Edit', command = self.e_edit_osc) helpmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") menubar.add_cascade(label='Help', menu = helpmenu) helpmenu.add_command(label='Help', command = self.show_help) helpmenu.add_command(label='About', command = self.about) self.root.config(menu=menubar) # status_frame status_frame=Frame(self.root,padx=5,pady=5) status_frame.pack(side=TOP, fill=BOTH, expand=1) status_label = Label(status_frame, text="Status",font="arial 12 bold") status_label.pack(side=LEFT) scrollbar = Scrollbar(status_frame, orient=VERTICAL) self.status_display=Text(status_frame,height=10, yscrollcommand=scrollbar.set) scrollbar.config(command=self.status_display.yview) scrollbar.pack(side=RIGHT, fill=Y) self.status_display.pack(side=LEFT,fill=BOTH, expand=1) # *************************************** # OSC CONFIGURATION # *************************************** def read_create_osc(self): if self.osc_config.read(self.osc_config_file) is False: self.osc_config.create(self.osc_config_file) eosc = OSCEditor(self.root, self.osc_config_file,'slave','Create OSC Monitor Configuration') self.osc_config.read(self.osc_config_file) def edit_osc(self): if self.osc_config.read(self.osc_config_file) is False: self.osc_config.create(self.osc_config_file) eosc = OSCEditor(self.root, self.osc_config_file,'slave','Edit OSC Monitor Configuration')
class Player(object): # common bits of __init__(...) def __init__( self, show_id, showlist, root, canvas, show_params, track_params, pp_dir, pp_home, pp_profile, end_callback, command_callback, ): # create debugging log object self.mon = Monitor() self.mon.trace(self, "") # instantiate arguments self.show_id = show_id self.showlist = showlist self.root = root self.canvas = canvas["canvas-obj"] self.show_canvas_x1 = canvas["show-canvas-x1"] self.show_canvas_y1 = canvas["show-canvas-y1"] self.show_canvas_x2 = canvas["show-canvas-x2"] self.show_canvas_y2 = canvas["show-canvas-y2"] self.show_canvas_width = canvas["show-canvas-width"] self.show_canvas_height = canvas["show-canvas-height"] self.show_canvas_centre_x = canvas["show-canvas-centre-x"] self.show_canvas_centre_y = canvas["show-canvas-centre-y"] self.show_params = show_params self.track_params = track_params self.pp_dir = pp_dir self.pp_home = pp_home self.pp_profile = pp_profile self.end_callback = end_callback self.command_callback = command_callback # get background image from profile. self.background_file = "" if self.track_params["background-image"] != "": self.background_file = self.track_params["background-image"] # get background colour from profile. if self.track_params["background-colour"] != "": self.background_colour = self.track_params["background-colour"] else: self.background_colour = self.show_params["background-colour"] # get animation instructions from profile self.animate_begin_text = self.track_params["animate-begin"] self.animate_end_text = self.track_params["animate-end"] # create an instance of showmanager so we can control concurrent shows # self.show_manager=ShowManager(self.show_id,self.showlist,self.show_params,self.root,self.canvas,self.pp_dir,self.pp_profile,self.pp_home) # open the plugin Manager self.pim = PluginManager( self.show_id, self.root, self.canvas, self.show_params, self.track_params, self.pp_dir, self.pp_home, self.pp_profile, ) # create an instance of Animate so we can send animation commands self.animate = Animate() # initialise state and signals self.background_obj = None self.show_text_obj = None self.track_text_obj = None self.hint_obj = None self.background = None self.freeze_at_end_required = "no" # overriden by videoplayer self.tick_timer = None self.terminate_signal = False self.play_state = "" def pre_load(self): # Control other shows at beginning self.show_control(self.track_params["show-control-begin"]) pass # common bits of show(....) def pre_show(self): self.mon.trace(self, "") # show_x_content moved to just before ready_callback to improve flicker. self.show_x_content() # and whatecer the plugin has created self.pim.show_plugin() # ready callback hides and closes players from previous track, also displays show background if self.ready_callback is not None: self.ready_callback(self.enable_show_background) # create animation events reason, message = self.animate.animate(self.animate_begin_text, id(self)) if reason == "error": self.mon.err(self, message) self.play_state = "show-failed" if self.finished_callback is not None: self.finished_callback("error", message) else: # return to start playing the track. self.mon.log(self, ">show track received from show Id: " + str(self.show_id)) return # to keep landscape happy def ready_callback(self, enable_show_background): self.mon.fatal(self, "ready callback not overridden") self.end("error", "ready callback not overridden") def finished_callback(self, reason, message): self.mon.fatal(self, "finished callback not overridden") self.end("error", "finished callback not overridden") def closed_callback(self, reason, message): self.mon.fatal(self, "closed callback not overridden") self.end("error", "closed callback not overridden") # Control shows so pass the show control commands back to PiPresents via the command callback def show_control(self, show_control_text): lines = show_control_text.split("\n") for line in lines: if line.strip() == "": continue # print 'show control command: ',line self.command_callback(line, self.show_params["show-ref"]) # ***************** # hide content and end animation, show control etc. # called by ready calback and end # ***************** def hide(self): self.mon.trace(self, "") # abort the timer if self.tick_timer is not None: self.canvas.after_cancel(self.tick_timer) self.tick_timer = None self.hide_x_content() # stop the plugin if self.track_params["plugin"] != "": self.pim.stop_plugin() # Control concurrent shows at end self.show_control(self.track_params["show-control-end"]) # clear events list for this track if self.track_params["animate-clear"] == "yes": self.animate.clear_events_list(id(self)) # create animation events for ending reason, message = self.animate.animate(self.animate_end_text, id(self)) if reason == "error": self.play_state = "show-failed" if self.finished_callback is not None: self.finished_callback("error", message) else: return def terminate(self): self.mon.trace(self, "") self.terminate_signal = True if self.play_state == "showing": # call the derived class's stop method self.stop() else: self.end("killed", "terminate with no track or show open") # must be overriden by derived class def stop(self): self.mon.fatal(self, "stop not overidden by derived class") self.play_state = "show-failed" if self.finished_callback is not None: self.finished_callback("error", "stop not overidden by derived class") def get_play_state(self): return self.play_state # ***************** # ending the player # ***************** def end(self, reason, message): self.mon.trace(self, "") # stop the plugin if self.terminate_signal is True: reason = "killed" self.terminate_signal = False self.hide() self.end_callback(reason, message) self = None # ***************** # displaying common things # ***************** def load_plugin(self): # load the plugin if required if self.track_params["plugin"] != "": reason, message, self.track = self.pim.load_plugin(self.track, self.track_params["plugin"]) return reason, message def draw_plugin(self): # load the plugin if required if self.track_params["plugin"] != "": self.pim.draw_plugin() return def load_x_content(self, enable_menu): self.mon.trace(self, "") self.background_obj = None self.background = None self.track_text_obj = None self.show_text_obj = None self.hint_obj = None self.track_obj = None # background image if self.background_file != "": background_img_file = self.complete_path(self.background_file) if not os.path.exists(background_img_file): return "error", "Track background file not found " + background_img_file else: pil_background_img = Image.open(background_img_file) # print 'pil_background_img ',pil_background_img image_width, image_height = pil_background_img.size window_width = self.show_canvas_width window_height = self.show_canvas_height if image_width != window_width or image_height != window_height: pil_background_img = pil_background_img.resize((window_width, window_height)) self.background = ImageTk.PhotoImage(pil_background_img) del pil_background_img self.background_obj = self.canvas.create_image( self.show_canvas_x1, self.show_canvas_y1, image=self.background, anchor=NW ) # print '\nloaded background_obj: ',self.background_obj # load the track content. Dummy function below is overridden in players status, message = self.load_track_content() if status == "error": return "error", message # load show text if enabled if self.show_params["show-text"] != "" and self.track_params["display-show-text"] == "yes": self.show_text_obj = self.canvas.create_text( int(self.show_params["show-text-x"]) + self.show_canvas_x1, int(self.show_params["show-text-y"]) + self.show_canvas_y1, anchor=NW, text=self.show_params["show-text"], fill=self.show_params["show-text-colour"], font=self.show_params["show-text-font"], ) # load track text if enabled if self.track_params["track-text"] != "": self.track_text_obj = self.canvas.create_text( int(self.track_params["track-text-x"]) + self.show_canvas_x1, int(self.track_params["track-text-y"]) + self.show_canvas_y1, anchor=NW, text=self.track_params["track-text"], fill=self.track_params["track-text-colour"], font=self.track_params["track-text-font"], ) # load instructions if enabled if enable_menu is True: self.hint_obj = self.canvas.create_text( int(self.show_params["hint-x"]) + self.show_canvas_x1, int(self.show_params["hint-y"]) + self.show_canvas_y1, text=self.show_params["hint-text"], fill=self.show_params["hint-colour"], font=self.show_params["hint-font"], anchor=NW, ) self.display_show_canvas_rectangle() self.pim.draw_plugin() self.canvas.tag_raise("pp-click-area") self.canvas.itemconfig(self.background_obj, state="hidden") self.canvas.itemconfig(self.show_text_obj, state="hidden") self.canvas.itemconfig(self.track_text_obj, state="hidden") self.canvas.itemconfig(self.hint_obj, state="hidden") self.canvas.update_idletasks() return "normal", "x-content loaded" # display the rectangle that is the show canvas def display_show_canvas_rectangle(self): # coords=[self.show_canvas_x1,self.show_canvas_y1,self.show_canvas_x2-1,self.show_canvas_y2-1] # self.canvas.create_rectangle(coords, # outline='yellow', # fill='') pass # dummy functions to manipulate the track content, overidden in some players, # message text in messageplayer # image in imageplayer # menu stuff in menuplayer def load_track_content(self): return "normal", "player has no track content to load" def show_track_content(self): pass def hide_track_content(self): pass def show_x_content(self): self.mon.trace(self, "") # background colour if self.background_colour != "": self.canvas.config(bg=self.background_colour) # print 'showing background_obj: ', self.background_obj # reveal background image and text self.canvas.itemconfig(self.background_obj, state="normal") self.show_track_content() self.canvas.itemconfig(self.show_text_obj, state="normal") self.canvas.itemconfig(self.track_text_obj, state="normal") self.canvas.itemconfig(self.hint_obj, state="normal") # self.canvas.update_idletasks( ) # decide whether the show background should be enabled. # print 'DISPLAY SHOW BG',self.track_params['display-show-background'],self.background_obj if self.background_obj is None and self.track_params["display-show-background"] == "yes": self.enable_show_background = True else: self.enable_show_background = False # print 'ENABLE SB',self.enable_show_background def hide_x_content(self): self.mon.trace(self, "") self.hide_track_content() self.canvas.itemconfig(self.background_obj, state="hidden") self.canvas.itemconfig(self.show_text_obj, state="hidden") self.canvas.itemconfig(self.track_text_obj, state="hidden") self.canvas.itemconfig(self.hint_obj, state="hidden") # self.canvas.update_idletasks( ) self.canvas.delete(self.background_obj) self.canvas.delete(self.show_text_obj) self.canvas.delete(self.track_text_obj) self.canvas.delete(self.hint_obj) self.background = None # self.canvas.update_idletasks( ) # **************** # utilities # ***************** def get_links(self): return self.track_params["links"] # produce an absolute path from the relative one in track paramters def complete_path(self, track_file): # complete path of the filename of the selected entry if track_file[0] == "+": track_file = self.pp_home + track_file[1:] # self.mon.log(self,"Background image is "+ track_file) return track_file # get a text string from resources.cfg def resource(self, section, item): value = self.rr.get(section, item) return value # False if not found
class MediaList(object): """ manages a media list of tracks and the track selected from the medialist """ def __init__(self,sequence): self.clear() self.mon=Monitor() self.sequence=sequence # Functions for the editor dealing with complete list def clear(self): self._tracks = [] #MediaList, stored as a list of dicts self._num_tracks=0 self._selected_track_index=-1 # index of currently selected track def print_list(self): print('\n') print(self._tracks) def first(self): self._selected_track_index=-1 self.next(self.sequence) #let this do the work of randomaising or advancing to 0 def length(self): return self._num_tracks def append(self, track_dict): # print '\ntrack dict',track_dict """appends a track dictionary to the end of the medialist store""" self._tracks.append(copy.deepcopy(track_dict)) self._num_tracks+=1 def update(self,index,values): self._tracks[index].update(values) def remove(self,index): self._tracks.pop(index) self._num_tracks-=1 # deselect any track, saves worrying about whether index needs changing self._selected_track_index=-1 def move_up(self): if self._selected_track_index != 0: self._tracks.insert(self._selected_track_index-1, self._tracks.pop(self._selected_track_index)) self.select(self._selected_track_index-1) def move_down(self): if self._selected_track_index != self._num_tracks-1: self._tracks.insert(self._selected_track_index+1, self._tracks.pop(self._selected_track_index)) self.select(self._selected_track_index+1) def copy(self): self._tracks.insert(self._selected_track_index+1, copy.deepcopy(self._tracks[self._selected_track_index])) self._num_tracks+=1 self.select(self._selected_track_index+1) def replace(self,index,replacement): self._tracks[index]= replacement # Common functions work for anything def track_is_selected(self): if self._selected_track_index>=0: return True else: return False def selected_track_index(self): return self._selected_track_index def track(self,index): return self._tracks[index] def selected_track(self): """returns a dictionary containing all fields in the selected track """ return self._selected_track def select(self,index): """does housekeeping necessary when a track is selected""" if self._num_tracks>0 and index>=0 and index< self._num_tracks: self._selected_track_index=index self._selected_track = self._tracks[index] return True else: return False # Dealing with anonymous tracks for use and display def at_end(self): # true is selected track is last anon index=self._num_tracks-1 while index>=0: if self._tracks[index] ['track-ref'] =="": end=index if self._selected_track_index==end: return True else: return False index -=1 return False def index_of_end(self): if self.anon_length()==0: return False index=self._num_tracks-1 while index >= 0: if self._tracks[index] ['track-ref'] =="": return index index -=1 return -1 def at_start(self): if self.anon_length()==0: return False index=0 while index<self._num_tracks: if self._tracks[index] ['track-ref'] =="": start = index if self._selected_track_index==start: return True else: return False index +=1 return False def index_of_start(self): if self.anon_length()==0: return False index=0 while index<self._num_tracks: if self._tracks[index] ['track-ref'] =="": return index index +=1 return False def anon_length(self): # number of anonymous tracks count=0 index=0 while index<self._num_tracks: if self._tracks[index] ['track-ref'] =="": count+=1 index +=1 return count def start(self): if self.anon_length()==0: return False # select first anonymous track in the list if self.sequence == 'ordered': index=0 while index<self._num_tracks: if self._tracks[index] ['track-ref'] =="": self.select(index) return True index +=1 return False else: match=random.randint(0,self.anon_length()-1) # print 'match',match index=0 while index<self._num_tracks: if self._tracks[index] ['track-ref'] =="" and index==match: self.select(index) # print index return index index +=1 def finish(self): if self.anon_length()==0: return False if self.sequence == 'ordered': # select last anymous track in the list index=self._num_tracks-1 while index>=0: if self._tracks[index] ['track-ref'] =="": self.select(index) return True index -=1 return False else: match=random.randint(0,self.anon_length()-1) # print 'match',match index=0 while index<self._num_tracks: if self._tracks[index] ['track-ref'] =="" and index==match: self.select(index) # print index return index index +=1 def select_anon_by_index(self,wanted): if self.anon_length()==0: return False index=0 anon_index=0 while index != self._num_tracks: # print index,self._tracks[index] ['track-ref'],wanted if self._tracks[index] ['track-ref'] =="": if anon_index==wanted: # print 'match\n' self.select(index) return True anon_index+=1 index= index+1 return False def next(self,sequence): if self.anon_length()==0: return False if sequence=='ordered': if self._selected_track_index== self._num_tracks-1: index=0 else: index= self._selected_track_index+1 end=self._selected_track_index else: index=random.randint(0,self.anon_length()-1) if index==0: end=self._num_tracks-1 else: end=index-1 # search for next anonymous track # print 'index', index, 'end',end while index != end: if self._tracks[index] ['track-ref'] =="": self.select(index) return True if index== self._num_tracks-1: index=0 else: index= index+1 return False def previous(self,sequence): if self.anon_length()==0: return False if sequence=='ordered': if self._selected_track_index == 0: index=self._num_tracks-1 else: index= self._selected_track_index-1 end = self._selected_track_index else: index=random.randint(0,self.anon_length()-1) if index==self._num_tracks-1: end=0 else: end=index+1 # print 'index', index, 'end',end # search for previous anonymous track while index != end : if self._tracks[index] ['track-ref'] =="": self.select(index) return True if index == 0: index=self._num_tracks-1 else: index= index-1 return False # Lookup for labelled tracks def index_of_track(self,wanted_track): index = 0 for track in self._tracks: if track['track-ref']==wanted_track: return index index +=1 return -1 # open and save def add_track_for_file(self, filename, filetype): """ Simulates adding an image or video as if it was coming from a media list. :param filename: The image to add :return: """ track = {} track["type"] = filetype track["location"] = filename track["track-ref"] = "" track["background-image"] = "" track["background-colour"] = "" track["animate-begin"] = "" track["animate-end"] = "" track["duration"] = "" track["image-window"] = "fit" track["image-rotation"] = "" track["image-rotate"] = "0" track["pause-timeout"] = "" track["plugin"] = "" track["track-html-background-colour"] = "white" track["track-html-height"] = "300" track["track-html-width"] = "300" track["track-text-x"] = "" track["track-text-y"] = "" track["track-text-justify"] = "" track["track-text-font"] = "" track["track-text-colour"] = "" track["track-text-location"] = "" track["track-text-type"] = "plain" track["track-text"] = "" track["display-show-background"] = "" track["show-control-begin"] = "" track["show-control-end"] = "" track["thumbnail"] = "" track["transition"] = "cut" track["pause-text"] = "" track["animate-clear"] = "" track["omx-audio"] = "" track["cmx-audio"] = "" track["omx-volume"] = "" track["omx-window"] = "" track["omx-other-options"] = "" track["freeze-at-start"] = "" track["freeze-at-end"] = "" track["seamless-loop"] = "" track["pause-timeout"] = "" self._tracks.append(track) self._num_tracks = len(self._tracks) self.mon.log(self, "Added file " + filename) def open_directory(self,directory, match_glob, exclude_regex): """ Opens a directory as if it was a tracklist of all the files in the directory. Right now, we're hard-coding the files to look for to .jpg files ... but extending it would be fairly straightforward ... :param directory: Directory to scan for files and load everything with a matching extension. Lame error-checking -- be sure no trailing slash :param match_glob: A set of file-specs to scan for and match. The list itself is separated by |. Then each item in the list consists of a glob-type match, separated by # from the type (image or video) that we're matching. i.e., glob#type|glob#type|glob#type e.g., /**/*.jpg#image|/**/*.mpg#video :param exclude_regex: A regular expression indicating files to skip :return: """ self.clear() self.mon.log(self, "Adding files matching " + match_glob + " from " + directory) self.mon.log(self, "Excluding files: " + exclude_regex) import glob import re if len(exclude_regex) > 0: matcher = re.compile(exclude_regex) else: matcher = None pats = match_glob.split("|") for each_pat in pats: thisglob, type = each_pat.split("#") self.mon.log(self, "Matching glob " + thisglob) for filename in glob.iglob(directory + thisglob): if matcher is None or not matcher.match(filename): self.add_track_for_file(filename, type) self.mon.log(self, "Loaded " + str(self._num_tracks) + " files.") self.last_num_tracks = self._num_tracks return True def open_list(self,filename,profile_version): """ opens a saved medialist medialists are stored as json arrays. """ ifile = open(filename, 'r') mdict = json.load(ifile) ifile.close() if 'issue' in mdict: self.medialist_version_string= mdict['issue'] else: self.medialist_version_string="1.0" # If the media file contains a loaddir key, then treat this as a directory to load, instead of a single file. # loaddir: the directory to (recursively) load # exclude-regex: optional - a regular expression to use to exclude files from the load (I use to exclude # directories) # match-glob: a list of glob specs and types separated by |, where the glob spec and type are separated by # # e.g.: # "loaddir":"/media/some-pictures" # "exclude-regex":".*\\/(skip-this-dir|skip-this-dir-also)\\/.*" # "match-glob":"//**/*.jpg#image|/**/*.JPG#image|/**/*.gif#image|/**/*.GIF#image|/***/*.mpg#video" if 'loaddir' in mdict: if 'match-glob' in mdict: match_glob = mdict['match-glob'] else: match_glob = "/**/*.jpg" if 'exclude-regex' in mdict: exclude_regex = mdict['exclude-regex'] else: exclude_regex = '' return self.open_directory(mdict['loaddir'], match_glob, exclude_regex) else: self._tracks = mdict['tracks'] if self.medialist_version()==profile_version: self._num_tracks=len(self._tracks) self.last_num_tracks=self._num_tracks self._selected_track_index=-1 return True else: return False def medialist_version(self): vitems=self.medialist_version_string.split('.') if len(vitems)==2: # cope with 2 digit version numbers before 1.3.2 return 1000*int(vitems[0])+100*int(vitems[1]) else: return 1000*int(vitems[0])+100*int(vitems[1])+int(vitems[2]) # dummy for mediliast, in livelist the list is created from the live_track directories def use_new_livelist(self): pass def create_new_livelist(self): pass def new_length(self): return self.length() # for medialist the content of the list never changes so return False def livelist_changed(self): return False def save_list(self,filename): """ save a medialist """ if filename=="": return False dic={'issue':self.medialist_version_string,'tracks':self._tracks} filename=str(filename) filename = str.replace(filename,'\\','/') tries = 1 while tries<=10: # print "save medialist ",filename try: ofile = open(filename, "w") json.dump(dic,ofile,sort_keys=True,indent=1) ofile.close() self.mon.log(self,"Saved medialist "+ filename) break except IOError: self.mon.err(self,"failed to save medialist, trying again " + str(tries)) tries+=1 return
class OMXDriver(object): _STATUS_REXP = re.compile(r"M:\s*(\w*)\s*V:") _DONE_REXP = re.compile(r"have a nice day.*") _LAUNCH_CMD = '/usr/bin/omxplayer -s ' #needs changing if user has installed his own version of omxplayer elsewhere def __init__(self,widget): self.widget=widget self.mon=Monitor() self.mon.on() self.paused=None self._process=None def control(self,char): self._process.send(char) def pause(self): self._process.send('p') if not self.paused: self.paused = True else: self.paused=False def play(self, track, options): self._pp(track, options,False) def prepare(self, track, options): self._pp(track, options,True) def show(self): # unpause to start playing self._process.send('p') self.paused = False def stop(self): if self._process<>None: self._process.send('q') # kill the subprocess (omxplayer.bin). Used for tidy up on exit. def terminate(self,reason): self.terminate_reason=reason if self._process<>None: self._process.send('q') def terminate_reason(self): return self.terminate_reason # test of whether _process is running def is_running(self): return self._process.isalive() # kill off omxplayer when it hasn't terminated at the end of a track. # send SIGINT (CTRL C) so it has a chance to tidy up daemons and omxplayer.bin def kill(self): self._process.kill(signal.SIGINT) # *********************************** # INTERNAL FUNCTIONS # ************************************ def _pp(self, track, options, pause_before_play): self.paused=False self.start_play_signal = False self.end_play_signal=False self.terminate_reason='' track= "'"+ track.replace("'","'\\''") + "'" cmd = OMXDriver._LAUNCH_CMD + options +" " + track self.mon.log(self, "Send command to omxplayer: "+ cmd) self._process = pexpect.spawn(cmd) # uncomment to monitor output to and input from omxplayer.bin (read pexpect manual) fout= file('omxlogfile.txt','w') #uncomment and change sys.stdout to fout to log to a file # self._process.logfile_send = sys.stdout # send just commands to stdout self._process.logfile=fout # send all communications to log file if pause_before_play: self._process.send('p') self.paused = True #start the thread that is going to monitor sys.stdout. Presumably needs a thread because of blocking self._position_thread = Thread(target=self._get_position) self._position_thread.start() def _get_position(self): self.start_play_signal = True self.video_position=0.0 self.audio_position=0.0 while True: index = self._process.expect([OMXDriver._DONE_REXP, pexpect.TIMEOUT, pexpect.EOF, OMXDriver._STATUS_REXP] ,timeout=10) if index == 1: #timeout omxplayer should not do this self.end_play_signal=True self.xbefore=self._process.before self.xafter=self._process.after self.match=self._process.match self.end_play_reason='timeout' break # continue elif index == 2: #2 is eof omxplayer should not send this #eof detected self.end_play_signal=True self.xbefore=self._process.before self.xafter=self._process.after self.match=self._process.match self.end_play_reason='eof' break elif index==0: #0 is done #Have a nice day detected self.end_play_signal=True self.xbefore=self._process.before self.xafter=self._process.after self.match=self._process.match self.end_play_reason='nice_day' break else: # - 3 matches _STATUS_REXP so get time stamp self.video_position = float(self._process.match.group(1)) self.audio_position = 0.0 #sleep is Ok here as it is a seperate thread. self.widget.after has funny effects as its not in the maion thread. sleep(0.05) # stats output rate seem to be about 170mS.
class MenuShow: """ Displays a menu with optional hint below it. User can traverse the menu and select a track using key or button presses. Interface: * play - displays the menu and selects the first entry * input_pressed, - receives user events passes them to a Player if a track is playing, otherwise actions them with _next, _previous, _play_selected_track, _end Optional display of eggtimer by means of Players ready_callback Supports imageplayer, videoplayer,messagplayer,audioplayer,menushow,mediashow Destroys itself on exit """ # ********************* # external interface # ******************** def __init__(self, show_params, root, canvas, showlist, pp_dir, pp_home, pp_profile): """ canvas - the canvas that the menu is to be written on show - the name of the configuration dictionary section for the menu showlist - the showlist pp_home - Pi presents data_home directory pp_profile - Pi presents profile directory""" self.mon=Monitor() self.mon.on() self.display_guidelines_command=show_params['menu-guidelines'] self.display_guidelines=self.display_guidelines_command #instantiate arguments self.show_params=show_params self.root=root self.canvas=canvas self.showlist=showlist self.pp_dir=pp_dir self.pp_home=pp_home self.pp_profile=pp_profile # open resources self.rr=ResourceReader() # init variables self.drawn = None self.player=None self.shower=None self.menu_timeout_running=None self.error=False def play(self,show_id,end_callback,ready_callback,top=False,command='nil'): """ displays the menu end_callback - function to be called when the menu exits ready_callback - callback when menu is ready to display (not used) top is True when the show is top level (run from [start]) """ #instantiate arguments self.show_id=show_id self.end_callback=end_callback self.ready_callback=ready_callback self.top=top self.command=command # check data files are available. self.menu_file = self.pp_profile + "/" + self.show_params['medialist'] if not os.path.exists(self.menu_file): self.mon.err(self,"Medialist file not found: "+ self.menu_file) self.end('error',"Medialist file not found") #create a medialist for the menu and read it. self.medialist=MediaList() if self.medialist.open_list(self.menu_file,self.showlist.sissue()) == False: self.mon.err(self,"Version of medialist different to Pi Presents") self.end('error',"Version of medialist different to Pi Presents") #get control bindings for this show if top level controlsmanager=ControlsManager() if self.top==True: self.controls_list=controlsmanager.default_controls() # and merge in controls from profile self.controls_list=controlsmanager.merge_show_controls(self.controls_list,self.show_params['controls']) if self.show_params['has-background']=="yes": background_index=self.medialist.index_of_track ('pp-menu-background') if background_index>=0: self.menu_img_file = self.complete_path(self.medialist.track(background_index)['location']) if not os.path.exists(self.menu_img_file): self.mon.err(self,"Menu background file not found: "+ self.menu_img_file) self.end('error',"Menu background file not found") else: self.mon.err(self,"Menu background not found in medialist") self.end('error',"Menu background not found") self.end_menushow_signal= False if self.ready_callback<>None: self.ready_callback() self.menu_timeout_value=int(self.show_params['timeout'])*1000 self.do_menu() def do_menu(self): #start timeout alarm if required if int(self.show_params['timeout'])<>0: self.menu_timeout_running=self.canvas.after(self.menu_timeout_value,self.timeout_menu) if self.show_params['menu-background-colour']<>'': self.canvas.config(bg=self.show_params['menu-background-colour']) self.canvas.delete('pp-content') self.canvas.update() # display background image if self.show_params['has-background']=="yes": self.display_background() self.delete_eggtimer() self.display_new_menu() self.canvas.tag_raise('pp-click-area') self.canvas.update_idletasks( ) # display menu text if enabled if self.show_params['menu-text']<> '': self.canvas.create_text(int(self.show_params['menu-text-x']),int(self.show_params['menu-text-y']), anchor=NW, text=self.show_params['menu-text'], fill=self.show_params['menu-text-colour'], font=self.show_params['menu-text-font'], tag='pp-content') self.canvas.update_idletasks( ) # display instructions (hint) hint_text=self.show_params['hint-text'] if hint_text<>'': self.canvas.create_text(int(self.show_params['hint-x']),int(self.show_params['hint-y']), anchor=NW, text=hint_text, fill=self.show_params['hint-colour'], font=self.show_params['hint-font'], tag='pp-content') self.canvas.update_idletasks( ) #stop received from another concurrent show def managed_stop(self): if self.menu_timeout_running<>None: self.canvas.after_cancel(self.menu_timeout_running) self.menu_timeout_running=None if self.shower<>None: self.shower.managed_stop() elif self.player<>None: self.end_menushow_signal=True self.player.input_pressed('stop') else: self.end('normal','stopped by ShowManager') # kill or error received def terminate(self,reason): if self.menu_timeout_running<>None: self.canvas.after_cancel(self.menu_timeout_running) self.menu_timeout_running=None if self.shower<>None: self.shower.terminate(reason) elif self.player<>None: self.player.terminate(reason) else: self.end(reason,'Terminated no shower or player running') # respond to user inputs. def input_pressed(self,symbol,edge,source): self.mon.log(self,"Show Id: "+str(self.show_id)+" received key or operation: " + symbol) if self.show_params['disable-controls']=='yes': return # if at top convert symbolic name to operation otherwise lower down we have received an operation # look through list of standard symbols to find match (symbolic-name, function name) operation =lookup (symbol if self.top==True: operation=self.lookup_control(symbol,self.controls_list) else: operation=symbol # print 'operation',operation # if no match for symbol against standard operations then return if operation=='': return else: if self.shower<>None: # if next lower show is running pass down operatin to the show and lower levels self.shower.input_pressed(operation,source,edge) else: #service the standard inputs for this show if operation=='stop': if self.menu_timeout_running<>None: self.canvas.after_cancel(self.menu_timeout_running) self.menu_timeout_running=None if self.shower<>None: self.shower.input_pressed('stop',edge,source) elif self.player<>None: self.player.input_pressed('stop') else: # not at top so end the show if self.top == False: self.end('normal',"exit from stop command") else: pass elif operation in ('up','down'): # if child or sub-show running and is a show pass down # if child not running - move if self.shower<>None: self.shower.input_pressed(operation,edge,source) else: if self.player==None: if self.menu_timeout_running<>None: self.canvas.after_cancel(self.menu_timeout_running) self.menu_timeout_running=self.canvas.after(self.menu_timeout_value,self.timeout_menu) if operation=='up': self.previous() else: self.next() elif operation =='play': # if child running and is show - pass down # if no track already running - play if self.shower<>None: self.shower.input_pressed(operation,edge,source) else: if self.player==None: self.play_selected_track(self.medialist.selected_track()) elif operation == 'pause': # pass down if show or track running. if self.shower<>None: self.shower.input_pressed(operation,edge,source) elif self.player<>None: self.player.input_pressed(operation) elif operation[0:4]=='omx-' or operation[0:6]=='mplay-'or operation[0:5]=='uzbl-': if self.player<>None: self.player.input_pressed(operation) def lookup_control(self,symbol,controls_list): for control in controls_list: if symbol == control[0]: return control[1] return '' # ********************* # INTERNAL FUNCTIONS # ******************** # ********************* # Sequencing # ********************* def timeout_menu(self): self.end('normal','menu timeout') return def next(self): self.highlight_menu_entry(self.menu_index,False) self.medialist.next('ordered') if self.menu_index==self.menu_length-1: self.menu_index=0 else: self.menu_index+=1 self.highlight_menu_entry(self.menu_index,True) def previous(self): self.highlight_menu_entry(self.menu_index,False) if self.menu_index==0: self.menu_index=self.menu_length-1 else: self.menu_index-=1 self.medialist.previous('ordered') self.highlight_menu_entry(self.menu_index,True) # at the end of a track just re-display the menu with the original callback from the menu def what_next(self,message): # user wants to end if self.end_menushow_signal==True: self.end_menushow_signal=False self.end('normal',"show ended by user") else: self.do_menu() # ********************* # Dispatching to Players # ********************* def play_selected_track(self,selected_track): """ selects the appropriate player from type field of the medialist and computes the parameters for that type selected track is a dictionary for the track/show """ #remove menu and show working..... if self.menu_timeout_running<>None: self.canvas.after_cancel(self.menu_timeout_running) self.menu_timeout_running=None self.canvas.delete('pp-content') self.display_eggtimer(self.resource('menushow','m01')) # dispatch track by type self.player=None self.shower=None track_type = selected_track['type'] self.mon.log(self,"Track type is: "+ track_type) if track_type=="video": # create a videoplayer track_file=self.complete_path(selected_track['location']) self.player=VideoPlayer(self.show_id,self.root,self.canvas,self.show_params,selected_track,self.pp_dir,self.pp_home,self.pp_profile) self.player.play(track_file, self.showlist, self.end_player, self.delete_eggtimer, enable_menu=False) elif track_type=="audio": # create a audioplayer track_file=self.complete_path(selected_track['location']) self.player=AudioPlayer(self.show_id,self.root,self.canvas,self.show_params,selected_track,self.pp_dir,self.pp_home,self.pp_profile) self.player.play(track_file, self.showlist, self.end_player, self.delete_eggtimer, enable_menu=False) elif track_type=="image": # images played from menus don't have children track_file=self.complete_path(selected_track['location']) self.player=ImagePlayer(self.show_id,self.root,self.canvas,self.show_params,selected_track,self.pp_dir,self.pp_home,self.pp_profile) self.player.play(track_file, self.showlist, self.end_player, self.delete_eggtimer, enable_menu=False, ) elif track_type=="web": # create a browser track_file=self.complete_path(selected_track['location']) self.player=BrowserPlayer(self.show_id,self.root,self.canvas,self.show_params,selected_track,self.pp_dir,self.pp_home,self.pp_profile) self.player.play(track_file, self.showlist, self.end_player, self.delete_eggtimer, enable_menu=False) elif track_type=="message": # bit odd because MessagePlayer is used internally to display text. text=selected_track['text'] self.player=MessagePlayer(self.show_id,self.root,self.canvas,self.show_params,selected_track,self.pp_dir,self.pp_home,self.pp_profile) self.player.play(text, self.showlist, self.end_player, self.delete_eggtimer, enable_menu=False ) elif track_type=="show": # get the show from the showlist index = self.showlist.index_of_show(selected_track['sub-show']) if index >=0: self.showlist.select(index) selected_show=self.showlist.selected_show() else: self.mon.err(self,"Show not found in showlist: "+ selected_track['sub-show']) self.end("Unknown show") if selected_show['type']=="mediashow": self.shower= MediaShow(selected_show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id,self.end_shower,self.delete_eggtimer,top=False,command='nil') elif selected_show['type']=="liveshow": self.shower= LiveShow(selected_show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id,self.end_shower,self.delete_eggtimer,top=False,command='nil') elif selected_show['type']=="radiobuttonshow": self.shower= RadioButtonShow(selected_show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id,self.end_shower,self.ready_callback,top=False,command='nil') elif selected_show['type']=="hyperlinkshow": self.shower= HyperlinkShow(selected_show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id,self.end_shower,self.ready_callback,top=False,command='nil') elif selected_show['type']=="menu": self.shower= MenuShow(selected_show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id,self.end_shower,self.delete_eggtimer,top=False,command='nil') else: self.mon.err(self,"Unknown Show Type: "+ selected_show['type']) self.end("Unknown show type") else: self.mon.err(self,"Unknown Track Type: "+ track_type) self.end("Unknown track type") # callback from when player ends def end_player(self,reason,message): self.mon.log(self,"Returned from player with message: "+ message) self.player=None if reason in("killed","error"): self.end(reason,message) else: self.display_eggtimer(self.resource('menushow','m02')) self.what_next(message) # callback from when shower ends def end_shower(self,show_id,reason,message): self.mon.log(self,"Returned from shower with message: "+ message) self.shower=None if reason in ("killed","error"): self.end(reason,message) else: self.display_eggtimer(self.resource('menushow','m03')) self.what_next(message) # ********************* # Ending the show # ********************* # finish the player for killing, error or normally # this may be called directly if sub/child shows or players are not running # if they might be running then need to call terminate????? def end(self,reason,message): self.mon.log(self,"Ending menushow: "+ self.show_params['show-ref']) if self.menu_timeout_running<>None: self.canvas.after_cancel(self.menu_timeout_running) self.menu_timeout_running=None self.end_callback(self.show_id,reason,message) self=None return # ********************* # Displaying things # ********************* def display_background(self): pil_menu_img=PIL.Image.open(self.menu_img_file) self.menu_background = PIL.ImageTk.PhotoImage(pil_menu_img) self.drawn = self.canvas.create_image(int(self.canvas['width'])/2, int(self.canvas['height'])/2, image=self.menu_background, anchor=CENTER, tag='pp-content') def display_new_menu(self): # calculate menu geometry error,reason=self.calculate_geometry() if error<>'normal': self.mon.err(self,"Menu geometry error: "+ reason) self.end('error',"Menu geometry error") else: # display the menu entries self.display_menu_entries() def display_menu_entries(self): # init the loop column_index=0 row_index=0 self.menu_length=1 # id store is a list of elements each being a list of the three ids of the elements of the entry self.menu_entry_id=[] # offsets for the above self.icon_id_index=0 # rectangle around the icon self.image_id_index=1 # icon image - needed for tkinter self.text_id_index=2 # the text - need whn no icon is displayed #select the startof the medialist self.medialist.start() #loop through menu entries while True: #display the entry #calculate top left corner of entry self.calculate_entry_position(column_index,row_index) # display the button strip self.display_entry_strip() #display the selected entry highlight icon_id=self.display_icon_rectangle() #display the image in the icon image_id=self.display_icon_image() if self.show_params['menu-text-mode']<>'none': text_id=self.display_icon_text() else: text_id=None #append id's to the list self.menu_entry_id.append([icon_id,image_id,text_id]) self.canvas.update_idletasks( ) #and loop if self.medialist.at_end(): break self.menu_length+=1 self.medialist.next('ordered') if self.direction=='horizontal': column_index+=1 if column_index>=self.menu_columns: column_index=0 row_index+=1 else: row_index+=1 if row_index>=self.menu_rows: row_index=0 column_index+=1 # finally select and highlight the first entry self.medialist.start() self.menu_index=0 self.highlight_menu_entry(self.menu_index,True) def print_geometry(self,total_width,total_height): print 'menu width: ', self.menu_width print 'columns', self.menu_columns print 'icon width: ', self.icon_width print 'horizontal padding: ', self.menu_horizontal_padding print 'text width: ', self.text_width print 'entry width: ', self.entry_width print 'total width: ', total_width print 'x separation: ', self.x_separation print '' print 'menu height', self.menu_height print 'rows: ', self.menu_rows print 'icon height', self.icon_height print 'vertical padding: ', self.menu_vertical_padding print 'text height', self.text_height print 'entry height', self.entry_height print 'total height', total_height print 'y separation', self.y_separation # ------------------------------------------------------------------ #calculate menu entry size and separation between menu entries # ------------------------------------------------------------------ def calculate_geometry(self): self.display_strip=self.show_params['menu-strip'] self.screen_width=int(self.canvas['width']) self.screen_height=int(self.canvas['height']) if self.display_strip=='yes': self.strip_padding=int(self.show_params['menu-strip-padding']) else: self.strip_padding=0 # parse the menu window error,reason,self.menu_x_left,self.menu_y_top,self.menu_x_right,self.menu_y_bottom=self.parse_menu_window(self.show_params['menu-window']) if error<>'normal': return 'error',"Menu Window error: "+ reason if self.show_params['menu-icon-mode']=='none' and self.show_params['menu-text-mode']=='none': return 'error','Icon and Text are both None' if self.show_params['menu-icon-mode']=='none' and self.show_params['menu-text-mode']=='overlay': return 'error','cannot overlay none icon' self.direction=self.show_params['menu-direction'] self.menu_width=self.menu_x_right - self.menu_x_left self.menu_height=self.menu_y_bottom - self.menu_y_top self.list_length=self.medialist.display_length() # get or calculate rows and columns if self.direction=='horizontal': if self.show_params['menu-columns']=='': return 'error','blank columns for horizontal direction' self.menu_columns=int(self.show_params['menu-columns']) self.menu_rows=self.list_length//self.menu_columns if self.list_length % self.menu_columns<>0: self.menu_rows+=1 else: if self.show_params['menu-rows']=='': return 'error','blank rows for vertical direction' self.menu_rows=int(self.show_params['menu-rows']) self.menu_columns=self.list_length//self.menu_rows if self.list_length % self.menu_rows<>0: self.menu_columns+=1 self.x_separation=int(self.show_params['menu-horizontal-separation']) self.y_separation=int(self.show_params['menu-vertical-separation']) # get size of padding depending on exitence of icon and text if self.show_params['menu-icon-mode'] in ('thumbnail','bullet') and self.show_params['menu-text-mode'] == 'right': self.menu_horizontal_padding=int(self.show_params['menu-horizontal-padding']) else: self.menu_horizontal_padding=0 if self.show_params['menu-icon-mode'] in ('thumbnail','bullet') and self.show_params['menu-text-mode'] == 'below': self.menu_vertical_padding=int(self.show_params['menu-vertical-padding']) else: self.menu_vertical_padding=0 #calculate size of icon depending on use if self.show_params['menu-icon-mode'] in ('thumbnail','bullet'): self.icon_width=int(self.show_params['menu-icon-width']) self.icon_height=int(self.show_params['menu-icon-height']) else: self.icon_width=0 self.icon_height=0 #calculate size of text box depending on mode if self.show_params['menu-text-mode']<>'none': self.text_width=int(self.show_params['menu-text-width']) self.text_height=int(self.show_params['menu-text-height']) else: self.text_width=0 self.text_height=0 # calculate size of entry box by combining text and icon sizes if self.show_params['menu-text-mode'] == 'right': self.entry_width=self.icon_width+self.menu_horizontal_padding+self.text_width self.entry_height=max(self.text_height,self.icon_height) elif self.show_params['menu-text-mode']=='below': self.entry_width=max(self.text_width,self.icon_width) self.entry_height=self.icon_height + self.menu_vertical_padding + self.text_height else: # no text or overlaid text if self.show_params['menu-icon-mode'] in ('thumbnail','bullet'): # icon only self.entry_width=self.icon_width self.entry_height=self.icon_height else: #text only self.entry_width=self.text_width self.entry_height=self.text_height if self.entry_width<=self.menu_horizontal_padding: return 'error','entry width is zero' if self.entry_height<=self.menu_vertical_padding: return 'error','entry height is zero' # calculate totals for debugging puropses total_width=self.menu_columns * self.entry_width +(self.menu_columns-1)*self.x_separation total_height=self.menu_rows * self.entry_height + (self.menu_rows-1)*self.y_separation # self.print_geometry(total_width,total_height) # display guidelines and debgging text if there is a problem if total_width>self.menu_width and self.display_guidelines<>'never': self.display_guidelines='always' self.mon.log(self,'\nMENU IS WIDER THAN THE WINDOW') self.print_geometry(total_width,total_height) if total_height>self.menu_height and self.display_guidelines<>'never': self.display_guidelines='always' self.mon.log(self,'\nMENU IS TALLER THAN THE WINDOW') self.print_geometry(total_width,total_height) # display calculated total rectangle guidelines for debugging if self.display_guidelines=='always': points=[self.menu_x_left,self.menu_y_top, self.menu_x_left+total_width,self.menu_y_top+total_height] # and display the icon rectangle self.canvas.create_rectangle(points, outline='red', fill='', tag='pp-content') # display menu rectangle guidelines for debugging if self.display_guidelines=='always': points=[self.menu_x_left,self.menu_y_top, self.menu_x_right,self.menu_y_bottom] self.canvas.create_rectangle(points, outline='blue', fill='', tag='pp-content') return 'normal','' def calculate_entry_position(self,column_index,row_index): self.entry_x=self.menu_x_left+ column_index*(self.x_separation+self.entry_width) self.entry_y=self.menu_y_top+ row_index*(self.y_separation+self.entry_height) def display_entry_strip(self): if self.display_strip=='yes': if self.direction=='vertical': #display the strip strip_points=[self.entry_x - self.strip_padding -1 , self.entry_y - self.strip_padding - 1, self.entry_x+ self.entry_width + self.strip_padding - 1, self.entry_y+self.entry_height+ self.strip_padding - 1] self.canvas.create_rectangle(strip_points, outline='', fill='gray', stipple='gray12', tag='pp-content') top_l_points=[self.entry_x - self.strip_padding, self.entry_y - self.strip_padding, self.entry_x + self.entry_width + self.strip_padding , self.entry_y - self.strip_padding] self.canvas.create_line(top_l_points, fill='light gray', tag='pp-content') bottom_l_points=[self.entry_x - self.strip_padding, self.entry_y + self.entry_height + self.strip_padding, self.entry_x+ self.entry_width + self.strip_padding , self.entry_y+ self.entry_height + self.strip_padding] self.canvas.create_line(bottom_l_points, fill='dark gray', tag='pp-content') left_l_points=[self.entry_x - self.strip_padding, self.entry_y - self.strip_padding, self.entry_x - self.strip_padding, self.entry_y + self.entry_height + self.strip_padding] self.canvas.create_line(left_l_points, fill='gray', tag='pp-content') else: #display the strip vertically strip_points=[self.entry_x - self.strip_padding +1 , self.entry_y - self.strip_padding +1, self.entry_x+self.entry_width + self.strip_padding -1, self.entry_y + self.entry_height+ self.strip_padding -1] self.canvas.create_rectangle(strip_points, outline='', fill='gray', stipple='gray12', tag='pp-content') top_l_points=[self.entry_x - self.strip_padding, self.entry_y - self.strip_padding, self.entry_x + self.entry_width + self.strip_padding, self.entry_y - self.strip_padding] self.canvas.create_line(top_l_points, fill='light gray', tag='pp-content') left_l_points=[self.entry_x - self.strip_padding, self.entry_y - self.strip_padding, self.entry_x - self.strip_padding, self.entry_y + self.entry_height+ self.strip_padding] self.canvas.create_line(left_l_points, fill='gray', tag='pp-content') right_l_points=[self.entry_x +self.entry_width + self.strip_padding, self.entry_y - self.strip_padding, self.entry_x +self.entry_width + self.strip_padding, self.entry_y + self.entry_height+ self.strip_padding] self.canvas.create_line(right_l_points, fill='dark gray', tag='pp-content') # display the rectangle that goes arond the icon when the entry is selected def display_icon_rectangle(self): if self.show_params['menu-icon-mode'] in ('thumbnail','bullet'): #calculate icon parameters if self.icon_width<self.text_width and self.show_params['menu-text-mode']=='below': self.icon_x_left=self.entry_x+abs(self.icon_width-self.text_width)/2 else: self.icon_x_left=self.entry_x self.icon_x_right=self.icon_x_left+self.icon_width if self.icon_height<self.text_height and self.show_params['menu-text-mode']=='right': self.icon_y_top=self.entry_y+abs(self.icon_height-self.text_height)/2 else: self.icon_y_top=self.entry_y self.icon_y_bottom=self.icon_y_top+self.icon_height req_horiz_sep=self.menu_horizontal_padding req_vert_sep=self.menu_vertical_padding points=[self.icon_x_left,self.icon_y_top,self.icon_x_right,self.icon_y_top,self.icon_x_right,self.icon_y_bottom,self.icon_x_left,self.icon_y_bottom] # display guidelines make it white when not selctedfor debugging if self.display_guidelines=='always': outline='white' else: outline='' # and display the icon rectangle icon_id=self.canvas.create_polygon(points, outline=outline, fill='', tag='pp-content') else: # not using icon so set starting point for text to zero icon size self.icon_x_right=self.entry_x self.icon_y_bottom=self.entry_y req_horiz_sep=0 req_vert_sep=0 icon_id=None return icon_id #display the image in a menu entry def display_icon_image(self): image_id=None if self.show_params['menu-icon-mode'] == 'thumbnail': # try for the thumbnail if self.medialist.selected_track()['thumbnail']<>'' and os.path.exists(self.complete_path(self.medialist.selected_track()['thumbnail'])): self.pil_image=PIL.Image.open(self.complete_path(self.medialist.selected_track()['thumbnail'])) else: #cannot find thumbnail get the image if its an image track if self.medialist.selected_track()['type'] =='image': self.track=self.complete_path(self.medialist.selected_track()['location']) else: self.track='' if self.medialist.selected_track()['type']=='image' and os.path.exists(self.track)==True: self.pil_image=PIL.Image.open(self.track) else: #use a standard thumbnail type=self.medialist.selected_track()['type'] standard=self.pp_dir+os.sep+'pp_home'+os.sep+'pp_resources'+os.sep+type+'.png' if os.path.exists(standard)==True: self.pil_image=PIL.Image.open(standard) self.mon.log(self,'WARNING: default thumbnail used for '+self.medialist.selected_track()['title']) else: self.pil_image=None # display the image if self.pil_image<>None: self.pil_image=self.pil_image.resize((self.icon_width-2,self.icon_height-2)) image_id=PIL.ImageTk.PhotoImage(self.pil_image) self.canvas.create_image(self.icon_x_left+1, self.icon_y_top+1, image=image_id, anchor=NW, tag='pp-content') else: image_id=None elif self.show_params['menu-icon-mode'] =='bullet': bullet=self.complete_path(self.show_params['menu-bullet']) if os.path.exists(bullet)==False: self.pil_image=None else: self.pil_image=PIL.Image.open(bullet) if self.pil_image<>None: self.pil_image=self.pil_image.resize((self.icon_width-2,self.icon_height-2)) image_id=PIL.ImageTk.PhotoImage(self.pil_image) self.canvas.create_image(self.icon_x_left+1, self.icon_y_top+1, image=image_id, anchor=NW, tag='pp-content') else: image_id=None return image_id #display the text of a menu entry def display_icon_text(self): text_mode=self.show_params['menu-text-mode'] if self.show_params['menu-icon-mode'] in ('thumbnail','bullet'): if text_mode=='right': if self.icon_height>self.text_height: text_y_top=self.entry_y+abs(self.icon_height-self.text_height)/2 else: text_y_top=self.entry_y text_y_bottom=text_y_top+self.text_height text_x_left=self.icon_x_right+self.menu_horizontal_padding text_x_right=text_x_left+self.text_width text_x=text_x_left text_y=text_y_top+(self.text_height/2) elif text_mode=='below': text_y_top=self.icon_y_bottom+self.menu_vertical_padding text_y_bottom=text_y_top+self.text_height if self.icon_width>self.text_width: text_x_left=self.entry_x+abs(self.icon_width-self.text_width)/2 else: text_x_left=self.entry_x text_x_right=text_x_left+self.text_width text_x=text_x_left+(self.text_width/2) text_y=text_y_top else: # icon with text_mode=overlay or none text_x_left=self.icon_x_left text_x_right= self.icon_x_right text_y_top=self.icon_y_top text_y_bottom=self.icon_y_bottom text_x=(text_x_left+text_x_right)/2 text_y=(text_y_top+text_y_bottom)/2 else: #no icon text only text_y_top=self.entry_y text_y_bottom=text_y_top+self.text_height text_x_left=self.entry_x text_x_right=text_x_left+self.text_width text_x=self.entry_x text_y=self.entry_y+self.text_height/2 #display the guidelines for debugging if self.display_guidelines=='always': points=[text_x_left,text_y_top,text_x_right,text_y_top,text_x_right,text_y_bottom,text_x_left,text_y_bottom] self.canvas.create_polygon(points,fill= '' , outline='white', tag='pp-content') # display the text if text_mode=='below' and self.show_params['menu-icon-mode'] in ('thumbnail','bullet'): anchor=N justify=CENTER elif text_mode=='overlay' and self.show_params['menu-icon-mode'] in ('thumbnail','bullet'): anchor=CENTER justify=CENTER else: anchor=W justify=LEFT text_id=self.canvas.create_text(text_x,text_y, text=self.medialist.selected_track()['title'], anchor=anchor, fill=self.show_params['entry-colour'], font=self.show_params['entry-font'], width=self.text_width, justify=justify, tag='pp-content') return text_id def highlight_menu_entry(self,index,state): if self.show_params['menu-icon-mode']<>'none': if state==True: self.canvas.itemconfig(self.menu_entry_id[index][self.icon_id_index], outline=self.show_params['entry-select-colour'], width=4, ) else: self.canvas.itemconfig(self.menu_entry_id[index][self.icon_id_index], outline='', width=1 ) else: if state==True: self.canvas.itemconfig(self.menu_entry_id[index][self.text_id_index], fill=self.show_params['entry-select-colour']) else: self.canvas.itemconfig(self.menu_entry_id[index][self.text_id_index], fill=self.show_params['entry-colour']) def display_eggtimer(self,text): # print "display eggtimer" self.canvas.create_text(int(self.canvas['width'])/2, int(self.canvas['height'])/2, text= text, fill='white', font="Helvetica 20 bold", tag='pp-eggtimer') self.canvas.update_idletasks( ) def delete_eggtimer(self): # print"delete eggtimer" self.canvas.delete('pp-eggtimer') self.canvas.update_idletasks( ) # ********************* # utilities # ********************* def complete_path(self,track_file): # complete path of the filename of the selected entry if track_file<>'' and track_file[0]=="+": track_file=self.pp_home+track_file[1:] return track_file def parse_menu_window(self,line): if line<>'': fields = line.split() if len(fields) not in (1, 2,4): return 'error','wrong number of fields',0,0,0,0 if len(fields)==1: if fields[0]=='fullscreen': return 'normal','',0,0,self.screen_width - 1, self.screen_height - 1 else: return 'error','single field is not fullscreen',0,0,0,0 if len(fields)==2: if fields[0].isdigit() and fields[1].isdigit(): return 'normal','',int(fields[0]),int(fields[1]),self.screen_width, self.screen_height else: return 'error','field is not a digit',0,0,0,0 if len(fields)==4: if fields[0].isdigit() and fields[1].isdigit() and fields[2].isdigit() and fields[3].isdigit(): return 'normal','',int(fields[0]),int(fields[1]),int(fields[2]),int(fields[3]) else: return 'error','field is not a digit',0,0,0,0 else: return 'error','line is blank',0,0,0,0 def resource(self,section,item): value=self.rr.get(section,item) if value==False: self.mon.err(self, "resource: "+section +': '+ item + " not found" ) # timers may be running so need terminate self.terminate("error") else: return value
class AudioPlayer: """ plays an audio track using mplayer against a coloured backgroud and image track can be paused and interrupted See pp_imageplayer for common software design description """ # state constants _CLOSED = "mplayer_closed" # probably will not exist _STARTING = "mplayer_starting" # track is being prepared _PLAYING = "mplayer_playing" # track is playing to the screen, may be paused _ENDING = "mplayer_ending" # track is in the process of ending due to quit or end of track _WAITING = "wait for timeout" # track has finished but timeout still running # audio mixer matrix settings _LEFT = "channels=2:1:0:0:1:1" _RIGHT = "channels=2:1:0:1:1:0" _STEREO = "channels=2" # *************************************** # EXTERNAL COMMANDS # *************************************** def __init__(self, show_id, root, canvas, show_params, track_params, pp_dir, pp_home, pp_profile): self.mon = Monitor() self.mon.off() # instantiate arguments self.show_id = show_id self.root = root self.canvas = canvas self.show_params = show_params self.track_params = track_params self.pp_dir = pp_dir self.pp_home = pp_home self.pp_profile = pp_profile # get duration limit (secs ) from profile if self.track_params["duration"] <> "": self.duration = int(self.track_params["duration"]) self.duration_limit = 20 * self.duration else: self.duration_limit = -1 # get background image from profile. self.background_file = "" if self.track_params["background-image"] <> "": self.background_file = self.track_params["background-image"] else: if self.track_params["display-show-background"] == "yes": self.background_file = self.show_params["background-image"] # get background colour from profile. if self.track_params["background-colour"] <> "": self.background_colour = self.track_params["background-colour"] else: self.background_colour = self.show_params["background-colour"] # get audio device from profile. if self.track_params["mplayer-audio"] <> "": self.mplayer_audio = self.track_params["mplayer-audio"] else: self.mplayer_audio = self.show_params["mplayer-audio"] # get audio volume from profile. if self.track_params["mplayer-volume"] <> "": self.mplayer_volume = self.track_params["mplayer-volume"].strip() else: self.mplayer_volume = self.show_params["mplayer-volume"].strip() self.volume_option = "volume=" + self.mplayer_volume # get speaker from profile if self.track_params["audio-speaker"] <> "": self.audio_speaker = self.track_params["audio-speaker"] else: self.audio_speaker = self.show_params["audio-speaker"] if self.audio_speaker == "left": self.speaker_option = AudioPlayer._LEFT elif self.audio_speaker == "right": self.speaker_option = AudioPlayer._RIGHT else: self.speaker_option = AudioPlayer._STEREO # get animation instructions from profile self.animate_begin_text = self.track_params["animate-begin"] self.animate_end_text = self.track_params["animate-end"] # open the plugin Manager self.pim = PluginManager( self.show_id, self.root, self.canvas, self.show_params, self.track_params, self.pp_dir, self.pp_home, self.pp_profile, ) # create an instance of PPIO so we can create gpio events self.ppio = PPIO() # could put instance generation in play, not sure which is better. self.mplayer = mplayerDriver(self.canvas) self.tick_timer = None self.init_play_state_machine() def play(self, track, showlist, end_callback, ready_callback, enable_menu=False): # instantiate arguments self.track = track self.showlist = showlist self.end_callback = end_callback # callback when finished self.ready_callback = ready_callback # callback when ready to play self.enable_menu = enable_menu # select the sound device if self.mplayer_audio <> "": if self.mplayer_audio == "hdmi": os.system("amixer -q -c 0 cset numid=3 2") else: os.system("amixer -q -c 0 cset numid=3 1") # callback to the calling object to e.g remove egg timer. if self.ready_callback <> None: self.ready_callback() # create an instance of showmanager so we can control concurrent shows self.show_manager = ShowManager( self.show_id, self.showlist, self.show_params, self.root, self.canvas, self.pp_dir, self.pp_profile, self.pp_home, ) # Control other shows at beginning reason, message = self.show_manager.show_control(self.track_params["show-control-begin"]) if reason == "error": self.mon.err(self, message) self.end_callback(reason, message) self = None else: # display image and text reason, message = self.display_content() if reason == "error": self.mon.err(self, message) self.end_callback(reason, message) self = None else: # create animation events reason, message = self.ppio.animate(self.animate_begin_text, id(self)) if reason == "error": self.mon.err(self, message) self.end_callback(reason, message) self = None else: # start playing the track. if self.duration_limit <> 0: self.start_play_state_machine() else: self.tick_timer = self.canvas.after(10, self.end_zero) def end_zero(self): self.end("normal", "zero duration") def terminate(self, reason): """ terminate the player in special circumstances normal user termination if by key_pressed 'stop' reason will be killed or error """ # circumvents state machine to terminate lower level and then itself. if self.mplayer <> None: self.mon.log(self, "sent terminate to mplayerdriver") self.mplayer.terminate(reason) self.end("killed", " end without waiting for mplayer to finish") # end without waiting else: self.mon.log(self, "terminate, mplayerdriver not running") self.end("killed", "terminate, mplayerdriver not running") def get_links(self): return self.track_params["links"] def input_pressed(self, symbol): if symbol[0:6] == "mplay-": self.control(symbol[6]) elif symbol == "pause": self.pause() elif symbol == "stop": self.stop() # *************************************** # INTERNAL FUNCTIONS # *************************************** # toggle pause def pause(self): if self.play_state in (AudioPlayer._PLAYING, AudioPlayer._ENDING) and self.track <> "": self.mplayer.pause() return True else: self.mon.log(self, "!<pause rejected") return False # other control when playing, not currently used def control(self, char): if self.play_state == AudioPlayer._PLAYING and self.track <> "" and char not in ("q"): self.mon.log(self, "> send control to mplayer: " + char) self.mplayer.control(char) return True else: self.mon.log(self, "!<control rejected") return False # respond to normal stop def stop(self): # send signal to stop the track to the state machine self.mon.log(self, ">stop received") self.quit_signal = True # *************************************** # sequencing # *************************************** """self. play_state controls the playing sequence, it has the following values. I am not entirely sure the starting and ending states are required. - _closed - the mplayer process is not running, mplayer process can be initiated - _starting - mplayer process is running but is not yet able to receive controls - _playing - playing a track, controls can be sent - _ending - mplayer is doing its termination, controls cannot be sent """ def init_play_state_machine(self): self.quit_signal = False self.play_state = AudioPlayer._CLOSED def start_play_state_machine(self): # initialise all the state machine variables self.duration_count = 0 self.quit_signal = False # signal that user has pressed stop # play the track options = ( self.show_params["mplayer-other-options"] + "-af " + self.speaker_option + "," + self.volume_option + " " ) if self.track <> "": self.mplayer.play(self.track, options) self.mon.log(self, "Playing track from show Id: " + str(self.show_id)) self.play_state = AudioPlayer._STARTING else: self.play_state = AudioPlayer._PLAYING # and start polling for state changes and count duration self.tick_timer = self.canvas.after(50, self.play_state_machine) def play_state_machine(self): self.duration_count += 1 if self.play_state == AudioPlayer._CLOSED: self.mon.log(self, " State machine: " + self.play_state) return elif self.play_state == AudioPlayer._STARTING: self.mon.log(self, " State machine: " + self.play_state) # if mplayer is playing the track change to play state if self.mplayer.start_play_signal == True: self.mon.log(self, " <start play signal received from mplayer") self.mplayer.start_play_signal = False self.play_state = AudioPlayer._PLAYING self.mon.log(self, " State machine: mplayer_playing started") self.tick_timer = self.canvas.after(50, self.play_state_machine) elif self.play_state == AudioPlayer._PLAYING: # self.mon.log(self," State machine: " + self.play_state) # service any queued stop signals if self.quit_signal == True or (self.duration_limit > 0 and self.duration_count > self.duration_limit): self.mon.log(self, " Service stop required signal or timeout") # self.quit_signal=False if self.track <> "": self.stop_mplayer() self.play_state = AudioPlayer._ENDING else: self.play_state = AudioPlayer._CLOSED self.end("normal", "stop required signal or timeout") # mplayer reports it is terminating so change to ending state if self.track <> "" and self.mplayer.end_play_signal: self.mon.log(self, " <end play signal received") self.mon.log(self, " <end detected at: " + str(self.mplayer.audio_position)) self.play_state = AudioPlayer._ENDING self.tick_timer = self.canvas.after(50, self.play_state_machine) elif self.play_state == AudioPlayer._ENDING: # self.mon.log(self," State machine: " + self.play_state) # if spawned process has closed can change to closed state # self.mon.log (self," State machine : is mplayer process running? - " + str(self.mplayer.is_running())) if self.mplayer.is_running() == False: self.mon.log(self, " <mplayer process is dead") if self.quit_signal == True: self.quit_signal = False self.play_state = AudioPlayer._CLOSED self.end("normal", "quit required or timeout") elif self.duration_limit > 0 and self.duration_count < self.duration_limit: self.play_state = AudioPlayer._WAITING self.tick_timer = self.canvas.after(50, self.play_state_machine) else: self.play_state = AudioPlayer._CLOSED self.end("normal", "mplayer dead") else: self.tick_timer = self.canvas.after(50, self.play_state_machine) elif self.play_state == AudioPlayer._WAITING: # self.mon.log(self," State machine: " + self.play_state) if self.quit_signal == True or (self.duration_limit > 0 and self.duration_count > self.duration_limit): self.mon.log(self, " Service stop required signal or timeout from wait") self.quit_signal = False self.play_state = AudioPlayer._CLOSED self.end("normal", "mplayer dead") else: self.tick_timer = self.canvas.after(50, self.play_state_machine) def stop_mplayer(self): # send signal to stop the track to the state machine self.mon.log(self, " >stop mplayer received from state machine") if self.play_state == AudioPlayer._PLAYING: self.mplayer.stop() return True else: self.mon.log(self, "!<stop rejected") return False # ***************** # ending the player # ***************** def end(self, reason, message): # stop the plugin if self.track_params["plugin"] <> "": self.pim.stop_plugin() # abort the timer if self.tick_timer <> None: self.canvas.after_cancel(self.tick_timer) self.tick_timer = None if reason in ("error", "killed"): self.end_callback(reason, message) self = None else: # normal end so do show control and animation # Control concurrent shows at end reason, message = self.show_manager.show_control(self.track_params["show-control-end"]) if reason == "error": self.mon.err(self, message) self.end_callback(reason, message) self = None else: # clear events list for this track if self.track_params["animate-clear"] == "yes": self.ppio.clear_events_list(id(self)) # create animation events for ending reason, message = self.ppio.animate(self.animate_end_text, id(self)) if reason == "error": self.mon.err(self, message) self.end_callback(reason, message) self = None else: self.end_callback("normal", "track has terminated or quit") self = None # ***************** # displaying things # ***************** def display_content(self): # if self.background_file<>'' or self.show_params['show-text']<> '' or #self.track_params['track-text']<> '' or self.enable_menu== True or #self.track_params['clear-screen']=='yes': if self.track_params["clear-screen"] == "yes": self.canvas.delete("pp-content") # self.canvas.update() # background colour if self.background_colour <> "": self.canvas.config(bg=self.background_colour) if self.background_file <> "": self.background_img_file = self.complete_path(self.background_file) if not os.path.exists(self.background_img_file): self.mon.err(self, "Audio background file not found: " + self.background_img_file) self.end("error", "Audio background file not found") else: pil_background_img = PIL.Image.open(self.background_img_file) self.background = PIL.ImageTk.PhotoImage(pil_background_img) self.drawn = self.canvas.create_image( int(self.canvas["width"]) / 2, int(self.canvas["height"]) / 2, image=self.background, anchor=CENTER, tag="pp-content", ) # execute the plugin if required if self.track_params["plugin"] <> "": reason, message, self.track = self.pim.do_plugin(self.track, self.track_params["plugin"]) if reason <> "normal": return reason, message # display hint text if enabled if self.enable_menu == True: self.canvas.create_text( int(self.show_params["hint-x"]), int(self.show_params["hint-y"]), text=self.show_params["hint-text"], fill=self.show_params["hint-colour"], font=self.show_params["hint-font"], anchor=NW, tag="pp-content", ) # display show text if enabled if self.show_params["show-text"] <> "" and self.track_params["display-show-text"] == "yes": self.canvas.create_text( int(self.show_params["show-text-x"]), int(self.show_params["show-text-y"]), anchor=NW, text=self.show_params["show-text"], fill=self.show_params["show-text-colour"], font=self.show_params["show-text-font"], tag="pp-content", ) # display track text if enabled if self.track_params["track-text"] <> "": self.canvas.create_text( int(self.track_params["track-text-x"]), int(self.track_params["track-text-y"]), anchor=NW, text=self.track_params["track-text"], fill=self.track_params["track-text-colour"], font=self.track_params["track-text-font"], tag="pp-content", ) self.mon.log(self, "Displayed background and text ") self.canvas.tag_raise("pp-click-area") self.canvas.update_idletasks() return "normal", "" # ***************** # utilities # ***************** def complete_path(self, track_file): # complete path of the filename of the selected entry if track_file[0] == "+": track_file = self.pp_home + track_file[1:] self.mon.log(self, "Background image is " + track_file) return track_file
class OMXDriver(object): # adjust this to determine freeze after the first frame after_first_frame_position =-50000 # microseconds _LAUNCH_CMD = '/usr/bin/omxplayer --no-keys ' # needs changing if user has installed his own version of omxplayer elsewhere # add more keys here, see popcornmix/omxplayer github files readme.md and KeyConfig.h KEY_MAP = {'<':3,'>':4,'z':5,'j':6,'k':7,'i':8,'o':9,'n':10,'m':11,'s':12, '-': 17, '+': 18, '=':18,'x':30,'w':31} def __init__(self,widget,pp_dir): self.widget=widget self.pp_dir=pp_dir self.mon=Monitor() self.start_play_signal=False self.end_play_signal=False self.end_play_reason='nothing' self.duration=0 self.video_position=0 self.pause_at_end_required=False self.paused_at_end=False self.pause_at_end_time=0 # self.pause_before_play_required='before-first-frame' #no,before-first-frame, after-first-frame # self.pause_before_play_required='no' self.paused_at_start='False' self.paused=False self.terminate_reason='' # dbus and subprocess self._process=None self.__iface_root=None self.__iface_props = None self.__iface_player = None def load(self, track, freeze_at_start,options,caller): self.pause_before_play_required=freeze_at_start self.caller=caller track= "'"+ track.replace("'","'\\''") + "'" # self.mon.log(self,'TIME OF DAY: '+ strftime("%Y-%m-%d %H:%M")) self.dbus_user = os.environ["USER"] self.id=str(int(time()*10)) self.dbus_name = "org.mpris.MediaPlayer2.omxplayer"+self.id self.omxplayer_cmd = OMXDriver._LAUNCH_CMD + options + " --dbus_name '"+ self.dbus_name + "' " + track # self.mon.log(self, 'dbus user ' + self.dbus_user) # self.mon.log(self, 'dbus name ' + self.dbus_name) # print self.omxplayer_cmd self.mon.log(self, "Send command to omxplayer: "+ self.omxplayer_cmd) # self._process=subprocess.Popen(self.omxplayer_cmd,shell=True,stdout=file('/home/pi/pipresents/pp_logs/stdout.txt','a'),stderr=file('/home/pi/pipresents/pp_logs/stderr.txt','a')) self._process=subprocess.Popen(self.omxplayer_cmd,shell=True,stdout=file('/dev/null','a'),stderr=file('/dev/null','a')) self.pid=self._process.pid # wait for omxplayer to start then start monitoring thread self.dbus_tries = 0 self.omx_loaded = False self._wait_for_dbus() return def _wait_for_dbus(self): connect_success=self.__dbus_connect() if connect_success is True: # print 'SUCCESS' self.mon.log(self,'connected to omxplayer dbus after ' + str(self.dbus_tries) + ' centisecs') # get duration of the track in microsecs if fails return a very large duration # posibly faile because omxplayer is running but not omxplayer.bin duration_success,duration=self.get_duration() if duration_success is False: self.mon.warn(self,'get duration failed for n attempts using '+ str(duration/60000000)+ ' minutes') # calculate time to pause before last frame self.duration = duration self.pause_at_end_time = duration - 350000 # start the thread that is going to monitor output from omxplayer. self._monitor_status() else: self.dbus_tries+=1 self.widget.after(100,self._wait_for_dbus) def _monitor_status(self): # print '\n',self.id, '** STARTING ',self.duration self.start_play_signal=False self.end_play_signal=False self.end_play_reason='nothing' self.paused_at_end=False self.paused_at_start='False' self.delay = 50 self.widget.after(0,self._status_loop) """ freeze at start 'no' - unpause in show - test !=0 'before_first_frame' - don't unpause in show, test !=0 'after_first_frame' - don't unpause in show, test > -100000 """ def _status_loop(self): if self.is_running() is False: # process is not running because quit or natural end - seems not to happen self.end_play_signal=True self.end_play_reason='nice_day' # print ' send nice day - process not running' return else: success, video_position = self.get_position() # if video_position <= 0: print 'read position',video_position if success is False: # print 'send nice day - exception when reading video position' self.end_play_signal=True self.end_play_reason='nice_day' return else: self.video_position=video_position # if timestamp is near the end then pause if self.pause_at_end_required is True and self.video_position>self.pause_at_end_time: #microseconds # print 'pausing at end, leeway ',self.duration - self.video_position pause_end_success = self.pause(' at end of track') if pause_end_success is True: # print self.id,' pause for end success', self.video_position self.paused_at_end=True self.end_play_signal=True self.end_play_reason='pause_at_end' return else: print 'pause at end failed, probably because of delay after detection, just run on' self.widget.after(self.delay,self._status_loop) else: # need to do the pausing for preload after first timestamp is received 0 is default value before start # print self.pause_before_play_required,self.paused_at_start,self.video_position,OMXDriver.after_first_frame_position if (self.pause_before_play_required == 'after-first-frame' and self.paused_at_start == 'False' and self.video_position >OMXDriver.after_first_frame_position)\ or(self.pause_before_play_required != 'after-first-frame' and self.paused_at_start == 'False' and self.video_position !=0): pause_after_load_success=self.pause('after load') if pause_after_load_success is True: # print self.id,' pause after load success',self.video_position self.start_play_signal = True self.paused_at_start='True' else: # should never fail, just warn at the moment # print 'pause after load failed ' + str(self.video_position) self.mon.warn(self, str(self.id)+ ' pause after load fail ' + str(self.video_position)) self.widget.after(self.delay,self._status_loop) else: self.widget.after(self.delay,self._status_loop) def show(self,freeze_at_end_required,initial_volume): self.initial_volume=initial_volume self.pause_at_end_required=freeze_at_end_required # unpause to start playing if self.pause_before_play_required =='no': unpause_show_success=self.unpause(' to start showing') # print 'unpause for show',self.paused if unpause_show_success is True: pass # print self.id,' unpause for show success', self.video_position else: # should never fail, just warn at the moment self.mon.warn(self, str(self.id)+ ' unpause for show fail ' + str(self.video_position)) def control(self,char): val = OMXDriver.KEY_MAP[char] self.mon.log(self,'>control received and sent to omxplayer ' + str(self.pid)) if self.is_running(): try: self.__iface_player.Action(dbus.Int32(val)) except dbus.exceptions.DBusException as ex: self.mon.warn(self,'Failed to send control - dbus exception: {}'.format(ex.get_dbus_message())) return else: self.mon.warn(self,'Failed to send control - process not running') return # USE ONLY at end and after load # return succces of the operation, several tries if pause did not work and no error reported. def pause(self,reason): self.mon.log(self,'pause received '+reason) if self.paused is False: self.mon.log(self,'not paused so send pause '+reason) tries=1 while True: if self.send_pause() is False: # failed for good reason return False status=self.omxplayer_is_paused() # test omxplayer after sending the command if status == 'Paused': self.paused = True return True if status == 'Failed': # failed for good reason because of exception or process not running caused by end of track return False else: # failed for no good reason self.mon.warn(self, '!!!!! repeat pause ' + str(tries)) # print self.id,' !!!!! repeat pause ',self.video_position, tries tries +=1 if tries >5: # print self.id, ' pause failed for n attempts' self.mon.warn(self,'pause failed for n attempts') return False # repeat # USE ONLY for show def unpause(self,reason): self.mon.log(self,'Unpause received '+ reason) if self.paused is True: self.mon.log(self,'Is paused so Track will be unpaused '+ reason) tries=1 while True: if self.send_unpause() is False: return False status = self.omxplayer_is_paused() # test omxplayer if status == 'Playing': self.paused = False self.paused_at_start='done' self.set_volume(self.initial_volume) return True if status == 'Failed': # failed for good reason because of exception or process not running caused by end of track return False else: # self.mon.warn(self, '!!!!! repeat unpause ' + str(tries)) # print self.id,' !!!! repeat unpause ',self.video_position, tries tries +=1 if tries >200: # print self.id, ' unpause failed for n attempts' self.mon.warn(self,'unpause failed for 200 attempts') return False def omxplayer_is_paused(self): if self.is_running(): try: result=self.__iface_props.PlaybackStatus() except dbus.exceptions.DBusException as ex: self.mon.warn(self,'Failed to test paused - dbus exception: {}'.format(ex.get_dbus_message())) return 'Failed' return result else: self.mon.warn(self,'Failed to test paused - process not running') # print self.id,' test paused not successful - process' return 'Failed' def send_pause(self): if self.is_running(): try: self.__iface_player.Pause() except dbus.exceptions.DBusException as ex: self.mon.warn(self,'Failed to send pause - dbus exception: {}'.format(ex.get_dbus_message())) return False return True else: self.mon.warn(self,'Failed to send pause - process not running') # print self.id,' send pause not successful - process' return False def send_unpause(self): if self.is_running(): try: self.__iface_player.Action(16) except dbus.exceptions.DBusException as ex: self.mon.warn(self,'Failed to send unpause - dbus exception: {}'.format(ex.get_dbus_message())) return False return True else: self.mon.warn(self,'Failed to send unpause - process not running') # print self.id,' send unpause not successful - process' return False def pause_on(self): self.mon.log(self,'pause on received ') # print 'pause on',self.paused if self.paused is True: return if self.is_running(): try: # self.__iface_player.Action(16) self.__iface_player.Pause() # - this should work but does not!!! self.paused=True # print 'paused OK' return except dbus.exceptions.DBusException as ex: self.mon.warn(self,'Failed to do pause on - dbus exception: {}'.format(ex.get_dbus_message())) return else: self.mon.warn(self,'Failed to do pause on - process not running') return def pause_off(self): self.mon.log(self,'pause off received ') # print 'pause off',self.paused if self.paused is False: return if self.is_running(): try: self.__iface_player.Action(16) self.paused=False # print 'not paused OK' return except dbus.exceptions.DBusException as ex: self.mon.warn(self,'Failed to do pause off - dbus exception: {}'.format(ex.get_dbus_message())) return else: self.mon.warn(self,'Failed to do pause off - process not running') return def toggle_pause(self,reason): self.mon.log(self,'toggle pause received '+ reason) if self.is_running(): try: self.__iface_player.Action(16) if not self.paused: self.paused = True else: self.paused=False except dbus.exceptions.DBusException as ex: self.mon.warn(self,'Failed to toggle pause - dbus exception: {}'.format(ex.get_dbus_message())) return else: self.mon.warn(self,'Failed to toggle pause - process not running') return def go(self): self.mon.log(self,'go received ') self.unpause('for go') def mute(self): self.__iface_player.Mute() def unmute(self): self.__iface_player.Unmute() def set_volume(self,millibels): volume = pow(10, millibels / 2000.0); self.__iface_props.Volume(volume) def stop(self): self.mon.log(self,'>stop received and quit sent to omxplayer ' + str(self.pid)) # need to send 'nice day' if self.paused_at_end is True: self.end_play_signal=True self.end_play_reason='nice_day' # print 'send nice day for close track' if self.is_running(): try: self.__iface_root.Quit() except dbus.exceptions.DBusException as ex: self.mon.warn(self,'Failed to quit - dbus exception: {}'.format(ex.get_dbus_message())) return else: self.mon.warn(self,'Failed to quit - process not running') return # kill the subprocess (omxplayer and omxplayer.bin). Used for tidy up on exit. def terminate(self,reason): self.terminate_reason=reason self.stop() def get_terminate_reason(self): return self.terminate_reason # test of whether _process is running def is_running(self): retcode=self._process.poll() # print 'is alive', retcode if retcode is None: return True else: return False # kill off omxplayer when it hasn't terminated at the end of a track. # send SIGINT (CTRL C) so it has a chance to tidy up daemons and omxplayer.bin def kill(self): if self.is_running()is True: self._process.send_signal(signal.SIGINT) def get_position(self): # don't test process as is done just before try: micros = self.__iface_props.Position() return True,micros except dbus.exceptions.DBusException as ex: # print 'Failed get_position - dbus exception: {}'.format(ex.get_dbus_message()) return False,-1 def get_duration(self): tries=1 while True: success,duration=self._try_duration() if success is True: return True,duration else: self.mon.warn(self, 'repeat get duration ' + str(tries)) tries +=1 if tries >5: return False,sys.maxint*100 def _try_duration(self): """Return the total length of the playing media""" if self.is_running() is True: try: micros = self.__iface_props.Duration() return True,micros except dbus.exceptions.DBusException as ex: self.mon.warn(self,'Failed get duration - dbus exception: {}'.format(ex.get_dbus_message())) return False,-1 else: return False,-1 # ********************* # connect to dbus # ********************* def __dbus_connect(self): if self.omx_loaded is False: # read the omxplayer dbus data from files generated by omxplayer bus_address_filename = "/tmp/omxplayerdbus.{}".format(self.dbus_user) bus_pid_filename = "/tmp/omxplayerdbus.{}.pid".format(self.dbus_user) if not os.path.exists(bus_address_filename): self.mon.log(self, 'waiting for bus address file ' + bus_address_filename) self.omx_loaded=False return False else: f = open(bus_address_filename, "r") bus_address = f.read().rstrip() if bus_address == '': self.mon.log(self, 'waiting for bus address in file ' + bus_address_filename) self.omx_loaded=False return False else: # self.mon.log(self, 'bus address found ' + bus_address) if not os.path.exists(bus_pid_filename): self.mon.warn(self, 'bus pid file does not exist ' + bus_pid_filename) self.omx_loaded=False return False else: f= open(bus_pid_filename, "r") bus_pid = f.read().rstrip() if bus_pid == '': self.omx_loaded=False return False else: # self.mon.log(self, 'bus pid found ' + bus_pid) os.environ["DBUS_SESSION_BUS_ADDRESS"] = bus_address os.environ["DBUS_SESSION_BUS_PID"] = bus_pid self.omx_loaded = True if self.omx_loaded is True: session_bus = dbus.SessionBus() try: omx_object = session_bus.get_object(self.dbus_name, "/org/mpris/MediaPlayer2", introspect=False) self.__iface_root = dbus.Interface(omx_object, "org.mpris.MediaPlayer2") self.__iface_props = dbus.Interface(omx_object, "org.freedesktop.DBus.Properties") self.__iface_player = dbus.Interface(omx_object, "org.mpris.MediaPlayer2.Player") except dbus.exceptions.DBusException as ex: # self.mon.log(self,"Waiting for dbus connection to omxplayer: {}".format(ex.get_dbus_message())) return False return True
class IOPluginManager(object): plugins=[] def __init__(self): self.mon=Monitor() def init(self,pp_dir,pp_profile,widget,callback): self.pp_dir=pp_dir self.pp_profile=pp_profile IOPluginManager.plugins=[] if os.path.exists(self.pp_profile+os.sep+'pp_io_config'): # read the .cfg files in /pp_io_config in profile registring the I/O plugin for cfgfile in os.listdir(self.pp_profile+os.sep+'pp_io_config'): if cfgfile in ('screen.cfg','osc.cfg'): continue cfgfilepath = self.pp_profile+os.sep+'pp_io_config'+os.sep+cfgfile status,message=self.init_config(cfgfile,cfgfilepath,widget,callback) if status == 'error': return status,message #read .cfg file in /pipresents/pp_io_config if file not present in profile then use this one for cfgfile in os.listdir(self.pp_dir+os.sep+'pp_io_config'): if cfgfile in ('screen.cfg','osc.cfg'): continue if not os.path.exists(self.pp_profile+os.sep+'pp_io_config'+os.sep+cfgfile): cfgfilepath=self.pp_dir+os.sep+'pp_io_config'+os.sep+cfgfile status,message=self.init_config(cfgfile,cfgfilepath,widget,callback) if status == 'error': return status,message # print IOPluginManager.plugins return 'normal','I/O Plugins registered' def init_config(self,cfgfile,cfgfilepath,widget,callback): # print cfgfile,cfgfilepath reason,message,config=self._read(cfgfile,cfgfilepath) if reason =='error': self.mon.err(self,'Failed to read '+cfgfile + ' ' + message) return 'error','Failed to read '+cfgfile + ' ' + message if config.has_section('DRIVER') is False: self.mon.err(self,'No DRIVER section in '+cfgfilepath) return 'error','No DRIVER section in '+cfgfilepath entry = dict() #read information from DRIVER section entry['title']=config.get('DRIVER','title') if config.get('DRIVER','enabled')=='yes': driver_name=config.get('DRIVER','module') driver_path=self.pp_dir+os.sep+'pp_io_plugins'+os.sep+driver_name+'.py' if not os.path.exists(driver_path): self.mon.err(self,driver_name + ' Driver not found in ' + driver_path) return 'error',driver_name + ' Driver not found in ' + driver_path instance = self._load_plugin_file(driver_name,self.pp_dir+os.sep+'pp_io_plugins') reason,message=instance.init(cfgfile,cfgfilepath,widget,callback) if reason=='warn': self.mon.warn(self,message) return 'error',message if reason=='error': self.mon.warn(self,message) return 'error',message entry['instance']=instance self.mon.log(self,message) IOPluginManager.plugins.append(entry) return 'normal','I/O Plugins registered' def start(self): for entry in IOPluginManager.plugins: plugin=entry['instance'] if plugin.is_active() is True: plugin.start() def terminate(self): for entry in IOPluginManager.plugins: plugin=entry['instance'] if plugin.is_active() is True: plugin.terminate() self.mon.log(self,'I/O plugin '+entry['title']+ ' terminated') def get_input(self,key): for entry in IOPluginManager.plugins: plugin=entry['instance'] # print 'trying ',entry['title'],plugin.is_active() if plugin.is_active() is True: found,value = plugin.get_input(key) if found is True: return found,value # key not found in any plugin return False,None def handle_output_event(self,name,param_type,param_values,req_time): for entry in IOPluginManager.plugins: plugin=entry['instance'] # print 'trying ',entry['title'],name,param_type,plugin.is_active() if plugin.is_active() is True: reason,message= plugin.handle_output_event(name,param_type,param_values,req_time) if reason == 'error': # self.mon.err(self,message) return 'error',message else: self.mon.log(self,message) return 'normal','output scan complete' def _load_plugin_file(self, name, driver_dir): fp, pathname,description = imp.find_module(name,[driver_dir]) module_id = imp.load_module(name,fp,pathname,description) plugin_class = getattr(module_id,name) return plugin_class() def _read(self,filename,filepath): if os.path.exists(filepath): config = ConfigParser.ConfigParser() config.read(filepath) self.mon.log(self,filename+" read from "+ filepath) return 'normal',filename+' read',config else: return 'error',filename+' not found at: '+filepath,None
class Network(object): ip='' interface='' def __init__(self): self.mon=Monitor() def get_ip(self): return Network.interface, Network.ip def set_ip(self, interface,ip): Network.interface = interface Network.ip = ip def read_config(self,options_file_path): config=ConfigParser.ConfigParser() config.read(options_file_path) self.preferred_interface=config.get('network','preferred_interface',0) self.unit=config.get('network','unit',0) self.force_ip=config.get('network','force_ip',0) self.manager_port=int(config.get('manager','port',0)) self.manager_username=config.get('manager','username',0) self.manager_password=config.get('manager','password',0) self.editor_username=config.get('editor','username',0) self.editor_password=config.get('editor','password',0) self.editor_port=int(config.get('editor','port',0)) def wait_for_network(self,tries): i=0 while True: if self.is_connected() is True: return True else: i+=1 self.mon.log(self, 'Trying to connect to network ' + str(i)) if i>tries: return False sleep(1) def is_connected(self): interfaces,ips = self.get_ips() if interfaces == 0: return False else: return True def get_ips(self): arg='ip route list' p=subprocess.Popen(arg,shell=True,stdout=subprocess.PIPE) data = p.communicate() # Split data [0] obtained from stdout into lines # print ' data is',data[0] ip_lines = data[0].splitlines() if len(ip_lines) == 0: # print 'not connected' return 0, [] interfaces=0 result=[] for line in ip_lines: fields= line.split() if fields[0] != 'default': interface= fields[fields.index('dev')+1] ip = fields[fields.index('src')+1] interfaces +=1 result=result + [[interface,ip]] return interfaces, result def get_preferred_ip(self): # returned ip_type # none available - return '' # 1 available - return it # 2 or more available - return one that matches or if none match first found if self.force_ip.strip() != '': return self.preferred_interface,self.force_ip number, interfaces=self.get_ips() # print interfaces,ips if number == 0: return '','' elif number == 1: #if one interface return only i_type = interfaces[0][0] ip = interfaces[0][1] return i_type,ip else: i_type='' for interface in interfaces: if interface[0] == self.preferred_interface: i_type=interface[0] ip=interface[1] return i_type,ip # nothing matches preferred so use first one i_type=interfaces[0][0] ip=interfaces[0][1] return i_type,ip
class PPIO: """ PPIO provides some IO facilties for Pi presents - configures GPIO pins from data in gpio.cfg - reads and debounces inputs pins, provides callbacks on state changes which are used to trigger mediashows - for output pins allows players to put events, which request the change of state of pins, into a queue. Events are executed at the required time. """ # constants for buttons # cofiguration from gpio.cfg PIN=0 # pin on RPi board GPIO connector e.g. P1-11 DIRECTION = 1 # IN/OUT/NONE (None is not used) NAME = 2 # name for output RISING_NAME=3 # name for rising edge callback FALLING_NAME=4 # name ofr falling edge callback ONE_NAME=5 # name for one state callback ZERO_NAME = 6 # name for zero state callback REPEAT = 7 #reperat interval for state callbacks (mS) THRESHOLD = 8 # threshold of debounce count for state change to be considered PULL = 9 # pull up or down or none # dynamic data COUNT=10 # variable - count of the number of times the input has been 0 (limited to threshold) PRESSED = 11 # variable - debounced state LAST = 12 # varible - last state - used to detect edge REPEAT_COUNT = 13 TEMPLATE = ['', #pin '', # direction '', #name '','','','', #input names 0, # repeat 0, #threshold '', #pull 0,False,False,0] #dynamics PINLIST = ('P1-03','P1-05','P1-07','P1-08', 'P1-10','P1-11','P1-12','P1-13','P1-15','P1-16','P1-18','P1-19', 'P1-21','P1-22','P1-23','P1-24','P1-26') # index of shutdown pin SHUTDOWN_INDEX=0 # constants for sequencer SEQUENCER_PIN = 0 # GPIO pin number, the xx in P1-xx SEQUENCER_TO_STATE = 1 # False = off , True =on SEQUENCER_TIME = 2 # time since the epoch in seconds SEQUENCER_TAG = 3 # tag used to delete all matching event, usually a track reference. # CLASS VARIABLES events=[] pins=[] last_poll_time=0 options=None # gpio_enabled=False EVENT_TEMPLATE=[0,False,0,None] #executed by main program and by each object using gpio def __init__(self): self.mon=Monitor() self.mon.on() self.options=command_options() # executed once from main program def init(self,pp_dir,pp_home,pp_profile,widget,button_tick,callback=None): # instantiate arguments self.widget=widget self.pp_dir=pp_dir self.pp_profile=pp_profile self.pp_home=pp_home self.button_tick=button_tick self.callback=callback PPIO.SHUTDOWN_INDEX=0 # read gpio.cfg file. if self.read(self.pp_dir,self.pp_home,self.pp_profile)==False: return False import RPi.GPIO as GPIO self.GPIO = GPIO #construct the GPIO control list from the configuration for index, pin_def in enumerate(PPIO.PINLIST): pin=copy.deepcopy(PPIO.TEMPLATE) pin_bits = pin_def.split('-') pin_num=pin_bits[1:] pin[PPIO.PIN]=int(pin_num[0]) if self.config.has_section(pin_def)==False: self.mon.log(self, "no pin definition for "+ pin_def) pin[PPIO.DIRECTION]='None' else: # unused pin if self.config.get(pin_def,'direction')=='none': pin[PPIO.DIRECTION]='none' else: pin[PPIO.DIRECTION]=self.config.get(pin_def,'direction') if pin[PPIO.DIRECTION]=='in': # input pin pin[PPIO.RISING_NAME]=self.config.get(pin_def,'rising-name') pin[PPIO.FALLING_NAME]=self.config.get(pin_def,'falling-name') pin[PPIO.ONE_NAME]=self.config.get(pin_def,'one-name') pin[PPIO.ZERO_NAME]=self.config.get(pin_def,'zero-name') if pin[PPIO.FALLING_NAME]=='pp-shutdown': PPIO.SHUTDOWN_INDEX=index if self.config.get(pin_def,'repeat')<>'': pin[PPIO.REPEAT]=int(self.config.get(pin_def,'repeat')) else: pin[PPIO.REPEAT]=-1 pin[PPIO.THRESHOLD]=int(self.config.get(pin_def,'threshold')) if self.config.get(pin_def,'pull-up-down')=='up': pin[PPIO.PULL]=GPIO.PUD_UP elif self.config.get(pin_def,'pull-up-down')=='down': pin[PPIO.PULL]=GPIO.PUD_DOWN else: pin[PPIO.PULL]=GPIO.PUD_OFF else: # output pin pin[PPIO.NAME]=self.config.get(pin_def,'name') # print pin PPIO.pins.append(copy.deepcopy(pin)) # setup GPIO self.GPIO.setwarnings(False) self.GPIO.setmode(self.GPIO.BOARD) # set up the GPIO inputs and outputs for index, pin in enumerate(PPIO.pins): num = pin[PPIO.PIN] if pin[PPIO.DIRECTION]=='in': self.GPIO.setup(num,self.GPIO.IN,pull_up_down=pin[PPIO.PULL]) elif pin[PPIO.DIRECTION]=='out': self.GPIO.setup(num,self.GPIO.OUT) self.GPIO.setup(num,False) self.reset_inputs() PPIO.gpio_enabled=True #init timer self.button_tick_timer=None PPIO.last_scheduler_time=long(time.time()) return True # called by main program only def poll(self): # look at the buttons self.do_buttons() # kick off output pin sequencer poll_time=long(time.time()) # is current time greater than last time the sceduler was run (previous second or more) # run in a loop to catch up because root.after can get behind when images are being rendered etc. while PPIO.last_scheduler_time<=poll_time: self.do_sequencer(PPIO.last_scheduler_time) PPIO.last_scheduler_time +=1 # and loop self.button_tick_timer=self.widget.after(self.button_tick,self.poll) # called by main program only def terminate(self): if self.button_tick_timer<>None: self.widget.after_cancel(self.button_tick_timer) self.clear_events_list(None) self.reset_outputs() self.GPIO.cleanup() # ************************************************ # gpio input functions # called by main program only # ************************************************ def reset_inputs(self): for pin in PPIO.pins: pin[PPIO.COUNT]=0 pin[PPIO.PRESSED]==False pin[PPIO.LAST]==False pin[PPIO.REPEAT_COUNT]=pin[PPIO.REPEAT] # index is of the pins array, provided by the callback ***** needs to be name def shutdown_pressed(self): if PPIO.SHUTDOWN_INDEX<>0: return PPIO.pins[PPIO.SHUTDOWN_INDEX][PPIO.PRESSED] else: return False def do_buttons(self): for index, pin in enumerate(PPIO.pins): if pin[PPIO.DIRECTION]=='in': # debounce if self.GPIO.input(pin[PPIO.PIN])==0: if pin[PPIO.COUNT]<pin[PPIO.THRESHOLD]: pin[PPIO.COUNT]+=1 if pin[PPIO.COUNT]==pin[PPIO.THRESHOLD]: pin[PPIO.PRESSED]=True else: # input us 1 if pin[PPIO.COUNT]>0: pin[PPIO.COUNT]-=1 if pin[PPIO.COUNT]==0: pin[PPIO.PRESSED]=False #detect edges # falling edge if pin[PPIO.PRESSED]==True and pin[PPIO.LAST]==False: pin[PPIO.LAST]=pin[PPIO.PRESSED] pin[PPIO.REPEAT_COUNT]=pin[PPIO.REPEAT] if pin[PPIO.FALLING_NAME]<>'' and self.callback <> None: self.callback(index, pin[PPIO.FALLING_NAME],"falling") #rising edge if pin[PPIO.PRESSED]==False and pin[PPIO.LAST]==True: pin[PPIO.LAST]=pin[PPIO.PRESSED] pin[PPIO.REPEAT_COUNT]=pin[PPIO.REPEAT] if pin[PPIO.RISING_NAME]<>'' and self.callback <> None: self.callback(index, pin[PPIO.RISING_NAME],"rising") # do state callbacks if pin[PPIO.REPEAT_COUNT]==0: if pin[PPIO.ZERO_NAME]<>'' and pin[PPIO.PRESSED]==True and self.callback<>None: self.callback(index, pin[PPIO.ZERO_NAME],"zero") if pin[PPIO.ONE_NAME]<>'' and pin[PPIO.PRESSED]==False and self.callback<>None: self.callback(index, pin[PPIO.ONE_NAME],"zero") pin[PPIO.REPEAT_COUNT]=pin[PPIO.REPEAT] else: if pin[PPIO.REPEAT]<>-1: pin[PPIO.REPEAT_COUNT]-=1 # ************************************************ # gpio output sequencer functions # ************************************************ # execute events at the appropriate time and remove from list (runs from main program only) # runs through list a number of times because of problems with pop messing up list def do_sequencer(self,schedule_time): # print 'sequencer run for: ' + str(schedule_time) + ' at ' + str(long(time.time())) while True: event_found=False for index, item in enumerate(PPIO.events): if item[PPIO.SEQUENCER_TIME]<=schedule_time: event=PPIO.events.pop(index) event_found=True self.do_event(event[PPIO.SEQUENCER_PIN],event[PPIO.SEQUENCER_TO_STATE],item[PPIO.SEQUENCER_TIME]) break if event_found==False: break # execute an event def do_event(self,pin,to_state,req_time): self.mon.log (self,'pin P1-'+ str(pin)+ ' set '+ str(to_state) + ' required: ' + str(req_time)+ ' actual: ' + str(long(time.time()))) # print 'pin P1-'+ str(pin)+ ' set '+ str(to_state) + ' required: ' + str(req_time)+ ' actual: ' + str(long(time.time())) self.GPIO.output(pin,to_state) # ************************************************ # gpio output sequencer interface methods # these can be called from many classes so need to operate on class variables # ************************************************ def animate(self,text,tag): if self.options['gpio']==True: lines = text.split("\n") for line in lines: error_text=self.parse_animate_fields(line,tag) if error_text <>'': return 'error',error_text return 'normal','' return 'normal','' # clear event list def clear_events_list(self,tag): if self.options['gpio']==True: self.mon.log(self,'clear events list ') # empty event list if tag==None: PPIO.events=[] else: self.remove_events(tag) def reset_outputs(self): if self.options['gpio']==True: self.mon.log(self,'reset outputs') for index, pin in enumerate(PPIO.pins): num = pin[PPIO.PIN] if pin[PPIO.DIRECTION]=='out': self.GPIO.output(num,False) # ************************************************ # internal functions # these can be called from many classes so need to operate on class variables # ************************************************ def parse_animate_fields(self,line,tag): fields= line.split() if len(fields)==0: return '' name=fields[0] pin= self.pin_of(name) if pin ==-1: return 'Unknown gpio logical output in: ' + line to_state_text=fields[1] if not (to_state_text in ('on','off')): return 'Illegal to-state in : '+ line if to_state_text == 'on': to_state=True else: to_state=False if len(fields)==2: delay_text='0' else: delay_text=fields[2] if not delay_text.isdigit(): return 'Delay is not an integer in : '+ line delay=int(delay_text) self.add_event(pin,to_state,delay,tag) # self.print_events() return '' def pin_of(self,name): for pin in PPIO.pins: # print " in list" + pin[PPIO.NAME] + str(pin[PPIO.PIN] ) if pin[PPIO.NAME]==name and pin[PPIO.DIRECTION]=='out': return pin[PPIO.PIN] return -1 def print_events(self): print for i in PPIO.events: print i def add_event(self,sequencer_pin,sequencer_to_state,sequencer_time,sequencer_tag): poll_time=long(time.time()) # delay is 0 so just do it, don't queue it. #if sequencer_time == 0: #print "firing now",poll_time #self.do_event(sequencer_pin,sequencer_to_state,poll_time) #return # prepare the event event=PPIO.EVENT_TEMPLATE event[PPIO.SEQUENCER_PIN]=sequencer_pin event[PPIO.SEQUENCER_TO_STATE]=sequencer_to_state event[PPIO.SEQUENCER_TIME]=sequencer_time+poll_time+1 event[PPIO.SEQUENCER_TAG]=sequencer_tag # print event # find the place in the events list and insert # first item in the list is earliest, if two have the same time then last to be added is fired last. abs_time=sequencer_time+poll_time copy_event= copy.deepcopy(event) for index, item in enumerate(PPIO.events): if abs_time<item[PPIO.SEQUENCER_TIME]: PPIO.events.insert(index,copy_event) return copy_event PPIO.events.append(copy_event) return copy_event # remove an event not used and does not work def remove_event(self,event): for index, item in enumerate(PPIO.events): if event==item: del PPIO.events[index] return True return False # remove all the events with the same tag, usually a track reference def remove_events(self,tag): left=[] for item in PPIO.events: if tag<>item[PPIO.SEQUENCER_TAG]: left.append(item) PPIO.events= left #self.print_events() # *********************************** # reading gpio.cfg functions # ************************************ def read(self,pp_dir,pp_home,pp_profile): # try inside profile tryfile=pp_profile+os.sep+"gpio.cfg" # self.mon.log(self,"Trying gpio.cfg in profile at: "+ tryfile) if os.path.exists(tryfile): filename=tryfile else: # try inside pp_home # self.mon.log(self,"gpio.cfg not found at "+ tryfile+ " trying pp_home") tryfile=pp_home+os.sep+"gpio.cfg" if os.path.exists(tryfile): filename=tryfile else: # try inside pipresents # self.mon.log(self,"gpio.cfg not found at "+ tryfile + " trying inside pipresents") tryfile=pp_dir+os.sep+'pp_home'+os.sep+"gpio.cfg" if os.path.exists(tryfile): filename=tryfile else: self.mon.log(self,"gpio.cfg not found at "+ tryfile) self.mon.err(self,"gpio.cfg not found") return False self.config = ConfigParser.ConfigParser() self.config.read(filename) self.mon.log(self,"gpio.cfg read from "+ filename) return True
class KbdDriver: config=None def __init__(self): self.mon=Monitor() self.mon.on() # sets up tkinter keyboard events such that any key press # does a callback to 'callback' with the event object and a symbolic name. def bind_keys(self,widget,callback): for option in KbdDriver.config.items('keys'): condition=option[0] symbolic_name=option[1] # print condition,symbolic_name widget.bind(condition, lambda event, name=symbolic_name: self.specific_key(callback,name)) # bind all the normal keys that return a printing character such that x produces pp-key-x widget.bind("<Key>", lambda event : self.normal_key(callback,event)) ## # bind special keys to the specified symbolic name ## widget.bind("<Break>", lambda event, name='pp-exit': self.specific_key(callback,name)) ## widget.bind("<Escape>", lambda event, name='pp-stop': self.specific_key(callback,name)) ## widget.bind("<Up>", lambda event, name='pp-up': self.specific_key(callback,name)) ## widget.bind("<Down>", lambda event, name='pp-down': self.specific_key(callback,name)) ## widget.bind("<Return>", lambda event, name='pp-play': self.specific_key(callback,name)) ## def specific_key(self,callback,name): callback(name,'front','key') # alphanumeric keys- convert to symbolic by adding pp-key- def normal_key(self,callback,event): key=event.char if key<>'': callback('pp-key-'+key,'front','key') #read the key bindings from keys.cfg def read(self,pp_dir,pp_home,pp_profile): if KbdDriver.config==None: # try inside profile tryfile=pp_profile+os.sep+"keys.cfg" # self.mon.log(self,"Trying keys.cfg in profile at: "+ tryfile) if os.path.exists(tryfile): filename=tryfile else: # try inside pp_home # self.mon.log(self,"keys.cfg not found at "+ tryfile+ " trying pp_home") tryfile=pp_home+os.sep+"keys.cfg" if os.path.exists(tryfile): filename=tryfile else: # try inside pipresents # self.mon.log(self,"keys.cfg not found at "+ tryfile + " trying inside pipresents") tryfile=pp_dir+os.sep+'pp_home'+os.sep+"keys.cfg" if os.path.exists(tryfile): filename=tryfile else: self.mon.log(self,"keys.cfg not found at "+ tryfile) self.mon.err(self,"keys.cfg not found") return False KbdDriver.config = ConfigParser.ConfigParser() KbdDriver.config.optionxform=str KbdDriver.config.read(filename) self.mon.log(self,"keys.cfg read from "+ filename) if KbdDriver.config.has_section('keys')==False: self.mon.err(self,"no [keys] section in keys.cfg") return False return True def has_section(self,section): if KbdDriver.config.has_section('keys')==False: return False
class uzblDriver(object): def __init__(self, widget): self.widget = widget self.mon = Monitor() self.mon.on() self._process = None self.fifo = "" def pause(self): pass def stop(self): self.control("exit") # kill the subprocess (uzbl). Used for tidy up on exit. def terminate(self, reason): self.terminate_reason = reason if self.exists_fifo(): self.control("exit") # self._process.close(force=True) self.end_play_signal = True def play(self, track, geometry): self.start_play_signal = False self.end_play_signal = False # track= "'"+ track.replace("'","'\\''") + "'" cmd = "uzbl-browser " + geometry + "--uri=" + track self.mon.log(self, "Send command to uzbl: " + cmd) self._process = pexpect.spawn(cmd) # uncomment to monitor output to and input from uzbl (read pexpect manual) # fout= file('/home/pi/pipresents/uzbllogfile.txt','w') #uncomment and change sys.stdout to fout to log to a file # self._process.logfile_send = sys.stdout # send just commands to stdout # self._process.logfile=fout # send all communications to log # and poll for fifo to be available self.get_fifo() # poll for fifo to be available # when it is set start_play_signal # then monitor for it to be delted because browser is closed # and the set end_play signal def get_fifo(self): """ Look for UZBL's FIFO-file in /tmp. Don't give up until it has been found. """ candidates = glob("/tmp/uzbl_fifo_*") for file in candidates: if S_ISFIFO(os_stat(file).st_mode): self.mon.log(self, "Found UZBL fifo in %s." % file) self.fifo = file self.start_play_signal = True return # print 'not found trying again' self.widget.after(500, self.get_fifo) def exists_fifo(self): if os.path.exists(self.fifo): return True else: return False # send commands to uzbl via the fifo def control(self, data): if self.exists_fifo(): self.mon.log(self, "send command to uzbl:" + data) f = open(self.fifo, "a") f.write("%s\n" % data) f.close() # test of whether _process is running def is_running(self): return self._process.isalive()
class MplayerDriver(object): _STATUS_REXP = re.compile(r"V :\s*([\d.]+).*") _DONE_REXP = re.compile(r"Exiting*") _LAUNCH_CMD = 'mplayer -quiet ' def __init__(self,widget,pp_dir): self.widget=widget self.pp_dir=pp_dir self.mon=Monitor() self._process=None self.paused=False def control(self,char): if self._process is not None: self._process.send(char) def pause(self): if self._process is not None: self._process.send('p') if not self.paused: self.paused = True else: self.paused=False def play(self, track, options): self._pp(track, options,False) def prepare(self, track, options): self._pp(track, options,True) def show(self): # unpause to start playing if self._process is not None: self._process.send('p') self.paused = False def stop(self): if self._process is not None: self._process.send('q') # kill the subprocess (mplayer). Used for tidy up on exit. def terminate(self,reason): self.terminate_reason=reason if self._process<>None: self._process.send('q') else: self.end_play_signal=True def get_terminate_reason(self): return self.terminate_reason # test of whether _process is running def is_running(self): return self._process.isalive() # *********************************** # INTERNAL FUNCTIONS # ************************************ def _pp(self, track, options, pause_before_play): self.paused=False self.start_play_signal = False self.end_play_signal=False self.terminate_reason='' track= "'"+ track.replace("'","'\\''") + "'" cmd = MplayerDriver._LAUNCH_CMD +' '+options +" " + track self.mon.log(self, "Send command to mplayer: "+ cmd) self._process = pexpect.spawn(cmd) # uncomment to monitor output to and input from mplayer (read pexpect manual) fout= file(self.pp_dir + os.sep + 'pp_logs' + os.sep + 'mplayerlogfile.txt','w') #uncomment and change sys.stdout to fout to log to a file # self._process.logfile_send = sys.stdout # send just commands to stdout self._process.logfile=fout # send all communications to log file if pause_before_play: self._process.send('p') self.paused = True # start the thread that is going to monitor sys.stdout. Presumably needs a thread because of blocking self._position_thread = Thread(target=self._get_position) self._position_thread.start() def _get_position(self): # print 'hang' # while True: # pass self.start_play_signal = True self.audio_position=0.0 while True: index = self._process.expect([MplayerDriver._DONE_REXP, pexpect.TIMEOUT, pexpect.EOF, MplayerDriver._STATUS_REXP], timeout=10) # mplayer does not produce regular status messages just 'exit' at end if index == 0: # nice day # print 'nice day' self.end_play_signal=True break else: # matches _STATUS_REXP so audio position self.audio_position = 0.0 sleep(0.05)
class LiveShow: """ plays a set of tracks the content of which is dynamically specified by plaacing track files in one of two directories. Tracks are played in file leafname alphabetical order. Can be interrupted """ # ******************* # External interface # ******************** def __init__(self, show_params, root, canvas, showlist, pp_dir, pp_home, pp_profile): self.mon=Monitor() self.mon.on() #instantiate arguments self.show_params =show_params self.showlist=showlist self.root=root self.canvas=canvas self.pp_dir=pp_dir self.pp_home=pp_home self.pp_profile=pp_profile # open resources self.rr=ResourceReader() #create and instance of TimeOfDay scheduler so we can add events self.tod=TimeOfDay() # Init variables self.player=None self.shower=None self.end_liveshow_signal=False self.end_trigger_signal= False self.play_child_signal = False self.error=False self.egg_timer=None self.duration_timer=None self.state='closed' self.livelist=None self.new_livelist= None def play(self,show_id,end_callback,ready_callback, top=False,command='nil'): global defaultDur if defaultDur == None: defaultDur = self.showlist.get_dur() #instantiate the arguments self.show_id=show_id self.end_callback=end_callback self.ready_callback=ready_callback self.top=top self.mon.log(self,"Starting show: " + self.show_params['show-ref']) # check data files are available. self.media_file = self.pp_profile + os.sep + self.show_params['medialist'] if not os.path.exists(self.media_file): self.mon.err(self,"Medialist file not found: "+ self.media_file) self.end_liveshow_signal=True self.options=command_options() self.pp_live_dir1 = self.pp_home + os.sep + 'pp_live_tracks' if not os.path.exists(self.pp_live_dir1): os.mkdir(self.pp_live_dir1) os.mkdir(self.pp_live_dir1+os.sep+ 'Archive') self.pp_live_dir2='' if self.options['liveshow'] <>"": self.pp_live_dir2 = self.options['liveshow'] if not os.path.exists(self.pp_live_dir2): self.mon.err(self,"live tracks directory not found " + self.pp_live_dir2) self.end('error',"live tracks directory not found") #create a medialist for the liveshow and read it. # it should be empty of anonymous tracks but read it to check its version. self.medialist=MediaList() if self.medialist.open_list(self.media_file,self.showlist.sissue())==False: self.mon.err(self,"Version of medialist different to Pi Presents") self.end('error',"Version of medialist different to Pi Presents") #get control bindings for this show if top level controlsmanager=ControlsManager() if self.top==True: self.controls_list=controlsmanager.default_controls() # and merge in controls from profile self.controls_list=controlsmanager.merge_show_controls(self.controls_list,self.show_params['controls']) #set up the time of day triggers for the show if self.show_params['trigger-start']in('time','time-quiet'): error_text=self.tod.add_times(self.show_params['trigger-start-time'],id(self),self.tod_start_callback,self.show_params['trigger-start']) if error_text<>'': self.mon.err(self,error_text) self.end('error',error_text) if self.show_params['trigger-end']=='time': error_text=self.tod.add_times(self.show_params['trigger-end-time'],id(self),self.tod_end_callback,'n/a') if error_text<>'': self.mon.err(self,error_text) self.end('error',error_text) if self.show_params['trigger-end']=='duration': error_text=self.calculate_duration(self.show_params['trigger-end-time']) if error_text<>'': self.mon.err(self,error_text) self.end('error',error_text) self.wait_for_trigger() def managed_stop(self): # if next lower show eor player is running pass down to stop the show/track if self.shower<>None: self.shower.managed_stop() else: self.end_liveshow_signal=True if self.player<>None: self.player.input_pressed('stop') # kill or error def terminate(self,reason): if self.shower<>None: self.shower.terminate(reason) elif self.player<>None: self.player.terminate(reason) else: self.end(reason,'terminated without terminating shower or player') # respond to key presses. def input_pressed(self,symbol,edge,source): self.mon.log(self,"received key: " + symbol) if self.show_params['disable-controls']=='yes': return # if at top convert symbolic name to operation otherwise lower down we have received an operation # look through list of standard symbols to find match (symbolic-name, function name) operation =lookup (symbol if self.top==True: operation=self.lookup_control(symbol,self.controls_list) else: operation=symbol # print 'operation',operation # if no match for symbol against standard operations then return if operation=='': return else: #service the standard inputs for this show if operation=='stop': # if next lower show eor player is running pass down to stop the show/track # ELSE stop this show except for exceptions if self.shower<>None: self.shower.input_pressed('stop',edge,source) elif self.player<>None: self.player.input_pressed('stop') else: # not at top so stop the show if self.top == False: self.end_liveshow_signal=True else: pass elif operation in ('up','down'): # if child or sub-show is running and is a show pass to show, track does not use up/down if self.shower<>None: self.shower.input_pressed(operation,edge,source) elif operation=='play': # if child show or sub-show is running and is show - pass down # ELSE use Return to start child if self.shower<>None: self.shower.input_pressed(operation,edge,source) else: if self.show_params['has-child']=="yes": self.play_child_signal=True if self.player<>None: self.player.input_pressed("stop") elif operation == 'pause': # pass down if show or track running. if self.shower<>None: self.shower.input_pressed(operation,edge,source) elif self.player<>None: self.player.input_pressed(operation) elif operation[0:4]=='omx-' or operation[0:6]=='mplay-': if self.player<>None: self.player.input_pressed(operation) def lookup_control(self,symbol,controls_list): for control in controls_list: if symbol == control[0]: return control[1] return '' # *************************** # Constructing Livelist # *************************** def livelist_add_track(self,afile): (root,title)=os.path.split(afile) (root_plus,ext)= os.path.splitext(afile) if ext.lower() in PPdefinitions.IMAGE_FILES: self.livelist_new_track(PPdefinitions.new_tracks['image'],{'title':title,'track-ref':'','location':afile}) if ext.lower() in PPdefinitions.VIDEO_FILES: self.livelist_new_track(PPdefinitions.new_tracks['video'],{'title':title,'track-ref':'','location':afile}) if ext.lower() in PPdefinitions.AUDIO_FILES: self.livelist_new_track(PPdefinitions.new_tracks['audio'],{'title':title,'track-ref':'','location':afile}) if ext.lower() in PPdefinitions.WEB_FILES: self.livelist_new_track(PPdefinitions.new_tracks['web'],{'title':title,'track-ref':'','location':afile}) if ext.lower()=='.cfg': self.livelist_new_plugin(afile,title) def livelist_new_plugin(self,plugin_cfg,title): # read the file which is a plugin cfg file into a dictionary self.plugin_config = ConfigParser.ConfigParser() self.plugin_config.read(plugin_cfg) self.plugin_params = dict(self.plugin_config.items('plugin')) # create a new livelist entry of a type specified in the config file with plugin self.livelist_new_track(PPdefinitions.new_tracks[self.plugin_params['type']],{'title':title,'track-ref':'','plugin':plugin_cfg,'location':plugin_cfg}) def livelist_new_track(self,fields,values): new_track=fields self.new_livelist.append(copy.deepcopy(new_track)) last = len(self.new_livelist)-1 self.new_livelist[last].update(values) def new_livelist_create(self): self.new_livelist=[] if os.path.exists(self.pp_live_dir1): for file in os.listdir(self.pp_live_dir1): file = self.pp_live_dir1 + os.sep + file (root_file,ext_file)= os.path.splitext(file) if (ext_file.lower() in PPdefinitions.IMAGE_FILES+PPdefinitions.VIDEO_FILES+PPdefinitions.AUDIO_FILES+PPdefinitions.WEB_FILES) or (ext_file.lower()=='.cfg'): self.livelist_add_track(file) if os.path.exists(self.pp_live_dir2): for file in os.listdir(self.pp_live_dir2): file = self.pp_live_dir2 + os.sep + file (root_file,ext_file)= os.path.splitext(file) if ext_file.lower() in PPdefinitions.IMAGE_FILES+PPdefinitions.VIDEO_FILES+PPdefinitions.AUDIO_FILES+PPdefinitions.WEB_FILES or (ext_file.lower()=='.cfg'): self.livelist_add_track(file) self.new_livelist= sorted(self.new_livelist, key= lambda track: os.path.basename(track['location']).lower()) # print 'LIVELIST' # for it in self.new_livelist: # print 'type: ', it['type'], 'loc: ',it['location'],'\nplugin cfg: ', it['plugin'] # print '' def livelist_replace_if_changed(self): self.new_livelist_create() if self.new_livelist<>self.livelist: self.livelist=copy.deepcopy(self.new_livelist) self.livelist_index = 1 def livelist_next(self): skip = False if self.livelist_index== len(self.livelist)-1: self.livelist_index=0 else: self.livelist_index +=1 #Author Joe Houng #get properties from file name if it exists runningFileName = self.livelist[self.livelist_index]['title'] fileNameTupel = ProcessFileName(runningFileName) dur = fileNameTupel[0] startDate = fileNameTupel[1] endDate = fileNameTupel[2] if dur == "": #duration not specified in filename global defaultDur dur = defaultDur if startDate != "": curDate = time.strftime('%Y-%m-%d-%H-%M-%S') if startDate > curDate: print dur self.livelist_index +=1 skip = True dur = defaultDur if skip == False: if endDate != "": if endDate <= time.strftime('%Y-%m-%d-%H-%M-%S'): try: toArchive(runningFileName, self.pp_home) except IOError: None self.showlist.assign_dur(dur); skip = False # *************************** # Sequencing # *************************** def wait_for_trigger(self): self.state='waiting' if self.ready_callback<>None: self.ready_callback() self.mon.log(self,"Waiting for trigger: "+ self.show_params['trigger-start']) if self.show_params['trigger-start'] in ('time','time-quiet'): # if next show is this one display text next_show=self.tod.next_event_time() if next_show[3]<>True: if next_show[1]=='tomorrow': text = self.resource('liveshow','m04') else: text = self.resource('liveshow','m03') text=text.replace('%tt',next_show[0]) self.display_message(self.canvas,'text',text,0,self.play_first_track) elif self.show_params['trigger-start']=="start": self.play_first_track() else: self.mon.err(self,"Unknown trigger: "+ self.show_params['trigger-start']) self.end('error',"Unknown trigger type") # callbacks from time of day scheduler def tod_start_callback(self): if self.state=='waiting' and self.show_params['trigger-start']in('time','time-quiet'): self.play_first_track() def tod_end_callback(self): if self.state=='playing' and self.show_params['trigger-end'] in ('time','duration'): self.end_trigger_signal=True if self.shower<>None: self.shower.input_pressed('stop','front','') elif self.player<>None: self.player.input_pressed('stop') def play_first_track(self): self.state='playing' skip = False # start duration timer if self.show_params['trigger-end']=='duration': # print 'set alarm ', self.duration self.duration_timer = self.canvas.after(self.duration*1000,self.tod_end_callback) self.new_livelist_create() self.livelist = copy.deepcopy(self.new_livelist) self.livelist_index = 0 #Author Joe Houng #get properties from file name if it exists runningFileName = self.livelist[self.livelist_index]['title'] fileNameTupel = ProcessFileName(runningFileName) dur = fileNameTupel[0] startDate = fileNameTupel[1] endDate = fileNameTupel[2] if dur == "": #duration not specified in filename global defaultDur dur = defaultDur if startDate != "": curDate = time.strftime('%Y-%m-%d') if startDate > curDate: print dur self.livelist_index +=1 skip = True if skip == False: if endDate != "": if endDate <= time.strftime('%Y-%m-%d'): toArchive(runningFileName) self.showlist.assign_dur(dur); skip = False self.play_track() def play_track(self): self.livelist_replace_if_changed() if len(self.livelist)>0: self.play_selected_track(self.livelist[self.livelist_index]) else: self.display_message(self.canvas,None,self.resource('liveshow','m01'),5,self.what_next) def what_next(self): # end of show time trigger if self.end_trigger_signal==True: self.end_trigger_signal=False if self.top==True: self.state='waiting' self.wait_for_trigger() else: # not at top so stop the show self.end('normal','sub-show end time trigger') # user wants to end elif self.end_liveshow_signal==True: self.end_liveshow_signal=False self.end('normal',"show ended by user") # play child? elif self.play_child_signal == True: self.play_child_signal=False index = self.medialist.index_of_track('pp-child-show') if index >=0: #don't select the track as need to preserve mediashow sequence. child_track=self.medialist.track(index) self.display_eggtimer(self.resource('liveshow','m02')) self.play_selected_track(child_track) else: self.mon.err(self,"Child show not found in medialist: "+ self.show_params['pp-child-show']) self.end('error',"child show not found in medialist") # otherwise loop to next track else: self.livelist_next() self.play_track() # *************************** # Dispatching to Players/Shows # *************************** def ready_callback(self): self.delete_eggtimer() def play_selected_track(self,selected_track): """ selects the appropriate player from type field of the medialist and computes the parameters for that type selected_track is a dictionary for the track/show """ self.canvas.delete('pp-content') # is menu required if self.show_params['has-child']=="yes": enable_child=True else: enable_child=False #dispatch track by type self.player=None self.shower=None track_type = selected_track['type'] self.mon.log(self,"Track type is: "+ track_type) if track_type=="image": track_file=self.complete_path(selected_track) # images played from menus don't have children self.player=ImagePlayer(self.show_id,self.root,self.canvas,self.show_params,selected_track,self.pp_dir,self.pp_home,self.pp_profile) self.player.play(track_file, self.showlist, self.end_player, self.ready_callback, enable_menu=enable_child) elif track_type=="video": # create a videoplayer track_file=self.complete_path(selected_track) self.player=VideoPlayer(self.show_id,self.root,self.canvas,self.show_params,selected_track,self.pp_dir,self.pp_home,self.pp_profile) self.player.play(track_file, self.showlist, self.end_player, self.ready_callback, enable_menu=enable_child) elif track_type=="audio": # create a audioplayer track_file=self.complete_path(selected_track) self.player=AudioPlayer(self.show_id,self.root,self.canvas,self.show_params,selected_track,self.pp_dir,self.pp_home,self.pp_profile) self.player.play(track_file, self.showlist, self.end_player, self.ready_callback, enable_menu=enable_child) elif track_type=="message": # bit odd because MessagePlayer is used internally to display text. text=selected_track['text'] self.player=MessagePlayer(self.show_id,self.root,self.canvas,self.show_params,selected_track,self.pp_dir,self.pp_home,self.pp_profile) self.player.play(text, self.showlist, self.end_player, self.ready_callback, enable_menu=enable_child ) elif track_type=="web": # create a browser track_file=self.complete_path(selected_track) self.player=BrowserPlayer(self.show_id,self.root,self.canvas,self.show_params,selected_track,self.pp_dir,self.pp_home,self.pp_profile) self.player.play(track_file, self.showlist, self.end_player, self.ready_callback, enable_menu=enable_child) elif track_type=="show": # get the show from the showlist index = self.showlist.index_of_show(selected_track['sub-show']) if index >=0: self.showlist.select(index) selected_show=self.showlist.selected_show() else: self.mon.err(self,"Show not found in showlist: "+ selected_track['sub-show']) self.end_liveshow_signal=True if selected_show['type']=="mediashow": self.shower= MediaShow(selected_show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id,self.end_shower,self.ready_callback,top=False,command='nil') elif selected_show['type']=="menu": self.shower= MenuShow(selected_show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id,self.end_shower,self.ready_callback,top=False,command='nil') elif selected_show['type']=="radiobuttonshow": self.shower= RadioButtonShow(selected_show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id,self.end_shower,self.ready_callback,top=False,command='nil') elif selected_show['type']=="hyperlinkshow": self.shower= HyperlinkShow(selected_show, sef.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id,self.end_shower,self.ready_callback,top=False,command='nil') else: self.mon.err(self,"Unknown Show Type: "+ selected_show['type']) self.end_liveshow_signal=True else: self.mon.err(self,"Unknown Track Type: "+ track_type) self.end_liveshow_signal=True def end_shower(self,show_id,reason,message): self.mon.log(self,"Returned from shower with message: "+ message) self.shower=None if reason in("killed","error"): self.end(reason,message) else: self.what_next() def end_player(self,reason,message): self.mon.log(self,"Returned from player with message: "+ message) self.player=None if reason in("killed","error"): self.end(reason,message) else: self.what_next() # *************************** # end of show # *************************** def end(self,reason,message): self.end_liveshow_signal=False self.mon.log(self,"Ending Liveshow: "+ self.show_params['show-ref']) self.tidy_up() self.end_callback(self.show_id,reason,message) self=None def tidy_up(self): if self.duration_timer<>None: self.canvas.after_cancel(self.duration_timer) self.duration_timer=None #clear outstanding time of day events for this show # self.tod.clear_times_list(id(self)) # ****************************** # Displaying things # ********************************* def display_eggtimer(self,text): self.egg_timer=self.canvas.create_text(int(self.canvas['width'])/2, int(self.canvas['height'])/2, text= text, fill='white', font="Helvetica 20 bold", tag='pp-eggtimer') self.canvas.update_idletasks( ) def delete_eggtimer(self): self.canvas.delete('pp-eggtimer') self.canvas.update_idletasks( ) # used to display internal messages in situations where a medialist entry could not be used. def display_message(self,canvas,source,content,duration,display_message_callback): self.display_message_callback=display_message_callback tp={'duration':duration,'message-colour':'white','message-font':'Helvetica 20 bold','message-justify':'left', 'background-colour':'','background-image':'','show-control-begin':'','show-control-end':'', 'animate-begin':'','animate-clear':'','animate-end':'','message-x':'','message-y':'', 'display-show-background':'no','display-show-text':'no','show-text':'','track-text':'', 'plugin':''} self.player=MessagePlayer(self.show_id,self.root,canvas,tp,tp,self.pp_dir,self.pp_home,self.pp_profile) self.player.play(content,self.showlist,self.display_message_end,None) def display_message_end(self,reason,message): self.player=None if reason in ("killed",'error'): self.end(reason,message) else: self.display_message_callback() # ****************************** # utilities # ********************************* def resource(self,section,item): value=self.rr.get(section,item) if value==False: self.mon.err(self, "resource: "+section +': '+ item + " not found" ) self.terminate("error",'Cannot find resource') else: return value def complete_path(self,selected_track): # complete path of the filename of the selected entry track_file = selected_track['location'] if track_file<>'' and track_file[0]=="+": track_file=self.pp_home+track_file[1:] self.mon.log(self,"Track to play is: "+ track_file) return track_file def calculate_duration(self,line): fields=line.split(':') if len(fields)==1: secs=fields[0] minutes='0' hours='0' if len(fields)==2: secs=fields[1] minutes=fields[0] hours='0' if len(fields)==3: secs=fields[2] minutes=fields[1] hours=fields[0] self.duration=3600*long(hours)+60*long(minutes)+long(secs) return ''
class TimeOfDay(object): # CLASS VARIABLES # change this for another language DAYS_OF_WEEK = [ 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' ] """ TimeOfDay.events is a dictionary the keys being show-refs. Each dictionary entry is a list of time_elements sorted by descending time Each time element is a list with the fields: 0 - command 1 - time, seconds since midnight """ events = { } # list of times of day used to generate callbacks, earliest first # executed by main program and by each object using tod def __init__(self): self.mon = Monitor() # executed once from main program only def init(self, pp_dir, pp_home, pp_profile, root, callback): # instantiate arguments TimeOfDay.root = root self.pp_dir = pp_dir self.pp_home = pp_home self.pp_profile = pp_profile self.callback = callback # init variables self.testing = False self.tod_tick = 500 self.tick_timer = None TimeOfDay.now = datetime.now().replace(microsecond=0) # read the schedule self.schedule = self.open_schedule() #create the initial events list if 'simulate-time' in self.schedule and self.schedule[ 'simulate-time'] == 'yes': year = int(self.schedule['sim-year']) month = int(self.schedule['sim-month']) day = int(self.schedule['sim-day']) hour = int(self.schedule['sim-hour']) minute = int(self.schedule['sim-minute']) second = int(self.schedule['sim-second']) TimeOfDay.now = datetime(day=day, month=month, year=year, hour=hour, minute=minute, second=second) self.testing = True print '\nInitial SIMULATED time', TimeOfDay.now.ctime() else: #get the current date/time only this once TimeOfDay.now = datetime.now().replace(microsecond=0) # print '\nInitial REAL time',TimeOfDay.now.ctime() self.testing = False TimeOfDay.last_now = TimeOfDay.now - timedelta(seconds=1) self.build_schedule_for_today(self.schedule) if self.testing: self.print_todays_schedule() self.build_events_lists() # self.print_events_lists() # and do exitpipresents or start any show that should be running at start up time self.do_catchup() def do_catchup(self): TimeOfDay.scheduler_time = TimeOfDay.now.time() # shutdown or exit if current time is later than time in event list for show_ref in TimeOfDay.events: if show_ref == 'pp_core': times = TimeOfDay.events[show_ref] for time_element in reversed(times): # print 'now', TimeOfDay.scheduler_time, 'time from events', time_element[1], time_element[0] # got past current time can give up and execute exitpipresents or closedown if TimeOfDay.scheduler_time >= time_element[1]: self.do_event(show_ref, time_element) return 'exiting' # do the catchup for each real show in turn for show_ref in TimeOfDay.events: if show_ref != 'pp_core': # print '\n*****',show_ref times = TimeOfDay.events[show_ref] # go through the event list for a show rembering show state until the first future event is found. # then if last command was to start the show send it show_running = False last_start_element = [] for time_element in reversed(times): # print 'now', now_seconds, 'time from events', time_element[1], time_element[0] # got past current time can give up catch up if time_element[1] >= TimeOfDay.scheduler_time: # print ' gone past time - break' break if time_element[0] == 'open': last_start_element = time_element # print 'open - show-running= true' show_running = True elif time_element[0] == 'close': # print 'close - show-running= false' show_running = False if show_running is True: if self.testing: print 'End of Catch Up Search doing', show_ref, last_start_element self.do_event(show_ref, last_start_element) ## # print 'catchup time match', show_ref ## # now do the inital real time command if time now matches event time ## for time_element in reversed(times): ## if time_element[1] == TimeOfDay.scheduler_time: ## print ' do event - catchup', TimeOfDay.scheduler_time, 'time from events', time_element[1], time_element[0] ## self.do_event(show_ref,time_element) return 'not exiting' # called by main program only def poll(self): if self.testing: poll_time = TimeOfDay.now else: poll_time = datetime.now() # print 'poll time: ',poll_time.time(),'scheduler time: ',TimeOfDay.now.time() # if poll_time != TimeOfDay.now : print 'times different ',poll_time.time(),TimeOfDay.now.time() # is current time greater than last time the scheduler was run # run in a loop to catch up because root.after can get behind when images are being rendered etc. # poll time can be the same twice as poll is run at half second intervals. catchup_time = 0 while TimeOfDay.now <= poll_time: if TimeOfDay.now - TimeOfDay.last_now != timedelta(seconds=1): print 'POLL TIME FAILED', TimeOfDay.last_now, TimeOfDay.now #if catchup_time != 0: # print 'scheduler behind by: ',catchup_time, TimeOfDay.now.time(),poll_time.time() self.do_scheduler() TimeOfDay.last_now = TimeOfDay.now catchup_time += 1 TimeOfDay.now = TimeOfDay.now + timedelta(seconds=1) # and loop if self.testing: self.tick_timer = TimeOfDay.root.after(1000, self.poll) else: self.tick_timer = TimeOfDay.root.after(self.tod_tick, self.poll) # called by main program only def terminate(self): if self.tick_timer is not None: TimeOfDay.root.after_cancel(self.tick_timer) self.clear_events_lists() # execute events at the appropriate time. # called by main program only def do_scheduler(self): # if its midnight then build the events lists for the new day TimeOfDay.scheduler_time = TimeOfDay.now.time() if TimeOfDay.scheduler_time == time(hour=0, minute=0, second=0): if self.testing: print 'Its midnight, today is now', TimeOfDay.now.ctime() self.build_schedule_for_today(self.schedule) if self.testing: self.print_todays_schedule() self.build_events_lists() # self.print_events_lists() # print TimeOfDay.scheduler_time for show_ref in TimeOfDay.events: # print 'scheduler time match', show_ref times = TimeOfDay.events[show_ref] # now send a command if time matches for time_element in reversed(times): # print time_element[1],TimeOfDay.scheduler_time if time_element[1] == TimeOfDay.scheduler_time: self.do_event(show_ref, time_element) # execute an event def do_event(self, show_ref, time_element): self.mon.log( self, 'Event : ' + time_element[0] + ' ' + show_ref + ' required at: ' + time_element[1].isoformat()) if self.testing: print 'Event : ' + time_element[ 0] + ' ' + show_ref + ' required at: ' + time_element[ 1].isoformat() self.callback(time_element[0] + ' ' + show_ref) # # ************************************************ # The methods below can be called from many classes so need to operate on class variables # ************************************************ # clear events list def clear_events_lists(self): self.mon.log(self, 'clear time of day events list ') # empty event list TimeOfDay.events = {} # *********************************** # Preparing schedule and todays event list # ************************************ def open_schedule(self): # look for the schedule.json file # try inside profile filename = self.pp_profile + os.sep + "schedule.json" ifile = open(filename, 'rb') schedule = json.load(ifile) ifile.close() self.mon.log(self, "schedule.json read from " + filename) return schedule def build_schedule_for_today(self, schedule): # print this_day.year, this_day.month, this_day.day, TimeOfDay.DAYS_OF_WEEK[ this_day.weekday()] """ self.todays_schedule is a dictionary the keys being show-refs. Each dictionary entry is a list of time_elements Each time element is a list with the fields: 0 - command 1 - time hour:min[:sec] """ self.todays_schedule = {} for show in schedule['shows']: show_ref = show['show-ref'] if 'everyday' in show: day = show['everyday'] # print day['day'] times = day['times'] for time in times: self.todays_schedule[show['show-ref']] = copy.deepcopy( day['times']) # print '\nafter everyday' # self.print_todays_schedule() if 'weekday' in show: day = show['weekday'] # print day['day'] if TimeOfDay.DAYS_OF_WEEK[ TimeOfDay.now.weekday()] in day['day']: # print 'weekday matched', TimeOfDay.DAYS_OF_WEEK[ TimeOfDay.now.weekday()] times = day['times'] for time in times: self.todays_schedule[show['show-ref']] = copy.deepcopy( day['times']) # print '\nafter weekday' # self.print_todays_schedule() if 'monthday' in show: day = show['monthday'] # print day['day'] if TimeOfDay.now.day in map(int, day['day']): # print 'monthday matched', day['day'] times = day['times'] for time in times: self.todays_schedule[show['show-ref']] = copy.deepcopy( day['times']) # print '\nafter monthday' # self.print_todays_schedule() if 'specialday' in show: days = show['specialday'] # print days['day'] for day in days['day']: sdate = datetime.strptime(day, '%Y-%m-%d') if sdate.year == TimeOfDay.now.year and sdate.month == TimeOfDay.now.month and sdate.day == TimeOfDay.now.day: # print 'special matched', day times = days['times'] # for time in times: self.todays_schedule[show['show-ref']] = copy.deepcopy( days['times']) # print '\nafter specialday' # self.print_todays_schedule() def build_events_lists(self): # builds events dictionary from todays_schedule by # converting times in todays schedule from hour:min:sec to datetime # and sorts them earliest last TimeOfDay.events = {} for show_ref in self.todays_schedule: # print show_ref times = self.todays_schedule[show_ref] for time_element in times: time_element[1] = self.parse_event_time(time_element[1]) sorted_times = sorted(times, key=lambda time_element: time_element[1], reverse=True) TimeOfDay.events[show_ref] = sorted_times # print times def parse_event_time(self, time_text): fields = time_text.split(':') if len(fields) > 2: secs = int(fields[2]) else: secs = 0 hours = int(fields[0]) mins = int(fields[1]) return time(hour=hours, minute=mins, second=secs) # ********************* # print for debug # ********************* def print_todays_schedule(self): print '\nSchedule For ' + TimeOfDay.now.ctime() for key in self.todays_schedule: print ' ' + key for show in self.todays_schedule[key]: print ' ' + show[0] + ': ' + show[1] print def print_events_lists(self): print '\nevents list for today' for key in self.events: print '\n', key for show in self.events[key]: print show[0], show[1].isoformat() def save_schedule(self, filename): """ save a schedule """ if filename == "": return False if os.name == 'nt': filename = string.replace(filename, '/', '\\') else: filename = string.replace(filename, '\\', '/') ofile = open(filename, "wb") json.dump(self.schedule, ofile, sort_keys=False, indent=1) ofile.close() return
class OMXDriver(object): # adjust this to determine freeze after the first frame after_first_frame_position = -50000 # microseconds _LAUNCH_CMD = '/usr/bin/omxplayer --no-keys ' # needs changing if user has installed his own version of omxplayer elsewhere # add more keys here, see popcornmix/omxplayer github files readme.md and KeyConfig.h KEY_MAP = { '<': 3, '>': 4, 'z': 5, 'j': 6, 'k': 7, 'i': 8, 'o': 9, 'n': 10, 'm': 11, 's': 12, 'x': 30, 'w': 31 } def __init__(self, widget, pp_dir): self.widget = widget self.pp_dir = pp_dir self.mon = Monitor() self.start_play_signal = False self.end_play_signal = False self.end_play_reason = 'nothing' self.duration = 0 self.video_position = 0 self.pause_at_end_required = False self.paused_at_end = False self.pause_at_end_time = 0 # self.pause_before_play_required='before-first-frame' #no,before-first-frame, after-first-frame # self.pause_before_play_required='no' self.paused_at_start = 'False' self.paused = False self.terminate_reason = '' # dbus and subprocess self._process = None self.__iface_root = None self.__iface_props = None self.__iface_player = None def load(self, track, freeze_at_start, options, caller, omx_volume): self.omx_volume = omx_volume self.pause_before_play_required = freeze_at_start self.caller = caller track = "'" + track.replace("'", "'\\''") + "'" # self.mon.log(self,'TIME OF DAY: '+ strftime("%Y-%m-%d %H:%M")) self.dbus_user = os.environ["USER"] self.id = str(int(time() * 10)) self.dbus_name = "org.mpris.MediaPlayer2.omxplayer" + self.id self.omxplayer_cmd = OMXDriver._LAUNCH_CMD + options + " --dbus_name '" + self.dbus_name + "' " + track # self.mon.log(self, 'dbus user ' + self.dbus_user) # self.mon.log(self, 'dbus name ' + self.dbus_name) # print self.omxplayer_cmd self.mon.log(self, "Send command to omxplayer: " + self.omxplayer_cmd) # self._process=subprocess.Popen(self.omxplayer_cmd,shell=True,stdout=file('/home/pi/pipresents/pp_logs/stdout.txt','a'),stderr=file('/home/pi/pipresents/pp_logs/stderr.txt','a')) self._process = subprocess.Popen(self.omxplayer_cmd, shell=True, stdout=file('/dev/null', 'a'), stderr=file('/dev/null', 'a')) self.pid = self._process.pid # wait for omxplayer to start then start monitoring thread self.dbus_tries = 0 self.omx_loaded = False self._wait_for_dbus() return def _wait_for_dbus(self): connect_success = self.__dbus_connect() if connect_success is True: # print 'SUCCESS' self.mon.log( self, 'connected to omxplayer dbus after ' + str(self.dbus_tries) + ' centisecs') # get duration of the track in microsecs if fails return a very large duration # posibly faile because omxplayer is running but not omxplayer.bin duration_success, duration = self.get_duration() if duration_success is False: self.mon.warn( self, 'get duration failed for n attempts using ' + str(duration / 60000000) + ' minutes') # calculate time to pause before last frame self.duration = duration self.pause_at_end_time = duration - 350000 # start the thread that is going to monitor output from omxplayer. self._monitor_status() else: self.dbus_tries += 1 self.widget.after(100, self._wait_for_dbus) def _monitor_status(self): # print '\n',self.id, '** STARTING ',self.duration self.start_play_signal = False self.end_play_signal = False self.end_play_reason = 'nothing' self.paused_at_end = False self.paused_at_start = 'False' self.delay = 5 self.widget.after(0, self._status_loop) """ freeze at start 'no' - unpause in show - test !=0 'before_first_frame' - don't unpause in show, test >-xx just large enough negative to stop first frame showing 'after_first_frame' - don't unpause in show, test > -yy large enough so that first frame always shows """ after_first_frame_position = -50000 #microseconds before_first_frame_position = -80000 #microseconds def _status_loop(self): if self.is_running() is False: # process is not running because quit or natural end - seems not to happen self.end_play_signal = True self.end_play_reason = 'nice_day' # print ' send nice day - process not running' return else: success, video_position = self.get_position() # if video_position <= 0: print 'read position',video_position if success is False: # print 'send nice day - exception when reading video position' self.end_play_signal = True self.end_play_reason = 'nice_day' return else: self.video_position = video_position # if timestamp is near the end then pause if self.pause_at_end_required is True and self.video_position > self.pause_at_end_time: #microseconds # print 'pausing at end, leeway ',self.duration - self.video_position pause_end_success = self.pause(' at end of track') if pause_end_success is True: # print self.id,' pause for end success', self.video_position self.paused_at_end = True self.end_play_signal = True self.end_play_reason = 'pause_at_end' return else: print 'pause at end failed, probably because of delay after detection, just run on' self.widget.after(self.delay, self._status_loop) else: # need to do the pausing for preload after first timestamp is received 0 is default value before start # print self.pause_before_play_required,self.paused_at_start,self.video_position,OMXDriver.after_first_frame_position if (self.pause_before_play_required == 'after-first-frame' and self.paused_at_start == 'False'\ and self.video_position >OMXDriver.after_first_frame_position\ and self.video_position !=0)\ or(self.pause_before_play_required != 'after-first-frame' and self.paused_at_start == 'False'\ and self.video_position > OMXDriver.before_first_frame_position\ and self.video_position !=0): pause_after_load_success = self.pause('after load') if pause_after_load_success is True: # print self.id,' pause after load success',self.video_position self.start_play_signal = True self.paused_at_start = 'True' else: # should never fail, just warn at the moment # print 'pause after load failed ' + str(self.video_position) self.mon.warn( self, str(self.id) + ' pause after load fail ' + str(self.video_position)) self.widget.after(self.delay, self._status_loop) else: self.widget.after(self.delay, self._status_loop) def show(self, freeze_at_end_required): self.pause_at_end_required = freeze_at_end_required # unpause to start playing if self.pause_before_play_required == 'no': unpause_show_success = self.unpause(' to start showing') # print 'unpause for show',self.paused if unpause_show_success is True: pass # print self.id,' unpause for show success', self.video_position else: # should never fail, just warn at the moment self.mon.warn( self, str(self.id) + ' unpause for show fail ' + str(self.video_position)) def control(self, char): val = OMXDriver.KEY_MAP[char] self.mon.log( self, '>control received and sent to omxplayer ' + str(self.pid) + ' ' + str(val)) if self.is_running(): try: self.__iface_player.Action(dbus.Int32(val)) except dbus.exceptions.DBusException as ex: self.mon.warn( self, 'Failed to send control - dbus exception: {}'.format( ex.get_dbus_message())) return else: self.mon.warn(self, 'Failed to send control - process not running') return # USE ONLY at end and after load # return succces of the operation, several tries if pause did not work and no error reported. def pause(self, reason): self.mon.log(self, 'pause received ' + reason) if self.paused is False: self.mon.log(self, 'not paused so send pause ' + reason) tries = 1 while True: if self.send_pause() is False: # failed for good reason return False status = self.omxplayer_is_paused( ) # test omxplayer after sending the command if status == 'Paused': self.paused = True return True if status == 'Failed': # failed for good reason because of exception or process not running caused by end of track return False else: # failed for no good reason self.mon.warn(self, '!!!!! repeat pause ' + str(tries)) # print self.id,' !!!!! repeat pause ',self.video_position, tries tries += 1 if tries > 5: # print self.id, ' pause failed for n attempts' self.mon.warn(self, 'pause failed for n attempts') return False # repeat # USE ONLY for show def unpause(self, reason): self.mon.log(self, 'Unpause received ' + reason) if self.paused is True: self.mon.log(self, 'Is paused so Track will be unpaused ' + reason) tries = 1 while True: if self.send_unpause() is False: return False status = self.omxplayer_is_paused() # test omxplayer if status == 'Playing': self.paused = False self.paused_at_start = 'done' self.set_volume() return True if status == 'Failed': # failed for good reason because of exception or process not running caused by end of track return False else: # self.mon.warn(self, '!!!!! repeat unpause ' + str(tries)) # print self.id,' !!!! repeat unpause ',self.video_position, tries tries += 1 if tries > 200: # print self.id, ' unpause failed for n attempts' self.mon.warn(self, 'unpause failed for 200 attempts') return False def omxplayer_is_paused(self): if self.is_running(): try: result = self.__iface_props.PlaybackStatus() except dbus.exceptions.DBusException as ex: self.mon.warn( self, 'Failed to test paused - dbus exception: {}'.format( ex.get_dbus_message())) return 'Failed' return result else: self.mon.warn(self, 'Failed to test paused - process not running') # print self.id,' test paused not successful - process' return 'Failed' def send_pause(self): if self.is_running(): try: self.__iface_player.Pause() except dbus.exceptions.DBusException as ex: self.mon.warn( self, 'Failed to send pause - dbus exception: {}'.format( ex.get_dbus_message())) return False return True else: self.mon.warn(self, 'Failed to send pause - process not running') # print self.id,' send pause not successful - process' return False def send_unpause(self): if self.is_running(): try: self.__iface_player.Action(16) except dbus.exceptions.DBusException as ex: self.mon.warn( self, 'Failed to send unpause - dbus exception: {}'.format( ex.get_dbus_message())) return False return True else: self.mon.warn(self, 'Failed to send unpause - process not running') # print self.id,' send unpause not successful - process' return False def pause_on(self): self.mon.log(self, 'pause on received ') # print 'pause on',self.paused if self.paused is True: return if self.is_running(): try: # self.__iface_player.Action(16) self.__iface_player.Pause( ) # - this should work but does not!!! self.paused = True # print 'paused OK' return except dbus.exceptions.DBusException as ex: self.mon.warn( self, 'Failed to do pause on - dbus exception: {}'.format( ex.get_dbus_message())) return else: self.mon.warn(self, 'Failed to do pause on - process not running') return def pause_off(self): self.mon.log(self, 'pause off received ') # print 'pause off',self.paused if self.paused is False: return if self.is_running(): try: self.__iface_player.Action(16) self.paused = False # print 'not paused OK' return except dbus.exceptions.DBusException as ex: self.mon.warn( self, 'Failed to do pause off - dbus exception: {}'.format( ex.get_dbus_message())) return else: self.mon.warn(self, 'Failed to do pause off - process not running') return def toggle_pause(self, reason): self.mon.log(self, 'toggle pause received ' + reason) if self.is_running(): try: self.__iface_player.Action(16) if not self.paused: self.paused = True else: self.paused = False except dbus.exceptions.DBusException as ex: self.mon.warn( self, 'Failed to toggle pause - dbus exception: {}'.format( ex.get_dbus_message())) return else: self.mon.warn(self, 'Failed to toggle pause - process not running') return def go(self): self.mon.log(self, 'go received ') self.unpause('for go') def mute(self): self.__iface_player.Mute() def unmute(self): self.__iface_player.Unmute() def set_volume(self): millibels = self.omx_volume * 100 out = pow(10, millibels / 2000.0) self.__iface_props.Volume(out) def inc_volume(self): self.omx_volume += 3 self.set_volume() def dec_volume(self): self.omx_volume -= 3 self.set_volume() def stop(self): self.mon.log( self, '>stop received and quit sent to omxplayer ' + str(self.pid)) # need to send 'nice day' if self.paused_at_end is True: self.end_play_signal = True self.end_play_reason = 'nice_day' # print 'send nice day for close track' if self.is_running(): try: self.__iface_root.Quit() except dbus.exceptions.DBusException as ex: self.mon.warn( self, 'Failed to quit - dbus exception: {}'.format( ex.get_dbus_message())) return else: self.mon.warn(self, 'Failed to quit - process not running') return # kill the subprocess (omxplayer and omxplayer.bin). Used for tidy up on exit. def terminate(self, reason): self.terminate_reason = reason self.stop() def get_terminate_reason(self): return self.terminate_reason # test of whether _process is running def is_running(self): retcode = self._process.poll() # print 'is alive', retcode if retcode is None: return True else: return False # kill off omxplayer when it hasn't terminated at the end of a track. # send SIGINT (CTRL C) so it has a chance to tidy up daemons and omxplayer.bin def kill(self): if self.is_running() is True: self._process.send_signal(signal.SIGINT) def get_position(self): # don't test process as is done just before try: micros = self.__iface_props.Position() return True, micros except dbus.exceptions.DBusException as ex: # print 'Failed get_position - dbus exception: {}'.format(ex.get_dbus_message()) return False, -1 def get_duration(self): tries = 1 while True: success, duration = self._try_duration() if success is True: return True, duration else: self.mon.warn(self, 'repeat get duration ' + str(tries)) tries += 1 if tries > 5: return False, sys.maxint * 100 def _try_duration(self): """Return the total length of the playing media""" if self.is_running() is True: try: micros = self.__iface_props.Duration() return True, micros except dbus.exceptions.DBusException as ex: self.mon.warn( self, 'Failed get duration - dbus exception: {}'.format( ex.get_dbus_message())) return False, -1 else: return False, -1 # ********************* # connect to dbus # ********************* def __dbus_connect(self): if self.omx_loaded is False: # read the omxplayer dbus data from files generated by omxplayer bus_address_filename = "/tmp/omxplayerdbus.{}".format( self.dbus_user) bus_pid_filename = "/tmp/omxplayerdbus.{}.pid".format( self.dbus_user) if not os.path.exists(bus_address_filename): self.mon.log( self, 'waiting for bus address file ' + bus_address_filename) self.omx_loaded = False return False else: f = open(bus_address_filename, "r") bus_address = f.read().rstrip() if bus_address == '': self.mon.log( self, 'waiting for bus address in file ' + bus_address_filename) self.omx_loaded = False return False else: # self.mon.log(self, 'bus address found ' + bus_address) if not os.path.exists(bus_pid_filename): self.mon.warn( self, 'bus pid file does not exist ' + bus_pid_filename) self.omx_loaded = False return False else: f = open(bus_pid_filename, "r") bus_pid = f.read().rstrip() if bus_pid == '': self.omx_loaded = False return False else: # self.mon.log(self, 'bus pid found ' + bus_pid) os.environ[ "DBUS_SESSION_BUS_ADDRESS"] = bus_address os.environ["DBUS_SESSION_BUS_PID"] = bus_pid self.omx_loaded = True if self.omx_loaded is True: session_bus = dbus.SessionBus() try: omx_object = session_bus.get_object(self.dbus_name, "/org/mpris/MediaPlayer2", introspect=False) self.__iface_root = dbus.Interface(omx_object, "org.mpris.MediaPlayer2") self.__iface_props = dbus.Interface( omx_object, "org.freedesktop.DBus.Properties") self.__iface_player = dbus.Interface( omx_object, "org.mpris.MediaPlayer2.Player") except dbus.exceptions.DBusException as ex: # self.mon.log(self,"Waiting for dbus connection to omxplayer: {}".format(ex.get_dbus_message())) return False return True
class KbdDriver(object): config=None def __init__(self): self.mon=Monitor() # sets up tkinter keyboard events such that any key press # does a callback to 'callback' with the event object and a symbolic name. def bind_keys(self,widget,callback): for option in KbdDriver.config.items('keys'): condition=option[0] symbolic_name=option[1] # print condition,symbolic_name widget.bind(condition, lambda event, name=symbolic_name: self.specific_key(callback,name)) # bind all the normal keys that return a printing character such that x produces pp-key-x widget.bind("<Key>", lambda event : self.normal_key(callback,event)) def specific_key(self,callback,name): callback(name,'KBD') # alphanumeric keys- convert to symbolic by adding pp-key- def normal_key(self,callback,event): key=event.char if key != '': callback('pp-key-'+key,'KBD') # read the key bindings from keys.cfg def read(self,pp_dir,pp_home,pp_profile): if KbdDriver.config is None: # try inside profile tryfile=pp_profile+os.sep+'pp_io_config'+os.sep+'keys.cfg' if os.path.exists(tryfile): self.mon.log(self,"Found keys.cfg in profile at: "+ tryfile) filename=tryfile else: # try inside pipresents tryfile=pp_dir+os.sep+'pp_config'+os.sep+"keys.cfg" if os.path.exists(tryfile): filename=tryfile self.mon.log(self,"Fallback keys.cfg found at "+ tryfile) else: self.mon.log(self,"keys.cfg not found at "+ tryfile) self.mon.err(self,"keys.cfg not found in profile or fallback in Pi Presents") return False KbdDriver.config = ConfigParser.ConfigParser() KbdDriver.config.optionxform=str KbdDriver.config.read(filename) self.mon.log(self,"keys.cfg read from "+ filename) if KbdDriver.config.has_section('keys') is False: self.mon.err(self,"no [keys] section in keys.cfg") return False return True def has_section(self,section): if KbdDriver.config.has_section(section) is False: return False
class Animate(object): """ allows players to put events, which request the change of state of pins, into a queue. Events are executed at the required time. using the interface to an output driver. """ # constants for sequencer events list name = 0 # GPIO pin number, the xx in P1-xx param_type = 1 param_values = 2 # off , on time = 3 # time since the epoch in seconds tag = 4 # tag used to delete all matching events, usually a track reference. event_template=['','','',0,None] # CLASS VARIABLES (Animate.) events=[] last_poll_time=0 # executed by main program and by each object using animate def __init__(self): self.mon=Monitor() # executed once from main program def init(self,pp_dir,pp_home,pp_profile,widget,sequencer_tick,event_callback): # instantiate arguments self.widget=widget #something to hang 'after' on self.pp_dir=pp_dir self.pp_profile=pp_profile self.pp_home=pp_home self.sequencer_tick=sequencer_tick self.event_callback=event_callback # Initialise time used by sequencer Animate.sequencer_time=long(time.time()) # init timer self.sequencer_tick_timer=None # called by main program only def terminate(self): if self.sequencer_tick_timer is not None: self.widget.after_cancel(self.sequencer_tick_timer) self.clear_events_list(None) # ************************************************ # output sequencer # ************************************************ # called by main program only def poll(self): poll_time=long(time.time()) # is current time greater than last time the scheduler was run (previous second or more) # run in a loop to catch up because root.after can get behind when images are being rendered etc. while Animate.sequencer_time<=poll_time: # kick off output pin sequencer self.do_sequencer() Animate.sequencer_time +=1 # and loop the polling self.sequencer_tick_timer=self.widget.after(self.sequencer_tick,self.poll) # execute events at the appropriate time and remove from list (runs from main program only) # runs through list a number of times because of problems with pop messing up list def do_sequencer(self): # print 'sequencer run for: ' + str(sequencer_time) + ' at ' + str(long(time.time())) while True: event_found=False for index, item in enumerate(Animate.events): if item[Animate.time]<=Animate.sequencer_time: event=Animate.events.pop(index) event_found=True self.send_event(event[Animate.name],event[Animate.param_type],event[Animate.param_values],item[Animate.time]) break if event_found is False: break def send_event(self,name,param_type,param_values,req_time): self.event_callback(name,param_type,param_values,req_time) self.mon.log(self, 'send event '+ name) # ************************************************ # output sequencer interface methods # these can be called from many classes so need to operate on class variables # ************************************************ def animate(self,text,tag): lines = text.split("\n") for line in lines: reason,message,delay,name,param_type,param_values=self.parse_animate_fields(line) if reason == 'error': return 'error',message if name !='': self.add_event(name,param_type,param_values,delay,tag) # self.print_events() return 'normal','events processed' def add_event(self,name,param_type,param_values,delay,tag): poll_time=long(time.time()) # prepare the event event=Animate.event_template event[Animate.name]=name event[Animate.param_type]=param_type event[Animate.param_values]=param_values event[Animate.time]=delay+poll_time #+1? event[Animate.tag]=tag # print 'add event ',event # find the place in the events list and insert # first item in the list is earliest, if two have the same time then last to be added is fired last. # events are fired from top of list abs_time=poll_time+delay # print 'new event',abs_time copy_event= copy.deepcopy(event) length=len(Animate.events) if length ==0: Animate.events.append(copy_event) # print 'append to empty ist',abs_time return copy_event else: index=length-1 if abs_time>Animate.events[index][Animate.time]: Animate.events.append(copy_event) # print 'append to end of list if greater than last item',abs_time return copy_event while index!=-1: if abs_time==Animate.events[index][Animate.time]: Animate.events.insert(index+1,copy_event) # print 'insert after if equal',abs_time return copy_event if abs_time>Animate.events[index][Animate.time]: Animate.events.insert(index+1,copy_event) # print 'insert after if later',abs_time return copy_event if index==0: Animate.events.insert(index,copy_event) # print 'insert before if at start of list',abs_time return copy_event index -=1 # print 'error at start of list',abs_time def print_events(self): print '\nevents list' for event in Animate.events: print event # remove all the events with the same tag, usually a track reference def remove_events(self,tag): left=[] for item in Animate.events: if tag != item[Animate.tag]: left.append(item) Animate.events= left # self.print_events() # clear event list def clear_events_list(self,tag): self.mon.log(self,'clear events list ') # empty event list Animate.events=[] # [delay],symbol,type,state def parse_animate_fields(self,line): fields= line.split() if len(fields) == 0: return 'normal','no fields','','',[],0 elif len(fields) < 4: return 'error','Wrong number of fields in : '+ line,'','',[],0 delay_text=fields[0] name=fields[1] param_type=fields[2] start_params = 3 # check each field if not delay_text.isdigit(): return 'error','Delay is not an integer in : '+ line,'','',[],0 else: delay=int(delay_text) #only one param type at the moment. if param_type != 'state': return 'error','uknown parameter type in : '+ line,'','',[],0 else: params_length = 1 params_check = ('on','off') params=[] for index in range(start_params, start_params+ params_length): param=fields[index] if not param in params_check: return 'error','unknown parameter value in : '+ line,'','',[],0 params.append(param) return 'normal','event parsed OK',delay,name,param_type,params
class PiPresents(object): def pipresents_version(self): vitems = self.pipresents_issue.split('.') if len(vitems) == 2: # cope with 2 digit version numbers before 1.3.2 return 1000 * int(vitems[0]) + 100 * int(vitems[1]) else: return 1000 * int(vitems[0]) + 100 * int(vitems[1]) + int( vitems[2]) def __init__(self): # gc.set_debug(gc.DEBUG_UNCOLLECTABLE|gc.DEBUG_INSTANCES|gc.DEBUG_OBJECTS|gc.DEBUG_SAVEALL) gc.set_debug(gc.DEBUG_UNCOLLECTABLE | gc.DEBUG_SAVEALL) self.pipresents_issue = "1.4.4" self.pipresents_minorissue = '1.4.4a' # position and size of window without -f command line option self.nonfull_window_width = 0.45 # proportion of width self.nonfull_window_height = 0.7 # proportion of height self.nonfull_window_x = 0 # position of top left corner self.nonfull_window_y = 0 # position of top left corner StopWatch.global_enable = False # set up the handler for SIGTERM signal.signal(signal.SIGTERM, self.handle_sigterm) # **************************************** # Initialisation # *************************************** # get command line options self.options = command_options() # print (self.options) # get Pi Presents code directory pp_dir = sys.path[0] self.pp_dir = pp_dir if not os.path.exists(pp_dir + "/pipresents.py"): if self.options['manager'] is False: tkinter.messagebox.showwarning("Pi Presents", "Bad Application Directory") exit(102) # Initialise logging and tracing Monitor.log_path = pp_dir self.mon = Monitor() # Init in PiPresents only self.mon.init() # uncomment to enable control of logging from within a class # Monitor.enable_in_code = True # enables control of log level in the code for a class - self.mon.set_log_level() # make a shorter list to log/trace only some classes without using enable_in_code. Monitor.classes = [ 'PiPresents', 'HyperlinkShow', 'RadioButtonShow', 'ArtLiveShow', 'ArtMediaShow', 'MediaShow', 'LiveShow', 'MenuShow', 'GapShow', 'Show', 'ArtShow', 'AudioPlayer', 'BrowserPlayer', 'ImagePlayer', 'MenuPlayer', 'MessagePlayer', 'VideoPlayer', 'Player', 'MediaList', 'LiveList', 'ShowList', 'PathManager', 'ControlsManager', 'ShowManager', 'PluginManager', 'IOPluginManager', 'MplayerDriver', 'OMXDriver', 'UZBLDriver', 'TimeOfDay', 'ScreenDriver', 'Animate', 'OSCDriver', 'CounterManager', 'BeepsManager', 'Network', 'Mailer' ] # Monitor.classes=['PiPresents','MediaShow','GapShow','Show','VideoPlayer','Player','OMXDriver'] # Monitor.classes=['OSCDriver'] # get global log level from command line Monitor.log_level = int(self.options['debug']) Monitor.manager = self.options['manager'] # print self.options['manager'] self.mon.newline(3) self.mon.sched( self, None, "Pi Presents is starting, Version:" + self.pipresents_minorissue + ' at ' + time.strftime("%Y-%m-%d %H:%M.%S")) self.mon.log( self, "Pi Presents is starting, Version:" + self.pipresents_minorissue + ' at ' + time.strftime("%Y-%m-%d %H:%M.%S")) # self.mon.log (self," OS and separator:" + os.name +' ' + os.sep) self.mon.log(self, "sys.path[0] - location of code: " + sys.path[0]) # log versions of Raspbian and omxplayer, and GPU Memory with open("/boot/issue.txt") as ifile: self.mon.log(self, '\nRaspbian: ' + ifile.read()) self.mon.log( self, '\n' + check_output(["omxplayer", "-v"], universal_newlines=True)) self.mon.log( self, '\nGPU Memory: ' + check_output(["vcgencmd", "get_mem", "gpu"], universal_newlines=True)) if os.geteuid() == 0: print('Do not run Pi Presents with sudo') self.mon.log(self, 'Do not run Pi Presents with sudo') self.mon.finish() sys.exit(102) if "DESKTOP_SESSION" not in os.environ: print('Pi Presents must be run from the Desktop') self.mon.log(self, 'Pi Presents must be run from the Desktop') self.mon.finish() sys.exit(102) else: self.mon.log(self, 'Desktop is ' + os.environ['DESKTOP_SESSION']) # optional other classes used self.root = None self.ppio = None self.tod = None self.animate = None self.ioplugin_manager = None self.oscdriver = None self.osc_enabled = False self.tod_enabled = False self.email_enabled = False user = os.getenv('USER') if user is None: tkinter.messagebox.showwarning( "You must be logged in to run Pi Presents") exit(102) if user != 'pi': self.mon.warn(self, "You must be logged as pi to use GPIO") self.mon.log(self, 'User is: ' + user) # self.mon.log(self,"os.getenv('HOME') - user home directory (not used): " + os.getenv('HOME')) # does not work # self.mon.log(self,"os.path.expanduser('~') - user home directory: " + os.path.expanduser('~')) # does not work # check network is available self.network_connected = False self.network_details = False self.interface = '' self.ip = '' self.unit = '' # sets self.network_connected and self.network_details self.init_network() # start the mailer and send email when PP starts self.email_enabled = False if self.network_connected is True: self.init_mailer() if self.email_enabled is True and self.mailer.email_at_start is True: subject = '[Pi Presents] ' + self.unit + ': PP Started on ' + time.strftime( "%Y-%m-%d %H:%M") message = time.strftime( "%Y-%m-%d %H:%M" ) + '\nUnit: ' + self.unit + ' Profile: ' + self.options[ 'profile'] + '\n ' + self.interface + '\n ' + self.ip self.send_email('start', subject, message) # get profile path from -p option if self.options['profile'] != '': self.pp_profile_path = "/pp_profiles/" + self.options['profile'] else: self.mon.err(self, "Profile not specified in command ") self.end('error', 'Profile not specified with the commands -p option') # get directory containing pp_home from the command, if self.options['home'] == "": home = os.sep + 'home' + os.sep + user + os.sep + "pp_home" else: home = self.options['home'] + os.sep + "pp_home" self.mon.log(self, "pp_home directory is: " + home) # check if pp_home exists. # try for 10 seconds to allow usb stick to automount found = False for i in range(1, 10): self.mon.log(self, "Trying pp_home at: " + home + " (" + str(i) + ')') if os.path.exists(home): found = True self.pp_home = home break time.sleep(1) if found is True: self.mon.log( self, "Found Requested Home Directory, using pp_home at: " + home) else: self.mon.err(self, "Failed to find pp_home directory at " + home) self.end('error', "Failed to find pp_home directory at " + home) # check profile exists self.pp_profile = self.pp_home + self.pp_profile_path if os.path.exists(self.pp_profile): self.mon.sched(self, None, "Running profile: " + self.pp_profile_path) self.mon.log( self, "Found Requested profile - pp_profile directory is: " + self.pp_profile) else: self.mon.err( self, "Failed to find requested profile: " + self.pp_profile) self.end('error', "Failed to find requested profile: " + self.pp_profile) self.mon.start_stats(self.options['profile']) if self.options['verify'] is True: self.mon.err(self, "Validation option not supported - use the editor") self.end('error', 'Validation option not supported - use the editor') # initialise and read the showlist in the profile self.showlist = ShowList() self.showlist_file = self.pp_profile + "/pp_showlist.json" if os.path.exists(self.showlist_file): self.showlist.open_json(self.showlist_file) else: self.mon.err(self, "showlist not found at " + self.showlist_file) self.end('error', "showlist not found at " + self.showlist_file) # check profile and Pi Presents issues are compatible if self.showlist.profile_version() != self.pipresents_version(): self.mon.err( self, "Version of showlist " + self.showlist.profile_version_string + " is not same as Pi Presents") self.end( 'error', "Version of showlist " + self.showlist.profile_version_string + " is not same as Pi Presents") # get the 'start' show from the showlist index = self.showlist.index_of_start_show() if index >= 0: self.showlist.select(index) self.starter_show = self.showlist.selected_show() else: self.mon.err(self, "Show [start] not found in showlist") self.end('error', "Show [start] not found in showlist") # ******************** # SET UP THE GUI # ******************** # turn off the screenblanking and saver if self.options['noblank'] is True: call(["xset", "s", "off"]) call(["xset", "s", "-dpms"]) # find connected displays and create a canvas for each display self.dm = DisplayManager() status, message, self.root = self.dm.init(self.options, self.handle_user_abort) if status != 'normal': self.mon.err(self, message) self.end('error', message) self.mon.log( self, str(DisplayManager.num_displays) + ' Displays are connected:') for display_name in DisplayManager.display_map: status, message, display_id, canvas_obj = self.dm.id_of_canvas( display_name) if status != 'normal': continue width, height = self.dm.real_display_dimensions(display_id) self.mon.log( self, ' - ' + self.dm.name_of_display(display_id) + ' Id: ' + str(display_id) + ' ' + str(width) + '*' + str(height)) canvas_obj.config(bg=self.starter_show['background-colour']) # **************************************** # INITIALISE THE TOUCHSCREEN DRIVER # **************************************** # each driver takes a set of inputs, binds them to symboic names # and sets up a callback which returns the symbolic name when an input event occurs self.sr = ScreenDriver() # read the screen click area config file reason, message = self.sr.read(pp_dir, self.pp_home, self.pp_profile) if reason == 'error': self.end('error', 'cannot find, or error in screen.cfg') # create click areas on the canvases, must be polygon as outline rectangles are not filled as far as find_closest goes reason, message = self.sr.make_click_areas(self.handle_input_event) if reason == 'error': self.mon.err(self, message) self.end('error', message) # **************************************** # INITIALISE THE APPLICATION AND START # **************************************** self.shutdown_required = False self.reboot_required = False self.terminate_required = False self.exitpipresents_required = False # initialise the Beeps Manager self.beepsmanager = BeepsManager() self.beepsmanager.init(self.pp_home, self.pp_profile) # initialise the I/O plugins by importing their drivers self.ioplugin_manager = IOPluginManager() reason, message = self.ioplugin_manager.init(self.pp_dir, self.pp_profile, self.root, self.handle_input_event, self.pp_home) if reason == 'error': # self.mon.err(self,message) self.end('error', message) # kick off animation sequencer self.animate = Animate() self.animate.init(pp_dir, self.pp_home, self.pp_profile, self.root, 200, self.handle_output_event) self.animate.poll() #create a showmanager ready for time of day scheduler and osc server show_id = -1 self.show_manager = ShowManager(show_id, self.showlist, self.starter_show, self.root, self.pp_dir, self.pp_profile, self.pp_home) # first time through set callback to terminate Pi Presents if all shows have ended. self.show_manager.init(self.all_shows_ended_callback, self.handle_command, self.showlist) # Register all the shows in the showlist reason, message = self.show_manager.register_shows() if reason == 'error': self.mon.err(self, message) self.end('error', message) # Init OSCDriver, read config and start OSC server self.osc_enabled = False if self.network_connected is True: if os.path.exists(self.pp_profile + os.sep + 'pp_io_config' + os.sep + 'osc.cfg'): self.oscdriver = OSCDriver() reason, message = self.oscdriver.init( self.pp_profile, self.unit, self.interface, self.ip, self.handle_command, self.handle_input_event, self.e_osc_handle_animate) if reason == 'error': self.mon.err(self, message) self.end('error', message) else: self.osc_enabled = True self.root.after(1000, self.oscdriver.start_server()) # initialise ToD scheduler calculating schedule for today self.tod = TimeOfDay() reason, message, self.tod_enabled = self.tod.init( pp_dir, self.pp_home, self.pp_profile, self.showlist, self.root, self.handle_command) if reason == 'error': self.mon.err(self, message) self.end('error', message) # warn if the network not available when ToD required if self.tod_enabled is True and self.network_connected is False: self.mon.warn( self, 'Network not connected so Time of Day scheduler may be using the internal clock' ) # init the counter manager self.counter_manager = CounterManager() if self.starter_show['counters-store'] == 'yes': store_enable = True else: store_enable = False reason, message = self.counter_manager.init( self.pp_profile + '/counters.cfg', store_enable, self.options['loadcounters'], self.starter_show['counters-initial']) if reason == 'error': self.mon.err(self, message) self.end('error', message) # warn about start shows and scheduler if self.starter_show['start-show'] == '' and self.tod_enabled is False: self.mon.sched( self, None, "No Start Shows in Start Show and no shows scheduled") self.mon.warn( self, "No Start Shows in Start Show and no shows scheduled") if self.starter_show['start-show'] != '' and self.tod_enabled is True: self.mon.sched( self, None, "Start Shows in Start Show and shows scheduled - conflict?") self.mon.warn( self, "Start Shows in Start Show and shows scheduled - conflict?") # run the start shows self.run_start_shows() # kick off the time of day scheduler which may run additional shows if self.tod_enabled is True: self.tod.poll() # start the I/O plugins input event generation self.ioplugin_manager.start() # start Tkinters event loop self.root.mainloop() # ********************* # RUN START SHOWS # ******************** def run_start_shows(self): self.mon.trace(self, 'run start shows') # parse the start shows field and start the initial shows show_refs = self.starter_show['start-show'].split() for show_ref in show_refs: reason, message = self.show_manager.control_a_show( show_ref, 'open') if reason == 'error': self.mon.err(self, message) # ********************* # User inputs # ******************** def e_osc_handle_animate(self, line): #jump out of server thread self.root.after(1, lambda arg=line: self.osc_handle_animate(arg)) def osc_handle_animate(self, line): self.mon.log(self, "animate command received: " + line) #osc sends output events as a string reason, message, delay, name, param_type, param_values = self.animate.parse_animate_fields( line) if reason == 'error': self.mon.err(self, message) self.end(reason, message) self.handle_output_event(name, param_type, param_values, 0) # output events are animate commands def handle_output_event(self, symbol, param_type, param_values, req_time): reason, message = self.ioplugin_manager.handle_output_event( symbol, param_type, param_values, req_time) if reason == 'error': self.mon.err(self, message) self.end(reason, message) # all input events call this callback providing a symbolic name. # handle events that affect PP overall, otherwise pass to all active shows def handle_input_event(self, symbol, source): self.mon.log(self, "event received: " + symbol + ' from ' + source) if symbol == 'pp-terminate': self.handle_user_abort() elif symbol == 'pp-shutdown': self.mon.err( self, 'pp-shutdown removed in version 1.3.3a, see Release Notes') self.end( 'error', 'pp-shutdown removed in version 1.3.3a, see Release Notes') elif symbol == 'pp-shutdownnow': # need root.after to grt out of st thread self.root.after(1, self.shutdownnow_pressed) return elif symbol == 'pp-exitpipresents': self.exitpipresents_required = True if self.show_manager.all_shows_exited() is True: # need root.after to grt out of st thread self.root.after(1, self.e_all_shows_ended_callback) return reason, message = self.show_manager.exit_all_shows() else: # pass the input event to all registered shows for show in self.show_manager.shows: show_obj = show[ShowManager.SHOW_OBJ] if show_obj is not None: show_obj.handle_input_event(symbol) # commands are generated by tracks and shows # they can open or close shows, generate input events and do special tasks # commands also generate osc outputs to other computers # handles one command provided as a line of text def handle_command(self, command_text, source='', show=''): # print 'PIPRESENTS ',command_text,'\n Source',source,'from',show self.mon.log(self, "command received: " + command_text) if command_text.strip() == "": return fields = command_text.split() if fields[0] in ('osc', 'OSC'): if self.osc_enabled is True: status, message = self.oscdriver.parse_osc_command(fields[1:]) if status == 'warn': self.mon.warn(self, message) if status == 'error': self.mon.err(self, message) self.end('error', message) return else: return if fields[0] == 'counter': status, message = self.counter_manager.parse_counter_command( fields[1:]) if status == 'error': self.mon.err(self, message) self.end('error', message) return if fields[0] == 'beep': # cheat, field 0 will always be beep message, fields = self.beepsmanager.parse_beep(command_text) if message != '': self.mon.err(self, message) self.end('error', message) return location = self.beepsmanager.complete_path(fields[1]) if not os.path.exists(location): message = 'Beep file does not exist: ' + location self.mon.err(self, message) self.end('error', message) return else: self.beepsmanager.do_beep(location) return show_command = fields[0] if len(fields) > 1: show_ref = fields[1] else: show_ref = '' if show_command in ('open', 'close', 'closeall', 'openexclusive'): self.mon.sched(self, TimeOfDay.now, command_text + ' received from show:' + show) if self.shutdown_required is False and self.terminate_required is False: reason, message = self.show_manager.control_a_show( show_ref, show_command) else: return elif show_command == 'monitor': self.handle_monitor_command(show_ref) return elif show_command == 'cec': self.handle_cec_command(show_ref) return elif show_command == 'event': self.handle_input_event(show_ref, 'Show Control') return elif show_command == 'exitpipresents': self.exitpipresents_required = True if self.show_manager.all_shows_exited() is True: # need root.after to get out of st thread self.root.after(1, self.e_all_shows_ended_callback) return else: reason, message = self.show_manager.exit_all_shows() elif show_command == 'shutdownnow': # need root.after to get out of st thread self.root.after(1, self.shutdownnow_pressed) return elif show_command == 'reboot': # need root.after to get out of st thread self.root.after(1, self.reboot_pressed) return else: reason = 'error' message = 'command not recognised: ' + show_command if reason == 'error': self.mon.err(self, message) return def handle_monitor_command(self, command): if command == 'on': os.system('vcgencmd display_power 1 >/dev/null') elif command == 'off': os.system('vcgencmd display_power 0 >/dev/null') def handle_cec_command(self, command): if command == 'on': os.system('echo "on 0" | cec-client -s') elif command == 'standby': os.system('echo "standby 0" | cec-client -s') elif command == 'scan': os.system('echo scan | cec-client -s -d 1') # deal with differnt commands/input events def shutdownnow_pressed(self): self.shutdown_required = True if self.show_manager.all_shows_exited() is True: self.all_shows_ended_callback('normal', 'no shows running') else: # calls exit method of all shows, results in all_shows_closed_callback self.show_manager.exit_all_shows() def reboot_pressed(self): self.reboot_required = True if self.show_manager.all_shows_exited() is True: self.all_shows_ended_callback('normal', 'no shows running') else: # calls exit method of all shows, results in all_shows_closed_callback self.show_manager.exit_all_shows() def handle_sigterm(self, signum, fframe): self.mon.log(self, 'SIGTERM received - ' + str(signum)) self.terminate() def handle_user_abort(self): self.mon.log(self, 'User abort received') self.terminate() def terminate(self): self.mon.log(self, "terminate received") self.terminate_required = True needs_termination = False for show in self.show_manager.shows: # print show[ShowManager.SHOW_OBJ], show[ShowManager.SHOW_REF] if show[ShowManager.SHOW_OBJ] is not None: needs_termination = True self.mon.log( self, "Sent terminate to show " + show[ShowManager.SHOW_REF]) # call shows terminate method # eventually the show will exit and after all shows have exited all_shows_callback will be executed. show[ShowManager.SHOW_OBJ].terminate() if needs_termination is False: self.end('killed', 'killed - no termination of shows required') # ****************************** # Ending Pi Presents after all the showers and players are closed # ************************** def e_all_shows_ended_callback(self): self.all_shows_ended_callback('normal', 'no shows running') # callback from ShowManager when all shows have ended def all_shows_ended_callback(self, reason, message): for display_name in DisplayManager.display_map: status, message, display_id, canvas_obj = self.dm.id_of_canvas( display_name) if status != 'normal': continue canvas_obj.config(bg=self.starter_show['background-colour']) if reason in ( 'killed', 'error' ) or self.shutdown_required is True or self.exitpipresents_required is True or self.reboot_required is True: self.end(reason, message) def end(self, reason, message): self.mon.log(self, "Pi Presents ending with reason: " + reason) if self.root is not None: self.root.destroy() self.tidy_up() if reason == 'killed': if self.email_enabled is True and self.mailer.email_on_terminate is True: subject = '[Pi Presents] ' + self.unit + ': PP Exited with reason: Terminated' message = time.strftime( "%Y-%m-%d %H:%M" ) + '\n ' + self.unit + '\n ' + self.interface + '\n ' + self.ip self.send_email(reason, subject, message) self.mon.sched(self, None, "Pi Presents Terminated, au revoir\n") self.mon.log(self, "Pi Presents Terminated, au revoir") # close logging files self.mon.finish() print('Uncollectable Garbage', gc.collect()) # objgraph.show_backrefs(objgraph.by_type('Canvas'),filename='backrefs.png') sys.exit(101) elif reason == 'error': if self.email_enabled is True and self.mailer.email_on_error is True: subject = '[Pi Presents] ' + self.unit + ': PP Exited with reason: Error' message_text = 'Error message: ' + message + '\n' + time.strftime( "%Y-%m-%d %H:%M" ) + '\n ' + self.unit + '\n ' + self.interface + '\n ' + self.ip self.send_email(reason, subject, message_text) self.mon.sched(self, None, "Pi Presents closing because of error, sorry\n") self.mon.log(self, "Pi Presents closing because of error, sorry") # close logging files self.mon.finish() print('uncollectable garbage', gc.collect()) sys.exit(102) else: self.mon.sched(self, None, "Pi Presents exiting normally, bye\n") self.mon.log(self, "Pi Presents exiting normally, bye") # close logging files self.mon.finish() if self.reboot_required is True: # print 'REBOOT' call(['sudo', 'reboot']) if self.shutdown_required is True: # print 'SHUTDOWN' call(['sudo', 'shutdown', 'now', 'SHUTTING DOWN']) print('uncollectable garbage', gc.collect()) sys.exit(100) # tidy up all the peripheral bits of Pi Presents def tidy_up(self): self.handle_monitor_command('on') self.mon.log(self, "Tidying Up") # turn screen blanking back on if self.options['noblank'] is True: call(["xset", "s", "on"]) call(["xset", "s", "+dpms"]) # tidy up animation if self.animate is not None: self.animate.terminate() # tidy up i/o plugins if self.ioplugin_manager != None: self.ioplugin_manager.terminate() if self.osc_enabled is True: self.oscdriver.terminate() # tidy up time of day scheduler if self.tod_enabled is True: self.tod.terminate() # ******************************* # Connecting to network and email # ******************************* def init_network(self): timeout = int(self.options['nonetwork']) if timeout == 0: self.network_connected = False self.unit = '' self.ip = '' self.interface = '' return self.network = Network() self.network_connected = False # try to connect to network self.mon.log(self, 'Waiting up to ' + str(timeout) + ' seconds for network') success = self.network.wait_for_network(timeout) if success is False: self.mon.warn( self, 'Failed to connect to network after ' + str(timeout) + ' seconds') # tkMessageBox.showwarning("Pi Presents","Failed to connect to network so using fake-hwclock") return self.network_connected = True self.mon.sched( self, None, 'Time after network check is ' + time.strftime("%Y-%m-%d %H:%M.%S")) self.mon.log( self, 'Time after network check is ' + time.strftime("%Y-%m-%d %H:%M.%S")) # Get web configuration self.network_details = False network_options_file_path = self.pp_dir + os.sep + 'pp_config' + os.sep + 'pp_web.cfg' if not os.path.exists(network_options_file_path): self.mon.warn( self, "pp_web.cfg not found at " + network_options_file_path) return self.mon.log(self, 'Found pp_web.cfg in ' + network_options_file_path) self.network.read_config(network_options_file_path) self.unit = self.network.unit # get interface and IP details of preferred interface self.interface, self.ip = self.network.get_preferred_ip() if self.interface == '': self.network_connected = False return self.network_details = True self.mon.log( self, 'Network details ' + self.unit + ' ' + self.interface + ' ' + self.ip) def init_mailer(self): self.email_enabled = False email_file_path = self.pp_dir + os.sep + 'pp_config' + os.sep + 'pp_email.cfg' if not os.path.exists(email_file_path): self.mon.log(self, 'pp_email.cfg not found at ' + email_file_path) return self.mon.log(self, 'Found pp_email.cfg at ' + email_file_path) self.mailer = Mailer() self.mailer.read_config(email_file_path) # all Ok so can enable email if config file allows it. if self.mailer.email_allowed is True: self.email_enabled = True self.mon.log(self, 'Email Enabled') def try_connect(self): tries = 1 while True: success, error = self.mailer.connect() if success is True: return True else: self.mon.log( self, 'Failed to connect to email SMTP server ' + str(tries) + '\n ' + str(error)) tries += 1 if tries > 5: self.mon.log( self, 'Failed to connect to email SMTP server after ' + str(tries)) return False def send_email(self, reason, subject, message): if self.try_connect() is False: return False else: success, error = self.mailer.send(subject, message) if success is False: self.mon.log(self, 'Failed to send email: ' + str(error)) success, error = self.mailer.disconnect() if success is False: self.mon.log(self, 'Failed disconnect after send:' + str(error)) return False else: self.mon.log(self, 'Sent email for ' + reason) success, error = self.mailer.disconnect() if success is False: self.mon.log( self, 'Failed disconnect from email server ' + str(error)) return True
class MediaList(object): """ manages a media list of tracks and the track selected from the medialist """ def __init__(self, sequence): self.clear() self.mon = Monitor() self.sequence = sequence # Functions for the editor dealing with complete list def clear(self): self._tracks = [] #MediaList, stored as a list of dicts self._num_tracks = 0 self._selected_track_index = -1 # index of currently selected track def print_list(self): print '\n' print self._tracks def first(self): self._selected_track_index = -1 self.next(self.sequence ) #let this do the work of randomaising or advancing to 0 def length(self): return self._num_tracks def append(self, track_dict): # print '\ntrack dict',track_dict """appends a track dictionary to the end of the medialist store""" self._tracks.append(copy.deepcopy(track_dict)) self._num_tracks += 1 def update(self, index, values): self._tracks[index].update(values) def remove(self, index): self._tracks.pop(index) self._num_tracks -= 1 # deselect any track, saves worrying about whether index needs changing self._selected_track_index = -1 def move_up(self): if self._selected_track_index != 0: self._tracks.insert(self._selected_track_index - 1, self._tracks.pop(self._selected_track_index)) self.select(self._selected_track_index - 1) def move_down(self): if self._selected_track_index != self._num_tracks - 1: self._tracks.insert(self._selected_track_index + 1, self._tracks.pop(self._selected_track_index)) self.select(self._selected_track_index + 1) def replace(self, index, replacement): self._tracks[index] = replacement # Common functions work for anything def track_is_selected(self): if self._selected_track_index >= 0: return True else: return False def selected_track_index(self): return self._selected_track_index def track(self, index): return self._tracks[index] def selected_track(self): """returns a dictionary containing all fields in the selected track """ return self._selected_track def select(self, index): """does housekeeping necessary when a track is selected""" if self._num_tracks > 0 and index >= 0 and index < self._num_tracks: self._selected_track_index = index self._selected_track = self._tracks[index] return True else: return False # Dealing with anonymous tracks for use and display def at_end(self): # true is selected track is last anon index = self._num_tracks - 1 while index >= 0: if self._tracks[index]['track-ref'] == "": end = index if self._selected_track_index == end: return True else: return False index -= 1 return False def index_of_end(self): if self.anon_length() == 0: return False index = self._num_tracks - 1 while index >= 0: if self._tracks[index]['track-ref'] == "": return index index -= 1 return -1 def at_start(self): if self.anon_length() == 0: return False index = 0 while index < self._num_tracks: if self._tracks[index]['track-ref'] == "": start = index if self._selected_track_index == start: return True else: return False index += 1 return False def index_of_start(self): if self.anon_length() == 0: return False index = 0 while index < self._num_tracks: if self._tracks[index]['track-ref'] == "": return index index += 1 return False def anon_length(self): # number of anonymous tracks count = 0 index = 0 while index < self._num_tracks: if self._tracks[index]['track-ref'] == "": count += 1 index += 1 return count def start(self): if self.anon_length() == 0: return False # select first anonymous track in the list if self.sequence == 'ordered': index = 0 while index < self._num_tracks: if self._tracks[index]['track-ref'] == "": self.select(index) return True index += 1 return False else: match = random.randint(0, self.anon_length() - 1) # print 'match',match index = 0 while index < self._num_tracks: if self._tracks[index]['track-ref'] == "" and index == match: self.select(index) # print index return index index += 1 def finish(self): if self.anon_length() == 0: return False if self.sequence == 'ordered': # select last anymous track in the list index = self._num_tracks - 1 while index >= 0: if self._tracks[index]['track-ref'] == "": self.select(index) return True index -= 1 return False else: match = random.randint(0, self.anon_length() - 1) # print 'match',match index = 0 while index < self._num_tracks: if self._tracks[index]['track-ref'] == "" and index == match: self.select(index) # print index return index index += 1 def select_anon_by_index(self, wanted): if self.anon_length() == 0: return False index = 0 anon_index = 0 while index != self._num_tracks: # print index,self._tracks[index] ['track-ref'],wanted if self._tracks[index]['track-ref'] == "": if anon_index == wanted: # print 'match\n' self.select(index) return True anon_index += 1 index = index + 1 return False def next(self, sequence): if self.anon_length() == 0: return False if sequence == 'ordered': if self._selected_track_index == self._num_tracks - 1: index = 0 else: index = self._selected_track_index + 1 end = self._selected_track_index else: index = random.randint(0, self.anon_length() - 1) if index == 0: end = self._num_tracks - 1 else: end = index - 1 # search for next anonymous track # print 'index', index, 'end',end while index != end: if self._tracks[index]['track-ref'] == "": self.select(index) return True if index == self._num_tracks - 1: index = 0 else: index = index + 1 return False def previous(self, sequence): if self.anon_length() == 0: return False if sequence == 'ordered': if self._selected_track_index == 0: index = self._num_tracks - 1 else: index = self._selected_track_index - 1 end = self._selected_track_index else: index = random.randint(0, self.anon_length() - 1) if index == self._num_tracks - 1: end = 0 else: end = index + 1 # print 'index', index, 'end',end # search for previous anonymous track while index != end: if self._tracks[index]['track-ref'] == "": self.select(index) return True if index == 0: index = self._num_tracks - 1 else: index = index - 1 return False # Lookup for labelled tracks def index_of_track(self, wanted_track): index = 0 for track in self._tracks: if track['track-ref'] == wanted_track: return index index += 1 return -1 # open and save def open_list(self, filename, profile_version): """ opens a saved medialist medialists are stored as json arrays. """ ifile = open(filename, 'rb') mdict = json.load(ifile) ifile.close() self._tracks = mdict['tracks'] if 'issue' in mdict: self.medialist_version_string = mdict['issue'] else: self.medialist_version_string = "1.0" if self.medialist_version() == profile_version: self._num_tracks = len(self._tracks) self.last_num_tracks = self._num_tracks self._selected_track_index = -1 return True else: return False def medialist_version(self): vitems = self.medialist_version_string.split('.') if len(vitems) == 2: # cope with 2 digit version numbers before 1.3.2 return 1000 * int(vitems[0]) + 100 * int(vitems[1]) else: return 1000 * int(vitems[0]) + 100 * int(vitems[1]) + int( vitems[2]) # dummy for mediliast, in livelist the list is created from the live_track directories def use_new_livelist(self): pass def create_new_livelist(self): pass def new_length(self): return self.length() # for medialist the content of the list never changes so return False def livelist_changed(self): return False def save_list(self, filename): """ save a medialist """ if filename == "": return False dic = {'issue': self.medialist_version_string, 'tracks': self._tracks} filename = str(filename) filename = string.replace(filename, '\\', '/') tries = 1 while tries <= 10: # print "save medialist ",filename try: ofile = open(filename, "wb") json.dump(dic, ofile, sort_keys=True, indent=1) ofile.close() self.mon.log(self, "Saved medialist " + filename) break except IOError: self.mon.err( self, "failed to save medialist, trying again " + str(tries)) tries += 1 return
class PiPresents: def __init__(self): self.pipresents_issue="1.2" self.nonfull_window_width = 0.6 # proportion of width self.nonfull_window_height= 0.6 # proportion of height self.nonfull_window_x = 0 # position of top left corner self.nonfull_window_y=0 # position of top left corner StopWatch.global_enable=False #**************************************** # Initialisation # *************************************** # get command line options self.options=command_options() # get pi presents code directory pp_dir=sys.path[0] self.pp_dir=pp_dir if not os.path.exists(pp_dir+"/pipresents.py"): tkMessageBox.showwarning("Pi Presents","Bad Application Directory") exit() #Initialise logging Monitor.log_path=pp_dir self.mon=Monitor() self.mon.on() if self.options['debug']==True: Monitor.global_enable=True else: Monitor.global_enable=False self.mon.log (self, "Pi Presents is starting") self.mon.log (self," OS and separator:" + os.name +' ' + os.sep) self.mon.log(self,"sys.path[0] - location of code: "+sys.path[0]) # self.mon.log(self,"os.getenv('HOME') - user home directory (not used): " + os.getenv('HOME')) # self.mon.log(self,"os.path.expanduser('~') - user home directory: " + os.path.expanduser('~')) # optional other classes used self.ppio=None self.tod=None #get profile path from -p option if self.options['profile']<>"": self.pp_profile_path="/pp_profiles/"+self.options['profile'] else: self.pp_profile_path = "/pp_profiles/pp_profile" #get directory containing pp_home from the command, if self.options['home'] =="": home = os.path.expanduser('~')+ os.sep+"pp_home" else: home = self.options['home'] + os.sep+ "pp_home" self.mon.log(self,"pp_home directory is: " + home) #check if pp_home exists. # try for 10 seconds to allow usb stick to automount # fall back to pipresents/pp_home self.pp_home=pp_dir+"/pp_home" found=False for i in range (1, 10): self.mon.log(self,"Trying pp_home at: " + home + " (" + str(i)+')') if os.path.exists(home): found=True self.pp_home=home break time.sleep (1) if found==True: self.mon.log(self,"Found Requested Home Directory, using pp_home at: " + home) else: self.mon.log(self,"FAILED to find requested home directory, using default to display error message: " + self.pp_home) #check profile exists, if not default to error profile inside pipresents self.pp_profile=self.pp_home+self.pp_profile_path if os.path.exists(self.pp_profile): self.mon.log(self,"Found Requested profile - pp_profile directory is: " + self.pp_profile) else: self.pp_profile=pp_dir+"/pp_home/pp_profiles/pp_profile" self.mon.log(self,"FAILED to find requested profile, using default to display error message: pp_profile") if self.options['verify']==True: val =Validator() if val.validate_profile(None,pp_dir,self.pp_home,self.pp_profile,self.pipresents_issue,False) == False: tkMessageBox.showwarning("Pi Presents","Validation Failed") exit() # open the resources self.rr=ResourceReader() # read the file, done once for all the other classes to use. if self.rr.read(pp_dir,self.pp_home,self.pp_profile)==False: self.end('error','cannot find resources.cfg') #initialise and read the showlist in the profile self.showlist=ShowList() self.showlist_file= self.pp_profile+ "/pp_showlist.json" if os.path.exists(self.showlist_file): self.showlist.open_json(self.showlist_file) else: self.mon.err(self,"showlist not found at "+self.showlist_file) self.end('error','showlist not found') # check profile and Pi Presents issues are compatible if float(self.showlist.sissue())<>float(self.pipresents_issue): self.mon.err(self,"Version of profile " + self.showlist.sissue() + " is not same as Pi Presents, must exit") self.end('error','wrong version of profile') # get the 'start' show from the showlist index = self.showlist.index_of_show('start') if index >=0: self.showlist.select(index) self.starter_show=self.showlist.selected_show() else: self.mon.err(self,"Show [start] not found in showlist") self.end('error','start show not found') # ******************** # SET UP THE GUI # ******************** #turn off the screenblanking and saver if self.options['noblank']==True: call(["xset","s", "off"]) call(["xset","s", "-dpms"]) self.root=Tk() self.title='Pi Presents - '+ self.pp_profile self.icon_text= 'Pi Presents' self.root.title(self.title) self.root.iconname(self.icon_text) self.root.config(bg='black') # get size of the screen self.screen_width = self.root.winfo_screenwidth() self.screen_height = self.root.winfo_screenheight() # set window dimensions and decorations if self.options['fullscreen']==True: self.root.attributes('-fullscreen', True) os.system('unclutter &') self.window_width=self.screen_width self.window_height=self.screen_height self.window_x=0 self.window_y=0 self.root.geometry("%dx%d%+d%+d" % (self.window_width,self.window_height,self.window_x,self.window_y)) self.root.attributes('-zoomed','1') else: self.window_width=int(self.screen_width*self.nonfull_window_width) self.window_height=int(self.screen_height*self.nonfull_window_height) self.window_x=self.nonfull_window_x self.window_y=self.nonfull_window_y self.root.geometry("%dx%d%+d%+d" % (self.window_width,self.window_height,self.window_x,self.window_y)) #canvas covers the whole window self.canvas_height=self.screen_height self.canvas_width=self.screen_width # make sure focus is set. self.root.focus_set() #define response to main window closing. self.root.protocol ("WM_DELETE_WINDOW", self.exit_pressed) #setup a canvas onto which will be drawn the images or text self.canvas = Canvas(self.root, bg='black') self.canvas.config(height=self.canvas_height, width=self.canvas_width, highlightthickness=0) # self.canvas.pack() self.canvas.place(x=0,y=0) self.canvas.focus_set() # **************************************** # INITIALISE THE INPUT DRIVERS # **************************************** # looks after bindings between symbolic names and internal operations controlsmanager=ControlsManager() if controlsmanager.read(pp_dir,self.pp_home,self.pp_profile)==False: self.end('error','cannot find or error in controls.cfg.cfg') else: controlsmanager.parse_defaults() # each driver takes a set of inputs, binds them to symboic names # and sets up a callback which returns the symbolic name when an input event occurs/ # use keyboard driver to bind keys to symbolic names and to set up callback kbd=KbdDriver() if kbd.read(pp_dir,self.pp_home,self.pp_profile)==False: self.end('error','cannot find or error in keys.cfg') kbd.bind_keys(self.root,self.input_pressed) self.sr=ScreenDriver() # read the screen click area config file if self.sr.read(pp_dir,self.pp_home,self.pp_profile)==False: self.end('error','cannot find screen.cfg') # create click areas on the canvas, must be polygon as outline rectangles are not filled as far as find_closest goes reason,message = self.sr.make_click_areas(self.canvas,self.input_pressed) if reason=='error': self.mon.err(self,message) self.end('error',message) # **************************************** # INITIALISE THE APPLICATION AND START # **************************************** self.shutdown_required=False #kick off GPIO if enabled by command line option if self.options['gpio']==True: from pp_gpio import PPIO # initialise the GPIO self.ppio=PPIO() # PPIO.gpio_enabled=False if self.ppio.init(pp_dir,self.pp_home,self.pp_profile,self.canvas,50,self.gpio_pressed)==False: self.end('error','gpio error') # and start polling gpio self.ppio.poll() #kick off the time of day scheduler self.tod=TimeOfDay() self.tod.init(pp_dir,self.pp_home,self.canvas,500) self.tod.poll() # Create list of start shows initialise them and then run them self.run_start_shows() #start tkinter self.root.mainloop( ) # ********************* # RUN START SHOWS # ******************** def run_start_shows(self): #start show manager show_id=-1 #start show self.show_manager=ShowManager(show_id,self.showlist,self.starter_show,self.root,self.canvas,self.pp_dir,self.pp_profile,self.pp_home) #first time through so empty show register and set callback to terminate Pi Presents if all shows have ended. self.show_manager.init(self.all_shows_ended_callback) #parse the start shows field and start the initial shows start_shows_text=self.starter_show['start-show'] self.show_manager.start_initial_shows(start_shows_text) #callback from ShowManager when all shows have ended def all_shows_ended_callback(self,reason,message,force_shutdown): self.mon.log(self,"All shows ended, so terminate Pi Presents") if force_shutdown==True: self.shutdown_required=True self.mon.log(self,"shutdown forced by profile") self.terminate('killed') else: self.end(reason,message) # ********************* # User inputs # ******************** #gpio callback - symbol provided by gpio def gpio_pressed(self,index,symbol,edge): self.mon.log(self, "GPIO Pressed: "+ symbol) self.input_pressed(symbol,edge,'gpio') # all input events call this callback with a symbolic name. def input_pressed(self,symbol,edge,source): self.mon.log(self,"input received: "+symbol) if symbol=='pp-exit': self.exit_pressed() elif symbol=='pp-shutdown': self.shutdown_pressed('delay') elif symbol=='pp-shutdownnow': self.shutdown_pressed('now') else: for show in self.show_manager.shows: show_obj=show[ShowManager.SHOW_OBJ] if show_obj<>None: show_obj.input_pressed(symbol,edge,source) # ************************************** # respond to exit inputs by terminating # ************************************** def shutdown_pressed(self, when): if when=='delay': self.root.after(5000,self.on_shutdown_delay) else: self.shutdown_required=True self.exit_pressed() def on_shutdown_delay(self): if self.ppio.shutdown_pressed(): self.shutdown_required=True self.exit_pressed() def exit_pressed(self): self.mon.log(self, "kill received from user") #terminate any running shows and players self.mon.log(self,"kill sent to shows") self.terminate('killed') # kill or error def terminate(self,reason): needs_termination=False for show in self.show_manager.shows: if show[ShowManager.SHOW_OBJ]<>None: needs_termination=True self.mon.log(self,"Sent terminate to show "+ show[ShowManager.SHOW_REF]) show[ShowManager.SHOW_OBJ].terminate(reason) if needs_termination==False: self.end(reason,'terminate - no termination of lower levels required') # ****************************** # Ending Pi Presents after all the showers and players are closed # ************************** def end(self,reason,message): self.mon.log(self,"Pi Presents ending with message: " + reason + ' ' + message) if reason=='error': self.tidy_up() self.mon.log(self, "exiting because of error") #close logging files self.mon.finish() exit() else: self.tidy_up() self.mon.log(self,"no error - exiting normally") #close logging files self.mon.finish() if self.shutdown_required==True: call(['sudo', 'shutdown', '-h', '-t 5','now']) exit() else: exit() # tidy up all the peripheral bits of Pi Presents def tidy_up(self): #turn screen blanking back on if self.options['noblank']==True: call(["xset","s", "on"]) call(["xset","s", "+dpms"]) # tidy up gpio if self.options['gpio']==True and self.ppio<>None: self.ppio.terminate() #tidy up time of day scheduler if self.tod<>None: self.tod.terminate() # ***************************** # utilitities # **************************** def resource(self,section,item): value=self.rr.get(section,item) if value==False: self.mon.err(self, "resource: "+section +': '+ item + " not found" ) self.terminate("error") else: return value
class IOPluginManager(object): plugins = [] def __init__(self): self.mon = Monitor() def init(self, pp_dir, pp_profile, widget, callback, pp_home): self.pp_dir = pp_dir self.pp_profile = pp_profile self.pp_home = pp_home IOPluginManager.plugins = [] if os.path.exists(self.pp_profile + os.sep + 'pp_io_config'): # read the .cfg files in /pp_io_config in profile registring the I/O plugin for cfgfile in os.listdir(self.pp_profile + os.sep + 'pp_io_config'): if cfgfile in ('screen.cfg', 'osc.cfg'): continue cfgfilepath = self.pp_profile + os.sep + 'pp_io_config' + os.sep + cfgfile status, message = self.init_config(cfgfile, cfgfilepath, widget, callback) if status == 'error': return status, message #read .cfg file in /pipresents/pp_io_config if file not present in profile then use this one for cfgfile in os.listdir(self.pp_dir + os.sep + 'pp_io_config'): if cfgfile in ('screen.cfg', 'osc.cfg'): continue if not os.path.exists(self.pp_profile + os.sep + 'pp_io_config' + os.sep + cfgfile): cfgfilepath = self.pp_dir + os.sep + 'pp_io_config' + os.sep + cfgfile status, message = self.init_config(cfgfile, cfgfilepath, widget, callback) if status == 'error': return status, message # print IOPluginManager.plugins return 'normal', 'I/O Plugins registered' def init_config(self, cfgfile, cfgfilepath, widget, callback): # print cfgfile,cfgfilepath reason, message, config = self._read(cfgfile, cfgfilepath) if reason == 'error': self.mon.err(self, 'Failed to read ' + cfgfile + ' ' + message) return 'error', 'Failed to read ' + cfgfile + ' ' + message if config.has_section('DRIVER') is False: self.mon.err(self, 'No DRIVER section in ' + cfgfilepath) return 'error', 'No DRIVER section in ' + cfgfilepath entry = dict() #read information from DRIVER section entry['title'] = config.get('DRIVER', 'title') if config.get('DRIVER', 'enabled') == 'yes': if config.has_option('DRIVER', 'driver-ref'): entry['driver-ref'] = config.get('DRIVER', 'driver-ref') else: entry['driver-ref'] = '' driver_name = config.get('DRIVER', 'module') driver_path = self.pp_dir + os.sep + 'pp_io_plugins' + os.sep + driver_name + '.py' if not os.path.exists(driver_path): self.mon.err( self, driver_name + ' Driver not found in ' + driver_path) return 'error', driver_name + ' Driver not found in ' + driver_path instance = self._load_plugin_file( driver_name, self.pp_dir + os.sep + 'pp_io_plugins') reason, message = instance.init(cfgfile, cfgfilepath, widget, self.pp_dir, self.pp_home, self.pp_profile, callback) if reason == 'warn': self.mon.warn(self, message) return 'error', message if reason == 'error': self.mon.warn(self, message) return 'error', message entry['instance'] = instance self.mon.log(self, message) IOPluginManager.plugins.append(entry) return 'normal', 'I/O Plugins registered' def start(self): for entry in IOPluginManager.plugins: plugin = entry['instance'] if plugin.is_active() is True: plugin.start() def terminate(self): for entry in IOPluginManager.plugins: plugin = entry['instance'] if plugin.is_active() is True: plugin.terminate() self.mon.log(self, 'I/O plugin ' + entry['title'] + ' terminated') def get_input(self, key, driver_ref=''): for entry in IOPluginManager.plugins: plugin = entry['instance'] # print ('trying ',entry['title'],plugin.is_active()) if plugin.is_active( ) is True and driver_ref == entry['driver-ref']: # need to test found in plugin to allow key to match if driver-ref not used found, value = plugin.get_input(key) if found is True: return found, value # key not found in any plugin return False, None def handle_output_event(self, name, param_type, param_values, req_time): for entry in IOPluginManager.plugins: plugin = entry['instance'] # print ('trying ',entry['title'],name,param_type,plugin.is_active()) if plugin.is_active() is True: # print (name,param_type,param_values,req_time) reason, message = plugin.handle_output_event( name, param_type, param_values, req_time) if reason == 'error': # self.mon.err(self,message) return 'error', message else: self.mon.log(self, message) return 'normal', 'output scan complete' def _load_plugin_file(self, name, driver_dir): fp, pathname, description = imp.find_module(name, [driver_dir]) module_id = imp.load_module(name, fp, pathname, description) plugin_class = getattr(module_id, name) return plugin_class() def _read(self, filename, filepath): if os.path.exists(filepath): config = configparser.ConfigParser(inline_comment_prefixes=(';', )) config.read(filepath) self.mon.log(self, filename + " read from " + filepath) return 'normal', filename + ' read', config else: return 'error', filename + ' not found at: ' + filepath, None
class mplayerDriver(object): _STATUS_REXP = re.compile(r"V :\s*([\d.]+).*") _DONE_REXP = re.compile(r"Exiting*") _LAUNCH_CMD = "mplayer -quiet " def __init__(self, widget): self.widget = widget self.mon = Monitor() self.mon.off() self._process = None self.paused = None def control(self, char): if self._process <> None: self._process.send(char) def pause(self): if self._process <> None: self._process.send("p") if not self.paused: self.paused = True else: self.paused = False def play(self, track, options): self._pp(track, options, False) def prepare(self, track, options): self._pp(track, options, True) def show(self): # unpause to start playing if self._process <> None: self._process.send("p") self.paused = False def stop(self): if self._process <> None: self._process.send("q") # kill the subprocess (mplayer). Used for tidy up on exit. def terminate(self, reason): self.terminate_reason = reason if self._process <> None: self._process.send("q") else: self.end_play_signal = True def terminate_reason(self): return self.terminate_reason # test of whether _process is running def is_running(self): return self._process.isalive() # *********************************** # INTERNAL FUNCTIONS # ************************************ def _pp(self, track, options, pause_before_play): self.paused = False self.start_play_signal = False self.end_play_signal = False self.terminate_reason = "" track = "'" + track.replace("'", "'\\''") + "'" cmd = mplayerDriver._LAUNCH_CMD + options + " " + track self.mon.log(self, "Send command to mplayer: " + cmd) self._process = pexpect.spawn(cmd) # uncomment to monitor output to and input from mplayer (read pexpect manual) # fout= file('/home/pi/pipresents/mplayerlogfile.txt','w') #uncomment and change sys.stdout to fout to log to a file # self._process.logfile_send = sys.stdout # send just commands to stdout # self._process.logfile=fout # send all communications to log file if pause_before_play: self._process.send("p") self.paused = True # start the thread that is going to monitor sys.stdout. Presumably needs a thread because of blocking self._position_thread = Thread(target=self._get_position) self._position_thread.start() def _get_position(self): # print 'hang' # while True: # pass self.start_play_signal = True self.audio_position = 0.0 while True: index = self._process.expect( [mplayerDriver._STATUS_REXP, pexpect.TIMEOUT, pexpect.EOF, mplayerDriver._DONE_REXP] ) if index == 1: continue # timeout - it doesn't block so is a thread needed? elif index in (2, 3): # Exiting self.end_play_signal = True break else: # presumably matches _STATUS_REXP so get video position # has a bug, position is not displayed for an audio track (mp3). Need to look at another field in the status, but how to extract it self.audio_position = 0.0 sleep(0.05)
class ScreenDriver(object): image_obj = [] def __init__(self): self.mon = Monitor() self.dm = DisplayManager() # read screen.cfg def read(self, pp_dir, pp_home, pp_profile): self.pp_dir = pp_dir self.pp_home = pp_home # try inside profile tryfile = pp_profile + os.sep + 'pp_io_config' + os.sep + 'screen.cfg' # self.mon.log(self,"Trying screen.cfg in profile at: "+ tryfile) if os.path.exists(tryfile): filename = tryfile else: #give congiparser an empty filename so it returns an empty config. filename = '' ScreenDriver.config = configparser.ConfigParser( inline_comment_prefixes=(';', )) ScreenDriver.config.read(filename) if filename != '': self.mon.log(self, "screen.cfg read from " + filename) return 'normal', 'screen.cfg read' def click_areas(self): return ScreenDriver.config.sections() def get(self, section, item): return ScreenDriver.config.get(section, item) def is_in_config(self, section, item): return ScreenDriver.config.has_option(section, item) def parse_displays(self, text): return text.split(' ') # make click areas on the screen, bind them to their symbolic name, and create a callback if it is clicked. # click areas must be polygon as outline rectangles are not filled as far as find_closest goes # canvas is the PiPresents canvas def make_click_areas(self, callback): self.callback = callback reason = '' ScreenDriver.image_obj = [] for area in self.click_areas(): if not self.is_in_config(area, 'displays'): reason = 'error' message = 'missing displays field in screen.cfg' break displays_list = self.parse_displays(self.get(area, 'displays')) # print ('\n\n',displays_list) reason, message, points = self.parse_points( self.get(area, 'points'), self.get(area, 'name')) if reason == 'error': break # calculate centre of polygon vertices = len(points) // 2 # print area, 'vertices',vertices sum_x = 0 sum_y = 0 for i in range(0, vertices): # print i sum_x = sum_x + int(points[2 * i]) # print int(points[2*i]) sum_y = sum_y + int(points[2 * i + 1]) # print int(points[2*i+1]) polygon_centre_x = sum_x / vertices polygon_centre_y = sum_y / vertices for display_name in DisplayManager.display_map: if display_name in displays_list: status, message, display_id, canvas = self.dm.id_of_canvas( display_name) if status != 'normal': continue canvas.create_polygon( points, fill=self.get(area, 'fill-colour'), outline=self.get(area, 'outline-colour'), tags=("pp-click-area", self.get(area, 'name')), state='hidden') # image for the button if not self.is_in_config(area, 'image'): reason = 'error' message = 'missing image fields in screen.cfg' break image_name = self.get(area, 'image') if image_name != '': image_width = int(self.get(area, 'image-width')) image_height = int(self.get(area, 'image-height')) image_path = self.complete_path(image_name) if os.path.exists(image_path) is True: self.pil_image = Image.open(image_path) else: image_path = self.pp_dir + os.sep + 'pp_resources' + os.sep + 'button.jpg' if os.path.exists(image_path) is True: self.mon.warn( self, 'Default button image used for ' + area) self.pil_image = Image.open(image_path) else: self.mon.warn( self, 'Button image does not exist for ' + area) self.pil_image = None if self.pil_image is not None: self.pil_image = self.pil_image.resize( (image_width - 1, image_height - 1)) photo_image_id = ImageTk.PhotoImage(self.pil_image) # print (display_id, canvas,self.pil_image,photo_image_id,self.get(area,'name')) image_id = canvas.create_image( polygon_centre_x, polygon_centre_y, image=photo_image_id, anchor=CENTER, tags=('pp-click-area', self.get(area, 'name')), state='hidden') del self.pil_image ScreenDriver.image_obj.append(photo_image_id) # print (ScreenDriver.image_obj) # write the label at the centroid if self.get(area, 'text') != '': canvas.create_text(polygon_centre_x, polygon_centre_y, text=self.get(area, 'text'), fill=self.get(area, 'text-colour'), font=self.get(area, 'text-font'), tags=('pp-click-area', self.get(area, 'name')), state='hidden') canvas.bind('<Button-1>', self.click_pressed) if reason == 'error': return 'error', message else: return 'normal', 'made click areas' # callback for click on screen def click_pressed(self, event): x = event.x y = event.y # fail to correct the pointer position on touch so set for mouse click # x,y,text=self.dm.correct_touchscreen_pointer(event,0,False) overlapping = event.widget.find_overlapping(x - 5, y - 5, x + 5, y + 5) for item in overlapping: # print ScreenDriver.canvas.gettags(item) if ('pp-click-area' in event.widget.gettags(item)) and event.widget.itemcget( item, 'state') == 'normal': self.mon.log( self, "Click on screen: " + event.widget.gettags(item)[1]) self.callback(event.widget.gettags(item)[1], 'SCREEN') # need break as find_overlapping returns two results for each click, one with 'current' one without. break def is_click_area(self, test_area, canvas): click_areas = canvas.find_withtag('pp-click-area') for area in click_areas: if test_area in canvas.gettags(area): return True return False # use links with the symbolic name of click areas to enable the click areas in a show def enable_click_areas(self, links, canvas): for link in links: if self.is_click_area(link[0], canvas) and link[1] != 'null': # print 'enabling link ',link[0] canvas.itemconfig(link[0], state='normal') def hide_click_areas(self, links, canvas): # hide click areas for link in links: if self.is_click_area(link[0], canvas) and link[1] != 'null': # print 'disabling link ',link[0] canvas.itemconfig(link[0], state='hidden') # this does not seem to change the colour of the polygon # ScreenDriver.canvas.itemconfig('pp-click-area',state='hidden') canvas.update_idletasks() def parse_points(self, points_text, area): if points_text.strip() == '': return 'error', 'No points in click area: ' + area, [] if '+' in points_text: # parse x+y+width*height fields = points_text.split('+') if len(fields) != 3: return 'error','Do not understand click area points: '+area,[] dimensions = fields[2].split('*') if len(dimensions) != 2: return 'error','Do not understand click area points: '+area,[] if not fields[0].isdigit(): return 'error','x1 is not a positive integer in click area: '+area,[] else: x1 = int(fields[0]) if not fields[1].isdigit(): return 'error','y1 is not a positive integer in click area: '+area,[] else: y1 = int(fields[1]) if not dimensions[0].isdigit(): return 'error','width1 is not a positive integer in click area: '+area,[] else: width = int(dimensions[0]) if not dimensions[1].isdigit(): return 'error','height is not a positive integer in click area: '+area,[] else: height = int(dimensions[1]) return 'normal', '', [ str(x1), str(y1), str(x1 + width), str(y1), str(x1 + width), str(y1 + height), str(x1), str(y1 + height) ] else: # parse unlimited set of x,y,coords points = points_text.split() if len(points) < 6: return 'error','Less than 3 vertices in click area: '+area,[] if len(points) % 2 != 0: return 'error','Odd number of points in click area: '+area,[] for point in points: if not point.isdigit(): return 'error','point is not a positive integer in click area: '+area,[] return 'normal', 'parsed points OK', points def complete_path(self, track_file): # complete path of the filename of the selected entry if track_file != '' and track_file[0] == "+": track_file = self.pp_home + track_file[1:] elif track_file[0] == "@": track_file = self.pp_profile + track_file[1:] return track_file
class PPEditor: # *************************************** # INIT # *************************************** def __init__(self): self.editor_issue="1.2" # get command options self.command_options=ed_options() # get directory holding the code self.pp_dir=sys.path[0] if not os.path.exists(self.pp_dir+os.sep+"pp_editor.py"): tkMessageBox.showwarning("Pi Presents","Bad Application Directory") exit() #Initialise logging Monitor.log_path=self.pp_dir self.mon=Monitor() self.mon.on() if self.command_options['debug'] == True: Monitor.global_enable=True else: Monitor.global_enable=False self.mon.log (self, "Pi Presents Editor is starting") self.mon.log (self," OS and separator " + os.name +' ' + os.sep) self.mon.log(self,"sys.path[0] - location of code: code "+sys.path[0]) # set up the gui #root is the Tkinter root widget self.root = tk.Tk() self.root.title("Editor for Pi Presents") # self.root.configure(background='grey') self.root.resizable(False,False) #define response to main window closing self.root.protocol ("WM_DELETE_WINDOW", self.app_exit) # bind some display fields self.filename = tk.StringVar() self.display_selected_track_title = tk.StringVar() self.display_show = tk.StringVar() # define menu menubar = Menu(self.root) profilemenu = Menu(menubar, tearoff=0, bg="grey", fg="black") profilemenu.add_command(label='Open', command = self.open_existing_profile) profilemenu.add_command(label='Validate', command = self.validate_profile) menubar.add_cascade(label='Profile', menu = profilemenu) ptypemenu = Menu(profilemenu, tearoff=0, bg="grey", fg="black") ptypemenu.add_command(label='Exhibit', command = self.new_exhibit_profile) ptypemenu.add_command(label='Media Show', command = self.new_mediashow_profile) ptypemenu.add_command(label='Menu', command = self.new_menu_profile) ptypemenu.add_command(label='Presentation', command = self.new_presentation_profile) ptypemenu.add_command(label='Interactive', command = self.new_interactive_profile) ptypemenu.add_command(label='Live Show', command = self.new_liveshow_profile) ptypemenu.add_command(label='RadioButton Show', command = self.new_radiobuttonshow_profile) ptypemenu.add_command(label='Hyperlink Show', command = self.new_hyperlinkshow_profile) ptypemenu.add_command(label='Blank', command = self.new_blank_profile) profilemenu.add_cascade(label='New from Template', menu = ptypemenu) showmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") showmenu.add_command(label='Delete', command = self.remove_show) showmenu.add_command(label='Edit', command = self.m_edit_show) showmenu.add_command(label='Copy To', command = self.copy_show) menubar.add_cascade(label='Show', menu = showmenu) stypemenu = Menu(showmenu, tearoff=0, bg="grey", fg="black") stypemenu.add_command(label='Menu', command = self.add_menu) stypemenu.add_command(label='MediaShow', command = self.add_mediashow) stypemenu.add_command(label='LiveShow', command = self.add_liveshow) stypemenu.add_command(label='HyperlinkShow', command = self.add_hyperlinkshow) stypemenu.add_command(label='RadioButtonShow', command = self.add_radiobuttonshow) showmenu.add_cascade(label='Add', menu = stypemenu) medialistmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") menubar.add_cascade(label='MediaList', menu = medialistmenu) medialistmenu.add_command(label='Add', command = self.add_medialist) medialistmenu.add_command(label='Delete', command = self.remove_medialist) trackmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") trackmenu.add_command(label='Delete', command = self.remove_track) trackmenu.add_command(label='Edit', command = self.m_edit_track) trackmenu.add_command(label='Add from Dir', command = self.add_tracks_from_dir) trackmenu.add_command(label='Add from File', command = self.add_track_from_file) menubar.add_cascade(label='Track', menu = trackmenu) typemenu = Menu(trackmenu, tearoff=0, bg="grey", fg="black") typemenu.add_command(label='Video', command = self.new_video_track) typemenu.add_command(label='Audio', command = self.new_audio_track) typemenu.add_command(label='Image', command = self.new_image_track) typemenu.add_command(label='Web', command = self.new_web_track) typemenu.add_command(label='Message', command = self.new_message_track) typemenu.add_command(label='Show', command = self.new_show_track) typemenu.add_command(label='Menu Background', command = self.new_menu_background_track) typemenu.add_command(label='Child Show', command = self.new_child_show_track) trackmenu.add_cascade(label='New', menu = typemenu) toolsmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") menubar.add_cascade(label='Tools', menu = toolsmenu) toolsmenu.add_command(label='Update All', command = self.update_all) optionsmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") menubar.add_cascade(label='Options', menu = optionsmenu) optionsmenu.add_command(label='Edit', command = self.edit_options) helpmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") menubar.add_cascade(label='Help', menu = helpmenu) helpmenu.add_command(label='Help', command = self.show_help) helpmenu.add_command(label='About', command = self.about) self.root.config(menu=menubar) top_frame=Frame(self.root) top_frame.pack(side=TOP) bottom_frame=Frame(self.root) bottom_frame.pack(side=TOP, fill=BOTH, expand=1) left_frame=Frame(bottom_frame, padx=5) left_frame.pack(side=LEFT) middle_frame=Frame(bottom_frame,padx=5) middle_frame.pack(side=LEFT) right_frame=Frame(bottom_frame,padx=5,pady=10) right_frame.pack(side=LEFT) updown_frame=Frame(bottom_frame,padx=5) updown_frame.pack(side=LEFT) tracks_title_frame=Frame(right_frame) tracks_title_frame.pack(side=TOP) tracks_label = Label(tracks_title_frame, text="Tracks in Selected Medialist") tracks_label.pack() tracks_frame=Frame(right_frame) tracks_frame.pack(side=TOP) shows_title_frame=Frame(left_frame) shows_title_frame.pack(side=TOP) shows_label = Label(shows_title_frame, text="Shows") shows_label.pack() shows_frame=Frame(left_frame) shows_frame.pack(side=TOP) shows_title_frame=Frame(left_frame) shows_title_frame.pack(side=TOP) medialists_title_frame=Frame(left_frame) medialists_title_frame.pack(side=TOP) medialists_label = Label(medialists_title_frame, text="Medialists") medialists_label.pack() medialists_frame=Frame(left_frame) medialists_frame.pack(side=LEFT) # define buttons add_button = Button(middle_frame, width = 5, height = 2, text='Edit\nShow', fg='black', command = self.m_edit_show, bg="light grey") add_button.pack(side=RIGHT) add_button = Button(updown_frame, width = 5, height = 1, text='Add', fg='black', command = self.add_track_from_file, bg="light grey") add_button.pack(side=TOP) add_button = Button(updown_frame, width = 5, height = 1, text='Edit', fg='black', command = self.m_edit_track, bg="light grey") add_button.pack(side=TOP) add_button = Button(updown_frame, width = 5, height = 1, text='Up', fg='black', command = self.move_track_up, bg="light grey") add_button.pack(side=TOP) add_button = Button(updown_frame, width = 5, height = 1, text='Down', fg='black', command = self.move_track_down, bg="light grey") add_button.pack(side=TOP) # define display of showlist scrollbar = Scrollbar(shows_frame, orient=tk.VERTICAL) self.shows_display = Listbox(shows_frame, selectmode=SINGLE, height=7, width = 40, bg="white",activestyle=NONE, fg="black", yscrollcommand=scrollbar.set) scrollbar.config(command=self.shows_display.yview) scrollbar.pack(side=RIGHT, fill=Y) self.shows_display.pack(side=LEFT, fill=BOTH, expand=1) self.shows_display.bind("<ButtonRelease-1>", self.e_select_show) # define display of medialists scrollbar = Scrollbar(medialists_frame, orient=tk.VERTICAL) self.medialists_display = Listbox(medialists_frame, selectmode=SINGLE, height=7, width = 40, bg="white",activestyle=NONE, fg="black",yscrollcommand=scrollbar.set) scrollbar.config(command=self.medialists_display.yview) scrollbar.pack(side=RIGHT, fill=Y) self.medialists_display.pack(side=LEFT, fill=BOTH, expand=1) self.medialists_display.bind("<ButtonRelease-1>", self.select_medialist) # define display of tracks scrollbar = Scrollbar(tracks_frame, orient=tk.VERTICAL) self.tracks_display = Listbox(tracks_frame, selectmode=SINGLE, height=15, width = 40, bg="white",activestyle=NONE, fg="black",yscrollcommand=scrollbar.set) scrollbar.config(command=self.tracks_display.yview) scrollbar.pack(side=RIGHT, fill=Y) self.tracks_display.pack(side=LEFT,fill=BOTH, expand=1) self.tracks_display.bind("<ButtonRelease-1>", self.e_select_track) # initialise editor options class self.options=Options(self.pp_dir) #creates options file in code directory if necessary # initialise variables self.init() #and enter Tkinter event loop self.root.mainloop() # *************************************** # INIT AND EXIT # *************************************** def app_exit(self): self.root.destroy() exit() def init(self): self.options.read() self.pp_home_dir = self.options.pp_home_dir self.initial_media_dir = self.options.initial_media_dir self.mon.log(self,"Data Home from options is "+self.pp_home_dir) self.mon.log(self,"Initial Media from options is "+self.initial_media_dir) self.current_medialist=None self.current_showlist=None self.current_show=None self.shows_display.delete(0,END) self.medialists_display.delete(0,END) self.tracks_display.delete(0,END) # *************************************** # MISCELLANEOUS # *************************************** def edit_options(self): """edit the options then read them from file""" eo = OptionsDialog(self.root, self.options.options_file,'Edit Options') if eo.result==True: self.init() def show_help (self): tkMessageBox.showinfo("Help", "Read 'manual.pdf'") def about (self): tkMessageBox.showinfo("About","Editor for Pi Presents Profiles\n" +"For profile version: " + self.editor_issue + "\nAuthor: Ken Thompson"+ "\nWebsite: http://pipresents.wordpress.com/") def validate_profile(self): val =Validator() val.validate_profile(self.root,self.pp_dir,self.pp_home_dir,self.pp_profile_dir,self.editor_issue,True) # ************** # PROFILES # ************** def open_existing_profile(self): initial_dir=self.pp_home_dir+os.sep+"pp_profiles" if os.path.exists(initial_dir)==False: self.mon.err(self,"Home directory not found: " + initial_dir + "\n\nHint: Data Home option must end in pp_home") return dir_path=tkFileDialog.askdirectory(initialdir=initial_dir) # dir_path="C:\Users\Ken\pp_home\pp_profiles\\ttt" if len(dir_path)>0: self.open_profile(dir_path) def open_profile(self,dir_path): showlist_file = dir_path + os.sep + "pp_showlist.json" if os.path.exists(showlist_file)==False: self.mon.err(self,"Not a Profile: " + dir_path + "\n\nHint: Have you opened the profile directory?") return self.pp_profile_dir = dir_path self.root.title("Editor for Pi Presents - "+ self.pp_profile_dir) if self.open_showlist(self.pp_profile_dir)==False: self.init() return self.open_medialists(self.pp_profile_dir) self.refresh_tracks_display() def new_profile(self,profile): d = Edit1Dialog(self.root,"New Profile","Name", "") if d .result == None: return name=str(d.result) if name=="": tkMessageBox.showwarning("New Profile","Name is blank") return to = self.pp_home_dir + os.sep + "pp_profiles"+ os.sep + name if os.path.exists(to)== True: tkMessageBox.showwarning( "New Profile","Profile exists\n(%s)" % to ) return shutil.copytree(profile, to, symlinks=False, ignore=None) self.open_profile(to) def new_exhibit_profile(self): profile = self.pp_dir+"/pp_home/pp_profiles/ppt_exhibit" self.new_profile(profile) def new_interactive_profile(self): profile = self.pp_dir+"/pp_home/pp_profiles/ppt_interactive" self.new_profile(profile) def new_menu_profile(self): profile = self.pp_dir+"/pp_home/pp_profiles/ppt_menu" self.new_profile(profile) def new_presentation_profile(self): profile = self.pp_dir+"/pp_home/pp_profiles/ppt_presentation" self.new_profile(profile) def new_blank_profile(self): profile = self.pp_dir+"/pp_home/pp_profiles/ppt_blank" self.new_profile(profile) def new_mediashow_profile(self): profile = self.pp_dir+"/pp_home/pp_profiles/ppt_mediashow" self.new_profile(profile) def new_liveshow_profile(self): profile = self.pp_dir+"/pp_home/pp_profiles/ppt_liveshow" self.new_profile(profile) def new_radiobuttonshow_profile(self): profile = self.pp_dir+"/pp_home/pp_profiles/ppt_radiobuttonshow" self.new_profile(profile) def new_hyperlinkshow_profile(self): profile = self.pp_dir+"/pp_home/pp_profiles/ppt_hyperlinkshow" self.new_profile(profile) # *************************************** # Shows # *************************************** def open_showlist(self,dir): showlist_file = dir + os.sep + "pp_showlist.json" if os.path.exists(showlist_file)==False: self.mon.err(self,"showlist file not found at " + dir + "\n\nHint: Have you opened the profile directory?") self.app_exit() self.current_showlist=ShowList() self.current_showlist.open_json(showlist_file) if float(self.current_showlist.sissue())<float(self.editor_issue) or (self.command_options['forceupdate'] == True and float(self.current_showlist.sissue())==float(self.editor_issue)): self.update_profile() self.mon.err(self,"Version of profile has been updated to "+self.editor_issue+", please re-open") return False if float(self.current_showlist.sissue())>float(self.editor_issue): self.mon.err(self,"Version of profile is greater than editor, must exit") self.app_exit() self.refresh_shows_display() return True def save_showlist(self,dir): if self.current_showlist<>None: showlist_file = dir + os.sep + "pp_showlist.json" self.current_showlist.save_list(showlist_file) def add_eventshow(self): self.add_show(PPdefinitions.new_shows['eventshow']) def add_mediashow(self): self.add_show(PPdefinitions.new_shows['mediashow']) def add_liveshow(self): self.add_show(PPdefinitions.new_shows['liveshow']) def add_radiobuttonshow(self): self.add_show(PPdefinitions.new_shows['radiobuttonshow']) def add_hyperlinkshow(self): self.add_show(PPdefinitions.new_shows['hyperlinkshow']) def add_menu(self): self.add_show(PPdefinitions.new_shows['menu']) def add_start(self): self.add_show(PPdefinitions.new_shows['start']) def add_show(self,default): # append it to the showlist and then add the medialist if self.current_showlist<>None: d = Edit1Dialog(self.root,"AddShow", "Show Reference", "") if d.result == None: return name=str(d.result) if name=="": tkMessageBox.showwarning( "Add Show", "Name is blank" ) return if self.current_showlist.index_of_show(name)<>-1: tkMessageBox.showwarning( "Add Show", "A Show with this name already exists" ) return copied_show=self.current_showlist.copy(default,name) mediafile=self.add_medialist(name) if mediafile<>'': copied_show['medialist']=mediafile self.current_showlist.append(copied_show) self.save_showlist(self.pp_profile_dir) self.refresh_shows_display() def remove_show(self): if self.current_showlist<>None and self.current_showlist.length()>0 and self.current_showlist.show_is_selected(): if tkMessageBox.askokcancel("Delete Show","Delete Show"): index= self.current_showlist.selected_show_index() self.current_showlist.remove(index) self.save_showlist(self.pp_profile_dir) self.refresh_shows_display() def show_refs(self): _show_refs=[] for index in range(self.current_showlist.length()): if self.current_showlist.show(index)['show-ref']<>"start": _show_refs.append(copy.deepcopy(self.current_showlist.show(index)['show-ref'])) return _show_refs def refresh_shows_display(self): self.shows_display.delete(0,self.shows_display.size()) for index in range(self.current_showlist.length()): self.shows_display.insert(END, self.current_showlist.show(index)['title']+" ["+self.current_showlist.show(index)['show-ref']+"]") if self.current_showlist.show_is_selected(): self.shows_display.itemconfig(self.current_showlist.selected_show_index(),fg='red') self.shows_display.see(self.current_showlist.selected_show_index()) def e_select_show(self,event): if self.current_showlist<>None and self.current_showlist.length()>0: mouse_item_index=int(event.widget.curselection()[0]) self.current_showlist.select(mouse_item_index) self.refresh_shows_display() def copy_show(self): if self.current_showlist<>None and self.current_showlist.show_is_selected(): self.add_show(self.current_showlist.selected_show()) def m_edit_show(self): self.edit_show(PPdefinitions.show_types,PPdefinitions.show_field_specs) def edit_show(self,show_types,field_specs): if self.current_showlist<>None and self.current_showlist.show_is_selected(): d=EditItem(self.root,"Edit Show",self.current_showlist.selected_show(),show_types,field_specs,self.show_refs(), self.initial_media_dir,self.pp_home_dir,'show') if d.result == True: self.save_showlist(self.pp_profile_dir) self.refresh_shows_display() # *************************************** # Medialists # *************************************** def open_medialists(self,dir): self.medialists = [] for file in os.listdir(dir): if file.endswith(".json") and file<>'pp_showlist.json': self.medialists = self.medialists + [file] self.medialists_display.delete(0,self.medialists_display.size()) for index in range (len(self.medialists)): self.medialists_display.insert(END, self.medialists[index]) self.current_medialists_index=-1 self.current_medialist=None def add_medialist(self,name=None): if name==None: d = Edit1Dialog(self.root,"Add Medialist", "File", "") if d.result == None: return '' name=str(d.result) if name=="": tkMessageBox.showwarning( "Add medialist", "Name is blank" ) return '' if not name.endswith(".json"): name=name+(".json") path = self.pp_profile_dir + os.sep + name if os.path.exists(path)== True: tkMessageBox.showwarning("Add medialist","Medialist file exists\n(%s)" % path) return '' nfile = open(path,'wb') nfile.write("{") nfile.write("\"issue\": \""+self.editor_issue+"\",\n") nfile.write("\"tracks\": [") nfile.write("]") nfile.write("}") nfile.close() # append it to the list self.medialists.append(copy.deepcopy(name)) # add title to medialists display self.medialists_display.insert(END, name) # and set it as the selected medialist self.refresh_medialists_display() return name def remove_medialist(self): if self.current_medialist<>None: if tkMessageBox.askokcancel("Delete Medialist","Delete Medialist"): os.remove(self.pp_profile_dir+ os.sep + self.medialists[self.current_medialists_index]) self.open_medialists(self.pp_profile_dir) self.refresh_medialists_display() self.refresh_tracks_display() def select_medialist(self,event): """ user clicks on a medialst in a profile so try and select it. """ # needs forgiving int for possible tkinter upgrade if len(self.medialists)>0: self.current_medialists_index=int(event.widget.curselection()[0]) self.current_medialist=MediaList() if not self.current_medialist.open_list(self.pp_profile_dir+ os.sep + self.medialists[self.current_medialists_index],self.current_showlist.sissue()): self.mon.err(self,"medialist is a different version to showlist: "+ self.medialists[self.current_medialists_index]) self.app_exit() self.refresh_tracks_display() self.refresh_medialists_display() def refresh_medialists_display(self): self.medialists_display.delete(0,len(self.medialists)) for index in range (len(self.medialists)): self.medialists_display.insert(END, self.medialists[index]) if self.current_medialist<>None: self.medialists_display.itemconfig(self.current_medialists_index,fg='red') self.medialists_display.see(self.current_medialists_index) def save_medialist(self): basefile=self.medialists[self.current_medialists_index] #print type(basefile) # basefile=str(basefile) #print type(basefile) file = self.pp_profile_dir+ os.sep + basefile self.current_medialist.save_list(file) # *************************************** # Tracks # *************************************** def refresh_tracks_display(self): self.tracks_display.delete(0,self.tracks_display.size()) if self.current_medialist<>None: for index in range(self.current_medialist.length()): if self.current_medialist.track(index)['track-ref']<>"": track_ref_string=" ["+self.current_medialist.track(index)['track-ref']+"]" else: track_ref_string="" self.tracks_display.insert(END, self.current_medialist.track(index)['title']+track_ref_string) if self.current_medialist.track_is_selected(): self.tracks_display.itemconfig(self.current_medialist.selected_track_index(),fg='red') self.tracks_display.see(self.current_medialist.selected_track_index()) def e_select_track(self,event): if self.current_medialist<>None and self.current_medialist.length()>0: mouse_item_index=int(event.widget.curselection()[0]) self.current_medialist.select(mouse_item_index) self.refresh_tracks_display() def m_edit_track(self): self.edit_track(PPdefinitions.track_types,PPdefinitions.track_field_specs) def edit_track(self,track_types,field_specs): if self.current_medialist<>None and self.current_medialist.track_is_selected(): d=EditItem(self.root,"Edit Track",self.current_medialist.selected_track(),track_types,field_specs,self.show_refs(), self.initial_media_dir,self.pp_home_dir,'track') if d.result == True: self.save_medialist() self.refresh_tracks_display() def move_track_up(self): if self.current_medialist<>None and self.current_medialist.track_is_selected(): self.current_medialist.move_up() self.refresh_tracks_display() self.save_medialist() def move_track_down(self): if self.current_medialist<>None and self.current_medialist.track_is_selected(): self.current_medialist.move_down() self.refresh_tracks_display() self.save_medialist() def new_track(self,fields,values): if self.current_medialist<>None: #print '\nfields ', fields #print '\nvalues ', values new_track=copy.deepcopy(fields) #print ',\new track ',new_track self.current_medialist.append(new_track) #print '\nbefore values ',self.current_medialist.print_list() if values<>None: self.current_medialist.update(self.current_medialist.length()-1,values) self.current_medialist.select(self.current_medialist.length()-1) self.refresh_tracks_display() self.save_medialist() def new_message_track(self): self.new_track(PPdefinitions.new_tracks['message'],None) def new_video_track(self): self.new_track(PPdefinitions.new_tracks['video'],None) def new_audio_track(self): self.new_track(PPdefinitions.new_tracks['audio'],None) def new_web_track(self): self.new_track(PPdefinitions.new_tracks['web'],None) def new_image_track(self): self.new_track(PPdefinitions.new_tracks['image'],None) def new_show_track(self): self.new_track(PPdefinitions.new_tracks['show'],None) def new_menu_background_track(self): self.new_track(PPdefinitions.new_tracks['menu-background'],None) def new_child_show_track(self): self.new_track(PPdefinitions.new_tracks['child-show'],None) def remove_track(self): if self.current_medialist<>None and self.current_medialist.length()>0 and self.current_medialist.track_is_selected(): if tkMessageBox.askokcancel("Delete Track","Delete Track"): index= self.current_medialist.selected_track_index() self.current_medialist.remove(index) self.save_medialist() self.refresh_tracks_display() def add_track_from_file(self): if self.current_medialist==None: return # print "initial directory ", self.options.initial_media_dir files_path=tkFileDialog.askopenfilename(initialdir=self.options.initial_media_dir, multiple=True) # fix for tkinter bug files_path = self.root.tk.splitlist(files_path) for file_path in files_path: file_path=os.path.normpath(file_path) # print "file path ", file_path self.add_track(file_path) self.save_medialist() def add_tracks_from_dir(self): if self.current_medialist==None: return image_specs =[ PPdefinitions.IMAGE_FILES, PPdefinitions.VIDEO_FILES, PPdefinitions.AUDIO_FILES, PPdefinitions.WEB_FILES, ('All files', '*')] #last one is ignored in finding files # in directory, for dialog box only directory=tkFileDialog.askdirectory(initialdir=self.options.initial_media_dir) # deal with tuple returned on Cancel if len(directory)==0: return # make list of exts we recognise exts = [] for image_spec in image_specs[:-1]: image_list=image_spec[1:] for ext in image_list: exts.append(copy.deepcopy(ext)) for file in os.listdir(directory): (root_file,ext_file)= os.path.splitext(file) if ext_file.lower() in exts: file_path=directory+os.sep+file #print "file path before ", file_path file_path=os.path.normpath(file_path) #print "file path after ", file_path self.add_track(file_path) self.save_medialist() def add_track(self,afile): relpath = os.path.relpath(afile,self.pp_home_dir) #print "relative path ",relpath common = os.path.commonprefix([afile,self.pp_home_dir]) #print "common ",common if common.endswith("pp_home") == False: location = afile else: location = "+" + os.sep + relpath location = string.replace(location,'\\','/') #print "location ",location (root,title)=os.path.split(afile) (root,ext)= os.path.splitext(afile) if ext.lower() in PPdefinitions.IMAGE_FILES: self.new_track(PPdefinitions.new_tracks['image'],{'title':title,'track-ref':'','location':location}) elif ext.lower() in PPdefinitions.VIDEO_FILES: self.new_track(PPdefinitions.new_tracks['video'],{'title':title,'track-ref':'','location':location}) elif ext.lower() in PPdefinitions.AUDIO_FILES: self.new_track(PPdefinitions.new_tracks['audio'],{'title':title,'track-ref':'','location':location}) elif ext.lower() in PPdefinitions.WEB_FILES: self.new_track(PPdefinitions.new_tracks['web'],{'title':title,'track-ref':'','location':location}) else: self.mon.err(self,afile + " - file extension not recognised") # ********************************************* # update profile # ********************************************** def update_all(self): self.init() for profile_file in os.listdir(self.pp_home_dir+os.sep+'pp_profiles'): # self.mon.log (self,"Updating "+profile_file) self.pp_profile_dir = self.pp_home_dir+os.sep+'pp_profiles'+ os.sep + profile_file if not os.path.exists(self.pp_profile_dir+os.sep+"pp_showlist.json"): tkMessageBox.showwarning("Pi Presents","Not a profile, skipping "+self.pp_profile_dir) else: self.current_showlist=ShowList() #self.mon.log (self,"Checking version "+profile_file) self.current_showlist.open_json(self.pp_profile_dir+os.sep+"pp_showlist.json") if float(self.current_showlist.sissue())<float(self.editor_issue): self.mon.log(self,"Version of profile "+profile_file+ " is being updated to "+self.editor_issue) self.update_profile() elif (self.command_options['forceupdate'] == True and float(self.current_showlist.sissue())==float(self.editor_issue)): self.mon.log(self, "Forced updating of " + profile_file + ' to '+self.editor_issue) self.update_profile() elif float(self.current_showlist.sissue())>float(self.editor_issue): tkMessageBox.showwarning("Pi Presents", "Version of profile " +profile_file+ " is greater than editor, skipping") else: self.mon.log(self," Profile " + profile_file + " Already up to date ") self.init() tkMessageBox.showwarning("Pi Presents","All profiles updated") def update_profile(self): #open showlist and update its shows # self.mon.log (self,"Updating show ") ifile = open(self.pp_profile_dir + os.sep + "pp_showlist.json", 'rb') shows = json.load(ifile)['shows'] ifile.close() replacement_shows=self.update_shows(shows) dic={'issue':self.editor_issue,'shows':replacement_shows} ofile = open(self.pp_profile_dir + os.sep + "pp_showlist.json", "wb") json.dump(dic,ofile,sort_keys=True,indent=1) # UPDATE MEDIALISTS AND THEIR TRACKS for file in os.listdir(self.pp_profile_dir): if file.endswith(".json") and file<>'pp_showlist.json': # self.mon.log (self,"Updating medialist " + file) #open a medialist and update its tracks ifile = open(self.pp_profile_dir + os.sep + file, 'rb') tracks = json.load(ifile)['tracks'] ifile.close() replacement_tracks=self.update_tracks(tracks) dic={'issue':self.editor_issue,'tracks':replacement_tracks} ofile = open(self.pp_profile_dir + os.sep + file, "wb") json.dump(dic,ofile,sort_keys=True,indent=1) def update_tracks(self,old_tracks): # get correct spec from type of field replacement_tracks=[] for old_track in old_tracks: #print '\nold track ',old_track track_type=old_track['type'] spec_fields=PPdefinitions.new_tracks[track_type] left_overs=dict() # go through track and delete fields not in spec for key in old_track.keys(): if key in spec_fields: left_overs[key]=old_track[key] #print '\n leftovers',left_overs replacement_track=copy.deepcopy(PPdefinitions.new_tracks[track_type]) #print '\n before update', replacement_track replacement_track.update(left_overs) #print '\nafter update',replacement_track replacement_tracks.append(copy.deepcopy(replacement_track)) return replacement_tracks def update_shows(self,old_shows): # get correct spec from type of field replacement_shows=[] for old_show in old_shows: show_type=old_show['type'] spec_fields=PPdefinitions.new_shows[show_type] left_overs=dict() # go through track and delete fields not in spec for key in old_show.keys(): if key in spec_fields: left_overs[key]=old_show[key] # print '\n leftovers',left_overs replacement_show=copy.deepcopy(PPdefinitions.new_shows[show_type]) replacement_show.update(left_overs) replacement_shows.append(copy.deepcopy(replacement_show)) return replacement_shows
class PiPresents(object): def pipresents_version(self): vitems=self.pipresents_issue.split('.') if len(vitems)==2: # cope with 2 digit version numbers before 1.3.2 return 1000*int(vitems[0])+100*int(vitems[1]) else: return 1000*int(vitems[0])+100*int(vitems[1])+int(vitems[2]) def __init__(self): gc.set_debug(gc.DEBUG_UNCOLLECTABLE|gc.DEBUG_INSTANCES|gc.DEBUG_OBJECTS|gc.DEBUG_SAVEALL) self.pipresents_issue="1.3.2" self.pipresents_minorissue = '1.3.2a' # position and size of window without -f command line option self.nonfull_window_width = 0.45 # proportion of width self.nonfull_window_height= 0.7 # proportion of height self.nonfull_window_x = 0 # position of top left corner self.nonfull_window_y=0 # position of top left corner StopWatch.global_enable=False # set up the handler for SIGTERM signal.signal(signal.SIGTERM,self.handle_sigterm) # **************************************** # Initialisation # *************************************** # get command line options self.options=command_options() # get Pi Presents code directory pp_dir=sys.path[0] self.pp_dir=pp_dir if not os.path.exists(pp_dir+"/pipresents.py"): if self.options['manager'] is False: tkMessageBox.showwarning("Pi Presents","Bad Application Directory") exit(102) # Initialise logging and tracing Monitor.log_path=pp_dir self.mon=Monitor() # Init in PiPresents only self.mon.init() # uncomment to enable control of logging from within a class # Monitor.enable_in_code = True # enables control of log level in the code for a class - self.mon.set_log_level() # make a shorter list to log/trace only some classes without using enable_in_code. Monitor.classes = ['PiPresents', 'HyperlinkShow','RadioButtonShow','ArtLiveShow','ArtMediaShow','MediaShow','LiveShow','MenuShow', 'GapShow','Show','ArtShow', 'AudioPlayer','BrowserPlayer','ImagePlayer','MenuPlayer','MessagePlayer','VideoPlayer','Player', 'MediaList','LiveList','ShowList', 'PathManager','ControlsManager','ShowManager','PluginManager', 'MplayerDriver','OMXDriver','UZBLDriver', 'KbdDriver','GPIODriver','TimeOfDay','ScreenDriver','Animate','OSCDriver', 'Network','Mailer' ] # Monitor.classes=['PiPresents','MediaShow','GapShow','Show','VideoPlayer','Player','OMXDriver'] # get global log level from command line Monitor.log_level = int(self.options['debug']) Monitor.manager = self.options['manager'] # print self.options['manager'] self.mon.newline(3) self.mon.sched (self, "Pi Presents is starting, Version:"+self.pipresents_minorissue + ' at '+time.strftime("%Y-%m-%d %H:%M.%S")) self.mon.log (self, "Pi Presents is starting, Version:"+self.pipresents_minorissue+ ' at '+time.strftime("%Y-%m-%d %H:%M.%S")) # self.mon.log (self," OS and separator:" + os.name +' ' + os.sep) self.mon.log(self,"sys.path[0] - location of code: "+sys.path[0]) # log versions of Raspbian and omxplayer, and GPU Memory with open("/boot/issue.txt") as file: self.mon.log(self,'\nRaspbian: '+file.read()) self.mon.log(self,'\n'+check_output(["omxplayer", "-v"])) self.mon.log(self,'\nGPU Memory: '+check_output(["vcgencmd", "get_mem", "gpu"])) if "DESKTOP_SESSION" not in os.environ: print 'Pi Presents must be run from the Desktop' self.mon.log(self,'Pi Presents must be run from the Desktop') self.mon.finish() sys.exit(102) else: self.mon.log(self,'Desktop is '+ os.environ['DESKTOP_SESSION']) # optional other classes used self.root=None self.ppio=None self.tod=None self.animate=None self.gpiodriver=None self.oscdriver=None self.osc_enabled=False self.gpio_enabled=False self.tod_enabled=False self.email_enabled=False if os.geteuid() == 0: self.mon.err(self,'Do not run Pi Presents with sudo') self.end('error','Do not run Pi Presents with sudo') user=os.getenv('USER') self.mon.log(self,'User is: '+ user) # self.mon.log(self,"os.getenv('HOME') - user home directory (not used): " + os.getenv('HOME')) # does not work # self.mon.log(self,"os.path.expanduser('~') - user home directory: " + os.path.expanduser('~')) # does not work # check network is available self.network_connected=False self.network_details=False self.interface='' self.ip='' self.unit='' # sets self.network_connected and self.network_details self.init_network() # start the mailer and send email when PP starts self.email_enabled=False if self.network_connected is True: self.init_mailer() if self.email_enabled is True and self.mailer.email_at_start is True: subject= '[Pi Presents] ' + self.unit + ': PP Started on ' + time.strftime("%Y-%m-%d %H:%M") message = time.strftime("%Y-%m-%d %H:%M") + '\nUnit: ' + self.unit + ' Profile: '+ self.options['profile']+ '\n ' + self.interface + '\n ' + self.ip self.send_email('start',subject,message) # get profile path from -p option if self.options['profile'] != '': self.pp_profile_path="/pp_profiles/"+self.options['profile'] else: self.mon.err(self,"Profile not specified in command ") self.end('error','Profile not specified with the commands -p option') # get directory containing pp_home from the command, if self.options['home'] == "": home = os.sep+ 'home' + os.sep + user + os.sep+"pp_home" else: home = self.options['home'] + os.sep+ "pp_home" self.mon.log(self,"pp_home directory is: " + home) # check if pp_home exists. # try for 10 seconds to allow usb stick to automount found=False for i in range (1, 10): self.mon.log(self,"Trying pp_home at: " + home + " (" + str(i)+')') if os.path.exists(home): found=True self.pp_home=home break time.sleep (1) if found is True: self.mon.log(self,"Found Requested Home Directory, using pp_home at: " + home) else: self.mon.err(self,"Failed to find pp_home directory at " + home) self.end('error',"Failed to find pp_home directory at " + home) # check profile exists self.pp_profile=self.pp_home+self.pp_profile_path if os.path.exists(self.pp_profile): self.mon.sched(self,"Running profile: " + self.pp_profile_path) self.mon.log(self,"Found Requested profile - pp_profile directory is: " + self.pp_profile) else: self.mon.err(self,"Failed to find requested profile: "+ self.pp_profile) self.end('error',"Failed to find requested profile: "+ self.pp_profile) self.mon.start_stats(self.options['profile']) if self.options['verify'] is True: val =Validator() if val.validate_profile(None,pp_dir,self.pp_home,self.pp_profile,self.pipresents_issue,False) is False: self.mon.err(self,"Validation Failed") self.end('error','Validation Failed') # initialise and read the showlist in the profile self.showlist=ShowList() self.showlist_file= self.pp_profile+ "/pp_showlist.json" if os.path.exists(self.showlist_file): self.showlist.open_json(self.showlist_file) else: self.mon.err(self,"showlist not found at "+self.showlist_file) self.end('error',"showlist not found at "+self.showlist_file) # check profile and Pi Presents issues are compatible if self.showlist.profile_version() != self.pipresents_version(): self.mon.err(self,"Version of showlist " + self.showlist.profile_version_string + " is not same as Pi Presents") self.end('error',"Version of showlist " + self.showlist.profile_version_string + " is not same as Pi Presents") # get the 'start' show from the showlist index = self.showlist.index_of_show('start') if index >=0: self.showlist.select(index) self.starter_show=self.showlist.selected_show() else: self.mon.err(self,"Show [start] not found in showlist") self.end('error',"Show [start] not found in showlist") # ******************** # SET UP THE GUI # ******************** # turn off the screenblanking and saver if self.options['noblank'] is True: call(["xset","s", "off"]) call(["xset","s", "-dpms"]) self.root=Tk() self.title='Pi Presents - '+ self.pp_profile self.icon_text= 'Pi Presents' self.root.title(self.title) self.root.iconname(self.icon_text) self.root.config(bg=self.starter_show['background-colour']) self.mon.log(self, 'monitor screen dimensions are ' + str(self.root.winfo_screenwidth()) + ' x ' + str(self.root.winfo_screenheight()) + ' pixels') if self.options['screensize'] =='': self.screen_width = self.root.winfo_screenwidth() self.screen_height = self.root.winfo_screenheight() else: reason,message,self.screen_width,self.screen_height=self.parse_screen(self.options['screensize']) if reason =='error': self.mon.err(self,message) self.end('error',message) self.mon.log(self, 'forced screen dimensions (--screensize) are ' + str(self.screen_width) + ' x ' + str(self.screen_height) + ' pixels') # set window dimensions and decorations if self.options['fullscreen'] is False: self.window_width=int(self.root.winfo_screenwidth()*self.nonfull_window_width) self.window_height=int(self.root.winfo_screenheight()*self.nonfull_window_height) self.window_x=self.nonfull_window_x self.window_y=self.nonfull_window_y self.root.geometry("%dx%d%+d%+d" % (self.window_width,self.window_height,self.window_x,self.window_y)) else: self.window_width=self.screen_width self.window_height=self.screen_height self.root.attributes('-fullscreen', True) os.system('unclutter &') self.window_x=0 self.window_y=0 self.root.geometry("%dx%d%+d%+d" % (self.window_width,self.window_height,self.window_x,self.window_y)) self.root.attributes('-zoomed','1') # canvas cover the whole screen whatever the size of the window. self.canvas_height=self.screen_height self.canvas_width=self.screen_width # make sure focus is set. self.root.focus_set() # define response to main window closing. self.root.protocol ("WM_DELETE_WINDOW", self.handle_user_abort) # setup a canvas onto which will be drawn the images or text self.canvas = Canvas(self.root, bg=self.starter_show['background-colour']) if self.options['fullscreen'] is True: self.canvas.config(height=self.canvas_height, width=self.canvas_width, highlightthickness=0) else: self.canvas.config(height=self.canvas_height, width=self.canvas_width, highlightthickness=1, highlightcolor='yellow') self.canvas.place(x=0,y=0) # self.canvas.config(bg='black') self.canvas.focus_set() # **************************************** # INITIALISE THE INPUT DRIVERS # **************************************** # each driver takes a set of inputs, binds them to symboic names # and sets up a callback which returns the symbolic name when an input event occurs/ # use keyboard driver to bind keys to symbolic names and to set up callback kbd=KbdDriver() if kbd.read(pp_dir,self.pp_home,self.pp_profile) is False: self.end('error','cannot find, or error in keys.cfg') kbd.bind_keys(self.root,self.handle_input_event) self.sr=ScreenDriver() # read the screen click area config file reason,message = self.sr.read(pp_dir,self.pp_home,self.pp_profile) if reason == 'error': self.end('error','cannot find, or error in screen.cfg') # create click areas on the canvas, must be polygon as outline rectangles are not filled as far as find_closest goes # click areas are made on the Pi Presents canvas not the show canvases. reason,message = self.sr.make_click_areas(self.canvas,self.handle_input_event) if reason == 'error': self.mon.err(self,message) self.end('error',message) # **************************************** # INITIALISE THE APPLICATION AND START # **************************************** self.shutdown_required=False self.terminate_required=False self.exitpipresents_required=False # delete omxplayer dbus files # if os.path.exists("/tmp/omxplayerdbus.{}".format(user)): # os.remove("/tmp/omxplayerdbus.{}".format(user)) # if os.path.exists("/tmp/omxplayerdbus.{}.pid".format(user)): # os.remove("/tmp/omxplayerdbus.{}.pid".format(user)) # kick off GPIO if enabled by command line option self.gpio_enabled=False if os.path.exists(self.pp_profile + os.sep + 'pp_io_config'+os.sep+ 'gpio.cfg'): # initialise the GPIO self.gpiodriver=GPIODriver() reason,message=self.gpiodriver.init(pp_dir,self.pp_home,self.pp_profile,self.canvas,50,self.handle_input_event) if reason == 'error': self.end('error',message) else: self.gpio_enabled=True # and start polling gpio self.gpiodriver.poll() # kick off animation sequencer self.animate = Animate() self.animate.init(pp_dir,self.pp_home,self.pp_profile,self.canvas,200,self.handle_output_event) self.animate.poll() #create a showmanager ready for time of day scheduler and osc server show_id=-1 self.show_manager=ShowManager(show_id,self.showlist,self.starter_show,self.root,self.canvas,self.pp_dir,self.pp_profile,self.pp_home) # first time through set callback to terminate Pi Presents if all shows have ended. self.show_manager.init(self.canvas,self.all_shows_ended_callback,self.handle_command,self.showlist) # Register all the shows in the showlist reason,message=self.show_manager.register_shows() if reason == 'error': self.mon.err(self,message) self.end('error',message) # Init OSCDriver, read config and start OSC server self.osc_enabled=False if self.network_connected is True: if os.path.exists(self.pp_profile + os.sep + 'pp_io_config'+ os.sep + 'osc.cfg'): self.oscdriver=OSCDriver() reason,message=self.oscdriver.init(self.pp_profile,self.handle_command,self.handle_input_event,self.e_osc_handle_output_event) if reason == 'error': self.mon.err(self,message) self.end('error',message) else: self.osc_enabled=True self.root.after(1000,self.oscdriver.start_server()) # enable ToD scheduler if schedule exists if os.path.exists(self.pp_profile + os.sep + 'schedule.json'): self.tod_enabled = True else: self.tod_enabled=False # warn if the network not available when ToD required if self.tod_enabled is True and self.network_connected is False: self.mon.warn(self,'Network not connected so Time of Day scheduler may be using the internal clock') # warn about start shows and scheduler if self.starter_show['start-show']=='' and self.tod_enabled is False: self.mon.sched(self,"No Start Shows in Start Show and no shows scheduled") self.mon.warn(self,"No Start Shows in Start Show and no shows scheduled") if self.starter_show['start-show'] !='' and self.tod_enabled is True: self.mon.sched(self,"Start Shows in Start Show and shows scheduled - conflict?") self.mon.warn(self,"Start Shows in Start Show and shows scheduled - conflict?") # run the start shows self.run_start_shows() # kick off the time of day scheduler which may run additional shows if self.tod_enabled is True: self.tod=TimeOfDay() self.tod.init(pp_dir,self.pp_home,self.pp_profile,self.root,self.handle_command) self.tod.poll() # start Tkinters event loop self.root.mainloop( ) def parse_screen(self,size_text): fields=size_text.split('*') if len(fields)!=2: return 'error','do not understand --screensize comand option',0,0 elif fields[0].isdigit() is False or fields[1].isdigit() is False: return 'error','dimensions are not positive integers in --screensize',0,0 else: return 'normal','',int(fields[0]),int(fields[1]) # ********************* # RUN START SHOWS # ******************** def run_start_shows(self): self.mon.trace(self,'run start shows') # parse the start shows field and start the initial shows show_refs=self.starter_show['start-show'].split() for show_ref in show_refs: reason,message=self.show_manager.control_a_show(show_ref,'open') if reason == 'error': self.mon.err(self,message) # ********************* # User inputs # ******************** # handles one command provided as a line of text def handle_command(self,command_text,source='',show=''): # print 'PIPRESENTS ',command_text,source,'from',show self.mon.log(self,"command received: " + command_text) if command_text.strip()=="": return if command_text[0]=='/': if self.osc_enabled is True: self.oscdriver.send_command(command_text) return fields= command_text.split() show_command=fields[0] if len(fields)>1: show_ref=fields[1] else: show_ref='' if show_command in ('open','close'): self.mon.sched(self, command_text + ' received from show:'+show) if self.shutdown_required is False and self.terminate_required is False: reason,message=self.show_manager.control_a_show(show_ref,show_command) else: return elif show_command =='monitor': self.handle_monitor_command(show_ref) return elif show_command == 'event': self.handle_input_event(show_ref,'Show Control') return elif show_command == 'exitpipresents': self.exitpipresents_required=True if self.show_manager.all_shows_exited() is True: # need root.after to get out of st thread self.root.after(1,self.e_all_shows_ended_callback) return else: reason,message= self.show_manager.exit_all_shows() elif show_command == 'shutdownnow': # need root.after to get out of st thread self.root.after(1,self.e_shutdown_pressed) return else: reason='error' message = 'command not recognised: '+ show_command if reason=='error': self.mon.err(self,message) return def handle_monitor_command(self,command): if command == 'on': os.system('vcgencmd display_power 1 >/dev/null') elif command == 'off': os.system('vcgencmd display_power 0 >/dev/null') def e_all_shows_ended_callback(self): self.all_shows_ended_callback('normal','no shows running') def e_shutdown_pressed(self): self.shutdown_pressed('now') def e_osc_handle_output_event(self,line): #jump out of server thread self.root.after(1, lambda arg=line: self.osc_handle_output_event(arg)) def osc_handle_output_event(self,line): self.mon.log(self,"output event received: "+ line) #osc sends output events as a string reason,message,delay,name,param_type,param_values=self.animate.parse_animate_fields(line) if reason == 'error': self.mon.err(self,message) self.end(reason,message) self.handle_output_event(name,param_type,param_values,0) def handle_output_event(self,symbol,param_type,param_values,req_time): if self.gpio_enabled is True: reason,message=self.gpiodriver.handle_output_event(symbol,param_type,param_values,req_time) if reason =='error': self.mon.err(self,message) self.end(reason,message) else: self.mon.warn(self,'GPIO not enabled') # all input events call this callback with a symbolic name. # handle events that affect PP overall, otherwise pass to all active shows def handle_input_event(self,symbol,source): self.mon.log(self,"event received: "+symbol + ' from '+ source) if symbol == 'pp-terminate': self.handle_user_abort() elif symbol == 'pp-shutdown': self.shutdown_pressed('delay') elif symbol == 'pp-shutdownnow': # need root.after to grt out of st thread self.root.after(1,self.e_shutdown_pressed) return elif symbol == 'pp-exitpipresents': self.exitpipresents_required=True if self.show_manager.all_shows_exited() is True: # need root.after to grt out of st thread self.root.after(1,self.e_all_shows_ended_callback) return reason,message= self.show_manager.exit_all_shows() else: # events for shows affect the show and could cause it to exit. for show in self.show_manager.shows: show_obj=show[ShowManager.SHOW_OBJ] if show_obj is not None: show_obj.handle_input_event(symbol) def shutdown_pressed(self, when): if when == 'delay': self.root.after(5000,self.on_shutdown_delay) else: self.shutdown_required=True if self.show_manager.all_shows_exited() is True: self.all_shows_ended_callback('normal','no shows running') else: # calls exit method of all shows, results in all_shows_closed_callback self.show_manager.exit_all_shows() def on_shutdown_delay(self): # 5 second delay is up, if shutdown button still pressed then shutdown if self.gpiodriver.shutdown_pressed() is True: self.shutdown_required=True if self.show_manager.all_shows_exited() is True: self.all_shows_ended_callback('normal','no shows running') else: # calls exit method of all shows, results in all_shows_closed_callback self.show_manager.exit_all_shows() def handle_sigterm(self,signum,frame): self.mon.log(self,'SIGTERM received - '+ str(signum)) self.terminate() def handle_user_abort(self): self.mon.log(self,'User abort received') self.terminate() def terminate(self): self.mon.log(self, "terminate received") self.terminate_required=True needs_termination=False for show in self.show_manager.shows: # print show[ShowManager.SHOW_OBJ], show[ShowManager.SHOW_REF] if show[ShowManager.SHOW_OBJ] is not None: needs_termination=True self.mon.log(self,"Sent terminate to show "+ show[ShowManager.SHOW_REF]) # call shows terminate method # eventually the show will exit and after all shows have exited all_shows_callback will be executed. show[ShowManager.SHOW_OBJ].terminate() if needs_termination is False: self.end('killed','killed - no termination of shows required') # ****************************** # Ending Pi Presents after all the showers and players are closed # ************************** # callback from ShowManager when all shows have ended def all_shows_ended_callback(self,reason,message): self.canvas.config(bg=self.starter_show['background-colour']) if reason in ('killed','error') or self.shutdown_required is True or self.exitpipresents_required is True: self.end(reason,message) def end(self,reason,message): self.mon.log(self,"Pi Presents ending with reason: " + reason) if self.root is not None: self.root.destroy() self.tidy_up() # gc.collect() # print gc.garbage if reason == 'killed': if self.email_enabled is True and self.mailer.email_on_terminate is True: subject= '[Pi Presents] ' + self.unit + ': PP Exited with reason: Terminated' message = time.strftime("%Y-%m-%d %H:%M") + '\n ' + self.unit + '\n ' + self.interface + '\n ' + self.ip self.send_email(reason,subject,message) self.mon.sched(self, "Pi Presents Terminated, au revoir\n") self.mon.log(self, "Pi Presents Terminated, au revoir") # close logging files self.mon.finish() sys.exit(101) elif reason == 'error': if self.email_enabled is True and self.mailer.email_on_error is True: subject= '[Pi Presents] ' + self.unit + ': PP Exited with reason: Error' message_text = 'Error message: '+ message + '\n'+ time.strftime("%Y-%m-%d %H:%M") + '\n ' + self.unit + '\n ' + self.interface + '\n ' + self.ip self.send_email(reason,subject,message_text) self.mon.sched(self, "Pi Presents closing because of error, sorry\n") self.mon.log(self, "Pi Presents closing because of error, sorry") # close logging files self.mon.finish() sys.exit(102) else: self.mon.sched(self,"Pi Presents exiting normally, bye\n") self.mon.log(self,"Pi Presents exiting normally, bye") # close logging files self.mon.finish() if self.shutdown_required is True: # print 'SHUTDOWN' call (['sudo','shutdown','now','SHUTTING DOWN']) sys.exit(100) def init_network(self): timeout=int(self.options['nonetwork']) if timeout== 0: self.network_connected=False self.unit='' self.ip='' self.interface='' return self.network=Network() self.network_connected=False # try to connect to network self.mon.log (self, 'Waiting up to '+ str(timeout) + ' seconds for network') success=self.network.wait_for_network(timeout) if success is False: self.mon.warn(self,'Failed to connect to network after ' + str(timeout) + ' seconds') # tkMessageBox.showwarning("Pi Presents","Failed to connect to network so using fake-hwclock") return self.network_connected=True self.mon.sched (self, 'Time after network check is '+ time.strftime("%Y-%m-%d %H:%M.%S")) self.mon.log (self, 'Time after network check is '+ time.strftime("%Y-%m-%d %H:%M.%S")) # Get web configuration self.network_details=False network_options_file_path=self.pp_dir+os.sep+'pp_config'+os.sep+'pp_web.cfg' if not os.path.exists(network_options_file_path): self.mon.warn(self,"pp_web.cfg not found at "+network_options_file_path) return self.mon.log(self, 'Found pp_web.cfg in ' + network_options_file_path) self.network.read_config(network_options_file_path) self.unit=self.network.unit # get interface and IP details of preferred interface self.interface,self.ip = self.network.get_preferred_ip() if self.interface == '': self.network_connected=False return self.network_details=True self.mon.log (self, 'Network details ' + self.unit + ' ' + self.interface + ' ' +self.ip) def init_mailer(self): self.email_enabled=False email_file_path = self.pp_dir+os.sep+'pp_config'+os.sep+'pp_email.cfg' if not os.path.exists(email_file_path): self.mon.log(self,'pp_email.cfg not found at ' + email_file_path) return self.mon.log(self,'Found pp_email.cfg at ' + email_file_path) self.mailer=Mailer() self.mailer.read_config(email_file_path) # all Ok so can enable email if config file allows it. if self.mailer.email_allowed is True: self.email_enabled=True self.mon.log (self,'Email Enabled') def send_email(self,reason,subject,message): if self.try_connect() is False: return False else: success,error = self.mailer.send(subject,message) if success is False: self.mon.log(self, 'Failed to send email: ' + str(error)) success,error=self.mailer.disconnect() if success is False: self.mon.log(self,'Failed disconnect after send:' + str(error)) return False else: self.mon.log(self,'Sent email for ' + reason) success,error=self.mailer.disconnect() if success is False: self.mon.log(self,'Failed disconnect from email server ' + str(error)) return True def try_connect(self): tries=1 while True: success, error = self.mailer.connect() if success is True: return True else: self.mon.log(self,'Failed to connect to email SMTP server ' + str(tries) + '\n ' +str(error)) tries +=1 if tries >5: self.mon.log(self,'Failed to connect to email SMTP server after ' + str(tries)) return False # tidy up all the peripheral bits of Pi Presents def tidy_up(self): self.handle_monitor_command('on') self.mon.log(self, "Tidying Up") # turn screen blanking back on if self.options['noblank'] is True: call(["xset","s", "on"]) call(["xset","s", "+dpms"]) # tidy up animation and gpio if self.animate is not None: self.animate.terminate() if self.gpio_enabled==True: self.gpiodriver.terminate() if self.osc_enabled is True: self.oscdriver.terminate() # tidy up time of day scheduler if self.tod_enabled is True: self.tod.terminate()
class MediaShow: # ******************* # External interface # ******************** def __init__(self, show, canvas, showlist, pp_home, pp_profile): """ canvas - the canvas that the menu is to be written on show - the dictionary fo the show to be played pp_home - Pi presents data_home directory pp_profile - Pi presents profile directory """ self.mon=Monitor() self.mon.on() #instantiate arguments self.show =show self.showlist=showlist self.canvas=canvas self.pp_home=pp_home self.pp_profile=pp_profile # open resources self.rr=ResourceReader() # Init variables self.player=None self.shower=None self._poll_for_interval_timer=None self._poll_for_continue_timer=None self._waiting_for_interval=False self._interval_timer=None self.error=False self._interval_timer_signal=False self._end_mediashow_signal=False self._next_track_signal=False self._previous_track_signal=False self._play_child_signal = False self._req_next='nil' self._state='closed' def play(self,end_callback,ready_callback=None, top=False,command='nil'): """ displays the mediashow end_callback - function to be called when the menu exits ready_callback - callback when menu is ready to display (not used) top is True when the show is top level (run from [start]) """ #instantiate the arguments self._end_callback=end_callback self._ready_callback=ready_callback self.top=top self.command=command self.mon.log(self,"Starting show: " + self.show['show-ref']) # check data files are available. self.media_file = self.pp_profile + "/" + self.show['medialist'] if not os.path.exists(self.media_file): self.mon.err(self,"Medialist file not found: "+ self.media_file) self._end('error',"Medialist file not found") #create a medialist for the mediashow and read it. self.medialist=MediaList() if self.medialist.open_list(self.media_file,self.showlist.sissue())==False: self.mon.err(self,"Version of medialist different to Pi Presents") self._end('error',"Version of medialist different to Pi Presents") self._wait_for_trigger() # respond to key presses. def key_pressed(self,key_name): self.mon.log(self,"received key: " + key_name) if key_name=='': pass elif key_name=='escape': # if next lower show is running pass down to stop the show and lower level if self.shower<>None: self.shower.key_pressed(key_name) # if not at top stop the show else: if self.top == False: self._end_mediashow_signal=True # and if a track is running stop that first if self.player<>None: self.player.key_pressed(key_name) else: # at top level in a manual presentation stop the track if self.show['progress']=='manual': if self.player<>None: self.player.key_pressed(key_name) elif key_name in ('up','down'): # if child or sub-show is running and is a show pass to show, track does not use up/down # otherwise use keys for next or previous if self.shower<>None: self.shower.key_pressed(key_name) else: if key_name=='up': self._previous() else: self._next() elif key_name=='return': # if child show or sub-show is running and is show - pass down- player does not use return # ELSE use Return to start child or to start the show if waiting if self.shower<>None: self.shower.key_pressed(key_name) else: if self._state=='playing': if self.show['has-child']=='yes': self._play_child_signal=True # and stop the current track if its running if self.player<>None: self.player.key_pressed("escape") else: self._start_show() elif key_name=='pir': self._start_show() elif key_name in ('p',' '): # pass down if show or track running. if self.shower<>None: self.shower.key_pressed(key_name) elif self.player<>None: self.player.key_pressed(key_name) def button_pressed(self,button,edge): if button=='play': self.key_pressed("return") elif button =='up': self.key_pressed("up") elif button=='down': self.key_pressed("down") elif button=='stop': self.key_pressed("escape") elif button=='pause': self.key_pressed('p') elif button=='PIR': self.key_pressed('pir') # kill or error def terminate(self,reason): if self.shower<>None: self.mon.log(self,"sent terminate to shower") self.shower.terminate(reason) elif self.player<>None: self.mon.log(self,"sent terminate to player") self.player.terminate(reason) else: self._end(reason,'terminated without terminating shower or player') def _tidy_up(self): if self._poll_for_continue_timer<>None: self.canvas.after_cancel(self._poll_for_continue_timer) self._poll_for_continue_timer=None if self._poll_for_interval_timer<>None: self.canvas.after_cancel(self._poll_for_interval_timer) self._poll_for_interval_timer=None if self._interval_timer<>None: self.canvas.after_cancel(self._interval_timer) self._interval_timer=None def resource(self,section,item): value=self.rr.get(section,item) if value==False: self.mon.err(self, "resource: "+section +': '+ item + " not found" ) self.terminate("error") else: return value # *************************** # Respond to key/button presses # *************************** def _stop(self,message): self._end_mediashow_signal=True if self._interval_timer<>None: self.canvas.after_cancel(self._interval_timer) def _next(self): # stop track if running and set signal self._next_track_signal=True if self.shower<>None: self.shower.key_pressed("escape") else: if self.player<>None: self.player.key_pressed("escape") def _previous(self): self._previous_track_signal=True if self.shower<>None: self.shower.key_pressed("escape") else: if self.player<>None: self.player.key_pressed("escape") # *************************** # end of show functions # *************************** def _end(self,reason,message): self._end_mediashow_signal=False self.mon.log(self,"Ending Mediashow: "+ self.show['show-ref']) self._tidy_up() self._end_callback(reason,message) self=None return # *************************** # Show sequencer # *************************** def _wait_for_trigger(self): self._state='waiting' if self.ready_callback<>None: self.ready_callback() self.mon.log(self,"Waiting for trigger: "+ self.show['trigger']) if self.show['trigger']=="button": # blank screen waiting for trigger if auto, otherwise display something if self.show['progress']=="manual": text= self.resource('mediashow','m01') else: text="" self.display_message(self.canvas,'text',text,0,self._start_show) elif self.show['trigger']=="PIR": # blank screen waiting for trigger text = self.resource('mediashow','m02') self.display_message(self.canvas,'text',text,0,self._start_show) elif self.show['trigger']=="start": self._start_show() else: self.mon.err(self,"Unknown trigger: "+ self.show['trigger']) self._end('error',"Unknown trigger type") def _start_show(self): self._state='playing' self._direction='forward' # start interval timer if self.show['repeat']=="interval" and self.show['repeat-interval']<>0: self._interval_timer_signal=False self._interval_timer=self.canvas.after(int(self.show['repeat-interval'])*1000,self._end_interval_timer) # and play the first track unless commanded otherwise if self.command=='backward': self.medialist.finish() else: self.medialist.start() self._play_selected_track(self.medialist.selected_track()) def _what_next(self): self._direction='forward' # user wants to end, wait for any shows or tracks to have ended then end show if self._end_mediashow_signal==True: if self.player==None and self.shower==None: self._end_mediashow_signal=False self._end('normal',"show ended by user") else: pass #returning from a subshow needing to move onward elif self._req_next=='do-next': self._req_next='nil' self.medialist.next() self._play_selected_track(self.medialist.selected_track()) #returning from a subshow needing to move backward elif self._req_next=='do-previous': self._req_next='nil' self._direction='backward' self.medialist.previous() self._play_selected_track(self.medialist.selected_track()) # user wants to play child elif self._play_child_signal == True: self._play_child_signal=False index = self.medialist.index_of_track('pp-child-show') if index >=0: #don't select the track as need to preserve mediashow sequence. child_track=self.medialist.track(index) self._display_eggtimer(self.resource('mediashow','m07')) self._play_selected_track(child_track) else: self.mon.err(self,"Child show not found in medialist: "+ self.show['pp-child-show']) self._end('error',"child show not found in medialist") # skip to next track on user input elif self._next_track_signal==True: self._next_track_signal=False if self.medialist.at_end()==True: if self.show['sequence']=="ordered" and self.show['repeat']=='oneshot' and self.top==False: self._end('do-next',"Return from Sub Show") else: self.medialist.next() self._play_selected_track(self.medialist.selected_track()) else: self.medialist.next() self._play_selected_track(self.medialist.selected_track()) # skip to previous track on user input elif self._previous_track_signal==True: self._previous_track_signal=False self._direction='backward' if self.medialist.at_start()==True: if self.show['sequence']=="ordered" and self.show['repeat']=='oneshot' and self.top==False: self._end('do-previous',"Return from Sub Show") else: self.medialist.previous() self._play_selected_track(self.medialist.selected_track()) else: self.medialist.previous() self._play_selected_track(self.medialist.selected_track()) # track is finished and we are on auto elif self.show['progress']=="auto": if self.medialist.at_end()==True: if self.show['sequence']=="ordered" and self.show['repeat']=='oneshot' and self.top==False: self._end('do-next',"Return from Sub Show") #### elif elif self.show['sequence']=="ordered" and self.show['repeat']=='oneshot' and self.top==True: self._wait_for_trigger() elif self._waiting_for_interval==True: if self._interval_timer_signal==True: self._interval_timer_signal=False self._waiting_for_interval=False self._start_show() else: self._poll_for_interval_timer=self.canvas.after(1000,self._what_next) elif self.show['sequence']=="ordered" and self.show['repeat']=='interval' and int(self.show['repeat-interval'])>0: self._waiting_for_interval=True self._poll_for_interval_timer=self.canvas.after(1000,self._what_next) elif self.show['sequence']=="ordered" and self.show['repeat']=='interval' and int(self.show['repeat-interval'])==0: self.medialist.next() self._play_selected_track(self.medialist.selected_track()) else: self.mon.err(self,"Unhandled playing event: ") self._end('error',"Unhandled playing event") else: self.medialist.next() self._play_selected_track(self.medialist.selected_track()) # track has finished and we are on manual progress elif self.show['progress']=="manual": self._delete_eggtimer() self._display_eggtimer(self.resource('mediashow','m03')) self._poll_for_continue_timer=self.canvas.after(500,self._what_next) else: #unhandled state self.mon.err(self,"Unhandled playing event: ") self._end('error',"Unhandled playing event") def _end_interval_timer(self): self._interval_timer_signal=True # *************************** # Dispatching to Players/Shows # *************************** # used to display internal messages in situations where a medialist entry could be used. def display_message(self,canvas,source,content,duration,_display_message_callback): self._display_message_callback=_display_message_callback tp={'duration':duration,'message-colour':'white','message-font':'Helvetica 20 bold'} self.player=MessagePlayer(canvas,tp,tp) self.player.play(content,self._display_message_end,None) def _display_message_end(self,reason,message): self.player=None if reason in ('error','killed'): self._end(reason,message) else: self._display_message_callback() def complete_path(self,selected_track): # complete path of the filename of the selected entry track_file = selected_track['location'] if track_file[0]=="+": track_file=self.pp_home+track_file[1:] self.mon.log(self,"Track to play is: "+ track_file) return track_file def _play_selected_track(self,selected_track): """ selects the appropriate player from type field of the medialist and computes the parameters for that type selected track is a dictionary for the track/show """ self.canvas.delete(ALL) if self.show['progress']=="manual": self._display_eggtimer(self.resource('mediashow','m04')) # is menu required if self.show['has-child']=="yes": enable_child=True else: enable_child=False #dispatch track by type self.player=None self.shower=None track_type = selected_track['type'] self.mon.log(self,"Track type is: "+ track_type) if track_type=="video": # create a videoplayer track_file=self.complete_path(selected_track) self.player=VideoPlayer(self.canvas,self.show,selected_track) self.player.play(track_file, self.end_player, self.ready_callback, enable_menu=enable_child) elif track_type=="image": track_file=self.complete_path(selected_track) # images played from menus don't have children self.player=ImagePlayer(self.canvas,self.show,selected_track) self.player.play(track_file, self.end_player, self.ready_callback, enable_menu=enable_child) elif track_type=="message": # bit odd because MessagePlayer is used internally to display text. text=selected_track['text'] self.player=MessagePlayer(self.canvas,self.show,selected_track) self.player.play(text, self.end_player, self.ready_callback, enable_menu=enable_child ) elif track_type=="show": # get the show from the showlist index = self.showlist.index_of_show(selected_track['sub-show']) if index >=0: self.showlist.select(index) selected_show=self.showlist.selected_show() else: self.mon.err(self,"Show not found in showlist: "+ selected_track['sub-show']) self._end('error',"Unknown show") if selected_show['type']=="mediashow": self.shower= MediaShow(selected_show, self.canvas, self.showlist, self.pp_home, self.pp_profile) self.shower.play(self.end_shower,top=False,command=self._direction) elif selected_show['type']=="liveshow": self.shower= LiveShow(selected_show, self.canvas, self.showlist, self.pp_home, self.pp_profile) self.shower.play(self.end_shower,top=False,command='nil') elif selected_show['type']=="menu": self.shower= MenuShow(selected_show, self.canvas, self.showlist, self.pp_home, self.pp_profile) self.shower.play(self.end_shower,top=False,command='nil') else: self.mon.err(self,"Unknown Show Type: "+ selected_show['type']) self._end('error'"Unknown show type") else: self.mon.err(self,"Unknown Track Type: "+ track_type) self._end('error',"Unknown track type") def ready_callback(self): self._delete_eggtimer() def end_player(self,reason,message): self._req_next='nil' self.mon.log(self,"Returned from player with message: "+ message) self.player=None if reason in("killed","error"): self._end(reason,message) elif self.show['progress']=="manual": self._display_eggtimer(self.resource('mediashow','m05')) self._req_next=reason self._what_next() else: self._req_next=reason self._what_next() def end_shower(self,reason,message): self._req_next='nil' self.mon.log(self,"Returned from shower with message: "+ message) self.shower=None if reason in("killed","error"): self._end(reason,message) elif self.show['progress']=="manual": self._display_eggtimer(self.resource('mediashow','m06')) self._req_next=reason self._what_next() else: self._req_next=reason self._what_next() def _display_eggtimer(self,text): self.canvas.create_text(int(self.canvas['width'])/2, int(self.canvas['height'])/2, text= text, fill='white', font="Helvetica 20 bold") self.canvas.update_idletasks( ) def _delete_eggtimer(self): self.canvas.delete(ALL)
class ControlsManager: config = None global_controls = [] def __init__(self): self.mon = Monitor() self.mon.on() #read controls.cfg, done once in Pi Presents def read(self, pp_dir, pp_home, pp_profile): if ControlsManager.config == None: # try inside profile tryfile = pp_profile + os.sep + "controls.cfg" # self.mon.log(self,"Trying controls.cfg in profile at: "+ tryfile) if os.path.exists(tryfile): filename = tryfile else: # try inside pp_home # self.mon.log(self,"controls.cfg not found at "+ tryfile+ " trying pp_home") tryfile = pp_home + os.sep + "controls.cfg" if os.path.exists(tryfile): filename = tryfile else: # try inside pipresents # self.mon.log(self,"controls.cfg not found at "+ tryfile + " trying inside pipresents") tryfile = pp_dir + os.sep + 'pp_home' + os.sep + "controls.cfg" if os.path.exists(tryfile): filename = tryfile else: self.mon.log(self, "controls.cfg not found at " + tryfile) self.mon.err(self, "controls.cfg not found") return False ControlsManager.config = ConfigParser.ConfigParser() ControlsManager.config.read(filename) self.mon.log(self, "controls.cfg read from " + filename) return True def parse_defaults(self): if ControlsManager.config.has_section('controls'): ControlsManager.global_controls = ControlsManager.config.items( 'controls') # print 'global controls ',ControlsManager.global_controls return True else: return False # get the default controls for the show that has been read in by read from controls.cfg def default_controls(self): control_defs = ControlsManager.global_controls controls_list = [] for control_def in control_defs: op = control_def[1] default_name = control_def[0] controls_list.append([default_name, op]) return controls_list # Merge in controls from a show. # the set of default controls for all shows can be overridden in a top level show # by the controls defined in the show # if show has an operation other than 'null' change the symbolic name to that inthe show # if the show has a null operation change the operation of the attached symbolic name to null def merge_show_controls(self, controls_list, show_text): # print 'show text: ',show_text reason, message, show_controls = self.parse_controls(show_text) # print 'show controls:',show_controls # overwrite the default symbolic_name if re-defined in the show for show_control in show_controls: show_name = show_control[0] show_operation = show_control[1] # find the operation in the controls list and change its name # print 'op to change name of: ',show_operation # print 'change to ',show_name for control in controls_list: if control[1] == show_operation: control[0] = show_name # print 'after rename ',controls_list # add additional operations if defined in the show for show_control in show_controls: show_name = show_control[0] show_operation = show_control[1] # is operation in the controls list # print 'op to add: ',show_operation # print 'name to add ',show_name found = False for control in controls_list: if control[1] == show_operation: found = True # if the operation has not been found and it is omx- or mplay- then add it. if found == False and (show_operation[0:4] == 'omx-' or show_operation[0:6] == 'mplay-'): # print 'appending ', show_name,show_operation controls_list.append([show_name, show_operation]) # step through controls list dealing with null new_controls = [] # find the name in controls list and delete it? for control in controls_list: name = control[0] operation = control[1] found = False for show_control in show_controls: show_name = show_control[0] show_operation = show_control[1] if show_name == name and show_operation == 'null': found = True break if found == False: new_controls.append(control) # print 'preserved ',control # print 'merged controls',new_controls return new_controls # parse controls from controls field in a show def parse_controls(self, controls_text): controls = [] lines = controls_text.split('\n') num_lines = 0 for line in lines: if line.strip() == "": continue num_lines += 1 error_text, control = self.parse_control(line.strip()) if error_text <> "": return 'error', error_text, controls controls.append(copy.deepcopy(control)) return 'normal', '', controls def parse_control(self, line): fields = line.split() if len(fields) <> 2: return "incorrect number of fields in control", ['', ''] symbol = fields[0] operation = fields[1] if operation in ( 'stop', 'play', 'up', 'down', 'pause', 'null' ) or operation[0:4] == 'omx-' or operation[0:6] == 'mplay-': return '', [symbol, operation] else: return "unknown operation", ['', '']
class OMXDriver(object): _LAUNCH_CMD = '/usr/bin/omxplayer --no-keys ' # needs changing if user has installed his own version of omxplayer elsewhere KEY_MAP = { '-': 17, '+': 18, '=': 18 } # add more keys here, see popcornmix/omxplayer github def __init__(self, widget, pp_dir): self.widget = widget self.pp_dir = pp_dir self.mon = Monitor() self.start_play_signal = False self.end_play_signal = False self.end_play_reason = 'nothing' self.duration = 0 self.video_position = 0 self.pause_at_end_required = False self.paused_at_end = False self.pause_before_play_required = True self.paused_at_start = False self.paused = False self.terminate_reason = '' # dbus and subprocess self._process = None self.__iface_root = None self.__iface_props = None self.__iface_player = None # legacy self.xbefore = 'not used - legacy' self.xafter = 'not used - legacy' def load(self, track, options, caller): self.caller = caller track = "'" + track.replace("'", "'\\''") + "'" self.dbus_user = os.environ["USER"] self.dbus_name = "org.mpris.MediaPlayer2.omxplayer" + str( int(time() * 10)) self.omxplayer_cmd = OMXDriver._LAUNCH_CMD + options + " --dbus_name '" + self.dbus_name + "' " + track # self.mon.log(self, 'dbus user ' + self.dbus_user) # self.mon.log(self, 'dbus name ' + self.dbus_name) # print self.omxplayer_cmd self.mon.log(self, "Send command to omxplayer: " + self.omxplayer_cmd) # self._process=subprocess.Popen(self.omxplayer_cmd,shell=True,stdout=file('/home/pi/pipresents/pp_logs/stdout.txt','a'),stderr=file('/home/pi/pipresents/pp_logs/stderr.txt','a')) self._process = subprocess.Popen(self.omxplayer_cmd, shell=True, stdout=file('/dev/null', 'a'), stderr=file('/dev/null', 'a')) self.pid = self._process.pid # wait for omxplayer to start then start monitoring thread self.dbus_tries = 0 self.omx_loaded = False self._wait_for_dbus() return def _wait_for_dbus(self): connect_success = self.__dbus_connect() if connect_success is True: # print 'SUCCESS' self.mon.log( self, 'connected to omxplayer dbus after ' + str(self.dbus_tries) + ' centisecs') self.start_play_signal = True if self.pause_before_play_required is True and self.paused_at_start is False: self.pause('after load') self.paused_at_start = True # get duration of the track in microsecs duration_success, duration = self.get_duration() if duration_success is False: self.duration = 0 self.end_play_signal = True self.end_play_reason = 'Failed to read duration - Not connected to omxplayer DBus' else: # pause before last frame self.duration = duration - 350000 # start the thread that is going to monitor output from omxplayer. self._position_thread = Thread(target=self._monitor_status) self._position_thread.start() else: self.dbus_tries += 1 self.widget.after(100, self._wait_for_dbus) def _monitor_status(self): while True: retcode = self._process.poll() # print 'in loop', self.pid, retcode if retcode is not None: # print 'process not running', retcode, self.pid # process is not running because quit or natural end self.end_play_signal = True self.end_play_reason = 'nice_day' break else: # test position ony when process is running, could have race condition if self.paused_at_end is False: # test position only when not paused for the end, in case process is dead success, video_position = self.get_position() if success is False: # print 'process ended when reading video position ' + str(self.video_position) pass # failure can happen because track has ended and process ended. Don't change video position else: self.video_position = video_position # if timestamp is near the end then pause if self.pause_at_end_required is True and self.video_position > self.duration: #microseconds # print 'pausing at end' self.pause(' at end of track') self.paused_at_end = True self.end_play_signal = True self.end_play_reason = 'pause_at_end' sleep(0.05) def show(self, freeze_at_end_required): self.pause_at_end_required = freeze_at_end_required # unpause to start playing self.unpause(' to start showing') def control(self, char): val = OMXDriver.KEY_MAP[char] self.mon.log( self, '>control received and sent to omxplayer ' + str(self.pid)) if self.is_running(): try: self.__iface_player.Action(dbus.Int32(val)) except dbus.exceptions.DBusException: self.mon.warn(self, 'Failed to send control - dbus exception') return else: self.mon.warn(self, 'Failed to send control - process not running') return def pause(self, reason): self.mon.log(self, 'pause received ' + reason) if not self.paused: self.paused = True self.mon.log(self, 'not paused so send pause ' + reason) if self.is_running(): try: self.__iface_player.Pause() except dbus.exceptions.DBusException: self.mon.warn(self, 'Failed to send pause - dbus exception') return else: self.mon.warn(self, 'Failed to send pause - process not running') return def toggle_pause(self, reason): self.mon.log(self, 'toggle pause received ' + reason) if not self.paused: self.paused = True else: self.paused = False if self.is_running(): try: self.__iface_player.Action(16) except dbus.exceptions.DBusException: self.mon.warn(self, 'Failed to toggle pause - dbus exception') return else: self.mon.warn(self, 'Failed to toggle pause - process not running') return def unpause(self, reason): self.mon.log(self, 'Unpause received ' + reason) if self.paused: self.paused = False self.mon.log(self, 'Is paused so Track unpaused ' + reason) if self.is_running(): try: self.__iface_player.Action(16) except dbus.exceptions.DBusException as ex: self.mon.warn( self, 'Failed to send unpause - dbus exception: {}'.format( ex.get_dbus_message())) return # print 'unpause successful' else: self.mon.warn(self, 'Failed to send pause - process not running') return def stop(self): self.mon.log( self, '>stop received and quit sent to omxplayer ' + str(self.pid)) if self.is_running(): try: self.__iface_root.Quit() except dbus.exceptions.DBusException: self.mon.warn(self, 'Failed to quit - dbus exception') return else: self.mon.warn(self, 'Failed to quit - process not running') return # kill the subprocess (omxplayer and omxplayer.bin). Used for tidy up on exit. def terminate(self, reason): self.terminate_reason = reason self.stop() def get_terminate_reason(self): return self.terminate_reason # test of whether _process is running def is_running(self): retcode = self._process.poll() # print 'is alive', retcode if retcode is None: return True else: return False # kill off omxplayer when it hasn't terminated at the end of a track. # send SIGINT (CTRL C) so it has a chance to tidy up daemons and omxplayer.bin def kill(self): if self.is_running(): self._process.send_signal(signal.SIGINT) def get_position(self): """Return the current position""" if self.is_running(): try: micros = self.__iface_props.Position() return True, micros except dbus.exceptions.DBusException: return False, -1 else: return False, -1 def get_duration(self): """Return the total length of the playing media""" if self.is_running(): try: micros = self.__iface_props.Duration() return True, micros except dbus.exceptions.DBusException: return False, -1 else: return False, -1 # ********************* # connect to dbus # ********************* def __dbus_connect(self): if self.omx_loaded is False: # read the omxplayer dbus data from files generated by omxplayer bus_address_filename = "/tmp/omxplayerdbus.{}".format( self.dbus_user) bus_pid_filename = "/tmp/omxplayerdbus.{}.pid".format( self.dbus_user) if not os.path.exists(bus_address_filename): self.mon.log( self, 'waiting for bus address file ' + bus_address_filename) self.omx_loaded = False return False else: f = open(bus_address_filename, "r") bus_address = f.read().rstrip() if bus_address == '': self.mon.log( self, 'waiting for bus address in file ' + bus_address_filename) self.omx_loaded = False return False else: # self.mon.log(self, 'bus address found ' + bus_address) if not os.path.exists(bus_pid_filename): self.mon.warn( self, 'bus pid file does not exist ' + bus_pid_filename) self.omx_loaded = False return False else: f = open(bus_pid_filename, "r") bus_pid = f.read().rstrip() if bus_pid == '': self.omx_loaded = False return False else: # self.mon.log(self, 'bus pid found ' + bus_pid) os.environ[ "DBUS_SESSION_BUS_ADDRESS"] = bus_address os.environ["DBUS_SESSION_BUS_PID"] = bus_pid self.omx_loaded = True if self.omx_loaded is True: session_bus = dbus.SessionBus() try: omx_object = session_bus.get_object(self.dbus_name, "/org/mpris/MediaPlayer2", introspect=False) self.__iface_root = dbus.Interface(omx_object, "org.mpris.MediaPlayer2") self.__iface_props = dbus.Interface( omx_object, "org.freedesktop.DBus.Properties") self.__iface_player = dbus.Interface( omx_object, "org.mpris.MediaPlayer2.Player") except dbus.exceptions.DBusException as ex: # self.mon.log(self,"Waiting for dbus connection to omxplayer: {}".format(ex.get_dbus_message())) return False return True
class GPIODriver(object): """ GPIODriver provides GPIO facilties for Pi presents - configures and binds GPIO pins from data in gpio.cfg - reads and debounces inputs pins, provides callbacks on state changes which generate input events - changes the stae of output pins as required by calling programs """ # constants for buttons # cofiguration from gpio.cfg PIN=0 # pin on RPi board GPIO connector e.g. P1-11 DIRECTION = 1 # IN/OUT/NONE (None is not used) NAME = 2 # symbolic name for output RISING_NAME=3 # symbolic name for rising edge callback FALLING_NAME=4 # symbolic name of falling edge callback ONE_NAME=5 # symbolic name for one state callback ZERO_NAME = 6 # symbolic name for zero state callback REPEAT = 7 # repeat interval for state callbacks (mS) THRESHOLD = 8 # threshold of debounce count for state change to be considered PULL = 9 # pull up or down or none LINKED_NAME = 10 # output pin that follows the input LINKED_INVERT = 11 # invert the linked pin # dynamic data COUNT=12 # variable - count of the number of times the input has been 0 (limited to threshold) PRESSED = 13 # variable - debounced state LAST = 14 # varible - last state - used to detect edge REPEAT_COUNT = 15 TEMPLATE = ['', # pin '', # direction '', # name '','','','', #input names 0, # repeat 0, # threshold '', #pull -1, #linked pin False, # linked invert 0,False,False,0] #dynamics # for A and B # PINLIST = ('P1-03','P1-05','P1-07','P1-08', # 'P1-10','P1-11','P1-12','P1-13','P1-15','P1-16','P1-18','P1-19', # 'P1-21','P1-22','P1-23','P1-24','P1-26') # for A+ and B+ seems to work for A and B PINLIST = ('P1-03','P1-05','P1-07','P1-08', 'P1-10','P1-11','P1-12','P1-13','P1-15','P1-16','P1-18','P1-19', 'P1-21','P1-22','P1-23','P1-24','P1-26','P1-29', 'P1-31','P1-32','P1-33','P1-35','P1-36','P1-37','P1-38','P1-40') # CLASS VARIABLES (GPIODriver.) shutdown_index=0 #index of shutdown pin pins=[] options=None gpio_enabled=False # executed by main program and by each object using gpio def __init__(self): self.mon=Monitor() # executed once from main program def init(self,pp_dir,pp_home,pp_profile,widget,button_tick,button_callback=None): # instantiate arguments self.widget=widget self.pp_dir=pp_dir self.pp_profile=pp_profile self.pp_home=pp_home self.button_tick=button_tick self.button_callback=button_callback GPIODriver.shutdown_index=0 # read gpio.cfg file. reason,message=self.read(self.pp_dir,self.pp_home,self.pp_profile) if reason =='error': return 'error',message import RPi.GPIO as GPIO self.GPIO = GPIO # construct the GPIO control list from the configuration for index, pin_def in enumerate(GPIODriver.PINLIST): pin=copy.deepcopy(GPIODriver.TEMPLATE) pin_bits = pin_def.split('-') pin_num=pin_bits[1:] pin[GPIODriver.PIN]=int(pin_num[0]) if self.config.has_section(pin_def) is False: self.mon.warn(self, "no pin definition for "+ pin_def) pin[GPIODriver.DIRECTION]='None' else: # unused pin if self.config.get(pin_def,'direction') == 'none': pin[GPIODriver.DIRECTION]='none' else: pin[GPIODriver.DIRECTION]=self.config.get(pin_def,'direction') if pin[GPIODriver.DIRECTION] == 'in': # input pin pin[GPIODriver.RISING_NAME]=self.config.get(pin_def,'rising-name') pin[GPIODriver.FALLING_NAME]=self.config.get(pin_def,'falling-name') pin[GPIODriver.ONE_NAME]=self.config.get(pin_def,'one-name') pin[GPIODriver.ZERO_NAME]=self.config.get(pin_def,'zero-name') if self.config.has_option(pin_def,'linked-output'): # print self.config.get(pin_def,'linked-output') pin[GPIODriver.LINKED_NAME]=self.config.get(pin_def,'linked-output') if self.config.get(pin_def,'linked-invert') == 'yes': pin[GPIODriver.LINKED_INVERT]=True else: pin[GPIODriver.LINKED_INVERT]=False else: pin[GPIODriver.LINKED_NAME]= '' pin[GPIODriver.LINKED_INVERT]=False if pin[GPIODriver.FALLING_NAME] == 'pp-shutdown': GPIODriver.shutdown_index=index if self.config.get(pin_def,'repeat') != '': pin[GPIODriver.REPEAT]=int(self.config.get(pin_def,'repeat')) else: pin[GPIODriver.REPEAT]=-1 pin[GPIODriver.THRESHOLD]=int(self.config.get(pin_def,'threshold')) if self.config.get(pin_def,'pull-up-down') == 'up': pin[GPIODriver.PULL]=GPIO.PUD_UP elif self.config.get(pin_def,'pull-up-down') == 'down': pin[GPIODriver.PULL]=GPIO.PUD_DOWN else: pin[GPIODriver.PULL]=GPIO.PUD_OFF else: # output pin pin[GPIODriver.NAME]=self.config.get(pin_def,'name') # print pin GPIODriver.pins.append(copy.deepcopy(pin)) # setup GPIO self.GPIO.setwarnings(True) self.GPIO.setmode(self.GPIO.BOARD) # set up the GPIO inputs and outputs for index, pin in enumerate(GPIODriver.pins): num = pin[GPIODriver.PIN] if pin[GPIODriver.DIRECTION] == 'in': self.GPIO.setup(num,self.GPIO.IN,pull_up_down=pin[GPIODriver.PULL]) elif pin[GPIODriver.DIRECTION] == 'out': self.GPIO.setup(num,self.GPIO.OUT) self.GPIO.setup(num,False) self.reset_input_state() GPIODriver.gpio_enabled=True # init timer self.button_tick_timer=None return 'normal','GPIO initialised' # called by main program only def poll(self): # loop to look at the buttons self.do_buttons() self.button_tick_timer=self.widget.after(self.button_tick,self.poll) # called by main program only def terminate(self): if GPIODriver.gpio_enabled is True: if self.button_tick_timer is not None: self.widget.after_cancel(self.button_tick_timer) self.reset_outputs() self.GPIO.cleanup() # ************************************************ # gpio input functions # called by main program only # ************************************************ def reset_input_state(self): for pin in GPIODriver.pins: pin[GPIODriver.COUNT]=0 pin[GPIODriver.PRESSED]=False pin[GPIODriver.LAST]=False pin[GPIODriver.REPEAT_COUNT]=pin[GPIODriver.REPEAT] # index is of the pins array, provided by the callback ***** needs to be name def shutdown_pressed(self): if GPIODriver.shutdown_index != 0: return GPIODriver.pins[GPIODriver.shutdown_index][GPIODriver.PRESSED] else: return False def do_buttons(self): for index, pin in enumerate(GPIODriver.pins): if pin[GPIODriver.DIRECTION] == 'in': # linked pin if pin[GPIODriver.LINKED_NAME] != '': link_pin=self.output_pin_of(pin[GPIODriver.LINKED_NAME]) if link_pin!=-1: self.GPIO.output(link_pin,self.GPIO.input(pin[GPIODriver.PIN]) ^ pin[GPIODriver.LINKED_INVERT]) # debounce if self.GPIO.input(pin[GPIODriver.PIN]) == 0: if pin[GPIODriver.COUNT]<pin[GPIODriver.THRESHOLD]: pin[GPIODriver.COUNT]+=1 if pin[GPIODriver.COUNT] == pin[GPIODriver.THRESHOLD]: pin[GPIODriver.PRESSED]=True else: # input us 1 if pin[GPIODriver.COUNT]>0: pin[GPIODriver.COUNT]-=1 if pin[GPIODriver.COUNT] == 0: pin[GPIODriver.PRESSED]=False # detect edges # falling edge if pin[GPIODriver.PRESSED] is True and pin[GPIODriver.LAST] is False: pin[GPIODriver.LAST]=pin[GPIODriver.PRESSED] pin[GPIODriver.REPEAT_COUNT]=pin[GPIODriver.REPEAT] if pin[GPIODriver.FALLING_NAME] != '' and self.button_callback is not None: self.button_callback(pin[GPIODriver.FALLING_NAME],"GPIO") # rising edge if pin[GPIODriver.PRESSED] is False and pin[GPIODriver.LAST] is True: pin[GPIODriver.LAST]=pin[GPIODriver.PRESSED] pin[GPIODriver.REPEAT_COUNT]=pin[GPIODriver.REPEAT] if pin[GPIODriver.RISING_NAME] != '' and self.button_callback is not None: self.button_callback(pin[GPIODriver.RISING_NAME],"GPIO") # do state callbacks if pin[GPIODriver.REPEAT_COUNT] == 0: if pin[GPIODriver.ZERO_NAME] != '' and pin[GPIODriver.PRESSED] is True and self.button_callback is not None: self.button_callback(pin[GPIODriver.ZERO_NAME],"GPIO") if pin[GPIODriver.ONE_NAME] != '' and pin[GPIODriver.PRESSED] is False and self.button_callback is not None: self.button_callback(pin[GPIODriver.ONE_NAME],"GPIO") pin[GPIODriver.REPEAT_COUNT]=pin[GPIODriver.REPEAT] else: if pin[GPIODriver.REPEAT] != -1: pin[GPIODriver.REPEAT_COUNT]-=1 # execute an output event def handle_output_event(self,name,param_type,param_values,req_time): if GPIODriver.gpio_enabled is False: return 'normal','gpio not enabled' #gpio only handles state parameters if param_type != 'state': return 'error','gpio does not handle: ' + param_type to_state=param_values[0] if to_state== 'on': state=True else: state=False pin= self.output_pin_of(name) if pin == -1: return 'error','Not an output for gpio: ' + name self.mon.log (self,'pin P1-'+ str(pin)+ ' set '+ str(state) + ' required at: ' + str(req_time)+ ' sent at: ' + str(long(time.time()))) # print 'pin P1-'+ str(pin)+ ' set '+ str(state) + ' required: ' + str(req_time)+ ' actual: ' + str(long(time.time())) self.GPIO.output(pin,state) return 'normal','gpio handled OK' # ************************************************ # gpio output interface methods # these can be called from many classes so need to operate on class variables # ************************************************ def reset_outputs(self): if GPIODriver.gpio_enabled is True: self.mon.log(self,'reset outputs') for index, pin in enumerate(GPIODriver.pins): num = pin[GPIODriver.PIN] if pin[GPIODriver.DIRECTION] == 'out': self.GPIO.output(num,False) # ************************************************ # internal functions # these can be called from many classes so need to operate on class variables # ************************************************ def output_pin_of(self,name): for pin in GPIODriver.pins: # print " in list" + pin[GPIODriver.NAME] + str(pin[GPIODriver.PIN] ) if pin[GPIODriver.NAME] == name and pin[GPIODriver.DIRECTION] == 'out': # print " linked pin " + pin[GPIODriver.NAME] + ' ' + str(pin[GPIODriver.PIN] ) return pin[GPIODriver.PIN] return -1 # *********************************** # reading gpio.cfg # ************************************ def read(self,pp_dir,pp_home,pp_profile): # try inside profile filename=pp_profile+os.sep+'pp_io_config'+os.sep+'gpio.cfg' if os.path.exists(filename): self.config = ConfigParser.ConfigParser() self.config.read(filename) self.mon.log(self,"gpio.cfg read from "+ filename) return 'normal','gpio.cfg read' else: return 'normal','gpio.cfg not found in profile: '+filename
class OMXDriver(object): _STATUS_REXP = re.compile(r"V :\s*([\d.]+).*") _DONE_REXP = re.compile(r"have a nice day.*") _LAUNCH_CMD = '/usr/bin/omxplayer -s ' #needs changing if user has installed his own version of omxplayer elsewhere def __init__(self,widget): self.widget=widget self.mon=Monitor() self.mon.on() self.paused=None def control(self,char): self._process.send(char) def pause(self): self._process.send('p') if not self.paused: self.paused = True else: self.paused=False def play(self, track, options): self._pp(track, options,False) def prepare(self, track, options): self._pp(track, options,True) def show(self): # unpause to start playing self._process.send('p') self.paused = False def stop(self): self._process.send('q') # kill the subprocess (omxplayer.bin). Used for tidy up on exit. def terminate(self,reason): self.terminate_reason=reason self._process.send('q') def terminate_reason(self): return self.terminate_reason # test of whether _process is running def is_running(self): return self._process.isalive() # *********************************** # INTERNAL FUNCTIONS # ************************************ def _pp(self, track, options, pause_before_play): self.paused=False self.start_play_signal = False self.end_play_signal=False self.terminate_reason='' track= "'"+ track.replace("'","'\\''") + "'" cmd = OMXDriver._LAUNCH_CMD + options +" " + track self.mon.log(self, "Send command to omxplayer: "+ cmd) self._process = pexpect.spawn(cmd) # uncomment to monitor output to and input from omxplayer.bin (read pexpect manual) fout= file('omxlogfile.txt','w') #uncomment and change sys.stdout to fout to log to a file # self._process.logfile_send = sys.stdout # send just commands to stdout self._process.logfile=fout # send all communications to log file if pause_before_play: self._process.send('p') self.paused = True #start the thread that is going to monitor sys.stdout. Presumably needs a thread because of blocking self._position_thread = Thread(target=self._get_position) self._position_thread.start() def _get_position(self): self.start_play_signal = True self.video_position=0.0 self.audio_position=0.0 while True: index = self._process.expect([OMXDriver._STATUS_REXP, pexpect.TIMEOUT, pexpect.EOF, OMXDriver._DONE_REXP]) if index == 1: continue elif index in (2, 3): #Have a nice day detected self.end_play_signal=True break else: # presumably matches _STATUS_REXP so get video position # has a bug, position is not displayed for an audio track (mp3). Need to look at another field in the status, but how to extract it self.video_position = float(self._process.match.group(1)) self.audio_position = 0.0 #sleep is Ok here as it is a seperate thread. self.widget.after has funny effects as its not in the maion thread. sleep(0.05)
class LiveShow: NEW_TRACKS={'image':{'title':'New Image','track-ref':'','type':'image','location':'','duration':'','transition':'', 'track-text':'','track-text-font':'','track-text-colour':'','track-text-x':'0','track-text-y':'0'}, 'video':{'title':'New Video','track-ref':'','type':'video','location':'','omx-audio':''}} IMAGE_FILES=('.gif','.jpg','.jpeg','.bmp','.png','.tif') VIDEO_FILES=('.mp4','.mkv','.avi','.mp2','.wmv', '.vob') AUDIO_FILES=('.mp3','.wav','.ogg') # ******************* # External interface # ******************** def __init__(self, show, canvas, showlist, pp_home, pp_profile): """ canvas - the canvas that the show is to be written on showlist - used jus to check the issue of medialist against showlist show - the dictionary for the show to be played pp_home - Pi presents data_home directory pp_profile - Pi presents profile directory """ self.mon=Monitor() self.mon.on() #instantiate arguments self.show =show self.showlist=showlist self.canvas=canvas self.pp_home=pp_home self.pp_profile=pp_profile # open resources self.rr=ResourceReader() # Init variables self.player=None self.shower=None self._end_liveshow_signal=False self._play_child_signal = False self.error=False self._livelist=None self._new_livelist= None def play(self,end_callback,ready_callback=None, top=False,command='nil'): """ displays the liveshow end_callback - function to be called when the liveshow exits ready_callback - callback when liveshow is ready to display top is True when the show is top level (i.e. run from start show) """ #instantiate the arguments self._end_callback=end_callback self._ready_callback=ready_callback self.top=top self.mon.log(self,"Starting show: " + self.show['show-ref']) # check data files are available. self.media_file = self.pp_profile + os.sep + self.show['medialist'] if not os.path.exists(self.media_file): self.mon.err(self,"Medialist file not found: "+ self.media_file) self._stop("Medialist file not found") self.options=command_options() self._pp_live_dir1 = self.pp_home + os.sep + 'pp_live_tracks' if not os.path.exists(self._pp_live_dir1): os.mkdir(self._pp_live_dir1) self._pp_live_dir2='' if self.options['liveshow'] <>"": self._pp_live_dir2 = self.options['liveshow'] if not os.path.exists(self._pp_live_dir2): self.mon.err(self,"live tracks directory not found " + self._pp_live_dir2) self._end('error',"live tracks directory not found") #create a medialist for the liveshow and read it. # it should be empty of anonymous tracks but read it to check its version. self.medialist=MediaList() if self.medialist.open_list(self.media_file,self.showlist.sissue())==False: self.mon.err(self,"Version of medialist different to Pi Presents") self._end('error',"Version of medialist different to Pi Presents") if self.ready_callback<>None: self.ready_callback() self._play_first_track() # respond to key presses. def key_pressed(self,key_name): self.mon.log(self,"received key: " + key_name) if key_name=='': pass elif key_name=='escape': # if next lower show eor player is running pass down to stop the show/track # ELSE stop this show except for exceptions if self.shower<>None: self.shower.key_pressed(key_name) elif self.player<>None: self.player.key_pressed(key_name) else: # not at top so stop the show if self.top == False: self._stop("exit show to higher level") else: pass elif key_name in ('up','down'): # if child or sub-show is running and is a show pass to show, track does not use up/down if self.shower<>None: self.shower.key_pressed(key_name) elif key_name=='return': # if child show or sub-show is running and is show - pass down # ELSE use Return to start child if self.shower<>None: self.shower.key_pressed(key_name) else: if self.show['has-child']=="yes": self._play_child() elif key_name in ('p',' '): # pass down if show or track running. if self.shower<>None: self.shower.key_pressed(key_name) elif self.player<>None: self.player.key_pressed(key_name) def button_pressed(self,button,edge): if button=='play': self.key_pressed("return") elif button =='up': self.key_pressed("up") elif button=='down': self.key_pressed("down") elif button=='stop': self.key_pressed("escape") elif button=='pause': self.key_pressed('p') # kill or error def terminate(self,reason): if self.shower<>None: self.mon.log(self,"sent terminate to shower") self.shower.terminate(reason) elif self.player<>None: self.mon.log(self,"sent terminate to player") self.player.terminate(reason) else: self._end(reason,'terminated without terminating shower or player') def _tidy_up(self): pass def resource(self,section,item): value=self.rr.get(section,item) if value==False: self.mon.err(self, "resource: "+section +': '+ item + " not found" ) self.terminate("error",'Cannot find resource') else: return value # *************************** # Respond to key/button presses # *************************** def _stop(self,message): self._end_liveshow_signal=True def _play_child(self): self._play_child_signal=True if self.player<>None: self.player.key_pressed("escape") # *************************** # end of show functions # *************************** def _end(self,reason,message): self._end_liveshow_signal=False self.mon.log(self,"Ending Liveshow: "+ self.show['show-ref']) self._tidy_up() self._end_callback(reason,message) self=None return def _nend(self): self._end('normal','end from state machine') # *************************** # Livelist # *************************** def _livelist_add_track(self,afile): (root,title)=os.path.split(afile) (root,ext)= os.path.splitext(afile) if ext.lower() in LiveShow.IMAGE_FILES: self._livelist_new_track(LiveShow.NEW_TRACKS['image'],{'title':title,'track-ref':'','location':afile}) if ext.lower() in LiveShow.VIDEO_FILES: self._livelist_new_track(LiveShow.NEW_TRACKS['video'],{'title':title,'track-ref':'','location':afile}) if ext.lower() in LiveShow.AUDIO_FILES: self._livelist_new_track(LiveShow.NEW_TRACKS['video'],{'title':title,'track-ref':'','location':afile}) def _livelist_new_track(self,fields,values): new_track=fields self._new_livelist.append(copy.deepcopy(new_track)) last = len(self._new_livelist)-1 self._new_livelist[last].update(values) def _new_livelist_create(self): self._new_livelist=[] if os.path.exists(self._pp_live_dir1): for file in os.listdir(self._pp_live_dir1): file = self._pp_live_dir1 + os.sep + file (root_file,ext_file)= os.path.splitext(file) if ext_file.lower() in LiveShow.IMAGE_FILES+LiveShow.VIDEO_FILES+LiveShow.AUDIO_FILES: self._livelist_add_track(file) if os.path.exists(self._pp_live_dir2): for file in os.listdir(self._pp_live_dir2): file = self._pp_live_dir2 + os.sep + file (root_file,ext_file)= os.path.splitext(file) if ext_file.lower() in LiveShow.IMAGE_FILES+LiveShow.VIDEO_FILES+LiveShow.AUDIO_FILES: self._livelist_add_track(file) self._new_livelist= sorted(self._new_livelist, key= lambda track: os.path.basename(track['location']).lower()) # for it in self._new_livelist: # print it['location'] # print '' def _livelist_replace_if_changed(self): self._new_livelist_create() if self._new_livelist<>self._livelist: self._livelist=copy.deepcopy(self._new_livelist) self._livelist_index=0 def _livelist_next(self): if self._livelist_index== len(self._livelist)-1: self._livelist_index=0 else: self._livelist_index +=1 # *************************** # Play Loop # *************************** def _play_first_track(self): self._new_livelist_create() self._livelist = copy.deepcopy(self._new_livelist) self._livelist_index = 0 self._play_track() def _play_track(self): self._livelist_replace_if_changed() if len(self._livelist)>0: self._play_selected_track(self._livelist[self._livelist_index]) else: self.display_message(self.canvas,None,self.resource('liveshow','m01'),5,self._what_next) def _what_next(self): # user wants to end if self._end_liveshow_signal==True: self._end_liveshow_signal=False self._end('normal',"show ended by user") # play child? elif self._play_child_signal == True: self._play_child_signal=False index = self.medialist.index_of_track('pp-child-show') if index >=0: #don't select the track as need to preserve mediashow sequence. child_track=self.medialist.track(index) self._display_eggtimer(self.resource('liveshow','m02')) self._play_selected_track(child_track) else: self.mon.err(self,"Child show not found in medialist: "+ self.show['pp-child-show']) self._end('error',"child show not found in medialist") # otherwise loop to next track else: self._livelist_next() self._play_track() # *************************** # Dispatching to Players/Shows # *************************** # used to display internal messages in situations where a medialist entry could not be used. def display_message(self,canvas,source,content,duration,_display_message_callback): self._display_message_callback=_display_message_callback tp={'duration':duration,'message-colour':'white','message-font':'Helvetica 20 bold'} self.player=MessagePlayer(canvas,tp,tp) self.player.play(content,self._display_message_end,None) def _display_message_end(self,reason,message): self.player=None if reason in ("killed",'error'): self._end(reason,message) else: self._display_message_callback() def complete_path(self,selected_track): # complete path of the filename of the selected entry track_file = selected_track['location'] if track_file[0]=="+": track_file=self.pp_home+track_file[1:] self.mon.log(self,"Track to play is: "+ track_file) return track_file def _play_selected_track(self,selected_track): """ selects the appropriate player from type field of the medialist and computes the parameters for that type selected_track is a dictionary for the track/show """ # self.canvas.delete(ALL) # is menu required if self.show['has-child']=="yes": enable_child=True else: enable_child=False #dispatch track by type self.player=None self.shower=None track_type = selected_track['type'] self.mon.log(self,"Track type is: "+ track_type) if track_type=="image": track_file=self.complete_path(selected_track) # images played from menus don't have children self.player=ImagePlayer(self.canvas,self.show,selected_track) self.player.play(track_file, self.end_player, self.ready_callback, enable_menu=enable_child) elif track_type=="video": # create a videoplayer track_file=self.complete_path(selected_track) self.player=VideoPlayer(self.canvas,self.show,selected_track) self.player.play(track_file, self.end_player, self.ready_callback, enable_menu=enable_child) elif track_type=="show": # get the show from the showlist index = self.showlist.index_of_show(selected_track['sub-show']) if index >=0: self.showlist.select(index) selected_show=self.showlist.selected_show() else: self.mon.err(self,"Show not found in showlist: "+ selected_track['sub-show']) self._stop("Unknown show") if selected_show['type']=="mediashow": self.shower= MediaShow(selected_show, self.canvas, self.showlist, self.pp_home, self.pp_profile) self.shower.play(self.end_shower,top=False,command='nil') elif selected_show['type']=="menu": self.shower= MenuShow(selected_show, self.canvas, self.showlist, self.pp_home, self.pp_profile) self.shower.play(self.end_shower,top=False,command='nil') else: self.mon.err(self,"Unknown Show Type: "+ selected_show['type']) self._stop("Unknown show type") else: self.mon.err(self,"Unknown Track Type: "+ track_type) self._stop("Unknown track type") def ready_callback(self): self._delete_eggtimer() def end_player(self,reason,message): self.mon.log(self,"Returned from player with message: "+ message) self.player=None if reason in("killed","error"): self._end(reason,message) else: self._what_next() def end_shower(self,reason,message): self.mon.log(self,"Returned from shower with message: "+ message) self.shower=None if reason in("killed","error"): self._end(reason,message) else: self._what_next() def _display_eggtimer(self,text): self.canvas.create_text(int(self.canvas['width'])/2, int(self.canvas['height'])/2, text= text, fill='white', font="Helvetica 20 bold") self.canvas.update_idletasks( ) def _delete_eggtimer(self): self.canvas.delete(ALL)
class VideoPlayer: _CLOSED = "omx_closed" #probably will not exist _STARTING = "omx_starting" #track is being prepared _PLAYING = "omx_playing" #track is playing to the screen, may be paused _ENDING = "omx_ending" #track is in the process of ending due to quit or end of track # *************************************** # EXTERNAL COMMANDS # *************************************** def __init__(self, canvas, cd, track_params ): """ canvas - the canvas onto which the video is to be drawn (not!!) cd - configuration dictionary track_params - config dictionary for this track overides cd """ self.mon=Monitor() self.mon.on() #instantiate arguments self.cd=cd #configuration dictionary for the videoplayer self.canvas = canvas #canvas onto which video should be played but isn't! Use as widget for alarm self.track_params=track_params # get config from medialist if there. if 'omx-audio' in self.track_params and self.track_params['omx-audio']<>"": self.omx_audio= self.track_params['omx-audio'] else: self.omx_audio= self.cd['omx-audio'] if self.omx_audio<>"": self.omx_audio= "-o "+ self.omx_audio # could put instance generation in play, not sure which is better. self.omx=OMXDriver() self._tick_timer=None self.error=False self.terminate_required=False self._init_play_state_machine() def play(self, track, end_callback, ready_callback, enable_menu=False, starting_callback=None, playing_callback=None, ending_callback=None): #instantiate arguments self.ready_callback=ready_callback #callback when ready to play self.end_callback=end_callback # callback when finished self.starting_callback=starting_callback #callback during starting state self.playing_callback=playing_callback #callback during playing state self.ending_callback=ending_callback # callback during ending state # enable_menu is not used by videoplayer # and start playing the video. if self.play_state == VideoPlayer._CLOSED: self.mon.log(self,">play track received") self._start_play_state_machine(track) return True else: self.mon.log(self,"!< play track rejected") return False def key_pressed(self,key_name): if key_name=='': return elif key_name in ('p',' '): self._pause() return elif key_name=='escape': self._stop() return def button_pressed(self,button,edge): if button =='pause': self._pause() return elif button=='stop': self._stop() return def terminate(self,reason): # circumvents state machine self.terminate_required=True if self.omx<>None: self.mon.log(self,"sent terminate to omxdriver") self.omx.terminate(reason) else: self.mon.log(self,"terminate, omxdriver not running") self._end() # *************************************** # INTERNAL FUNCTIONS # *************************************** # respond to normal stop def _stop(self): # send signal to stop the track to the state machine self.mon.log(self,">stop received") self._stop_required_signal=True #respond to internal error def _error(self): self.error=True self._stop_required_signal=True #toggle pause def _pause(self): if self.play_state in (VideoPlayer._PLAYING,VideoPlayer._ENDING): self.omx.pause() return True else: self.mon.log(self,"!<pause rejected") return False # other control when playing, used? def _control(self,char): if self.play_state==VideoPlayer._PLAYING: self.mon.log(self,"> send control ot omx: "+ char) self.omx.control(char) return True else: self.mon.log(self,"!<control rejected") return False # called to end omxdriver def _end(self): if self._tick_timer<>None: self.canvas.after_cancel(self._tick_timer) self._tick_timer=None if self.error==True: self.end_callback("error",'error') self=None elif self.terminate_required==True: self.end_callback("killed",'killed') self=None else: self.end_callback('normal',"track has terminated") self=None # *************************************** # # PLAYING STATE MACHINE # *************************************** """self. play_state controls the playing sequence, it has the following values. I am not entirely sure the starting and ending states are required. - _closed - the omx process is not running, omx process can be initiated - _starting - omx process is running but is not yet able to receive controls - _playing - playing a track, controls can be sent - _ending - omx is doing its termination, controls cannot be sent """ def _init_play_state_machine(self): self._stop_required_signal=False self.play_state=VideoPlayer._CLOSED def _start_play_state_machine(self,track): #initialise all the state machine variables #self.iteration = 0 # for debugging self._stop_required_signal=False # signal that user has pressed stop self.play_state=VideoPlayer._STARTING #play the selected track options=self.omx_audio+" "+self.cd['omx-other-options']+" " self.omx.play(track,options) # and start polling for state changes self._tick_timer=self.canvas.after(50, self._play_state_machine) def _play_state_machine(self): if self.play_state == VideoPlayer._CLOSED: self.mon.log(self," State machine: " + self.play_state) return elif self.play_state == VideoPlayer._STARTING: self.mon.log(self," State machine: " + self.play_state) # if omxplayer is playing the track change to play state if self.omx.start_play_signal==True: self.mon.log(self," <start play signal received from omx") self.omx.start_play_signal=False # callback to the calling object to e.g remove egg timer. if self.ready_callback<>None: self.ready_callback() self.play_state=VideoPlayer._PLAYING self.mon.log(self," State machine: omx_playing started") self._do_starting() self._tick_timer=self.canvas.after(50, self._play_state_machine) elif self.play_state == VideoPlayer._PLAYING: # self.mon.log(self," State machine: " + self.play_state) # service any queued stop signals if self._stop_required_signal==True: self.mon.log(self," Service stop required signal") self._stop_omx() self._stop_required_signal=False self.play_state = VideoPlayer._ENDING # omxplayer reports it is terminating so change to ending state if self.omx.end_play_signal: self.mon.log(self," <end play signal received") self.mon.log(self," <end detected at: " + str(self.omx.video_position)) self.play_state = VideoPlayer._ENDING self._do_playing() self._tick_timer=self.canvas.after(200, self._play_state_machine) elif self.play_state == VideoPlayer._ENDING: self.mon.log(self," State machine: " + self.play_state) self._do_ending() # if spawned process has closed can change to closed state self.mon.log (self," State machine : is omx process running? - " + str(self.omx.is_running())) if self.omx.is_running() ==False: self.mon.log(self," <omx process is dead") self.play_state = VideoPlayer._CLOSED self._end() else: self._tick_timer=self.canvas.after(200, self._play_state_machine) # allow calling object do things in each state by calling the appropriate callback def _do_playing(self): self.video_position=self.omx.video_position self.audio_position=self.omx.audio_position if self.playing_callback<>None: self.playing_callback() def _do_starting(self): self.video_position=0.0 self.audio_position=0.0 if self.starting_callback<>None: self.starting_callback() def _do_ending(self): if self.ending_callback<>None: self.ending_callback() def _stop_omx(self): # send signal to stop the track to the state machine self.mon.log(self," >stop omx received from state machine") if self.play_state==VideoPlayer._PLAYING: self.omx.stop() return True else: self.mon.log(self,"!<stop rejected") return False
class BeepPlayer(object): pp_home = '' pp_profile = '' def __init__(self): self.mon = Monitor() def init(self, pp_home, pp_profile): BeepPlayer.pp_home = pp_home BeepPlayer.pp_profile = pp_profile def play_animate_beep(self, location, device): # check location path = self.complete_path(location) if not os.path.exists(path): return 'error', 'beep file does not exist: ' + path status, message = self.do_beep(path, device) if status == 'error': return 'error', message return 'normal', '' def play_show_beep(self, command_text): fields = command_text.split() if len(fields) not in (2, 3): return 'error', "incorrect number of fields in beep command" + line symbol = fields[0] location = fields[1] if len(fields) == 3: device = fields[2] else: device = '' path = self.complete_path(location) if not os.path.exists(path): return 'error', 'beep file does not exist: ' + path status, message = self.do_beep(path, device) if status == 'error': return 'error', message return 'normal', '' def complete_path(self, track_file): # complete path of the filename of the selected entry if track_file != '' and track_file[0] == "+": track_file = BeepPlayer.pp_home + track_file[1:] elif track_file[0] == "@": track_file = BeepPlayer.pp_profile + track_file[1:] return track_file def do_beep(self, path, device): self.am = AudioManager() self.mon.log(self, 'Do Beep: ' + path + ' ' + device) #print ('play beep',path,device) audio_sys = self.am.get_audio_sys() if audio_sys == 'cset': #print ('cset',device) if device != "": if device in ('hdmi', 'hdmi0'): os.system("amixer -q -c 0 cset numid=3 2") elif device == 'hdmi1': os.system("amixer -q -c 0 cset numid=3 3") elif device in ('local', 'A/V'): os.system("amixer -q -c 0 cset numid=3 1") fields = path.split('.') if fields[1] == 'mp3': #print ('cset mp3') os.system('mpg123 -q ' + path) else: #print ('cset aplay') os.system("aplay -q " + path) return 'normal', '' elif audio_sys == 'alsa': #print ('alsa',device) # alsa audio fields = path.split('.') if fields[1] != 'mp3': # other than mp3 driver_option = '' if device != "": if device in ('hdmi', 'hdmi0'): driver_option = ' -D plughw:b1 ' elif device == 'hdmi1': driver_option = ' -D plughw:b2 ' elif device in ('USB', 'alsa'): driver_option = ' -D plughw:Device ' elif device in ('A/V', 'local'): driver_option = ' -D plughw:Headphones ' else: driver_option = '' #print ('alsa wav',device,driver_option) os.system("aplay -q " + driver_option + ' ' + path) else: #mp3 driver_option = '' if device != "": if device in ('hdmi', 'hdmi0'): driver_option = ' -o alsa:plughw:b1 ' elif device == 'hdmi1': driver_option = ' -o alsa:plughw:b2 ' elif device in ('USB', 'alsa'): driver_option = ' -o alsa:plughw:Device ' elif device in ('A/V', 'local'): driver_option = ' -o alsa:plughw:Headphones ' else: driver_option = '' #print ('alsa mp3',driver_option) os.system('mpg123 -q ' + driver_option + ' ' + path) return 'normal', '' elif audio_sys == "pulse": status, message, sink = self.am.get_sink(device) if status == 'error': return 'error', message if not self.am.sink_connected(sink): return 'error', 'sound device not connected - ' + device fields = path.split('.') if fields[1] != 'mp3': # other than mp3 if sink != '': driver_option = ' --device=' + sink + ' --stream-name=pipresents ' else: driver_option = ' --stream-name=pipresents ' #print ('pulse wav',device) os.system('paplay ' + driver_option + path) return 'normal', '' else: #mp3 if sink == '': driver_option = ' -o pulse ' else: driver_option = ' -o pulse -a ' + sink #print ('pulse mp3',device) command = 'mpg123 -q ' + driver_option + ' ' + path # print (command) os.system(command) return 'normal', '' else: print('bad audio system', audio_sys) return 'error', 'bad audio system'
class OSCRemote(object): def __init__(self): self.editor_issue = "1.3" # get command options self.command_options = remote_options() # get directory holding the code self.pp_dir = sys.path[0] if not os.path.exists(self.pp_dir + os.sep + "pp_oscremote.py"): tkMessageBox.showwarning("Pi Presents", "Bad Application Directory") exit() # Initialise logging Monitor.log_path = self.pp_dir self.mon = Monitor() self.mon.init() Monitor.classes = ['OSCRemote', 'OSCConfig', 'OSCEditor'] Monitor.log_level = int(self.command_options['debug']) self.mon.log(self, "Pi Presents Remote is starting") self.mon.log(self, " OS and separator " + os.name + ' ' + os.sep) self.mon.log(self, "sys.path[0] - location of code: code " + sys.path[0]) self.setup_gui() # OSC config class self.osc_config = OSCConfig() self.init() #and start the system self.root.after(1000, self.run_app) self.root.mainloop() def init(self): self.osc_config_file = self.pp_dir + os.sep + 'pp_config' + os.sep + 'pp_oscremote.cfg' self.read_create_osc() self.pp_home_dir = self.osc_config.pp_home_dir self.pp_profiles_offset = self.osc_config.pp_profiles_offset self.mon.log(self, "Data Home from options is " + self.pp_home_dir) self.mon.log( self, "Current Profiles Offset from options is " + self.pp_profiles_offset) self.pp_profile_dir = '' self.current_showlist = None self.current_show = None self.current_show_ref = '' self.shows_display.delete(0, END) self.results.set('') def add_status(self, text): self.status_display.insert(END, text + '\n') self.status_display.see(END) def run_app(self): self.client = None self.server = None self.st = None # initialise OSC variables self.prefix = '/pipresents' self.this_unit = '/' + self.osc_config.this_unit_name self.add_status('this unit is: ' + self.this_unit) self.controlled_unit = '/' + self.osc_config.controlled_unit_1_name self.add_status('controlled unit is: ' + self.controlled_unit) #connect client then start server to listen for replies self.init_client() self.add_status('connecting to controlled unit: ' + self.osc_config.controlled_unit_1_ip + ':' + self.osc_config.controlled_unit_1_port + ' ' + self.osc_config.controlled_unit_1_name) self.connect_client(self.osc_config.controlled_unit_1_ip, self.osc_config.controlled_unit_1_port) self.add_status('listening for replies on:' + self.osc_config.this_unit_ip + ':' + self.osc_config.this_unit_port) self.init_server(self.osc_config.this_unit_ip, self.osc_config.this_unit_port, self.client) self.add_initial_handlers() self.start_server() # *************************************** # RESPOND TO BUTTONS # *************************************** def open_show(self): self.msg_path = '/core/open ' self.msg_arg_text = self.current_show_ref self.display_msg_text() def close_show(self): self.msg_path = '/core/close ' self.msg_arg_text = self.current_show_ref self.display_msg_text() def exit_pipresents(self): self.msg_path = '/core/exitpipresents' self.msg_arg_text = '' self.display_msg_text() def play_event(self): self.msg_path = '/core/event' self.msg_arg_text = 'pp-play' self.display_msg_text() def pause_event(self): self.msg_path = '/core/event' self.msg_arg_text = 'pp-pause' self.display_msg_text() pass def stop_event(self): self.msg_path = '/core/event' self.msg_arg_text = 'pp-stop' self.display_msg_text() pass def up_event(self): self.msg_path = '/core/event' self.msg_arg_text = 'pp-up' self.display_msg_text() def down_event(self): self.msg_path = '/core/event' self.msg_arg_text = 'pp-down' self.display_msg_text() def output(self): self.msg_path = '/core/output' self.msg_arg_text = '' self.display_msg_text() def loopback(self): self.msg_path = '/system/loopback' self.msg_arg_text = '' self.display_msg_text() def server_info(self): self.msg_path = '/system/server-info' self.msg_arg_text = '' self.display_msg_text() # and put the created text in the results box in the gui def display_msg_text(self): self.results.set(self.prefix + self.controlled_unit + self.msg_path + ' ' + self.msg_arg_text) #calback from the Send button # parses the message string into fields and sends - NO error checking def send_message(self): msg_text = self.results.get() self.add_status('Send message:' + msg_text) self.mon.log(self, 'send message: ' + msg_text) fields = msg_text.split() address = fields[0] arg_list = fields[1:] self.send(address, arg_list) # *************************************** # OSC CLIENT TO SEND MESSAGES # *************************************** def init_client(self): self.client = OSC.OSCClient() def connect_client(self, ip, port): self.mon.log(self, 'connect to: ' + ip + ':' + str(port)) self.client.connect((ip, int(port))) def send(self, address, arg_list): msg = OSC.OSCMessage() msg.setAddress(address) for arg in arg_list: msg.append(arg) self.client.send(msg) def disconnect_client(self): self.client.close() return # *************************************** # OSC SERVER TO LISTEN TO REPLIES # *************************************** def init_server(self, ip, port_text, client): self.mon.log(self, 'Start Server: ' + ip + ':' + port_text) self.server = OSC.OSCServer((ip, int(port_text)), client) def start_server(self): self.st = threading.Thread(target=self.server.serve_forever) self.st.start() def close_server(self): if self.server != None: self.server.close() self.mon.log(self, 'Waiting for Server-thread to finish') if self.st != None: self.st.join() ##!!! self.mon.log(self, 'server thread closed') def add_initial_handlers(self): self.server.addMsgHandler('default', self.no_match_handler) self.server.addMsgHandler( self.prefix + self.this_unit + "/system/loopback-reply", self.loopback_reply_handler) self.server.addMsgHandler( self.prefix + self.this_unit + "/system/server-info-reply", self.server_info_reply_handler) def no_match_handler(self, addr, tags, stuff, source): text = '' text += "no match for new osc msg from %s" % OSC.getUrlStr( source) + '\n' text += "with addr : %s" % addr + '\n' text += "typetags %s" % tags + '\n' text += "data %s" % stuff + '\n' self.add_status(text + '\n') def loopback_reply_handler(self, addr, tags, stuff, source): self.add_status('Loopback reply received from: ' + self.pretty_list(source)) def server_info_reply_handler(self, addr, tags, stuff, source): self.add_status('Server Information from: ' + self.pretty_list(source) + '\n ' + self.pretty_list(stuff)) def pretty_list(self, fields): text = ' ' for field in fields: text += str(field) + ' ' return text # *************************************** # INIT EXIT MISC # *************************************** def e_edit_osc(self): self.disconnect_client() self.close_server() self.edit_osc() self.init() self.add_status('\n\n\nRESTART') self.run_app() def app_exit(self): self.disconnect_client() self.close_server() if self.root is not None: self.root.destroy() self.mon.finish() sys.exit() def show_help(self): tkMessageBox.showinfo("Help", "Read 'manual.pdf'") def about(self): tkMessageBox.showinfo( "About", "Simple Remote Control for Pi Presents\n" + "Author: Ken Thompson" + "\nWebsite: http://pipresents.wordpress.com/") def setup_gui(self): # set up the gui # root is the Tkinter root widget self.root = Tk() self.root.title("Remote Control for Pi Presents") # self.root.configure(background='grey') self.root.resizable(False, False) # define response to main window closing self.root.protocol("WM_DELETE_WINDOW", self.app_exit) # bind some display fields self.filename = StringVar() self.display_show = StringVar() self.results = StringVar() self.status = StringVar() # define menu menubar = Menu(self.root) profilemenu = Menu(menubar, tearoff=0, bg="grey", fg="black") profilemenu.add_command(label='Select', command=self.open_existing_profile) menubar.add_cascade(label='Profile', menu=profilemenu) toolsmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") menubar.add_cascade(label='Tools', menu=toolsmenu) osc_configmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") menubar.add_cascade(label='OSC', menu=osc_configmenu) osc_configmenu.add_command(label='Edit', command=self.e_edit_osc) helpmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") menubar.add_cascade(label='Help', menu=helpmenu) helpmenu.add_command(label='Help', command=self.show_help) helpmenu.add_command(label='About', command=self.about) self.root.config(menu=menubar) #top frame top_frame = Frame(self.root, padx=5, pady=5) top_frame.pack(side=TOP) results_label = Label(top_frame, text="Message to Send", font="arial 12 bold") results_label.pack(side=LEFT) results_display = Entry(top_frame, textvariable=self.results, width=70) results_display.pack(side=LEFT, fill=BOTH, expand=1) send_button = Button(top_frame, width=5, height=1, text='Send', fg='black', command=self.send_message, bg="light grey") send_button.pack(side=RIGHT) #bottom frame bottom_frame = Frame(self.root, padx=5, pady=5) bottom_frame.pack(side=TOP, fill=BOTH, expand=1) left_frame = Frame(bottom_frame, padx=5) left_frame.pack(side=LEFT) right_frame = Frame(bottom_frame, padx=5, pady=5) right_frame.pack(side=LEFT) suplabel_frame = Frame(right_frame, pady=5) suplabel_frame.pack(side=TOP) commands_label = Label(suplabel_frame, text="Show Control", font="arial 12 bold") commands_label.pack() supervisor_frame = Frame(right_frame, pady=5) supervisor_frame.pack(side=TOP) # supervisor buttons add_button = Button(supervisor_frame, width=5, height=1, text='Open\nShow', fg='black', command=self.open_show, bg="light grey") add_button.pack(side=LEFT) add_button = Button(supervisor_frame, width=5, height=1, text='Close\nShow', fg='black', command=self.close_show, bg="light grey") add_button.pack(side=LEFT) add_button = Button(supervisor_frame, width=10, height=1, text='Exit\nPi Presents', fg='black', command=self.exit_pipresents, bg="light grey") add_button.pack(side=LEFT) # events buttons oplabel_frame = Frame(right_frame, pady=5) oplabel_frame.pack(side=TOP) operations_label = Label(oplabel_frame, text="Input Events", font="arial 12 bold") operations_label.pack() operations_frame = Frame(right_frame, pady=5) operations_frame.pack(side=TOP) add_button = Button(operations_frame, width=5, height=1, text='Play', fg='black', command=self.play_event, bg="light grey") add_button.pack(side=LEFT) add_button = Button(operations_frame, width=5, height=1, text='Pause', fg='black', command=self.pause_event, bg="light grey") add_button.pack(side=LEFT) add_button = Button(operations_frame, width=5, height=1, text='Stop', fg='black', command=self.stop_event, bg="light grey") add_button.pack(side=LEFT) add_button = Button(operations_frame, width=5, height=1, text='Up', fg='black', command=self.up_event, bg="light grey") add_button.pack(side=LEFT) add_button = Button(operations_frame, width=5, height=1, text='Down', fg='black', command=self.down_event, bg="light grey") add_button.pack(side=LEFT) # animate buttons animate_frame = Frame(right_frame, pady=5) animate_frame.pack(side=TOP) animate_label = Label(animate_frame, text="Control Outputs", font="arial 12 bold") animate_label.pack() animate_frame = Frame(right_frame, pady=5) animate_frame.pack(side=TOP) add_button = Button(animate_frame, width=5, height=1, text='Output', fg='black', command=self.output, bg="light grey") add_button.pack(side=LEFT) # system buttons systemlabel_frame = Frame(right_frame, pady=5) systemlabel_frame.pack(side=TOP) system_label = Label(systemlabel_frame, text="System", font="arial 12 bold") system_label.pack() system_frame = Frame(right_frame, pady=5) system_frame.pack(side=TOP) add_button = Button(system_frame, width=5, height=1, text='Loopback', fg='black', command=self.loopback, bg="light grey") add_button.pack(side=LEFT) add_button = Button(system_frame, width=10, height=1, text='Server Info', fg='black', command=self.server_info, bg="light grey") add_button.pack(side=LEFT) # define display of showlist shows_title_frame = Frame(left_frame) shows_title_frame.pack(side=TOP) shows_label = Label(shows_title_frame, text="Shows") shows_label.pack() shows_frame = Frame(left_frame) shows_frame.pack(side=TOP) scrollbar = Scrollbar(shows_frame, orient=VERTICAL) self.shows_display = Listbox(shows_frame, selectmode=SINGLE, height=12, width=40, bg="white", activestyle=NONE, fg="black", yscrollcommand=scrollbar.set) scrollbar.config(command=self.shows_display.yview) scrollbar.pack(side=RIGHT, fill=Y) self.shows_display.pack(side=LEFT, fill=BOTH, expand=1) self.shows_display.bind("<ButtonRelease-1>", self.e_select_show) # status_frame status_frame = Frame(self.root, padx=5, pady=5) status_frame.pack(side=TOP, fill=BOTH, expand=1) status_label = Label(status_frame, text="Status", font="arial 12 bold") status_label.pack(side=LEFT) scrollbar = Scrollbar(status_frame, orient=VERTICAL) self.status_display = Text(status_frame, height=10, yscrollcommand=scrollbar.set) scrollbar.config(command=self.status_display.yview) scrollbar.pack(side=RIGHT, fill=Y) self.status_display.pack(side=LEFT, fill=BOTH, expand=1) # *************************************** # SHOWLIST # *************************************** def open_existing_profile(self): initial_dir = self.pp_home_dir + os.sep + "pp_profiles" + self.pp_profiles_offset if os.path.exists(initial_dir) is False: self.mon.err( self, "Profiles directory not found: " + initial_dir + "\n\nHint: Data Home option must end in pp_home") return dir_path = tkFileDialog.askdirectory(initialdir=initial_dir) # dir_path="C:\Users\Ken\pp_home\pp_profiles\\ttt" if len(dir_path) > 0: self.open_profile(dir_path) def open_profile(self, dir_path): showlist_file = dir_path + os.sep + "pp_showlist.json" if os.path.exists(showlist_file) is False: self.mon.err( self, "Not a Profile: " + dir_path + "\n\nHint: Have you opened the profile directory?") return self.pp_profile_dir = dir_path self.root.title("Remote for Pi Presents - " + self.pp_profile_dir) self.open_showlist(self.pp_profile_dir) def open_showlist(self, profile_dir): showlist_file = profile_dir + os.sep + "pp_showlist.json" if os.path.exists(showlist_file) is False: self.mon.err( self, "showlist file not found at " + profile_dir + "\n\nHint: Have you opened the profile directory?") self.app_exit() self.current_showlist = ShowList() self.current_showlist.open_json(showlist_file) if float(self.current_showlist.sissue()) != float(self.editor_issue): self.mon.err( self, "Version of profile does not match Remote: " + self.editor_issue) self.app_exit() self.refresh_shows_display() def refresh_shows_display(self): self.shows_display.delete(0, self.shows_display.size()) for index in range(self.current_showlist.length()): self.shows_display.insert( END, self.current_showlist.show(index)['title'] + " [" + self.current_showlist.show(index)['show-ref'] + "]") if self.current_showlist.show_is_selected(): self.shows_display.itemconfig( self.current_showlist.selected_show_index(), fg='red') self.shows_display.see(self.current_showlist.selected_show_index()) def e_select_show(self, event): print 'select show', self.current_showlist.length() if self.current_showlist is not None and self.current_showlist.length( ) > 0: mouse_item_index = int(event.widget.curselection()[0]) self.current_showlist.select(mouse_item_index) self.current_show_ref = self.current_showlist.selected_show( )['show-ref'] self.refresh_shows_display() else: self.current_show_ref = '' # *************************************** # OSC CONFIGURATION # *************************************** def read_create_osc(self): if self.osc_config.read(self.osc_config_file) is False: self.osc_config.create(self.osc_config_file) eosc = OSCEditor(self.root, self.osc_config_file, 'remote', 'Create OSC Remote Configuration') self.osc_config.read(self.osc_config_file) def edit_osc(self): if self.osc_config.read(self.osc_config_file) is False: self.osc_config.create(self.osc_config_file) eosc = OSCEditor(self.root, self.osc_config_file, 'remote', 'Edit OSC Reomote Configuration')
class Player(object): # common bits of __init__(...) def __init__(self, show_id, showlist, root, canvas, show_params, track_params, pp_dir, pp_home, pp_profile, end_callback, command_callback): # create debugging log object self.mon = Monitor() self.mon.trace(self, '') # instantiate arguments self.show_id = show_id self.showlist = showlist self.root = root self.canvas = canvas['canvas-obj'] self.show_canvas_x1 = canvas['show-canvas-x1'] self.show_canvas_y1 = canvas['show-canvas-y1'] self.show_canvas_x2 = canvas['show-canvas-x2'] self.show_canvas_y2 = canvas['show-canvas-y2'] self.show_canvas_width = canvas['show-canvas-width'] self.show_canvas_height = canvas['show-canvas-height'] self.show_canvas_centre_x = canvas['show-canvas-centre-x'] self.show_canvas_centre_y = canvas['show-canvas-centre-y'] self.show_params = show_params self.track_params = track_params self.pp_dir = pp_dir self.pp_home = pp_home self.pp_profile = pp_profile self.end_callback = end_callback self.command_callback = command_callback # get background image from profile. self.background_file = '' if self.track_params['background-image'] != '': self.background_file = self.track_params['background-image'] # get background colour from profile. if self.track_params['background-colour'] != '': self.background_colour = self.track_params['background-colour'] else: self.background_colour = self.show_params['background-colour'] # get animation instructions from profile self.animate_begin_text = self.track_params['animate-begin'] self.animate_end_text = self.track_params['animate-end'] # create an instance of showmanager so we can control concurrent shows # self.show_manager=ShowManager(self.show_id,self.showlist,self.show_params,self.root,self.canvas,self.pp_dir,self.pp_profile,self.pp_home) # open the plugin Manager self.pim = PluginManager(self.show_id, self.root, self.canvas, self.show_params, self.track_params, self.pp_dir, self.pp_home, self.pp_profile) # create an instance of Animate so we can send animation commands self.animate = Animate() # initialise state and signals self.background_obj = None self.show_text_obj = None self.track_text_obj = None self.hint_obj = None self.background = None self.freeze_at_end_required = 'no' # overriden by videoplayer self.tick_timer = None self.terminate_signal = False self.play_state = '' def pre_load(self): # Control other shows at beginning self.show_control(self.track_params['show-control-begin']) pass # common bits of show(....) def pre_show(self): self.mon.trace(self, '') # show_x_content moved to just before ready_callback to improve flicker. self.show_x_content() # and whatecer the plugin has created self.pim.show_plugin() #ready callback hides and closes players from previous track, also displays show background if self.ready_callback is not None: self.ready_callback(self.enable_show_background) # create animation events reason, message = self.animate.animate(self.animate_begin_text, id(self)) if reason == 'error': self.mon.err(self, message) self.play_state = 'show-failed' if self.finished_callback is not None: self.finished_callback('error', message) else: # return to start playing the track. self.mon.log( self, ">show track received from show Id: " + str(self.show_id)) return # to keep landscape happy def ready_callback(self, enable_show_background): self.mon.fatal(self, 'ready callback not overridden') self.end('error', 'ready callback not overridden') def finished_callback(self, reason, message): self.mon.fatal(self, 'finished callback not overridden') self.end('error', 'finished callback not overridden') def closed_callback(self, reason, message): self.mon.fatal(self, 'closed callback not overridden') self.end('error', 'closed callback not overridden') # Control shows so pass the show control commands back to PiPresents via the command callback def show_control(self, show_control_text): lines = show_control_text.split('\n') for line in lines: if line.strip() == "": continue # print 'show control command: ',line self.command_callback(line, source='track', show=self.show_params['show-ref']) # ***************** # hide content and end animation, show control etc. # called by ready calback and end # ***************** def hide(self): self.mon.trace(self, '') # abort the timer if self.tick_timer is not None: self.canvas.after_cancel(self.tick_timer) self.tick_timer = None self.hide_x_content() # stop the plugin if self.track_params['plugin'] != '': self.pim.stop_plugin() # Control concurrent shows at end self.show_control(self.track_params['show-control-end']) # clear events list for this track if self.track_params['animate-clear'] == 'yes': self.animate.clear_events_list(id(self)) # create animation events for ending reason, message = self.animate.animate(self.animate_end_text, id(self)) if reason == 'error': self.play_state = 'show-failed' if self.finished_callback is not None: self.finished_callback('error', message) else: return def terminate(self): self.mon.trace(self, '') self.terminate_signal = True if self.play_state == 'showing': # call the derived class's stop method self.stop() else: self.end('killed', 'terminate with no track or show open') # must be overriden by derived class def stop(self): self.mon.fatal(self, 'stop not overidden by derived class') self.play_state = 'show-failed' if self.finished_callback is not None: self.finished_callback('error', 'stop not overidden by derived class') def get_play_state(self): return self.play_state # ***************** # ending the player # ***************** def end(self, reason, message): self.mon.trace(self, '') # stop the plugin if self.terminate_signal is True: reason = 'killed' self.terminate_signal = False self.hide() self.end_callback(reason, message) self = None # ***************** # displaying common things # ***************** def load_plugin(self): # load the plugin if required if self.track_params['plugin'] != '': reason, message, self.track = self.pim.load_plugin( self.track, self.track_params['plugin']) return reason, message def draw_plugin(self): # load the plugin if required if self.track_params['plugin'] != '': self.pim.draw_plugin() return def load_x_content(self, enable_menu): self.mon.trace(self, '') self.background_obj = None self.background = None self.track_text_obj = None self.show_text_obj = None self.hint_obj = None self.track_obj = None # background image if self.background_file != '': background_img_file = self.complete_path(self.background_file) if not os.path.exists(background_img_file): return 'error', "Track background file not found " + background_img_file else: pil_background_img = Image.open(background_img_file) # print 'pil_background_img ',pil_background_img image_width, image_height = pil_background_img.size window_width = self.show_canvas_width window_height = self.show_canvas_height if image_width != window_width or image_height != window_height: pil_background_img = pil_background_img.resize( (window_width, window_height)) self.background = ImageTk.PhotoImage(pil_background_img) del pil_background_img self.background_obj = self.canvas.create_image( self.show_canvas_x1, self.show_canvas_y1, image=self.background, anchor=NW) # print '\nloaded background_obj: ',self.background_obj # load the track content. Dummy function below is overridden in players status, message = self.load_track_content() if status == 'error': return 'error', message # load show text if enabled if self.show_params['show-text'] != '' and self.track_params[ 'display-show-text'] == 'yes': x, y, anchor, justify = calculate_text_position( self.show_params['show-text-x'], self.show_params['show-text-y'], self.show_canvas_x1, self.show_canvas_y1, self.show_canvas_centre_x, self.show_canvas_centre_y, self.show_canvas_x2, self.show_canvas_y2, self.show_params['show-text-justify']) self.show_text_obj = self.canvas.create_text( x, y, anchor=anchor, justify=justify, text=self.show_params['show-text'], fill=self.show_params['show-text-colour'], font=self.show_params['show-text-font']) # load track text if enabled if self.track_params['track-text'] != '': x, y, anchor, justify = calculate_text_position( self.track_params['track-text-x'], self.track_params['track-text-y'], self.show_canvas_x1, self.show_canvas_y1, self.show_canvas_centre_x, self.show_canvas_centre_y, self.show_canvas_x2, self.show_canvas_y2, self.track_params['track-text-justify']) self.track_text_obj = self.canvas.create_text( x, y, anchor=anchor, justify=justify, text=self.track_params['track-text'], fill=self.track_params['track-text-colour'], font=self.track_params['track-text-font']) # load instructions if enabled if enable_menu is True: x, y, anchor, justify = calculate_text_position( self.show_params['hint-x'], self.show_params['hint-y'], self.show_canvas_x1, self.show_canvas_y1, self.show_canvas_centre_x, self.show_canvas_centre_y, self.show_canvas_x2, self.show_canvas_y2, self.show_params['hint-justify']) self.hint_obj = self.canvas.create_text( x, y, justify=justify, text=self.show_params['hint-text'], fill=self.show_params['hint-colour'], font=self.show_params['hint-font'], anchor=anchor) self.display_show_canvas_rectangle() self.pim.draw_plugin() self.canvas.tag_raise('pp-click-area') self.canvas.itemconfig(self.background_obj, state='hidden') self.canvas.itemconfig(self.show_text_obj, state='hidden') self.canvas.itemconfig(self.track_text_obj, state='hidden') self.canvas.itemconfig(self.hint_obj, state='hidden') self.canvas.update_idletasks() return 'normal', 'x-content loaded' # display the rectangle that is the show canvas def display_show_canvas_rectangle(self): # coords=[self.show_canvas_x1,self.show_canvas_y1,self.show_canvas_x2-1,self.show_canvas_y2-1] # self.canvas.create_rectangle(coords, # outline='yellow', # fill='') pass # dummy functions to manipulate the track content, overidden in some players, # message text in messageplayer # image in imageplayer # menu stuff in menuplayer def load_track_content(self): return 'normal', 'player has no track content to load' def show_track_content(self): pass def hide_track_content(self): pass def show_x_content(self): self.mon.trace(self, '') # background colour if self.background_colour != '': self.canvas.config(bg=self.background_colour) # print 'showing background_obj: ', self.background_obj # reveal background image and text self.canvas.itemconfig(self.background_obj, state='normal') self.show_track_content() self.canvas.itemconfig(self.show_text_obj, state='normal') self.canvas.itemconfig(self.track_text_obj, state='normal') self.canvas.itemconfig(self.hint_obj, state='normal') # self.canvas.update_idletasks( ) # decide whether the show background should be enabled. # print 'DISPLAY SHOW BG',self.track_params['display-show-background'],self.background_obj if self.background_obj is None and self.track_params[ 'display-show-background'] == 'yes': self.enable_show_background = True else: self.enable_show_background = False # print 'ENABLE SB',self.enable_show_background def hide_x_content(self): self.mon.trace(self, '') self.hide_track_content() self.canvas.itemconfig(self.background_obj, state='hidden') self.canvas.itemconfig(self.show_text_obj, state='hidden') self.canvas.itemconfig(self.track_text_obj, state='hidden') self.canvas.itemconfig(self.hint_obj, state='hidden') # self.canvas.update_idletasks( ) self.canvas.delete(self.background_obj) self.canvas.delete(self.show_text_obj) self.canvas.delete(self.track_text_obj) self.canvas.delete(self.hint_obj) self.background = None # self.canvas.update_idletasks( ) # **************** # utilities # ***************** def get_links(self): return self.track_params['links'] # produce an absolute path from the relative one in track paramters def complete_path(self, track_file): # complete path of the filename of the selected entry if track_file[0] == "+": track_file = self.pp_home + track_file[1:] elif track_file[0] == "@": track_file = self.pp_profile + track_file[1:] return track_file # get a text string from resources.cfg def resource(self, section, item): value = self.rr.get(section, item) return value # False if not found
class OSCMonitor(object): def __init__(self): # get command options self.command_options = remote_options() # get directory holding the code self.pp_dir = sys.path[0] if not os.path.exists(self.pp_dir + os.sep + "pipresents.py"): tkinter.messagebox.showwarning("Pi Presents", "Bad Application Directory") exit() # Initialise logging Monitor.log_path = self.pp_dir self.mon = Monitor() self.mon.init() Monitor.classes = ['OSCMonitor', 'OSCConfig', 'OSCEditor'] Monitor.log_level = int(self.command_options['debug']) self.mon.log(self, "Pi Presents Monitor is starting") self.mon.log(self, " OS and separator " + os.name + ' ' + os.sep) self.mon.log(self, "sys.path[0] - location of code: code " + sys.path[0]) self.root = Tk() # initialise OSC config class self.osc_config = OSCConfig() # read the options and allow their editing self.osc_config_file = self.pp_dir + os.sep + 'pp_config' + os.sep + 'pp_oscmonitor.cfg' self.read_create_osc() if self.osc_config.slave_enabled != 'yes': self.mon.err(self, 'OSC Slave is not enabled in pp_oscmonitor.cfg') exit() #build gui self.setup_gui() # initialise self.init() #and start the system self.root.after(1000, self.run_app) self.root.mainloop() def init(self): self.desc.set('Listening for Commands from Master on: ' + self.osc_config.this_unit_ip + ':' + self.osc_config.listen_port) self.client = None self.server = None self.st = None def add_status(self, text): self.status_display.insert(END, text + '\n') self.status_display.see(END) def run_app(self): if self.osc_config.slave_enabled != 'yes': self.mon.err(self, 'Slave not enabled in oscmonitor.cfg') return if self.osc_config.this_unit_ip == '': self.mon.err(self, 'IP of own unit must be provided in oscmonitor.cfg') return if self.osc_config.listen_port == '': self.mon.err(self, 'Listen port must be provided in oscmonitor.cfg') return self.client = None self.server = None self.st = None # initialise OSC variables self.prefix = '/pipresents' self.this_unit = '/' + self.osc_config.this_unit_name self.add_status('this unit OSC address is: ' + self.this_unit) self.add_status('Listening for Commands from Master on: ' + self.osc_config.this_unit_ip + ':' + self.osc_config.listen_port) #connect client for replies then start server to listen for commands self.client = OSC.OSCClient() self.init_server(self.osc_config.this_unit_ip, self.osc_config.listen_port, self.client) self.add_initial_handlers() self.start_server() # *************************************** # OSC CLIENT TO SEND REPLIES # *************************************** def disconnect_client(self): if self.client != None: self.client.close() return # *************************************** # OSC SERVER TO LISTEN TO COMMANDS # *************************************** def init_server(self, ip, port_text, client): self.mon.log(self, 'Init Server: ' + ip + ':' + port_text) self.server = myOSCServer((ip, int(port_text)), client) def start_server(self): self.st = threading.Thread(target=self.server.serve_forever) self.st.start() def close_server(self): if self.server != None: self.server.close() self.mon.log(self, 'Waiting for Server-thread to finish') if self.st != None: self.st.join() ##!!! self.mon.log(self, 'server thread closed') def add_initial_handlers(self): pass self.server.addMsgHandler('default', self.no_match_handler) self.server.addMsgHandler( self.prefix + self.this_unit + "/system/server-info", self.server_info_handler) self.server.addMsgHandler( self.prefix + self.this_unit + "/system/loopback", self.loopback_handler) def no_match_handler(self, addr, tags, stuff, source): text = "Message from %s" % OSC.getUrlStr(source) + '\n' text += " %s" % addr + self.pretty_list(stuff) self.add_status(text + '\n') def server_info_handler(self, addr, tags, stuff, source): # send a reply to the client. msg = OSC.OSCMessage(self.prefix + '/system/server-info-reply') msg.append(self.osc_config.this_unit_name) msg.append(self.server.getOSCAddressSpace()) text = "Message from %s" % OSC.getUrlStr(source) + '\n' text += " %s" % addr + self.pretty_list(stuff) self.add_status(text) self.add_status('Sent reply to Server Info request to %s:' % OSC.getUrlStr(source) + '\n') return msg def loopback_handler(self, addr, tags, stuff, source): # send a reply to the client. msg = OSC.OSCMessage(self.prefix + '/system/loopback-reply') text = "Message from %s" % OSC.getUrlStr(source) + '\n' text += " %s" % addr + self.pretty_list(stuff) self.add_status(text + '\n' + 'Sent reply to Loopback request to %s:' % OSC.getUrlStr(source) + '\n') return msg def pretty_list(self, fields): text = ' ' for field in fields: text += str(field) + ' ' return text # *************************************** # INIT EXIT MISC # *************************************** def e_edit_osc(self): self.disconnect_client() self.close_server() self.edit_osc() self.read_create_osc() self.init() self.add_status('\n\n\nRESTART') self.run_app() def app_exit(self): self.disconnect_client() self.close_server() if self.root is not None: self.root.destroy() self.mon.finish() sys.exit() def show_help(self): tkinter.messagebox.showinfo("Help", "Read 'manual.pdf'") def about(self): tkinter.messagebox.showinfo( "About", "Simple Remote Monitor for Pi Presents\n" + "Author: Ken Thompson" + "\nWebsite: http://pipresents.wordpress.com/") def setup_gui(self): # set up the gui # root is the Tkinter root widget self.root.title("Remote Monitor for Pi Presents") # self.root.configure(background='grey') self.root.resizable(False, False) # define response to main window closing self.root.protocol("WM_DELETE_WINDOW", self.app_exit) # bind some display fields self.desc = StringVar() self.filename = StringVar() self.display_show = StringVar() self.results = StringVar() self.status = StringVar() # define menu menubar = Menu(self.root) osc_configmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") menubar.add_cascade(label='Options', menu=osc_configmenu) osc_configmenu.add_command(label='Edit', command=self.e_edit_osc) helpmenu = Menu(menubar, tearoff=0, bg="grey", fg="black") menubar.add_cascade(label='Help', menu=helpmenu) helpmenu.add_command(label='Help', command=self.show_help) helpmenu.add_command(label='About', command=self.about) self.root.config(menu=menubar) # info frame info_frame = Frame(self.root, padx=5, pady=5) info_frame.pack(side=TOP, fill=BOTH, expand=1) info_name = Label(info_frame, text="Slave Unit's Name: " + self.osc_config.this_unit_name, font="arial 12 bold") info_name.pack(side=TOP) info_this_address = Label(info_frame, textvariable=self.desc, font="arial 12 bold") info_this_address.pack(side=TOP) # status_frame status_frame = Frame(self.root, padx=5, pady=5) status_frame.pack(side=TOP, fill=BOTH, expand=1) status_label = Label(status_frame, text="Status:", font="arial 12 bold") status_label.pack(side=LEFT) scrollbar = Scrollbar(status_frame, orient=VERTICAL) self.status_display = Text(status_frame, height=20, yscrollcommand=scrollbar.set) scrollbar.config(command=self.status_display.yview) scrollbar.pack(side=RIGHT, fill=Y) self.status_display.pack(side=LEFT, fill=BOTH, expand=1) # *************************************** # OSC CONFIGURATION # *************************************** def read_create_osc(self): if self.osc_config.read(self.osc_config_file) is False: self.osc_config.create(self.osc_config_file, 'slave') eosc = OSCEditor(self.root, self.osc_config_file, 'slave', 'Create OSC Monitor Configuration') self.osc_config.read(self.osc_config_file) def edit_osc(self): if self.osc_config.read(self.osc_config_file) is False: self.osc_config.create(self.osc_config_file) eosc = OSCEditor(self.root, self.osc_config_file, 'slave', 'Edit OSC Monitor Configuration')
class MediaShow: # ******************* # External interface # ******************** def __init__(self, show, canvas, showlist, pp_home, pp_profile): """ canvas - the canvas that the menu is to be written on show - the dictionary fo the show to be played pp_home - Pi presents data_home directory pp_profile - Pi presents profile directory """ self.mon = Monitor() self.mon.on() #instantiate arguments self.show = show self.showlist = showlist self.canvas = canvas self.pp_home = pp_home self.pp_profile = pp_profile # open resources self.rr = ResourceReader() # Init variables self.player = None self.shower = None self._poll_for_interval_timer = None self._poll_for_continue_timer = None self._waiting_for_interval = False self._interval_timer = None self.error = False self._interval_timer_signal = False self._end_mediashow_signal = False self._next_track_signal = False self._previous_track_signal = False self._play_child_signal = False self._req_next = 'nil' self._state = 'closed' def play(self, end_callback, ready_callback=None, top=False, command='nil'): """ displays the mediashow end_callback - function to be called when the menu exits ready_callback - callback when menu is ready to display (not used) top is True when the show is top level (run from [start]) """ #instantiate the arguments self._end_callback = end_callback self._ready_callback = ready_callback self.top = top self.command = command self.mon.log(self, "Starting show: " + self.show['show-ref']) # check data files are available. self.media_file = self.pp_profile + "/" + self.show['medialist'] if not os.path.exists(self.media_file): self.mon.err(self, "Medialist file not found: " + self.media_file) self._end('error', "Medialist file not found") #create a medialist for the mediashow and read it. self.medialist = MediaList() if self.medialist.open_list(self.media_file, self.showlist.sissue()) == False: self.mon.err(self, "Version of medialist different to Pi Presents") self._end('error', "Version of medialist different to Pi Presents") self._wait_for_trigger() # respond to key presses. def key_pressed(self, key_name): self.mon.log(self, "received key: " + key_name) if key_name == '': pass elif key_name == 'escape': # if next lower show is running pass down to stop the show and lower level if self.shower <> None: self.shower.key_pressed(key_name) # if not at top stop the show else: if self.top == False: self._end_mediashow_signal = True # and if a track is running stop that first if self.player <> None: self.player.key_pressed(key_name) else: # at top level in a manual presentation stop the track if self.show['progress'] == 'manual': if self.player <> None: self.player.key_pressed(key_name) elif key_name in ('up', 'down'): # if child or sub-show is running and is a show pass to show, track does not use up/down # otherwise use keys for next or previous if self.shower <> None: self.shower.key_pressed(key_name) else: if key_name == 'up': self._previous() else: self._next() elif key_name == 'return': # if child show or sub-show is running and is show - pass down- player does not use return # ELSE use Return to start child or to start the show if waiting if self.shower <> None: self.shower.key_pressed(key_name) else: if self._state == 'playing': if self.show['has-child'] == 'yes': self._play_child_signal = True # and stop the current track if its running if self.player <> None: self.player.key_pressed("escape") else: self._start_show() elif key_name == 'pir': self._start_show() elif key_name in ('p', ' '): # pass down if show or track running. if self.shower <> None: self.shower.key_pressed(key_name) elif self.player <> None: self.player.key_pressed(key_name) def button_pressed(self, button, edge): if button == 'play': self.key_pressed("return") elif button == 'up': self.key_pressed("up") elif button == 'down': self.key_pressed("down") elif button == 'stop': self.key_pressed("escape") elif button == 'pause': self.key_pressed('p') elif button == 'PIR': self.key_pressed('pir') # kill or error def terminate(self, reason): if self.shower <> None: self.mon.log(self, "sent terminate to shower") self.shower.terminate(reason) elif self.player <> None: self.mon.log(self, "sent terminate to player") self.player.terminate(reason) else: self._end(reason, 'terminated without terminating shower or player') def _tidy_up(self): if self._poll_for_continue_timer <> None: self.canvas.after_cancel(self._poll_for_continue_timer) self._poll_for_continue_timer = None if self._poll_for_interval_timer <> None: self.canvas.after_cancel(self._poll_for_interval_timer) self._poll_for_interval_timer = None if self._interval_timer <> None: self.canvas.after_cancel(self._interval_timer) self._interval_timer = None def resource(self, section, item): value = self.rr.get(section, item) if value == False: self.mon.err(self, "resource: " + section + ': ' + item + " not found") self.terminate("error") else: return value # *************************** # Respond to key/button presses # *************************** def _stop(self, message): self._end_mediashow_signal = True if self._interval_timer <> None: self.canvas.after_cancel(self._interval_timer) def _next(self): # stop track if running and set signal self._next_track_signal = True if self.shower <> None: self.shower.key_pressed("escape") else: if self.player <> None: self.player.key_pressed("escape") def _previous(self): self._previous_track_signal = True if self.shower <> None: self.shower.key_pressed("escape") else: if self.player <> None: self.player.key_pressed("escape") # *************************** # end of show functions # *************************** def _end(self, reason, message): self._end_mediashow_signal = False self.mon.log(self, "Ending Mediashow: " + self.show['show-ref']) self._tidy_up() self._end_callback(reason, message) self = None return # *************************** # Show sequencer # *************************** def _wait_for_trigger(self): self._state = 'waiting' if self.ready_callback <> None: self.ready_callback() self.mon.log(self, "Waiting for trigger: " + self.show['trigger']) if self.show['trigger'] == "button": # blank screen waiting for trigger if auto, otherwise display something if self.show['progress'] == "manual": text = self.resource('mediashow', 'm01') else: text = "" self.display_message(self.canvas, 'text', text, 0, self._start_show) elif self.show['trigger'] == "PIR": # blank screen waiting for trigger text = self.resource('mediashow', 'm02') self.display_message(self.canvas, 'text', text, 0, self._start_show) elif self.show['trigger'] == "start": self._start_show() else: self.mon.err(self, "Unknown trigger: " + self.show['trigger']) self._end('error', "Unknown trigger type") def _start_show(self): self._state = 'playing' self._direction = 'forward' # start interval timer if self.show[ 'repeat'] == "interval" and self.show['repeat-interval'] <> 0: self._interval_timer_signal = False self._interval_timer = self.canvas.after( int(self.show['repeat-interval']) * 1000, self._end_interval_timer) # and play the first track unless commanded otherwise if self.command == 'backward': self.medialist.finish() else: self.medialist.start() self._play_selected_track(self.medialist.selected_track()) def _what_next(self): self._direction = 'forward' # user wants to end, wait for any shows or tracks to have ended then end show if self._end_mediashow_signal == True: if self.player == None and self.shower == None: self._end_mediashow_signal = False self._end('normal', "show ended by user") else: pass #returning from a subshow needing to move onward elif self._req_next == 'do-next': self._req_next = 'nil' self.medialist.next() self._play_selected_track(self.medialist.selected_track()) #returning from a subshow needing to move backward elif self._req_next == 'do-previous': self._req_next = 'nil' self._direction = 'backward' self.medialist.previous() self._play_selected_track(self.medialist.selected_track()) # user wants to play child elif self._play_child_signal == True: self._play_child_signal = False index = self.medialist.index_of_track('pp-child-show') if index >= 0: #don't select the track as need to preserve mediashow sequence. child_track = self.medialist.track(index) self._display_eggtimer(self.resource('mediashow', 'm07')) self._play_selected_track(child_track) else: self.mon.err( self, "Child show not found in medialist: " + self.show['pp-child-show']) self._end('error', "child show not found in medialist") # skip to next track on user input elif self._next_track_signal == True: self._next_track_signal = False if self.medialist.at_end() == True: if self.show['sequence'] == "ordered" and self.show[ 'repeat'] == 'oneshot' and self.top == False: self._end('do-next', "Return from Sub Show") else: self.medialist.next() self._play_selected_track(self.medialist.selected_track()) else: self.medialist.next() self._play_selected_track(self.medialist.selected_track()) # skip to previous track on user input elif self._previous_track_signal == True: self._previous_track_signal = False self._direction = 'backward' if self.medialist.at_start() == True: if self.show['sequence'] == "ordered" and self.show[ 'repeat'] == 'oneshot' and self.top == False: self._end('do-previous', "Return from Sub Show") else: self.medialist.previous() self._play_selected_track(self.medialist.selected_track()) else: self.medialist.previous() self._play_selected_track(self.medialist.selected_track()) # track is finished and we are on auto elif self.show['progress'] == "auto": if self.medialist.at_end() == True: if self.show['sequence'] == "ordered" and self.show[ 'repeat'] == 'oneshot' and self.top == False: self._end('do-next', "Return from Sub Show") #### elif elif self.show['sequence'] == "ordered" and self.show[ 'repeat'] == 'oneshot' and self.top == True: self._wait_for_trigger() elif self._waiting_for_interval == True: if self._interval_timer_signal == True: self._interval_timer_signal = False self._waiting_for_interval = False self._start_show() else: self._poll_for_interval_timer = self.canvas.after( 1000, self._what_next) elif self.show['sequence'] == "ordered" and self.show[ 'repeat'] == 'interval' and int( self.show['repeat-interval']) > 0: self._waiting_for_interval = True self._poll_for_interval_timer = self.canvas.after( 1000, self._what_next) elif self.show['sequence'] == "ordered" and self.show[ 'repeat'] == 'interval' and int( self.show['repeat-interval']) == 0: self.medialist.next() self._play_selected_track(self.medialist.selected_track()) else: self.mon.err(self, "Unhandled playing event: ") self._end('error', "Unhandled playing event") else: self.medialist.next() self._play_selected_track(self.medialist.selected_track()) # track has finished and we are on manual progress elif self.show['progress'] == "manual": self._delete_eggtimer() self._display_eggtimer(self.resource('mediashow', 'm03')) self._poll_for_continue_timer = self.canvas.after( 500, self._what_next) else: #unhandled state self.mon.err(self, "Unhandled playing event: ") self._end('error', "Unhandled playing event") def _end_interval_timer(self): self._interval_timer_signal = True # *************************** # Dispatching to Players/Shows # *************************** # used to display internal messages in situations where a medialist entry could be used. def display_message(self, canvas, source, content, duration, _display_message_callback): self._display_message_callback = _display_message_callback tp = { 'duration': duration, 'message-colour': 'white', 'message-font': 'Helvetica 20 bold' } self.player = MessagePlayer(canvas, tp, tp) self.player.play(content, self._display_message_end, None) def _display_message_end(self, reason, message): self.player = None if reason in ('error', 'killed'): self._end(reason, message) else: self._display_message_callback() def complete_path(self, selected_track): # complete path of the filename of the selected entry track_file = selected_track['location'] if track_file[0] == "+": track_file = self.pp_home + track_file[1:] self.mon.log(self, "Track to play is: " + track_file) return track_file def _play_selected_track(self, selected_track): """ selects the appropriate player from type field of the medialist and computes the parameters for that type selected track is a dictionary for the track/show """ self.canvas.delete(ALL) if self.show['progress'] == "manual": self._display_eggtimer(self.resource('mediashow', 'm04')) # is menu required if self.show['has-child'] == "yes": enable_child = True else: enable_child = False #dispatch track by type self.player = None self.shower = None track_type = selected_track['type'] self.mon.log(self, "Track type is: " + track_type) if track_type == "video": # create a videoplayer track_file = self.complete_path(selected_track) self.player = VideoPlayer(self.canvas, self.show, selected_track) self.player.play(track_file, self.end_player, self.ready_callback, enable_menu=enable_child) elif track_type == "image": track_file = self.complete_path(selected_track) # images played from menus don't have children self.player = ImagePlayer(self.canvas, self.show, selected_track) self.player.play(track_file, self.end_player, self.ready_callback, enable_menu=enable_child) elif track_type == "message": # bit odd because MessagePlayer is used internally to display text. text = selected_track['text'] self.player = MessagePlayer(self.canvas, self.show, selected_track) self.player.play(text, self.end_player, self.ready_callback, enable_menu=enable_child) elif track_type == "show": # get the show from the showlist index = self.showlist.index_of_show(selected_track['sub-show']) if index >= 0: self.showlist.select(index) selected_show = self.showlist.selected_show() else: self.mon.err( self, "Show not found in showlist: " + selected_track['sub-show']) self._end('error', "Unknown show") if selected_show['type'] == "mediashow": self.shower = MediaShow(selected_show, self.canvas, self.showlist, self.pp_home, self.pp_profile) self.shower.play(self.end_shower, top=False, command=self._direction) elif selected_show['type'] == "liveshow": self.shower = LiveShow(selected_show, self.canvas, self.showlist, self.pp_home, self.pp_profile) self.shower.play(self.end_shower, top=False, command='nil') elif selected_show['type'] == "menu": self.shower = MenuShow(selected_show, self.canvas, self.showlist, self.pp_home, self.pp_profile) self.shower.play(self.end_shower, top=False, command='nil') else: self.mon.err(self, "Unknown Show Type: " + selected_show['type']) self._end('error' "Unknown show type") else: self.mon.err(self, "Unknown Track Type: " + track_type) self._end('error', "Unknown track type") def ready_callback(self): self._delete_eggtimer() def end_player(self, reason, message): self._req_next = 'nil' self.mon.log(self, "Returned from player with message: " + message) self.player = None if reason in ("killed", "error"): self._end(reason, message) elif self.show['progress'] == "manual": self._display_eggtimer(self.resource('mediashow', 'm05')) self._req_next = reason self._what_next() else: self._req_next = reason self._what_next() def end_shower(self, reason, message): self._req_next = 'nil' self.mon.log(self, "Returned from shower with message: " + message) self.shower = None if reason in ("killed", "error"): self._end(reason, message) elif self.show['progress'] == "manual": self._display_eggtimer(self.resource('mediashow', 'm06')) self._req_next = reason self._what_next() else: self._req_next = reason self._what_next() def _display_eggtimer(self, text): self.canvas.create_text(int(self.canvas['width']) / 2, int(self.canvas['height']) / 2, text=text, fill='white', font="Helvetica 20 bold") self.canvas.update_idletasks() def _delete_eggtimer(self): self.canvas.delete(ALL)
class LiveShow: NEW_TRACKS = { 'image': { 'title': 'New Image', 'track-ref': '', 'type': 'image', 'location': '', 'duration': '', 'transition': '', 'track-text': '', 'track-text-font': '', 'track-text-colour': '', 'track-text-x': '0', 'track-text-y': '0' }, 'video': { 'title': 'New Video', 'track-ref': '', 'type': 'video', 'location': '', 'omx-audio': '' } } IMAGE_FILES = ('.gif', '.jpg', '.jpeg', '.bmp', '.png', '.tif') VIDEO_FILES = ('.mp4', '.mkv', '.avi', '.mp2', '.wmv') AUDIO_FILES = ('.mp3', '.wav', '.ogg') # ******************* # External interface # ******************** def __init__(self, show, canvas, showlist, pp_home, pp_profile): """ canvas - the canvas that the show is to be written on showlist - used jus to check the issue of medialist against showlist show - the dictionary for the show to be played pp_home - Pi presents data_home directory pp_profile - Pi presents profile directory """ self.mon = Monitor() self.mon.on() #instantiate arguments self.show = show self.showlist = showlist self.canvas = canvas self.pp_home = pp_home self.pp_profile = pp_profile # open resources self.rr = ResourceReader() # Init variables self.player = None self.shower = None self._end_liveshow_signal = False self._play_child_signal = False self.error = False self._livelist = None self._new_livelist = None def play(self, end_callback, ready_callback=None, top=False, command='nil'): """ displays the liveshow end_callback - function to be called when the liveshow exits ready_callback - callback when liveshow is ready to display top is True when the show is top level (i.e. run from start show) """ #instantiate the arguments self._end_callback = end_callback self._ready_callback = ready_callback self.top = top self.mon.log(self, "Starting show: " + self.show['show-ref']) # check data files are available. self.media_file = self.pp_profile + os.sep + self.show['medialist'] if not os.path.exists(self.media_file): self.mon.err(self, "Medialist file not found: " + self.media_file) self._stop("Medialist file not found") self.options = command_options() self._pp_live_dir1 = self.pp_home + os.sep + 'pp_live_tracks' if not os.path.exists(self._pp_live_dir1): os.mkdir(self._pp_live_dir1) self._pp_live_dir2 = '' if self.options['liveshow'] <> "": self._pp_live_dir2 = self.options['liveshow'] if not os.path.exists(self._pp_live_dir2): self.mon.err( self, "live tracks directory not found " + self._pp_live_dir2) self._end('error', "live tracks directory not found") #create a medialist for the liveshow and read it. # it should be empty of anonymous tracks but read it to check its version. self.medialist = MediaList() if self.medialist.open_list(self.media_file, self.showlist.sissue()) == False: self.mon.err(self, "Version of medialist different to Pi Presents") self._end('error', "Version of medialist different to Pi Presents") if self.ready_callback <> None: self.ready_callback() self._play_first_track() # respond to key presses. def key_pressed(self, key_name): self.mon.log(self, "received key: " + key_name) if key_name == '': pass elif key_name == 'escape': # if next lower show eor player is running pass down to stop the show/track # ELSE stop this show except for exceptions if self.shower <> None: self.shower.key_pressed(key_name) elif self.player <> None: self.player.key_pressed(key_name) else: # not at top so stop the show if self.top == False: self._stop("exit show to higher level") else: pass elif key_name in ('up', 'down'): # if child or sub-show is running and is a show pass to show, track does not use up/down if self.shower <> None: self.shower.key_pressed(key_name) elif key_name == 'return': # if child show or sub-show is running and is show - pass down # ELSE use Return to start child if self.shower <> None: self.shower.key_pressed(key_name) else: if self.show['has-child'] == "yes": self._play_child() elif key_name in ('p', ' '): # pass down if show or track running. if self.shower <> None: self.shower.key_pressed(key_name) elif self.player <> None: self.player.key_pressed(key_name) def button_pressed(self, button, edge): if button == 'play': self.key_pressed("return") elif button == 'up': self.key_pressed("up") elif button == 'down': self.key_pressed("down") elif button == 'stop': self.key_pressed("escape") elif button == 'pause': self.key_pressed('p') # kill or error def terminate(self, reason): if self.shower <> None: self.mon.log(self, "sent terminate to shower") self.shower.terminate(reason) elif self.player <> None: self.mon.log(self, "sent terminate to player") self.player.terminate(reason) else: self._end(reason, 'terminated without terminating shower or player') def _tidy_up(self): pass def resource(self, section, item): value = self.rr.get(section, item) if value == False: self.mon.err(self, "resource: " + section + ': ' + item + " not found") self.terminate("error", 'Cannot find resource') else: return value # *************************** # Respond to key/button presses # *************************** def _stop(self, message): self._end_liveshow_signal = True def _play_child(self): self._play_child_signal = True if self.player <> None: self.player.key_pressed("escape") # *************************** # end of show functions # *************************** def _end(self, reason, message): self._end_liveshow_signal = False self.mon.log(self, "Ending Liveshow: " + self.show['show-ref']) self._tidy_up() self._end_callback(reason, message) self = None return def _nend(self): self._end('normal', 'end from state machine') # *************************** # Livelist # *************************** def _livelist_add_track(self, afile): (root, title) = os.path.split(afile) (root, ext) = os.path.splitext(afile) if ext.lower() in LiveShow.IMAGE_FILES: self._livelist_new_track(LiveShow.NEW_TRACKS['image'], { 'title': title, 'track-ref': '', 'location': afile }) if ext.lower() in LiveShow.VIDEO_FILES: self._livelist_new_track(LiveShow.NEW_TRACKS['video'], { 'title': title, 'track-ref': '', 'location': afile }) if ext.lower() in LiveShow.AUDIO_FILES: self._livelist_new_track(LiveShow.NEW_TRACKS['video'], { 'title': title, 'track-ref': '', 'location': afile }) def _livelist_new_track(self, fields, values): new_track = fields self._new_livelist.append(copy.deepcopy(new_track)) last = len(self._new_livelist) - 1 self._new_livelist[last].update(values) def _new_livelist_create(self): self._new_livelist = [] if os.path.exists(self._pp_live_dir1): for file in os.listdir(self._pp_live_dir1): file = self._pp_live_dir1 + os.sep + file (root_file, ext_file) = os.path.splitext(file) if ext_file.lower( ) in LiveShow.IMAGE_FILES + LiveShow.VIDEO_FILES + LiveShow.AUDIO_FILES: self._livelist_add_track(file) if os.path.exists(self._pp_live_dir2): for file in os.listdir(self._pp_live_dir2): file = self._pp_live_dir2 + os.sep + file (root_file, ext_file) = os.path.splitext(file) if ext_file.lower( ) in LiveShow.IMAGE_FILES + LiveShow.VIDEO_FILES + LiveShow.AUDIO_FILES: self._livelist_add_track(file) self._new_livelist = sorted( self._new_livelist, key=lambda track: os.path.basename(track['location']).lower()) # for it in self._new_livelist: # print it['location'] # print '' def _livelist_replace_if_changed(self): self._new_livelist_create() if self._new_livelist <> self._livelist: self._livelist = copy.deepcopy(self._new_livelist) self._livelist_index = 0 def _livelist_next(self): if self._livelist_index == len(self._livelist) - 1: self._livelist_index = 0 else: self._livelist_index += 1 # *************************** # Play Loop # *************************** def _play_first_track(self): self._new_livelist_create() self._livelist = copy.deepcopy(self._new_livelist) self._livelist_index = 0 self._play_track() def _play_track(self): self._livelist_replace_if_changed() if len(self._livelist) > 0: self._play_selected_track(self._livelist[self._livelist_index]) else: self.display_message(self.canvas, None, self.resource('liveshow', 'm01'), 5, self._what_next) def _what_next(self): # user wants to end if self._end_liveshow_signal == True: self._end_liveshow_signal = False self._end('normal', "show ended by user") # play child? elif self._play_child_signal == True: self._play_child_signal = False index = self.medialist.index_of_track('pp-child-show') if index >= 0: #don't select the track as need to preserve mediashow sequence. child_track = self.medialist.track(index) self._display_eggtimer(self.resource('liveshow', 'm02')) self._play_selected_track(child_track) else: self.mon.err( self, "Child show not found in medialist: " + self.show['pp-child-show']) self._end('error', "child show not found in medialist") # otherwise loop to next track else: self._livelist_next() self._play_track() # *************************** # Dispatching to Players/Shows # *************************** # used to display internal messages in situations where a medialist entry could not be used. def display_message(self, canvas, source, content, duration, _display_message_callback): self._display_message_callback = _display_message_callback tp = { 'duration': duration, 'message-colour': 'white', 'message-font': 'Helvetica 20 bold' } self.player = MessagePlayer(canvas, tp, tp) self.player.play(content, self._display_message_end, None) def _display_message_end(self, reason, message): self.player = None if reason in ("killed", 'error'): self._end(reason, message) else: self._display_message_callback() def complete_path(self, selected_track): # complete path of the filename of the selected entry track_file = selected_track['location'] if track_file[0] == "+": track_file = self.pp_home + track_file[1:] self.mon.log(self, "Track to play is: " + track_file) return track_file def _play_selected_track(self, selected_track): """ selects the appropriate player from type field of the medialist and computes the parameters for that type selected_track is a dictionary for the track/show """ # self.canvas.delete(ALL) # is menu required if self.show['has-child'] == "yes": enable_child = True else: enable_child = False #dispatch track by type self.player = None self.shower = None track_type = selected_track['type'] self.mon.log(self, "Track type is: " + track_type) if track_type == "image": track_file = self.complete_path(selected_track) # images played from menus don't have children self.player = ImagePlayer(self.canvas, self.show, selected_track) self.player.play(track_file, self.end_player, self.ready_callback, enable_menu=enable_child) elif track_type == "video": # create a videoplayer track_file = self.complete_path(selected_track) self.player = VideoPlayer(self.canvas, self.show, selected_track) self.player.play(track_file, self.end_player, self.ready_callback, enable_menu=enable_child) elif track_type == "show": # get the show from the showlist index = self.showlist.index_of_show(selected_track['sub-show']) if index >= 0: self.showlist.select(index) selected_show = self.showlist.selected_show() else: self.mon.err( self, "Show not found in showlist: " + selected_track['sub-show']) self._stop("Unknown show") if selected_show['type'] == "mediashow": self.shower = MediaShow(selected_show, self.canvas, self.showlist, self.pp_home, self.pp_profile) self.shower.play(self.end_shower, top=False, command='nil') elif selected_show['type'] == "menu": self.shower = MenuShow(selected_show, self.canvas, self.showlist, self.pp_home, self.pp_profile) self.shower.play(self.end_shower, top=False, command='nil') else: self.mon.err(self, "Unknown Show Type: " + selected_show['type']) self._stop("Unknown show type") else: self.mon.err(self, "Unknown Track Type: " + track_type) self._stop("Unknown track type") def ready_callback(self): self._delete_eggtimer() def end_player(self, reason, message): self.mon.log(self, "Returned from player with message: " + message) self.player = None if reason in ("killed", "error"): self._end(reason, message) else: self._what_next() def end_shower(self, reason, message): self.mon.log(self, "Returned from shower with message: " + message) self.shower = None if reason in ("killed", "error"): self._end(reason, message) else: self._what_next() def _display_eggtimer(self, text): self.canvas.create_text(int(self.canvas['width']) / 2, int(self.canvas['height']) / 2, text=text, fill='white', font="Helvetica 20 bold") self.canvas.update_idletasks() def _delete_eggtimer(self): self.canvas.delete(ALL)
class MediaShow: # ******************* # External interface # ******************** def __init__(self, show_params, root, canvas, showlist, pp_dir, pp_home, pp_profile): """ canvas - the canvas that the menu is to be written on show - the dictionary fo the show to be played pp_home - Pi presents data_home directory pp_profile - Pi presents profile directory """ self.mon = Monitor() self.mon.on() #instantiate arguments self.show_params = show_params self.showlist = showlist self.root = root self.canvas = canvas self.pp_dir = pp_dir self.pp_home = pp_home self.pp_profile = pp_profile # open resources self.rr = ResourceReader() # Init variables self.player = None self.shower = None self.poll_for_interval_timer = None self.poll_for_continue_timer = None self.waiting_for_interval = False self.interval_timer = None self.duration_timer = None self.error = False self.interval_timer_signal = False self.end_trigger_signal = False self.end_mediashow_signal = False self.next_track_signal = False self.previous_track_signal = False self.play_child_signal = False self.req_next = 'nil' #create and instance of TimeOfDay scheduler so we can add events self.tod = TimeOfDay() self.state = 'closed' def play(self, show_id, end_callback, show_ready_callback, top=False, command='nil'): """ displays the mediashow end_callback - function to be called when the menu exits ready_callback - callback when menu is ready to display (not used) top is True when the show is top level (run from [start]) """ #instantiate the arguments self.show_id = show_id self.end_callback = end_callback self.show_ready_callback = show_ready_callback self.top = top self.command = command self.mon.log( self, self.show_params['show-ref'] + ' ' + str(self.show_id) + ": Starting show") # check data files are available. self.media_file = self.pp_profile + "/" + self.show_params['medialist'] if not os.path.exists(self.media_file): self.mon.err(self, "Medialist file not found: " + self.media_file) self.end('error', "Medialist file not found") #create a medialist for the mediashow and read it. self.medialist = MediaList() if self.medialist.open_list(self.media_file, self.showlist.sissue()) == False: self.mon.err(self, "Version of medialist different to Pi Presents") self.end('error', "Version of medialist different to Pi Presents") #get controls for this show if top level controlsmanager = ControlsManager() if self.top == True: self.controls_list = controlsmanager.default_controls() # and merge in controls from profile self.controls_list = controlsmanager.merge_show_controls( self.controls_list, self.show_params['controls']) #set up the time of day triggers for the show if self.show_params['trigger'] in ('time', 'time-quiet'): error_text = self.tod.add_times(self.show_params['trigger-input'], id(self), self.tod_start_callback, self.show_params['trigger']) if error_text <> '': self.mon.err(self, error_text) self.end('error', error_text) if self.show_params['trigger-end'] == 'time': # print self.show_params['trigger-end-time'] error_text = self.tod.add_times( self.show_params['trigger-end-time'], id(self), self.tod_end_callback, 'n/a') if error_text <> '': self.mon.err(self, error_text) self.end('error', error_text) if self.show_params['trigger-end'] == 'duration': error_text = self.calculate_duration( self.show_params['trigger-end-time']) if error_text <> '': self.mon.err(self, error_text) self.end('error', error_text) self.state = 'closed' self.egg_timer = None self.wait_for_trigger() # ******************************** # Respond to external events # ******************************** #stop received from another concurrent show def managed_stop(self): # if next lower show is running pass down to stop the show and lower level if self.shower <> None: self.shower.managed_stop() else: #stop the show if not at top self.end_mediashow_signal = True # and if track is runing stop that first if self.player <> None: self.player.input_pressed('stop') # kill or error def terminate(self, reason): if self.shower <> None: self.shower.terminate(reason) elif self.player <> None: self.player.terminate(reason) else: self.end(reason, ' terminated with no shower or player to terminate') # respond to input events def input_pressed(self, symbol, edge, source): self.mon.log( self, self.show_params['show-ref'] + ' ' + str(self.show_id) + ": received input: " + symbol) # check symbol against mediashow triggers, triggers can be used at top or lower level # and not affected by disable-controls if self.state == 'waiting' and self.show_params['trigger'] in ( 'input', 'input-quiet') and symbol == self.show_params['trigger-input']: self.start_show() elif self.state == 'playing' and self.show_params[ 'trigger-next'] == 'input' and symbol == self.show_params[ 'next-input']: self.next() # internal functions are triggered only when disable-controls is 'no' if self.show_params['disable-controls'] == 'yes': return # if at top convert symbolic name to operation otherwise lower down we have received an operation # look through list of standard symbols to find match (symbolic-name, function name) operation =lookup (symbol if self.top == True: operation = self.lookup_control(symbol, self.controls_list) else: operation = symbol # print 'operation',operation self.do_operation(operation, edge, source) #service the standard inputs for this show def do_operation(self, operation, edge, source): if self.shower <> None: # if next lower show is running pass down to stop the show and lower level self.shower.input_pressed(operation, edge, source) else: # control this show and its tracks # print 'operation',operation if operation == 'stop': if self.top == False: # not at top so stop the current show self.end_mediashow_signal = True # and if a track is running stop that first if self.player <> None: self.player.input_pressed('stop') else: # top = True, just stop track if running if self.player <> None: self.player.input_pressed('stop') elif operation in ('up', 'down'): #if playing rather than waiting use keys for next or previous if operation == 'up' and self.state == 'playing': self.previous() else: self.next() elif operation == 'play': # use 'play' to start child if state=playing or to trigger the show if waiting for trigger if self.state == 'playing': if self.show_params['has-child'] == 'yes': self.play_child_signal = True self.child_track_ref = 'pp-child-show' # and stop the current track if its running if self.player <> None: self.player.input_pressed('stop') else: if self.state == 'waiting': self.start_show() elif operation == 'pause': if self.player <> None: self.player.input_pressed(operation) #if the operation is omxplayer or mplayer runtime control then pass it to player if running elif operation[0:4] == 'omx-' or operation[ 0:6] == 'mplay-' or operation[0:5] == 'uzbl-': if self.player <> None: self.player.input_pressed(operation) def lookup_control(self, symbol, controls_list): for control in controls_list: if symbol == control[0]: return control[1] return '' # *************************** # Show sequencer # *************************** def end_interval_timer(self): self.interval_timer_signal = True # callback from time of day scheduler def tod_start_callback(self): if self.state == 'waiting' and self.show_params['trigger'] in ( 'time', 'time-quiet'): self.start_show() def tod_end_callback(self): if self.state == 'playing' and self.show_params['trigger-end'] in ( 'time', 'duration'): self.end_trigger_signal = True if self.shower <> None: self.shower.input_pressed('stop') elif self.player <> None: self.player.input_pressed('stop') def stop(self, message): self.end_mediashow_signal = True if self.interval_timer <> None: self.canvas.after_cancel(self.interval_timer) def next(self): # stop track if running and set signal self.next_track_signal = True if self.shower <> None: self.shower.input_pressed("stop") else: if self.player <> None: self.player.input_pressed("stop") def previous(self): self.previous_track_signal = True if self.shower <> None: self.shower.input_pressed("stop") else: if self.player <> None: self.player.input_pressed("stop") # wait for trigger sets the state to waiting so that events can do a start show. def wait_for_trigger(self): self.state = 'waiting' if self.show_ready_callback <> None: self.show_ready_callback() self.mon.log( self, self.show_params['show-ref'] + ' ' + str(self.show_id) + ": Waiting for trigger: " + self.show_params['trigger']) if self.show_params['trigger'] == "input": # blank screen waiting for trigger if auto, otherwise display something if self.show_params['progress'] == "manual": text = self.resource('mediashow', 'm01') else: text = self.resource('mediashow', 'm02') self.display_message(self.canvas, 'text', text, 0, self.start_show) elif self.show_params['trigger'] == "input-quiet": # blank screen waiting for trigger text = self.resource('mediashow', 'm10') self.display_message(self.canvas, 'text', text, 0, self.start_show) pass elif self.show_params['trigger'] in ('time', 'time-quiet'): # show next show notice quiet = 3 # if next show is this one display text next_show = self.tod.next_event_time() if next_show[quiet] == False: if next_show[1] == 'tomorrow': text = self.resource('mediashow', 'm09') else: text = self.resource('mediashow', 'm08') text = text.replace('%tt', next_show[0]) self.display_message(self.canvas, 'text', text, 0, self.start_show) elif self.show_params['trigger'] == "start": self.start_show() else: self.mon.err(self, "Unknown trigger: " + self.show_params['trigger']) self.end('error', "Unknown trigger type") def start_show(self): self.state = 'playing' self.direction = 'forward' # self.canvas.delete(ALL) # start interval timer if self.show_params['repeat'] == "interval" and self.show_params[ 'repeat-interval'] <> 0: self.interval_timer_signal = False self.interval_timer = self.canvas.after( int(self.show_params['repeat-interval']) * 1000, self.end_interval_timer) # start duration timer if self.show_params['trigger-end'] == 'duration': # print 'set alarm ', self.duration self.duration_timer = self.canvas.after(self.duration * 1000, self.tod_end_callback) # and play the first track unless commanded otherwise if self.command == 'backward': self.medialist.finish() else: self.medialist.start() self.play_selected_track(self.medialist.selected_track()) def what_next(self): self.direction = 'forward' # end of show trigger caused by tod if self.end_trigger_signal == True: self.end_trigger_signal = False if self.top == True: self.state = 'waiting' self.wait_for_trigger() else: # not at top so stop the show self.end('normal', 'sub-show end time trigger') # user wants to end, wait for any shows or tracks to have ended then end show # probalby will get here with end_m set when player and shower has finished elif self.end_mediashow_signal == True: if self.player == None and self.shower == None: self.end_mediashow_signal = False self.end('normal', "show ended by user") #returning from a subshow needing to move onward elif self.req_next == 'do-next': self.req_next = 'nil' self.medialist.next(self.show_params['sequence']) self.play_selected_track(self.medialist.selected_track()) #returning from a subshow needing to move backward elif self.req_next == 'do-previous': self.req_next = 'nil' self.direction = 'backward' self.medialist.previous(self.show_params['sequence']) self.play_selected_track(self.medialist.selected_track()) # user wants to play child elif self.play_child_signal == True: self.play_child_signal = False index = self.medialist.index_of_track(self.child_track_ref) if index >= 0: #don't use select the track as need to preserve mediashow sequence. child_track = self.medialist.track(index) self.display_eggtimer(self.resource('mediashow', 'm07')) self.play_selected_track(child_track) else: self.mon.err( self, "Child show not found in medialist: " + self.show_params['pp-child-show']) self.end('error', "child show not found in medialist") # skip to next track on user input elif self.next_track_signal == True: self.next_track_signal = False if self.medialist.at_end() == True: if self.show_params[ 'sequence'] == "ordered" and self.show_params[ 'repeat'] == 'oneshot' and self.top == False: self.end('do-next', "Return from Sub Show") elif self.show_params[ 'sequence'] == "ordered" and self.show_params[ 'repeat'] == 'single-run' and self.top == False: self.end('do-next', "Return from Sub Show") else: self.medialist.next(self.show_params['sequence']) self.play_selected_track(self.medialist.selected_track()) else: self.medialist.next(self.show_params['sequence']) self.play_selected_track(self.medialist.selected_track()) # skip to previous track on user input elif self.previous_track_signal == True: self.previous_track_signal = False self.direction = 'backward' if self.medialist.at_start() == True: if self.show_params[ 'sequence'] == "ordered" and self.show_params[ 'repeat'] == 'oneshot' and self.top == False: self.end('do-previous', "Return from Sub Show") elif self.show_params[ 'sequence'] == "ordered" and self.show_params[ 'repeat'] == 'single-run' and self.top == False: self.end('do-previous', "Return from Sub Show") else: self.medialist.previous(self.show_params['sequence']) self.play_selected_track(self.medialist.selected_track()) else: self.medialist.previous(self.show_params['sequence']) self.play_selected_track(self.medialist.selected_track()) # track is finished and we are on auto elif self.show_params['progress'] == "auto": if self.medialist.at_end() == True: # oneshot if self.show_params[ 'sequence'] == "ordered" and self.show_params[ 'repeat'] == 'oneshot' and self.top == False: self.end('normal', "End of Oneshot in subshow") elif self.show_params[ 'sequence'] == "ordered" and self.show_params[ 'repeat'] == 'oneshot' and self.top == True: self.wait_for_trigger() # single run elif self.show_params[ 'sequence'] == "ordered" and self.show_params[ 'repeat'] == 'single-run' and self.top == True: self.end('normal', "End of Single Run") elif self.show_params[ 'sequence'] == "ordered" and self.show_params[ 'repeat'] == 'single-run' and self.top == False: self.end('do-next', "End of single run - Return from Sub Show") # repeating and waiting to restart elif self.waiting_for_interval == True: if self.interval_timer_signal == True: self.interval_timer_signal = False self.waiting_for_interval = False self.start_show() else: self.poll_for_interval_timer = self.canvas.after( 1000, self.what_next) elif self.show_params[ 'sequence'] == "ordered" and self.show_params[ 'repeat'] == 'interval' and int( self.show_params['repeat-interval']) > 0: self.waiting_for_interval = True self.poll_for_interval_timer = self.canvas.after( 1000, self.what_next) #elif self.show_params['sequence']=="ordered" and self.show_params['repeat']=='interval' and int(self.show_params['repeat-interval'])==0: elif self.show_params['repeat'] == 'interval' and int( self.show_params['repeat-interval']) == 0: self.medialist.next(self.show_params['sequence']) self.play_selected_track(self.medialist.selected_track()) # shuffling so there is no end condition elif self.show_params['sequence'] == "shuffle": self.medialist.next(self.show_params['sequence']) self.play_selected_track(self.medialist.selected_track()) else: self.mon.err( self, "Unhandled playing event: " + self.show_params['sequence'] + ' with ' + self.show_params['repeat'] + " of " + self.show_params['repeat-interval']) self.end('error', "Unhandled playing event") else: self.medialist.next(self.show_params['sequence']) self.play_selected_track(self.medialist.selected_track()) # track has finished and we are on manual progress elif self.show_params['progress'] == "manual": self.delete_eggtimer() self.canvas.delete('pp-content') if self.show_params['trigger-next'] == 'input': self.display_eggtimer(self.resource('mediashow', 'm03')) self.poll_for_continue_timer = self.canvas.after( 2000, self.what_next) else: #unhandled state self.mon.err(self, "Unhandled playing event: ") self.end('error', "Unhandled playing event") # *************************** # Dispatching to Players/Shows # *************************** def ready_callback(self): self.delete_eggtimer() def play_selected_track(self, selected_track): """ selects the appropriate player from type field of the medialist and computes the parameters for that type selected track is a dictionary for the track/show """ self.delete_eggtimer() if self.show_params['progress'] == "manual": self.display_eggtimer(self.resource('mediashow', 'm04')) # is menu required if self.show_params['has-child'] == "yes": self.enable_child = True else: self.enable_child = False #dispatch track by type self.player = None self.shower = None track_type = selected_track['type'] self.mon.log( self, self.show_params['show-ref'] + ' ' + str(self.show_id) + ": Track type is: " + track_type) if track_type == "video": # create a videoplayer track_file = self.complete_path(selected_track) self.player = VideoPlayer(self.show_id, self.root, self.canvas, self.show_params, selected_track, self.pp_dir, self.pp_home, self.pp_profile) self.player.play(track_file, self.showlist, self.end_player, self.ready_callback, enable_menu=self.enable_child) elif track_type == "audio": # create a audioplayer track_file = self.complete_path(selected_track) self.player = AudioPlayer(self.show_id, self.root, self.canvas, self.show_params, selected_track, self.pp_dir, self.pp_home, self.pp_profile) self.player.play(track_file, self.showlist, self.end_player, self.ready_callback, enable_menu=self.enable_child) elif track_type == "web": # create a browser track_file = self.complete_path(selected_track) self.player = BrowserPlayer(self.show_id, self.root, self.canvas, self.show_params, selected_track, self.pp_dir, self.pp_home, self.pp_profile) self.player.play(track_file, self.showlist, self.end_player, self.ready_callback, enable_menu=self.enable_child) elif track_type == "image": track_file = self.complete_path(selected_track) # images played from menus don't have children self.player = ImagePlayer(self.show_id, self.root, self.canvas, self.show_params, selected_track, self.pp_dir, self.pp_home, self.pp_profile) self.player.play(track_file, self.showlist, self.end_player, self.ready_callback, enable_menu=self.enable_child) elif track_type == "message": # bit odd because MessagePlayer is used internally to display text. text = selected_track['text'] self.player = MessagePlayer(self.show_id, self.root, self.canvas, self.show_params, selected_track, self.pp_dir, self.pp_home, self.pp_profile) self.player.play(text, self.showlist, self.end_player, self.ready_callback, enable_menu=self.enable_child) elif track_type == "show": # get the show from the showlist index = self.showlist.index_of_show(selected_track['sub-show']) if index >= 0: self.showlist.select(index) selected_show = self.showlist.selected_show() else: self.mon.err( self, "Show not found in showlist: " + selected_track['sub-show']) self.end('error', "Unknown show") if selected_show['type'] == "mediashow": self.shower = MediaShow(selected_show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id, self.end_shower, self.ready_callback, top=False, command=self.direction) elif selected_show['type'] == "liveshow": self.shower = LiveShow(selected_show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id, self.end_shower, self.ready_callback, top=False, command='nil') elif selected_show['type'] == "radiobuttonshow": self.shower = RadioButtonShow(selected_show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id, self.end_shower, self.ready_callback, top=False, command='nil') elif selected_show['type'] == "hyperlinkshow": self.shower = HyperlinkShow(selected_show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id, self.end_shower, self.ready_callback, top=False, command='nil') elif selected_show['type'] == "menu": self.shower = MenuShow(selected_show, self.root, self.canvas, self.showlist, self.pp_dir, self.pp_home, self.pp_profile) self.shower.play(self.show_id, self.end_shower, self.ready_callback, top=False, command='nil') else: self.mon.err(self, "Unknown Show Type: " + selected_show['type']) self.end('error' "Unknown show type") else: self.mon.err(self, "Unknown Track Type: " + track_type) self.end('error', "Unknown track type") def end_player(self, reason, message): self.mon.log( self, self.show_params['show-ref'] + ' ' + str(self.show_id) + ": Returned from player with message: " + message) self.player = None self.req_next = 'nil' if reason in ("killed", "error"): self.end(reason, message) else: # elif>else move to what-next? if self.show_params['progress'] == "manual": self.display_eggtimer(self.resource('mediashow', 'm05')) self.req_next = reason self.what_next() else: self.req_next = reason self.what_next() def end_shower(self, show_id, reason, message): self.mon.log( self, self.show_params['show-ref'] + ' ' + str(self.show_id) + ": Returned from shower with message: " + message) self.shower = None self.req_next = 'nil' if reason in ("killed", "error"): self.end(reason, message) else: if self.show_params['progress'] == "manual": self.display_eggtimer(self.resource('mediashow', 'm06')) self.req_next = reason self.what_next() else: self.req_next = reason self.what_next() # *************************** # end of show # *************************** def end(self, reason, message): self.end_mediashow_signal = False self.mon.log( self, self.show_params['show-ref'] + ' ' + str(self.show_id) + ": Ending Mediashow") self.tidy_up() self.end_callback(self.show_id, reason, message) self = None return def tidy_up(self): #clear outstanding time of day events for this show # self.tod.clear_times_list(id(self)) if self.poll_for_continue_timer <> None: self.canvas.after_cancel(self.poll_for_continue_timer) self.poll_for_continue_timer = None if self.poll_for_interval_timer <> None: self.canvas.after_cancel(self.poll_for_interval_timer) self.poll_for_interval_timer = None if self.interval_timer <> None: self.canvas.after_cancel(self.interval_timer) self.interval_timer = None if self.duration_timer <> None: self.canvas.after_cancel(self.duration_timer) self.duration_timer = None # *************************** # displaying things # *************************** def display_eggtimer(self, text): self.canvas.create_text(int(self.canvas['width']) / 2, int(self.canvas['height']) / 2, text=text, fill='white', font="Helvetica 20 bold", tag='pp-eggtimer') self.canvas.update_idletasks() def delete_eggtimer(self): self.canvas.delete('pp-eggtimer') self.canvas.update_idletasks() # used to display internal messages in situations where a medialist entry could not be used. def display_message(self, canvas, source, content, duration, _display_message_callback): self.display_message_callback = _display_message_callback tp = { 'duration': duration, 'message-colour': 'white', 'message-font': 'Helvetica 20 bold', 'background-colour': '', 'message-justify': 'left', 'background-image': '', 'show-control-begin': '', 'show-control-end': '', 'animate-begin': '', 'animate-clear': '', 'animate-end': '', 'message-x': '', 'message-y': '', 'display-show-background': 'no', 'display-show-text': 'no', 'show-text': '', 'track-text': '', 'plugin': '' } self.player = MessagePlayer(self.show_id, self.root, canvas, tp, tp, self.pp_dir, self.pp_home, self.pp_profile) self.player.play(content, self.showlist, self.display_message_end, None, False) def display_message_end(self, reason, message): self.player = None if reason in ('error', 'killed'): self.end(reason, message) else: self.display_message_callback() # *************************** # utilities # *************************** def calculate_duration(self, line): fields = line.split(':') if len(fields) == 1: secs = fields[0] minutes = '0' hours = '0' if len(fields) == 2: secs = fields[1] minutes = fields[0] hours = '0' if len(fields) == 3: secs = fields[2] minutes = fields[1] hours = fields[0] self.duration = 3600 * long(hours) + 60 * long(minutes) + long(secs) return '' def resource(self, section, item): value = self.rr.get(section, item) if value == False: self.mon.err(self, "resource: " + section + ': ' + item + " not found") self.terminate("error") else: return value def complete_path(self, selected_track): # complete path of the filename of the selected entry track_file = selected_track['location'] if track_file <> '' and track_file[0] == "+": track_file = self.pp_home + track_file[1:] self.mon.log( self, self.show_params['show-ref'] + ' ' + str(self.show_id) + ": Track to play is: " + track_file) return track_file
class Show(object): # ****************************** # init a show # ****************************** def base__init__(self, show_id, show_params, root, canvas, showlist, pp_dir, pp_home, pp_profile, command_callback): # instantiate arguments self.show_id = show_id self.show_params = show_params self.root = root self.show_canvas = canvas self.canvas = canvas['canvas-obj'] self.show_canvas_x1 = canvas['show-canvas-x1'] self.show_canvas_y1 = canvas['show-canvas-y1'] self.show_canvas_x2 = canvas['show-canvas-x2'] self.show_canvas_y2 = canvas['show-canvas-y2'] self.show_canvas_width = canvas['show-canvas-width'] self.show_canvas_height = canvas['show-canvas-height'] self.show_canvas_centre_x = canvas['show-canvas-centre-x'] self.show_canvas_centre_y = canvas['show-canvas-centre-y'] self.showlist = showlist self.pp_dir = pp_dir self.pp_home = pp_home self.pp_profile = pp_profile self.command_callback = command_callback # init things that will then be reinitialised by derived classes self.medialist = None # set up logging self.mon = Monitor() self.mon.set_log_level(16) # create and instance of TimeOfDay scheduler so we can add events self.tod = TimeOfDay() # create an instance of showmanager so we can init child/subshows self.show_manager = ShowManager(self.show_id, self.showlist, self.show_params, self.root, self.show_canvas, self.pp_dir, self.pp_profile, self.pp_home) # init variables self.current_player = None self.previous_player = None self.shower = None self.previous_shower = None self.user_stop_signal = False self.exit_signal = False self.terminate_signal = False self.show_timeout_signal = False self.egg_timer = None self.admin_message = None self.ending_reason = '' self.background_obj = None self.background_file = '' self.level = 0 self.subshow_kickback_signal = False self.kickback_for_next_track = False # get background image from profile. # print 'background', self.show_params['background-image'] if self.show_params['background-image'] != '': self.background_file = self.show_params['background-image'] def base_play(self, end_callback, show_ready_callback, parent_kickback_signal, level, controls_list): """ starts the common parts of the show end_callback - function to be called when the show exits- callback gets last player of subshow show_ready_callback - callback at start to get previous_player top is True when the show is top level (run from [start] or by show command from another show) direction_command - 'forward' or 'backward' direction to play a subshow """ # instantiate the arguments self.end_callback = end_callback self.show_ready_callback = show_ready_callback self.parent_kickback_signal = parent_kickback_signal self.level = level # not needed as controls list is not passed down to subshows. # self.controls_list=controls_list self.mon.trace( self, self.show_params['show-ref'] + ' at level ' + str(self.level)) self.mon.log( self, self.show_params['show-ref'] + ' ' + str(self.show_id) + ": Starting show") # check data files are available. if self.show_params['medialist'] == '': self.mon.err(self, "Blank Medialist in: " + self.show_params['title']) self.end('error', "Blank Medialist in: " + self.show_params['title']) self.medialst_file = self.pp_profile + "/" + self.show_params[ 'medialist'] if not os.path.exists(self.medialst_file): self.mon.err(self, "Medialist file not found: " + self.medialst_file) self.end('error', "Medialist file not found: " + self.medialst_file) # read the medialist for the show if self.medialist.open_list(self.medialst_file, self.showlist.sissue()) is False: self.mon.err(self, "Version of medialist different to Pi Presents") self.end('error', "Version of medialist different to Pi Presents") if self.show_ready_callback is not None: # get the previous player from calling show its stored in current because its going to be shuffled before use self.previous_shower, self.current_player = self.show_ready_callback( ) self.mon.trace( self, ' - previous shower and player is ' + self.mon.pretty_inst(self.previous_shower) + ' ' + self.mon.pretty_inst(self.current_player)) #load the show background reason, message = Show.base_load_show_background(self) if reason == 'error': self.mon.err(self, message) self.end('error', message) # dummy, must be overidden by derived class def subshow_ready_callback(self): self.mon.err(self, "subshow_ready_callback not overidden") # set what to do when closed or unloaded self.ending_reason = 'killed' Show.base_close_or_unload(self) def base_subshow_ready_callback(self): # callback from begining of a subshow, provide previous player to called show # used by show_ready_callback of called show # in the case of a menushow last track is always the menu self.mon.trace( self, ' - sends ' + self.mon.pretty_inst(self.previous_player)) return self, self.previous_player def base_shuffle(self): self.previous_player = self.current_player self.current_player = None self.mon.trace( self, ' - LOOP STARTS WITH current is: ' + self.mon.pretty_inst(self.current_player)) self.mon.trace( self, ' - previous is: ' + self.mon.pretty_inst(self.previous_player)) def base_load_track_or_show(self, selected_track, loaded_callback, end_shower_callback, enable_menu): track_type = selected_track['type'] if track_type == "show": # get the show from the showlist index = self.showlist.index_of_show(selected_track['sub-show']) if index < 0: self.mon.err( self, "Show not found in showlist: " + selected_track['sub-show']) self.end('error', 'show not in showlist: ' + selected_track['sub-show']) else: self.showlist.select(index) selected_show = self.showlist.selected_show() self.shower = self.show_manager.init_subshow( self.show_id, selected_show, self.show_canvas) self.mon.trace( self, ' - show is: ' + self.mon.pretty_inst(self.shower) + ' ' + selected_show['show-ref']) if self.shower is None: self.mon.err(self, "Unknown Show Type: " + selected_show['type']) self.terminate_signal = True self.what_next_after_showing() else: # empty controls list as not used, pleases landscape # print 'send direction to subshow from show',self.kickback_for_next_track # Show.base_withdraw_show_background(self) self.shower.play(end_shower_callback, self.subshow_ready_callback, self.kickback_for_next_track, self.level + 1, []) else: # dispatch track by type self.mon.log( self, self.show_params['show-ref'] + ' ' + str(self.show_id) + ": Track type is: " + track_type) self.current_player = self.base_init_selected_player( selected_track) #menu has no track file if selected_track['type'] == 'menu': track_file = '' # messageplayer passes the text not a file name elif selected_track['type'] == 'message': track_file = selected_track['text'] else: track_file = self.base_complete_path( selected_track['location']) self.mon.trace(self, ' - track is: ' + track_file) self.mon.trace( self, ' - current_player is: ' + self.mon.pretty_inst(self.current_player)) self.current_player.load(track_file, loaded_callback, enable_menu=enable_menu) # DUMMY, must be overidden by derived class def what_next_after_showing(self): self.mon.err(self, "what_next_after showing not overidden") # set what to do when closed or unloaded self.ending_reason = 'killed' Show.base_close_or_unload(self) def base_init_selected_player(self, selected_track): # dispatch track by type track_type = selected_track['type'] self.mon.log(self, "Track type is: " + track_type) if track_type == "image": return ImagePlayer(self.show_id, self.showlist, self.root, self.show_canvas, self.show_params, selected_track, self.pp_dir, self.pp_home, self.pp_profile, self.end, self.command_callback) elif track_type == "video": return VideoPlayer(self.show_id, self.showlist, self.root, self.show_canvas, self.show_params, selected_track, self.pp_dir, self.pp_home, self.pp_profile, self.end, self.command_callback) elif track_type == "audio": return AudioPlayer(self.show_id, self.showlist, self.root, self.show_canvas, self.show_params, selected_track, self.pp_dir, self.pp_home, self.pp_profile, self.end, self.command_callback) elif track_type == "web" and self.show_params['type'] not in ( 'artmediashow', 'artliveshow'): return BrowserPlayer(self.show_id, self.showlist, self.root, self.show_canvas, self.show_params, selected_track, self.pp_dir, self.pp_home, self.pp_profile, self.end, self.command_callback) elif track_type == "message": return MessagePlayer(self.show_id, self.showlist, self.root, self.show_canvas, self.show_params, selected_track, self.pp_dir, self.pp_home, self.pp_profile, self.end, self.command_callback) elif track_type == "menu": return MenuPlayer(self.show_id, self.showlist, self.root, self.show_canvas, self.show_params, selected_track, self.pp_dir, self.pp_home, self.pp_profile, self.end, self.command_callback) else: return None # DUMMY, must be overidden by derived class def track_ready_callback(self, track_background): self.mon.err(self, "track_ready_callback not overidden") # set what to do when closed or unloaded self.ending_reason = 'killed' Show.base_close_or_unload(self) # called just before a track is shown to remove the previous track from the screen # and if necessary close it def base_track_ready_callback(self, enable_show_background): self.mon.trace(self, '') # show the show background done for every track but quick operation if enable_show_background is True: self.base_show_show_background() else: self.base_withdraw_show_background() # !!!!!!!!! withdraw the background from the parent show if self.previous_shower != None: self.previous_shower.base_withdraw_show_background() # close the player from the previous track if self.previous_player is not None: self.mon.trace( self, ' - hiding previous: ' + self.mon.pretty_inst(self.previous_player)) self.previous_player.hide() # print 'Not None - previous state is',self.previous_player.get_play_state() if self.previous_player.get_play_state() == 'showing': # print 'showing so closing previous' # showing or frozen self.mon.trace( self, ' - closing previous: ' + self.mon.pretty_inst(self.previous_player)) self.previous_player.close(self._base_closed_callback_previous) else: self.mon.trace(self, ' - previous is none\n') self.previous_player = None self.canvas.update_idletasks() def _base_closed_callback_previous(self, status, message): self.mon.trace( self, ' - previous is None - was: ' + self.mon.pretty_inst(self.previous_player)) self.previous_player = None # used by end_shower to get the last track of the subshow def base_end_shower(self): self.mon.trace(self, ' - returned back to level: ' + str(self.level)) # get the previous subshow and last track it played self.previous_shower, self.current_player = self.shower.base_subshow_ended_callback( ) if self.previous_shower != None: self.subshow_kickback_signal = self.shower.subshow_kickback_signal # print 'get subshow kickback from subshow',self.subshow_kickback_signal self.previous_shower.base_withdraw_show_background() self.base_show_show_background() self.previous_player = None self.mon.trace( self, '- get previous_player from subshow: ' + self.mon.pretty_inst(self.current_player)) self.shower = None # close or unload the current player when ending the show def base_close_or_unload(self): self.mon.trace(self, self.mon.pretty_inst(self.current_player)) # need to test for None because player may be made None by subshow lower down the stack for terminate if self.current_player is not None: self.mon.trace(self, self.current_player.get_play_state()) if self.current_player.get_play_state() in ('loaded', 'showing', 'show-failed'): if self.current_player.get_play_state() == 'loaded': self.mon.trace( self, ' - unloading current from: ' + self.ending_reason) self.current_player.unload() else: self.mon.trace( self, ' - closing current from: ' + self.ending_reason) self.current_player.close(None) self._wait_for_end() else: # current_player is None because closed further down show stack self.mon.trace( self, ' - show ended with current_player=None because: ' + self.ending_reason) # if exiting pipresents then need to close previous show else get memotry leak # if not exiting pipresents the keep previous so it can be closed when showing the next track # print 'CURRENT PLAYER IS NONE' ,self.ending_reason if self.ending_reason == 'killed': self.base_close_previous() elif self.ending_reason == 'error': self.base_close_previous() elif self.ending_reason == 'exit': self.end('normal', "show quit by exit command") elif self.ending_reason == 'user-stop': self.end('normal', "show quit by stop operation") else: self.mon.fatal( self, "Unhandled ending_reason: " + self.ending_reason) self.end('error', "Unhandled ending_reason: " + self.ending_reason) def _base_closed_callback_current(self, status, message): self.mon.trace( self, ' current is None - was: ' + self.mon.pretty_inst(self.current_player)) # wait for unloading or closing to complete then end def _wait_for_end(self): self.mon.trace(self, self.mon.pretty_inst(self.current_player)) if self.current_player is not None: self.mon.trace( self, ' - play state is ' + self.current_player.get_play_state()) if self.current_player.play_state not in ('unloaded', 'closed', 'load-failed'): #### self.canvas.after(50, self._wait_for_end) else: self.mon.trace( self, ' - current closed ' + self.mon.pretty_inst(self.current_player) + ' ' + self.ending_reason) #why is some of thsi different to close and unload????????????? perhaps because current_player isn't none, just closed if self.ending_reason == 'killed': self.current_player.hide() self.current_player = None self.base_close_previous() elif self.ending_reason == 'error': self.current_player.hide() self.current_player = None self.base_close_previous() elif self.ending_reason == 'exit': self.current_player.hide() self.current_player = None self.base_close_previous() elif self.ending_reason == 'change-medialist': self.current_player.hide() self.current_player = None # self.base_close_previous() # go to start of list via wait for trigger. self.wait_for_trigger() elif self.ending_reason == 'show-timeout': self.current_player.hide() self.current_player = None self.end('normal', "show timeout") elif self.ending_reason == 'user-stop': if self.level != 0: self.end('normal', "show quit by stop operation") else: self.current_player.hide() self.current_player = None self.base_close_previous() else: self.mon.fatal( self, "Unhandled ending_reason: " + self.ending_reason) self.end('error', "Unhandled ending_reason: " + self.ending_reason) else: self.mon.trace( self, ' - current is None ' + self.mon.pretty_inst(self.current_player) + ' ' + self.ending_reason) # *************************** # end of show # *************************** # dummy, normally overidden by derived class def end(self, reason, message): self.mon.err(self, "end not overidden") self.base_end('error', message) def base_end(self, reason, message): self.base_withdraw_show_background() self.base_delete_show_background() self.mon.trace( self, ' at level ' + str(self.level) + '\n - Current is ' + self.mon.pretty_inst(self.current_player) + '\n - Previous is ' + self.mon.pretty_inst(self.previous_player) + '\n with reason' + reason + '\n\n') self.mon.log( self, self.show_params['show-ref'] + ' Show Id: ' + str(self.show_id) + ": Ending Show") self.end_callback(self.show_id, reason, message) self = None def base_subshow_ended_callback(self): # called by end_shower of a parent show to get the last track of the subshow and the subshow self.mon.trace( self, ' - returns ' + self.mon.pretty_inst(self.current_player)) return self, self.current_player # ******************************** # Respond to external events # ******************************** def base_close_previous(self): self.mon.trace(self, '') # close the player from the previous track if self.previous_player is not None: self.mon.trace( self, ' - previous not None ' + self.mon.pretty_inst(self.previous_player)) if self.previous_player.get_play_state() == 'showing': # showing or frozen self.mon.trace( self, ' - closing previous ' + self.mon.pretty_inst(self.previous_player)) self.previous_player.close(self._base_close_previous_callback) else: self.mon.trace(self, 'previous is not showing') self.previous_player.hide() self.previous_player = None self.end(self.ending_reason, '') else: self.mon.trace(self, ' - previous is None') self.end(self.ending_reason, '') def _base_close_previous_callback(self, status, message): self.mon.trace( self, ' - previous is None - was ' + self.mon.pretty_inst(self.previous_player)) self.previous_player.hide() self.previous_player = None self.end(self.ending_reason, '') # exit received from external source def base_exit(self): self.mon.log( self, self.show_params['show-ref'] + ' ' + str(self.show_id) + ": Exit received") self.mon.trace(self, '') # set signal to exit the show when all sub-shows and players have ended self.exit_signal = True # then stop subshow or tracks. if self.shower is not None: self.shower.exit() elif self.current_player is not None: self.current_player.input_pressed('stop') else: self.end('normal', 'exit by ShowManager') # show timeout callback received def base_show_timeout_stop(self): self.mon.trace(self, '') # set signal to exit the show when all sub-shows and players have ended self.show_timeout_signal = True # then stop and shows or tracks. if self.shower is not None: self.shower.show_timeout_stop() elif self.current_player is not None: self.current_player.input_pressed('stop') else: self.end('normal', 'stopped by Show Timeout') # dummy, normally overidden by derived class def terminate(self): self.mon.err(self, "terminate not overidden") self.base_end('error', "terminate not overidden") # terminate Pi Presents def base_terminate(self): self.mon.trace(self, '') # set signal to stop the show when all sub-shows and players have ended self.terminate_signal = True if self.shower is not None: self.shower.terminate() elif self.current_player is not None: self.ending_reason = 'killed' Show.base_close_or_unload(self) else: self.end('killed', ' terminated with no shower or player to terminate') # respond to input events def base_handle_input_event(self, symbol): self.mon.log( self, self.show_params['show-ref'] + ' Show Id: ' + str(self.show_id) + ": received input event: " + symbol) if self.shower is not None: self.shower.handle_input_event(symbol) else: self.handle_input_event_this_show(symbol) #dummy must be overridden in derived class def handle_input_event_this_show(self, symbol): self.mon.err(self, "input_pressed_this_show not overidden") self.ending_reason = 'killed' Show.base_close_or_unload(self) def base_load_show_background(self): # load show background image if self.background_file != '': background_img_file = self.base_complete_path(self.background_file) if not os.path.exists(background_img_file): return 'error', "Show background file not found " + background_img_file else: pil_background_img = Image.open(background_img_file) # print 'pil_background_img ',pil_background_img image_width, image_height = pil_background_img.size window_width = self.show_canvas_width window_height = self.show_canvas_height if image_width != window_width or image_height != window_height: pil_background_img = pil_background_img.resize( (window_width, window_height)) self.background = ImageTk.PhotoImage(pil_background_img) del pil_background_img # print 'self.background ',self.background self.background_obj = self.canvas.create_image( self.show_canvas_x1, self.show_canvas_y1, image=self.background, anchor=NW) self.canvas.itemconfig(self.background_obj, state='hidden') self.canvas.update_idletasks() # print '\nloaded background_obj: ',self.background_obj return 'normal', 'show background loaded' else: return 'normal', 'no backgound to load' def base_show_show_background(self): if self.background_obj is not None: # print 'show show background' self.canvas.itemconfig(self.background_obj, state='normal') # self.canvas.update_idletasks( ) def base_withdraw_show_background(self): self.mon.trace(self, '') if self.background_obj is not None: # print 'withdraw background obj', self.background_obj self.canvas.itemconfig(self.background_obj, state='hidden') # self.canvas.update_idletasks( ) def base_delete_show_background(self): if self.background_obj is not None: # print 'delete background obj' self.canvas.delete(self.background_obj) self.background = None # self.canvas.update_idletasks( ) # ****************************** # write statiscics # ********************************* def write_stats(self, command, show_params, next_track): # action, this ref, this name, type, ref, name, location if next_track['type'] == 'show': # get the show from the showlist index = self.showlist.index_of_show(next_track['sub-show']) if index < 0: self.mon.err( self, "Show not found in showlist: " + next_track['sub-show']) self.end('error', 'show not in showlist: ' + next_track['sub-show']) else: target = self.showlist.show(index) ref = target['show-ref'] title = target['title'] track_type = target['type'] else: # its a track ref = next_track['track-ref'] title = next_track['title'] track_type = next_track['type'] if next_track['type'] in ('show', 'message'): loc = '' else: loc = next_track['location'] self.mon.stats(show_params['type'], show_params['show-ref'], show_params['title'], command, track_type, ref, title, loc) # ****************************** # lookup controls # ********************************* def base_lookup_control(self, symbol, controls_list): for control in controls_list: if symbol == control[0]: return control[1] # not found so must be a trigger return '' # ****************************** # Eggtimer # ********************************* def display_eggtimer(self): text = self.show_params['eggtimer-text'] if text != '': self.egg_timer = self.canvas.create_text( int(self.show_params['eggtimer-x']) + self.show_canvas_x1, int(self.show_params['eggtimer-y']) + self.show_canvas_y1, text=text, fill=self.show_params['eggtimer-colour'], font=self.show_params['eggtimer-font'], anchor='nw') self.canvas.update_idletasks() def delete_eggtimer(self): if self.egg_timer is not None: self.canvas.delete(self.egg_timer) self.egg_timer = None self.canvas.update_idletasks() # ****************************** # Display Admin Messages # ********************************* def display_admin_message(self, text): self.admin_message = self.canvas.create_text( int(self.show_params['admin-x']) + self.show_canvas_x1, int(self.show_params['admin-y']) + self.show_canvas_y1, text=text, fill=self.show_params['admin-colour'], font=self.show_params['admin-font'], anchor='nw') self.canvas.update_idletasks() def delete_admin_message(self): if self.admin_message is not None: self.canvas.delete(self.admin_message) self.canvas.update_idletasks() # ****************************** # utilities # ****************************** def base_complete_path(self, track_file): # complete path of the filename of the selected entry if track_file != '' and track_file[0] == "+": track_file = self.pp_home + track_file[1:] self.mon.log(self, "Track to load is: " + track_file) return track_file def calculate_duration(self, line): fields = line.split(':') if len(fields) == 1: secs = fields[0] minutes = '0' hours = '0' if len(fields) == 2: secs = fields[1] minutes = fields[0] hours = '0' if len(fields) == 3: secs = fields[2] minutes = fields[1] hours = fields[0] if not secs.isdigit() or not minutes.isdigit() or not hours.isdigit(): return 'error', 'bad time: ' + line, 0 else: return 'normal', '', 3600 * long(hours) + 60 * long( minutes) + long(secs)
class Animate(object): """ allows players to put events, which request the change of state of pins, into a queue. Events are executed at the required time. using the interface to an output driver. """ # constants for sequencer events list name = 0 # GPIO pin number, the xx in P1-xx param_type = 1 param_values = 2 # off , on time = 3 # time since the epoch in seconds tag = 4 # tag used to delete all matching events, usually a track reference. event_template = ['', '', '', 0, None] # CLASS VARIABLES (Animate.) events = [] last_poll_time = 0 # executed by main program and by each object using animate def __init__(self): self.mon = Monitor() # executed once from main program def init(self, pp_dir, pp_home, pp_profile, widget, sequencer_tick, event_callback): # instantiate arguments self.widget = widget #something to hang 'after' on self.pp_dir = pp_dir self.pp_profile = pp_profile self.pp_home = pp_home self.sequencer_tick = sequencer_tick self.event_callback = event_callback # Initialise time used by sequencer Animate.sequencer_time = long(time.time()) # init timer self.sequencer_tick_timer = None # called by main program only def terminate(self): if self.sequencer_tick_timer is not None: self.widget.after_cancel(self.sequencer_tick_timer) self.clear_events_list(None) # ************************************************ # output sequencer # ************************************************ # called by main program only def poll(self): poll_time = long(time.time()) # is current time greater than last time the scheduler was run (previous second or more) # run in a loop to catch up because root.after can get behind when images are being rendered etc. while Animate.sequencer_time <= poll_time: # kick off output pin sequencer self.do_sequencer() Animate.sequencer_time += 1 # and loop the polling self.sequencer_tick_timer = self.widget.after(self.sequencer_tick, self.poll) # execute events at the appropriate time and remove from list (runs from main program only) # runs through list a number of times because of problems with pop messing up list def do_sequencer(self): # print 'sequencer run for: ' + str(sequencer_time) + ' at ' + str(long(time.time())) while True: event_found = False for index, item in enumerate(Animate.events): if item[Animate.time] <= Animate.sequencer_time: event = Animate.events.pop(index) event_found = True self.send_event(event[Animate.name], event[Animate.param_type], event[Animate.param_values], item[Animate.time]) break if event_found is False: break def send_event(self, name, param_type, param_values, req_time): self.mon.log( self, 'send event ' + name + ' ' + param_type + ' ' + ' '.join(param_values)) self.event_callback(name, param_type, param_values, req_time) # ************************************************ # output sequencer interface methods # these can be called from many classes so need to operate on class variables # ************************************************ def animate(self, text, tag): lines = text.split("\n") for line in lines: reason, message, delay, name, param_type, param_values = self.parse_animate_fields( line) if reason == 'error': return 'error', message if name != '': self.add_event(name, param_type, param_values, delay, tag) return 'normal', 'events processed' def add_event(self, name, param_type, param_values, delay, tag): poll_time = long(time.time()) # prepare the event event = Animate.event_template event[Animate.name] = name event[Animate.param_type] = param_type event[Animate.param_values] = param_values event[Animate.time] = delay + poll_time #+1? event[Animate.tag] = tag # print '\nadd event ',event # find the place in the events list and insert # first item in the list is earliest, if two have the same time then last to be added is fired last. # events are fired from top of list abs_time = poll_time + delay # print 'new event',abs_time copy_event = copy.deepcopy(event) length = len(Animate.events) if length == 0: Animate.events.append(copy_event) # print 'append to empty ist',abs_time return copy_event else: index = length - 1 if abs_time > Animate.events[index][Animate.time]: Animate.events.append(copy_event) # print 'append to end of list if greater than last item',abs_time return copy_event while index != -1: if abs_time == Animate.events[index][Animate.time]: Animate.events.insert(index + 1, copy_event) # print 'insert after if equal',abs_time return copy_event if abs_time > Animate.events[index][Animate.time]: Animate.events.insert(index + 1, copy_event) # print 'insert after if later',abs_time return copy_event if index == 0: Animate.events.insert(index, copy_event) # print 'insert before if at start of list',abs_time return copy_event index -= 1 # print 'error at start of list',abs_time def print_events(self): print 'events list' for event in Animate.events: print 'EVENT: ', event # remove all the events with the same tag, usually a track reference def remove_events(self, tag): left = [] for item in Animate.events: if tag != item[Animate.tag]: left.append(item) Animate.events = left # self.print_events() # clear event list def clear_events_list(self, tag): self.mon.log(self, 'clear events list ') # empty event list Animate.events = [] # [delay],symbol,type,values(one or more) def parse_animate_fields(self, line): if line == '': return 'normal', 'no fields', '', '', [], 0 # split the line using "" for text with spaces for l in csv.reader([line], delimiter=' ', skipinitialspace=True, quotechar='"'): fields = l if len(fields) == 0: return 'normal', 'no fields', '', '', [], 0 elif len(fields) < 3: return 'error', 'too few fields in : ' + line, '', '', [], 0 delay_text = fields[0] # check each field if not delay_text.isdigit(): return 'error', 'Delay is not an integer in : ' + line, '', '', [], 0 else: delay = int(delay_text) name = fields[1] if len(fields) == 2: param_type = '' params = [] else: param_type = fields[2] params = [] for index in range(3, len(fields)): param = fields[index] params.append(param) # print 'event parsed OK',delay,name,param_type,params return 'normal', 'event parsed OK', delay, name, param_type, params
class TimeOfDay(object): # CLASS VARIABLES # change this for another language DAYS_OF_WEEK = [ 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' ] """ TimeOfDay.events is a dictionary the keys being show-refs. Each dictionary entry is a list of time_elements sorted by descending time Each time element is a list with the fields: 0 - command 1 - time, seconds since midnight """ events = { } # list of times of day used to generate callbacks, earliest first # executed by main program and by each object using tod def __init__(self): self.mon = Monitor() # executed once from main program only def init(self, pp_dir, pp_home, pp_profile, showlist, root, callback): # instantiate arguments TimeOfDay.root = root self.pp_dir = pp_dir self.pp_home = pp_home self.pp_profile = pp_profile self.showlist = showlist self.callback = callback # init variables self.testing = False self.tod_tick = 500 self.tick_timer = None # set time of day for if no schedule TimeOfDay.now = datetime.now().replace(microsecond=0) # read and error check the schedule reason, message, schedule_enabled = self.read_schedule() if reason == 'error': return 'error', message, False if schedule_enabled is False: return 'normal', '', False #create the initial events list if self.simulate_time is True: year = int(self.sim_year) month = int(self.sim_month) day = int(self.sim_day) hour = int(self.sim_hour) minute = int(self.sim_minute) second = int(self.sim_second) TimeOfDay.now = datetime(day=day, month=month, year=year, hour=hour, minute=minute, second=second) self.testing = True print '\nInitial SIMULATED time', TimeOfDay.now.ctime() self.mon.sched( self, TimeOfDay.now, 'Testing is ON, Initial SIMULATED time ' + str(TimeOfDay.now.ctime())) else: #get the current date/time only this once TimeOfDay.now = datetime.now().replace(microsecond=0) self.mon.sched( self, TimeOfDay.now, 'Testing is OFF, Initial REAL time ' + str(TimeOfDay.now.ctime())) # print '\nInitial REAL time',TimeOfDay.now.ctime() self.testing = False # print 'init',TimeOfDay.now TimeOfDay.last_now = TimeOfDay.now - timedelta(seconds=1) reason, message = self.build_schedule_for_today() if reason == 'error': return 'error', message, False self.mon.sched(self, TimeOfDay.now, self.pretty_todays_schedule()) if self.testing: self.print_todays_schedule() self.build_events_lists() if self.testing: self.print_events_lists() # and do exitpipresents or start any show that should be running at start up time self.do_catchup() return 'normal', '', True def do_catchup(self): TimeOfDay.scheduler_time = TimeOfDay.now.time() # do the catchup for each real show in turn # nothing required for start show, all previous events are just ignored. for show_ref in TimeOfDay.events: if show_ref != 'start' and self.enable_catchup[show_ref] == 'yes': if self.testing: print 'Catch Up', show_ref times = TimeOfDay.events[show_ref] # go through the event list for a show rembering show state until the first future event is found. # then if last command was to start the show send it show_running = False last_start_element = [] for time_element in reversed(times): # print 'now', now_seconds, 'time from events', time_element[1], time_element[0] # got past current time can give up catch up if time_element[1] >= TimeOfDay.scheduler_time: # print ' gone past time - break' break if time_element[0] == 'open': last_start_element = time_element # print 'open - show-running= true' show_running = True elif time_element[0] == 'close': # print 'close - show-running= false' show_running = False if show_running is True: #if self.testing: # print 'End of Catch Up Search', show_ref,last_start_element self.mon.sched( self, TimeOfDay.now, 'Catch up for show: ' + show_ref + ' requires ' + last_start_element[0] + ' ' + str(last_start_element[1])) self.do_event(show_ref, last_start_element) return 'not exiting' # called by main program only def poll(self): if self.testing: poll_time = TimeOfDay.now else: poll_time = datetime.now() # print 'poll time: ',poll_time.time(),'scheduler time: ',TimeOfDay.now.time() # if poll_time != TimeOfDay.now : print 'times different ',poll_time.time(),TimeOfDay.now.time() # is current time greater than last time the scheduler was run # run in a loop to catch up because root.after can get behind when images are being rendered etc. # poll time can be the same twice as poll is run at half second intervals. catchup_time = 0 while TimeOfDay.now <= poll_time: if TimeOfDay.now - TimeOfDay.last_now != timedelta(seconds=1): print 'POLL TIME FAILED', TimeOfDay.last_now, TimeOfDay.now #if catchup_time != 0: # print 'scheduler behind by: ',catchup_time, TimeOfDay.now.time(),poll_time.time() self.do_scheduler() TimeOfDay.last_now = TimeOfDay.now catchup_time += 1 # print 'poll',TimeOfDay.now, timedelta(seconds=1) TimeOfDay.now = TimeOfDay.now + timedelta(seconds=1) # and loop if self.testing: self.tick_timer = TimeOfDay.root.after(1000, self.poll) else: self.tick_timer = TimeOfDay.root.after(self.tod_tick, self.poll) # called by main program only def terminate(self): if self.tick_timer is not None: TimeOfDay.root.after_cancel(self.tick_timer) self.clear_events_lists() # execute events at the appropriate time. # called by main program only def do_scheduler(self): # if its midnight then build the events lists for the new day TimeOfDay.scheduler_time = TimeOfDay.now.time() if TimeOfDay.scheduler_time == time(hour=0, minute=0, second=0): # if self.testing: # print 'Its midnight, today is now', TimeOfDay.now.ctime() self.mon.sched( self, TimeOfDay.now, 'Its midnight, today is now ' + str(TimeOfDay.now.ctime())) reason, message = self.build_schedule_for_today() if reason == 'error': self.mon.err(self, 'system error- illegal time at midnight') return self.mon.sched(self, TimeOfDay.now, self.pretty_todays_schedule()) # if self.testing: # self.print_todays_schedule() self.build_events_lists() # self.mon.sched(self,TimeOfDay.now,self.pretty_events_lists()) # self.print_events_lists() # print TimeOfDay.scheduler_time for show_ref in TimeOfDay.events: # print 'scheduler time match', show_ref times = TimeOfDay.events[show_ref] # now send a command if time matches for time_element in reversed(times): # print time_element[1],TimeOfDay.scheduler_time if time_element[1] == TimeOfDay.scheduler_time: self.do_event(show_ref, time_element) # execute an event def do_event(self, show_ref, time_element): self.mon.log( self, 'Event : ' + time_element[0] + ' ' + show_ref + ' required at: ' + time_element[1].isoformat()) self.mon.sched( self, TimeOfDay.now, ' ToD Scheduler : ' + time_element[0] + ' ' + show_ref + ' required at: ' + time_element[1].isoformat()) if self.testing: print 'Event : ' + time_element[ 0] + ' ' + show_ref + ' required at: ' + time_element[ 1].isoformat() if show_ref != 'start': self.callback(time_element[0] + ' ' + show_ref) else: self.callback(time_element[0]) # # ************************************************ # The methods below can be called from many classes so need to operate on class variables # ************************************************ # clear events list def clear_events_lists(self): self.mon.log(self, 'clear time of day events list ') # empty event list TimeOfDay.events = {} # *********************************** # Preparing schedule and todays event list # ************************************ def read_schedule(self): # get schedule from showlist index = self.showlist.index_of_start_show() self.showlist.select(index) starter_show = self.showlist.selected_show() sched_enabled = starter_show['sched-enable'] if sched_enabled != 'yes': return 'normal', '', False if starter_show['simulate-time'] == 'yes': self.simulate_time = True self.sim_second = starter_show['sim-second'] if not self.sim_second.isdigit(): return 'error', 'Simulate time - second is not a positive integer ' + self.sim_second, False if int(self.sim_second) > 59: return 'error', 'Simulate time - second is out of range ' + self.sim_second, False self.sim_minute = starter_show['sim-minute'] if not self.sim_minute.isdigit(): return 'error', 'Simulate time - minute is not a positive integer ' + self.sim_minute, False if int(self.sim_minute) > 59: return 'error', 'Simulate time - minute is out of range ' + self.sim_minute, False self.sim_hour = starter_show['sim-hour'] if not self.sim_hour.isdigit(): return 'error', 'Simulate time - hour is not a positive integer ' + self.sim_hour, False if int(self.sim_hour) > 23: return 'error', 'Simulate time - hour is out of range ' + self.sim_hour, False self.sim_day = starter_show['sim-day'] if not self.sim_day.isdigit(): return 'error', 'Simulate time - day is not a positive integer ' + self.sim_day, False if int(self.sim_day) > 31: return 'error', 'Simulate time - day is out of range ' + self.sim_day, False self.sim_month = starter_show['sim-month'] if not self.sim_month.isdigit(): return 'error', 'Simulate time - month is not a positive integer ' + self.sim_month, False if int(self.sim_month) > 12: return 'error', 'Simulate time - month is out of range ' + self.sim_month, False self.sim_year = starter_show['sim-year'] if not self.sim_year.isdigit(): return 'error', 'Simulate time - year is not a positive integer ' + self.sim_year, False if int(self.sim_year) < 2019: return 'error', 'Simulate time - year is out of range ' + self.sim_year, False else: self.simulate_time = False return 'normal', '', True def build_schedule_for_today(self): # print this_day.year, this_day.month, this_day.day, TimeOfDay.DAYS_OF_WEEK[ this_day.weekday()] """ self.todays_schedule is a dictionary the keys being show-refs. Each dictionary entry is a list of time_elements Each time element is a list with the fields: 0 - command 1 - time hour:min[:sec] """ self.enable_catchup = {} self.todays_schedule = {} for index in range(self.showlist.length()): show = self.showlist.show(index) show_type = show['type'] show_ref = show['show-ref'] if show['type'] != 'start': self.enable_catchup[show_ref] = show['enable-catchup'] # print 'looping build ',show_type,show_ref,self.showlist.length() if 'sched-everyday' in show: text = show['sched-everyday'] lines = text.splitlines() while len(lines) != 0: status, message, day_lines, lines = self.get_one_day( lines, show_ref) if status == 'error': return 'error', message status, message, days_list, times_list = self.parse_day( day_lines, 'everyday', show_ref, show_type) if status == 'error': return 'error', message #print 'everyday ',status,message,days_list,times_list self.todays_schedule[show['show-ref']] = copy.deepcopy( times_list) # print '\nafter everyday' # self.print_todays_schedule() if 'sched-weekday' in show: text = show['sched-weekday'] lines = text.splitlines() while len(lines) != 0: status, message, day_lines, lines = self.get_one_day( lines, show_ref) if status == 'error': return 'error', message status, message, days_list, times_list = self.parse_day( day_lines, 'weekday', show_ref, show_type) if status == 'error': return 'error', message #print 'weekday ',status,message,days_list,times_list # is current day of the week in list of days in schedule if TimeOfDay.DAYS_OF_WEEK[ TimeOfDay.now.weekday()] in days_list: self.todays_schedule[show['show-ref']] = copy.deepcopy( times_list) #print '\nafter weekday' #self.print_todays_schedule() if 'sched-monthday' in show: text = show['sched-monthday'] lines = text.splitlines() while len(lines) != 0: status, message, day_lines, lines = self.get_one_day( lines, show_ref) # print 'in monthday',day_lines if status == 'error': return 'error', message status, message, days_list, times_list = self.parse_day( day_lines, 'monthday', show_ref, show_type) if status == 'error': return 'error', message #print 'monthday ',status,message,days_list,times_list if TimeOfDay.now.day in map(int, days_list): self.todays_schedule[show['show-ref']] = copy.deepcopy( times_list) #print '\nafter monthday' #self.print_todays_schedule() if 'sched-specialday' in show: text = show['sched-specialday'] lines = text.splitlines() while len(lines) != 0: status, message, day_lines, lines = self.get_one_day( lines, show_ref) if status == 'error': return 'error', message status, message, days_list, times_list = self.parse_day( day_lines, 'specialday', show_ref, show_type) if status == 'error': return 'error', message # print 'specialday ',status,message,days_list,times_list for day in days_list: sdate = datetime.strptime(day, '%Y-%m-%d') if sdate.year == TimeOfDay.now.year and sdate.month == TimeOfDay.now.month and sdate.day == TimeOfDay.now.day: self.todays_schedule[ show['show-ref']] = copy.deepcopy(times_list) #print '\nafter specialday' #self.print_todays_schedule() # print self.enable_catchup return 'normal', '' def get_one_day(self, lines, show_ref): this_day = [] left_over = [] #print 'get one day',lines # check first line is day and move tt output #print lines[0] if not lines[0].startswith('day'): return 'error', 'first line of section is not day ' + lines[ 0] + ' ' + show_ref, [], [] this_day = [lines[0]] #print ' this day',this_day left_over = lines[1:] # print 'left over',left_over x_left_over = lines[1:] for line in x_left_over: #print 'in loop',line if line.startswith('day'): # print 'one day day',this_day,left_over return 'normal', '', this_day, left_over this_day.append(line) left_over = left_over[1:] # print 'one day end',this_day,left_over return 'normal', '', this_day, left_over def parse_day(self, lines, section, show_ref, show_type): # text # day monday # open 1:42 # close 1:45 # returns status,message,list of days,list of time lines if section == 'everyday': status, message, days_list = self.parse_everyday( lines[0], show_ref) elif section == 'weekday': status, message, days_list = self.parse_weekday(lines[0], show_ref) elif section == 'monthday': # print 'parse_day',lines status, message, days_list = self.parse_monthday( lines[0], show_ref) elif section == 'specialday': status, message, days_list = self.parse_specialday( lines[0], show_ref) else: return 'error','illegal section name '+section + ' '+ show_ref,[],[] if status == 'error': return 'error', message, [], [] if len(lines) > 1: time_lines = lines[1:] status, message, times_list = self.parse_time_lines( time_lines, show_ref, show_type) if status == 'error': return 'error', message, [], [] else: times_list = [] return 'normal', '', days_list, times_list def parse_everyday(self, line, show_ref): words = line.split() if words[0] != 'day': return 'error','day line does not contain day '+ line + ' ' + show_ref,[] if words[1] != 'everyday': return 'error','everday line does not contain everyday '+ show_ref,[] return 'normal', '', ['everyday'] def parse_weekday(self, line, show_ref): words = line.split() if words[0] != 'day': return 'error','day line does not contain day ' + line + ' ' + show_ref,[] days = words[1:] for day in days: if day not in TimeOfDay.DAYS_OF_WEEK: return 'error','weekday line has illegal day '+ day + ' '+ show_ref,[] return 'normal', '', days def parse_monthday(self, line, show_ref): words = line.split() if words[0] != 'day': return 'error', 'day line does not contain day ' + show_ref, [] days = words[1:] for day in days: if not day.isdigit(): return 'error','monthday line has illegal day '+ day + ' '+ show_ref,[] if int(day) < 1 or int(day) > 31: return 'error','monthday line has out of range day '+ line+ ' '+ show_ref,[] return 'normal', '', days def parse_specialday(self, line, show_ref): words = line.split() if words[0] != 'day': return 'error', 'day line does not contain day ' + show_ref, [] days = words[1:] for day in days: status, message = self.parse_date(day, show_ref) if status == 'error': return 'error', message, '' return 'normal', '', days def parse_time_lines(self, lines, show_ref, show_type): # lines - list of lines each with text 'command time' # returns list of lists each being [command, time] time_lines = [] for line in lines: # split line into time,command words = line.split() if len(words) < 2: return 'error','time line has wrong length '+ line+ ' '+ show_ref,[] status, message, time_item = self.parse_time(words[0], show_ref) if status == 'error': return 'error', message, [] if show_type == 'start': command = ' '.join(words[1:]) time_lines.append([command, time_item]) else: if words[1] not in ('open', 'close'): return 'error','illegal command in '+ line+ ' '+ show_ref,[] time_lines.append([words[1], time_item]) return 'normal', '', time_lines def build_events_lists(self): # builds events dictionary from todays_schedule by # converting times in todays schedule from hour:min:sec to datetime # and sorts them earliest last TimeOfDay.events = {} for show_ref in self.todays_schedule: # print show_ref times = self.todays_schedule[show_ref] for time_element in times: time_element[1] = self.parse_event_time(time_element[1]) sorted_times = sorted(times, key=lambda time_element: time_element[1], reverse=True) TimeOfDay.events[show_ref] = sorted_times # print times def parse_event_time(self, time_text): fields = time_text.split(':') if len(fields) > 2: secs = int(fields[2]) else: secs = 0 hours = int(fields[0]) mins = int(fields[1]) return time(hour=hours, minute=mins, second=secs) def parse_time(self, item, show_ref): fields = item.split(':') if len(fields) == 0: return 'error', 'Time field is empty ' + item + ' ' + show_ref, item if len(fields) > 3: return 'error', 'Too many fields in ' + item + ' ' + show_ref, item if len(fields) == 1: seconds = fields[0] minutes = '0' hours = '0' if len(fields) == 2: seconds = fields[1] minutes = fields[0] hours = '0' if len(fields) == 3: seconds = fields[2] minutes = fields[1] hours = fields[0] if not seconds.isdigit() or not minutes.isdigit() or not hours.isdigit( ): return 'error', 'Fields of ' + item + ' are not positive integers ' + show_ref, item if int(minutes) > 59: return 'error', 'Minutes of ' + item + ' is out of range ' + show_ref, item if int(seconds) > 59: return 'error', 'Seconds of ' + item + ' is out of range ' + show_ref, item if int(hours) > 23: return 'error', 'Hours of ' + item + ' is out of range ' + show_ref, item return 'normal', '', item def parse_date(self, item, show_ref): fields = item.split('-') if len(fields) == 0: return 'error', 'Date field is empty ' + item + ' ' + show_ref if len(fields) != 3: return 'error', 'Too many or few fields in date ' + item + ' ' + show_ref year = fields[0] month = fields[1] day = fields[2] if not year.isdigit() or not month.isdigit() or not day.isdigit(): return 'error', 'Fields of ' + item + ' are not positive integers ' + show_ref if int(year) < 2018: return 'error', 'Year of ' + item + ' is out of range ' + show_ref + year if int(month) > 12: return 'error', 'Month of ' + item + ' is out of range ' + show_ref if int(day) > 31: return 'error', 'Day of ' + item + ' is out of range ' + show_ref return 'normal', '' # ********************* # print for debug # ********************* def pretty_todays_schedule(self): op = 'Schedule For ' + TimeOfDay.now.ctime() + '\n' for key in self.todays_schedule: op += ' ' + key + '\n' for show in self.todays_schedule[key]: op += ' ' + show[0] + ': ' + str(show[1]) + '\n' op += '\n' return op def pretty_events_lists(self): op = ' Task list for today' for key in self.events: op += '\n' + key for show in self.events[key]: op += '\n ' + show[0] + ' ' + str(show[1].isoformat()) return op def print_todays_schedule(self): print '\nSchedule For ' + TimeOfDay.now.ctime() for key in self.todays_schedule: print ' ' + key for show in self.todays_schedule[key]: print ' ' + show[0] + ': ' + show[1] print def print_events_lists(self): print '\nTask list for today' for key in self.events: print '\n', key for show in self.events[key]: print show[0], show[1].isoformat() print
class PiPresents(object): def pipresents_version(self): vitems=self.pipresents_issue.split('.') if len(vitems)==2: # cope with 2 digit version numbers before 1.3.2 return 1000*int(vitems[0])+100*int(vitems[1]) else: return 1000*int(vitems[0])+100*int(vitems[1])+int(vitems[2]) def __init__(self): # gc.set_debug(gc.DEBUG_UNCOLLECTABLE|gc.DEBUG_INSTANCES|gc.DEBUG_OBJECTS|gc.DEBUG_SAVEALL) gc.set_debug(gc.DEBUG_UNCOLLECTABLE|gc.DEBUG_SAVEALL) self.pipresents_issue="1.3.5" self.pipresents_minorissue = '1.3.5d' # position and size of window without -f command line option self.nonfull_window_width = 0.45 # proportion of width self.nonfull_window_height= 0.7 # proportion of height self.nonfull_window_x = 0 # position of top left corner self.nonfull_window_y=0 # position of top left corner StopWatch.global_enable=False # set up the handler for SIGTERM signal.signal(signal.SIGTERM,self.handle_sigterm) # **************************************** # Initialisation # *************************************** # get command line options self.options=command_options() # get Pi Presents code directory pp_dir=sys.path[0] self.pp_dir=pp_dir if not os.path.exists(pp_dir+"/pipresents.py"): if self.options['manager'] is False: tkMessageBox.showwarning("Pi Presents","Bad Application Directory") exit(102) # Initialise logging and tracing Monitor.log_path=pp_dir self.mon=Monitor() # Init in PiPresents only self.mon.init() # uncomment to enable control of logging from within a class # Monitor.enable_in_code = True # enables control of log level in the code for a class - self.mon.set_log_level() # make a shorter list to log/trace only some classes without using enable_in_code. Monitor.classes = ['PiPresents', 'HyperlinkShow','RadioButtonShow','ArtLiveShow','ArtMediaShow','MediaShow','LiveShow','MenuShow', 'GapShow','Show','ArtShow', 'AudioPlayer','BrowserPlayer','ImagePlayer','MenuPlayer','MessagePlayer','VideoPlayer','Player', 'MediaList','LiveList','ShowList', 'PathManager','ControlsManager','ShowManager','PluginManager','IOPluginManager', 'MplayerDriver','OMXDriver','UZBLDriver', 'TimeOfDay','ScreenDriver','Animate','OSCDriver','CounterManager', 'Network','Mailer' ] # Monitor.classes=['PiPresents','MediaShow','GapShow','Show','VideoPlayer','Player','OMXDriver'] # Monitor.classes=['OSCDriver'] # get global log level from command line Monitor.log_level = int(self.options['debug']) Monitor.manager = self.options['manager'] # print self.options['manager'] self.mon.newline(3) self.mon.sched (self,None, "Pi Presents is starting, Version:"+self.pipresents_minorissue + ' at '+time.strftime("%Y-%m-%d %H:%M.%S")) self.mon.log (self, "Pi Presents is starting, Version:"+self.pipresents_minorissue+ ' at '+time.strftime("%Y-%m-%d %H:%M.%S")) # self.mon.log (self," OS and separator:" + os.name +' ' + os.sep) self.mon.log(self,"sys.path[0] - location of code: "+sys.path[0]) # log versions of Raspbian and omxplayer, and GPU Memory with open("/boot/issue.txt") as ifile: self.mon.log(self,'\nRaspbian: '+ifile.read()) self.mon.log(self,'\n'+check_output(["omxplayer", "-v"])) self.mon.log(self,'\nGPU Memory: '+check_output(["vcgencmd", "get_mem", "gpu"])) if os.geteuid() == 0: print 'Do not run Pi Presents with sudo' self.mon.log(self,'Do not run Pi Presents with sudo') self.mon.finish() sys.exit(102) if "DESKTOP_SESSION" not in os.environ: print 'Pi Presents must be run from the Desktop' self.mon.log(self,'Pi Presents must be run from the Desktop') self.mon.finish() sys.exit(102) else: self.mon.log(self,'Desktop is '+ os.environ['DESKTOP_SESSION']) # optional other classes used self.root=None self.ppio=None self.tod=None self.animate=None self.ioplugin_manager=None self.oscdriver=None self.osc_enabled=False self.tod_enabled=False self.email_enabled=False user=os.getenv('USER') if user is None: tkMessageBox.showwarning("You must be logged in to run Pi Presents") exit(102) if user !='pi': self.mon.warn(self,"You must be logged as pi to use GPIO") self.mon.log(self,'User is: '+ user) # self.mon.log(self,"os.getenv('HOME') - user home directory (not used): " + os.getenv('HOME')) # does not work # self.mon.log(self,"os.path.expanduser('~') - user home directory: " + os.path.expanduser('~')) # does not work # check network is available self.network_connected=False self.network_details=False self.interface='' self.ip='' self.unit='' # sets self.network_connected and self.network_details self.init_network() # start the mailer and send email when PP starts self.email_enabled=False if self.network_connected is True: self.init_mailer() if self.email_enabled is True and self.mailer.email_at_start is True: subject= '[Pi Presents] ' + self.unit + ': PP Started on ' + time.strftime("%Y-%m-%d %H:%M") message = time.strftime("%Y-%m-%d %H:%M") + '\nUnit: ' + self.unit + ' Profile: '+ self.options['profile']+ '\n ' + self.interface + '\n ' + self.ip self.send_email('start',subject,message) # get profile path from -p option if self.options['profile'] != '': self.pp_profile_path="/pp_profiles/"+self.options['profile'] else: self.mon.err(self,"Profile not specified in command ") self.end('error','Profile not specified with the commands -p option') # get directory containing pp_home from the command, if self.options['home'] == "": home = os.sep+ 'home' + os.sep + user + os.sep+"pp_home" else: home = self.options['home'] + os.sep+ "pp_home" self.mon.log(self,"pp_home directory is: " + home) # check if pp_home exists. # try for 10 seconds to allow usb stick to automount found=False for i in range (1, 10): self.mon.log(self,"Trying pp_home at: " + home + " (" + str(i)+')') if os.path.exists(home): found=True self.pp_home=home break time.sleep (1) if found is True: self.mon.log(self,"Found Requested Home Directory, using pp_home at: " + home) else: self.mon.err(self,"Failed to find pp_home directory at " + home) self.end('error',"Failed to find pp_home directory at " + home) # check profile exists self.pp_profile=self.pp_home+self.pp_profile_path if os.path.exists(self.pp_profile): self.mon.sched(self,None,"Running profile: " + self.pp_profile_path) self.mon.log(self,"Found Requested profile - pp_profile directory is: " + self.pp_profile) else: self.mon.err(self,"Failed to find requested profile: "+ self.pp_profile) self.end('error',"Failed to find requested profile: "+ self.pp_profile) self.mon.start_stats(self.options['profile']) if self.options['verify'] is True: self.mon.err(self,"Validation option not supported - use the editor") self.end('error','Validation option not supported - use the editor') # initialise and read the showlist in the profile self.showlist=ShowList() self.showlist_file= self.pp_profile+ "/pp_showlist.json" if os.path.exists(self.showlist_file): self.showlist.open_json(self.showlist_file) else: self.mon.err(self,"showlist not found at "+self.showlist_file) self.end('error',"showlist not found at "+self.showlist_file) # check profile and Pi Presents issues are compatible if self.showlist.profile_version() != self.pipresents_version(): self.mon.err(self,"Version of showlist " + self.showlist.profile_version_string + " is not same as Pi Presents") self.end('error',"Version of showlist " + self.showlist.profile_version_string + " is not same as Pi Presents") # get the 'start' show from the showlist index = self.showlist.index_of_start_show() if index >=0: self.showlist.select(index) self.starter_show=self.showlist.selected_show() else: self.mon.err(self,"Show [start] not found in showlist") self.end('error',"Show [start] not found in showlist") # ******************** # SET UP THE GUI # ******************** # turn off the screenblanking and saver if self.options['noblank'] is True: call(["xset","s", "off"]) call(["xset","s", "-dpms"]) self.root=Tk() self.title='Pi Presents - '+ self.pp_profile self.icon_text= 'Pi Presents' self.root.title(self.title) self.root.iconname(self.icon_text) self.root.config(bg=self.starter_show['background-colour']) self.mon.log(self, 'monitor screen dimensions are ' + str(self.root.winfo_screenwidth()) + ' x ' + str(self.root.winfo_screenheight()) + ' pixels') if self.options['screensize'] =='': self.screen_width = self.root.winfo_screenwidth() self.screen_height = self.root.winfo_screenheight() else: reason,message,self.screen_width,self.screen_height=self.parse_screen(self.options['screensize']) if reason =='error': self.mon.err(self,message) self.end('error',message) self.mon.log(self, 'forced screen dimensions (--screensize) are ' + str(self.screen_width) + ' x ' + str(self.screen_height) + ' pixels') # set window dimensions and decorations if self.options['fullscreen'] is False: self.window_width=int(self.root.winfo_screenwidth()*self.nonfull_window_width) self.window_height=int(self.root.winfo_screenheight()*self.nonfull_window_height) self.window_x=self.nonfull_window_x self.window_y=self.nonfull_window_y self.root.geometry("%dx%d%+d%+d" % (self.window_width,self.window_height,self.window_x,self.window_y)) else: self.window_width=self.screen_width self.window_height=self.screen_height self.root.attributes('-fullscreen', True) os.system('unclutter &') self.window_x=0 self.window_y=0 self.root.geometry("%dx%d%+d%+d" % (self.window_width,self.window_height,self.window_x,self.window_y)) self.root.attributes('-zoomed','1') # canvas cover the whole screen whatever the size of the window. self.canvas_height=self.screen_height self.canvas_width=self.screen_width # make sure focus is set. self.root.focus_set() # define response to main window closing. self.root.protocol ("WM_DELETE_WINDOW", self.handle_user_abort) # setup a canvas onto which will be drawn the images or text self.canvas = Canvas(self.root, bg=self.starter_show['background-colour']) if self.options['fullscreen'] is True: self.canvas.config(height=self.canvas_height, width=self.canvas_width, highlightthickness=0) else: self.canvas.config(height=self.canvas_height, width=self.canvas_width, highlightthickness=1, highlightcolor='yellow') self.canvas.place(x=0,y=0) # self.canvas.config(bg='black') self.canvas.focus_set() # **************************************** # INITIALISE THE TOUCHSCREEN DRIVER # **************************************** # each driver takes a set of inputs, binds them to symboic names # and sets up a callback which returns the symbolic name when an input event occurs self.sr=ScreenDriver() # read the screen click area config file reason,message = self.sr.read(pp_dir,self.pp_home,self.pp_profile) if reason == 'error': self.end('error','cannot find, or error in screen.cfg') # create click areas on the canvas, must be polygon as outline rectangles are not filled as far as find_closest goes # click areas are made on the Pi Presents canvas not the show canvases. reason,message = self.sr.make_click_areas(self.canvas,self.handle_input_event) if reason == 'error': self.mon.err(self,message) self.end('error',message) # **************************************** # INITIALISE THE APPLICATION AND START # **************************************** self.shutdown_required=False self.reboot_required=False self.terminate_required=False self.exitpipresents_required=False # initialise the I/O plugins by importing their drivers self.ioplugin_manager=IOPluginManager() reason,message=self.ioplugin_manager.init(self.pp_dir,self.pp_profile,self.root,self.handle_input_event) if reason == 'error': # self.mon.err(self,message) self.end('error',message) # kick off animation sequencer self.animate = Animate() self.animate.init(pp_dir,self.pp_home,self.pp_profile,self.canvas,200,self.handle_output_event) self.animate.poll() #create a showmanager ready for time of day scheduler and osc server show_id=-1 self.show_manager=ShowManager(show_id,self.showlist,self.starter_show,self.root,self.canvas,self.pp_dir,self.pp_profile,self.pp_home) # first time through set callback to terminate Pi Presents if all shows have ended. self.show_manager.init(self.canvas,self.all_shows_ended_callback,self.handle_command,self.showlist) # Register all the shows in the showlist reason,message=self.show_manager.register_shows() if reason == 'error': self.mon.err(self,message) self.end('error',message) # Init OSCDriver, read config and start OSC server self.osc_enabled=False if self.network_connected is True: if os.path.exists(self.pp_profile + os.sep + 'pp_io_config'+ os.sep + 'osc.cfg'): self.oscdriver=OSCDriver() reason,message=self.oscdriver.init(self.pp_profile, self.unit,self.interface,self.ip, self.handle_command,self.handle_input_event,self.e_osc_handle_animate) if reason == 'error': self.mon.err(self,message) self.end('error',message) else: self.osc_enabled=True self.root.after(1000,self.oscdriver.start_server()) # initialise ToD scheduler calculating schedule for today self.tod=TimeOfDay() reason,message,self.tod_enabled = self.tod.init(pp_dir,self.pp_home,self.pp_profile,self.showlist,self.root,self.handle_command) if reason == 'error': self.mon.err(self,message) self.end('error',message) # warn if the network not available when ToD required if self.tod_enabled is True and self.network_connected is False: self.mon.warn(self,'Network not connected so Time of Day scheduler may be using the internal clock') # init the counter manager self.counter_manager=CounterManager() self.counter_manager.init() # warn about start shows and scheduler if self.starter_show['start-show']=='' and self.tod_enabled is False: self.mon.sched(self,None,"No Start Shows in Start Show and no shows scheduled") self.mon.warn(self,"No Start Shows in Start Show and no shows scheduled") if self.starter_show['start-show'] !='' and self.tod_enabled is True: self.mon.sched(self,None,"Start Shows in Start Show and shows scheduled - conflict?") self.mon.warn(self,"Start Shows in Start Show and shows scheduled - conflict?") # run the start shows self.run_start_shows() # kick off the time of day scheduler which may run additional shows if self.tod_enabled is True: self.tod.poll() # start the I/O plugins input event generation self.ioplugin_manager.start() # start Tkinters event loop self.root.mainloop( ) def parse_screen(self,size_text): fields=size_text.split('*') if len(fields)!=2: return 'error','do not understand --screensize comand option',0,0 elif fields[0].isdigit() is False or fields[1].isdigit() is False: return 'error','dimensions are not positive integers in --screensize',0,0 else: return 'normal','',int(fields[0]),int(fields[1]) # ********************* # RUN START SHOWS # ******************** def run_start_shows(self): self.mon.trace(self,'run start shows') # parse the start shows field and start the initial shows show_refs=self.starter_show['start-show'].split() for show_ref in show_refs: reason,message=self.show_manager.control_a_show(show_ref,'open') if reason == 'error': self.mon.err(self,message) # ********************* # User inputs # ******************** def e_osc_handle_animate(self,line): #jump out of server thread self.root.after(1, lambda arg=line: self.osc_handle_animate(arg)) def osc_handle_animate(self,line): self.mon.log(self,"animate command received: "+ line) #osc sends output events as a string reason,message,delay,name,param_type,param_values=self.animate.parse_animate_fields(line) if reason == 'error': self.mon.err(self,message) self.end(reason,message) self.handle_output_event(name,param_type,param_values,0) # output events are animate commands def handle_output_event(self,symbol,param_type,param_values,req_time): reason,message=self.ioplugin_manager.handle_output_event(symbol,param_type,param_values,req_time) if reason =='error': self.mon.err(self,message) self.end(reason,message) # all input events call this callback providing a symbolic name. # handle events that affect PP overall, otherwise pass to all active shows def handle_input_event(self,symbol,source): self.mon.log(self,"event received: "+symbol + ' from '+ source) if symbol == 'pp-terminate': self.handle_user_abort() elif symbol == 'pp-shutdown': self.mon.err(self,'pp-shutdown removed in version 1.3.3a, see Release Notes') self.end('error','pp-shutdown removed in version 1.3.3a, see Release Notes') elif symbol == 'pp-shutdownnow': # need root.after to grt out of st thread self.root.after(1,self.shutdownnow_pressed) return elif symbol == 'pp-exitpipresents': self.exitpipresents_required=True if self.show_manager.all_shows_exited() is True: # need root.after to grt out of st thread self.root.after(1,self.e_all_shows_ended_callback) return reason,message= self.show_manager.exit_all_shows() else: # pass the input event to all registered shows for show in self.show_manager.shows: show_obj=show[ShowManager.SHOW_OBJ] if show_obj is not None: show_obj.handle_input_event(symbol) # commands are generaed by tracks and shows # they can open or close shows, generate input events and do special tasks # commands also generate osc outputs to other computers # handles one command provided as a line of text def handle_command(self,command_text,source='',show=''): # print 'PIPRESENTS ',command_text,'\n Source',source,'from',show self.mon.log(self,"command received: " + command_text) if command_text.strip()=="": return fields= command_text.split() if fields[0] in ('osc','OSC'): if self.osc_enabled is True: status,message=self.oscdriver.parse_osc_command(fields[1:]) if status=='warn': self.mon.warn(self,message) if status=='error': self.mon.err(self,message) self.end('error',message) return if fields[0] =='counter': status,message=self.counter_manager.parse_counter_command(fields[1:]) if status=='error': self.mon.err(self,message) self.end('error',message) return show_command=fields[0] if len(fields)>1: show_ref=fields[1] else: show_ref='' if show_command in ('open','close','closeall','openexclusive'): self.mon.sched(self, TimeOfDay.now,command_text + ' received from show:'+show) if self.shutdown_required is False and self.terminate_required is False: reason,message=self.show_manager.control_a_show(show_ref,show_command) else: return elif show_command =='monitor': self.handle_monitor_command(show_ref) return elif show_command =='cec': self.handle_cec_command(show_ref) return elif show_command == 'event': self.handle_input_event(show_ref,'Show Control') return elif show_command == 'exitpipresents': self.exitpipresents_required=True if self.show_manager.all_shows_exited() is True: # need root.after to get out of st thread self.root.after(1,self.e_all_shows_ended_callback) return else: reason,message= self.show_manager.exit_all_shows() elif show_command == 'shutdownnow': # need root.after to get out of st thread self.root.after(1,self.shutdownnow_pressed) return elif show_command == 'reboot': # need root.after to get out of st thread self.root.after(1,self.reboot_pressed) return else: reason='error' message = 'command not recognised: '+ show_command if reason=='error': self.mon.err(self,message) return def handle_monitor_command(self,command): if command == 'on': os.system('vcgencmd display_power 1 >/dev/null') elif command == 'off': os.system('vcgencmd display_power 0 >/dev/null') def handle_cec_command(self,command): if command == 'on': os.system('echo "on 0" | cec-client -s') elif command == 'standby': os.system('echo "standby 0" | cec-client -s') elif command == 'scan': os.system('echo scan | cec-client -s -d 1') # deal with differnt commands/input events def shutdownnow_pressed(self): self.shutdown_required=True if self.show_manager.all_shows_exited() is True: self.all_shows_ended_callback('normal','no shows running') else: # calls exit method of all shows, results in all_shows_closed_callback self.show_manager.exit_all_shows() def reboot_pressed(self): self.reboot_required=True if self.show_manager.all_shows_exited() is True: self.all_shows_ended_callback('normal','no shows running') else: # calls exit method of all shows, results in all_shows_closed_callback self.show_manager.exit_all_shows() def handle_sigterm(self,signum,fframe): self.mon.log(self,'SIGTERM received - '+ str(signum)) self.terminate() def handle_user_abort(self): self.mon.log(self,'User abort received') self.terminate() def terminate(self): self.mon.log(self, "terminate received") self.terminate_required=True needs_termination=False for show in self.show_manager.shows: # print show[ShowManager.SHOW_OBJ], show[ShowManager.SHOW_REF] if show[ShowManager.SHOW_OBJ] is not None: needs_termination=True self.mon.log(self,"Sent terminate to show "+ show[ShowManager.SHOW_REF]) # call shows terminate method # eventually the show will exit and after all shows have exited all_shows_callback will be executed. show[ShowManager.SHOW_OBJ].terminate() if needs_termination is False: self.end('killed','killed - no termination of shows required') # ****************************** # Ending Pi Presents after all the showers and players are closed # ************************** def e_all_shows_ended_callback(self): self.all_shows_ended_callback('normal','no shows running') # callback from ShowManager when all shows have ended def all_shows_ended_callback(self,reason,message): self.canvas.config(bg=self.starter_show['background-colour']) if reason in ('killed','error') or self.shutdown_required is True or self.exitpipresents_required is True or self.reboot_required is True: self.end(reason,message) def end(self,reason,message): self.mon.log(self,"Pi Presents ending with reason: " + reason) if self.root is not None: self.root.destroy() self.tidy_up() if reason == 'killed': if self.email_enabled is True and self.mailer.email_on_terminate is True: subject= '[Pi Presents] ' + self.unit + ': PP Exited with reason: Terminated' message = time.strftime("%Y-%m-%d %H:%M") + '\n ' + self.unit + '\n ' + self.interface + '\n ' + self.ip self.send_email(reason,subject,message) self.mon.sched(self, None,"Pi Presents Terminated, au revoir\n") self.mon.log(self, "Pi Presents Terminated, au revoir") # close logging files self.mon.finish() print 'Uncollectable Garbage',gc.collect() # objgraph.show_backrefs(objgraph.by_type('Monitor')) sys.exit(101) elif reason == 'error': if self.email_enabled is True and self.mailer.email_on_error is True: subject= '[Pi Presents] ' + self.unit + ': PP Exited with reason: Error' message_text = 'Error message: '+ message + '\n'+ time.strftime("%Y-%m-%d %H:%M") + '\n ' + self.unit + '\n ' + self.interface + '\n ' + self.ip self.send_email(reason,subject,message_text) self.mon.sched(self,None, "Pi Presents closing because of error, sorry\n") self.mon.log(self, "Pi Presents closing because of error, sorry") # close logging files self.mon.finish() print 'uncollectable garbage',gc.collect() sys.exit(102) else: self.mon.sched(self,None,"Pi Presents exiting normally, bye\n") self.mon.log(self,"Pi Presents exiting normally, bye") # close logging files self.mon.finish() if self.reboot_required is True: # print 'REBOOT' call (['sudo','reboot']) if self.shutdown_required is True: # print 'SHUTDOWN' call (['sudo','shutdown','now','SHUTTING DOWN']) print 'uncollectable garbage',gc.collect() sys.exit(100) # tidy up all the peripheral bits of Pi Presents def tidy_up(self): self.handle_monitor_command('on') self.mon.log(self, "Tidying Up") # turn screen blanking back on if self.options['noblank'] is True: call(["xset","s", "on"]) call(["xset","s", "+dpms"]) # tidy up animation if self.animate is not None: self.animate.terminate() # tidy up i/o plugins if self.ioplugin_manager != None: self.ioplugin_manager.terminate() if self.osc_enabled is True: self.oscdriver.terminate() # tidy up time of day scheduler if self.tod_enabled is True: self.tod.terminate() # ******************************* # Connecting to network and email # ******************************* def init_network(self): timeout=int(self.options['nonetwork']) if timeout== 0: self.network_connected=False self.unit='' self.ip='' self.interface='' return self.network=Network() self.network_connected=False # try to connect to network self.mon.log (self, 'Waiting up to '+ str(timeout) + ' seconds for network') success=self.network.wait_for_network(timeout) if success is False: self.mon.warn(self,'Failed to connect to network after ' + str(timeout) + ' seconds') # tkMessageBox.showwarning("Pi Presents","Failed to connect to network so using fake-hwclock") return self.network_connected=True self.mon.sched (self, None,'Time after network check is '+ time.strftime("%Y-%m-%d %H:%M.%S")) self.mon.log (self, 'Time after network check is '+ time.strftime("%Y-%m-%d %H:%M.%S")) # Get web configuration self.network_details=False network_options_file_path=self.pp_dir+os.sep+'pp_config'+os.sep+'pp_web.cfg' if not os.path.exists(network_options_file_path): self.mon.warn(self,"pp_web.cfg not found at "+network_options_file_path) return self.mon.log(self, 'Found pp_web.cfg in ' + network_options_file_path) self.network.read_config(network_options_file_path) self.unit=self.network.unit # get interface and IP details of preferred interface self.interface,self.ip = self.network.get_preferred_ip() if self.interface == '': self.network_connected=False return self.network_details=True self.mon.log (self, 'Network details ' + self.unit + ' ' + self.interface + ' ' +self.ip) def init_mailer(self): self.email_enabled=False email_file_path = self.pp_dir+os.sep+'pp_config'+os.sep+'pp_email.cfg' if not os.path.exists(email_file_path): self.mon.log(self,'pp_email.cfg not found at ' + email_file_path) return self.mon.log(self,'Found pp_email.cfg at ' + email_file_path) self.mailer=Mailer() self.mailer.read_config(email_file_path) # all Ok so can enable email if config file allows it. if self.mailer.email_allowed is True: self.email_enabled=True self.mon.log (self,'Email Enabled') def try_connect(self): tries=1 while True: success, error = self.mailer.connect() if success is True: return True else: self.mon.log(self,'Failed to connect to email SMTP server ' + str(tries) + '\n ' +str(error)) tries +=1 if tries >5: self.mon.log(self,'Failed to connect to email SMTP server after ' + str(tries)) return False def send_email(self,reason,subject,message): if self.try_connect() is False: return False else: success,error = self.mailer.send(subject,message) if success is False: self.mon.log(self, 'Failed to send email: ' + str(error)) success,error=self.mailer.disconnect() if success is False: self.mon.log(self,'Failed disconnect after send:' + str(error)) return False else: self.mon.log(self,'Sent email for ' + reason) success,error=self.mailer.disconnect() if success is False: self.mon.log(self,'Failed disconnect from email server ' + str(error)) return True