|
22 | 22 | # GNU General Public License for more details. |
23 | 23 | # You should have received a copy of the GNU General Public License |
24 | 24 | # along with this program. If not, see <https://www.gnu.org/licenses/>. |
25 | | -""" CRTP UDP Driver. Work either with the UDP server or with an UDP device |
26 | | -See udpserver.py for the protocol""" |
| 25 | +""" |
| 26 | +Crazyflie UDP driver. |
| 27 | +
|
| 28 | +This driver communicates with a Crazyflie (or simulator) over UDP using the CRTP |
| 29 | +protocol. It enables connecting to software-in-the-loop (SITL) simulations. |
| 30 | +Scanning feature assumes a crazyflie server is running on port 19850-19859 |
| 31 | +that will respond to a null CRTP packet with a valid CRTP packet. |
| 32 | +
|
| 33 | +Wire Protocol |
| 34 | +------------- |
| 35 | +The UDP driver uses a simple wire protocol where each UDP datagram contains exactly |
| 36 | +one CRTP packet. The packet format is: |
| 37 | +
|
| 38 | + Byte 0: CRTP header (port, channel, and reserved bits) |
| 39 | + Bytes 1-N: CRTP payload data (0 to 30 bytes) set by CRTP_MAX_DATA_SIZE in firmware |
| 40 | +
|
| 41 | +Total packet size: 1 to 31 bytes per UDP datagram |
| 42 | +
|
| 43 | +The payload follows standard CRTP format as defined in the Crazyflie protocol |
| 44 | +specification. No additional framing, checksums, or encapsulation is added by |
| 45 | +the UDP driver. |
| 46 | +
|
| 47 | +Scan Behavior |
| 48 | +------------- |
| 49 | +The scan_interface() method discovers available Crazyflie devices by probing |
| 50 | +UDP ports in sequence: |
| 51 | +
|
| 52 | +Port Range: 19850 to 19859 (10 ports total, BASE_PORT + 0 through 9) |
| 53 | +Scan Address: Configurable via SCAN_ADDRESS environment variable (default: 127.0.0.1) |
| 54 | +Probe Packet: Single 0xFF byte (null CRTP packet) |
| 55 | +Timeout: 0.1 seconds per port |
| 56 | +
|
| 57 | +Scan Procedure: |
| 58 | + 1. For each port in range: |
| 59 | + - Send 0xFF probe packet to SCAN_ADDRESS:port |
| 60 | + - Wait up to 0.1 seconds for response |
| 61 | + - If any response received, device is present |
| 62 | + - Add URI "udp://SCAN_ADDRESS:port" to results |
| 63 | +
|
| 64 | +Environment Variables |
| 65 | +--------------------- |
| 66 | +SCAN_ADDRESS: IP address to scan for Crazyflie devices |
| 67 | + Default: 127.0.0.1 |
| 68 | + Example: export SCAN_ADDRESS=192.168.1.100 |
| 69 | +
|
| 70 | + This is useful when the Crazyflie server and client run on different |
| 71 | + hosts. The client will scan the specified address instead of localhost. |
| 72 | +
|
| 73 | +Connection URI Format |
| 74 | +-------------------- |
| 75 | +udp://<host>:<port> |
| 76 | +
|
| 77 | +Examples: |
| 78 | + udp://127.0.0.1:19850 - Local simulator on port 19850 |
| 79 | + udp://192.168.1.5:19850 - Remote device at 192.168.1.5 |
| 80 | +""" |
| 81 | +# changelog: |
| 82 | +# - Complete rewrite to align with other CRTP driver implementations |
| 83 | +# - Added dedicated _UdpReceiveThread class for asynchronous packet reception |
| 84 | +# - Implemented functional scan_interface() that probes UDP ports 19850-19859 |
| 85 | +# - Fixed send_packet() with null checks, proper error callbacks, and removed checksum |
| 86 | +# - Added proper socket cleanup in close() method |
| 87 | +# - Changed variable naming to align with other CRTP drivers and added docstrings |
| 88 | +# - Added environment variable SCAN_ADDRESS for scan_interface() to specify target IP address |
| 89 | +# This is useful for server and clients running on different hosts |
| 90 | +import logging |
| 91 | +import os |
27 | 92 | import queue |
28 | 93 | import re |
29 | 94 | import socket |
30 | 95 | import struct |
| 96 | +import threading |
31 | 97 | from urllib.parse import urlparse |
32 | 98 |
|
33 | | -from .crtpdriver import CRTPDriver |
34 | 99 | from .crtpstack import CRTPPacket |
35 | 100 | from .exceptions import WrongUriType |
| 101 | +from cflib.crtp.crtpdriver import CRTPDriver |
36 | 102 |
|
37 | 103 | __author__ = 'Bitcraze AB' |
38 | 104 | __all__ = ['UdpDriver'] |
39 | 105 |
|
| 106 | +logger = logging.getLogger(__name__) |
| 107 | + |
| 108 | +_BASE_PORT = 19850 |
| 109 | +_NR_OF_PORTS_TO_SCAN = 10 |
| 110 | +_SCAN_TIMEOUT = 0.1 |
| 111 | + |
40 | 112 |
|
41 | 113 | class UdpDriver(CRTPDriver): |
| 114 | + """ Crazyflie UDP link driver """ |
42 | 115 |
|
43 | 116 | def __init__(self): |
44 | | - None |
| 117 | + """ Create the link driver """ |
| 118 | + CRTPDriver.__init__(self) |
| 119 | + self.socket = None |
| 120 | + self.addr = None |
| 121 | + self.uri = '' |
| 122 | + self.link_error_callback = None |
| 123 | + self.in_queue = None |
| 124 | + self._thread = None |
| 125 | + self.needs_resending = False |
| 126 | + |
| 127 | + def connect(self, uri, radio_link_statistics_callback, link_error_callback): |
| 128 | + """ |
| 129 | + Connect the link driver to a specified URI of the format: |
| 130 | + udp://<host>:<port> |
45 | 131 |
|
46 | | - def connect(self, uri, linkQualityCallback, linkErrorCallback): |
| 132 | + The callback for radio link statistics is not used by the UDP driver. |
| 133 | + The callback from link_error_callback will be called when an error |
| 134 | + occurs with an error message. |
| 135 | + """ |
47 | 136 | if not re.search('^udp://', uri): |
48 | | - raise WrongUriType('Not an UDP URI') |
| 137 | + raise WrongUriType('Not a UDP URI') |
| 138 | + |
| 139 | + if self.socket is not None: |
| 140 | + raise Exception('Link already open!') |
| 141 | + |
| 142 | + self.uri = uri |
| 143 | + self.link_error_callback = link_error_callback |
49 | 144 |
|
50 | 145 | parse = urlparse(uri) |
51 | 146 |
|
52 | | - self.queue = queue.Queue() |
| 147 | + # Prepare the inter-thread communication queue |
| 148 | + self.in_queue = queue.Queue() |
| 149 | + |
53 | 150 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
54 | 151 | self.addr = (parse.hostname, parse.port) |
55 | 152 | self.socket.connect(self.addr) |
56 | 153 |
|
57 | | - self.socket.sendto('\xFF\x01\x01\x01'.encode(), self.addr) |
| 154 | + # Launch the comm thread |
| 155 | + self._thread = _UdpReceiveThread(self.socket, self.in_queue, |
| 156 | + link_error_callback) |
| 157 | + self._thread.start() |
58 | 158 |
|
59 | | - def receive_packet(self, time=0): |
60 | | - data, addr = self.socket.recvfrom(1024) |
| 159 | + def receive_packet(self, wait=0): |
| 160 | + """ |
| 161 | + Receive a packet though the link. This call is blocking but will |
| 162 | + timeout and return None if a timeout is supplied. |
| 163 | + """ |
| 164 | + if wait == 0: |
| 165 | + try: |
| 166 | + return self.in_queue.get(False) |
| 167 | + except queue.Empty: |
| 168 | + return None |
| 169 | + elif wait < 0: |
| 170 | + try: |
| 171 | + return self.in_queue.get(True) |
| 172 | + except queue.Empty: |
| 173 | + return None |
| 174 | + else: |
| 175 | + try: |
| 176 | + return self.in_queue.get(True, wait) |
| 177 | + except queue.Empty: |
| 178 | + return None |
61 | 179 |
|
62 | | - if data: |
63 | | - data = struct.unpack('B' * (len(data) - 1), data[0:len(data) - 1]) |
64 | | - pk = CRTPPacket() |
65 | | - pk.port = data[0] |
66 | | - pk.data = data[1:] |
67 | | - return pk |
| 180 | + def send_packet(self, pk): |
| 181 | + """ Send the packet pk though the link """ |
| 182 | + if self.socket is None: |
| 183 | + return |
68 | 184 |
|
69 | 185 | try: |
70 | | - if time == 0: |
71 | | - return self.rxqueue.get(False) |
72 | | - elif time < 0: |
73 | | - while True: |
74 | | - return self.rxqueue.get(True, 10) |
75 | | - else: |
76 | | - return self.rxqueue.get(True, time) |
77 | | - except queue.Empty: |
78 | | - return None |
| 186 | + raw = (pk.header,) + struct.unpack('B' * len(pk.data), pk.data) |
| 187 | + data = struct.pack('B' * len(raw), *raw) |
| 188 | + self.socket.send(data) |
| 189 | + except Exception as e: |
| 190 | + if self.link_error_callback: |
| 191 | + self.link_error_callback( |
| 192 | + 'UdpDriver: Could not send packet to Crazyflie\n' |
| 193 | + 'Exception: %s' % e) |
79 | 194 |
|
80 | | - def send_packet(self, pk): |
81 | | - raw = (pk.port,) + struct.unpack('B' * len(pk.data), pk.data) |
| 195 | + def pause(self): |
| 196 | + self._thread.stop() |
| 197 | + self._thread = None |
82 | 198 |
|
83 | | - cksum = 0 |
84 | | - for i in raw: |
85 | | - cksum += i |
| 199 | + def restart(self): |
| 200 | + if self._thread: |
| 201 | + return |
86 | 202 |
|
87 | | - cksum %= 256 |
| 203 | + self._thread = _UdpReceiveThread(self.socket, self.in_queue, |
| 204 | + self.link_error_callback) |
| 205 | + self._thread.start() |
88 | 206 |
|
89 | | - data = ''.join(chr(v) for v in (raw + (cksum,))) |
| 207 | + def close(self): |
| 208 | + """ Close the link. """ |
| 209 | + # Stop the comm thread |
| 210 | + if self._thread: |
| 211 | + self._thread.stop() |
90 | 212 |
|
91 | | - # print tuple(data) |
92 | | - self.socket.sendto(data.encode(), self.addr) |
| 213 | + # Close the UDP socket |
| 214 | + try: |
| 215 | + if self.socket: |
| 216 | + self.socket.close() |
| 217 | + except Exception as e: |
| 218 | + logger.info('Could not close {}'.format(e)) |
| 219 | + self.socket = None |
93 | 220 |
|
94 | | - def close(self): |
95 | | - # Remove this from the server clients list |
96 | | - self.socket.sendto('\xFF\x01\x02\x02'.encode(), self.addr) |
| 221 | + # Clear callbacks |
| 222 | + self.link_error_callback = None |
| 223 | + |
| 224 | + def get_status(self): |
| 225 | + return 'No information available' |
97 | 226 |
|
98 | 227 | def get_name(self): |
99 | 228 | return 'udp' |
100 | 229 |
|
101 | | - def scan_interface(self, address): |
102 | | - return [] |
| 230 | + def scan_interface(self, address=None): |
| 231 | + """ Scan interface for Crazyflies """ |
| 232 | + found = [] |
| 233 | + scan_address = os.getenv('SCAN_ADDRESS', '127.0.0.1') |
| 234 | + |
| 235 | + for i in range(_NR_OF_PORTS_TO_SCAN): |
| 236 | + port = _BASE_PORT + i |
| 237 | + try: |
| 238 | + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| 239 | + s.settimeout(_SCAN_TIMEOUT) |
| 240 | + s.connect((scan_address, port)) |
| 241 | + s.send(b'\xFF') # Null CRTP packet as probe |
| 242 | + s.recv(1024) |
| 243 | + # Got a response, Crazyflie is available |
| 244 | + s.close() |
| 245 | + found.append(['udp://{}:{}'.format(scan_address, port), '']) |
| 246 | + except socket.timeout: |
| 247 | + s.close() |
| 248 | + except Exception: |
| 249 | + pass |
| 250 | + |
| 251 | + return found |
| 252 | + |
| 253 | + |
| 254 | +# Receive thread |
| 255 | +class _UdpReceiveThread(threading.Thread): |
| 256 | + """ |
| 257 | + UDP link receiver thread used to read data from the |
| 258 | + UDP socket. """ |
| 259 | + |
| 260 | + def __init__(self, sock, inQueue, link_error_callback): |
| 261 | + """ Create the object """ |
| 262 | + threading.Thread.__init__(self, name='UdpReceiveThread') |
| 263 | + self._socket = sock |
| 264 | + self._in_queue = inQueue |
| 265 | + self._sp = False |
| 266 | + self._link_error_callback = link_error_callback |
| 267 | + self.daemon = True |
| 268 | + |
| 269 | + def stop(self): |
| 270 | + """ Stop the thread """ |
| 271 | + self._sp = True |
| 272 | + try: |
| 273 | + self.join() |
| 274 | + except Exception: |
| 275 | + pass |
| 276 | + |
| 277 | + def run(self): |
| 278 | + """ Run the receiver thread """ |
| 279 | + self._socket.settimeout(1.0) |
| 280 | + |
| 281 | + while True: |
| 282 | + if self._sp: |
| 283 | + break |
| 284 | + try: |
| 285 | + packet = self._socket.recv(1024) |
| 286 | + data = struct.unpack('B' * len(packet), packet) |
| 287 | + if len(data) > 0: |
| 288 | + pk = CRTPPacket(header=data[0], data=data[1:]) |
| 289 | + self._in_queue.put(pk) |
| 290 | + except socket.timeout: |
| 291 | + pass |
| 292 | + except Exception as e: |
| 293 | + import traceback |
| 294 | + |
| 295 | + self._link_error_callback( |
| 296 | + 'Error communicating with the Crazyflie\n' |
| 297 | + 'Exception:%s\n\n%s' % (e, traceback.format_exc())) |
0 commit comments