import abc import time class Communicator(abc.ABC): """ A Communicator: -- Establishes a connection with the robot's onboard controller (e.g., an Arduino). -- Implements the message-passing protocol on Commands: -- send_command, optionally followed by receiving an acknowledgement. -- receive_command_data, optionally followed by sending an acknowledgement. Subclasses must implement the following functions for message-passing on bytes (as opposed to Commands): -- establish_connection -- send_bytes -- receive_bytes CONSIDER What else should a Communicator be able to do? Maybe: -- Sleep safely (while any concurrent processes are running). -- Return the type of connection. -- ?? """ # TODO Add a DEBUG mode that prints messages # and applies to any subclass. # Subclasses should define their own __init__ which then calls # this one: def __init__(self, connect=True, wait_for_acknowledgement=True, send_acknowledgement=False, is_debug=False): self.should_wait_for_acknowledgement = wait_for_acknowledgement self.should_send_acknowledgement = send_acknowledgement self.is_debug = is_debug if connect: self.connect() self.is_connected = True # self.send_startup_sequence() # self.receive_startup_sequence() def connect(self): """ Does whatever is needed to establish a connection to the robot's onboard controller (e.g., an Arduino). """ try: # Subclass implements the following: self.establish_connection() except: raise # TODO Error handing def send_startup_sequence(self): """ Current implementation: send 255 3 times. """ # Sleep a bit in hopes to make this FOLLOW the # *HELLO**DONE* of the socket connection. if self.is_debug: print('SENDING startup sequence in 2 seconds:') time.sleep(2) for _ in range(20): self.send_message(bytearray([255])) time.sleep(0.1) if self.is_debug: print('Done SENDING startup sequence.') def receive_startup_sequence(self): """ Read until we get 254 three times in a row. """ if self.is_debug: print('Waiting to RECEIVE startiup sequence...') count = 0 while True: if count >= 3: break byte_read = self.receive_message(1) if byte_read == 254: count = count + 1 else: count = 0 if self.is_debug: print('Done RECEIVING startup sequence.') def send_command(self, command, data=None): """ Encodes and then sends to the Arduino the given command with the given data. Waits for an acknowledgement if this Communicator is set to do so. Returns the number of bytes sent. :type command: Command :type data: CommandData :rtype int """ message = command.to_bytes(data) bytes_sent = self.send_message(message) if self.should_wait_for_acknowledgement: self.wait_for_acknowledgement(message[0]) return bytes_sent def receive_command_data(self, command): """ Waits for and receives whatever data the given Command expects back from the Arduino. Sends an acknowledgement if this Communicator is set to do so. Returns the received data in whatever form the Command expects. :type data: CommandData """ if command.number_of_bytes_to_receive_varies: bytes_received = self._receive_variable_bytes(command) else: num_bytes = command.number_of_bytes_to_receive bytes_received = self.receive_message(num_bytes) if self.should_send_acknowledgement: self.send_acknowledgement() return command.value_of(bytes_received) def _receive_variable_bytes(self, command): beginning = self.receive_message(1) # print('Beginning:', beginning) if command.indicates_end_of_message(beginning): return beginning else: rest = self.receive_message(command. number_of_bytes_to_receive - 1) # print('Rest: ', end='') # for k in range(len(rest)): # print(rest[k], end=' ') # print(type(rest)) if type(rest) is int: return bytearray([beginning]) + bytearray([rest]) else: return bytearray([beginning]) + rest def wait_for_acknowledgement(self, expected_acknowledgement): # TODO Encapsulate this into a class that allows different # kinds of acknowledgements. if self.is_debug: print('Waiting for acknowledgement.') # TODO Improve error detection and handling response = self.receive_message(1) if response != expected_acknowledgement: print() print('COMMUNICATION ERROR!') print(' Expected:', expected_acknowledgement) print(' Received:', response) print(' TELL YOUR INSTRUCTOR THAT THIS HAPPENED.') print(' Include the above 2 numbers when doing so.') print() if self.is_debug: print('Received acknowledgement') def send_acknowledgement(self): # TODO Encapsulate this into a class that allows different # kinds of acknowledgements. pass # Not currently implemented @abc.abstractmethod def establish_connection(self): """ Does whatever is needed to establish a connection to the robot's onboard controller (e.g., an Arduino). """ # Subclass must implement this method. @abc.abstractmethod def disconnect(self): """ Does whatever is needed to close the connection cleanly. """ # Subclass must implement this method. def send_message(self, message): """ Sends the given message to the Arduino. :type message: bytes or bytearray :rtype int """ if self.is_debug: print('Sending: {}.'.format(message)) # Subclass must implement the following method: number_of_bytes_sent = self.send_bytes(message) if self.is_debug: print('Sent {} bytes.'.format(number_of_bytes_sent)) return number_of_bytes_sent def receive_message(self, number_of_bytes_to_receive=1): """ Receives from the Arduino the given number of bytes. Returns a byte (integer between 0 and 255) if the given number of bytes is 1, otherwise returns a bytearray containing the bytes. :rtype byte or bytearray """ if self.is_debug: print('Waiting to receive {} bytes.'.format( number_of_bytes_to_receive)) # Subclass must implement the following method: message = self.receive_bytes(number_of_bytes_to_receive) if self.is_debug: print('Received: ', message) return message @abc.abstractmethod def send_bytes(self, bytes_to_send): """ Sends the given bytes. """ # Subclass must implement this method. @abc.abstractmethod def receive_bytes(self, number_of_bytes): """ Receives the given number of bytes. """ # Subclass must implement this method.