# imports import struct import sys import os sys.path.append(os.path.join(os.path.dirname(__file__), "../")) # Append path to common tutorial FaradayIO module # noinspection PyPep8 from FaradayIO import faradaybasicproxyio # noinspection PyPep8 from FaradayIO import faradaycommands class MsgStateMachineTx(object): def __init__(self): """ A state machine that handles the functionality needed to create and transmit a message """ # Constants self.MAX_PAYLOAD_LENGTH = 40 # TBD self.MAX_MSG_DATA_LENGTH = 38 # TDB # States self.STATE_IDLE = 0 self.STATE_INIT = 1 self.STATE_FRAGMENT = 2 self.STATE_TX = 3 # Frame Types self.MSG_START = 255 self.MSG_DATA = 254 self.MSG_END = 253 # Variables self.list_packets = [] # Frame Definitions self.pkt_datagram_frame = struct.Struct('1B 40s') # Fixed self.pkt_start = struct.Struct('9s 3B') # Fixed self.pkt_data = struct.Struct('2B 38s') # Variable Data Length self.pkt_end = struct.Struct('1B') # Fixed def fragmentcount(self, msg_len): """ This function calculations the number of fragmentation "chucks" or packets will be needed to break the supplied message length into smaller fragmented pieces. If a message length smaller than a single packet is supplied then the fragmentation count will be 0. The fragementation byte count is determined by the self.MAX_MSG_DATA_LENGTH class variable. :param msg_len: The length of the packet to be fragmented :Return: Number of fragmentation packets calculated to be needed to break the message length into the predetermined sized "chunks" """ # Determine fragment count frag_cnt = msg_len / self.MAX_MSG_DATA_LENGTH if msg_len % self.MAX_MSG_DATA_LENGTH > 0: frag_cnt += 1 return frag_cnt def fragmentmsg(self, msg): """ This function fragments the supplied message into smaller packets or "chunks" that will fit into the pre-determined MTU (maximum transmissible unit) of the packet's path. Using an algorithm these fragments can be reassembled after reception. :param msg: The data to be fragmented :Return: A list containing the fragemented "chunks" of data of the pre-determined size. """ list_message_fragments = [msg[i:i + self.MAX_MSG_DATA_LENGTH] for i in range(0, len(msg), self.MAX_MSG_DATA_LENGTH)] for item in list_message_fragments: print item, "Frag Length", len(item) print repr(list_message_fragments) return list_message_fragments def createmsgpackets(self, src_call, src_id, msg): """ This function is the high level fragmentation creation function that accepts a message/data and a source callsign/ID. The source callsign/ID is used in the START packet to easily tell the receive program who sent the message. :param src_call: Source device callsign :param src_id: Source device callsign ID number :param msg: Message/data to be transmitted to the receiving device (will be fragmented) """ # Ensure callsign and ID are formatted correctly src_call = str(src_call).upper() src_id = int(src_id) # Create START Packet msg_start = self.createstartframe(src_call, src_id, len(msg)) msg_start = self.pkt_datagram_frame.pack(self.MSG_START, msg_start) # Create END Packet msg_end = self.createendframe(len(msg)) msg_end = self.pkt_datagram_frame.pack(self.MSG_END, msg_end) # Create DATA Packet(s) list_msg_fragments = self.fragmentmsg(msg) list_data_packets = [] del list_data_packets[:] # Remove all old indexes for i in range(0, len(list_msg_fragments), 1): data_packet = self.createdataframe(i, list_msg_fragments[i]) print "Pre-Pack:", repr(data_packet), len(data_packet) data_packet = self.pkt_datagram_frame.pack(self.MSG_DATA, data_packet) print "Post-Pack:", repr(data_packet), len(data_packet) list_data_packets.append(data_packet) # Insert all packets into final packet list in order of transmission self.list_packets = [] # Reset any old packet fragments del self.list_packets[:] # Remove all old indexes self.list_packets.append(msg_start) for i in range(0, len(list_data_packets), 1): self.list_packets.append(list_data_packets[i]) self.list_packets.append(msg_end) def createstartframe(self, src_call, src_id, msg_len): """ This function creates a START packet (frame) that resets the receivers state machine to prepare for a new message. In the START frame information about the message/data is stored. :param src_call: Source device callsign :param src_id: Source device callsign ID number :param msg_len: Length in bytes of the full Message/data to be transmitted to the receiving device :Return: A START packet """ # Calculate the number of fragmented packets frag_cnt = self.fragmentcount(msg_len) print frag_cnt # Create packet packet = self.pkt_start.pack(src_call, len(src_call), src_id, frag_cnt) # Return packet created return packet def createdataframe(self, sequence, data): """ This function creates a DATA packet (frame) that contains the main data/message to be reassembled at the receiver. This is fragmented packets of the main data/message being transmitted. :param sequence: The sequence number of the packet. This is used to re-order the original message fragments after reception. :param data: The data to be encapsulated in the data fragment packet. :Return: A DATA packet """ print "create:", repr(data), len(data) packet = self.pkt_data.pack(sequence, len(data), data) print "created:", repr(packet), len(packet) return packet def createendframe(self, msg_len): """ This function creates an END packet (frame) that indicates to the receiver device that the end of the fragment packets has occurred and it is safe to reassemble the original data/message. The END packet can contain information about the the prior packets/data if needed. :param msg_len: TThe length of the full data/message in bytes that was transmitted. :Return: An END packet """ frag_cnt = self.fragmentcount(msg_len) packet = self.pkt_end.pack(frag_cnt) return packet class MessageAppTx(object): def __init__(self, local_callsign, local_callsign_id, destination_callsign, destination_id): """ The message application object contains all the functions, definitions, and state machines needed to implement a bare-bones text message application using the Faraday command application "experimental RF Packet Forward" functionality." """ # Identification Variables self.local_device_callsign = str(local_callsign).upper() self.local_device_node_id = int(local_callsign_id) self.remote_callsign = str(destination_callsign).upper() self.remote_id = int(destination_id) # Initialize objects self.faraday_1 = faradaybasicproxyio.proxyio() self.faraday_cmd = faradaycommands.faraday_commands() # Initialize variables self.destination_callsign = '' self.destination_id = 0 self.command = '' def updatedestinationstation(self, dest_callsign, dest_id): """ This function updates the class object's destination callsign and ID that will change the respective device that the message/data will be transmitted to. This will allow the program to correctly address a Faraday device over the RF layer 2 link layer (and possibly above) Watch out for max callsign lengths! :param dest_callsign: The destination (remote) device callsign to address the transmissions to :param dest_id: The destination (remote) device callsign ID number to address the transmissions to """ self.destination_callsign = str(dest_callsign).upper() # Ensure callsign is always uppercase self.destination_id = int(dest_id) def transmitframe(self, payload): """ A basic function used to transmit a raw payload to the intended destination unit as defined in the class variables. This is the high level function used to initiate a transmit of payload data/message. :param payload: The payload data/message to be transmitted. """ self.command = self.faraday_cmd.CommandLocalExperimentalRfPacketForward(self.destination_callsign, self.destination_id, payload) print "Transmitting message:", repr(payload), "length:", len(payload) self.faraday_1.POST(self.local_device_callsign, self.local_device_node_id, self.faraday_1.CMD_UART_PORT, self.command) class MsgStateMachineRx(object): def __init__(self): """ A state machine that handles the functionality needed to reassemble a message """ # States self.STATE_IDLE = 0 self.STATE_RX_INIT = 1 self.STATE_RX_FRAGMENT = 2 self.STATE_RX_END = 3 # Frame Types self.MSG_START = 255 self.MSG_DATA = 254 self.MSG_END = 253 # Variables self.currentstate = self.STATE_IDLE self.message = '' self.rx_station = '' def changestate(self, state): """ A simple function used to change the class object state machine state. :param state: The new state to update the state machine to """ self.currentstate = state def frameassembler(self, frame_type, data): """ This function reassembles the received fragment packets into the original data/message using the class object's state machine. :param frame_type: The frame type being parsed and reassembled (START/DATA/END) packet types :param data: The data contained in the received fragment packet :Return: If END packet then returns full reassembled packet :Return: If NOT END packet then returns None """ # Not a true state machine yet, but working to it! # Start if frame_type == self.MSG_START: self.changestate(self.STATE_RX_INIT) self.message = '' callsign_len = int(data[1]) #fragments = data[3] self.rx_station = str(data[0][0:callsign_len]) + '-' + str(data[2]) return None # Data elif frame_type == self.MSG_DATA: self.changestate(self.STATE_RX_FRAGMENT) #data_sequence = data[0] data_len = data[1] data_data = str(data[2])[0:data_len] self.message += data_data return None # Stop elif frame_type == self.MSG_END: self.changestate(self.STATE_RX_END) #fragments = data[0] message_assembled = {'source_callsign': self.rx_station, 'message': self.message} return message_assembled # Else Type (Error) else: print "Incorrect frame type:", frame_type, repr(data) class MessageAppRx(object): def __init__(self): """ The message application object contains all the functions, definitions, and state machines needed to implement a bare-bones text message application using the Faraday command application "experimental RF Packet Forward" functionality." """ # Identification Variables self.local_device_callsign = 'kb1lqc' self.local_device_node_id = 1 # Initialize objects self.faraday_Rx = faradaybasicproxyio.proxyio() self.faraday_Rx_SM = MsgStateMachineRx() # Initialize variables # Frame Definitions (Should be combined later with TX?) self.pkt_datagram_frame = struct.Struct('1B 41s') # Fixed self.pkt_start = struct.Struct('9s 3B') # Fixed self.pkt_data = struct.Struct('2B 39s') # Variable Data Length self.pkt_end = struct.Struct('1B') # Fixed def getnextframe(self): """ This function is used to check for a new received packet to be retrieved in the receive Proxy queue for the expected experimental RF packet forwarding messaging UART service port number. The retrieved packet will be parsed accordingly. """ print repr(self.faraday_Rx.GETWait(self.local_device_callsign, self.local_device_node_id, self.faraday_Rx.CMD_UART_PORT, 1, False)) def parsepacketfromdatagram(self, datagram): """ This function parses recevied data packets (datagrams) and performs receiver state machine reassembly functions respectively. :param datagram: The received packet to be parsed :Return: If END packet then returns full reassembled packet :Return: If NOT END packet then returns None """ unpacked_datagram = self.pkt_datagram_frame.unpack(datagram) packet_identifier = unpacked_datagram[0] packet = unpacked_datagram[1] try: # Start Packet if packet_identifier == 255: unpacked_packet = self.pkt_start.unpack(packet[0:12]) # print unpacked_packet self.faraday_Rx_SM.frameassembler(255, unpacked_packet) return None # Data Packet if packet_identifier == 254: unpacked_packet = self.pkt_data.unpack(packet[0:41]) # print unpacked_packet self.faraday_Rx_SM.frameassembler(254, unpacked_packet) return None # END Packet if packet_identifier == 253: unpacked_packet = self.pkt_end.unpack(packet[0]) # print unpacked_packet message_assembled = self.faraday_Rx_SM.frameassembler(253, unpacked_packet) return message_assembled except Exception, err: print "Fail - Exception", Exception, err def rxmsgloop(self, local_callsign, local_callsign_id, uart_service_port_application_number, getwaittimeout): """ This function is used as the main high level "receiver" loop to check for new message/data fragments received and parse them accordingly. If a new fully reassembled message/data has been completed the function will return the data/message. This function should be called regularly from the receiver program on a reoccuring interval. :param local_callsign: Callsign of the local device to retrieve received data packet from :param local_callsign_id: Callsign ID number of the local device to retrieve received data packet from :param uart_service_port_application_number: The UART network stack service port number to retrieve received data/messages from. :param getwaittimeout: The time in seconds to wait for new data before unblocking the function loop. This is the timeout time in the FaradayIO faradaybasicproxyio programs "GETWait()" function. :Return: If message not received properly (but trigger) then returns None (unlikely) :Return: If new data/message received and reassembled then returns the received data/message as a dictionary of the source callsign/ID and data. """ data = None data = self.faraday_Rx.GETWait(str(local_callsign).upper(), int(local_callsign_id), uart_service_port_application_number, getwaittimeout) if (data is not None) and ('error' not in data): for item in data: datagram = self.faraday_Rx.DecodeRawPacket(item['data']) # All frames are 42 bytes long and need to be extracted from the much larger UART frame from Faraday datagram = datagram[0:42] message_status = self.parsepacketfromdatagram(datagram) if message_status is None: return None # Partial fragmented packet, still receiving else: return message_status # Full packet relieved!