Module PiMan

Expand source code
# ****************************************************
# Copyright: 2020 Team Visualizer (Carlos Miguel Sayao, Connor Bettermann, Issac Greenfield, Madeleine Elyea, Tanner Sundwall, Ted Moore, Prerna Agarwal)
# License: MIT
# ****************************************************
# Purpose:  Global command buffer queue data structure.
# Purpose:  manages all functionality of the Pi and our program’s processes post-boot
#           Retains boolean representation of status as “Leader” otherwise is “Follower”
#           If “leadc_queue.IsLeader == True), listens for mouse inputs and sends commands out through ethernet
#           If “followc_queue.IsLeader == False), listens for ethernet data and follows those commands
# Sources:  DocStrings/Comments, https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html
#           Python Coding conventions, https://www.python.org/dev/peps/pep-0008/#source-file-encoding
# ****************************************************

import os
import sys
import time
import threading
import subprocess
from datetime import datetime
from datetime import timedelta
import concurrent.futures
from ErrorHandler import ErrorHandler
from Commands import Commands
from UserInput import UserInput
from VLCMan import VLCMan
from EtherTalk import EtherTalk
from MessageMan import MessageMan
from constants import *
from OrderCode import *
from Device import Device
import Commands as c_queue
c_queue.init()


class PiMan:
    def __init__(self, error_handler=None):
        self.is_shutdown_time = False
        self.leader_response_time = None
        self.has_follower = False
        self.eh = error_handler
        self.timestamp = datetime.now() + timedelta(seconds=IDLE_SEND_TIME)
        self.has_checked_ping = False

    def waiting_loop(self) -> bool:
        """ This is the main-loop, active function for this program. It checks 
        for input from a user or other RasPis, and calls appropriate functions 
        when needed.

        Args:
            None

        Returns:
            bool: The return value indicates if everything closed properly.
                True for success, False otherwise.
        """
        # Get object resources up and running
        et = EtherTalk(error_handler=self.eh, verbose=False)
        mm = MessageMan(error_handler=self.eh)
        vlc = VLCMan(VIDEOS_PATH, PLAYLIST_NAME, "", error_handler=self.eh)
        device = Device().has_device

        if not self.get_started(vlc, et, mm, device):
            # Problem at startup, restart from scratch
            os.system('sudo shutdown -r now')

        self.time = datetime.now().second
        while not self.is_shutdown_time:
            # If leader check for user input and send commands
            if c_queue.IsLeader == True:
                device = Device().has_device
                self.next_command(vlc, et, mm, device)
                # Check to make sure a follower is still on the network
                if datetime.now().second > 57 and not self.has_checked_ping:
                    # Loop for three seconds, pinging the follower for a response
                    loop_end_time = datetime.now() + timedelta(seconds=3)
                    self.has_checked_ping = True
                    while True:
                        if self.send_orders("", et, mm, 0, OrderCode.PING_RESPONSE):
                            break
                        elif datetime.now() >= loop_end_time:
                            self.has_follower = False
                            break
                elif datetime.now().second > 53 and datetime.now().second <= 56:
                    self.has_checked_ping = False
            else:  # RasPi is the follower
                # Check if we need to become leader, dequeue, execute commands
                self.next_command_follower(mm, vlc)

            # Check time for shutdown
            self.is_shutdown_time = self.check_time()

        # Out of the Loop means it is Shutdown time for all units
        # Send shutdown signal 5 times, in case of false negative
        for i in range(4):
            if self.send_orders("", et, mm, 0, OrderCode.SHUT_DOWN):
                break
            time.sleep(5)
            
        # Then shutdown
        self.shutdown()

    def get_started(self, vlc, et, mm, device) -> bool:
        """ Responsible for getting ready all pre-visual settings

        Args:
            vlc (VLCMan): VLC manager with current playlist information
            et (EtherTalk): Communicator to other Raspis on the network
            mm (MessageMan): Information manager to compose/parse communications

        Returns:
            bool: True indicates that this unit is the leader, and needs to 
            send out the playlist it created, False indicates it is the 
            follower, and will have waited for the new playlist within this 
            function.
        """
        try:
            os.remove(PLAYLIST_NAME)
        except:
            pass

        try:
            # Check for usb device, this unit should be the Follower if
            # not found and no one else is on the network. Performed for 3 minutes.
            if not device:
                if et.ping() == 0:
                    print("[!] I am the follower (งツ)ว")
                    c_queue.IsLeader = False
                    self.listener(vlc, et)
                else:
                    print("[!] I am the leader ᕕ( ᐛ )ᕗ")
                    c_queue.IsLeader = True
                    UserInput(self.eh).input_listener()
            else:
                print("[!] I am the leader ᕕ( ᐛ )ᕗ")
                c_queue.IsLeader = True
                UserInput(self.eh).input_listener()
        except Exception as e:  # Something went wrong with the startup
            self.eh.log_error(e)
            return False

        # Playlist setup logic. Dependant on isLeader status
        try:
            # This is the Leader RasPi
            if c_queue.IsLeader == True:
                self.create_playlist(vlc)
            else:
                vlc.start_vlc()

        except Exception as e:
            self.eh.log_error(e)
            return False
        # This point indicates no major exceptions have occurred for either the
        # Leader or the Follower
        return True

    def shutdown(self):
        """ Safely exits and shuts down the RasPi unit

        Args:
            None

        Returns:
            None
        """
        subprocess.Popen("sudo shutdown -P now", shell=True)

    def check_time(self) -> bool:
        """ Keeps track of the time for autoshutdown and syncing with other
        RasPis on the network

        Args:
            None

        Returns:
            bool: True means the Pi should shutdown, False the program should 
            continue.
        """
        # NOTE: Remove after testing. Returns False so that it doesn't shutdown with testing
        return False
    
        hour = datetime.now().hour
        if hour >= SEVEN_AM and hour < SEVEN_PM:
            return True
        else:
            return False

    def exec_command(self, vlc, time_start, command) -> bool:
        """ Executes the argument `command` at the future start time.

        Args:
            vlc (VLCMan): VLC manager with current playlist information
            time_start (datetime.datetime): Future time to execute the order at
            command (OrderCode): Command to execute

        Returns:
            bool: True if successful, False if command is NONE or unsuccessful.
        """
        # Wait until the exact time we need to do the command
        while datetime.now() < time_start:
            pass
        
        if command == OrderCode.SHUT_DOWN:
            self.is_shutdown_time = True
            return True
        elif command == OrderCode.PLAYLIST:
            vlc.start_vlc()
            return True
        elif command == OrderCode.SLOW_DOWN:
            vlc.set_rate(-.1)
            return True
        elif command == OrderCode.SPEED_UP:
            vlc.set_rate(.1)
            return True
        elif command == OrderCode.LOOP_PLAY:
            vlc.toggle_loop_video()
            return True
        elif command == OrderCode.RESUME_LIST:
            vlc.reset_rate()
            vlc.next_video()
            return True
        elif command == OrderCode.RESUME_SPEED:
            vlc.reset_rate()
            return True
        elif command == OrderCode.PAUSE_PLAYBACK:
            vlc.play_pause_video()
            return True
        elif command == OrderCode.NEXT_VIDEO:
            vlc.next_video()
            vlc.set_loop_playlist()
            return True
        elif command == OrderCode.IDLE:
            return True
        elif command == OrderCode.NONE:
            return False
        return False

    def create_playlist(self, vlc):
        """ Update playlist information and package it in the message manager
        for transmission to other networked RasPis.

        Args:
            mm (MessageMan): The order information manager to compose and
                parse communications
            vlc (VLCMan): The VLC manager with current playlist and
                song information

        Returns: 
            None
        """
        try:
            if vlc.randomize_videos():
                print("Videos Randomized")
            if vlc.create_playlist():
                print("Playlist Created")
            if vlc.start_vlc():
                print("VLC Started")
        except Exception as e:
            self.eh.log_error(e)
            raise

    def send_orders(self, message, et, mm, time_start, command) -> bool:
        """ Issue instructions to other RasPis on the network

        Args:
            message (str): unused
            et (EtherTalk): The ethernet communication handler instance
            mm (MessageMan): The order information manager to compose and
                parse communications
            time_start (int): time offset for when the command will start
            command (enum OrderCode): command to be sent

        Returns:
            bool: True indicates that another RasPi has responded to the order,
                False indicates that either there was no answer,
                or an unrecoverable error has occurred for the other unit.
        """
        # Create dict to send
        message_dict = mm.compose_message(message, time_start, command)
        if et.send(message_dict) == 0:
            return True
        return False

    def next_command(self, vlc, et, mm, device):
        """ Issue instructions to other host on the network

        Args:
            vlc (VLCMan): The VLC manager with current playlist and
                song information
            et (EtherTalk): The ethernet communication handler instance
            mm (MessageMan): The order information manager to compose and
                parse communications

        Returns:
            bool: True indicates that another RasPi has responded to the order,
                False indicates that either there was no answer,
                or an unrecoverable error has occurred for the other unit.
        """
        try:
            # No mouse input and no queue, listen for another leader and give up Leadership
            if c_queue.CommandQueue.empty() and not device:
                if et.ping(45) == 0:
                    print("[!] I am the follower (งツ)ว")
                    c_queue.IsLeader = False
                    self.listener(vlc, et)
                    Commands(self.eh).flush_queue()
                    return
            # If we have something in our queue 
            elif not c_queue.CommandQueue.empty():
                # Grab next dict head from queue
                command_header = Commands(self.eh).dequeue()
                code = command_header['code']
                # If this comes back false, we have no follower
                if self.send_orders("", et, mm, START_TIME, OrderCode(command_header['code'])):
                    # If we didn't already have a follower, send the playlist
                    if not self.has_follower:
                        # Generate header to send playlist
                        header = mm.compose_message(
                            "", PLAYLIST_START_TIME, OrderCode.SEND_FILE)
                        et.send_file(header, PLAYLIST_NAME)
                        Commands(self.eh).flush_queue()
                        code = OrderCode.PLAYLIST
                        mm.start_time += 0.6
                    self.has_follower = True
                # We are leader, if execution is OK,
                if self.exec_command(vlc, datetime.fromtimestamp(mm.start_time), code):
                    self.leader_response_time = None
            # Otherwise, send IDLE to follower to indicate we are still alive every 10 seconds
            elif self.timestamp < datetime.now():
                self.timestamp = datetime.now() + timedelta(seconds=IDLE_SEND_TIME)
                code = OrderCode.IDLE
                # If we are successfully sending the idle, we still have follower
                if self.send_orders("", et, mm, START_TIME, code):
                    # If we didn't already have a follower, send the playlist
                    if not self.has_follower:
                        # Generate header to send playlist
                        header = mm.compose_message(
                            "", PLAYLIST_START_TIME, OrderCode.SEND_FILE)
                        et.send_file(header, PLAYLIST_NAME)
                        Commands(self.eh).flush_queue()
                        code = OrderCode.PLAYLIST
                        mm.start_time += 0.6
                    self.has_follower = True
                if self.exec_command(vlc, datetime.fromtimestamp(mm.start_time), code):
                    self.leader_response_time = None
        except Exception as e:
            self.eh.log_error(e)
            raise

    def listener(self, vlc, et):
        """ Follower wrapper to spawn listener thread

        Args:
            et (EtherTalk): The Communicator to other Raspis on the network
            vlc (VLCMan): VLC manager with current playlist information

        Returns:
            None
        """
        thread = threading.Thread(
            target=self.__listener, args=(vlc, et), daemon=True)
        thread.start()

    def __listener(self, vlc, et):
        """ Follower unit listens for instructions from Leader

        Args:
            et (EtherTalk): The Communicator to other Raspis on the network
            vlc (VLCMan): VLC manager with current playlist information

        Returns:
            None
        """
        try:
            # Stop this thread if you are no longer a follower
            while c_queue.IsLeader == False:
                # Receive status and header to display
                status, header = et.listen(path=WRITE_PATH)
                if status == 0:
                    # If we're receiving a playlist, we want to clear our queue
                    # and immediately play playlist at start_time
                    if header['code'] == OrderCode.SEND_FILE:
                        header['code'] = OrderCode.PLAYLIST
                        Commands(self.eh).flush_queue()
                    # Enqueue the new command
                    Commands(self.eh).enqueue(header)
        except Exception as e:
            self.eh.log_error(e)
            raise

    def next_command_follower(self, mm, vlc):
        """ Checks to see if follower needs to take leader status. Executes 
        next command in queue regardless.

        Args:
            mm (MessageMan): The message interpretor for et communications
            vlc (VLCMan): VLC manager with current playlist information

        Returns:
            None
        """

        try:
            # I want to check for empty queue and not add "NONE's" on the queue.
            # I believe it is causing syncing problems, so only exec_command queue's
            # with commands in them, otherwise we are either not getting commands or
            # the commands being sent are taking awhile to get here/being processed.
            # So I set the takeover time to be 3 mins in the future from when we have
            # an empty queue.
            if c_queue.CommandQueue.empty() and self.leader_response_time is None:
                self.leader_response_time = datetime.now(
                ) + timedelta(minutes=FOLLOWER_TAKEOVER_WAIT)
                
            elif not c_queue.CommandQueue.empty():
                # Dequeue next command header to be executed
                command_header = Commands(self.eh).dequeue()
                command_code = command_header['code']
                command_start = datetime.fromtimestamp(
                    command_header['start_time'])
                # Execute the command. NONE and an error will return False.
                if self.exec_command(vlc, command_start, command_code):
                    self.leader_response_time = None
            # We have waiting for a response from the leader and nothing.
            # So become the leader and start up the input listender.
            elif self.leader_response_time is not None and self.leader_response_time < datetime.now():
                print("[!] I am the leader ᕕ( ᐛ )ᕗ")
                # this should prob just restart the code from main....
                self.leader_response_time = None
                Commands(self.eh).flush_queue()
                c_queue.IsLeader = True
                UserInput(self.eh).input_listener()
        except Exception as e:
            self.eh.log_error(e)
            raise

