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

    # 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

    # 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

        if os.geteuid() != 0:
            self.mon.err(self, "GPIO requires Pi Presents to be run as root\nhint: sudo pipresents.py .... ")
            return "error", "GPIO without sudo"

        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 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":
                # 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":
                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
Пример #2
0
class ShowManager(object):
    """
    ShowManager manages PiPresents' concurrent shows. It does not manage sub-shows or child-shows but has a bit of common code to initilise them
    concurrent shows are always top level (level 0) shows:
    They can be opened/closed  by the start show(open only) or by 'open/close myshow' in the Show Control field of players, by time of day sceduler or by OSC
    
   Two shows with the same show reference cannot be run concurrently as there is no way to reference an individual instance.
   However a workaround is to make the secong instance a subshow of a mediashow with a different reference.

    """

    # Declare class variables
    shows = []
    canvas = None  #canvas for all 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
    showlist = []

    # Initialise class variables, first time through only in pipresents.py

    def init(self, canvas, all_shows_ended_callback, command_callback,
             showlist):
        ShowManager.all_shows_ended_callback = all_shows_ended_callback
        ShowManager.shows = []
        ShowManager.shutdown_required = False
        ShowManager.canvas = canvas
        ShowManager.command_callback = command_callback
        ShowManager.showlist = showlist

# **************************************
# functions to manipulate show register
# **************************************

    def register_shows(self):
        for show in ShowManager.showlist.shows():
            if show['show-ref'] != 'start':
                reason, message = self.register_show(show['show-ref'])
                if reason == 'error':
                    return reason, message
        return 'normal', 'shows regiistered'

    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
            self.mon.trace(
                self, ' - register show: show_ref = ' + ref + ' index = ' +
                str(index))
            return 'normal', 'show registered'
        else:
            # self.mon.err(self, ' more than one show in showlist with show-ref: ' + ref )
            return 'error', ' more than one show in showlist with show-ref: ' + ref

    # 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
        self.mon.trace(
            self,
            'show_ref= ' + ShowManager.shows[index][ShowManager.SHOW_REF] +
            ' show_id= ' + str(index))

    # is the show running?
    def show_running(self, index):
        if ShowManager.shows[index][ShowManager.SHOW_OBJ] is not None:
            return ShowManager.shows[index][ShowManager.SHOW_OBJ]
        else:
            return None

    def set_exited(self, index):
        ShowManager.shows[index][ShowManager.SHOW_OBJ] = None
        self.mon.trace(
            self,
            'show_ref= ' + ShowManager.shows[index][ShowManager.SHOW_REF] +
            ' show_id= ' + str(index))

    # are all shows exited?
    def all_shows_exited(self):
        all_exited = True
        for show in ShowManager.shows:
            if show[ShowManager.SHOW_OBJ] is not None:
                all_exited = False
        return all_exited

    # fromat for printing
    def pretty_shows(self):
        shows = '\n'
        for show in ShowManager.shows:
            shows += show[ShowManager.SHOW_REF] + '\n'
        return shows


# *********************************
# show control
# *********************************

# show manager can be initialised by a player, shower or by pipresents.py
# if by pipresents.py then show_id=-1

    def __init__(self, show_id, showlist, show_params, root, canvas, pp_dir,
                 pp_profile, pp_home):
        self.show_id = show_id
        self.showlist = showlist
        self.show_params = show_params
        self.root = root
        self.show_canvas = canvas
        self.pp_dir = pp_dir
        self.pp_profile = pp_profile
        self.pp_home = pp_home

        self.mon = Monitor()

    def control_a_show(self, show_ref, show_command):
        if show_command == 'open':
            return self.start_show(show_ref)
        elif show_command == 'close':
            return self.exit_show(show_ref)
        elif show_command == 'closeall':
            return self.exit_all_shows()
        elif show_command == 'openexclusive':
            return self.open_exclusive(show_ref)
        else:
            return 'error', 'command not recognised ' + show_command

    def open_exclusive(self, show_ref):
        self.exit_all_shows()
        self.exclusive_show = show_ref
        self.wait_for_openexclusive()
        return 'normal', 'opened exclusive'

    def wait_for_openexclusive(self):
        if self.all_shows_exited() is False:
            ShowManager.canvas.after(1, self.wait_for_openexclusive)
            return
        self.start_show(self.exclusive_show)

    def exit_all_shows(self):
        for show in ShowManager.shows:
            self.exit_show(show[ShowManager.SHOW_REF])
        return 'normal', 'exited all shows'

    # kick off the exit sequence of a show by calling the shows exit method.
    # it will result in all the shows in a stack being closed and end_play_show being called
    def exit_show(self, show_ref):
        index = self.show_registered(show_ref)
        self.mon.log(
            self,
            "De-registering show " + show_ref + ' show index:' + str(index))
        show_obj = self.show_running(index)
        if show_obj is not None:
            self.mon.log(
                self, "Exiting show " + show_ref + ' show index:' + str(index))
            show_obj.exit()
        return 'normal', 'exited a concurrent show'

    def start_show(self, show_ref):
        index = self.show_registered(show_ref)
        if index < 0:
            return 'error', "Show not found in showlist: " + show_ref
        show_index = self.showlist.index_of_show(show_ref)
        show = self.showlist.show(show_index)
        reason, message, show_canvas = self.compute_show_canvas(show)
        if reason == 'error':
            return reason, message
        # print 'STARTING TOP LEVEL SHOW',show_canvas
        self.mon.sched(
            self, TimeOfDay.now, 'Starting Show: ' + show_ref +
            ' from show: ' + self.show_params['show-ref'])
        self.mon.log(
            self, 'Starting Show: ' + show_ref + ' from: ' +
            self.show_params['show-ref'])
        if self.show_running(index):
            self.mon.sched(
                self, TimeOfDay.now,
                "show already running so ignoring command: " + show_ref)
            self.mon.warn(
                self, "show already running so ignoring command: " + show_ref)
            return 'normal', 'this concurrent show already running'
        show_obj = self.init_show(index, show, show_canvas)
        if show_obj is None:
            return 'error', "unknown show type in start concurrent show - " + show[
                'type']
        else:
            self.set_running(index, show_obj)
            # params - end_callback, show_ready_callback, parent_kickback_signal, level
            show_obj.play(self._end_play_show, None, False, 0, [])
            return 'normal', 'concurrent show started'

    # used by shows to create subshows or child shows
    def init_subshow(self, show_id, show, show_canvas):
        return self.init_show(show_id, show, show_canvas)

    def _end_play_show(self, index, reason, message):
        show_ref_to_exit = ShowManager.shows[index][ShowManager.SHOW_REF]
        show_to_exit = ShowManager.shows[index][ShowManager.SHOW_OBJ]
        self.mon.sched(self, TimeOfDay.now, 'Closed show: ' + show_ref_to_exit)
        self.mon.log(
            self, 'Exited from show: ' + show_ref_to_exit + ' ' + str(index))
        self.mon.log(self, 'Exited with Reason = ' + reason)
        self.mon.trace(
            self,
            ' Show is: ' + show_ref_to_exit + ' show index ' + str(index))
        # closes the video/audio from last track then closes the track
        # print 'show to exit ',show_to_exit, show_to_exit.current_player,show_to_exit.previous_player
        self.set_exited(index)
        if self.all_shows_exited() is True:
            ShowManager.all_shows_ended_callback(reason, message)
        return reason, message

    # common function to initilaise the show by type
    def init_show(
        self,
        show_id,
        selected_show,
        show_canvas,
    ):
        if selected_show['type'] == "mediashow":
            return MediaShow(show_id, selected_show, self.root, show_canvas,
                             self.showlist, self.pp_dir, self.pp_home,
                             self.pp_profile, ShowManager.command_callback)

        elif selected_show['type'] == "liveshow":
            return LiveShow(show_id, selected_show, self.root, show_canvas,
                            self.showlist, self.pp_dir, self.pp_home,
                            self.pp_profile, ShowManager.command_callback)

        elif selected_show['type'] == "radiobuttonshow":
            return RadioButtonShow(show_id, selected_show, self.root,
                                   show_canvas, self.showlist, self.pp_dir,
                                   self.pp_home, self.pp_profile,
                                   ShowManager.command_callback)

        elif selected_show['type'] == "hyperlinkshow":
            return HyperlinkShow(show_id, selected_show, self.root,
                                 show_canvas, self.showlist, self.pp_dir,
                                 self.pp_home, self.pp_profile,
                                 ShowManager.command_callback)

        elif selected_show['type'] == "menu":
            return MenuShow(show_id, selected_show, self.root, show_canvas,
                            self.showlist, self.pp_dir, self.pp_home,
                            self.pp_profile, ShowManager.command_callback)

        elif selected_show['type'] == "artmediashow":
            return ArtMediaShow(show_id, selected_show, self.root, show_canvas,
                                self.showlist, self.pp_dir, self.pp_home,
                                self.pp_profile, ShowManager.command_callback)

        elif selected_show['type'] == "artliveshow":
            return ArtLiveShow(show_id, selected_show, self.root, show_canvas,
                               self.showlist, self.pp_dir, self.pp_home,
                               self.pp_profile, ShowManager.command_callback)
        else:
            return None

    def compute_show_canvas(self, show_params):
        canvas = {}
        canvas['canvas-obj'] = ShowManager.canvas
        status, message, self.show_canvas_x1, self.show_canvas_y1, self.show_canvas_x2, self.show_canvas_y2 = self.parse_show_canvas(
            show_params['show-canvas'])
        if status == 'error':
            # self.mon.err(self,'show canvas error: ' + message + ' in ' + show_params['show-canvas'])
            return 'error', 'show canvas error: ' + message + ' in ' + show_params[
                'show-canvas'], canvas
        else:
            self.show_canvas_width = self.show_canvas_x2 - self.show_canvas_x1
            self.show_canvas_height = self.show_canvas_y2 - self.show_canvas_y1
            self.show_canvas_centre_x = self.show_canvas_width / 2
            self.show_canvas_centre_y = self.show_canvas_height / 2
            canvas['show-canvas-x1'] = self.show_canvas_x1
            canvas['show-canvas-y1'] = self.show_canvas_y1
            canvas['show-canvas-x2'] = self.show_canvas_x2
            canvas['show-canvas-y2'] = self.show_canvas_y2
            canvas['show-canvas-width'] = self.show_canvas_width
            canvas['show-canvas-height'] = self.show_canvas_height
            canvas['show-canvas-centre-x'] = self.show_canvas_centre_x
            canvas['show-canvas-centre-y'] = self.show_canvas_centre_y
            return 'normal', '', canvas

    def parse_show_canvas(self, text):
        fields = text.split()
        # blank so show canvas is the whole screen
        if len(fields) < 1:
            return 'normal', '', 0, 0, int(self.canvas['width']), int(
                self.canvas['height'])

        elif len(fields) in (1, 4):
            # window is specified
            status, message, x1, y1, x2, y2 = parse_rectangle(text)
            if status == 'error':
                return 'error', message, 0, 0, 0, 0
            else:
                return 'normal', '', x1, y1, x2, y2
        else:
            # error
            return 'error', 'Wrong number of fields in Show canvas: ' + text, 0, 0, 0, 0
