Source code for pyparrot.networking.wifiConnection

"""
Holds all the data and commands needed to fly a Bebop or Anafi drone.

Author: Amy McGovern, dramymcgovern@gmail.com
"""

from zeroconf import ServiceBrowser, Zeroconf
from datetime import datetime
import time
import socket
import ipaddress
import json
from pyparrot.utils.colorPrint import color_print
import struct
import threading
from pyparrot.commandsandsensors.DroneSensorParser import get_data_format_and_size

[docs]class mDNSListener(object): """ This is adapted from the listener code at https://pypi.python.org/pypi/zeroconf """ def __init__(self, wifi_connection): self.wifi_connection = wifi_connection
[docs] def remove_service(self, zeroconf, type, name): #print("Service %s removed" % (name,)) pass
[docs] def add_service(self, zeroconf, type, name): info = zeroconf.get_service_info(type, name) print("Service %s added, service info: %s" % (name, info)) self.wifi_connection._connect_listener_called(info)
[docs]class WifiConnection: def __init__(self, drone, drone_type="Bebop2", ip_address=None): """ Can be a connection to a Anafi, Bebop, Bebop2 or a Mambo right now :param type: type of drone to connect to """ self.is_connected = False if (drone_type not in ("Anafi", "Bebop", "Bebop2", "Mambo", "Disco")): color_print("Error: only type Anafi Bebop Disco and Mambo are currently supported", "ERROR") return self.drone = drone self.drone_type = drone_type self.udp_send_port = 44444 # defined during the handshake except not on Mambo after 3.0.26 firmware self.udp_receive_port = 43210 self.is_listening = True # for the UDP listener self.ip_address = ip_address if (drone_type is "Bebop"): self.mdns_address = "_arsdk-0901._udp.local." #Bebop video streaming self.stream_port = 55004 self.stream_control_port = 55005 elif (drone_type is "Anafi"): self.mdns_address = "_arsdk-0914._udp.local." self.stream_port = 55004 self.stream_control_port = 55005 elif (drone_type is "Bebop2"): self.mdns_address = "_arsdk-090c._udp.local." #Bebop video streaming self.stream_port = 55004 self.stream_control_port = 55005 elif (drone_type is "Disco"): self.mdns_address = "_arsdk-090e._udp.local." #Bebop video streaming self.stream_port = 55004 self.stream_control_port = 55005 elif (drone_type is "Mambo"): self.mdns_address = "_arsdk-090b._udp.local." # map of the data types by name (for outgoing packets) self.data_types_by_name = { 'ACK' : 1, 'DATA_NO_ACK': 2, 'LOW_LATENCY_DATA': 3, 'DATA_WITH_ACK' : 4 } # map of the incoming data types by number (to figure out if we need to ack etc) self.data_types_by_number = { 1 : 'ACK', 2 : 'DATA_NO_ACK', 3 : 'LOW_LATENCY_DATA', 4 : 'DATA_WITH_ACK' } self.sequence_counter = { 'PONG': 0, 'SEND_NO_ACK': 0, 'SEND_WITH_ACK': 0, 'SEND_HIGH_PRIORITY': 0, 'VIDEO_ACK': 0, 'ACK_DRONE_DATA': 0, 'NO_ACK_DRONE_DATA': 0, 'VIDEO_DATA': 0, } self.buffer_ids = { 'PING': 0, # pings from device 'PONG': 1, # respond to pings 'SEND_NO_ACK': 10, # not-ack commandsandsensors (piloting and camera rotations) 'SEND_WITH_ACK': 11, # ack commandsandsensors (all piloting commandsandsensors) 'SEND_HIGH_PRIORITY': 12, # emergency commandsandsensors 'VIDEO_ACK': 13, # ack for video 'ACK_DRONE_DATA' : 127, # drone data that needs an ack 'NO_ACK_DRONE_DATA' : 126, # data from drone (including battery and others), no ack 'VIDEO_DATA' : 125, # video data 'ACK_FROM_SEND_WITH_ACK': 139 # 128 + buffer id for 'SEND_WITH_ACK' is 139 } self.data_buffers = (self.buffer_ids['ACK_DRONE_DATA'], self.buffer_ids['NO_ACK_DRONE_DATA']) # store whether a command was acked self.command_received = { 'SEND_WITH_ACK': False, 'SEND_HIGH_PRIORITY': False, 'ACK_COMMAND': False } # maximum number of times to try a packet before assuming it failed self.max_packet_retries = 1 # threading lock for waiting self._lock = threading.Lock()
[docs] def connect(self, num_retries): """ Connects to the drone :param num_retries: maximum number of retries :return: True if the connection succeeded and False otherwise """ if (self.ip_address is None) and ("Mambo" not in self.drone_type): print("Setting up mDNS listener since this is not a Mambo") #parrot's latest mambo firmware (3.0.26 broke all of the mDNS services so this is (temporarily) commented #out but it is backwards compatible and will work with the hard-coded addresses for now. zeroconf = Zeroconf() listener = mDNSListener(self) print("Making a browser for %s" % self.mdns_address) browser = ServiceBrowser(zeroconf, self.mdns_address , listener) # basically have to sleep until the info comes through on the listener num_tries = 0 while (num_tries < num_retries and not self.is_connected): time.sleep(1) num_tries += 1 # if we didn't hear the listener, return False if (not self.is_connected): color_print("connection failed: did you remember to connect your machine to the Drone's wifi network?", "ERROR") return False else: browser.cancel() # perform the handshake and get the UDP info handshake = self._handshake(num_retries) if (handshake): self._create_udp_connection() self.listener_thread = threading.Thread(target=self._listen_socket) self.listener_thread.start() color_print("Success in setting up the wifi network to the drone!", "SUCCESS") return True else: color_print("Error: TCP handshake failed.", "ERROR") return False
def _listen_socket(self): """ Listens to the socket and sleeps in between receives. Runs forever (until disconnect is called) """ print("starting listening at ") data = None while (self.is_listening): try: (data, address) = self.udp_receive_sock.recvfrom(66000) except socket.timeout: print("timeout - trying again") except: pass self.handle_data(data) color_print("disconnecting", "INFO") self.disconnect()
[docs] def handle_data(self, data): """ Handles the data as it comes in :param data: raw data packet :return: """ # got the idea to of how to handle this data nicely (handling the perhaps extra data in the packets) # and unpacking the critical info first (id, size etc) from # https://github.com/N-Bz/bybop/blob/8d4c569c8e66bd1f0fdd768851409ca4b86c4ecd/src/Bybop_NetworkAL.py my_data = data while (my_data): #print("inside loop to handle data ") (data_type, buffer_id, packet_seq_id, packet_size) = struct.unpack('<BBBI', my_data[0:7]) recv_data = my_data[7:packet_size] #print("\tgot a data type of of %d " % data_type) #print("\tgot a buffer id of of %d " % buffer_id) #print("\tgot a packet seq id of of %d " % packet_seq_id) #print("\tsize is %d" % packet_size) self.handle_frame(data_type, buffer_id, packet_seq_id, recv_data) # loop in case there is more data my_data = my_data[packet_size:]
#print("assigned more data") #print("ended loop handling data")
[docs] def handle_frame(self, packet_type, buffer_id, packet_seq_id, recv_data): if (buffer_id == self.buffer_ids['PING']): #color_print("this is a ping! need to pong", "INFO") self._send_pong(recv_data) if (self.data_types_by_number[packet_type] == 'ACK'): #print("setting command received to true") ack_seq = int(struct.unpack("<B", recv_data)[0]) self._set_command_received('SEND_WITH_ACK', True, ack_seq) self.ack_packet(buffer_id, ack_seq) elif (self.data_types_by_number[packet_type] == 'DATA_NO_ACK'): #print("DATA NO ACK") if (buffer_id in self.data_buffers): self.drone.update_sensors(packet_type, buffer_id, packet_seq_id, recv_data, ack=False) elif (self.data_types_by_number[packet_type] == 'LOW_LATENCY_DATA'): print("Need to handle Low latency data") elif (self.data_types_by_number[packet_type] == 'DATA_WITH_ACK'): #print("DATA WITH ACK") if (buffer_id in self.data_buffers): self.drone.update_sensors(packet_type, buffer_id, packet_seq_id, recv_data, ack=True) else: color_print("HELP ME", "ERROR") print("got a different type of data - help")
def _send_pong(self, data): """ Send a PONG back to a PING :param data: data that needs to be PONG/ACK'd :return: nothing """ size = len(data) self.sequence_counter['PONG'] = (self.sequence_counter['PONG'] + 1) % 256 packet = struct.pack("<BBBI", self.data_types_by_name['DATA_NO_ACK'], self.buffer_ids['PONG'], self.sequence_counter['PONG'], size + 7) packet += data self.safe_send(packet) def _set_command_received(self, channel, val, seq_id): """ Set the command received on the specified channel to the specified value (used for acks) :param channel: channel :param val: True or False :return: """ self.command_received[(channel, seq_id)] = val def _is_command_received(self, channel, seq_id): """ Is the command received? :param channel: channel it was sent on :param seq_id: sequence id of the command :return: """ return self.command_received[(channel, seq_id)] def _handshake(self, num_retries): """ Performs the handshake over TCP to get all the connection info :return: True if it worked and False otherwise """ # create the TCP socket for the handshake tcp_sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) #print (self.connection_info.address, self.connection_info.port) #print(ipaddress.IPv4Address(self.connection_info.address)) # connect # handle the broken mambo firmware by hard-coding the port and IP address if ("Mambo" in self.drone_type): self.drone_ip = "192.168.99.3" tcp_sock.connect(("192.168.99.3", 44444)) else: if (self.ip_address is None): self.drone_ip = ipaddress.IPv4Address(self.connection_info.address).exploded tcp_sock.connect((self.drone_ip, self.connection_info.port)) else: self.drone_ip = ipaddress.IPv4Address(self.ip_address).exploded tcp_sock.connect((self.drone_ip, 44444)) # send the handshake information if(self.drone_type in ("Anafi", "Bebop", "Bebop2", "Disco")): # For Bebop add video stream ports to the json request json_string = json.dumps({"d2c_port":self.udp_receive_port, "controller_type":"computer", "controller_name":"pyparrot", "arstream2_client_stream_port":self.stream_port, "arstream2_client_control_port":self.stream_control_port}) else: json_string = json.dumps({"d2c_port":self.udp_receive_port, "controller_type":"computer", "controller_name":"pyparrot"}) json_obj = json.loads(json_string) print(json_string) try: # python 3 tcp_sock.send(bytes(json_string, 'utf-8')) except: # python 2 tcp_sock.send(json_string) # wait for the response finished = False num_try = 0 while (not finished and num_try < num_retries): data = tcp_sock.recv(4096).decode('utf-8') if (len(data) > 0): if (self.drone_type == "Anafi"): my_data = data #data[0:-1] else: my_data = data[0:-1] print("mydata", my_data) self.udp_data = json.loads(str(my_data)) # if the drone refuses the connection, return false if (self.udp_data['status'] != 0): return False print(self.udp_data) self.udp_send_port = self.udp_data['c2d_port'] print("c2d_port is %d" % self.udp_send_port) finished = True else: num_try += 1 # cleanup tcp_sock.close() return finished def _create_udp_connection(self): """ Create the UDP connection """ self.udp_send_sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) #self.udp_send_sock.connect((self.drone_ip, self.udp_send_port)) self.udp_receive_sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) # don't use the connect, use bind instead # learned from bybop code # https://github.com/N-Bz/bybop/blob/8d4c569c8e66bd1f0fdd768851409ca4b86c4ecd/src/Bybop_NetworkAL.py #self.udp_receive_sock.connect((self.drone_ip, self.udp_receive_port)) self.udp_receive_sock.settimeout(5.0) #Some computers having connection refused error (error was some kind of that, I dont remember actually) #These new setsockopt lines solving it (at least at my device) self.udp_receive_sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) self.udp_send_sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) self.udp_receive_sock.bind(('0.0.0.0', int(self.udp_receive_port))) def _connect_listener_called(self, connection_info): """ Save the connection info and set the connected to be true. This si called within the listener for the connection. :param connection_info: :return: """ self.connection_info = connection_info self.is_connected = True
[docs] def disconnect(self): """ Disconnect cleanly from the sockets """ self.is_listening = False # Sleep for a moment to allow all socket activity to cease before closing # This helps to avoids a Winsock error regarding a operations on a closed socket self.smart_sleep(0.5) # then put the close in a try/except to catch any further winsock errors # the errors seem to be mostly occurring on windows for some reason try: self.udp_receive_sock.close() self.udp_send_sock.close() except: pass
[docs] def safe_send(self, packet): packet_sent = False #print "inside safe send" try_num = 0 while (not packet_sent and try_num < self.max_packet_retries): try: self.udp_send_sock.sendto(packet, (self.drone_ip, self.udp_send_port)) packet_sent = True except: #print "resetting connection" self.udp_send_sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) #self.udp_send_sock.connect((self.drone_ip, self.udp_send_port)) try_num += 1
[docs] def send_command_packet_ack(self, packet, seq_id): """ Sends the actual packet on the ack channel. Internal function only. :param packet: packet constructed according to the command rules (variable size, constructed elsewhere) :return: True if the command was sent and False otherwise """ try_num = 0 self._set_command_received('SEND_WITH_ACK', False, seq_id) while (try_num < self.max_packet_retries and not self._is_command_received('SEND_WITH_ACK', seq_id)): color_print("sending packet on try %d", try_num) self.safe_send(packet) try_num += 1 self.smart_sleep(0.5) return self._is_command_received('SEND_WITH_ACK', seq_id)
[docs] def send_command_packet_noack(self, packet): """ Sends the actual packet on the No-ack channel. Internal function only. :param packet: packet constructed according to the command rules (variable size, constructed elsewhere) :return: True if the command was sent and False otherwise """ try_num = 0 color_print("sending packet on try %d", try_num) self.safe_send(packet)
[docs] def send_noparam_high_priority_command_packet(self, command_tuple): """ Send a no parameter command packet on the high priority channel :param command_tuple: :return: """ self.sequence_counter['SEND_HIGH_PRIORITY'] = (self.sequence_counter['SEND_HIGH_PRIORITY'] + 1) % 256 packet = struct.pack("<BBBIBBH", self.data_types_by_name['LOW_LATENCY_DATA'], self.buffer_ids['SEND_HIGH_PRIORITY'], self.sequence_counter['SEND_HIGH_PRIORITY'], 11, command_tuple[0], command_tuple[1], command_tuple[2]) self.safe_send(packet)
[docs] def send_noparam_command_packet_ack(self, command_tuple): """ Send a no parameter command packet on the ack channel :param command_tuple: :return: """ self.sequence_counter['SEND_WITH_ACK'] = (self.sequence_counter['SEND_WITH_ACK'] + 1) % 256 packet = struct.pack("<BBBIBBH", self.data_types_by_name['DATA_WITH_ACK'], self.buffer_ids['SEND_WITH_ACK'], self.sequence_counter['SEND_WITH_ACK'], 11, command_tuple[0], command_tuple[1], command_tuple[2]) return self.send_command_packet_ack(packet, self.sequence_counter['SEND_WITH_ACK'])
[docs] def send_param_command_packet(self, command_tuple, param_tuple=None, param_type_tuple=0,ack=True): """ Send a command packet with parameters. Ack channel is optional for future flexibility, but currently commands are always send over the Ack channel so it defaults to True. Contributed by awm102 on github :param: command_tuple: the command tuple derived from command_parser.get_command_tuple() :param: param_tuple (optional): the parameter values to be sent (can be found in the XML files) :param: param_size_tuple (optional): a tuple of strings representing the data type of the parameters e.g. u8, float etc. (can be found in the XML files) :param: ack (optional): allows ack to be turned off if required :return: """ # TODO: This function could potentially be extended to encompass send_noparam_command_packet_ack # and send_enum_command_packet_ack if desired for more modular code. # TODO: The function could be improved by looking up the parameter data types in the xml files # in the same way the send_enum_command_packet_ack does. # Create lists to store the number of bytes and pack chars needed for parameters # Default them to zero so that if no params are provided the packet size is correct param_size_list = [0] * len(param_tuple) pack_char_list = [0] * len(param_tuple) if param_tuple is not None: # Fetch the parameter sizes. By looping over the param_tuple we only get the data # for requested parameters so a mismatch in params and types does not matter for i,param in enumerate(param_tuple): pack_char_list[i], param_size_list[i] = get_data_format_and_size(param, param_type_tuple[i]) if ack: ack_string = 'SEND_WITH_ACK' data_ack_string = 'DATA_WITH_ACK' else: ack_string = 'SEND_NO_ACK' data_ack_string = 'DATA_NO_ACK' # Construct the base packet self.sequence_counter[ack_string] = (self.sequence_counter[ack_string] + 1) % 256 # Calculate packet size: # base packet <BBBIBBH is 11 bytes, param_size_list can be added up packet_size = 11 + sum(param_size_list) packet = struct.pack("<BBBIBBH", self.data_types_by_name[data_ack_string], self.buffer_ids[ack_string], self.sequence_counter[ack_string], packet_size, command_tuple[0], command_tuple[1], command_tuple[2]) if param_tuple is not None: # Add in the parameter values based on their sizes for i,param in enumerate(param_tuple): packet += struct.pack(pack_char_list[i],param) if ack: return self.send_command_packet_ack(packet, self.sequence_counter['SEND_WITH_ACK']) else: return self.send_command_packet_noack(packet)
[docs] def send_single_pcmd_command(self, command_tuple, roll, pitch, yaw, vertical_movement): """ Send a single PCMD command with the specified roll, pitch, and yaw. Note this will not make that command run forever. Instead it sends ONCE. This can be used in a loop (in your agent) that makes more smooth control than using the duration option. :param command_tuple: command tuple per the parser :param roll: :param pitch: :param yaw: :param vertical_movement: """ self.sequence_counter['SEND_NO_ACK'] = (self.sequence_counter['SEND_NO_ACK'] + 1) % 256 packet = struct.pack("<BBBIBBHBbbbbI", self.data_types_by_name['DATA_NO_ACK'], self.buffer_ids['SEND_NO_ACK'], self.sequence_counter['SEND_NO_ACK'], 20, command_tuple[0], command_tuple[1], command_tuple[2], 1, int(roll), int(pitch), int(yaw), int(vertical_movement), 0) self.safe_send(packet)
[docs] def send_pcmd_command(self, command_tuple, roll, pitch, yaw, vertical_movement, duration): """ Send the PCMD command with the specified roll, pitch, and yaw :param command_tuple: command tuple per the parser :param roll: :param pitch: :param yaw: :param vertical_movement: :param duration: """ start_time = datetime.now() new_time = datetime.now() diff = (new_time - start_time).seconds + ((new_time - start_time).microseconds / 1000000.0) while (diff < duration): self.send_single_pcmd_command(command_tuple, roll, pitch, yaw, vertical_movement) self.smart_sleep(0.1) new_time = datetime.now() diff = (new_time - start_time).seconds + ((new_time - start_time).microseconds / 1000000.0)
[docs] def send_fly_relative_command(self, command_tuple, change_x, change_y, change_z, change_angle): """ Send the packet to fly relative (this is Bebop only). :param command_tuple: command tuple per the parser :param change_x: change in x :param change_y: change in y :param change_z: change in z :param change_angle: change in angle """ self.sequence_counter['SEND_WITH_ACK'] = (self.sequence_counter['SEND_WITH_ACK'] + 1) % 256 packet = struct.pack("<BBBIBBHffff", self.data_types_by_name['DATA_WITH_ACK'], self.buffer_ids['SEND_WITH_ACK'], self.sequence_counter['SEND_WITH_ACK'], 27, command_tuple[0], command_tuple[1], command_tuple[2], change_x, change_y, change_z, change_angle) self.safe_send(packet)
[docs] def send_turn_command(self, command_tuple, degrees): """ Build the packet for turning and send it :param command_tuple: command tuple from the parser :param degrees: how many degrees to turn :return: True if the command was sent and False otherwise """ self.sequence_counter['SEND_WITH_ACK'] = (self.sequence_counter['SEND_WITH_ACK'] + 1) % 256 packet = struct.pack("<BBBIBBHh", self.data_types_by_name['DATA_WITH_ACK'], self.buffer_ids['SEND_WITH_ACK'], self.sequence_counter['SEND_WITH_ACK'], 13, command_tuple[0], command_tuple[1], command_tuple[2], degrees) return self.send_command_packet_ack(packet, self.sequence_counter['SEND_WITH_ACK'])
[docs] def send_camera_move_command(self, command_tuple, pan, tilt): """ Send the packet to move the camera (this is Bebop only). :param command_tuple: command tuple per the parser :param pan: :param tilt: """ self.sequence_counter['SEND_WITH_ACK'] = (self.sequence_counter['SEND_WITH_ACK'] + 1) % 256 packet = struct.pack("<BBBIBBHff", self.data_types_by_name['DATA_WITH_ACK'], self.buffer_ids['SEND_WITH_ACK'], self.sequence_counter['SEND_WITH_ACK'], 19, command_tuple[0], command_tuple[1], command_tuple[2], pan, tilt) self.safe_send(packet)
[docs] def send_enum_command_packet_ack(self, command_tuple, enum_value, usb_id=None): """ Send a command on the ack channel with enum parameters as well (most likely a flip). All commandsandsensors except PCMD go on the ack channel per http://forum.developer.parrot.com/t/ble-characteristics-of-minidrones/5912/2 the id of the last command sent (for use in ack) is the send counter (which is incremented before sending) :param command_tuple: 3 tuple of the command bytes. 0 padded for 4th byte :param enum_value: the enum index :return: nothing """ self.sequence_counter['SEND_WITH_ACK'] = (self.sequence_counter['SEND_WITH_ACK'] + 1) % 256 if (usb_id is None): packet = struct.pack("<BBBIBBHI", self.data_types_by_name['DATA_WITH_ACK'], self.buffer_ids['SEND_WITH_ACK'], self.sequence_counter['SEND_WITH_ACK'], 15, command_tuple[0], command_tuple[1], command_tuple[2], enum_value) else: packet = struct.pack("<BBBIBBHBI", self.data_types_by_name['DATA_WITH_ACK'], self.buffer_ids['SEND_WITH_ACK'], self.sequence_counter['SEND_WITH_ACK'], 16, command_tuple[0], command_tuple[1], command_tuple[2], usb_id, enum_value) return self.send_command_packet_ack(packet, self.sequence_counter['SEND_WITH_ACK'])
[docs] def smart_sleep(self, timeout): """ Sleeps the requested number of seconds but wakes up for notifications Note: time.sleep misbehaves for the BLE connections but seems ok for wifi. I encourage you to use smart_sleep since it handles the sleeping in a thread-safe way. :param timeout: number of seconds to sleep :return: """ start_time = datetime.now() new_time = datetime.now() diff = (new_time - start_time).seconds + ((new_time - start_time).microseconds / 1000000.0) while (diff < timeout): time.sleep(0.1) new_time = datetime.now() diff = (new_time - start_time).seconds + ((new_time - start_time).microseconds / 1000000.0)
[docs] def ack_packet(self, buffer_id, packet_id): """ Ack the packet id specified by the argument on the ACK_COMMAND channel :param packet_id: the packet id to ack :return: nothing """ #color_print("ack: buffer id of %d and packet id of %d" % (buffer_id, packet_id)) new_buf_id = (buffer_id + 128) % 256 if (new_buf_id not in self.sequence_counter): self.sequence_counter[new_buf_id] = 0 else: self.sequence_counter[new_buf_id] = (self.sequence_counter[new_buf_id] + 1) % 256 packet = struct.pack("<BBBIB", self.data_types_by_name['ACK'], new_buf_id, self.sequence_counter[new_buf_id], 8, packet_id) self.safe_send(packet)