dronemaster 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dronemaster/Drone.py +732 -0
- dronemaster/__init__.py +3 -0
- dronemaster/low_level.py +214 -0
- dronemaster/utils.py +8 -0
- dronemaster-1.0.0.dist-info/METADATA +135 -0
- dronemaster-1.0.0.dist-info/RECORD +9 -0
- dronemaster-1.0.0.dist-info/WHEEL +5 -0
- dronemaster-1.0.0.dist-info/licenses/LICENSE +661 -0
- dronemaster-1.0.0.dist-info/top_level.txt +1 -0
dronemaster/Drone.py
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
from .low_level import ProtocolError, RepeatAction, RetryAction, Action, OK, ANY
|
|
4
|
+
from . import low_level as l
|
|
5
|
+
from .utils import limit
|
|
6
|
+
from time import time
|
|
7
|
+
from typing import Dict, TypedDict, Any, Optional, Tuple, Callable, Coroutine, Literal
|
|
8
|
+
|
|
9
|
+
class DroneState(TypedDict):
|
|
10
|
+
pitch: int
|
|
11
|
+
"""pitch in degrees"""
|
|
12
|
+
roll: int
|
|
13
|
+
"""roll in degrees"""
|
|
14
|
+
yaw: int
|
|
15
|
+
"""yaw in degrees"""
|
|
16
|
+
|
|
17
|
+
vgx: int
|
|
18
|
+
"""velocity in x-direction (forwards/backwards) in dm/s"""
|
|
19
|
+
vgy: int
|
|
20
|
+
"""velocity in y-direction (left/right) in dm/s"""
|
|
21
|
+
vgz: int
|
|
22
|
+
"""velocity in z-direction (up/down) in dm/s"""
|
|
23
|
+
|
|
24
|
+
bat: int
|
|
25
|
+
"""battery percentage"""
|
|
26
|
+
templ: int
|
|
27
|
+
"""the lower range of the internal temperature sensor in °C"""
|
|
28
|
+
temph: int
|
|
29
|
+
"""the upper range of the internal temperature sensor in °C"""
|
|
30
|
+
|
|
31
|
+
tof: int
|
|
32
|
+
"""the vertical distance to ground in cm. reported as 10 if out of range"""
|
|
33
|
+
|
|
34
|
+
h: int
|
|
35
|
+
"""the calculated height, note that this only works in flight"""
|
|
36
|
+
|
|
37
|
+
time: int
|
|
38
|
+
"""number of seconds the drone has been flying"""
|
|
39
|
+
|
|
40
|
+
agx: float
|
|
41
|
+
"""acceleration in x-direction in cm/s²"""
|
|
42
|
+
agy: float
|
|
43
|
+
"""acceleration in y-direction in cm/s²"""
|
|
44
|
+
agz: float
|
|
45
|
+
"""acceleration in z-direction in cm/s²"""
|
|
46
|
+
|
|
47
|
+
baro: float
|
|
48
|
+
"""Height above sea level as reported by the barometer in m"""
|
|
49
|
+
|
|
50
|
+
last_update: float
|
|
51
|
+
"""unix timestamp of the last update"""
|
|
52
|
+
|
|
53
|
+
delta: float
|
|
54
|
+
"""time in seconds between the last two state packets"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Drone:
|
|
58
|
+
def __init__(self, ip: str):
|
|
59
|
+
self.ip = ip
|
|
60
|
+
self.flight = Flight(self)
|
|
61
|
+
self.rgb = RGBLed(self)
|
|
62
|
+
self.matrix = Matrix(self)
|
|
63
|
+
self.video = Video(self)
|
|
64
|
+
self.last_state: Dict[str, Any] = {}
|
|
65
|
+
self.connected = False
|
|
66
|
+
self._state_subscribers = []
|
|
67
|
+
|
|
68
|
+
async def action(self, action: Action, ignore_not_connected: bool = False) -> Any:
|
|
69
|
+
if not self.connected and not ignore_not_connected:
|
|
70
|
+
raise ProtocolError("you are not connected to the drone")
|
|
71
|
+
return await l.protocol.send_action(action, self.ip)
|
|
72
|
+
|
|
73
|
+
async def _on_state(self, state: dict):
|
|
74
|
+
if "last_update" in self.last_state:
|
|
75
|
+
delta = time() - self.last_state["last_update"]
|
|
76
|
+
else:
|
|
77
|
+
delta = 0
|
|
78
|
+
state.update({"last_update": time(), "delta": delta})
|
|
79
|
+
self.last_state = state
|
|
80
|
+
for sub in self._state_subscribers:
|
|
81
|
+
r = sub(state)
|
|
82
|
+
if inspect.iscoroutine(r):
|
|
83
|
+
await r
|
|
84
|
+
|
|
85
|
+
def state_subscribe(self, callable: Callable[[DroneState], Coroutine[Any, Any, None]]):
|
|
86
|
+
self._state_subscribers.append(callable)
|
|
87
|
+
|
|
88
|
+
async def initialize(self) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Initializes the drone connection and optionally starts the communication channel.
|
|
91
|
+
This function must be called before any other
|
|
92
|
+
|
|
93
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
94
|
+
:raises TimeoutError: raised when not answering after 2.5s
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
if not hasattr(l, "transport"):
|
|
98
|
+
await l.start()
|
|
99
|
+
|
|
100
|
+
if l.protocol.on_state != l.protocol._on_state:
|
|
101
|
+
raise RuntimeError("the current sdk supports only one connected drone at a time")
|
|
102
|
+
|
|
103
|
+
await self.action(RetryAction(
|
|
104
|
+
command="command",
|
|
105
|
+
positive_answers=OK,
|
|
106
|
+
negative_answers=ANY,
|
|
107
|
+
retry_count=5,
|
|
108
|
+
timeout=0.5
|
|
109
|
+
), ignore_not_connected=True)
|
|
110
|
+
|
|
111
|
+
l.protocol.on_state = self._on_state
|
|
112
|
+
self.connected = True
|
|
113
|
+
|
|
114
|
+
async def serial_number(self) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Queries the drone for its serial number in the format `[A-Z0-9]{14}`
|
|
117
|
+
|
|
118
|
+
:returns: the serial number in the format `[A-Z0-9]{14}`
|
|
119
|
+
:raises ProtocolError: raised when not receiving the correct format
|
|
120
|
+
:raises TimeoutError: raised when not answering after 5s
|
|
121
|
+
"""
|
|
122
|
+
return await self.action(RetryAction(
|
|
123
|
+
command="sn?",
|
|
124
|
+
positive_answers=[r"^[A-Z0-9]{14}$"],
|
|
125
|
+
negative_answers=ANY,
|
|
126
|
+
retry_count=5,
|
|
127
|
+
timeout=1
|
|
128
|
+
))
|
|
129
|
+
|
|
130
|
+
async def battery(self) -> int:
|
|
131
|
+
"""
|
|
132
|
+
Queries the drone for its current battery charce percentage from 0-100
|
|
133
|
+
|
|
134
|
+
:returns: the battery percentage
|
|
135
|
+
:raises ProtocolError: raised when not receiving the battery percentage
|
|
136
|
+
:raises TimeoutError: raised when not answering after 5s
|
|
137
|
+
"""
|
|
138
|
+
return int(await self.action(RetryAction(
|
|
139
|
+
command="battery?",
|
|
140
|
+
positive_answers=[r"^\d{1,3}$"],
|
|
141
|
+
negative_answers=ANY,
|
|
142
|
+
retry_count=5,
|
|
143
|
+
timeout=1
|
|
144
|
+
)))
|
|
145
|
+
|
|
146
|
+
async def ext_tof(self):
|
|
147
|
+
"""
|
|
148
|
+
Reads the horizontal distance of the drone to the front.
|
|
149
|
+
|
|
150
|
+
:returns: the horizontal distance to the front in mm or `None` if out of range
|
|
151
|
+
:raises ProtocolError: raised when not receiving data in the correct format
|
|
152
|
+
:raises TimeoutError: raised when not answering after 2.5s
|
|
153
|
+
"""
|
|
154
|
+
raw = await self.action(RetryAction(
|
|
155
|
+
command="EXT tof?",
|
|
156
|
+
positive_answers=[r"^tof \d+$"],
|
|
157
|
+
negative_answers=ANY,
|
|
158
|
+
retry_count=5,
|
|
159
|
+
timeout=1
|
|
160
|
+
))
|
|
161
|
+
tof = int(raw.split(" ")[1])
|
|
162
|
+
|
|
163
|
+
if tof == 8190:
|
|
164
|
+
return None
|
|
165
|
+
else:
|
|
166
|
+
return tof
|
|
167
|
+
|
|
168
|
+
async def tof(self) -> Optional[int]:
|
|
169
|
+
"""
|
|
170
|
+
Reads the vertical distance of the drone to ground.
|
|
171
|
+
|
|
172
|
+
:returns: the vertical distance to ground in mm or `None` if out of range
|
|
173
|
+
:raises ProtocolError: raised when not receiving data in the correct format
|
|
174
|
+
:raises TimeoutError: raised when not answering after 2.5s
|
|
175
|
+
"""
|
|
176
|
+
raw = await self.action(RetryAction(
|
|
177
|
+
command="tof?",
|
|
178
|
+
positive_answers=[r"^\d+mm$"],
|
|
179
|
+
negative_answers=ANY,
|
|
180
|
+
retry_count=5,
|
|
181
|
+
timeout=1
|
|
182
|
+
))
|
|
183
|
+
tof = int(raw[:-2])
|
|
184
|
+
|
|
185
|
+
if tof == 100:
|
|
186
|
+
return None
|
|
187
|
+
else:
|
|
188
|
+
return tof
|
|
189
|
+
|
|
190
|
+
async def keepalive(self) -> bool:
|
|
191
|
+
"""
|
|
192
|
+
Sends a "ping" packet to the drone to stop it from landing automatically, but only if there is no currently waiting command
|
|
193
|
+
|
|
194
|
+
:returns: True when the ping was successful, False when it was skipped
|
|
195
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
196
|
+
:raises TimeoutError: raised when not answering after 2.5s
|
|
197
|
+
"""
|
|
198
|
+
if l.protocol.waiting_action is None:
|
|
199
|
+
await self.action(RetryAction(
|
|
200
|
+
command="command",
|
|
201
|
+
positive_answers=OK,
|
|
202
|
+
negative_answers=ANY,
|
|
203
|
+
retry_count=5,
|
|
204
|
+
timeout=0.5
|
|
205
|
+
))
|
|
206
|
+
return True
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
async def send_raw_command(self, command, wait_for_answer: bool = True, timeout: float = 1) -> Optional[str]:
|
|
210
|
+
"""
|
|
211
|
+
Sends a raw command to the drone. Intended for debugging/manual mode
|
|
212
|
+
|
|
213
|
+
:param wait_for_answer: if the code expects an answer
|
|
214
|
+
:param timeout: when expecting an answer, how long to wait
|
|
215
|
+
:returns: the answer or `None` if no answer is expected
|
|
216
|
+
:raises ProtocolError: should never happen
|
|
217
|
+
:raises TimeoutError: raised when not answering after 5 * timeout
|
|
218
|
+
"""
|
|
219
|
+
if wait_for_answer:
|
|
220
|
+
return await self.action(RetryAction(
|
|
221
|
+
command=command,
|
|
222
|
+
positive_answers=ANY,
|
|
223
|
+
negative_answers=ANY,
|
|
224
|
+
retry_count=5,
|
|
225
|
+
timeout=timeout
|
|
226
|
+
))
|
|
227
|
+
else:
|
|
228
|
+
l.protocol.send_command_noanswer(command, self.ip)
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
def reboot(self) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Reboots the drone
|
|
234
|
+
"""
|
|
235
|
+
l.protocol.send_command_noanswer("reboot", self.ip)
|
|
236
|
+
self.connected = False
|
|
237
|
+
l.protocol.on_state = l.protocol._on_state
|
|
238
|
+
|
|
239
|
+
class Module:
|
|
240
|
+
def __init__(self, drone: Drone):
|
|
241
|
+
self.drone = drone
|
|
242
|
+
|
|
243
|
+
async def action(self, action: Action):
|
|
244
|
+
return await self.drone.action(action)
|
|
245
|
+
|
|
246
|
+
class Video(Module):
|
|
247
|
+
async def streamon(self) -> None:
|
|
248
|
+
"""
|
|
249
|
+
Starts the stream so the drone sends the H264 stream to port 11111
|
|
250
|
+
|
|
251
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
252
|
+
:raises TimeoutError: raised when not answering after 5s
|
|
253
|
+
"""
|
|
254
|
+
await self.action(RetryAction(
|
|
255
|
+
command="streamon",
|
|
256
|
+
positive_answers=OK,
|
|
257
|
+
negative_answers=ANY,
|
|
258
|
+
retry_count=5,
|
|
259
|
+
timeout=1
|
|
260
|
+
))
|
|
261
|
+
|
|
262
|
+
async def streamoff(self) -> None:
|
|
263
|
+
"""
|
|
264
|
+
Stops the video stream
|
|
265
|
+
|
|
266
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
267
|
+
:raises TimeoutError: raised when not answering after 5s
|
|
268
|
+
"""
|
|
269
|
+
await self.action(RetryAction(
|
|
270
|
+
command="streamoff",
|
|
271
|
+
positive_answers=OK,
|
|
272
|
+
negative_answers=ANY,
|
|
273
|
+
retry_count=5,
|
|
274
|
+
timeout=1
|
|
275
|
+
))
|
|
276
|
+
|
|
277
|
+
async def downvision(self, on: bool) -> None:
|
|
278
|
+
"""
|
|
279
|
+
Switches the used camera.
|
|
280
|
+
|
|
281
|
+
:param on: if the downwards camera should be used
|
|
282
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
283
|
+
:raises TimeoutError: raised when not answering after 5s
|
|
284
|
+
"""
|
|
285
|
+
await self.action(RetryAction(
|
|
286
|
+
command=f"downvision {1 if on else 0}",
|
|
287
|
+
positive_answers=OK,
|
|
288
|
+
negative_answers=ANY,
|
|
289
|
+
retry_count=5,
|
|
290
|
+
timeout=1
|
|
291
|
+
))
|
|
292
|
+
|
|
293
|
+
async def setfps(self, fps: Literal["high","middle","low"]) -> None:
|
|
294
|
+
"""
|
|
295
|
+
Sets the streams fps to
|
|
296
|
+
- `high` -> 30fps
|
|
297
|
+
- `middle` -> 15fps
|
|
298
|
+
- `low` -> 5fps
|
|
299
|
+
|
|
300
|
+
:param fps: the requested fps
|
|
301
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
302
|
+
:raises TimeoutError: raised when not answering after 5s
|
|
303
|
+
"""
|
|
304
|
+
await self.action(RetryAction(
|
|
305
|
+
command=f"setfps {fps}",
|
|
306
|
+
positive_answers=OK,
|
|
307
|
+
negative_answers=ANY,
|
|
308
|
+
retry_count=5,
|
|
309
|
+
timeout=1
|
|
310
|
+
))
|
|
311
|
+
|
|
312
|
+
async def setbitrate(self, bitrate: Literal["auto",1,2,3,4,5]) -> None:
|
|
313
|
+
"""
|
|
314
|
+
Sets the streams maximum bitrate to auto or the requested Mbps
|
|
315
|
+
|
|
316
|
+
:param bitrate: the bitrate in Mbps or `auto`
|
|
317
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
318
|
+
:raises TimeoutError: raised when not answering after 5s
|
|
319
|
+
"""
|
|
320
|
+
bt = 0
|
|
321
|
+
|
|
322
|
+
if bitrate == "auto":
|
|
323
|
+
bt = 0
|
|
324
|
+
else:
|
|
325
|
+
bt = bitrate
|
|
326
|
+
|
|
327
|
+
await self.action(RetryAction(
|
|
328
|
+
command=f"setbitrate {bt}",
|
|
329
|
+
positive_answers=OK,
|
|
330
|
+
negative_answers=ANY,
|
|
331
|
+
retry_count=5,
|
|
332
|
+
timeout=1
|
|
333
|
+
))
|
|
334
|
+
|
|
335
|
+
async def setresolution(self, resolution: Literal["high","low"]) -> None:
|
|
336
|
+
"""
|
|
337
|
+
Sets the video streams resolution
|
|
338
|
+
- `high` -> 720P
|
|
339
|
+
- `low` -> 480P
|
|
340
|
+
|
|
341
|
+
:param resolution: the requested resolution
|
|
342
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
343
|
+
:raises TimeoutError: raised when not answering after 5s
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
await self.action(RetryAction(
|
|
347
|
+
command=f"setresolution {resolution}",
|
|
348
|
+
positive_answers=OK,
|
|
349
|
+
negative_answers=ANY,
|
|
350
|
+
retry_count=5,
|
|
351
|
+
timeout=1
|
|
352
|
+
))
|
|
353
|
+
|
|
354
|
+
class Flight(Module):
|
|
355
|
+
async def takeoff(self) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Tries to takeoff
|
|
358
|
+
|
|
359
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
360
|
+
:raises TimeoutError: raised when not answering after 20s
|
|
361
|
+
"""
|
|
362
|
+
await self.action(RepeatAction(
|
|
363
|
+
command="takeoff",
|
|
364
|
+
positive_answers=OK,
|
|
365
|
+
negative_answers=ANY,
|
|
366
|
+
timeout=20
|
|
367
|
+
))
|
|
368
|
+
|
|
369
|
+
async def forward(self, dist: int, timeout: float = 5) -> None:
|
|
370
|
+
"""
|
|
371
|
+
The drone flies `dist` cm forwards.
|
|
372
|
+
|
|
373
|
+
:param dist: distance in cm in the range [20, 500]
|
|
374
|
+
:param timeout: the timeout in seconds
|
|
375
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
376
|
+
:raises TimeoutError: raised when not answering after `timeout`
|
|
377
|
+
"""
|
|
378
|
+
limit(dist, 20, 500)
|
|
379
|
+
await self.action(RepeatAction(
|
|
380
|
+
command=f"forward {dist}",
|
|
381
|
+
positive_answers=OK,
|
|
382
|
+
negative_answers=ANY,
|
|
383
|
+
timeout=timeout
|
|
384
|
+
))
|
|
385
|
+
|
|
386
|
+
async def back(self, dist: int, timeout: float = 5) -> None:
|
|
387
|
+
"""
|
|
388
|
+
The drone flies `dist` cm backwards.
|
|
389
|
+
|
|
390
|
+
:param dist: distance in cm in the range [20, 500]
|
|
391
|
+
:param timeout: the timeout in seconds
|
|
392
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
393
|
+
:raises TimeoutError: raised when not answering after `timeout`
|
|
394
|
+
"""
|
|
395
|
+
limit(dist, 20, 500)
|
|
396
|
+
await self.action(RepeatAction(
|
|
397
|
+
command=f"back {dist}",
|
|
398
|
+
positive_answers=OK,
|
|
399
|
+
negative_answers=ANY,
|
|
400
|
+
timeout=timeout
|
|
401
|
+
))
|
|
402
|
+
|
|
403
|
+
async def up(self, dist: int, timeout: float = 5) -> None:
|
|
404
|
+
"""
|
|
405
|
+
The drone flies `dist` cm upwards.
|
|
406
|
+
|
|
407
|
+
:param dist: distance in cm in the range [20, 500]
|
|
408
|
+
:param timeout: the timeout in seconds
|
|
409
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
410
|
+
:raises TimeoutError: raised when not answering after `timeout`
|
|
411
|
+
"""
|
|
412
|
+
limit(dist, 20, 500)
|
|
413
|
+
await self.action(RepeatAction(
|
|
414
|
+
command=f"up {dist}",
|
|
415
|
+
positive_answers=OK,
|
|
416
|
+
negative_answers=ANY,
|
|
417
|
+
timeout=timeout
|
|
418
|
+
))
|
|
419
|
+
|
|
420
|
+
async def down(self, dist: int, timeout: float = 5) -> None:
|
|
421
|
+
"""
|
|
422
|
+
The drone flies `dist` cm downwards.
|
|
423
|
+
|
|
424
|
+
:param dist: distance in cm in the range [20, 500]
|
|
425
|
+
:param timeout: the timeout in seconds
|
|
426
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
427
|
+
:raises TimeoutError: raised when not answering after `timeout`
|
|
428
|
+
"""
|
|
429
|
+
limit(dist, 20, 500)
|
|
430
|
+
await self.action(RepeatAction(
|
|
431
|
+
command=f"down {dist}",
|
|
432
|
+
positive_answers=OK,
|
|
433
|
+
negative_answers=ANY,
|
|
434
|
+
timeout=timeout
|
|
435
|
+
))
|
|
436
|
+
|
|
437
|
+
async def left(self, dist: int, timeout: float = 5) -> None:
|
|
438
|
+
"""
|
|
439
|
+
The drone flies `dist` cm to the left.
|
|
440
|
+
|
|
441
|
+
:param dist: distance in cm in the range [20, 500]
|
|
442
|
+
:param timeout: the timeout in seconds
|
|
443
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
444
|
+
:raises TimeoutError: raised when not answering after `timeout`
|
|
445
|
+
"""
|
|
446
|
+
limit(dist, 20, 500)
|
|
447
|
+
await self.action(RepeatAction(
|
|
448
|
+
command=f"left {dist}",
|
|
449
|
+
positive_answers=OK,
|
|
450
|
+
negative_answers=ANY,
|
|
451
|
+
timeout=timeout
|
|
452
|
+
))
|
|
453
|
+
|
|
454
|
+
async def right(self, dist: int, timeout: float = 5) -> None:
|
|
455
|
+
"""
|
|
456
|
+
The drone flies `dist` cm to the right.
|
|
457
|
+
|
|
458
|
+
:param dist: distance in cm in the range [20, 500]
|
|
459
|
+
:param timeout: the timeout in seconds
|
|
460
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
461
|
+
:raises TimeoutError: raised when not answering after `timeout`
|
|
462
|
+
"""
|
|
463
|
+
limit(dist, 20, 500)
|
|
464
|
+
await self.action(RepeatAction(
|
|
465
|
+
command=f"right {dist}",
|
|
466
|
+
positive_answers=OK,
|
|
467
|
+
negative_answers=ANY,
|
|
468
|
+
timeout=timeout
|
|
469
|
+
))
|
|
470
|
+
|
|
471
|
+
async def clockwise(self, angle: int, timeout: float = 5) -> None:
|
|
472
|
+
"""
|
|
473
|
+
The drone rotates `angle` degrees clockwise.
|
|
474
|
+
|
|
475
|
+
:param angle: angle in degrees in the range [1, 360]
|
|
476
|
+
:param timeout: the timeout in seconds
|
|
477
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
478
|
+
:raises TimeoutError: raised when not answering after `timeout`
|
|
479
|
+
"""
|
|
480
|
+
limit(angle, 1, 360)
|
|
481
|
+
await self.action(RepeatAction(
|
|
482
|
+
command=f"cw {angle}",
|
|
483
|
+
positive_answers=OK,
|
|
484
|
+
negative_answers=ANY,
|
|
485
|
+
timeout=timeout
|
|
486
|
+
))
|
|
487
|
+
|
|
488
|
+
async def counterclockwise(self, angle: int, timeout: float = 5) -> None:
|
|
489
|
+
"""
|
|
490
|
+
The drone rotates `angle` degrees counterclockwise.
|
|
491
|
+
|
|
492
|
+
:param angle: angle in degrees in the range [1, 360]
|
|
493
|
+
:param timeout: the timeout in seconds
|
|
494
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
495
|
+
:raises TimeoutError: raised when not answering after `timeout`
|
|
496
|
+
"""
|
|
497
|
+
limit(angle, 1, 360)
|
|
498
|
+
await self.action(RepeatAction(
|
|
499
|
+
command=f"ccw {angle}",
|
|
500
|
+
positive_answers=OK,
|
|
501
|
+
negative_answers=ANY,
|
|
502
|
+
timeout=timeout
|
|
503
|
+
))
|
|
504
|
+
|
|
505
|
+
async def land(self) -> None:
|
|
506
|
+
"""
|
|
507
|
+
Tries to land the drone.
|
|
508
|
+
|
|
509
|
+
:raises ProtocolError: raised when not receiving `ok` (eg. the drone is not in the air)
|
|
510
|
+
:raises TimeoutError: raised when not answering after 20s
|
|
511
|
+
"""
|
|
512
|
+
await self.action(RepeatAction(
|
|
513
|
+
command="land",
|
|
514
|
+
positive_answers=OK,
|
|
515
|
+
negative_answers=ANY,
|
|
516
|
+
timeout=20
|
|
517
|
+
))
|
|
518
|
+
|
|
519
|
+
async def stop(self) -> None:
|
|
520
|
+
"""
|
|
521
|
+
Immediately stops the drones movement and hovers
|
|
522
|
+
|
|
523
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
524
|
+
:raises TimeoutError: raised when not answering after 5s
|
|
525
|
+
"""
|
|
526
|
+
await self.action(RepeatAction(
|
|
527
|
+
command="stop",
|
|
528
|
+
positive_answers= [r"^forced stop$", r"^ok$"],
|
|
529
|
+
negative_answers=ANY,
|
|
530
|
+
timeout=5
|
|
531
|
+
))
|
|
532
|
+
|
|
533
|
+
async def emergency(self) -> None:
|
|
534
|
+
"""
|
|
535
|
+
Immediately stops all motors and lets the drone fall out of the sky
|
|
536
|
+
|
|
537
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
538
|
+
:raises TimeoutError: raised when not answering after 5s
|
|
539
|
+
"""
|
|
540
|
+
await self.action(RetryAction(
|
|
541
|
+
command="emergency",
|
|
542
|
+
positive_answers=OK,
|
|
543
|
+
negative_answers=ANY,
|
|
544
|
+
retry_count=5,
|
|
545
|
+
timeout=1
|
|
546
|
+
))
|
|
547
|
+
|
|
548
|
+
async def motoron(self) -> None:
|
|
549
|
+
"""
|
|
550
|
+
Enters motoron-mode
|
|
551
|
+
this is a low speed motor rotation mode to reduce the internal temperature in order to avoid overheating
|
|
552
|
+
|
|
553
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
554
|
+
:raises TimeoutError: raised when not answering after 5s
|
|
555
|
+
"""
|
|
556
|
+
await self.action(RetryAction(
|
|
557
|
+
command="motoron",
|
|
558
|
+
positive_answers=OK,
|
|
559
|
+
negative_answers=ANY,
|
|
560
|
+
retry_count=5,
|
|
561
|
+
timeout=1
|
|
562
|
+
))
|
|
563
|
+
|
|
564
|
+
async def motoroff(self) -> None:
|
|
565
|
+
"""
|
|
566
|
+
Exits motoron-mode
|
|
567
|
+
|
|
568
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
569
|
+
:raises TimeoutError: raised when not answering after 5s
|
|
570
|
+
"""
|
|
571
|
+
await self.action(RetryAction(
|
|
572
|
+
command="motoroff",
|
|
573
|
+
positive_answers=OK,
|
|
574
|
+
negative_answers=ANY,
|
|
575
|
+
retry_count=5,
|
|
576
|
+
timeout=1
|
|
577
|
+
))
|
|
578
|
+
|
|
579
|
+
async def flip(self, direction: Literal["l", "r", "f", "b"], timeout: float = 5) -> None:
|
|
580
|
+
"""
|
|
581
|
+
Flips the drone forwards, backwards, left or right
|
|
582
|
+
|
|
583
|
+
:param direction: the first character of the direction to flip
|
|
584
|
+
:param timeout: the timeout in seconds
|
|
585
|
+
:raises ProtocolError: raised when not receiving `ok`
|
|
586
|
+
:raises TimeoutError: raised when not answering after timeout
|
|
587
|
+
"""
|
|
588
|
+
if direction not in ("l", "r", "f", "b"):
|
|
589
|
+
raise ValueError("Direction must be in l,r,f,b")
|
|
590
|
+
|
|
591
|
+
await self.action(RepeatAction(
|
|
592
|
+
command=f"flip {direction}",
|
|
593
|
+
positive_answers=OK,
|
|
594
|
+
negative_answers=ANY,
|
|
595
|
+
timeout=timeout
|
|
596
|
+
))
|
|
597
|
+
|
|
598
|
+
def rc(self, roll: int, pitch: int, throttle: int, yaw: int) -> None:
|
|
599
|
+
"""
|
|
600
|
+
Sends movement like you would do on a rc-controller
|
|
601
|
+
|
|
602
|
+
:param roll: The roll (left-right) in the range [-100,100]
|
|
603
|
+
:param pitch: The pitch (forwards-backwards) in the range [-100,100]
|
|
604
|
+
:param throttle: The throttle (up-down) in the range [-100,100]
|
|
605
|
+
:param yaw: The yaw (rotation) in the range [-100,100]
|
|
606
|
+
"""
|
|
607
|
+
limit(roll, -100, 100)
|
|
608
|
+
limit(pitch, -100, 100)
|
|
609
|
+
limit(throttle, -100, 100)
|
|
610
|
+
limit(yaw, -100, 100)
|
|
611
|
+
|
|
612
|
+
l.protocol.send_command_noanswer(f"rc {roll} {pitch} {throttle} {yaw}", self.drone.ip)
|
|
613
|
+
|
|
614
|
+
class RGBLed(Module):
|
|
615
|
+
async def set(self, color: Tuple[int, int, int]) -> None:
|
|
616
|
+
"""
|
|
617
|
+
Sets the top-led color
|
|
618
|
+
|
|
619
|
+
:param color: The R,G,B values in the range [0,255]
|
|
620
|
+
:raises ProtocolError: raised when not receiving `led ok`
|
|
621
|
+
:raises TimeoutError: raised when not answering after 2.5s
|
|
622
|
+
"""
|
|
623
|
+
red, green, blue = color
|
|
624
|
+
limit(red, 0, 255)
|
|
625
|
+
limit(green, 0, 255)
|
|
626
|
+
limit(blue, 0, 255)
|
|
627
|
+
await self.action(RetryAction(
|
|
628
|
+
command=f"EXT led {red} {green} {blue}",
|
|
629
|
+
positive_answers=[r"^led ok$"],
|
|
630
|
+
negative_answers=ANY,
|
|
631
|
+
timeout=0.5,
|
|
632
|
+
retry_count=5
|
|
633
|
+
))
|
|
634
|
+
|
|
635
|
+
async def pulse(self, color: Tuple[int, int, int], frequency: float) -> None:
|
|
636
|
+
"""
|
|
637
|
+
Pulses the top-led color
|
|
638
|
+
|
|
639
|
+
:param color: The R,G,B values in the range [0,255]
|
|
640
|
+
:param frequency: The frequency to pulse in Hz in range [0.1,2.5]
|
|
641
|
+
:raises ProtocolError: raised when not receiving `led ok`
|
|
642
|
+
:raises TimeoutError: raised when not answering after 2.5s
|
|
643
|
+
"""
|
|
644
|
+
red, green, blue = color
|
|
645
|
+
limit(red, 0, 255)
|
|
646
|
+
limit(green, 0, 255)
|
|
647
|
+
limit(blue, 0, 255)
|
|
648
|
+
limit(frequency, 0.1, 2.5)
|
|
649
|
+
await self.action(RetryAction(
|
|
650
|
+
command=f"EXT led br {frequency} {red} {green} {blue}",
|
|
651
|
+
positive_answers=[r"^led ok$"],
|
|
652
|
+
negative_answers=ANY,
|
|
653
|
+
timeout=0.5,
|
|
654
|
+
retry_count=5
|
|
655
|
+
))
|
|
656
|
+
|
|
657
|
+
async def flash(self, color1: Tuple[int, int, int], color2: Tuple[int, int, int], frequency: float) -> None:
|
|
658
|
+
"""
|
|
659
|
+
Flashes the top-led color between color1 and color2
|
|
660
|
+
|
|
661
|
+
:param color1: The R,G,B values in the range [0,255]
|
|
662
|
+
:param color2: The R,G,B values in the range [0,255]
|
|
663
|
+
:param frequency: The frequency to pulse in Hz in range [0.1,10]
|
|
664
|
+
:raises ProtocolError: raised when not receiving `led ok`
|
|
665
|
+
:raises TimeoutError: raised when not answering after 2.5s
|
|
666
|
+
"""
|
|
667
|
+
red1, green1, blue1 = color1
|
|
668
|
+
red2, green2, blue2 = color2
|
|
669
|
+
limit(red1, 0, 255)
|
|
670
|
+
limit(green1, 0, 255)
|
|
671
|
+
limit(blue1, 0, 255)
|
|
672
|
+
limit(red2, 0, 255)
|
|
673
|
+
limit(green2, 0, 255)
|
|
674
|
+
limit(blue2, 0, 255)
|
|
675
|
+
limit(frequency, 0.1, 10)
|
|
676
|
+
await self.action(RetryAction(
|
|
677
|
+
command=f"EXT led bl {frequency} {red1} {green1} {blue1} {red2} {green2} {blue2}",
|
|
678
|
+
positive_answers=[r"^led ok$"],
|
|
679
|
+
negative_answers=ANY,
|
|
680
|
+
timeout=0.5,
|
|
681
|
+
retry_count=5
|
|
682
|
+
))
|
|
683
|
+
|
|
684
|
+
class Matrix(Module):
|
|
685
|
+
def __init__(self, drone: Drone):
|
|
686
|
+
super().__init__(drone)
|
|
687
|
+
self.pattern = "ppppp000"\
|
|
688
|
+
"00p00000"\
|
|
689
|
+
"00pbbbbb"\
|
|
690
|
+
"00p00b00"\
|
|
691
|
+
"00p00b00"\
|
|
692
|
+
"00000b00"\
|
|
693
|
+
"00000b00"\
|
|
694
|
+
"rrrrpppp"
|
|
695
|
+
|
|
696
|
+
async def set_brightness(self, brightness: int) -> None:
|
|
697
|
+
"""
|
|
698
|
+
Sets the brigthness of the 8x8 led-matrix
|
|
699
|
+
|
|
700
|
+
:param brightness: the brightness in the range [0,255]
|
|
701
|
+
:raises ProtocolError: raised when not receiving `mled ok`
|
|
702
|
+
:raises TimeoutError: raised when not answering after 2.5s
|
|
703
|
+
"""
|
|
704
|
+
limit(brightness, 0, 255)
|
|
705
|
+
await self.action(RetryAction(
|
|
706
|
+
command=f"EXT mled sl {brightness}",
|
|
707
|
+
positive_answers=[r"^matrix ok$"],
|
|
708
|
+
negative_answers=ANY,
|
|
709
|
+
timeout=0.5,
|
|
710
|
+
retry_count=5
|
|
711
|
+
))
|
|
712
|
+
|
|
713
|
+
async def set_pattern(self, pattern: str) -> None:
|
|
714
|
+
"""
|
|
715
|
+
Sets the pattern of the 8x8 led-matrix, starting at the topmost row and going to the right
|
|
716
|
+
|
|
717
|
+
:param pattern: the pattern string, consisting of `r` for red, `b` for blue, `p` for purple, `0` for off, limited to a length of max 64
|
|
718
|
+
:raises ProtocolError: raised when not receiving `mled ok`
|
|
719
|
+
:raises TimeoutError: raised when not answering after 2.5s
|
|
720
|
+
"""
|
|
721
|
+
limit(len(pattern.replace("r","").replace("b","").replace("p","").replace("0","")), 0, 0)
|
|
722
|
+
limit(len(pattern), 1, 64)
|
|
723
|
+
|
|
724
|
+
await self.action(RetryAction(
|
|
725
|
+
command=f"EXT mled g {pattern}",
|
|
726
|
+
positive_answers=[r"^matrix ok$"],
|
|
727
|
+
negative_answers=ANY,
|
|
728
|
+
timeout=0.5,
|
|
729
|
+
retry_count=5
|
|
730
|
+
))
|
|
731
|
+
|
|
732
|
+
self.pattern = pattern
|