Classes

class PiMan (error_handler=None)
Expand source code
class PiMan:
    def __init__(self, error_handler=None):
        self.is_shutdown_time = False
        self.leader_response_time = None
        self.has_follower = False
        self.eh = error_handler
        self.timestamp = datetime.now() + timedelta(seconds=IDLE_SEND_TIME)
        self.has_checked_ping = False

    def waiting_loop(self) -> bool:
        """ This is the main-loop, active function for this program. It checks 
        for input from a user or other RasPis, and calls appropriate functions 
        when needed.

        Args:
            None

        Returns:
            bool: The return value indicates if everything closed properly.
                True for success, False otherwise.
        """
        # Get object resources up and running
        et = EtherTalk(error_handler=self.eh, verbose=False)
        mm = MessageMan(error_handler=self.eh)
        vlc = VLCMan(VIDEOS_PATH, PLAYLIST_NAME, "", error_handler=self.eh)
        device = Device().has_device

        if not self.get_started(vlc, et, mm, device):
            # Problem at startup, restart from scratch
            os.system('sudo shutdown -r now')

        self.time = datetime.now().second
        while not self.is_shutdown_time:
            # If leader check for user input and send commands
            if c_queue.IsLeader == True:
                device = Device().has_device
                self.next_command(vlc, et, mm, device)
                # Check to make sure a follower is still on the network
                if datetime.now().second > 57 and not self.has_checked_ping:
                    # Loop for three seconds, pinging the follower for a response
                    loop_end_time = datetime.now() + timedelta(seconds=3)
                    self.has_checked_ping = True
                    while True:
                        if self.send_orders("", et, mm, 0, OrderCode.PING_RESPONSE):
                            break
                        elif datetime.now() >= loop_end_time:
                            self.has_follower = False
                            break
                elif datetime.now().second > 53 and datetime.now().second <= 56:
                    self.has_checked_ping = False
            else:  # RasPi is the follower
                # Check if we need to become leader, dequeue, execute commands
                self.next_command_follower(mm, vlc)

            # Check time for shutdown
            self.is_shutdown_time = self.check_time()

        # Out of the Loop means it is Shutdown time for all units
        # Send shutdown signal 5 times, in case of false negative
        for i in range(4):
            if self.send_orders("", et, mm, 0, OrderCode.SHUT_DOWN):
                break
            time.sleep(5)
            
        # Then shutdown
        self.shutdown()

    def get_started(self, vlc, et, mm, device) -> bool:
        """ Responsible for getting ready all pre-visual settings

        Args:
            vlc (VLCMan): VLC manager with current playlist information
            et (EtherTalk): Communicator to other Raspis on the network
            mm (MessageMan): Information manager to compose/parse communications

        Returns:
            bool: True indicates that this unit is the leader, and needs to 
            send out the playlist it created, False indicates it is the 
            follower, and will have waited for the new playlist within this 
            function.
        """
        try:
            os.remove(PLAYLIST_NAME)
        except:
            pass

        try:
            # Check for usb device, this unit should be the Follower if
            # not found and no one else is on the network. Performed for 3 minutes.
            if not device:
                if et.ping() == 0:
                    print("[!] I am the follower (งツ)ว")
                    c_queue.IsLeader = False
                    self.listener(vlc, et)
                else:
                    print("[!] I am the leader ᕕ( ᐛ )ᕗ")
                    c_queue.IsLeader = True
                    UserInput(self.eh).input_listener()
            else:
                print("[!] I am the leader ᕕ( ᐛ )ᕗ")
                c_queue.IsLeader = True
                UserInput(self.eh).input_listener()
        except Exception as e:  # Something went wrong with the startup
            self.eh.log_error(e)
            return False

        # Playlist setup logic. Dependant on isLeader status
        try:
            # This is the Leader RasPi
            if c_queue.IsLeader == True:
                self.create_playlist(vlc)
            else:
                vlc.start_vlc()

        except Exception as e:
            self.eh.log_error(e)
            return False
        # This point indicates no major exceptions have occurred for either the
        # Leader or the Follower
        return True

    def shutdown(self):
        """ Safely exits and shuts down the RasPi unit

        Args:
            None

        Returns:
            None
        """
        subprocess.Popen("sudo shutdown -P now", shell=True)

    def check_time(self) -> bool:
        """ Keeps track of the time for autoshutdown and syncing with other
        RasPis on the network

        Args:
            None

        Returns:
            bool: True means the Pi should shutdown, False the program should 
            continue.
        """
        # NOTE: Remove after testing. Returns False so that it doesn't shutdown with testing
        return False
    
        hour = datetime.now().hour
        if hour >= SEVEN_AM and hour < SEVEN_PM:
            return True
        else:
            return False

    def exec_command(self, vlc, time_start, command) -> bool:
        """ Executes the argument `command` at the future start time.

        Args:
            vlc (VLCMan): VLC manager with current playlist information
            time_start (datetime.datetime): Future time to execute the order at
            command (OrderCode): Command to execute

        Returns:
            bool: True if successful, False if command is NONE or unsuccessful.
        """
        # Wait until the exact time we need to do the command
        while datetime.now() < time_start:
            pass
        
        if command == OrderCode.SHUT_DOWN:
            self.is_shutdown_time = True
            return True
        elif command == OrderCode.PLAYLIST:
            vlc.start_vlc()
            return True
        elif command == OrderCode.SLOW_DOWN:
            vlc.set_rate(-.1)
            return True
        elif command == OrderCode.SPEED_UP:
            vlc.set_rate(.1)
            return True
        elif command == OrderCode.LOOP_PLAY:
            vlc.toggle_loop_video()
            return True
        elif command == OrderCode.RESUME_LIST:
            vlc.reset_rate()
            vlc.next_video()
            return True
        elif command == OrderCode.RESUME_SPEED:
            vlc.reset_rate()
            return True
        elif command == OrderCode.PAUSE_PLAYBACK:
            vlc.play_pause_video()
            return True
        elif command == OrderCode.NEXT_VIDEO:
            vlc.next_video()
            vlc.set_loop_playlist()
            return True
        elif command == OrderCode.IDLE:
            return True
        elif command == OrderCode.NONE:
            return False
        return False

    def create_playlist(self, vlc):
        """ Update playlist information and package it in the message manager
        for transmission to other networked RasPis.

        Args:
            mm (MessageMan): The order information manager to compose and
                parse communications
            vlc (VLCMan): The VLC manager with current playlist and
                song information

        Returns: 
            None
        """
        try:
            if vlc.randomize_videos():
                print("Videos Randomized")
            if vlc.create_playlist():
                print("Playlist Created")
            if vlc.start_vlc():
                print("VLC Started")
        except Exception as e:
            self.eh.log_error(e)
            raise

    def send_orders(self, message, et, mm, time_start, command) -> bool:
        """ Issue instructions to other RasPis on the network

        Args:
            message (str): unused
            et (EtherTalk): The ethernet communication handler instance
            mm (MessageMan): The order information manager to compose and
                parse communications
            time_start (int): time offset for when the command will start
            command (enum OrderCode): command to be sent

        Returns:
            bool: True indicates that another RasPi has responded to the order,
                False indicates that either there was no answer,
                or an unrecoverable error has occurred for the other unit.
        """
        # Create dict to send
        message_dict = mm.compose_message(message, time_start, command)
        if et.send(message_dict) == 0:
            return True
        return False

    def next_command(self, vlc, et, mm, device):
        """ Issue instructions to other host on the network

        Args:
            vlc (VLCMan): The VLC manager with current playlist and
                song information
            et (EtherTalk): The ethernet communication handler instance
            mm (MessageMan): The order information manager to compose and
                parse communications

        Returns:
            bool: True indicates that another RasPi has responded to the order,
                False indicates that either there was no answer,
                or an unrecoverable error has occurred for the other unit.
        """
        try:
            # No mouse input and no queue, listen for another leader and give up Leadership
            if c_queue.CommandQueue.empty() and not device:
                if et.ping(45) == 0:
                    print("[!] I am the follower (งツ)ว")
                    c_queue.IsLeader = False
                    self.listener(vlc, et)
                    Commands(self.eh).flush_queue()
                    return
            # If we have something in our queue 
            elif not c_queue.CommandQueue.empty():
                # Grab next dict head from queue
                command_header = Commands(self.eh).dequeue()
                code = command_header['code']
                # If this comes back false, we have no follower
                if self.send_orders("", et, mm, START_TIME, OrderCode(command_header['code'])):
                    # If we didn't already have a follower, send the playlist
                    if not self.has_follower:
                        # Generate header to send playlist
                        header = mm.compose_message(
                            "", PLAYLIST_START_TIME, OrderCode.SEND_FILE)
                        et.send_file(header, PLAYLIST_NAME)
                        Commands(self.eh).flush_queue()
                        code = OrderCode.PLAYLIST
                        mm.start_time += 0.6
                    self.has_follower = True
                # We are leader, if execution is OK,
                if self.exec_command(vlc, datetime.fromtimestamp(mm.start_time), code):
                    self.leader_response_time = None
            # Otherwise, send IDLE to follower to indicate we are still alive every 10 seconds
            elif self.timestamp < datetime.now():
                self.timestamp = datetime.now() + timedelta(seconds=IDLE_SEND_TIME)
                code = OrderCode.IDLE
                # If we are successfully sending the idle, we still have follower
                if self.send_orders("", et, mm, START_TIME, code):
                    # If we didn't already have a follower, send the playlist
                    if not self.has_follower:
                        # Generate header to send playlist
                        header = mm.compose_message(
                            "", PLAYLIST_START_TIME, OrderCode.SEND_FILE)
                        et.send_file(header, PLAYLIST_NAME)
                        Commands(self.eh).flush_queue()
                        code = OrderCode.PLAYLIST
                        mm.start_time += 0.6
                    self.has_follower = True
                if self.exec_command(vlc, datetime.fromtimestamp(mm.start_time), code):
                    self.leader_response_time = None
        except Exception as e:
            self.eh.log_error(e)
            raise

    def listener(self, vlc, et):
        """ Follower wrapper to spawn listener thread

        Args:
            et (EtherTalk): The Communicator to other Raspis on the network
            vlc (VLCMan): VLC manager with current playlist information

        Returns:
            None
        """
        thread = threading.Thread(
            target=self.__listener, args=(vlc, et), daemon=True)
        thread.start()

    def __listener(self, vlc, et):
        """ Follower unit listens for instructions from Leader

        Args:
            et (EtherTalk): The Communicator to other Raspis on the network
            vlc (VLCMan): VLC manager with current playlist information

        Returns:
            None
        """
        try:
            # Stop this thread if you are no longer a follower
            while c_queue.IsLeader == False:
                # Receive status and header to display
                status, header = et.listen(path=WRITE_PATH)
                if status == 0:
                    # If we're receiving a playlist, we want to clear our queue
                    # and immediately play playlist at start_time
                    if header['code'] == OrderCode.SEND_FILE:
                        header['code'] = OrderCode.PLAYLIST
                        Commands(self.eh).flush_queue()
                    # Enqueue the new command
                    Commands(self.eh).enqueue(header)
        except Exception as e:
            self.eh.log_error(e)
            raise

    def next_command_follower(self, mm, vlc):
        """ Checks to see if follower needs to take leader status. Executes 
        next command in queue regardless.

        Args:
            mm (MessageMan): The message interpretor for et communications
            vlc (VLCMan): VLC manager with current playlist information

        Returns:
            None
        """

        try:
            # I want to check for empty queue and not add "NONE's" on the queue.
            # I believe it is causing syncing problems, so only exec_command queue's
            # with commands in them, otherwise we are either not getting commands or
            # the commands being sent are taking awhile to get here/being processed.
            # So I set the takeover time to be 3 mins in the future from when we have
            # an empty queue.
            if c_queue.CommandQueue.empty() and self.leader_response_time is None:
                self.leader_response_time = datetime.now(
                ) + timedelta(minutes=FOLLOWER_TAKEOVER_WAIT)
                
            elif not c_queue.CommandQueue.empty():
                # Dequeue next command header to be executed
                command_header = Commands(self.eh).dequeue()
                command_code = command_header['code']
                command_start = datetime.fromtimestamp(
                    command_header['start_time'])
                # Execute the command. NONE and an error will return False.
                if self.exec_command(vlc, command_start, command_code):
                    self.leader_response_time = None
            # We have waiting for a response from the leader and nothing.
            # So become the leader and start up the input listender.
            elif self.leader_response_time is not None and self.leader_response_time < datetime.now():
                print("[!] I am the leader ᕕ( ᐛ )ᕗ")
                # this should prob just restart the code from main....
                self.leader_response_time = None
                Commands(self.eh).flush_queue()
                c_queue.IsLeader = True
                UserInput(self.eh).input_listener()
        except Exception as e:
            self.eh.log_error(e)
            raise

