diff --git a/cflib/crtp/udpdriver.py b/cflib/crtp/udpdriver.py
index 949c88ec6..537e10d06 100644
--- a/cflib/crtp/udpdriver.py
+++ b/cflib/crtp/udpdriver.py
@@ -22,81 +22,228 @@
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-""" CRTP UDP Driver. Work either with the UDP server or with an UDP device
-See udpserver.py for the protocol"""
+"""
+Crazyflie UDP driver.
+
+This driver is used to communicate with the Crazyflie using a UDP connection.
+Scanning feature assumes a crazyflie server is running on port 19850-19859
+that will respond to a null CRTP packet with a valid CRTP packet.
+
+v2.0 changelog:
+- Complete rewrite to align with other CRTP driver implementations
+- Added dedicated _UdpReceiveThread class for asynchronous packet reception
+- Implemented functional scan_interface() that probes UDP ports 19850-19859
+- Fixed send_packet() with null checks, proper error callbacks, and removed checksum.
+- Added proper socket cleanup in close() method
+- Changed variable naming to align with other CRTP drivers and added docstrings
+- Added environment variable SCAN_ADDRESS for scan_interface() to specify target IP address.
+ This is useful for server and clients running on different hosts.
+"""
+import logging
+import os
import queue
import re
import socket
import struct
+import threading
from urllib.parse import urlparse
-from .crtpdriver import CRTPDriver
from .crtpstack import CRTPPacket
from .exceptions import WrongUriType
+from cflib.crtp.crtpdriver import CRTPDriver
__author__ = 'Bitcraze AB'
__all__ = ['UdpDriver']
+logger = logging.getLogger(__name__)
+
+_BASE_PORT = 19850
+_NR_OF_PORTS_TO_SCAN = 10
+_SCAN_TIMEOUT = 0.1
+
class UdpDriver(CRTPDriver):
+ """ Crazyflie UDP link driver """
def __init__(self):
- None
-
- def connect(self, uri, linkQualityCallback, linkErrorCallback):
+ """ Create the link driver """
+ CRTPDriver.__init__(self)
+ self.socket = None
+ self.addr = None
+ self.uri = ''
+ self.link_error_callback = None
+ self.in_queue = None
+ self._thread = None
+ self.needs_resending = False
+
+ def connect(self, uri, radio_link_statistics_callback, link_error_callback):
+ """
+ Connect the link driver to a specified URI of the format:
+ udp://:
+
+ The callback for radio link statistics is not used by the UDP driver.
+ The callback from link_error_callback will be called when an error
+ occurs with an error message.
+ """
if not re.search('^udp://', uri):
- raise WrongUriType('Not an UDP URI')
+ raise WrongUriType('Not a UDP URI')
+
+ if self.socket is not None:
+ raise Exception('Link already open!')
+
+ self.uri = uri
+ self.link_error_callback = link_error_callback
parse = urlparse(uri)
- self.queue = queue.Queue()
+ # Prepare the inter-thread communication queue
+ self.in_queue = queue.Queue()
+
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.addr = (parse.hostname, parse.port)
self.socket.connect(self.addr)
- self.socket.sendto('\xFF\x01\x01\x01'.encode(), self.addr)
-
- def receive_packet(self, time=0):
- data, addr = self.socket.recvfrom(1024)
-
- if data:
- data = struct.unpack('B' * (len(data) - 1), data[0:len(data) - 1])
- pk = CRTPPacket()
- pk.port = data[0]
- pk.data = data[1:]
- return pk
-
- try:
- if time == 0:
- return self.rxqueue.get(False)
- elif time < 0:
- while True:
- return self.rxqueue.get(True, 10)
- else:
- return self.rxqueue.get(True, time)
- except queue.Empty:
- return None
+ # Launch the comm thread
+ self._thread = _UdpReceiveThread(self.socket, self.in_queue,
+ link_error_callback)
+ self._thread.start()
+
+ def receive_packet(self, wait=0):
+ """
+ Receive a packet though the link. This call is blocking but will
+ timeout and return None if a timeout is supplied.
+ """
+ if wait == 0:
+ try:
+ return self.in_queue.get(False)
+ except queue.Empty:
+ return None
+ elif wait < 0:
+ try:
+ return self.in_queue.get(True)
+ except queue.Empty:
+ return None
+ else:
+ try:
+ return self.in_queue.get(True, wait)
+ except queue.Empty:
+ return None
def send_packet(self, pk):
- raw = (pk.port,) + struct.unpack('B' * len(pk.data), pk.data)
+ """ Send the packet pk though the link """
+ if self.socket is None:
+ return
- cksum = 0
- for i in raw:
- cksum += i
+ try:
+ raw = (pk.header,) + struct.unpack('B' * len(pk.data), pk.data)
+ data = struct.pack('B' * len(raw), *raw)
+ self.socket.send(data)
+ except Exception as e:
+ if self.link_error_callback:
+ self.link_error_callback(
+ 'UdpDriver: Could not send packet to Crazyflie\n'
+ 'Exception: %s' % e)
+
+ def pause(self):
+ self._thread.stop()
+ self._thread = None
+
+ def restart(self):
+ if self._thread:
+ return
+
+ self._thread = _UdpReceiveThread(self.socket, self.in_queue,
+ self.link_error_callback)
+ self._thread.start()
- cksum %= 256
+ def close(self):
+ """ Close the link. """
+ # Stop the comm thread
+ if self._thread:
+ self._thread.stop()
- data = ''.join(chr(v) for v in (raw + (cksum,)))
+ # Close the UDP socket
+ try:
+ if self.socket:
+ self.socket.close()
+ except Exception as e:
+ logger.info('Could not close {}'.format(e))
+ self.socket = None
- # print tuple(data)
- self.socket.sendto(data.encode(), self.addr)
+ # Clear callbacks
+ self.link_error_callback = None
- def close(self):
- # Remove this from the server clients list
- self.socket.sendto('\xFF\x01\x02\x02'.encode(), self.addr)
+ def get_status(self):
+ return 'No information available'
def get_name(self):
return 'udp'
- def scan_interface(self, address):
- return []
+ def scan_interface(self, address=None):
+ """ Scan interface for Crazyflies """
+ found = []
+ scan_address = os.getenv('SCAN_ADDRESS', '127.0.0.1')
+
+ for i in range(_NR_OF_PORTS_TO_SCAN):
+ port = _BASE_PORT + i
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ s.settimeout(_SCAN_TIMEOUT)
+ s.connect((scan_address, port))
+ s.send(b'\xFF') # Null CRTP packet as probe
+ s.recv(1024)
+ # Got a response, Crazyflie is available
+ s.close()
+ found.append(['udp://{}:{}'.format(scan_address, port), ''])
+ except socket.timeout:
+ s.close()
+ except Exception:
+ pass
+
+ return found
+
+
+# Receive thread
+class _UdpReceiveThread(threading.Thread):
+ """
+ UDP link receiver thread used to read data from the
+ UDP socket. """
+
+ def __init__(self, sock, inQueue, link_error_callback):
+ """ Create the object """
+ threading.Thread.__init__(self, name='UdpReceiveThread')
+ self._socket = sock
+ self._in_queue = inQueue
+ self._sp = False
+ self._link_error_callback = link_error_callback
+ self.daemon = True
+
+ def stop(self):
+ """ Stop the thread """
+ self._sp = True
+ try:
+ self.join()
+ except Exception:
+ pass
+
+ def run(self):
+ """ Run the receiver thread """
+ self._socket.settimeout(1.0)
+
+ while True:
+ if self._sp:
+ break
+ try:
+ packet = self._socket.recv(1024)
+ data = struct.unpack('B' * len(packet), packet)
+ if len(data) > 0:
+ pk = CRTPPacket(header=data[0], data=data[1:])
+ self._in_queue.put(pk)
+ except socket.timeout:
+ pass
+ except Exception as e:
+ import traceback
+
+ self._link_error_callback(
+ 'Error communicating with the Crazyflie\n'
+ 'Exception:%s\n\n%s' % (e, traceback.format_exc()))