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.6" self.pipresents_minorissue = '1.4.6c' 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 # initlize VLCDriver logger self.logger = Logger(enabled=True) self.logger.init() # TURN OFF REST OF LOGGING IN pp_vlcdriver.py # Init main monitor 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', 'VLCPlayer', 'ChromePlayer', 'MediaList', 'LiveList', 'ShowList', 'PathManager', 'ControlsManager', 'ShowManager', 'TrackPluginManager', '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"], 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.dm = 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']) # 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, self.pp_dir, False) 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_id in DisplayManager.displays: if self.dm.has_canvas(display_id): canvas_obj = self.dm.canvas_widget(display_id) canvas_obj.config(bg=self.starter_show['background-colour']) name = self.dm.name_of_display(display_id) width, height = self.dm.real_display_dimensions(display_id) x, y = self.dm.real_display_position(display_id) matrix, ms = self.dm.touch_matrix_for(display_id) rotation = self.dm.real_display_orientation(display_id) self.mon.log( self, ' - ' + name + ' Id: ' + str(display_id) + ' ' + str(x) + '+' + str(y) + '+' + str(width) + '*' + str(height) + ' ' + rotation) self.mon.log(self, ' ' + ms) status, message, driver_name = self.dm.get_driver_name(display_id) if status == 'normal': self.mon.log(self, name + ': Touch Driver: ' + driver_name + '\n') elif status == 'null': self.mon.log(self, name + ': Touch Driver not Defined\n') else: self.mon.err(self, message) # **************************************** # 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 self.restartpipresents_required = False #initialise the Audio manager self.audiomanager = AudioManager() status, message = self.audiomanager.init(self.pp_dir) if status == 'error': self.mon.err(self, message) self.end('error', message) # initialise the Beeps Player self.bp = BeepPlayer() self.bp.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) self.terminate() #self.end(reason,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) def show_control_handle_animate(self, line): self.mon.log(self, "animate show control command received: " + line) line = '0 ' + line reason, message, delay, name, param_type, param_values = self.animate.parse_animate_fields( line) # print (reason,message,delay,name,param_type,param_values) 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 get 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() elif symbol == 'pp-restartpipresents': self.restartpipresents_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': status, message = self.bp.play_show_beep(command_text) if status == 'error': self.mon.err(self, message) self.end('error', message) return return if fields[0] == 'backlight': # on, off, inc val, dec val, set val fade val duration status, message = self.dm.do_backlight_command(command_text) if status == 'error': self.mon.err(self, message) self.end('error', message) return return if fields[0] == 'monitor': status, message = self.dm.handle_monitor_command(fields[1:]) if status == 'error': self.mon.err(self, message) self.end('error', message) return return if fields[0] == 'cec': status, message = self.handle_cec_command(fields[1:]) if status == 'error': self.mon.err(self, message) self.end('error', message) return return if fields[0] == 'animate': self.show_control_handle_animate(' '.join(fields[1:])) return # show commands 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 == '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) self.end('error', message) return return def handle_cec_command(self, args): if len(args) == 0: return 'error', 'no arguments for CEC command' if len(args) == 1: device = '0' if args[0] == 'scan': com = 'echo scan | cec-client -s -d 1' #print (com) os.system(com) return 'normal', '' if args[0] == 'as': com = 'echo as | cec-client -s -d 1' #print (com) os.system(com) return 'normal', '' if len(args) == 2: device = args[1] if not device.isdigit(): return 'error', 'device is not a positive integer' if args[0] == 'on': com = 'echo "on ' + device + '" | cec-client -s -d 1' #print (com) os.system(com) return 'normal', '' elif args[0] == 'standby': com = 'echo "standby ' + device + '" | cec-client -s -d 1' #print (com) os.system(com) return 'normal', '' else: return 'error', 'Unknown CEC command: ' + args[0] # 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 or self.restartpipresents_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 = 'Download log for error 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.restartpipresents_required is True: #print ('restart') return 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.mon.log(self, "Tidying Up") # backlight if self.dm != None: self.dm.terminate() # 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 ChromePlayer(Player): # *************************************** # EXTERNAL COMMANDS # *************************************** def __init__(self, show_id, showlist, root, canvas, show_params, track_params, pp_dir, pp_home, pp_profile, end_callback, command_callback): # initialise items common to all players Player.__init__(self, show_id, showlist, root, canvas, show_params, track_params, pp_dir, pp_home, pp_profile, end_callback, command_callback) self.mon.trace(self, '') # and initialise things for this player self.dm = DisplayManager() # get duration limit (secs ) from profile if self.track_params['duration'] != '': self.duration_text = self.track_params['duration'] else: self.duration_text = self.show_params['duration'] # process chrome window if self.track_params['chrome-window'] != '': self.chrome_window_text = self.track_params['chrome-window'] else: self.chrome_window_text = self.show_params['chrome-window'] # process chrome things if self.track_params['chrome-freeze-at-end'] != '': self.freeze_at_end = self.track_params['chrome-freeze-at-end'] else: self.freeze_at_end = self.show_params['chrome-freeze-at-end'] if self.track_params['chrome-zoom'] != '': self.chrome_zoom_text = self.track_params['chrome-zoom'] else: self.chrome_zoom_text = self.show_params['chrome-zoom'] if self.track_params['chrome-other-options'] != '': self.chrome_other_options = self.track_params[ 'chrome-other-options'] else: self.chrome_other_options = self.show_params[ 'chrome-other-options'] # Initialize variables self.command_timer = None self.tick_timer = None self.quit_signal = False # signal that user has pressed stop # initialise the play state self.play_state = 'initialised' self.load_state = '' # LOAD - loads the browser and show stuff def load(self, track, loaded_callback, enable_menu): # instantiate arguments self.loaded_callback = loaded_callback # callback when loaded self.mon.trace(self, '') status, message, duration100 = Player.parse_duration( self.duration_text) if status == 'error': self.mon.err(self, message) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', message) return self.duration = 2 * duration100 # Is display valid and connected status, message, self.display_id = self.dm.id_of_display( self.show_canvas_display_name) if status == 'error': self.mon.err(self, message) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', 'cannot find file; ' + track) return # does media exist if not ':' in track: if not os.path.exists(track): self.mon.err(self, 'cannot find file; ' + track) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', 'cannot find file; ' + track) return # add file:// to files. if ':' in track: self.current_url = track else: self.current_url = 'file://' + track # do common bits of load Player.pre_load(self) # prepare chromium options status, message = self.process_chrome_options() if status == 'error': self.mon.err(self, message) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', message) return # parse browser commands to self.command_list reason, message = self.parse_commands( self.track_params['browser-commands']) if reason == 'error': self.mon.err(self, message) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', message) return # load the plugin, this may modify self.track and enable the plugin drawing to canvas if self.track_params['plugin'] != '': status, message = self.load_plugin() if status == 'error': self.mon.err(self, message) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', message) return # start loading the browser self.play_state = 'loading' # load the images and text status, message = self.load_x_content(enable_menu) if status == 'error': self.mon.err(self, message) self.play_state = 'load-failed' if self.loaded_callback is not None: self.loaded_callback('error', message) return #start the browser self.driver_open() # for kiosk and fullscreen need to get the url - in browser command for app mode if self.app_mode is False: self.driver_get(self.current_url) self.mon.log(self, 'Loading browser from show Id: ' + str(self.show_id)) self.play_state = 'loaded' # and start executing the browser commands self.play_commands() self.mon.log(self, " State machine: chromium loaded") if self.loaded_callback is not None: self.loaded_callback('normal', 'browser loaded') return # UNLOAD - abort a load when browser is loading or loaded def unload(self): self.mon.trace(self, '') self.mon.log(self, ">unload received from show Id: " + str(self.show_id)) self.driver_close() self.play_state = 'closed' # SHOW - show a track from its loaded state def show(self, ready_callback, finished_callback, closed_callback): # instantiate arguments self.ready_callback = ready_callback # callback when ready to show a web page- self.finished_callback = finished_callback # callback when finished showing - not used self.closed_callback = closed_callback # callback when closed self.mon.trace(self, '') self.play_state = 'showing' # init state and signals self.quit_signal = False # do common bits Player.pre_show(self) #self.driver.get(self.current_url) self.duration_count = self.duration self.tick_timer = self.canvas.after(10, self.show_state_machine) def show_state_machine(self): if self.play_state == 'showing': self.duration_count -= 1 # self.mon.log(self," Show state machine: " + self.show_state) # service any queued stop signals and test duration count if self.quit_signal is True or (self.duration != 0 and self.duration_count == 0): self.mon.log(self, " Service stop required signal or timeout") if self.command_timer != None: self.canvas.after_cancel(self.command_timer) self.command_timer = None if self.quit_signal is True: self.quit_signal = False if self.freeze_at_end == 'yes': self.mon.log(self, 'chrome says pause_at_end') if self.finished_callback is not None: self.finished_callback('pause_at_end', 'pause at end') self.tick_timer = self.canvas.after( 50, self.show_state_machine) else: self.mon.log(self, 'chrome says niceday') self.driver_close() self.play_state = 'closed' if self.closed_callback is not None: self.closed_callback('normal', 'chromedriver closed') return else: self.tick_timer = self.canvas.after(50, self.show_state_machine) # CLOSE - nothing to do in browserplayer - x content is removed by ready callback and hide browser does not implement pause_at_end def close(self, closed_callback): self.mon.trace(self, '') self.closed_callback = closed_callback self.mon.log(self, ">close received from show Id: " + str(self.show_id)) self.driver_close() self.play_state = 'closed' # PP does not use close callback but it does read self.play_state def input_pressed(self, symbol): self.mon.trace(self, symbol) if symbol == 'pause': self.pause() elif symbol == 'pause-on': self.pause_on() elif symbol == 'pause-off': self.pause_off() elif symbol == 'stop': self.stop() # browsers do not do pause def pause(self): self.mon.log(self, "!<pause rejected") return False # browsers do not do pause def pause_on(self): self.mon.log(self, "!<pause on rejected") return False # browsers do not do pause def pause_off(self): self.mon.log(self, "!<pause off 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 # *********************** # veneer for controlling chromium browser # *********************** def driver_open(self): tries = 4 while tries > 0: try: self.driver = webdriver.Chrome(options=self.chrome_options) return except Exception as e: #print ("Failed to open Chromium", e, e.__class__,tries) tries -= 1 def driver_close(self): try: self.driver.quit() except WebDriverException as e: self.mon.warn(self, 'Browser Closed in Close !!!!!!\n' + str(e)) except Exception as e: print("Oops!", e, e.__class__, "occurred.") else: return def driver_refresh(self): try: self.driver.refresh() except WebDriverException as e: self.mon.warn(self, 'Browser Closed in Refresh !!!!!!\n' + str(e)) except Exception as e: print("Oops!", e, e.__class__, "occurred.") else: return def driver_get(self, url): self.mon.log(self, 'get: ' + url) try: self.driver.get(url) except WebDriverException as e: self.mon.warn(self, 'Browser Closed in Get !!!!!!\n' + str(e)) except Exception as e: print("Oops!", e, e.__class__, "occurred.") else: return # ******************* # browser commands # *********************** def parse_commands(self, command_text): self.command_list = [] self.max_loops = -1 #loop continuous if no loop command lines = command_text.split('\n') for line in lines: if line.strip() == '': continue #print (line) reason, entry = self.parse_command(line) if reason != 'normal': return 'error', entry self.command_list.append(copy.deepcopy(entry)) num_loops = 0 for entry in self.command_list: if entry[0] == 'loop': num_loops += 1 if num_loops > 1: return 'error', str( num_loops) + ' loop commands in browser commands' return 'normal', '' def parse_command(self, line): fields = line.split() #print (fields) 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', 'loop'): return 'error', 'unknown browser command: ' + line if command in ('refresh', ) and len(fields) != 1: return 'error', 'incorrect number of fields for ' + command + 'in: ' + line if command in ('refresh', ): return 'normal', [command, ''] if command == 'load': if len(fields) != 2: return 'error', 'incorrect number of fields for ' + command + 'in: ' + line arg = fields[1] track = self.complete_path(arg) # does media exist if not ':' in track: if not os.path.exists(track): return 'error', 'cannot find file: ' + track # add file:// to files. if ':' in track: url = track else: url = 'file://' + track return 'normal', [command, url] if command == 'loop': if len(fields) == 1: arg = '-1' self.max_loops = -1 #loop continuously if no argument return 'normal', [command, arg] elif len(fields) == 2: if not fields[1].isdigit() or fields[1] == '0': return 'error', 'Argument for Loop is not a positive number in: ' + line else: arg = fields[1] self.max_loops = int(arg) return 'normal', [command, arg] else: return 'error', 'incorrect number of fields for ' + command + 'in: ' + line 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 else: return 'normal', [command, arg] def play_commands(self): # init if len(self.command_list) == 0: return self.loop_index = -1 # -1 no loop comand found self.loop_count = 0 self.command_index = 0 self.next_command_index = 0 #start at beginning #loop round executing the commands self.canvas.after(100, self.execute_command) def execute_command(self): self.command_index = self.next_command_index if self.command_index == len(self.command_list): # past end of command list self.quit_signal = True return if self.command_index == len( self.command_list) - 1 and self.loop_index != -1: # last in list and need to loop self.next_command_index = self.loop_index else: self.next_command_index = self.command_index + 1 entry = self.command_list[self.command_index] command = entry[0] arg = entry[1] self.mon.log( self, str(self.command_index) + ' Do ' + command + ' ' + arg + ' Next: ' + str(self.next_command_index)) # and execute command if command == 'load': self.driver_get(arg) self.command_timer = self.canvas.after(10, self.execute_command) elif command == 'refresh': self.driver_refresh() 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 == 'loop': if self.loop_index == -1: # found loop for first time self.loop_index = self.command_index self.loop_count = 0 self.mon.log( self, 'Loop init To: ' + str(self.loop_index) + ' Count: ' + str(self.loop_count)) self.command_timer = self.canvas.after(10, self.execute_command) else: self.loop_count += 1 self.mon.log( self, 'Inc loop count: ' + ' Count: ' + str(self.loop_count)) # hit loop command after the requied number of loops if self.loop_count == self.max_loops: #max loops is -1 for continuous self.mon.log( self, 'end of loop: ' + ' Count: ' + str(self.loop_count)) self.quit_signal = True return else: self.mon.log( self, 'Looping to: ' + str(self.loop_index) + ' Count: ' + str(self.loop_count)) self.command_timer = self.canvas.after( 10, self.execute_command) elif command == 'exit': self.quit_signal = True return def process_chrome_options(self): self.chrome_options = Options() #self.add_option("--incognito") self.add_option("--noerrdialogs") self.add_option("--disable-infobars") self.add_option("--check-for-update-interval=31536000") self.add_option('--disable-overlay-scrollbar') self.chrome_options.add_experimental_option("excludeSwitches", ['enable-automation']) try: self.zoom = float(self.chrome_zoom_text) except ValueError: return 'error', 'Chrome Zoom is not a number' + self.chrome_zoom_text self.add_option('--force-device-scale-factor=' + self.chrome_zoom_text) status, message = self.process_chrome_window(self.chrome_window_text) if status == 'error': return 'error', message status, message = self.add_other_options() if status == 'error': return 'error', message return 'normal', '' def add_other_options(self): opts_list = self.chrome_other_options.split(' ') #print (opts_list) for opt in opts_list: if opt == '': continue if opt[0:2] != '--': return 'error', 'option is not preceded by -- : ' + opt else: self.add_option(opt) return 'normal', '' def add_option(self, option): #print ('Adding Option: ',option) self.chrome_options.add_argument(option) def process_chrome_window(self, line): #parse chrome window # kiosk,fullscreen,showcanvas,display # obxprop | grep "^_OB_APP" and click the window self.app_mode = False # showcanvas|display + [x+y+w*h] words = line.split() if len(words) not in (1, 2): return 'error', 'bad Chrome Web Window form ' + line if words[0] not in ('display', 'showcanvas', 'kiosk', 'fullscreen'): return 'error', 'No or invalid Chrome Web Window mode: ' + line if len(words) == 1 and words[0] == 'kiosk': self.add_option('--kiosk') x_org, y_org = self.dm.real_display_position(self.display_id) self.add_option('--window-position=' + str(x_org) + ',' + str(y_org)) return 'normal', '' if len(words) == 1 and words[0] == 'fullscreen': self.add_option('--start-fullscreen') x_org, y_org = self.dm.real_display_position(self.display_id) self.add_option('--window-position=' + str(x_org) + ',' + str(y_org)) return 'normal', '' # display or showcanvas with or without dimensions self.app_mode = True if words[0] == 'display': x_org, y_org = self.dm.real_display_position(self.display_id) width, height = self.dm.real_display_dimensions(self.display_id) if words[0] == 'showcanvas': x_org, y_org = self.dm.real_display_position(self.display_id) x_org += self.show_canvas_x1 y_org += self.show_canvas_y1 width = self.show_canvas_width height = self.show_canvas_height x_offset = 0 y_offset = 0 #calc offset and width/height from dimensions if len(words) > 1: status, message, x_offset, y_offset, width, height = self.parse_dimensions( words[1], width, height) if status == 'error': return 'error', message #correct for zoom width = int(width / self.zoom) height = int(height / self.zoom) x = x_org + x_offset y = y_org + y_offset self.chrome_window_x = x self.chrome_window_y = y self.chrome_window_width = width self.chrome_window_height = height #print ('app',self.app_mode,x,y,width,height) self.add_option('--app=' + self.current_url) self.add_option('--window-size=' + str(width) + ',' + str(height)) self.add_option('--window-position=' + str(x) + ',' + str(y)) return 'normal', '' def parse_dimensions(self, dim_text, show_width, show_height): if '+' in dim_text: # parse x+y+width*height fields = dim_text.split('+') if len(fields) != 3: return 'error', 'bad chrome window form ' + dim_text, 0, 0, 0, 0 if not fields[0].isdigit(): return 'error', 'x is not a positive decimal in chrome web window ' + dim_text, 0, 0, 0, 0 else: x = int(fields[0]) if not fields[1].isdigit(): return 'error', 'y is not a positive decimal in chrome webwindow ' + dim_text, 0, 0, 0, 0 else: y = int(fields[1]) dimensions = fields[2].split('*') if len(dimensions) != 2: return 'error', 'bad chrome web window dimensions ' + dim_text, '', 0, 0, 0, 0 if not dimensions[0].isdigit(): return 'error', 'width is not a positive decimal in chrome web window ' + dim_text, 0, 0, 0, 0 else: width = int(dimensions[0]) if not dimensions[1].isdigit(): return 'error', 'height is not a positive decimal in chrome web window ' + dim_text, 0, 0, 0, 0 else: height = int(dimensions[1]) return 'normal', '', x, y, width, height else: #width*height dimensions = dim_text.split('*') if len(dimensions) != 2: return 'error', 'bad chrome web window dimensions ' + line, '', 0, 0, 0, 0 if not dimensions[0].isdigit(): return 'error', 'width is not a positive decimal in chrome web window ' + line, '', 0, 0, 0, 0 else: window_width = int(dimensions[0]) if not dimensions[1].isdigit(): return 'error', 'height is not a positive decimal in chrome web window ' + line, '', 0, 0, 0, 0 else: window_height = int(dimensions[1]) x = int((show_width - window_width) / 2) y = int((show_height - window_height) / 2) return 'normal', '', x, y, window_width, window_height