Methods

def check_time(self)

Keeps track of the time for autoshutdown and syncing with other RasPis on the network

Args

None
 

Returns

bool
True means the Pi should shutdown, False the program should

continue.

Expand source code
def check_time(self) -> bool:
    """ Keeps track of the time for autoshutdown and syncing with other
    RasPis on the network

    Args:
        None

    Returns:
        bool: True means the Pi should shutdown, False the program should 
        continue.
    """
    # NOTE: Remove after testing. Returns False so that it doesn't shutdown with testing
    return False

    hour = datetime.now().hour
    if hour >= SEVEN_AM and hour < SEVEN_PM:
        return True
    else:
        return False
def create_playlist(self, vlc)

Update playlist information and package it in the message manager for transmission to other networked RasPis.

Args

mm : MessageMan
The order information manager to compose and parse communications
vlc : VLCMan
The VLC manager with current playlist and song information
Returns
None
Expand source code
def create_playlist(self, vlc):
    """ Update playlist information and package it in the message manager
    for transmission to other networked RasPis.

    Args:
        mm (MessageMan): The order information manager to compose and
            parse communications
        vlc (VLCMan): The VLC manager with current playlist and
            song information

    Returns: 
        None
    """
    try:
        if vlc.randomize_videos():
            print("Videos Randomized")
        if vlc.create_playlist():
            print("Playlist Created")
        if vlc.start_vlc():
            print("VLC Started")
    except Exception as e:
        self.eh.log_error(e)
        raise
