Skip to content

Commit 99ad0e3

Browse files
llanescataffanel
andauthored
improve udp driver based on other driver receiver thread implementati… (#592)
* improve udp driver based on other driver receiver thread implementations. * added None check guard for _thread in close method and improved documentation with changelogs. * update UDP driver to use localhost for socket connection in scanning feature * update scan_interface to use configurable scan address from environment variable * added SCAN_ADDRESS feature documentation. * improve auto-doc generated documentation * fix: remove unnecessary blank lines --------- Co-authored-by: Arnaud Taffanel <arnaud@bitcraze.io>
1 parent 12895de commit 99ad0e3

1 file changed

Lines changed: 234 additions & 39 deletions

File tree

cflib/crtp/udpdriver.py

Lines changed: 234 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,81 +22,276 @@
2222
# GNU General Public License for more details.
2323
# You should have received a copy of the GNU General Public License
2424
# 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
2792
import queue
2893
import re
2994
import socket
3095
import struct
96+
import threading
3197
from urllib.parse import urlparse
3298

33-
from .crtpdriver import CRTPDriver
3499
from .crtpstack import CRTPPacket
35100
from .exceptions import WrongUriType
101+
from cflib.crtp.crtpdriver import CRTPDriver
36102

37103
__author__ = 'Bitcraze AB'
38104
__all__ = ['UdpDriver']
39105

106+
logger = logging.getLogger(__name__)
107+
108+
_BASE_PORT = 19850
109+
_NR_OF_PORTS_TO_SCAN = 10
110+
_SCAN_TIMEOUT = 0.1
111+
40112

41113
class UdpDriver(CRTPDriver):
114+
""" Crazyflie UDP link driver """
42115

43116
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>
45131
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+
"""
47136
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
49144

50145
parse = urlparse(uri)
51146

52-
self.queue = queue.Queue()
147+
# Prepare the inter-thread communication queue
148+
self.in_queue = queue.Queue()
149+
53150
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
54151
self.addr = (parse.hostname, parse.port)
55152
self.socket.connect(self.addr)
56153

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()
58158

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
61179

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
68184

69185
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)
79194

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
82198

83-
cksum = 0
84-
for i in raw:
85-
cksum += i
199+
def restart(self):
200+
if self._thread:
201+
return
86202

87-
cksum %= 256
203+
self._thread = _UdpReceiveThread(self.socket, self.in_queue,
204+
self.link_error_callback)
205+
self._thread.start()
88206

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()
90212

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
93220

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'
97226

98227
def get_name(self):
99228
return 'udp'
100229

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

Comments
 (0)