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