def exec_command(self, vlc, time_start, command)

Executes the argument command at the future start time.

Args

vlc : VLCMan
VLC manager with current playlist information
time_start : datetime.datetime
Future time to execute the order at
command : OrderCode
Command to execute

Returns

bool
True if successful, False if command is NONE or unsuccessful.
Expand source code
def exec_command(self, vlc, time_start, command) -> bool:
    """ Executes the argument `command` at the future start time.

    Args:
        vlc (VLCMan): VLC manager with current playlist information
        time_start (datetime.datetime): Future time to execute the order at
        command (OrderCode): Command to execute

    Returns:
        bool: True if successful, False if command is NONE or unsuccessful.
    """
    # Wait until the exact time we need to do the command
    while datetime.now() < time_start:
        pass
    
    if command == OrderCode.SHUT_DOWN:
        self.is_shutdown_time = True
        return True
    elif command == OrderCode.PLAYLIST:
        vlc.start_vlc()
        return True
    elif command == OrderCode.SLOW_DOWN:
        vlc.set_rate(-.1)
        return True
    elif command == OrderCode.SPEED_UP:
        vlc.set_rate(.1)
        return True
    elif command == OrderCode.LOOP_PLAY:
        vlc.toggle_loop_video()
        return True
    elif command == OrderCode.RESUME_LIST:
        vlc.reset_rate()
        vlc.next_video()
        return True
    elif command == OrderCode.RESUME_SPEED:
        vlc.reset_rate()
        return True
    elif command == OrderCode.PAUSE_PLAYBACK:
        vlc.play_pause_video()
        return True
    elif command == OrderCode.NEXT_VIDEO:
        vlc.next_video()
        vlc.set_loop_playlist()
        return True
    elif command == OrderCode.IDLE:
        return True
    elif command == OrderCode.NONE:
        return False
    return False