Пример #3
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)    
Пример #4
0
class pp_gpiodriver(object):
    """
    pp_gpiodriver provides GPIO facilties for Pi presents
     - configures and binds GPIO pins from data in .cfg file 
     - reads and debounces inputs pins, provides callbacks on state changes which generate input events
    - changes the state 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  (pp_gpiodriver.)
    pins=[]
    driver_active=False
    title=''
    


    # executed by main program and by each object using gpio
    def __init__(self):
        self.mon=Monitor()



     # executed once from main program   
    def init(self,filename,filepath,widget,button_callback=None):
        
        # instantiate arguments
        self.widget=widget
        self.filename=filename
        self.filepath=filepath
        self.button_callback=button_callback
        pp_gpiodriver.driver_active = False

        # read gpio.cfg file.
        reason,message=self._read(self.filename,self.filepath)
        if reason =='error':
            return 'error',message
        if self.config.has_section('DRIVER') is False:
            return 'error','No DRIVER section in '+self.filepath
        
        #read information from DRIVER section
        pp_gpiodriver.title=self.config.get('DRIVER','title')
        button_tick_text = self.config.get('DRIVER','tick-interval')
        if button_tick_text.isdigit():
            if int(button_tick_text)>0:
                self.button_tick=int(button_tick_text)  # in mS
            else:
                return 'error','tick-interval is not a positive integer'
        else:
            return 'error','tick-interval is not an integer'            

        import RPi.GPIO as GPIO
        self.GPIO = GPIO
        
        # construct the GPIO control list from the configuration
        for index, pin_def in enumerate(pp_gpiodriver.PINLIST):
            pin=copy.deepcopy(pp_gpiodriver.TEMPLATE)
            pin_bits = pin_def.split('-')
            pin_num=pin_bits[1:]
            pin[pp_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[pp_gpiodriver.DIRECTION]='None'            
            else:
                # unused pin
                if self.config.get(pin_def,'direction') == 'none':
                    pin[pp_gpiodriver.DIRECTION]='none'
                else:
                    pin[pp_gpiodriver.DIRECTION]=self.config.get(pin_def,'direction')
                    if pin[pp_gpiodriver.DIRECTION] == 'in':
                        # input pin
                        pin[pp_gpiodriver.RISING_NAME]=self.config.get(pin_def,'rising-name')
                        pin[pp_gpiodriver.FALLING_NAME]=self.config.get(pin_def,'falling-name')
                        pin[pp_gpiodriver.ONE_NAME]=self.config.get(pin_def,'one-name')
                        pin[pp_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[pp_gpiodriver.LINKED_NAME]=self.config.get(pin_def,'linked-output')
                            if  self.config.get(pin_def,'linked-invert') == 'yes':
                                pin[pp_gpiodriver.LINKED_INVERT]=True
                            else:
                                pin[pp_gpiodriver.LINKED_INVERT]=False
                        else:
                            pin[pp_gpiodriver.LINKED_NAME]= ''
                            pin[pp_gpiodriver.LINKED_INVERT]=False
                                               
                        if pin[pp_gpiodriver.FALLING_NAME] == 'pp-shutdown':
                            pp_gpiodriver.shutdown_index=index
                        if self.config.get(pin_def,'repeat') != '':
                            pin[pp_gpiodriver.REPEAT]=int(self.config.get(pin_def,'repeat'))
                        else:
                            pin[pp_gpiodriver.REPEAT]=-1
                        pin[pp_gpiodriver.THRESHOLD]=int(self.config.get(pin_def,'threshold'))
                        
                        if self.config.get(pin_def,'pull-up-down') == 'up':
                            pin[pp_gpiodriver.PULL]=GPIO.PUD_UP
                        elif self.config.get(pin_def,'pull-up-down') == 'down':
                            pin[pp_gpiodriver.PULL]=GPIO.PUD_DOWN
                        else:
                            pin[pp_gpiodriver.PULL]=GPIO.PUD_OFF
                    else:
                        # output pin
                        pin[pp_gpiodriver.NAME]=self.config.get(pin_def,'name')
            
            pp_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(pp_gpiodriver.pins):
            num = pin[pp_gpiodriver.PIN]
            if pin[pp_gpiodriver.DIRECTION] == 'in':
                self.GPIO.setup(num,self.GPIO.IN,pull_up_down=pin[pp_gpiodriver.PULL])
            elif  pin[pp_gpiodriver.DIRECTION] == 'out':
                self.GPIO.setup(num,self.GPIO.OUT)
                self.GPIO.setup(num,False)
        self._reset_input_state()
        
        pp_gpiodriver.driver_active=True

        # init timer
        self.button_tick_timer=None
        return 'normal',pp_gpiodriver.title + ' active'

    # called by main program only         
    def start(self):
        # loop to look at the buttons
        self._do_buttons()
        self.button_tick_timer=self.widget.after(self.button_tick,self.start)


    # called by main program only                
    def terminate(self):
        if pp_gpiodriver.driver_active is True:
            if self.button_tick_timer is not None:
                self.widget.after_cancel(self.button_tick_timer)
            self._reset_outputs()
            self.GPIO.cleanup()
            pp_gpiodriver.driver_active=False


# ************************************************
# gpio input functions
# called by main program only
# ************************************************
    
    def _reset_input_state(self):
        for pin in pp_gpiodriver.pins:
            pin[pp_gpiodriver.COUNT]=0
            pin[pp_gpiodriver.PRESSED]=False
            pin[pp_gpiodriver.LAST]=False
            pin[pp_gpiodriver.REPEAT_COUNT]=pin[pp_gpiodriver.REPEAT]


    def _do_buttons(self):
        for index, pin in enumerate(pp_gpiodriver.pins):
            if pin[pp_gpiodriver.DIRECTION] == 'in':

                # linked pin
                if pin[pp_gpiodriver.LINKED_NAME] != '':
                    link_pin=self._output_pin_of(pin[pp_gpiodriver.LINKED_NAME])
                    if link_pin!=-1:
                        self.GPIO.output(link_pin,self.GPIO.input(pin[pp_gpiodriver.PIN]) ^ pin[pp_gpiodriver.LINKED_INVERT])
                    
                # debounce
                if self.GPIO.input(pin[pp_gpiodriver.PIN]) == 0:
                    if pin[pp_gpiodriver.COUNT]<pin[pp_gpiodriver.THRESHOLD]:
                        pin[pp_gpiodriver.COUNT]+=1
                        if pin[pp_gpiodriver.COUNT] == pin[pp_gpiodriver.THRESHOLD]:
                            pin[pp_gpiodriver.PRESSED]=True
                else: # input us 1
                    if pin[pp_gpiodriver.COUNT]>0:
                        pin[pp_gpiodriver.COUNT]-=1
                        if pin[pp_gpiodriver.COUNT] == 0:
                            pin[pp_gpiodriver.PRESSED]=False
     
                # detect edges
                # falling edge
                if pin[pp_gpiodriver.PRESSED] is True and pin[pp_gpiodriver.LAST] is False:
                    pin[pp_gpiodriver.LAST]=pin[pp_gpiodriver.PRESSED]
                    pin[pp_gpiodriver.REPEAT_COUNT]=pin[pp_gpiodriver.REPEAT]
                    if  pin[pp_gpiodriver.FALLING_NAME] != '' and self.button_callback  is not  None:
                        self.button_callback(pin[pp_gpiodriver.FALLING_NAME],pp_gpiodriver.title)
               # rising edge
                if pin[pp_gpiodriver.PRESSED] is False and pin[pp_gpiodriver.LAST] is True:
                    pin[pp_gpiodriver.LAST]=pin[pp_gpiodriver.PRESSED]
                    pin[pp_gpiodriver.REPEAT_COUNT]=pin[pp_gpiodriver.REPEAT]
                    if  pin[pp_gpiodriver.RISING_NAME] != '' and self.button_callback  is not  None:
                        self.button_callback(pin[pp_gpiodriver.RISING_NAME],pp_gpiodriver.title)

                # do state callbacks
                if pin[pp_gpiodriver.REPEAT_COUNT] == 0:
                    if pin[pp_gpiodriver.ZERO_NAME] != '' and pin[pp_gpiodriver.PRESSED] is True and self.button_callback is not None:
                        self.button_callback(pin[pp_gpiodriver.ZERO_NAME],pp_gpiodriver.title)
                    if pin[pp_gpiodriver.ONE_NAME] != '' and pin[pp_gpiodriver.PRESSED] is False and self.button_callback is not None:
                        self.button_callback(pin[pp_gpiodriver.ONE_NAME],pp_gpiodriver.title)
                    pin[pp_gpiodriver.REPEAT_COUNT]=pin[pp_gpiodriver.REPEAT]
                else:
                    if pin[pp_gpiodriver.REPEAT] != -1:
                        pin[pp_gpiodriver.REPEAT_COUNT]-=1

    def get_input(self,channel):
            return False, None



# ************************************************
# gpio output interface methods
# these can be called from many classes so need to operate on class variables
# ************************************************                            

    # execute an output event

    def handle_output_event(self,name,param_type,param_values,req_time):
        # print 'GPIO handle',name,param_type,param_values
        # does the symbolic name match any output pin
        pin= self._output_pin_of(name)
        if pin  == -1:
            return 'normal',pp_gpiodriver.title + 'Symbolic name not recognised: ' + name
        
        #gpio only handles state parameters, ignore otherwise
        if param_type != 'state':
            return 'normal',pp_gpiodriver.title + ' does not handle: ' + param_type
        
        to_state=param_values[0]
        if to_state not in ('on','off'):
            return 'error',pp_gpiodriver.title + ', illegal parameter value for ' + param_type +': ' + to_state

        if to_state== 'on':
            state=True
        else:
            state=False
            
        # print 'pin P1-'+ str(pin)+ ' set  '+ str(state) + ' required: ' + str(req_time)+ ' actual: ' + str(long(time.time()))
        self.GPIO.output(pin,state)
        return 'normal',pp_gpiodriver.title + ' pin P1-'+ str(pin)+ ' set  '+ str(state) + ' required at: ' + str(req_time)+ ' sent at: ' + str(long(time.time()))


    def _reset_outputs(self):
        if pp_gpiodriver.driver_active is True:
            for index, pin in enumerate(pp_gpiodriver.pins):
                num = pin[pp_gpiodriver.PIN]
                if pin[pp_gpiodriver.DIRECTION] == 'out':
                    self.GPIO.output(num,False)


    def is_active(self):
        return pp_gpiodriver.driver_active

# ************************************************
# 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 pp_gpiodriver.pins:
            # print " in list" + pin[pp_gpiodriver.NAME] + str(pin[pp_gpiodriver.PIN] )
            if pin[pp_gpiodriver.NAME] == name and pin[pp_gpiodriver.DIRECTION] == 'out':
                # print " linked pin " + pin[pp_gpiodriver.NAME] + ' ' + str(pin[pp_gpiodriver.PIN] )
                return pin[pp_gpiodriver.PIN]
        return -1



# ***********************************
# reading .cfg file
# ************************************

    def _read(self,filename,filepath):
        if os.path.exists(filepath):
            self.config = ConfigParser.ConfigParser()
            self.config.read(filepath)
            return 'normal',filename+' read'
        else:
            return 'error',filename+' not found at: '+filepath
Пример #5
0
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()
Пример #6
0
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
Пример #7
0
class ScreenDriver(object):
    canvas = None  ##The Pi presents canvas, click areas are draawn on this, not individual show canvases.
    image_obj=[]
    
    def __init__(self):
        self.mon=Monitor()


    # 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()
        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)

    
    # 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,canvas,callback):
        ScreenDriver.canvas=canvas
        self.callback=callback
        reason=''
        ScreenDriver.image_obj=[]
        for area in self.click_areas():
            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

            
            ScreenDriver.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))                 
                    self.photo_image_id=ImageTk.PhotoImage(self.pil_image)
                    image_id=self.canvas.create_image(polygon_centre_x,polygon_centre_y,
                                             image=self.photo_image_id,
                                             anchor=CENTER,
                                            tags=('pp-click-area',self.get(area,'name')),
                                            state='hidden')
                    del self.pil_image
                    ScreenDriver.image_obj.append(self.photo_image_id)

            # write the label at the centroid
            if self.get(area,'text') != '':
                ScreenDriver.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')
                
            ScreenDriver.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):
        overlapping =  event.widget.find_overlapping(event.x-5,event.y-5,event.x+5,event.y+5)
        for item in overlapping:
            # print ScreenDriver.canvas.gettags(item)
            if ('pp-click-area' in ScreenDriver.canvas.gettags(item)) and ScreenDriver.canvas.itemcget(item,'state') == 'normal':
                self.mon.log(self, "Click on screen: "+ ScreenDriver.canvas.gettags(item)[1])
                self.callback(ScreenDriver.canvas.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):
        click_areas=ScreenDriver.canvas.find_withtag('pp-click-area')
        for area in click_areas:        
            if test_area in ScreenDriver.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):
        for link in links:
            if self.is_click_area(link[0]) and link[1] != 'null':
                # print 'enabling link ',link[0]
                ScreenDriver.canvas.itemconfig(link[0],state='normal')


    def hide_click_areas(self,links):
        # hide  click areas
        for link in links:
            if self.is_click_area(link[0]) and link[1] != 'null':
                # print 'disabling link ',link[0]
                ScreenDriver.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')
        ScreenDriver.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   
Пример #8
0
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
Пример #9
0
class ShowManager(object):
    """
    ShowManager manages PiPresents' concurrent shows. It does not manage sub-shows or child-shows but has a bit of common code to initilise them
    concurrent shows are always top level (level 0) shows:
    They can be opened/closed  by the start show(open only) or by 'open/close myshow' in the Show Control field of players, by time of day sceduler or by OSC
    
   Two shows with the same show reference cannot be run concurrently as there is no way to reference an individual instance.
   However a workaround is to make the secong instance a subshow of a mediashow with a different reference.

    """
    
    # Declare class variables
    shows=[]
    canvas=None    #canvas for all 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
    showlist=[]


    # Initialise class variables, first time through only in pipresents.py

    def init(self,canvas,all_shows_ended_callback,command_callback,showlist):
        ShowManager.all_shows_ended_callback=all_shows_ended_callback
        ShowManager.shows=[]
        ShowManager.shutdown_required=False
        ShowManager.canvas=canvas
        ShowManager.command_callback = command_callback
        ShowManager.showlist = showlist

# **************************************
# functions to manipulate show register
# **************************************

    def register_shows(self):
        for show in ShowManager.showlist.shows():
            if show['show-ref'] != 'start':
                reason,message=self.register_show(show['show-ref'])
                if reason =='error':
                    return reason,message
        return 'normal','shows regiistered'
            

    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
            self.mon.trace(self,' - register show: show_ref = ' + ref + ' index = ' + str(index))
            return'normal','show registered'
        else:
            # self.mon.err(self, ' more than one show in showlist with show-ref: ' + ref )
            return 'error', ' more than one show in showlist with show-ref: ' + ref 
        
    # 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
        self.mon.trace(self, 'show_ref= ' + ShowManager.shows[index][ShowManager.SHOW_REF] + ' show_id= ' + str(index))


    # is the show running?
    def show_running(self,index):
        if ShowManager.shows[index][ShowManager.SHOW_OBJ] is not None:
            return ShowManager.shows[index][ShowManager.SHOW_OBJ]
        else:
            return None

    def set_exited(self,index):
        ShowManager.shows[index][ShowManager.SHOW_OBJ]=None
        self.mon.trace(self,'show_ref= ' + ShowManager.shows[index][ShowManager.SHOW_REF] + ' show_id= ' + str(index))


    # are all shows exited?
    def all_shows_exited(self):
        all_exited=True
        for show in ShowManager.shows:
            if show[ShowManager.SHOW_OBJ] is not None:
                all_exited=False
        return all_exited


    # fromat for printing
    def pretty_shows(self):
        shows='\n'
        for show in ShowManager.shows:
            shows += show[ShowManager.SHOW_REF] +'\n'
        return shows
            
# *********************************
# show control
# *********************************

    # show manager can be initialised by a player, shower or by pipresents.py
    # if by pipresents.py then show_id=-1
    def __init__(self,show_id,showlist,show_params,root,canvas,pp_dir,pp_profile,pp_home):
        self.show_id=show_id
        self.showlist=showlist
        self.show_params=show_params
        self.root=root
        self.show_canvas=canvas
        self.pp_dir=pp_dir
        self.pp_profile=pp_profile
        self.pp_home=pp_home


        self.mon=Monitor()

    def control_a_show(self,show_ref,show_command):
        if show_command == 'open':
            return self.start_show(show_ref)
        elif show_command  == 'close':
            return self.exit_show(show_ref)
        else:
            return 'error','command not recognised '+ show_command


    def exit_all_shows(self):
        for show in ShowManager.shows:
            self.exit_show(show[ShowManager.SHOW_REF])
        return 'normal','exited all shows'


    # kick off the exit sequence of a show by calling the shows exit method.
    # it will result in all the shows in a stack being closed and end_play_show being called
    def exit_show(self,show_ref):
        index=self.show_registered(show_ref)
        self.mon.log(self,"Exiting show "+ show_ref + ' show index:' + str(index))
        show_obj=self.show_running(index)
        if show_obj is not None:
            show_obj.exit()
        return 'normal','exited a concurrent show'
            

    def start_show(self,show_ref):
        index=self.show_registered(show_ref)
        if index <0:
            return 'error',"Show not found in showlist: "+ show_ref
        show_index = self.showlist.index_of_show(show_ref)
        show=self.showlist.show(show_index)
        reason,message,show_canvas=self.compute_show_canvas(show)
        if reason == 'error':
            return reason,message
        # print 'STARTING TOP LEVEL SHOW',show_canvas
        self.mon.log(self,'Starting Show: ' + show_ref  + ' from: ' + self.show_params['show-ref'])
        if self.show_running(index):
            self.mon.warn(self,"show already running so ignoring command: "+show_ref)
            return 'normal','this concurrent show already running'
        show_obj = self.init_show(index,show,show_canvas)
        if show_obj is None:
            return 'error',"unknown show type in start concurrent show - "+ show['type']
        else:
            self.set_running(index,show_obj)
            # params - end_callback, show_ready_callback, parent_kickback_signal, level
            show_obj.play(self._end_play_show,None,False,0,[])
            return 'normal','concurrent show started'

 
    # used by shows to create subshows or child shows
    def init_subshow(self,show_id,show,show_canvas):
        return self.init_show(show_id,show,show_canvas)



    def _end_play_show(self,index,reason,message):
        show_ref_to_exit =ShowManager.shows[index][ShowManager.SHOW_REF]
        show_to_exit =ShowManager.shows[index][ShowManager.SHOW_OBJ]
        self.mon.log(self,'Exited from show: ' + show_ref_to_exit + ' ' + str(index) )
        self.mon.log(self,'Exited with Reason = ' + reason)
        self.mon.trace(self,' Show is: ' + show_ref_to_exit + ' show index ' + str(index))
        # closes the video/audio from last track then closes the track
        # print 'show to exit ',show_to_exit, show_to_exit.current_player,show_to_exit.previous_player
        self.set_exited(index)
        if self.all_shows_exited() is True:
            ShowManager.all_shows_ended_callback(reason,message)
        return reason,message


    # common function to initilaise the show by type
    def init_show(self,show_id,selected_show,show_canvas,):
        if selected_show['type'] == "mediashow":
            return MediaShow(show_id,
                             selected_show,
                             self.root,
                             show_canvas,
                             self.showlist,
                             self.pp_dir,
                             self.pp_home,
                             self.pp_profile,
                             ShowManager.command_callback)

        elif selected_show['type'] == "liveshow":    
            return LiveShow(show_id,
                            selected_show,
                            self.root,
                            show_canvas,
                            self.showlist,
                            self.pp_dir,
                            self.pp_home,
                            self.pp_profile,
                            ShowManager.command_callback)


        elif selected_show['type'] == "radiobuttonshow":
            return RadioButtonShow(show_id,
                                   selected_show,
                                   self.root,
                                   show_canvas,
                                   self.showlist,
                                   self.pp_dir,
                                   self.pp_home,
                                   self.pp_profile,
                                   ShowManager.command_callback)

        elif selected_show['type'] == "hyperlinkshow":
            return HyperlinkShow(show_id,
                                 selected_show,
                                 self.root,
                                 show_canvas,
                                 self.showlist,
                                 self.pp_dir,
                                 self.pp_home,
                                 self.pp_profile,
                                 ShowManager.command_callback)
        
        elif selected_show['type'] == "menu":
            return MenuShow(show_id,
                            selected_show,
                            self.root,
                            show_canvas,
                            self.showlist,
                            self.pp_dir,
                            self.pp_home,
                            self.pp_profile,
                            ShowManager.command_callback)
        
        elif selected_show['type'] == "artmediashow":    
            return ArtMediaShow(show_id,
                                selected_show,
                                self.root,
                                show_canvas,
                                self.showlist,
                                self.pp_dir,
                                self.pp_home,
                                self.pp_profile,
                                ShowManager.command_callback)
        
        elif selected_show['type'] == "artliveshow":    
            return ArtLiveShow(show_id,
                               selected_show,
                               self.root,
                               show_canvas,
                               self.showlist,
                               self.pp_dir,
                               self.pp_home,
                               self.pp_profile,
                               ShowManager.command_callback)
        else:
            return None


    def compute_show_canvas(self,show_params):
        canvas={}
        canvas['canvas-obj']= ShowManager.canvas
        status,message,self.show_canvas_x1,self.show_canvas_y1,self.show_canvas_x2,self.show_canvas_y2= self.parse_show_canvas(show_params['show-canvas'])
        if status  == 'error':
            # self.mon.err(self,'show canvas error: ' + message + ' in ' + show_params['show-canvas'])
            return 'error','show canvas error: ' + message + ' in ' + show_params['show-canvas'],canvas
        else:
            self.show_canvas_width = self.show_canvas_x2 - self.show_canvas_x1
            self.show_canvas_height=self.show_canvas_y2 - self.show_canvas_y1
            self.show_canvas_centre_x= self.show_canvas_width/2
            self.show_canvas_centre_y= self.show_canvas_height/2
            canvas['show-canvas-x1'] = self.show_canvas_x1
            canvas['show-canvas-y1'] = self.show_canvas_y1
            canvas['show-canvas-x2'] = self.show_canvas_x2
            canvas['show-canvas-y2'] = self.show_canvas_y2
            canvas['show-canvas-width']  = self.show_canvas_width
            canvas['show-canvas-height'] = self.show_canvas_height
            canvas['show-canvas-centre-x'] = self.show_canvas_centre_x 
            canvas['show-canvas-centre-y'] = self.show_canvas_centre_y
            return 'normal','',canvas



    def parse_show_canvas(self,text):
        fields = text.split()
        # blank so show canvas is the whole screen
        if len(fields) < 1:
            return 'normal','',0,0,int(self.canvas['width']),int(self.canvas['height'])
             
        elif len(fields) == 4:
            # window is specified
            if not (fields[0].isdigit() and fields[1].isdigit() and fields[2].isdigit() and fields[3].isdigit()):
                return 'error','coordinates are not positive integers',0,0,0,0
            return 'normal','',int(fields[0]),int(fields[1]),int(fields[2]),int(fields[3])
        else:
            # error
            return 'error','illegal Show canvas dimensions '+ text,0,0,0,0
Пример #10
0
class PiPresents(object):
    def __init__(self):
        gc.set_debug(gc.DEBUG_UNCOLLECTABLE | gc.DEBUG_INSTANCES
                     | gc.DEBUG_OBJECTS | gc.DEBUG_SAVEALL)
        self.pipresents_issue = "1.3"
        self.pipresents_minorissue = '1.3.1g'
        # 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
        self.pp_background = 'black'

        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:\n{0}".format(pp_dir))
            exit(103)

        # 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', 'pp_paths', 'HyperlinkShow', 'RadioButtonShow',
            'ArtLiveShow', 'ArtMediaShow', 'MediaShow', 'LiveShow', 'MenuShow',
            'PathManager', 'ControlsManager', 'ShowManager', 'PluginManager',
            'MplayerDriver', 'OMXDriver', 'UZBLDriver', 'KbdDriver',
            'GPIODriver', 'TimeOfDay', 'ScreenDriver', 'Animate', 'OSCDriver'
        ]

        # Monitor.classes=['PiPresents','ArtMediaShow','VideoPlayer','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.log(
            self,
            "Pi Presents is starting, Version:" + self.pipresents_minorissue)
        # self.mon.log (self," OS and separator:" + os.name +'  ' + os.sep)
        self.mon.log(self, "sys.path[0] -  location of code: " + sys.path[0])
        if os.geteuid() != 0:
            user = os.getenv('USER')
        else:
            user = os.getenv('SUDO_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

        # 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

        # get home path from -o option
        self.pp_home = pp_paths.get_home(self.options['home'])
        if self.pp_home is None:
            self.end('error', 'Failed to find pp_home')

        # get profile path from -p option
        # pp_profile is the full path to the directory that contains
        # pp_showlist.json and other files for the profile
        self.pp_profile = pp_paths.get_profile_dir(self.pp_home,
                                                   self.options['profile'])
        if self.pp_profile is None:
            self.end('error', 'Failed to find profile')

        # check profile exists
        if os.path.exists(self.pp_profile):
            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 profile')

        self.mon.start_stats(self.options['profile'])

        # check 'verify' option
        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')

        # 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')

        if self.starter_show['start-show'] == '':
            self.mon.warn(self, "No Start Shows in Start Show")

# ********************
# 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.pp_background)

        self.mon.log(
            self, 'native screen dimensions are ' +
            str(self.root.winfo_screenwidth()) + ' x ' +
            str(self.root.winfo_screenheight()) + ' pixcels')
        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, 'commanded screen dimensions are ' + str(self.screen_width) +
            ' x ' + str(self.screen_height) + ' pixcels')

        # 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 1>&- 2>&- &'
            )  # Suppress 'someone created a subwindow' complaints from 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')

        # canvs 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.pp_background)

        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 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.exitpipresents_required = False

        # 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 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.end('error', message)
            else:
                self.osc_enabled = True
                self.root.after(1000, self.oscdriver.start_server())

        # and run the start shows
        self.run_start_shows()

        # set up the time of day scheduler including catchup
        self.tod_enabled = False
        if os.path.exists(self.pp_profile + os.sep + 'schedule.json'):
            # kick off the time of day scheduler which may run additional shows
            self.tod = TimeOfDay()
            self.tod.init(pp_dir, self.pp_home, self.pp_profile, self.root,
                          self.handle_command)
            self.tod_enabled = True

        # then start the time of day scheduler
        if self.tod_enabled is True:
            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 --fullscreen comand option', 0, 0
        elif fields[0].isdigit() is False or fields[1].isdigit() is False:
            return 'error', 'dimensions are not positive integers in ---fullscreen', 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):
        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'):
            if self.shutdown_required is False:
                reason, message = self.show_manager.control_a_show(
                    show_ref, show_command)
            else:
                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 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")
        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.pp_background)
        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':
            self.mon.log(self, "Pi Presents Aborted, au revoir")
            # close logging files
            self.mon.finish()
            sys.exit(101)
        elif reason == 'error':
            self.mon.log(self, "Pi Presents closing because of error, sorry")
            # close logging files
            self.mon.finish()
            sys.exit(102)
        else:
            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', '-h', '-t 5', 'now'])
                sys.exit(100)
            else:
                sys.exit(100)

    # tidy up all the peripheral bits of Pi Presents
    def tidy_up(self):
        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()
Пример #11
0
class PPEditor(object):

    # ***************************************
    # INIT
    # ***************************************

    def __init__(self):
    
        self.editor_issue="1.3"

        # 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.init()
        
        Monitor.classes  = ['PPEditor','EditItem','Validator']

        Monitor.log_level = int(self.command_options['debug'])

        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()
        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 = StringVar()
        self.display_selected_track_title = StringVar()
        self.display_show = 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='Art Media Show', command = self.new_artmediashow_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='Art Live Show', command = self.new_artliveshow_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_menushow)
        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)
        stypemenu.add_command(label='ArtMediaShow', command = self.add_artmediashow)
        stypemenu.add_command(label='ArtLiveShow', command = self.add_artliveshow)
        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)
        medialistmenu.add_command(label='Copy To', command = self.copy_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 Track', command = self.new_menu_track)
        trackmenu.add_cascade(label='New', menu = typemenu)

        oscmenu = Menu(menubar, tearoff=0, bg="grey", fg="black")
        menubar.add_cascade(label='OSC', menu = oscmenu)
        oscmenu.add_command(label='Create OSC configuration', command = self.create_osc)
        oscmenu.add_command(label='Edit OSC Configuration', command = self.edit_osc)
        oscmenu.add_command(label='Delete OSC Configuration', command = self.delete_osc)


        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=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)

    
        # define display of medialists
        scrollbar = Scrollbar(medialists_frame, orient=VERTICAL)
        self.medialists_display = Listbox(medialists_frame, selectmode=SINGLE, height=12,
                                          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=VERTICAL)
        self.tracks_display = Listbox(tracks_frame, selectmode=SINGLE, height=25,
                                      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 and OSC config class
        self.options=Options(self.pp_dir) # creates options file in code directory if necessary
        self.osc_config=OSCConfig()
        
        # 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.pp_profiles_offset = self.options.pp_profiles_offset
        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,"Current Profiles Offset from options is "+self.pp_profiles_offset)
        self.mon.log(self,"Initial Media from options is "+self.initial_media_dir)
        self.pp_profile_dir=''
        self.osc_config_file = ''
        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 is 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)


    # **************
    # OSC CONFIGURATION
    # **************

    def create_osc(self):
        if self.pp_profile_dir=='':
            return
        if self.osc_config.read(self.osc_config_file) is False:
            iodir=self.pp_profile_dir+os.sep+'pp_io_config'
            if not os.path.exists(iodir):
                os.makedirs(iodir)
            self.osc_config.create(self.osc_config_file)

    def edit_osc(self):
        if self.osc_config.read(self.osc_config_file) is False:
            # print 'no config file'
            return
        osc_ut=OSCUnitType(self.root,self.osc_config.this_unit_type)
        self.req_unit_type=osc_ut.result
        if self.req_unit_type != None:
            # print self.req_unit_type
            eosc = OSCEditor(self.root, self.osc_config_file,self.req_unit_type,'Edit OSC Configuration')
            
    def delete_osc(self):
        if self.osc_config.read(self.osc_config_file) is False:
            return
        os.rename(self.osc_config_file,self.osc_config_file+'.bak')
        

    
    # **************
    # PROFILES
    # **************

    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("Editor for Pi Presents - "+ self.pp_profile_dir)
        if self.open_showlist(self.pp_profile_dir) is False:
            self.init()
            return
        self.open_medialists(self.pp_profile_dir)
        self.refresh_tracks_display()
        self.osc_config_file=self.pp_profile_dir+os.sep+'pp_io_config'+os.sep+'osc.cfg'


    def new_profile(self,profile):
        d = Edit1Dialog(self.root,"New Profile","Name", "")
        if d .result  is  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"+ self.pp_profiles_offset + os.sep + name
        if os.path.exists(to) is  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+os.sep+'pp_resources'+os.sep+'pp_templates'+os.sep + 'ppt_exhibit_1p3'
        self.new_profile(profile)

    def new_interactive_profile(self):
        profile = self.pp_dir+os.sep+'pp_resources'+os.sep+'pp_templates'+os.sep + 'ppt_interactive_1p3'
        self.new_profile(profile)

    def new_menu_profile(self):
        profile = self.pp_dir+os.sep+'pp_resources'+os.sep+'pp_templates'+os.sep + 'ppt_menu_1p3'
        self.new_profile(profile)

    def new_presentation_profile(self):
        profile = self.pp_dir+os.sep+'pp_resources'+os.sep+'pp_templates'+os.sep + 'ppt_presentation_1p3'
        self.new_profile(profile)

    def new_blank_profile(self):
        profile = self.pp_dir+os.sep+'pp_resources'+os.sep+'pp_templates'+os.sep +"ppt_blank_1p3"
        self.new_profile(profile)

    def new_mediashow_profile(self):
        profile = self.pp_dir+os.sep+'pp_resources'+os.sep+'pp_templates'+os.sep + 'ppt_mediashow_1p3'
        self.new_profile(profile)
        
    def new_liveshow_profile(self):
        profile = self.pp_dir+os.sep+'pp_resources'+os.sep+'pp_templates'+os.sep + 'ppt_liveshow_1p3'
        self.new_profile(profile)

    def new_artmediashow_profile(self):
        profile = self.pp_dir+os.sep+'pp_resources'+os.sep+'pp_templates'+os.sep + 'ppt_artmediashow_1p3'
        self.new_profile(profile)
        
    def new_artliveshow_profile(self):
        profile = self.pp_dir+os.sep+'pp_resources'+os.sep+'pp_templates'+os.sep + 'ppt_artliveshow_1p3'
        self.new_profile(profile)

    def new_radiobuttonshow_profile(self):
        profile = self.pp_dir+os.sep+'pp_resources'+os.sep+'pp_templates'+os.sep + 'ppt_radiobuttonshow_1p3'
        self.new_profile(profile)

    def new_hyperlinkshow_profile(self):
        profile = self.pp_dir+os.sep+'pp_resources'+os.sep+'pp_templates'+os.sep + 'ppt_hyperlinkshow_1p3'
        self.new_profile(profile)

    # ***************************************
    # Shows
    # ***************************************

    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) or  (self.command_options['forceupdate']  is  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,showlist_dir):
        if self.current_showlist is not None:
            showlist_file = showlist_dir + os.sep + "pp_showlist.json"
            self.current_showlist.save_list(showlist_file)
            
    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_artliveshow(self):
        self.add_show(PPdefinitions.new_shows['artliveshow'])

    def add_artmediashow(self):
        self.add_show(PPdefinitions.new_shows['artmediashow'])
        
    def add_menushow(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 is not None:
            d = Edit1Dialog(self.root,"AddShow","Show Reference", "")
            if d.result  is  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 is not 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 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.refresh_shows_display()

    def copy_show(self):
        if  self.current_showlist is not 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 is not 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  is  True:

                self.save_showlist(self.pp_profile_dir)
                self.refresh_shows_display()

 

    # ***************************************
    #   Medialists
    # ***************************************

    def open_medialists(self,profile_dir):
        self.medialists = []
        for this_file in os.listdir(profile_dir):
            if this_file.endswith(".json") and this_file not in ('pp_showlist.json','schedule.json'):
                self.medialists = self.medialists + [this_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 is None:
            d = Edit1Dialog(self.root,"Add Medialist","File", "")
            if d.result  is  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) is  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 copy_medialist(self,to_file=None):
        if self.current_medialist is not None:
            #from_file= self.current_medialist 
            from_file= self.medialists[self.current_medialists_index]
            if to_file is None:
                d = Edit1Dialog(self.root,"Copy Medialist","File", "")
                if d.result  is  None:
                    return ''
                to_file=str(d.result)
                if to_file == "":
                    tkMessageBox.showwarning("Copy medialist","Name is blank")
                    return ''
                
            success_file = self.copy_medialist_file(from_file,to_file)
            if success_file =='':
                return ''

            # append it to the list
            self.medialists.append(copy.deepcopy(success_file))
            # add title to medialists display
            self.medialists_display.insert(END, success_file)
            # and reset  selected medialist
            self.current_medialist=None
            self.refresh_medialists_display()
            self.refresh_tracks_display()
            return success_file
        else:
            return ''

    def copy_medialist_file(self,from_file,to_file):
        if not to_file.endswith(".json"):
            to_file+=(".json")
                
        to_path = self.pp_profile_dir + os.sep + to_file
        if os.path.exists(to_path) is  True:
            tkMessageBox.showwarning("Copy medialist","Medialist file exists\n(%s)" % to_path)
            return ''
        
        from_path= self.pp_profile_dir + os.sep + from_file
        if os.path.exists(from_path) is  False:
            tkMessageBox.showwarning("Copy medialist","Medialist file not found\n(%s)" % from_path)
            return ''

        shutil.copy(from_path,to_path)
        return to_file


    def remove_medialist(self):
        if self.current_medialist is not 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('ordered')
            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 is not 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)
        medialist_file = self.pp_profile_dir+ os.sep + basefile
        self.current_medialist.save_list(medialist_file)

  
          
    # ***************************************
    #   Tracks
    # ***************************************
          
    def refresh_tracks_display(self):
        self.tracks_display.delete(0,self.tracks_display.size())
        if self.current_medialist is not 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 is not 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 is not 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  is  True:
                self.save_medialist()
            self.refresh_tracks_display()

    def move_track_up(self):
        if self.current_medialist is not 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 is not 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 is not 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 is not 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_track(self):
        self.new_track(PPdefinitions.new_tracks['menu'],None)
 
    def remove_track(self):
        if  self.current_medialist is not 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 is 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 is 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 this_file in os.listdir(directory):
            (root_file,ext_file)= os.path.splitext(this_file)
            if ext_file.lower() in exts:
                file_path=directory+os.sep+this_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")  is  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 + " - cannot determine track type, use menu track>new")



    # *********************************************
    # UPDATE PROFILE
    # **********************************************

    def update_all(self):
        self.init()
        for profile_file in os.listdir(self.pp_home_dir+os.sep+'pp_profiles'+self.pp_profiles_offset):
            # self.mon.log (self,"Updating "+profile_file)
            self.pp_profile_dir = self.pp_home_dir+os.sep+'pp_profiles'+self.pp_profiles_offset + 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.current_showlist.open_json(self.pp_profile_dir+os.sep+"pp_showlist.json")
                self.mon.log (self,"Version of profile "+ profile_file + ' is ' + self.current_showlist.sissue())
                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']  is  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," Skipping Profile " + profile_file + " It is already up to date ")
        self.init()
        tkMessageBox.showwarning("Pi Presents","All profiles updated")
            
    def update_profile(self):

        self.update_medialists()   # medialists and their tracks
        self.update_shows()         #shows in showlist, also creates menu tracks for 1.2>1.3
        

    def update_shows(self):
        # open showlist into a list of dictionaries
        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()

        # special 1.2>1.3 create menu medialists with menu track from show
        #go through shows - if type = menu and version is greater copy its medialist to a new medialist with  name = <show-ref>-menu1p3.json
        for show in shows:
            #create a new medialist medialist != show-ref as menus can't now share medialists
            if show['type']=='menu' and float(self.current_showlist.sissue())<float(self.editor_issue):
                to_file=show['show-ref']+'-menu1p3.json'
                from_file = show['medialist']
                if to_file != from_file:
                    self.copy_medialist_file(from_file,to_file)
                else:
                    self.mon.warn(self, 'medialist file' + to_file + ' already exists, must exit with incomplete update')
                    return False

                #update the reference to the medialist
                show['medialist']=to_file
                
                #delete show fields so they are recreated with new default content
                del show['controls']
                
                # open the  medialist and add the menu track then populate some of its fields from the show
                ifile  = open(self.pp_profile_dir + os.sep + to_file, 'rb')
                tracks = json.load(ifile)['tracks']
                ifile.close()
                
                new_track=copy.deepcopy(PPdefinitions.new_tracks['menu'])
                tracks.append(copy.deepcopy(new_track))

                # copy menu parameters from menu show to menu track and init values of some              
                self.transfer_show_params(show,tracks,'menu-track',("entry-colour","entry-font", "entry-select-colour", 
                                                                    "hint-colour", "hint-font", "hint-text", "hint-x","hint-y",
                                                                    "menu-bullet", "menu-columns", "menu-direction", "menu-guidelines",
                                                                    "menu-horizontal-padding", "menu-horizontal-separation", "menu-icon-height", "menu-icon-mode",
                                                                    "menu-icon-width", "menu-rows", "menu-strip", "menu-strip-padding",  "menu-text-height", "menu-text-mode", "menu-text-width",
                                                                     "menu-vertical-padding", "menu-vertical-separation",
                                                                    "menu-window"))
                                          
                # and save the medialist
                dic={'issue':self.editor_issue,'tracks':tracks}
                ofile  = open(self.pp_profile_dir + os.sep + to_file, "wb")
                json.dump(dic,ofile,sort_keys=True,indent=1)
                # end for show in shows

        #update the fields in  all shows
        replacement_shows=self.update_shows_in_showlist(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)
        return True


    def transfer_show_params(self,show,tracks,track_ref,fields):
        # find the menu track in medialist
        for index,track in enumerate(tracks):
            if track['track-ref']== 'menu-track':
                break
            
        #update some fields with new default content
        tracks[index]['links']=PPdefinitions.new_tracks['menu']['links']

        #transfer values from show to track
        for field in fields:
            tracks[index][field]=show[field]
            # print show[field], tracks[index][field]
            pass
            
        
                                          

    def update_medialists(self):
         # UPDATE MEDIALISTS AND THEIR TRACKS
        for this_file in os.listdir(self.pp_profile_dir):
            if this_file.endswith(".json") and this_file not in  ('pp_showlist.json','schedule.json'):
                self.mon.log (self,"Updating medialist " + this_file)
                # open a medialist and update its tracks
                ifile  = open(self.pp_profile_dir + os.sep + this_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 + this_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']
            #update if new tracks has the track type otherwise skip
            if track_type in PPdefinitions.new_tracks:
                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_in_showlist(self,old_shows):
        # get correct spec from type of field
        replacement_shows=[]
        for old_show in old_shows:
            show_type=old_show['type']
            ## menu to menushow
            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                
Пример #12
0
class OSCDriver(object):

    # executed by main program
    def  init(self,pp_profile,manager_unit,preferred_interface,my_ip,show_command_callback,input_event_callback,animate_callback):

        self.pp_profile=pp_profile
        self.show_command_callback=show_command_callback
        self.input_event_callback=input_event_callback
        self.animate_callback=animate_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 not found: '+config_file)
            return'error','OSC Configuration file nof found: '+config_file
        
        self.mon.log(self, 'OSC Configuration file found at: '+config_file)        
        self.osc_config=OSCConfig()
        
        # reads config data 
        if self.osc_config.read(config_file) ==False:
            return 'error','failed to read osc.cfg'

        # unpack config data and initialise

        if self.osc_config.this_unit_name =='':
            return 'error','OSC Config -  This Unit has no name'
        if len(self.osc_config.this_unit_name.split())>1:
            return 'error','OSC config - This Unit Name not a single word: '+self.osc_config.this_unit_name
        self.this_unit_name=self.osc_config.this_unit_name
               
                 
        if self.osc_config.this_unit_ip=='':
            self.this_unit_ip=my_ip
        else:
            self.this_unit_ip=self.osc_config.this_unit_ip

        if self.osc_config.slave_enabled == 'yes':
            if not self.osc_config.listen_port.isdigit():
                return 'error','OSC Config - Listen port is not a positve number: '+ self.osc_config.listen_port
            self.listen_port= self.osc_config.listen_port

        if self.osc_config.master_enabled == 'yes':
            if not self.osc_config.reply_listen_port.isdigit():
                return 'error','OSC Config - Reply Listen port is not a positve number: '+ self.osc_config.reply_listen_port
            self.reply_listen_port= self.osc_config.reply_listen_port

            # prepare the list of slaves
            status,message=self.parse_slaves()
            if status=='error':
                return status,message
             

        self.prefix='/pipresents'
        self.this_unit='/' + self.this_unit_name
        
        self.input_server=None
        self.input_reply_client=None
        self.input_st=None
        
        self.output_client=None
        self.output_reply_server=None
        self.output_reply_st=None


        if self.osc_config.slave_enabled == 'yes' and self.osc_config.master_enabled == 'yes' and self.listen_port == self.reply_listen_port:
            # The two listen ports are the same so use one server for input and output

            #start the client that sends commands to the slaves
            self.output_client=OSC.OSCClient()
            self.mon.log(self, 'sending commands to slaves and replies to master on: '+self.reply_listen_port)

            #start the input+output reply server
            self.mon.log(self, 'listen to commands and replies from slave units using: ' + self.this_unit_ip+':'+self.reply_listen_port)
            self.output_reply_server=myOSCServer((self.this_unit_ip,int(self.reply_listen_port)),self.output_client)
            self.add_default_handler(self.output_reply_server)
            self.add_input_handlers(self.output_reply_server)
            self.add_output_reply_handlers(self.output_reply_server)

            self.input_server=self.output_reply_server
            
        else:

            if self.osc_config.slave_enabled == 'yes':
                # we want this to be a slave to something else

                # start the client that sends replies to controlling unit
                self.input_reply_client=OSC.OSCClient()
                
                #start the input server
                self.mon.log(self, 'listening to commands on: ' + self.this_unit_ip+':'+self.listen_port)
                self.input_server=myOSCServer((self.this_unit_ip,int(self.listen_port)),self.input_reply_client)
                self.add_default_handler(self.input_server)
                self.add_input_handlers(self.input_server)
                print self.pretty_list(self.input_server.getOSCAddressSpace(),'\n')
                
            if self.osc_config.master_enabled =='yes':
                #we want to control other units
               
                #start the client that sends commands to the slaves
                self.output_client=OSC.OSCClient()
                self.mon.log(self, 'sending commands to slaves on port: '+self.reply_listen_port)

                #start the output reply server
                self.mon.log(self, 'listen to replies from slave units using: ' + self.this_unit_ip+':'+self.reply_listen_port)
                self.output_reply_server=myOSCServer((self.this_unit_ip,int(self.reply_listen_port)),self.output_client)
                self.add_default_handler(self.output_reply_server)
                self.add_output_reply_handlers(self.output_reply_server)
        
        return 'normal','osc.cfg read'


    def terminate(self):
        if self.input_server != None:
            self.input_server.close()
        if self.output_reply_server != None:
            self.output_reply_server.close()
        self.mon.log(self, 'Waiting for Server threads to finish')
        if self.input_st != None:
            self.input_st.join() ##!!!
        if self.output_reply_st != None:
            self.output_reply_st.join() ##!!!
        self.mon.log(self,'server threads closed')
        if self.input_reply_client !=None:
            self.input_reply_client.close()
        if self.output_client !=None:
            self.output_client.close()


    def start_server(self):
        # Start input Server
        self.mon.log(self,'Starting input OSCServer')
        if self.input_server != None:
            self.input_st = threading.Thread( target = self.input_server.serve_forever )
            self.input_st.start()

        # Start output_reply server
        self.mon.log(self,'Starting output reply OSCServer')
        if self.output_reply_server != None:
            self.output_reply_st = threading.Thread( target = self.output_reply_server.serve_forever )
            self.output_reply_st.start()

    def parse_slaves(self):
        name_list=self.osc_config.slave_units_name.split()
        ip_list=self.osc_config.slave_units_ip.split()
        if len(name_list)==0:
            return 'error','OSC Config - List of slaves name is empty'
        if len(name_list) != len(ip_list):
            return 'error','OSC Config - Lengths of list of slaves name and slaves IP is different'       
        self.slave_name_list=[]
        self.slave_ip_list=[]
        for i, name in enumerate(name_list):
            self.slave_name_list.append(name)
            self.slave_ip_list.append(ip_list[i])
        return 'normal','slaves parsed'


    def parse_osc_command(self,fields):
    # send message to slave unit - INTERFACE WITH pipresents
        if len(fields) <2:
               return 'error','too few fields in OSC command '+' '.join(fields)
        to_unit_name=fields[0]
        show_command=fields[1]
        # print 'FIELDS ',fields
        
        # send an arbitary osc message            
        if show_command == 'send':
            if len(fields)>2:
                osc_address= fields[2]
                arg_list=[]
                if len(fields)>3:
                    arg_list=fields[3:]
            else:
                return 'error','OSC - wrong nmber of fields in '+ ' '.join(fields)

        elif show_command in ('open','close','openexclusive'):
            if len(fields)==3:
                osc_address=self.prefix+'/'+ to_unit_name + '/core/'+ show_command
                arg_list= [fields[2]]
            else:
                return 'error','OSC - wrong number of fields in '+ ' '.join(fields)
                
        elif show_command =='monitor':
            if fields[2] in ('on','off'):
                osc_address=self.prefix+'/'+ to_unit_name + '/core/'+ show_command
                arg_list=[fields[2]]
            else:
                self.mon.err(self,'OSC - illegal state in '+ show_command + ' '+fields[2])
        
        elif show_command =='event':                
            if len(fields)==3:
                osc_address=self.prefix+'/'+ to_unit_name + '/core/'+ show_command
                arg_list= [fields[2]]


        elif show_command == 'animate':                
            if len(fields)>2:
                osc_address=self.prefix+'/'+ to_unit_name + '/core/'+ show_command
                arg_list= fields[2:]
            else:
                return 'error','OSC - wrong nmber of fields in '+ ' '.join(fields)
            
        elif show_command in ('closeall','exitpipresents','shutdownnow','reboot'):
            if len(fields)==2:
                osc_address=self.prefix+'/'+ to_unit_name + '/core/'+ show_command
                arg_list= []
            else:
                return 'error','OSC - wrong nmber of fields in '+ ' '.join(fields)

        elif show_command in ('loopback','server-info'):
            if len(fields)==2:
                osc_address=self.prefix+'/'+ to_unit_name + '/system/'+ show_command
                arg_list= []
            else:
                return 'error','OSC - wrong nmber of fields in '+ ' '.join(fields)
            
        else:
            return 'error','OSC - unkown command in '+ ' '.join(fields)

        
        ip=self.find_ip(to_unit_name,self.slave_name_list,self.slave_ip_list)
        if ip=='':
            return 'warn','OSC Unit Name not in the list of slaves: '+ to_unit_name
        self.sendto(ip,osc_address,arg_list)
        return 'normal','osc command sent'


    def find_ip(self,name,name_list,ip_list):
        i=0
        for j in name_list:
            if j == name:
                break
            i=i+1
            
        if i==len(name_list):
            return ''
        else:
            return ip_list[i]
                    

    def sendto(self,ip,osc_address,arg_list):
        # print ip,osc_address,arg_list
        if self.output_client is None:
            self.mon.warn(self,'Master not enabled, ignoring OSC command')
            return
        msg = OSC.OSCMessage()
        # print address
        msg.setAddress(osc_address)
        for arg in arg_list:
            # print arg
            msg.append(arg)
            
        try:
            self.output_client.sendto(msg,(ip,int(self.reply_listen_port)))
            self.mon.log(self,'Sent OSC command: '+osc_address+' '+' '.join(arg_list) + ' to '+ ip +':'+self.reply_listen_port)
        except Exception as e:
            self.mon.warn(self,'error in client when sending OSC command: '+ str(e))


# **************************************
# Handlers for fallback
# **************************************

    def add_default_handler(self,server):
        server.addMsgHandler('default', self.no_match_handler)

    def no_match_handler(self,addr, tags, stuff, source):
        text= "No handler for message from %s" % OSC.getUrlStr(source)+'\n'
        text+= "     %s" % addr+ self.pretty_list(stuff,'')
        self.mon.warn(self,text)
        return None

# **************************************
# Handlers for Slave (input)
# **************************************    

    def add_input_handlers(self,server):
        server.addMsgHandler(self.prefix + self.this_unit+"/system/server-info", self.server_info_handler)
        server.addMsgHandler(self.prefix + self.this_unit+"/system/loopback", self.loopback_handler)
        server.addMsgHandler(self.prefix+ self.this_unit+'/core/open', self.open_show_handler)
        server.addMsgHandler(self.prefix+ self.this_unit+'/core/close', self.close_show_handler)
        server.addMsgHandler(self.prefix+ self.this_unit+'/core/openexclusive', self.openexclusive_handler)
        server.addMsgHandler(self.prefix+ self.this_unit+'/core/closeall', self.closeall_handler)
        server.addMsgHandler(self.prefix+ self.this_unit+'/core/exitpipresents', self.exitpipresents_handler)
        server.addMsgHandler(self.prefix+ self.this_unit+'/core/shutdownnow', self.shutdownnow_handler)
        server.addMsgHandler(self.prefix+ self.this_unit+'/core/reboot', self.reboot_handler)
        server.addMsgHandler(self.prefix+ self.this_unit+'/core/event', self.input_event_handler)
        server.addMsgHandler(self.prefix+ self.this_unit+'/core/animate', self.animate_handler)
        server.addMsgHandler(self.prefix+ self.this_unit+'/core/monitor', self.monitor_handler)


    # reply to master unit with name of this unit and commands
    def server_info_handler(self,addr, tags, stuff, source):

        msg = OSC.OSCMessage(self.prefix+'/system/server-info-reply')
        msg.append(self.this_unit_name)
        msg.append(self.input_server.getOSCAddressSpace())
        self.mon.log(self,'Sent Server Info reply to %s:' % OSC.getUrlStr(source))
        return msg


    # reply to master unit with a loopback message
    def loopback_handler(self,addr, tags, stuff, source):
        msg = OSC.OSCMessage(self.prefix+'/system/loopback-reply')
        self.mon.log(self,'Sent loopback reply to %s:' % OSC.getUrlStr(source))
        return msg


 
    def open_show_handler(self,address, tags, args, source):
        self.prepare_show_command_callback('open ',args,1)

    def openexclusive_handler(self,address, tags, args, source):
        self.prepare_show_command_callback('openexclusive ',args,1)
        
    def close_show_handler(self,address, tags, args, source):
        self.prepare_show_command_callback('close ', args,1)

    def closeall_handler(self,address, tags, args, source):
        self.prepare_show_command_callback('closeall',args,0)

    def monitor_handler(self,address, tags, args, source):
        self.prepare_show_command_callback('monitor ', args,1)

    def exitpipresents_handler(self,address, tags, args, source):
        self.prepare_show_command_callback('exitpipresents',args,0)

    def reboot_handler(self,address, tags, args, source):
        self.prepare_show_command_callback('reboot',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.mon.sched(self,TimeOfDay.now,'Received from OSC: '+ command + ' ' +args[0])
                self.show_command_callback(command+args[0])
            else:
                self.mon.sched(self,TimeOfDay.now,'Received from OSC: '+ command)
                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 animate_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'
            print text
            self.animate_callback(text)
        else:
            self.mon.warn(self,'OSC output event has no arguments - ignoring')      

# **************************************
# Handlers for Master- replies from slaves (output)
# **************************************

    # reply handlers do not have the destinatuion unit in the address as they are always sent to the originator
    def add_output_reply_handlers(self,server):
        server.addMsgHandler(self.prefix+"/system/server-info-reply", self.server_info_reply_handler)
        server.addMsgHandler(self.prefix+"/system/loopback-reply", self.loopback_reply_handler)
        
        
    # print result of info request from slave unit
    def server_info_reply_handler(self,addr, tags, stuff, source):
        self.mon.log(self,'server info reply from slave '+OSC.getUrlStr(source)+ self.pretty_list(stuff,'\n'))
        print 'Received reply to Server-Info command from slave: ',OSC.getUrlStr(source), self.pretty_list(stuff,'\n')
        return None

    #print result of info request from slave unit
    def loopback_reply_handler(self,addr, tags, stuff, source):
        self.mon.log(self,'server info reply from slave '+OSC.getUrlStr(source)+ self.pretty_list(stuff,'\n'))
        print 'Received reply to Loopback command from slave: ' + OSC.getUrlStr(source)+ ' '+ self.pretty_list(stuff,'\n')
        return None


    def pretty_list(self,fields, separator):
        text=' '
        for field in fields:
            text += str(field) + separator
        return text+'\n'
class pp_inputdevicedriver(object):
    """
    pp_inputdevicedriver interfaces with the Linux generic input devices, for example USB keybads or remotes
    that is, ones that have a file in /dev/input
     - configures and binds key codes from specified .cfg file
     - reads input events from /dev/input and decodes them using evdev, provides callbacks on state changes which generate input events
    """
 
# constants for buttons

# cofiguration from gpio.cfg
    KEY_CODE=0                # pin on RPi board GPIO connector e.g. P1-11
    DIRECTION = 1 # IN/OUT/NONE (None is not used)
    RELEASED_NAME=2             # symbolic name for released callback
    PRESSED_NAME=3      # symbolic name of pressed callback
    UP_NAME=4     # symbolic name for up state callback
    DOWN_NAME = 5   # symbolic name for down state callback
    REPEAT =  6   # repeat interval for state callbacks (mS)


# dynamic data
    PRESSED = 7     # variable - debounced state 
    LAST = 8      # varible - last state - used to detect edge
    REPEAT_COUNT = 9

    
    TEMPLATE = ['',   # key code
                '',              # direction
                '','','','',       #input names
                0,             # repeat
                False,False,0]   #dynamics - pressed,last,repeat count
    




# CLASS VARIABLES  (pp_inputdevicedriver.)
    driver_active=False
    title=''
    KEYCODELIST=[]   # list of keycodes read from .cfg file
    key_codes=[] #dynamic list constructed from .cfg file and other dynamic variables
    device_refs=[]  # list of references to devices obtained by matching devide name against /dev/input


    # executed by main program and by each object using gpio
    def __init__(self):
        self.mon=Monitor()



     # executed once from main program   
    def init(self,filename,filepath,widget,button_callback=None):
        
        # instantiate arguments
        self.widget=widget
        self.filename=filename
        self.filepath=filepath
        self.button_callback=button_callback

        pp_inputdevicedriver.driver_active=False 

        # read .cfg file.
        reason,message=self._read(self.filename,self.filepath)
        if reason =='error':
            return 'error',message
        if self.config.has_section('DRIVER') is False:
            return 'error','no DRIVER section in '+ self.filename           

        #read information from DRIVER section
        pp_inputdevicedriver.title=self.config.get('DRIVER','title')
        self.button_tick=int(self.config.get('DRIVER','tick-interval'))  # in mS
        keycode_text=self.config.get('DRIVER','key-codes')
        pp_inputdevicedriver.KEYCODELIST = keycode_text.split(',')
        
        # construct the key_code control list from the configuration
        for index, key_code_def in enumerate(pp_inputdevicedriver.KEYCODELIST):
            if self.config.has_section(key_code_def) is False:
                self.mon.warn(self, "no key code definition for "+ key_code_def)
                continue          
            else:
                entry=copy.deepcopy(pp_inputdevicedriver.TEMPLATE)
                # unused pin
                entry[pp_inputdevicedriver.KEY_CODE]=key_code_def
                if self.config.get(key_code_def,'direction') == 'none':
                    entry[pp_inputdevicedriver.DIRECTION]='none'
                else:
                    entry[pp_inputdevicedriver.DIRECTION]=self.config.get(key_code_def,'direction')
                    if entry[pp_inputdevicedriver.DIRECTION] == 'in':
                        # input pin
                        entry[pp_inputdevicedriver.RELEASED_NAME]=self.config.get(key_code_def,'released-name')
                        entry[pp_inputdevicedriver.PRESSED_NAME]=self.config.get(key_code_def,'pressed-name')
                        entry[pp_inputdevicedriver.UP_NAME]=self.config.get(key_code_def,'up-name')
                        entry[pp_inputdevicedriver.DOWN_NAME]=self.config.get(key_code_def,'down-name')

                        if self.config.get(key_code_def,'repeat') != '':
                            entry[pp_inputdevicedriver.REPEAT]=int(self.config.get(key_code_def,'repeat'))
                        else:
                            entry[pp_inputdevicedriver.REPEAT]=-1
 
            # add entry to working list of key_codes  (list of lists [0] of sublist is key code       
            pp_inputdevicedriver.key_codes.append(copy.deepcopy(entry))

            
        # find the input device
        device_name=self.config.get('DRIVER','device-name')
        
        pp_inputdevicedriver.device_refs=self.find_devices(device_name)
        if pp_inputdevicedriver.device_refs==[]:
            return 'warn','Cannot find device: '+device_name + ' for '+ pp_inputdevicedriver.title

        pp_inputdevicedriver.driver_active=True

        # init timer
        self.button_tick_timer=None
        self.mon.log(self,pp_inputdevicedriver.title+' active')
        return 'normal',pp_inputdevicedriver.title+' active'



    def find_devices(self,name):
        devices = [evdev.InputDevice(fn) for fn in evdev.list_devices()]
        matching=[]
        # print '\nMatching devices references: '
        for device in devices:
            if name in device.name:
                # print '     Ref:',device.fn, ' Name: ',device.name
                device_ref = evdev.InputDevice(device.fn)
                matching.append(device_ref)
        return matching


    # called by main program only         
    def start(self):
        # poll for events then update all the buttons once
        self.do_buttons()
        self.button_tick_timer=self.widget.after(self.button_tick,self.start)

    # allow track plugins (or anyting else) to access input values
    def get_input(self,channel):
            return False, None



    # called by main program only                
    def terminate(self):
        if pp_inputdevicedriver.driver_active is True:
            if self.button_tick_timer is not None:
                self.widget.after_cancel(self.button_tick_timer)
            pp_inputdevicedriver.driver_active = False


# ************************************************
# inputdevice functions
# called by main program only
# ************************************************
    
    def reset_input_state(self):
        for entry in pp_inputdevicedriver.key_codes:
            entry[pp_inputdevicedriver.PRESSED]=False
            entry[pp_inputdevicedriver.LAST]=False
            entry[pp_inputdevicedriver.REPEAT_COUNT]=entry[pp_inputdevicedriver.REPEAT]


    def is_active(self):
        return pp_inputdevicedriver.driver_active

    def do_buttons(self):

        # get and process new events from remote
        self.get_events(pp_inputdevicedriver.device_refs)

        # do the things that are done every tick
        for index, entry in enumerate(pp_inputdevicedriver.key_codes):
            if entry[pp_inputdevicedriver.DIRECTION] == 'in':

                # detect falling edge
                if entry[pp_inputdevicedriver.PRESSED] is True and entry[pp_inputdevicedriver.LAST] is False:
                    entry[pp_inputdevicedriver.LAST]=entry[pp_inputdevicedriver.PRESSED]
                    entry[pp_inputdevicedriver.REPEAT_COUNT]=entry[pp_inputdevicedriver.REPEAT]
                    if  entry[pp_inputdevicedriver.PRESSED_NAME] != '' and self.button_callback  is not  None:
                        self.button_callback(entry[pp_inputdevicedriver.PRESSED_NAME],pp_inputdevicedriver.title)
               # detect rising edge
                if entry[pp_inputdevicedriver.PRESSED] is False and entry[pp_inputdevicedriver.LAST] is True:
                    entry[pp_inputdevicedriver.LAST]=entry[pp_inputdevicedriver.PRESSED]
                    entry[pp_inputdevicedriver.REPEAT_COUNT]=entry[pp_inputdevicedriver.REPEAT]
                    if  entry[pp_inputdevicedriver.RELEASED_NAME] != '' and self.button_callback  is not  None:
                        self.button_callback(entry[pp_inputdevicedriver.RELEASED_NAME],pp_inputdevicedriver.title)

                # do state callbacks
                if entry[pp_inputdevicedriver.REPEAT_COUNT] == 0:
                    if entry[pp_inputdevicedriver.DOWN_NAME] != '' and entry[pp_inputdevicedriver.PRESSED] is True and self.button_callback is not None:
                        self.button_callback(entry[pp_inputdevicedriver.DOWN_NAME],pp_inputdevicedriver.title)
                    if entry[pp_inputdevicedriver.UP_NAME] != '' and entry[pp_inputdevicedriver.PRESSED] is False and self.button_callback is not None:
                        self.button_callback(entry[pp_inputdevicedriver.UP_NAME],pp_inputdevicedriver.title)
                    entry[pp_inputdevicedriver.REPEAT_COUNT]=entry[pp_inputdevicedriver.REPEAT]
                else:
                    if entry[pp_inputdevicedriver.REPEAT] != -1:
                        entry[pp_inputdevicedriver.REPEAT_COUNT]-=1


    def find_event_entry(self,event_code):
        for entry in pp_inputdevicedriver.key_codes:
            if entry[pp_inputdevicedriver.KEY_CODE]==event_code:
                # print entry
                return entry
        return None

        
    def get_events(self,matching):
        # some input devices have more than one logical input device so use select to poll all of the devices
        r,w,x = select(matching, [], [],0)
        if r==[] and w==[] and x==[]:
            # print 'no event'
            return
        self.process_events(r)

    def process_events(self,r):
        for re in r:
            for event in re.read():
                # print '\nEvent Rxd: ',event,'\n',evdev.categorize(event)
                if event.type == evdev.ecodes.EV_KEY:
                    # print 'Key Event Rxd: ',event
                    key_event = evdev.categorize(event)
                    self.process_event(key_event.keycode,key_event.keystate)



    def process_event(self,event_code,button_state):              
        event_entry=self.find_event_entry(event_code)
        if event_entry is None or event_entry[pp_inputdevicedriver.DIRECTION] != 'in':
            self.mon.warn(self,'input event key code not in list of key codes or direction not in')
        else:
            if button_state==1:
                # print event_code, 'Pressed'
                event_entry[pp_inputdevicedriver.PRESSED] = True 
            elif button_state==0:
                # print event_code, 'Released'
                event_entry[pp_inputdevicedriver.PRESSED] = False             
            else:
                # ignore other button states
                pass
                # print 'unknown state: ',button_state



# ***********************************
# output events
# ************************************

    # execute an output event
    def handle_output_event(self,name,param_type,param_values,req_time):
        return 'normal',pp_inputdevicedriver.title + ' does not accept outputs'
        
    def reset_outputs(self):
        return


# ***********************************
# reading .cfg file
# ************************************

    def _read(self,filename,filepath):
        # try inside profile
        if os.path.exists(filepath):
            self.config = ConfigParser.ConfigParser()
            self.config.read(filepath)
            self.mon.log(self,filename + " read from "+ filepath)
            return 'normal',filename+' read'
        else:
            return 'error',filename + ' not found at: '+filepath
class OSCDriver(object):

    # executed by main program
    def init(self, pp_profile, manager_unit, preferred_interface, my_ip,
             show_command_callback, input_event_callback, animate_callback):

        self.pp_profile = pp_profile
        self.show_command_callback = show_command_callback
        self.input_event_callback = input_event_callback
        self.animate_callback = animate_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 not found: ' + config_file)
            return 'error', 'OSC Configuration file nof found: ' + config_file

        self.mon.log(self, 'OSC Configuration file found at: ' + config_file)
        self.osc_config = OSCConfig()

        # reads config data
        if self.osc_config.read(config_file) == False:
            return 'error', 'failed to read osc.cfg'

        # unpack config data and initialise

        if self.osc_config.this_unit_name == '':
            return 'error', 'OSC Config -  This Unit has no name'
        if len(self.osc_config.this_unit_name.split()) > 1:
            return 'error', 'OSC config - This Unit Name not a single word: ' + self.osc_config.this_unit_name
        self.this_unit_name = self.osc_config.this_unit_name

        if self.osc_config.this_unit_ip == '':
            self.this_unit_ip = my_ip
        else:
            self.this_unit_ip = self.osc_config.this_unit_ip

        if self.osc_config.slave_enabled == 'yes':
            if not self.osc_config.listen_port.isdigit():
                return 'error', 'OSC Config - Listen port is not a positve number: ' + self.osc_config.listen_port
            self.listen_port = self.osc_config.listen_port

        if self.osc_config.master_enabled == 'yes':
            if not self.osc_config.reply_listen_port.isdigit():
                return 'error', 'OSC Config - Reply Listen port is not a positve number: ' + self.osc_config.reply_listen_port
            self.reply_listen_port = self.osc_config.reply_listen_port

            # prepare the list of slaves
            status, message = self.parse_slaves()
            if status == 'error':
                return status, message

        self.prefix = '/pipresents'
        self.this_unit = '/' + self.this_unit_name

        self.input_server = None
        self.input_reply_client = None
        self.input_st = None

        self.output_client = None
        self.output_reply_server = None
        self.output_reply_st = None

        if self.osc_config.slave_enabled == 'yes' and self.osc_config.master_enabled == 'yes' and self.listen_port == self.reply_listen_port:
            # The two listen ports are the same so use one server for input and output

            #start the client that sends commands to the slaves
            self.output_client = OSC.OSCClient()
            self.mon.log(
                self, 'sending commands to slaves and replies to master on: ' +
                self.reply_listen_port)

            #start the input+output reply server
            self.mon.log(
                self,
                'listen to commands and replies from slave units using: ' +
                self.this_unit_ip + ':' + self.reply_listen_port)
            self.output_reply_server = myOSCServer(
                (self.this_unit_ip, int(self.reply_listen_port)),
                self.output_client)
            self.add_default_handler(self.output_reply_server)
            self.add_input_handlers(self.output_reply_server)
            self.add_output_reply_handlers(self.output_reply_server)

            self.input_server = self.output_reply_server

        else:

            if self.osc_config.slave_enabled == 'yes':
                # we want this to be a slave to something else

                # start the client that sends replies to controlling unit
                self.input_reply_client = OSC.OSCClient()

                #start the input server
                self.mon.log(
                    self, 'listening to commands on: ' + self.this_unit_ip +
                    ':' + self.listen_port)
                self.input_server = myOSCServer(
                    (self.this_unit_ip, int(self.listen_port)),
                    self.input_reply_client)
                self.add_default_handler(self.input_server)
                self.add_input_handlers(self.input_server)
                # print self.pretty_list(self.input_server.getOSCAddressSpace(),'\n')

            if self.osc_config.master_enabled == 'yes':
                #we want to control other units

                #start the client that sends commands to the slaves
                self.output_client = OSC.OSCClient()
                self.mon.log(
                    self, 'sending commands to slaves on port: ' +
                    self.reply_listen_port)

                #start the output reply server
                self.mon.log(
                    self, 'listen to replies from slave units using: ' +
                    self.this_unit_ip + ':' + self.reply_listen_port)
                self.output_reply_server = myOSCServer(
                    (self.this_unit_ip, int(self.reply_listen_port)),
                    self.output_client)
                self.add_default_handler(self.output_reply_server)
                self.add_output_reply_handlers(self.output_reply_server)

        return 'normal', 'osc.cfg read'

    def terminate(self):
        if self.input_server != None:
            self.input_server.close()
        if self.output_reply_server != None:
            self.output_reply_server.close()
        self.mon.log(self, 'Waiting for Server threads to finish')
        if self.input_st != None:
            self.input_st.join()  ##!!!
        if self.output_reply_st != None:
            self.output_reply_st.join()  ##!!!
        self.mon.log(self, 'server threads closed')
        if self.input_reply_client != None:
            self.input_reply_client.close()
        if self.output_client != None:
            self.output_client.close()

    def start_server(self):
        # Start input Server
        self.mon.log(self, 'Starting input OSCServer')
        if self.input_server != None:
            self.input_st = threading.Thread(
                target=self.input_server.serve_forever)
            self.input_st.start()

        # Start output_reply server
        self.mon.log(self, 'Starting output reply OSCServer')
        if self.output_reply_server != None:
            self.output_reply_st = threading.Thread(
                target=self.output_reply_server.serve_forever)
            self.output_reply_st.start()

    def parse_slaves(self):
        name_list = self.osc_config.slave_units_name.split()
        ip_list = self.osc_config.slave_units_ip.split()
        if len(name_list) == 0:
            return 'error', 'OSC Config - List of slaves name is empty'
        if len(name_list) != len(ip_list):
            return 'error', 'OSC Config - Lengths of list of slaves name and slaves IP is different'
        self.slave_name_list = []
        self.slave_ip_list = []
        for i, name in enumerate(name_list):
            self.slave_name_list.append(name)
            self.slave_ip_list.append(ip_list[i])
        return 'normal', 'slaves parsed'

    def parse_osc_command(self, fields):
        # send message to slave unit - INTERFACE WITH pipresents
        if len(fields) < 2:
            return 'error', 'too few fields in OSC command ' + ' '.join(fields)
        to_unit_name = fields[0]
        show_command = fields[1]
        # print 'FIELDS ',fields

        # send an arbitary osc message
        if show_command == 'send':
            if len(fields) > 2:
                osc_address = fields[2]
                arg_list = []
                if len(fields) > 3:
                    arg_list = fields[3:]
            else:
                return 'error', 'OSC - wrong nmber of fields in ' + ' '.join(
                    fields)

        elif show_command in ('open', 'close', 'openexclusive'):
            if len(fields) == 3:
                osc_address = self.prefix + '/' + to_unit_name + '/core/' + show_command
                arg_list = [fields[2]]
            else:
                return 'error', 'OSC - wrong number of fields in ' + ' '.join(
                    fields)

        elif show_command == 'monitor':
            if fields[2] in ('on', 'off'):
                osc_address = self.prefix + '/' + to_unit_name + '/core/' + show_command
                arg_list = [fields[2]]
            else:
                self.mon.err(
                    self,
                    'OSC - illegal state in ' + show_command + ' ' + fields[2])

        elif show_command == 'event':
            if len(fields) == 3:
                osc_address = self.prefix + '/' + to_unit_name + '/core/' + show_command
                arg_list = [fields[2]]

        elif show_command == 'animate':
            if len(fields) > 2:
                osc_address = self.prefix + '/' + to_unit_name + '/core/' + show_command
                arg_list = fields[2:]
            else:
                return 'error', 'OSC - wrong nmber of fields in ' + ' '.join(
                    fields)

        elif show_command in ('closeall', 'exitpipresents', 'shutdownnow',
                              'reboot'):
            if len(fields) == 2:
                osc_address = self.prefix + '/' + to_unit_name + '/core/' + show_command
                arg_list = []
            else:
                return 'error', 'OSC - wrong nmber of fields in ' + ' '.join(
                    fields)

        elif show_command in ('loopback', 'server-info'):
            if len(fields) == 2:
                osc_address = self.prefix + '/' + to_unit_name + '/system/' + show_command
                arg_list = []
            else:
                return 'error', 'OSC - wrong nmber of fields in ' + ' '.join(
                    fields)

        else:
            return 'error', 'OSC - unkown command in ' + ' '.join(fields)

        ip = self.find_ip(to_unit_name, self.slave_name_list,
                          self.slave_ip_list)
        if ip == '':
            return 'warn', 'OSC Unit Name not in the list of slaves: ' + to_unit_name
        self.sendto(ip, osc_address, arg_list)
        return 'normal', 'osc command sent'

    def find_ip(self, name, name_list, ip_list):
        i = 0
        for j in name_list:
            if j == name:
                break
            i = i + 1

        if i == len(name_list):
            return ''
        else:
            return ip_list[i]

    def sendto(self, ip, osc_address, arg_list):
        # print ip,osc_address,arg_list
        if self.output_client is None:
            self.mon.warn(self, 'Master not enabled, ignoring OSC command')
            return
        msg = OSC.OSCMessage()
        # print address
        msg.setAddress(osc_address)
        for arg in arg_list:
            # print arg
            msg.append(arg)

        try:
            self.output_client.sendto(msg, (ip, int(self.reply_listen_port)))
            self.mon.log(
                self,
                'Sent OSC command: ' + osc_address + ' ' + ' '.join(arg_list) +
                ' to ' + ip + ':' + self.reply_listen_port)
        except Exception as e:
            self.mon.warn(
                self, 'error in client when sending OSC command: ' + str(e))

# **************************************
# Handlers for fallback
# **************************************

    def add_default_handler(self, server):
        server.addMsgHandler('default', self.no_match_handler)

    def no_match_handler(self, addr, tags, stuff, source):
        text = "No handler for message from %s" % OSC.getUrlStr(source) + '\n'
        text += "     %s" % addr + self.pretty_list(stuff, '')
        self.mon.warn(self, text)
        return None

# **************************************
# Handlers for Slave (input)
# **************************************

    def add_input_handlers(self, server):
        server.addMsgHandler(
            self.prefix + self.this_unit + "/system/server-info",
            self.server_info_handler)
        server.addMsgHandler(self.prefix + self.this_unit + "/system/loopback",
                             self.loopback_handler)
        server.addMsgHandler(self.prefix + self.this_unit + '/core/open',
                             self.open_show_handler)
        server.addMsgHandler(self.prefix + self.this_unit + '/core/close',
                             self.close_show_handler)
        server.addMsgHandler(
            self.prefix + self.this_unit + '/core/openexclusive',
            self.openexclusive_handler)
        server.addMsgHandler(self.prefix + self.this_unit + '/core/closeall',
                             self.closeall_handler)
        server.addMsgHandler(
            self.prefix + self.this_unit + '/core/exitpipresents',
            self.exitpipresents_handler)
        server.addMsgHandler(
            self.prefix + self.this_unit + '/core/shutdownnow',
            self.shutdownnow_handler)
        server.addMsgHandler(self.prefix + self.this_unit + '/core/reboot',
                             self.reboot_handler)
        server.addMsgHandler(self.prefix + self.this_unit + '/core/event',
                             self.input_event_handler)
        server.addMsgHandler(self.prefix + self.this_unit + '/core/animate',
                             self.animate_handler)
        server.addMsgHandler(self.prefix + self.this_unit + '/core/monitor',
                             self.monitor_handler)

    # reply to master unit with name of this unit and commands
    def server_info_handler(self, addr, tags, stuff, source):

        msg = OSC.OSCMessage(self.prefix + '/system/server-info-reply')
        msg.append(self.this_unit_name)
        msg.append(self.input_server.getOSCAddressSpace())
        self.mon.log(self,
                     'Sent Server Info reply to %s:' % OSC.getUrlStr(source))
        return msg

    # reply to master unit with a loopback message
    def loopback_handler(self, addr, tags, stuff, source):
        msg = OSC.OSCMessage(self.prefix + '/system/loopback-reply')
        self.mon.log(self,
                     'Sent loopback reply to %s:' % OSC.getUrlStr(source))
        return msg

    def open_show_handler(self, address, tags, args, source):
        self.prepare_show_command_callback('open ', args, 1)

    def openexclusive_handler(self, address, tags, args, source):
        self.prepare_show_command_callback('openexclusive ', args, 1)

    def close_show_handler(self, address, tags, args, source):
        self.prepare_show_command_callback('close ', args, 1)

    def closeall_handler(self, address, tags, args, source):
        self.prepare_show_command_callback('closeall', args, 0)

    def monitor_handler(self, address, tags, args, source):
        self.prepare_show_command_callback('monitor ', args, 1)

    def exitpipresents_handler(self, address, tags, args, source):
        self.prepare_show_command_callback('exitpipresents', args, 0)

    def reboot_handler(self, address, tags, args, source):
        self.prepare_show_command_callback('reboot', 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.mon.sched(self, TimeOfDay.now,
                               'Received from OSC: ' + command + ' ' + args[0])
                self.show_command_callback(command + args[0])
            else:
                self.mon.sched(self, TimeOfDay.now,
                               'Received from OSC: ' + command)
                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 animate_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'
            # print text
            self.animate_callback(text)
        else:
            self.mon.warn(self, 'OSC output event has no arguments - ignoring')


# **************************************
# Handlers for Master- replies from slaves (output)
# **************************************

# reply handlers do not have the destinatuion unit in the address as they are always sent to the originator

    def add_output_reply_handlers(self, server):
        server.addMsgHandler(self.prefix + "/system/server-info-reply",
                             self.server_info_reply_handler)
        server.addMsgHandler(self.prefix + "/system/loopback-reply",
                             self.loopback_reply_handler)

    # print result of info request from slave unit
    def server_info_reply_handler(self, addr, tags, stuff, source):
        self.mon.log(
            self, 'server info reply from slave ' + OSC.getUrlStr(source) +
            self.pretty_list(stuff, '\n'))
        print 'Received reply to Server-Info command from slave: ', OSC.getUrlStr(
            source), self.pretty_list(stuff, '\n')
        return None

    #print result of info request from slave unit
    def loopback_reply_handler(self, addr, tags, stuff, source):
        self.mon.log(
            self, 'server info reply from slave ' + OSC.getUrlStr(source) +
            self.pretty_list(stuff, '\n'))
        print 'Received reply to Loopback command from slave: ' + OSC.getUrlStr(
            source) + ' ' + self.pretty_list(stuff, '\n')
        return None

    def pretty_list(self, fields, separator):
        text = ' '
        for field in fields:
            text += str(field) + separator
        return text + '\n'
Пример #15
0
class PiPresents(object):

    def __init__(self):
        gc.set_debug(gc.DEBUG_UNCOLLECTABLE|gc.DEBUG_INSTANCES|gc.DEBUG_OBJECTS|gc.DEBUG_SAVEALL)
        self.pipresents_issue="1.3"
        self.pipresents_minorissue = '1.3.1g'
        # 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
        self.pp_background='black'

        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:\n{0}".format(pp_dir))
            exit(103)

        
        # 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', 'pp_paths',
                            
                            'HyperlinkShow','RadioButtonShow','ArtLiveShow','ArtMediaShow','MediaShow','LiveShow','MenuShow',
                            'PathManager','ControlsManager','ShowManager','PluginManager',
                            'MplayerDriver','OMXDriver','UZBLDriver',
                            'KbdDriver','GPIODriver','TimeOfDay','ScreenDriver','Animate','OSCDriver'
                            ]

        # Monitor.classes=['PiPresents','ArtMediaShow','VideoPlayer','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.log (self, "Pi Presents is starting, Version:"+self.pipresents_minorissue)
        # self.mon.log (self," OS and separator:" + os.name +'  ' + os.sep)
        self.mon.log(self,"sys.path[0] -  location of code: "+sys.path[0])
        if os.geteuid() !=0:
            user=os.getenv('USER')
        else:
            user = os.getenv('SUDO_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

        # 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
         
        # get home path from -o option
        self.pp_home = pp_paths.get_home(self.options['home'])
        if self.pp_home is None:
            self.end('error','Failed to find pp_home')

        # get profile path from -p option
        # pp_profile is the full path to the directory that contains 
        # pp_showlist.json and other files for the profile
        self.pp_profile = pp_paths.get_profile_dir(self.pp_home, self.options['profile'])
        if self.pp_profile is None:
            self.end('error','Failed to find profile')

        # check profile exists
        if os.path.exists(self.pp_profile):
            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 profile')

        self.mon.start_stats(self.options['profile'])
        
        # check 'verify' option
        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')

        # 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')

        if self.starter_show['start-show']=='':
             self.mon.warn(self,"No Start Shows in Start Show")       

# ********************
# 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.pp_background)

        self.mon.log(self, 'native screen dimensions are ' + str(self.root.winfo_screenwidth()) + ' x ' + str(self.root.winfo_screenheight()) + ' pixcels')
        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, 'commanded screen dimensions are ' + str(self.screen_width) + ' x ' + str(self.screen_height) + ' pixcels')
       
        # 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 1>&- 2>&- &') # Suppress 'someone created a subwindow' complaints from 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')

        # canvs 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.pp_background)


        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 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.exitpipresents_required=False
        
        # 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 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.end('error',message)
            else:
                self.osc_enabled=True
                self.root.after(1000,self.oscdriver.start_server())

        # and run the start shows
        self.run_start_shows()

       # set up the time of day scheduler including catchup         
        self.tod_enabled=False
        if os.path.exists(self.pp_profile + os.sep + 'schedule.json'):
            # kick off the time of day scheduler which may run additional shows
            self.tod=TimeOfDay()
            self.tod.init(pp_dir,self.pp_home,self.pp_profile,self.root,self.handle_command)
            self.tod_enabled = True


        # then start the time of day scheduler
        if self.tod_enabled is True:
            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 --fullscreen comand option',0,0
        elif fields[0].isdigit()  is False or fields[1].isdigit()  is False:
            return 'error','dimensions are not positive integers in ---fullscreen',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):
        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'):
            if self.shutdown_required is False:
                reason,message=self.show_manager.control_a_show(show_ref,show_command)
            else:
                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 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")
        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.pp_background)
        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':
            self.mon.log(self, "Pi Presents Aborted, au revoir")
            # close logging files 
            self.mon.finish()
            sys.exit(101)     
        elif reason == 'error':
            self.mon.log(self, "Pi Presents closing because of error, sorry")
            # close logging files 
            self.mon.finish()
            sys.exit(102)            
        else:
            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', '-h', '-t 5','now'])
                sys.exit(100)
            else:
                sys.exit(100)


    
    # tidy up all the peripheral bits of Pi Presents
    def tidy_up(self):
        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()
Пример #16
0
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
Пример #17
0
class pp_gpiodriver(object):
    """
    pp_gpiodriver provides GPIO facilties for Pi presents
     - configures and binds GPIO pins from data in .cfg file 
     - reads and debounces inputs pins, provides callbacks on state changes which generate input events
    - changes the state 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  (pp_gpiodriver.)
    pins = []
    driver_active = False
    title = ''

    ################ NIK #################
    # motion sensor timeout, i.e. after SHUTOFF_DELAY seconds of no motion
    # pipresents pauses and the monitor turns off
    SHUTOFF_DELAY = 120

    ######################################

    # executed by main program and by each object using gpio
    def __init__(self):
        self.mon = Monitor()

    # executed once from main program
    def init(self, filename, filepath, widget, button_callback=None):

        # instantiate arguments
        self.widget = widget
        self.filename = filename
        self.filepath = filepath
        self.button_callback = button_callback
        pp_gpiodriver.driver_active = False

        ############### NIK ######################
        self.turned_off = False
        self.last_motion_time = time.time()
        ##########################################

        # read gpio.cfg file.
        reason, message = self._read(self.filename, self.filepath)
        if reason == 'error':
            return 'error', message
        if self.config.has_section('DRIVER') is False:
            return 'error', 'No DRIVER section in ' + self.filepath

        #read information from DRIVER section
        pp_gpiodriver.title = self.config.get('DRIVER', 'title')
        button_tick_text = self.config.get('DRIVER', 'tick-interval')
        if button_tick_text.isdigit():
            if int(button_tick_text) > 0:
                self.button_tick = int(button_tick_text)  # in mS
            else:
                return 'error', 'tick-interval is not a positive integer'
        else:
            return 'error', 'tick-interval is not an integer'

        import RPi.GPIO as GPIO
        self.GPIO = GPIO

        # construct the GPIO control list from the configuration
        for index, pin_def in enumerate(pp_gpiodriver.PINLIST):
            pin = copy.deepcopy(pp_gpiodriver.TEMPLATE)
            pin_bits = pin_def.split('-')
            pin_num = pin_bits[1:]
            pin[pp_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[pp_gpiodriver.DIRECTION] = 'None'
            else:
                # unused pin
                if self.config.get(pin_def, 'direction') == 'none':
                    pin[pp_gpiodriver.DIRECTION] = 'none'
                else:
                    pin[pp_gpiodriver.DIRECTION] = self.config.get(
                        pin_def, 'direction')
                    if pin[pp_gpiodriver.DIRECTION] == 'in':
                        # input pin
                        pin[pp_gpiodriver.RISING_NAME] = self.config.get(
                            pin_def, 'rising-name')
                        pin[pp_gpiodriver.FALLING_NAME] = self.config.get(
                            pin_def, 'falling-name')
                        pin[pp_gpiodriver.ONE_NAME] = self.config.get(
                            pin_def, 'one-name')
                        pin[pp_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[pp_gpiodriver.LINKED_NAME] = self.config.get(
                                pin_def, 'linked-output')
                            if self.config.get(pin_def,
                                               'linked-invert') == 'yes':
                                pin[pp_gpiodriver.LINKED_INVERT] = True
                            else:
                                pin[pp_gpiodriver.LINKED_INVERT] = False
                        else:
                            pin[pp_gpiodriver.LINKED_NAME] = ''
                            pin[pp_gpiodriver.LINKED_INVERT] = False

                        if pin[pp_gpiodriver.FALLING_NAME] == 'pp-shutdown':
                            pp_gpiodriver.shutdown_index = index
                        if self.config.get(pin_def, 'repeat') != '':
                            pin[pp_gpiodriver.REPEAT] = int(
                                self.config.get(pin_def, 'repeat'))
                        else:
                            pin[pp_gpiodriver.REPEAT] = -1
                        pin[pp_gpiodriver.THRESHOLD] = int(
                            self.config.get(pin_def, 'threshold'))

                        if self.config.get(pin_def, 'pull-up-down') == 'up':
                            pin[pp_gpiodriver.PULL] = GPIO.PUD_UP
                        elif self.config.get(pin_def,
                                             'pull-up-down') == 'down':
                            pin[pp_gpiodriver.PULL] = GPIO.PUD_DOWN
                        else:
                            pin[pp_gpiodriver.PULL] = GPIO.PUD_OFF
                    else:
                        # output pin
                        pin[pp_gpiodriver.NAME] = self.config.get(
                            pin_def, 'name')

            pp_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(pp_gpiodriver.pins):
            num = pin[pp_gpiodriver.PIN]
            if pin[pp_gpiodriver.DIRECTION] == 'in':
                self.GPIO.setup(num,
                                self.GPIO.IN,
                                pull_up_down=pin[pp_gpiodriver.PULL])
            elif pin[pp_gpiodriver.DIRECTION] == 'out':
                self.GPIO.setup(num, self.GPIO.OUT)
                self.GPIO.setup(num, False)
        self._reset_input_state()

        pp_gpiodriver.driver_active = True

        # init timer
        self.button_tick_timer = None
        return 'normal', pp_gpiodriver.title + ' active'

    # called by main program only
    def start(self):
        # loop to look at the buttons
        self._do_buttons()
        self.button_tick_timer = self.widget.after(self.button_tick,
                                                   self.start)

    # called by main program only
    def terminate(self):
        if pp_gpiodriver.driver_active is True:
            if self.button_tick_timer is not None:
                self.widget.after_cancel(self.button_tick_timer)
            self._reset_outputs()
            self.GPIO.cleanup()
            pp_gpiodriver.driver_active = False

    ################## NIK #######################################
    # turn on and off monitor
    def turn_on_monitor(self):
        subprocess.call("vcgencmd display_power 1 &> /dev/null", shell=True)

    def turn_off_monitor(self):
        subprocess.call("vcgencmd display_power 0 &> /dev/null", shell=True)

    ##############################################################

# ************************************************
# gpio input functions
# called by main program only
# ************************************************

    def _reset_input_state(self):
        for pin in pp_gpiodriver.pins:
            pin[pp_gpiodriver.COUNT] = 0
            pin[pp_gpiodriver.PRESSED] = False
            pin[pp_gpiodriver.LAST] = False
            pin[pp_gpiodriver.REPEAT_COUNT] = pin[pp_gpiodriver.REPEAT]

    def _do_buttons(self):

        ############################ NIK ######################################################
        # If motion hasn't been detected in SHUTOFF_DELAY seconds, turn off monitor and pause
        if not self.turned_off and time.time() > (self.last_motion_time +
                                                  pp_gpiodriver.SHUTOFF_DELAY):
            # print str(time.time()) + " PIR not motion, turned off is " + str(self.turned_off)
            # self.mon.log(self,"PIR not motion. self.pir_switch_on: "+str(self.pir_switch_on)+", self.turned_off: "+str(self.turned_off)+", self.paused: "+str(self.paused))
            # os.system ("echo `date` PIR not motion. self.pir_switch_on: "+str(self.pir_switch_on)+", self.turned_off: "+str(self.turned_off)+", self.paused: "+str(self.paused)+" >> /home/pi/pir.log")
            self.turned_off = True
            # turn off monitor and pause pi presents
            self.turn_off_monitor()
            self.button_callback("pp-pause-on", "GPIO")
        ########################################################################################

        for index, pin in enumerate(pp_gpiodriver.pins):
            if pin[pp_gpiodriver.DIRECTION] == 'in':

                # linked pin
                if pin[pp_gpiodriver.LINKED_NAME] != '':
                    link_pin = self._output_pin_of(
                        pin[pp_gpiodriver.LINKED_NAME])
                    if link_pin != -1:
                        self.GPIO.output(
                            link_pin,
                            self.GPIO.input(pin[pp_gpiodriver.PIN])
                            ^ pin[pp_gpiodriver.LINKED_INVERT])

                # debounce
                if self.GPIO.input(pin[pp_gpiodriver.PIN]) == 0:
                    if pin[pp_gpiodriver.COUNT] < pin[pp_gpiodriver.THRESHOLD]:
                        pin[pp_gpiodriver.COUNT] += 1
                        if pin[pp_gpiodriver.COUNT] == pin[
                                pp_gpiodriver.THRESHOLD]:
                            pin[pp_gpiodriver.PRESSED] = True
                else:  # input us 1
                    if pin[pp_gpiodriver.COUNT] > 0:
                        pin[pp_gpiodriver.COUNT] -= 1
                        if pin[pp_gpiodriver.COUNT] == 0:
                            pin[pp_gpiodriver.PRESSED] = False

                # detect edges
                # falling edge
                if pin[pp_gpiodriver.PRESSED] is True and pin[
                        pp_gpiodriver.LAST] is False:
                    pin[pp_gpiodriver.LAST] = pin[pp_gpiodriver.PRESSED]
                    pin[pp_gpiodriver.REPEAT_COUNT] = pin[pp_gpiodriver.REPEAT]
                    if pin[pp_gpiodriver.
                           FALLING_NAME] != '' and self.button_callback is not None:
                        self.button_callback(pin[pp_gpiodriver.FALLING_NAME],
                                             pp_gpiodriver.title)
            # rising edge
                if pin[pp_gpiodriver.PRESSED] is False and pin[
                        pp_gpiodriver.LAST] is True:
                    pin[pp_gpiodriver.LAST] = pin[pp_gpiodriver.PRESSED]
                    pin[pp_gpiodriver.REPEAT_COUNT] = pin[pp_gpiodriver.REPEAT]

                    ############################ NIK ############################################################
                    if pin[pp_gpiodriver.
                           RISING_NAME] == 'PIR' and self.button_callback is not None:
                        self.last_motion_time = time.time()
                        # print "PIR detected: " + str(self.last_motion_time)
                        if self.turned_off:
                            self.turned_off = False
                            self.turn_on_monitor()
                            self.button_callback("pp-pause-off", "GPIO")
##############################################################################################

                    elif pin[
                            pp_gpiodriver.
                            RISING_NAME] != '' and self.button_callback is not None:
                        self.button_callback(pin[pp_gpiodriver.RISING_NAME],
                                             pp_gpiodriver.title)

                # do state callbacks
                if pin[pp_gpiodriver.REPEAT_COUNT] == 0:
                    if pin[pp_gpiodriver.ZERO_NAME] != '' and pin[
                            pp_gpiodriver.
                            PRESSED] is True and self.button_callback is not None:
                        self.button_callback(pin[pp_gpiodriver.ZERO_NAME],
                                             pp_gpiodriver.title)
                    if pin[pp_gpiodriver.ONE_NAME] != '' and pin[
                            pp_gpiodriver.
                            PRESSED] is False and self.button_callback is not None:
                        self.button_callback(pin[pp_gpiodriver.ONE_NAME],
                                             pp_gpiodriver.title)
                    pin[pp_gpiodriver.REPEAT_COUNT] = pin[pp_gpiodriver.REPEAT]
                else:
                    if pin[pp_gpiodriver.REPEAT] != -1:
                        pin[pp_gpiodriver.REPEAT_COUNT] -= 1

    def get_input(self, channel):
        return False, None

# ************************************************
# gpio output interface methods
# these can be called from many classes so need to operate on class variables
# ************************************************

# execute an output event

    def handle_output_event(self, name, param_type, param_values, req_time):
        # print 'GPIO handle',name,param_type,param_values
        # does the symbolic name match any output pin
        pin = self._output_pin_of(name)
        if pin == -1:
            return 'normal', pp_gpiodriver.title + 'Symbolic name not recognised: ' + name

        #gpio only handles state parameters, ignore otherwise
        if param_type != 'state':
            return 'normal', pp_gpiodriver.title + ' does not handle: ' + param_type

        to_state = param_values[0]
        if to_state not in ('on', 'off'):
            return 'error', pp_gpiodriver.title + ', illegal parameter value for ' + param_type + ': ' + to_state

        if to_state == 'on':
            state = True
        else:
            state = False

        # print 'pin P1-'+ str(pin)+ ' set  '+ str(state) + ' required: ' + str(req_time)+ ' actual: ' + str(long(time.time()))
        self.GPIO.output(pin, state)
        return 'normal', pp_gpiodriver.title + ' pin P1-' + str(
            pin) + ' set  ' + str(state) + ' required at: ' + str(
                req_time) + ' sent at: ' + str(long(time.time()))

    def _reset_outputs(self):
        if pp_gpiodriver.driver_active is True:
            for index, pin in enumerate(pp_gpiodriver.pins):
                num = pin[pp_gpiodriver.PIN]
                if pin[pp_gpiodriver.DIRECTION] == 'out':
                    self.GPIO.output(num, False)

    def is_active(self):
        return pp_gpiodriver.driver_active

# ************************************************
# 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 pp_gpiodriver.pins:
            # print " in list" + pin[pp_gpiodriver.NAME] + str(pin[pp_gpiodriver.PIN] )
            if pin[pp_gpiodriver.NAME] == name and pin[
                    pp_gpiodriver.DIRECTION] == 'out':
                # print " linked pin " + pin[pp_gpiodriver.NAME] + ' ' + str(pin[pp_gpiodriver.PIN] )
                return pin[pp_gpiodriver.PIN]
        return -1


# ***********************************
# reading .cfg file
# ************************************

    def _read(self, filename, filepath):
        if os.path.exists(filepath):
            self.config = ConfigParser.ConfigParser()
            self.config.read(filepath)
            return 'normal', filename + ' read'
        else:
            return 'error', filename + ' not found at: ' + filepath
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
Пример #19
0
class pp_inputdevicedriver(object):
    """
    pp_inputdevicedriver interfaces with the Linux generic input devices, for example USB keybads or remotes
    that is, ones that have a file in /dev/input
     - configures and binds key codes from specified .cfg file
     - reads input events from /dev/input and decodes them using evdev, provides callbacks on state changes which generate input events
    """

    # constants for buttons

    # cofiguration from gpio.cfg
    KEY_CODE = 0  # pin on RPi board GPIO connector e.g. P1-11
    DIRECTION = 1  # IN/OUT/NONE (None is not used)
    RELEASED_NAME = 2  # symbolic name for released callback
    PRESSED_NAME = 3  # symbolic name of pressed callback
    UP_NAME = 4  # symbolic name for up state callback
    DOWN_NAME = 5  # symbolic name for down state callback
    REPEAT = 6  # repeat interval for state callbacks (mS)

    # dynamic data
    PRESSED = 7  # variable - debounced state
    LAST = 8  # varible - last state - used to detect edge
    REPEAT_COUNT = 9

    TEMPLATE = [
        '',  # key code
        '',  # direction
        '',
        '',
        '',
        '',  #input names
        0,  # repeat
        False,
        False,
        0
    ]  #dynamics - pressed,last,repeat count

    # CLASS VARIABLES  (pp_inputdevicedriver.)
    driver_active = False
    title = ''
    KEYCODELIST = []  # list of keycodes read from .cfg file
    key_codes = [
    ]  #dynamic list constructed from .cfg file and other dynamic variables
    device_refs = [
    ]  # list of references to devices obtained by matching devide name against /dev/input

    # executed by main program and by each object using gpio
    def __init__(self):
        self.mon = Monitor()

    # executed once from main program
    def init(self,
             filename,
             filepath,
             widget,
             pp_dir,
             pp_home,
             pp_profile,
             button_callback=None):

        # instantiate arguments
        self.widget = widget
        self.filename = filename
        self.filepath = filepath
        self.button_callback = button_callback

        pp_inputdevicedriver.driver_active = False

        # read .cfg file.
        reason, message = self._read(self.filename, self.filepath)
        if reason == 'error':
            return 'error', message
        if self.config.has_section('DRIVER') is False:
            return 'error', 'no DRIVER section in ' + self.filename

        #read information from DRIVER section
        pp_inputdevicedriver.title = self.config.get('DRIVER', 'title')
        self.button_tick = int(self.config.get('DRIVER',
                                               'tick-interval'))  # in mS
        keycode_text = self.config.get('DRIVER', 'key-codes')
        pp_inputdevicedriver.KEYCODELIST = keycode_text.split(',')

        # construct the key_code control list from the configuration
        for index, key_code_def in enumerate(pp_inputdevicedriver.KEYCODELIST):
            if self.config.has_section(key_code_def) is False:
                self.mon.warn(self,
                              "no key code definition for " + key_code_def)
                continue
            else:
                entry = copy.deepcopy(pp_inputdevicedriver.TEMPLATE)
                # unused pin
                entry[pp_inputdevicedriver.KEY_CODE] = key_code_def
                if self.config.get(key_code_def, 'direction') == 'none':
                    entry[pp_inputdevicedriver.DIRECTION] = 'none'
                else:
                    entry[pp_inputdevicedriver.DIRECTION] = self.config.get(
                        key_code_def, 'direction')
                    if entry[pp_inputdevicedriver.DIRECTION] == 'in':
                        # input pin
                        entry[pp_inputdevicedriver.
                              RELEASED_NAME] = self.config.get(
                                  key_code_def, 'released-name')
                        entry[pp_inputdevicedriver.
                              PRESSED_NAME] = self.config.get(
                                  key_code_def, 'pressed-name')
                        entry[pp_inputdevicedriver.UP_NAME] = self.config.get(
                            key_code_def, 'up-name')
                        entry[
                            pp_inputdevicedriver.DOWN_NAME] = self.config.get(
                                key_code_def, 'down-name')

                        if self.config.get(key_code_def, 'repeat') != '':
                            entry[pp_inputdevicedriver.REPEAT] = int(
                                self.config.get(key_code_def, 'repeat'))
                        else:
                            entry[pp_inputdevicedriver.REPEAT] = -1

            # add entry to working list of key_codes  (list of lists [0] of sublist is key code
            pp_inputdevicedriver.key_codes.append(copy.deepcopy(entry))

        # find the input device
        device_name = self.config.get('DRIVER', 'device-name')

        pp_inputdevicedriver.device_refs = self.find_devices(device_name)
        if pp_inputdevicedriver.device_refs == []:
            return 'warn', 'Cannot find device: ' + device_name + ' for ' + pp_inputdevicedriver.title

        pp_inputdevicedriver.driver_active = True

        # init timer
        self.button_tick_timer = None
        self.mon.log(self, pp_inputdevicedriver.title + ' active')
        return 'normal', pp_inputdevicedriver.title + ' active'

    def find_devices(self, name):
        devices = [evdev.InputDevice(fn) for fn in evdev.list_devices()]
        matching = []
        # print '\nMatching devices references: '
        for device in devices:
            if name in device.name:
                # print '     Ref:',device.fn, ' Name: ',device.name
                device_ref = evdev.InputDevice(device.fn)
                matching.append(device_ref)
        return matching

    # called by main program only
    def start(self):
        # poll for events then update all the buttons once
        self.do_buttons()
        self.button_tick_timer = self.widget.after(self.button_tick,
                                                   self.start)

    # allow track plugins (or anyting else) to access input values
    def get_input(self, channel):
        return False, None

    # called by main program only
    def terminate(self):
        if pp_inputdevicedriver.driver_active is True:
            if self.button_tick_timer is not None:
                self.widget.after_cancel(self.button_tick_timer)
            pp_inputdevicedriver.driver_active = False

# ************************************************
# inputdevice functions
# called by main program only
# ************************************************

    def reset_input_state(self):
        for entry in pp_inputdevicedriver.key_codes:
            entry[pp_inputdevicedriver.PRESSED] = False
            entry[pp_inputdevicedriver.LAST] = False
            entry[pp_inputdevicedriver.REPEAT_COUNT] = entry[
                pp_inputdevicedriver.REPEAT]

    def is_active(self):
        return pp_inputdevicedriver.driver_active

    def do_buttons(self):

        # get and process new events from remote
        self.get_events(pp_inputdevicedriver.device_refs)

        # do the things that are done every tick
        for index, entry in enumerate(pp_inputdevicedriver.key_codes):
            if entry[pp_inputdevicedriver.DIRECTION] == 'in':

                # detect falling edge
                if entry[pp_inputdevicedriver.PRESSED] is True and entry[
                        pp_inputdevicedriver.LAST] is False:
                    entry[pp_inputdevicedriver.LAST] = entry[
                        pp_inputdevicedriver.PRESSED]
                    entry[pp_inputdevicedriver.REPEAT_COUNT] = entry[
                        pp_inputdevicedriver.REPEAT]
                    if entry[
                            pp_inputdevicedriver.
                            PRESSED_NAME] != '' and self.button_callback is not None:
                        self.button_callback(
                            entry[pp_inputdevicedriver.PRESSED_NAME],
                            pp_inputdevicedriver.title)
            # detect rising edge
                if entry[pp_inputdevicedriver.PRESSED] is False and entry[
                        pp_inputdevicedriver.LAST] is True:
                    entry[pp_inputdevicedriver.LAST] = entry[
                        pp_inputdevicedriver.PRESSED]
                    entry[pp_inputdevicedriver.REPEAT_COUNT] = entry[
                        pp_inputdevicedriver.REPEAT]
                    if entry[
                            pp_inputdevicedriver.
                            RELEASED_NAME] != '' and self.button_callback is not None:
                        self.button_callback(
                            entry[pp_inputdevicedriver.RELEASED_NAME],
                            pp_inputdevicedriver.title)

                # do state callbacks
                if entry[pp_inputdevicedriver.REPEAT_COUNT] == 0:
                    if entry[pp_inputdevicedriver.DOWN_NAME] != '' and entry[
                            pp_inputdevicedriver.
                            PRESSED] is True and self.button_callback is not None:
                        self.button_callback(
                            entry[pp_inputdevicedriver.DOWN_NAME],
                            pp_inputdevicedriver.title)
                    if entry[pp_inputdevicedriver.UP_NAME] != '' and entry[
                            pp_inputdevicedriver.
                            PRESSED] is False and self.button_callback is not None:
                        self.button_callback(
                            entry[pp_inputdevicedriver.UP_NAME],
                            pp_inputdevicedriver.title)
                    entry[pp_inputdevicedriver.REPEAT_COUNT] = entry[
                        pp_inputdevicedriver.REPEAT]
                else:
                    if entry[pp_inputdevicedriver.REPEAT] != -1:
                        entry[pp_inputdevicedriver.REPEAT_COUNT] -= 1

    def find_event_entry(self, event_code):
        for entry in pp_inputdevicedriver.key_codes:
            if entry[pp_inputdevicedriver.KEY_CODE] == event_code:
                # print entry
                return entry
        return None

    def get_events(self, matching):
        # some input devices have more than one logical input device so use select to poll all of the devices
        r, w, x = select(matching, [], [], 0)
        if r == [] and w == [] and x == []:
            # print 'no event'
            return
        self.process_events(r)

    def process_events(self, r):
        for re in r:
            for event in re.read():
                # print '\nEvent Rxd: ',event,'\n',evdev.categorize(event)
                if event.type == evdev.ecodes.EV_KEY:
                    # print 'Key Event Rxd: ',event
                    key_event = evdev.categorize(event)
                    #key_event.keycode=[key_event.keycode,'fred']
                    #print (key_event)
                    if type(key_event.keycode) is list:
                        for kc in key_event.keycode:
                            self.process_event(kc, key_event.keystate)
                    else:
                        self.process_event(key_event.keycode,
                                           key_event.keystate)

    def process_event(self, event_code, button_state):
        event_entry = self.find_event_entry(event_code)
        if event_entry is None or event_entry[
                pp_inputdevicedriver.DIRECTION] != 'in':
            self.mon.warn(
                self,
                'input event key code not in list of key codes or direction not in'
            )
        else:
            if button_state == 1:
                # print event_code, 'Pressed'
                event_entry[pp_inputdevicedriver.PRESSED] = True
            elif button_state == 0:
                # print event_code, 'Released'
                event_entry[pp_inputdevicedriver.PRESSED] = False
            else:
                # ignore other button states
                pass
                # print 'unknown state: ',button_state

# ***********************************
# output events
# ************************************

# execute an output event

    def handle_output_event(self, name, param_type, param_values, req_time):
        return 'normal', pp_inputdevicedriver.title + ' does not accept outputs'

    def reset_outputs(self):
        return


# ***********************************
# reading .cfg file
# ************************************

    def _read(self, filename, filepath):
        # try inside profile
        if os.path.exists(filepath):
            self.config = configparser.ConfigParser(
                inline_comment_prefixes=(';', ))
            self.config.read(filepath)
            self.mon.log(self, filename + " read from " + filepath)
            return 'normal', filename + ' read'
        else:
            return 'error', filename + ' not found at: ' + filepath
Пример #20
0
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
Пример #21
0
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
Пример #22
0
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']
Пример #23
0
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 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']
Пример #25
0
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
Пример #26
0
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=''
        self.xafter=''



    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
        # print self.dbus_user, 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('/dev/null','a'),stderr=file('/dev/null','a'))
        # stdout=file('/dev/null','a'),stderr=file('/dev/null','a')
        self.pid=self._process.pid

        # wait for omxplayer to start
        tries=0
        while tries<400:
            # print 'trying',tries
            success=self.__dbus_connect()
            tries+=1
            sleep (0.1)
            if success is True: break
            
        if success is False:
            self.end_play_signal=True
            self.end_play_reason='Failed to connect to omxplayer DBus'
            return

        self.mon.log(self,'connected to omxplayer dbus ' + str(success))
            
        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
        success,duration=self.get_duration()
        if 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:
            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()


    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 prsecc 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:
                        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
                            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.Pause()
            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.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 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):
        # 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)
        try:
            with open(bus_address_filename, "r") as f:
                bus_address = f.read().rstrip()
            with open(bus_pid_filename, "r") as f:
                bus_pid = f.read().rstrip()
        except IOError:
            self.mon.trace(self,"Waiting for D-Bus session for user {}. Is omxplayer running under this user?".format(self.dbus_user))
            return False

        # looks like this saves some file opens as vars are used instead of files above
        os.environ["DBUS_SESSION_BUS_ADDRESS"] = bus_address
        os.environ["DBUS_SESSION_BUS_PID"] = bus_pid
        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.trace(self,"Waiting for dbus connection to omxplayer: {}".format(ex.get_dbus_message()))
            return False
        return True
Пример #27
-1
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