def get_started(self, vlc, et, mm, device)

Responsible for getting ready all pre-visual settings

Args

vlc : VLCMan
VLC manager with current playlist information
et : EtherTalk
Communicator to other Raspis on the network
mm : MessageMan
Information manager to compose/parse communications

Returns

bool
True indicates that this unit is the leader, and needs to
send out the playlist it created, False indicates it is the
 
follower, and will have waited for the new playlist within this
 

function.

Expand source code
def get_started(self, vlc, et, mm, device) -> bool:
    """ Responsible for getting ready all pre-visual settings

    Args:
        vlc (VLCMan): VLC manager with current playlist information
        et (EtherTalk): Communicator to other Raspis on the network
        mm (MessageMan): Information manager to compose/parse communications

    Returns:
        bool: True indicates that this unit is the leader, and needs to 
        send out the playlist it created, False indicates it is the 
        follower, and will have waited for the new playlist within this 
        function.
    """
    try:
        os.remove(PLAYLIST_NAME)
    except:
        pass

    try:
        # Check for usb device, this unit should be the Follower if
        # not found and no one else is on the network. Performed for 3 minutes.
        if not device:
            if et.ping() == 0:
                print("[!] I am the follower (งツ)ว")
                c_queue.IsLeader = False
                self.listener(vlc, et)
            else:
                print("[!] I am the leader ᕕ( ᐛ )ᕗ")
                c_queue.IsLeader = True
                UserInput(self.eh).input_listener()
        else:
            print("[!] I am the leader ᕕ( ᐛ )ᕗ")
            c_queue.IsLeader = True
            UserInput(self.eh).input_listener()
    except Exception as e:  # Something went wrong with the startup
        self.eh.log_error(e)
        return False

    # Playlist setup logic. Dependant on isLeader status
    try:
        # This is the Leader RasPi
        if c_queue.IsLeader == True:
            self.create_playlist(vlc)
        else:
            vlc.start_vlc()

    except Exception as e:
        self.eh.log_error(e)
        return False
    # This point indicates no major exceptions have occurred for either the
    # Leader or the Follower
    return True
def listener(self, vlc, et)

Follower wrapper to spawn listener thread

Args

et : EtherTalk
The Communicator to other Raspis on the network
vlc : VLCMan
VLC manager with current playlist information

Returns

None
 
Expand source code
def listener(self, vlc, et):
    """ Follower wrapper to spawn listener thread

    Args:
        et (EtherTalk): The Communicator to other Raspis on the network
        vlc (VLCMan): VLC manager with current playlist information

    Returns:
        None
    """
    thread = threading.Thread(
        target=self.__listener, args=(vlc, et), daemon=True)
    thread.start()
def next_command(self, vlc, et, mm, device)

Issue instructions to other host on the network

Args

vlc : VLCMan
The VLC manager with current playlist and song information
et : EtherTalk
The ethernet communication handler instance
mm : MessageMan
The order information manager to compose and parse communications

Returns

bool
True indicates that another RasPi has responded to the order, False indicates that either there was no answer, or an unrecoverable error has occurred for the other unit.
Expand source code
def next_command(self, vlc, et, mm, device):
    """ Issue instructions to other host on the network

    Args:
        vlc (VLCMan): The VLC manager with current playlist and
            song information
        et (EtherTalk): The ethernet communication handler instance
        mm (MessageMan): The order information manager to compose and
            parse communications

    Returns:
        bool: True indicates that another RasPi has responded to the order,
            False indicates that either there was no answer,
            or an unrecoverable error has occurred for the other unit.
    """
    try:
        # No mouse input and no queue, listen for another leader and give up Leadership
        if c_queue.CommandQueue.empty() and not device:
            if et.ping(45) == 0:
                print("[!] I am the follower (งツ)ว")
                c_queue.IsLeader = False
                self.listener(vlc, et)
                Commands(self.eh).flush_queue()
                return
        # If we have something in our queue 
        elif not c_queue.CommandQueue.empty():
            # Grab next dict head from queue
            command_header = Commands(self.eh).dequeue()
            code = command_header['code']
            # If this comes back false, we have no follower
            if self.send_orders("", et, mm, START_TIME, OrderCode(command_header['code'])):
                # If we didn't already have a follower, send the playlist
                if not self.has_follower:
                    # Generate header to send playlist
                    header = mm.compose_message(
                        "", PLAYLIST_START_TIME, OrderCode.SEND_FILE)
                    et.send_file(header, PLAYLIST_NAME)
                    Commands(self.eh).flush_queue()
                    code = OrderCode.PLAYLIST
                    mm.start_time += 0.6
                self.has_follower = True
            # We are leader, if execution is OK,
            if self.exec_command(vlc, datetime.fromtimestamp(mm.start_time), code):
                self.leader_response_time = None
        # Otherwise, send IDLE to follower to indicate we are still alive every 10 seconds
        elif self.timestamp < datetime.now():
            self.timestamp = datetime.now() + timedelta(seconds=IDLE_SEND_TIME)
            code = OrderCode.IDLE
            # If we are successfully sending the idle, we still have follower
            if self.send_orders("", et, mm, START_TIME, code):
                # If we didn't already have a follower, send the playlist
                if not self.has_follower:
                    # Generate header to send playlist
                    header = mm.compose_message(
                        "", PLAYLIST_START_TIME, OrderCode.SEND_FILE)
                    et.send_file(header, PLAYLIST_NAME)
                    Commands(self.eh).flush_queue()
                    code = OrderCode.PLAYLIST
                    mm.start_time += 0.6
                self.has_follower = True
            if self.exec_command(vlc, datetime.fromtimestamp(mm.start_time), code):
                self.leader_response_time = None
    except Exception as e:
        self.eh.log_error(e)
        raise
def next_command_follower(self, mm, vlc)

Checks to see if follower needs to take leader status. Executes next command in queue regardless.

Args

mm : MessageMan
The message interpretor for et communications
vlc : VLCMan
VLC manager with current playlist information

Returns

None
 
Expand source code
def next_command_follower(self, mm, vlc):
    """ Checks to see if follower needs to take leader status. Executes 
    next command in queue regardless.

    Args:
        mm (MessageMan): The message interpretor for et communications
        vlc (VLCMan): VLC manager with current playlist information

    Returns:
        None
    """

    try:
        # I want to check for empty queue and not add "NONE's" on the queue.
        # I believe it is causing syncing problems, so only exec_command queue's
        # with commands in them, otherwise we are either not getting commands or
        # the commands being sent are taking awhile to get here/being processed.
        # So I set the takeover time to be 3 mins in the future from when we have
        # an empty queue.
        if c_queue.CommandQueue.empty() and self.leader_response_time is None:
            self.leader_response_time = datetime.now(
            ) + timedelta(minutes=FOLLOWER_TAKEOVER_WAIT)
            
        elif not c_queue.CommandQueue.empty():
            # Dequeue next command header to be executed
            command_header = Commands(self.eh).dequeue()
            command_code = command_header['code']
            command_start = datetime.fromtimestamp(
                command_header['start_time'])
            # Execute the command. NONE and an error will return False.
            if self.exec_command(vlc, command_start, command_code):
                self.leader_response_time = None
        # We have waiting for a response from the leader and nothing.
        # So become the leader and start up the input listender.
        elif self.leader_response_time is not None and self.leader_response_time < datetime.now():
            print("[!] I am the leader ᕕ( ᐛ )ᕗ")
            # this should prob just restart the code from main....
            self.leader_response_time = None
            Commands(self.eh).flush_queue()
            c_queue.IsLeader = True
            UserInput(self.eh).input_listener()
    except Exception as e:
        self.eh.log_error(e)
        raise
def send_orders(self, message, et, mm, time_start, command)

Issue instructions to other RasPis on the network

Args

message : str
unused
et : EtherTalk
The ethernet communication handler instance
mm : MessageMan
The order information manager to compose and parse communications
time_start : int
time offset for when the command will start
command : enum OrderCode
command to be sent

Returns

bool
True indicates that another RasPi has responded to the order, False indicates that either there was no answer, or an unrecoverable error has occurred for the other unit.
Expand source code
def send_orders(self, message, et, mm, time_start, command) -> bool:
    """ Issue instructions to other RasPis on the network

    Args:
        message (str): unused
        et (EtherTalk): The ethernet communication handler instance
        mm (MessageMan): The order information manager to compose and
            parse communications
        time_start (int): time offset for when the command will start
        command (enum OrderCode): command to be sent

    Returns:
        bool: True indicates that another RasPi has responded to the order,
            False indicates that either there was no answer,
            or an unrecoverable error has occurred for the other unit.
    """
    # Create dict to send
    message_dict = mm.compose_message(message, time_start, command)
    if et.send(message_dict) == 0:
        return True
    return False
def shutdown(self)

Safely exits and shuts down the RasPi unit

Args

None
 

Returns

None
 
Expand source code
def shutdown(self):
    """ Safely exits and shuts down the RasPi unit

    Args:
        None

    Returns:
        None
    """
    subprocess.Popen("sudo shutdown -P now", shell=True)
def waiting_loop(self)

This is the main-loop, active function for this program. It checks for input from a user or other RasPis, and calls appropriate functions when needed.

Args

None
 

Returns

bool
The return value indicates if everything closed properly. True for success, False otherwise.
Expand source code
def waiting_loop(self) -> bool:
    """ This is the main-loop, active function for this program. It checks 
    for input from a user or other RasPis, and calls appropriate functions 
    when needed.

    Args:
        None

    Returns:
        bool: The return value indicates if everything closed properly.
            True for success, False otherwise.
    """
    # Get object resources up and running
    et = EtherTalk(error_handler=self.eh, verbose=False)
    mm = MessageMan(error_handler=self.eh)
    vlc = VLCMan(VIDEOS_PATH, PLAYLIST_NAME, "", error_handler=self.eh)
    device = Device().has_device

    if not self.get_started(vlc, et, mm, device):
        # Problem at startup, restart from scratch
        os.system('sudo shutdown -r now')

    self.time = datetime.now().second
    while not self.is_shutdown_time:
        # If leader check for user input and send commands
        if c_queue.IsLeader == True:
            device = Device().has_device
            self.next_command(vlc, et, mm, device)
            # Check to make sure a follower is still on the network
            if datetime.now().second > 57 and not self.has_checked_ping:
                # Loop for three seconds, pinging the follower for a response
                loop_end_time = datetime.now() + timedelta(seconds=3)
                self.has_checked_ping = True
                while True:
                    if self.send_orders("", et, mm, 0, OrderCode.PING_RESPONSE):
                        break
                    elif datetime.now() >= loop_end_time:
                        self.has_follower = False
                        break
            elif datetime.now().second > 53 and datetime.now().second <= 56:
                self.has_checked_ping = False
        else:  # RasPi is the follower
            # Check if we need to become leader, dequeue, execute commands
            self.next_command_follower(mm, vlc)

        # Check time for shutdown
        self.is_shutdown_time = self.check_time()

    # Out of the Loop means it is Shutdown time for all units
    # Send shutdown signal 5 times, in case of false negative
    for i in range(4):
        if self.send_orders("", et, mm, 0, OrderCode.SHUT_DOWN):
            break
        time.sleep(5)
        
    # Then shutdown
    self.shutdown()