codrone-edu 2.9__tar.gz → 2.10__tar.gz

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.
Files changed (36) hide show
  1. {codrone_edu-2.9 → codrone_edu-2.10}/PKG-INFO +1 -1
  2. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/drone.py +271 -14
  3. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/protocol.py +254 -13
  4. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/storage.py +6 -0
  5. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/system.py +93 -9
  6. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/tools/transformer.py +144 -2
  7. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu.egg-info/PKG-INFO +1 -1
  8. {codrone_edu-2.9 → codrone_edu-2.10}/setup.py +1 -1
  9. {codrone_edu-2.9 → codrone_edu-2.10}/MANIFEST.in +0 -0
  10. {codrone_edu-2.9 → codrone_edu-2.10}/README.md +0 -0
  11. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/__init__.py +0 -0
  12. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/__main__.py +0 -0
  13. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/crc.py +0 -0
  14. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/data/black.txt +0 -0
  15. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/data/blue.txt +0 -0
  16. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/data/green.txt +0 -0
  17. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/data/lightblue.txt +0 -0
  18. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/data/purple.txt +0 -0
  19. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/data/red.txt +0 -0
  20. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/data/white.txt +0 -0
  21. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/data/yellow.txt +0 -0
  22. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/errors.py +0 -0
  23. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/receiver.py +0 -0
  24. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/swarm.py +0 -0
  25. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/tools/__init__.py +0 -0
  26. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/tools/interrupter.py +0 -0
  27. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/tools/module_loader.py +0 -0
  28. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/tools/parser.py +0 -0
  29. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/tools/scrubber.py +0 -0
  30. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu/tools/update.py +0 -0
  31. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu.egg-info/SOURCES.txt +0 -0
  32. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu.egg-info/dependency_links.txt +0 -0
  33. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu.egg-info/not-zip-safe +0 -0
  34. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu.egg-info/requires.txt +0 -0
  35. {codrone_edu-2.9 → codrone_edu-2.10}/codrone_edu.egg-info/top_level.txt +0 -0
  36. {codrone_edu-2.9 → codrone_edu-2.10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codrone_edu
3
- Version: 2.9
3
+ Version: 2.10
4
4
  Summary: Python CoDrone EDU library
5
5
  Home-page:
6
6
  Author:
@@ -137,7 +137,7 @@ class Drone:
137
137
  # BaseFunctions Start
138
138
 
139
139
  def __init__(self, flagCheckBackground=True, flagShowErrorMessage=False, flagShowLogMessage=False,
140
- flagShowTransferData=False, flagShowReceiveData=False, swarm=False, color_data_dir=None):
140
+ flagShowTransferData=False, flagShowReceiveData=False, swarm=False, color_data_dir=None, flagShowDepLibraryVersion=True):
141
141
 
142
142
  self._serialport = None
143
143
  if sys.platform == 'emscripten':
@@ -270,10 +270,12 @@ class Drone:
270
270
  library_name = "codrone-edu"
271
271
  library = importlib.metadata.distribution(library_name)
272
272
  if sys.platform != 'emscripten' and not self._swarm:
273
- colorama.init()
274
- print(Fore.GREEN + f"Running {library_name} library version {library.version}" + Style.RESET_ALL)
273
+ if flagShowDepLibraryVersion:
274
+ colorama.init()
275
+ print(Fore.GREEN + f"Running {library_name} library version {library.version}" + Style.RESET_ALL)
275
276
  elif sys.platform == 'emscripten':
276
- print("Running {0} library version {1}".format(library_name, library.version), color="green")
277
+ if flagShowDepLibraryVersion:
278
+ print("Running {0} library version {1}".format(library_name, library.version), color="green")
277
279
  # this only works when using the release version of the library
278
280
 
279
281
  def __del__(self):
@@ -1308,6 +1310,70 @@ class Drone:
1308
1310
  await asyncio.sleep(0.01)
1309
1311
  await self._trigger_speed_callback()
1310
1312
 
1313
+ def control_change(self, mode=ModeControlFlight.Position):
1314
+ """
1315
+ Changes the flight control mode of the drone.
1316
+ default is set to ModeControlFlight.Position
1317
+ :param mode: ModeControlFlight enum value (e.g. Attitude, Position)
1318
+ :return: N/A
1319
+ """
1320
+ if not isinstance(mode, ModeControlFlight):
1321
+ return None
1322
+
1323
+ if sys.platform != 'emscripten' and not self._swarm:
1324
+ return self._control_change_desktop(mode)
1325
+ if sys.platform != 'emscripten' and self._swarm:
1326
+ return asyncio.create_task(self._control_change_swarm(mode))
1327
+ else:
1328
+ return asyncio.create_task(self._control_change_emscripten(mode))
1329
+
1330
+ def _control_change_desktop(self, mode=ModeControlFlight.Position):
1331
+ for _ in range(2):
1332
+ self.sendCommand(CommandType.ModeControlFlight, mode.value)
1333
+ time.sleep(0.03)
1334
+
1335
+ async def _control_change_swarm(self, mode=ModeControlFlight.Position):
1336
+ for _ in range(2):
1337
+ await self.sendCommand(CommandType.ModeControlFlight, mode.value)
1338
+ await asyncio.sleep(0.03)
1339
+
1340
+ async def _control_change_emscripten(self, mode=ModeControlFlight.Position):
1341
+ for _ in range(2):
1342
+ await self.sendCommand(CommandType.ModeControlFlight, mode.value)
1343
+ await asyncio.sleep(0.03)
1344
+
1345
+ def headless_change(self, mode=Headless.Normal):
1346
+ """
1347
+ Changes the headless mode of the drone.
1348
+ default is set to Headless.Normal
1349
+ :param mode: Headless enum value (Headless, Normal)
1350
+ :return: N/A
1351
+ """
1352
+ if not isinstance(mode, Headless):
1353
+ return None
1354
+
1355
+ if sys.platform != 'emscripten' and not self._swarm:
1356
+ return self._headless_change_desktop(mode)
1357
+ if sys.platform != 'emscripten' and self._swarm:
1358
+ return asyncio.create_task(self._headless_change_swarm(mode))
1359
+ else:
1360
+ return asyncio.create_task(self._headless_change_emscripten(mode))
1361
+
1362
+ def _headless_change_desktop(self, mode=Headless.Normal):
1363
+ for _ in range(3):
1364
+ self.sendCommand(CommandType.Headless, mode.value)
1365
+ time.sleep(0.03)
1366
+
1367
+ async def _headless_change_swarm(self, mode=Headless.Normal):
1368
+ for _ in range(3):
1369
+ await self.sendCommand(CommandType.Headless, mode.value)
1370
+ await asyncio.sleep(0.03)
1371
+
1372
+ async def _headless_change_emscripten(self, mode=Headless.Normal):
1373
+ for _ in range(3):
1374
+ await self.sendCommand(CommandType.Headless, mode.value)
1375
+ await asyncio.sleep(0.03)
1376
+
1311
1377
  def update_motion_data(self, drone_type):
1312
1378
 
1313
1379
  """
@@ -2221,7 +2287,11 @@ class Drone:
2221
2287
  # set the speed to medium level
2222
2288
  self.speed_change(speed_level=2)
2223
2289
  time.sleep(0.2)
2224
-
2290
+ self.control_change(mode=ModeControlFlight.Position)
2291
+ time.sleep(0.2)
2292
+ self.headless_change(mode=Headless.Normal)
2293
+ time.sleep(0.2)
2294
+
2225
2295
  # disable the previous YPRT commands
2226
2296
  self.sendControl(0, 0, 0, 0)
2227
2297
  time.sleep(0.2)
@@ -2303,6 +2373,10 @@ class Drone:
2303
2373
  # set the speed to medium level
2304
2374
  await self.speed_change(speed_level=2)
2305
2375
  await asyncio.sleep(0.2)
2376
+ await self.control_change(mode=ModeControlFlight.Position)
2377
+ await asyncio.sleep(0.2)
2378
+ await self.headless_change(mode=Headless.Normal)
2379
+ await asyncio.sleep(0.2)
2306
2380
 
2307
2381
  # disable the previous YPRT commands
2308
2382
  await self.sendControl(0, 0, 0, 0)
@@ -2350,6 +2424,12 @@ class Drone:
2350
2424
  await self.speed_change(speed_level=2)
2351
2425
  await asyncio.sleep(0.2)
2352
2426
 
2427
+ # put all users in the same control/headless modes
2428
+ await self.control_change(mode=ModeControlFlight.Position)
2429
+ await asyncio.sleep(0.2)
2430
+ await self.headless_change(mode=Headless.Normal)
2431
+ await asyncio.sleep(0.2)
2432
+
2353
2433
  # disable the previous YPRT commands
2354
2434
  await self.sendControl(0, 0, 0, 0)
2355
2435
  await asyncio.sleep(0.2)
@@ -2543,7 +2623,7 @@ class Drone:
2543
2623
 
2544
2624
  if monitor0.monitorDataType == MonitorDataType.F32:
2545
2625
 
2546
- dataCount = (dataArray.len() - 1 - Monitor0.getSize()) / 4
2626
+ dataCount = (len(dataArray) - 1 - Monitor0.getSize()) // 4
2547
2627
 
2548
2628
  for i in range(0, dataCount):
2549
2629
 
@@ -2559,7 +2639,7 @@ class Drone:
2559
2639
 
2560
2640
  self.systemTimeMonitorData = monitor4.systemTime
2561
2641
 
2562
- dataCount = (dataArray.len() - 1 - Monitor4.getSize()) / 4
2642
+ dataCount = (len(dataArray) - 1 - Monitor4.getSize()) // 4
2563
2643
 
2564
2644
  for i in range(0, dataCount):
2565
2645
 
@@ -2575,7 +2655,7 @@ class Drone:
2575
2655
 
2576
2656
  self.systemTimeMonitorData = monitor8.systemTime
2577
2657
 
2578
- dataCount = (dataArray.len() - 1 - Monitor8.getSize()) / 4
2658
+ dataCount = (len(dataArray) - 1 - Monitor8.getSize()) // 4
2579
2659
 
2580
2660
  for i in range(0, dataCount):
2581
2661
 
@@ -3408,7 +3488,7 @@ class Drone:
3408
3488
  """
3409
3489
  :param distance: the numerical value of the value to move
3410
3490
  :param units: can either be in inches, centimeters, meters, feet.
3411
- :param speed: default 1 meter per second. Max is 2 meters/second.
3491
+ :param speed: default 0.5 meter per second. Max is 2 meters/second.
3412
3492
  :return: N/A
3413
3493
  """
3414
3494
  if sys.platform != 'emscripten' and not self._swarm:
@@ -3497,7 +3577,7 @@ class Drone:
3497
3577
  """
3498
3578
  :param distance: the numerical value of the value to move
3499
3579
  :param units: can either be in inches, centimeters, meters, feet.
3500
- :param speed: default 1 meter per second. Max is 2 meters/second.
3580
+ :param speed: default 0.5 meter per second. Max is 2 meters/second.
3501
3581
  :return: N/A
3502
3582
  """
3503
3583
  if sys.platform != 'emscripten' and not self._swarm:
@@ -3586,7 +3666,7 @@ class Drone:
3586
3666
  """
3587
3667
  :param distance: the numerical value of the value to move
3588
3668
  :param units: can either be in inches, centimeters, meters, feet.
3589
- :param speed: default 1 meter per second. Max is 2 meters/second.
3669
+ :param speed: default 0.5 meter per second. Max is 2 meters/second.
3590
3670
  :return: N/A
3591
3671
  """
3592
3672
  if sys.platform != 'emscripten' and not self._swarm:
@@ -3674,7 +3754,7 @@ class Drone:
3674
3754
  """
3675
3755
  :param distance: the numerical value of the value to move
3676
3756
  :param units: can either be in inches, centimeters, meters, feet.
3677
- :param speed: default 1 meter per second. Max is 2 meters/second.
3757
+ :param speed: default 0.5 meter per second. Max is 2 meters/second.
3678
3758
  :return: N/A
3679
3759
  """
3680
3760
  if sys.platform != 'emscripten' and not self._swarm:
@@ -3758,6 +3838,183 @@ class Drone:
3758
3838
 
3759
3839
  await asyncio.sleep(delay + 1.0)
3760
3840
 
3841
+
3842
+ def move_upward(self, distance, units="cm", speed=0.5):
3843
+ """
3844
+ :param distance: the numerical value of the value to move
3845
+ :param units: can either be in inches, centimeters, meters, feet.
3846
+ :param speed: default 0.5 meter per second. Max is 2 meters/second.
3847
+ :return: N/A
3848
+ """
3849
+ if sys.platform != 'emscripten' and not self._swarm:
3850
+ return self._move_upward_desktop(distance, units, speed)
3851
+ else:
3852
+ return asyncio.create_task(self._move_upward_emscripten(distance, units, speed))
3853
+
3854
+ def _move_upward_desktop(self, distance, units="cm", speed=0.5):
3855
+ if units == "cm":
3856
+ distance_meters = distance / 100
3857
+ elif units == "ft":
3858
+ distance_meters = distance / 3.28084
3859
+ elif units == "in":
3860
+ distance_meters = distance / 39.37
3861
+ elif units == "m":
3862
+ distance_meters = distance * 1
3863
+ else:
3864
+ print(Fore.RED + "Error: Not a valid unit." + Style.RESET_ALL)
3865
+ return
3866
+
3867
+ # cap the speed
3868
+ if speed > 2:
3869
+ speed = 2
3870
+ elif speed < 0:
3871
+ speed = 0
3872
+
3873
+ self.sendControlPosition(positionX=0,
3874
+ positionY=0,
3875
+ positionZ=distance_meters,
3876
+ velocity=speed,
3877
+ heading=0,
3878
+ rotationalVelocity=0)
3879
+
3880
+ delay = distance_meters / speed
3881
+
3882
+ time.sleep(0.1)
3883
+
3884
+ while self.get_movement_state() != ModeMovement.Moving:
3885
+ self.sendControlPosition(positionX=0, positionY=0, positionZ=distance_meters, velocity=speed,
3886
+ heading=0,
3887
+ rotationalVelocity=0)
3888
+ time.sleep(0.1)
3889
+
3890
+ time.sleep(delay + 1.0)
3891
+
3892
+ async def _move_upward_emscripten(self, distance, units="cm", speed=0.5):
3893
+ if units == "cm":
3894
+ distance_meters = distance / 100
3895
+ elif units == "ft":
3896
+ distance_meters = distance / 3.28084
3897
+ elif units == "in":
3898
+ distance_meters = distance / 39.37
3899
+ elif units == "m":
3900
+ distance_meters = distance * 1
3901
+ else:
3902
+ print("Error: Not a valid unit.", color="error")
3903
+ return
3904
+
3905
+ # cap the speed
3906
+ if speed > 2:
3907
+ speed = 2
3908
+ elif speed < 0:
3909
+ speed = 0
3910
+
3911
+ await self.sendControlPosition(positionX=0,
3912
+ positionY=0,
3913
+ positionZ=distance_meters,
3914
+ velocity=speed,
3915
+ heading=0,
3916
+ rotationalVelocity=0)
3917
+
3918
+ delay = distance_meters / speed
3919
+
3920
+ await asyncio.sleep(0.1)
3921
+
3922
+ while (await self.get_movement_state()) != ModeMovement.Moving:
3923
+ await self.sendControlPosition(positionX=0, positionY=0, positionZ=distance_meters, velocity=speed,
3924
+ heading=0,
3925
+ rotationalVelocity=0)
3926
+ await asyncio.sleep(0.10)
3927
+
3928
+ await asyncio.sleep(delay + 1.0)
3929
+
3930
+ def move_downward(self, distance, units="cm", speed=0.5):
3931
+ """
3932
+ :param distance: the numerical value of the value to move
3933
+ :param units: can either be in inches, centimeters, meters, feet.
3934
+ :param speed: default 0.5 meter per second. Max is 2 meters/second.
3935
+ :return: N/A
3936
+ """
3937
+ if sys.platform != 'emscripten' and not self._swarm:
3938
+ return self._move_downward_desktop(distance, units, speed)
3939
+ else:
3940
+ return asyncio.create_task(self._move_downward_emscripten(distance, units, speed))
3941
+
3942
+ def _move_downward_desktop(self, distance, units="cm", speed=0.5):
3943
+ if units == "cm":
3944
+ distance_meters = distance / 100
3945
+ elif units == "ft":
3946
+ distance_meters = distance / 3.28084
3947
+ elif units == "in":
3948
+ distance_meters = distance / 39.37
3949
+ elif units == "m":
3950
+ distance_meters = distance * 1
3951
+ else:
3952
+ print(Fore.RED + "Error: Not a valid unit." + Style.RESET_ALL)
3953
+ return
3954
+
3955
+ # cap the speed
3956
+ if speed > 2:
3957
+ speed = 2
3958
+ elif speed < 0:
3959
+ speed = 0
3960
+
3961
+ self.sendControlPosition(positionX=0,
3962
+ positionY=0,
3963
+ positionZ=-distance_meters,
3964
+ velocity=speed,
3965
+ heading=0,
3966
+ rotationalVelocity=0)
3967
+
3968
+ delay = distance_meters / speed
3969
+
3970
+ time.sleep(0.1)
3971
+
3972
+ while self.get_movement_state() != ModeMovement.Moving:
3973
+ self.sendControlPosition(positionX=0, positionY=0, positionZ=-distance_meters, velocity=speed,
3974
+ heading=0,
3975
+ rotationalVelocity=0)
3976
+ time.sleep(0.1)
3977
+
3978
+ time.sleep(delay + 1.0)
3979
+
3980
+ async def _move_downward_emscripten(self, distance, units="cm", speed=0.5):
3981
+ if units == "cm":
3982
+ distance_meters = distance / 100
3983
+ elif units == "ft":
3984
+ distance_meters = distance / 3.28084
3985
+ elif units == "in":
3986
+ distance_meters = distance / 39.37
3987
+ elif units == "m":
3988
+ distance_meters = distance * 1
3989
+ else:
3990
+ print("Error: Not a valid unit.", color="error")
3991
+ return
3992
+
3993
+ # cap the speed
3994
+ if speed > 2:
3995
+ speed = 2
3996
+ elif speed < 0:
3997
+ speed = 0
3998
+
3999
+ await self.sendControlPosition(positionX=0,
4000
+ positionY=0,
4001
+ positionZ=-distance_meters,
4002
+ velocity=speed,
4003
+ heading=0,
4004
+ rotationalVelocity=0)
4005
+
4006
+ delay = distance_meters / speed
4007
+
4008
+ await asyncio.sleep(0.1)
4009
+
4010
+ while (await self.get_movement_state()) != ModeMovement.Moving:
4011
+ await self.sendControlPosition(positionX=0, positionY=0, positionZ=-distance_meters, velocity=speed,
4012
+ heading=0,
4013
+ rotationalVelocity=0)
4014
+ await asyncio.sleep(0.10)
4015
+
4016
+ await asyncio.sleep(delay + 1.0)
4017
+
3761
4018
  def send_absolute_position(self, positionX, positionY, positionZ, velocity, heading, rotationalVelocity):
3762
4019
  """
3763
4020
  drone movement command
@@ -4528,7 +4785,7 @@ class Drone:
4528
4785
  """
4529
4786
  A looped method that makes the drone fly forward until it reaches
4530
4787
  a desired distance. The drone will keep that distance.
4531
- The range of front sensor is from 0cm-100cm
4788
+ The range of front sensor is from 0cm-150cm
4532
4789
 
4533
4790
  :param timeout: duration in seconds that function will run
4534
4791
  :param distance: distance in cm the drone will maintain in front of object
@@ -4580,7 +4837,7 @@ class Drone:
4580
4837
  def detect_wall(self, distance=50):
4581
4838
  """
4582
4839
  Returns True when a distance below the threshold is reached.
4583
- The range of front sensor is from 0cm-100cm
4840
+ The range of front sensor is from 0cm-150cm
4584
4841
 
4585
4842
  :param distance: threshold in millimeters that returns True
4586
4843
  :return: Boolean
@@ -53,6 +53,7 @@ class DataType(Enum):
53
53
  Command = 0x11 # 명령
54
54
  Pairing = 0x12 # 페어링
55
55
  Rssi = 0x13 # RSSI
56
+ #ResponseRate = 0x13 # CDEP - response rate (success rate) metric
56
57
  TimeSync = 0x14 # 시간 동기화
57
58
  TransmissionPower = 0x15 # 전송 출력
58
59
  Configuration = 0x16 # 설정
@@ -66,6 +67,12 @@ class DataType(Enum):
66
67
  LightEvent = 0x22 # LED 이벤트
67
68
  LightDefault = 0x23 # LED 초기 모드
68
69
 
70
+ # CDEP - packet-based LED commands (sent as top-level DataType, bypass Command/LightMode/LightEvent)
71
+ LED_rainbow = 0x24 # CDEP LED rainbow effect
72
+ LED_flicker = 0x25 # CDEP LED flicker effect
73
+ LED_random = 0x26 # CDEP LED random color effect
74
+ LED_individual = 0x27 # CDEP per-LED individual color control (5 LEDs)
75
+
69
76
  # 센서 RAW 데이터
70
77
  RawMotion = 0x30 # Motion 센서 데이터 RAW 값
71
78
  RawFlow = 0x31 # Flow 센서 데이터 RAW 값
@@ -91,6 +98,7 @@ class DataType(Enum):
91
98
  MotorSingle = 0x61 # 한 개의 모터 제어
92
99
  Buzzer = 0x62 # 부저 제어
93
100
  Vibrator = 0x63 # 진동 제어
101
+ Battery = 0x64 # CDEP - dedicated battery information
94
102
 
95
103
  # Input
96
104
  Button = 0x70 # 버튼 입력
@@ -165,6 +173,8 @@ class CommandType(Enum):
165
173
  Backlight = 0x09 # 조종기 백라이트 설정
166
174
  ModeController = 0x0A # 조종기 동작 모드(0x10:조종, 0x80:링크)
167
175
  Link = 0x0B # 링크 제어(0:Client Mode, 1:Server Mode, 2:Pairing Start)
176
+ LoadDefaultColor = 0x0C # CDEP - apply default LED color
177
+ Trim = 0x0D # CDEP - absolute trim set (roll/pitch/yaw/throttle)
168
178
 
169
179
  # 관리자
170
180
  ClearCounter = 0xA0 # 카운터 클리어(관리자 권한을 획득했을 경우에만 동작)
@@ -177,11 +187,17 @@ class CommandType(Enum):
177
187
  NavigationStop = 0xE4 # 네비게이션 중단
178
188
  NavigationNext = 0xE5 # 네비게이션 목표점을 다음으로 변경
179
189
  NavigationReturnToHome = 0xE6 # 시작 위치로 귀환
180
-
190
+
181
191
  GpsRtkBase = 0xEA
182
192
  GpsRtkRover = 0xEB
183
193
 
184
- EndOfType = 0xEC
194
+ # CDEP
195
+ ModeTest = 0xF0 # test lock (unavailable until test completed / 27:enable, 11:disable)
196
+ ResetTimeCount = 0xFC # reset test time
197
+ RfChannel = 0xFD # RF channel (2400, 2440, 2483)
198
+ RfModulate = 0xFE # RF modulate (Modulate, Rx, Unmodulate)
199
+
200
+ EndOfType = 0xFF
185
201
 
186
202
 
187
203
  # CommandType End
@@ -704,6 +720,81 @@ class Rssi(ISerializable):
704
720
 
705
721
 
706
722
 
723
+ class PairingCDEP(ISerializable):
724
+ # CDEP - Protocol::Pairing. Same DataType slot as Pairing (0x12), but a completely different
725
+ # 14-byte layout (BLE-style device-type + 5-byte address + 8-channel list) rather than the
726
+ # 11-byte address0/1/2+scramble+channel0-3 shape classic CoDrone EDU sends on this slot.
727
+ # Kept as its own class — the existing Pairing/sendPairing() path must stay intact for
728
+ # non-Plus hardware.
729
+
730
+ def __init__(self):
731
+ self.connectorDeviceType = DeviceType.None_
732
+ self.address = bytearray(5)
733
+ self.channelArray = bytearray(8)
734
+
735
+
736
+ @classmethod
737
+ def getSize(cls):
738
+ return 14
739
+
740
+
741
+ def toArray(self):
742
+ dataArray = bytearray()
743
+ dataArray.extend(pack('<B', self.connectorDeviceType.value))
744
+ dataArray.extend(self.address[0:5])
745
+ dataArray.extend(self.channelArray[0:8])
746
+ return dataArray
747
+
748
+
749
+ @classmethod
750
+ def parse(cls, dataArray):
751
+ data = PairingCDEP()
752
+
753
+ if len(dataArray) != cls.getSize():
754
+ return None
755
+
756
+ indexStart = 0; indexEnd = 1; data.connectorDeviceType, = unpack('<B', dataArray[indexStart:indexEnd])
757
+ indexStart = indexEnd; indexEnd += 5; data.address = dataArray[indexStart:indexEnd]
758
+ indexStart = indexEnd; indexEnd += 8; data.channelArray = dataArray[indexStart:indexEnd]
759
+
760
+ data.connectorDeviceType = DeviceType(data.connectorDeviceType)
761
+
762
+ return data
763
+
764
+
765
+
766
+ class ResponseRate(ISerializable):
767
+ # CDEP - Protocol::ResponseRate. Same DataType slot and byte size as Rssi (0x13, 1 byte),
768
+ # but the Plus docs type the field as an unsigned success-rate metric rather than a signed
769
+ # dBm value. Kept as its own class rather than changing Rssi in place, since Rssi is still
770
+ # the shape classic CoDrone EDU hardware sends on this slot.
771
+
772
+ def __init__(self):
773
+ self.responseRate = 0
774
+
775
+
776
+ @classmethod
777
+ def getSize(cls):
778
+ return 1
779
+
780
+
781
+ def toArray(self):
782
+ return pack('<B', self.responseRate)
783
+
784
+
785
+ @classmethod
786
+ def parse(cls, dataArray):
787
+ data = ResponseRate()
788
+
789
+ if len(dataArray) != cls.getSize():
790
+ return None
791
+
792
+ data.responseRate, = unpack('<B', dataArray)
793
+
794
+ return data
795
+
796
+
797
+
707
798
  class Command(ISerializable):
708
799
 
709
800
  def __init__(self):
@@ -858,17 +949,17 @@ class MonitorHeaderType(Enum):
858
949
 
859
950
 
860
951
  class MonitorDataType(Enum):
861
-
862
- U8 = 0x00,
863
- S8 = 0x01,
864
- U16 = 0x02,
865
- S16 = 0x03,
866
- U32 = 0x04,
867
- S32 = 0x05,
868
- U64 = 0x06,
869
- S64 = 0x07,
870
- F32 = 0x08,
871
- F64 = 0x09,
952
+
953
+ U8 = 0x00
954
+ S8 = 0x01
955
+ U16 = 0x02
956
+ S16 = 0x03
957
+ U32 = 0x04
958
+ S32 = 0x05
959
+ U64 = 0x06
960
+ S64 = 0x07
961
+ F32 = 0x08
962
+ F64 = 0x09
872
963
 
873
964
  EndOfType = 0x0A
874
965
 
@@ -1646,6 +1737,156 @@ class LightEventColors(ISerializable):
1646
1737
  return data
1647
1738
 
1648
1739
 
1740
+
1741
+ # CDEP LED packet commands Start
1742
+ # Sent as top-level DataType (0x24-0x27), independent of Command/LightMode/LightEvent.
1743
+
1744
+
1745
+ class LightRainbow(ISerializable):
1746
+
1747
+ def __init__(self):
1748
+ self.color = Color()
1749
+ self.interval = 0
1750
+
1751
+
1752
+ @classmethod
1753
+ def getSize(cls):
1754
+ return Color.getSize() + 2
1755
+
1756
+
1757
+ def toArray(self):
1758
+ dataArray = bytearray()
1759
+ dataArray.extend(self.color.toArray())
1760
+ dataArray.extend(pack('<H', self.interval))
1761
+ return dataArray
1762
+
1763
+
1764
+ @classmethod
1765
+ def parse(cls, dataArray):
1766
+ data = LightRainbow()
1767
+
1768
+ if len(dataArray) != cls.getSize():
1769
+ return None
1770
+
1771
+ indexStart = 0; indexEnd = Color.getSize(); data.color = Color.parse(dataArray[indexStart:indexEnd])
1772
+ indexStart = indexEnd; indexEnd += 2; data.interval, = unpack('<H', dataArray[indexStart:indexEnd])
1773
+
1774
+ return data
1775
+
1776
+
1777
+
1778
+ class LightFlicker(ISerializable):
1779
+
1780
+ def __init__(self):
1781
+ self.color = Color()
1782
+ self.interval = 0
1783
+
1784
+
1785
+ @classmethod
1786
+ def getSize(cls):
1787
+ return Color.getSize() + 2
1788
+
1789
+
1790
+ def toArray(self):
1791
+ dataArray = bytearray()
1792
+ dataArray.extend(self.color.toArray())
1793
+ dataArray.extend(pack('<H', self.interval))
1794
+ return dataArray
1795
+
1796
+
1797
+ @classmethod
1798
+ def parse(cls, dataArray):
1799
+ data = LightFlicker()
1800
+
1801
+ if len(dataArray) != cls.getSize():
1802
+ return None
1803
+
1804
+ indexStart = 0; indexEnd = Color.getSize(); data.color = Color.parse(dataArray[indexStart:indexEnd])
1805
+ indexStart = indexEnd; indexEnd += 2; data.interval, = unpack('<H', dataArray[indexStart:indexEnd])
1806
+
1807
+ return data
1808
+
1809
+
1810
+
1811
+ class LightRandom(ISerializable):
1812
+ # Doc's template shares the same 5-byte shape as LightRainbow/LightFlicker; the drone may
1813
+ # ignore the color/interval fields for this mode, but that is not confirmed by the docs.
1814
+
1815
+ def __init__(self):
1816
+ self.color = Color()
1817
+ self.interval = 0
1818
+
1819
+
1820
+ @classmethod
1821
+ def getSize(cls):
1822
+ return Color.getSize() + 2
1823
+
1824
+
1825
+ def toArray(self):
1826
+ dataArray = bytearray()
1827
+ dataArray.extend(self.color.toArray())
1828
+ dataArray.extend(pack('<H', self.interval))
1829
+ return dataArray
1830
+
1831
+
1832
+ @classmethod
1833
+ def parse(cls, dataArray):
1834
+ data = LightRandom()
1835
+
1836
+ if len(dataArray) != cls.getSize():
1837
+ return None
1838
+
1839
+ indexStart = 0; indexEnd = Color.getSize(); data.color = Color.parse(dataArray[indexStart:indexEnd])
1840
+ indexStart = indexEnd; indexEnd += 2; data.interval, = unpack('<H', dataArray[indexStart:indexEnd])
1841
+
1842
+ return data
1843
+
1844
+
1845
+
1846
+ class LightIndividual(ISerializable):
1847
+ # 5 independently addressable LEDs + a shared interval. Doc example: 17-byte payload
1848
+ # (5 * 3-byte Color + 2-byte interval), matching the LED_individual header length of 0x11.
1849
+
1850
+ def __init__(self):
1851
+ self.colors = [Color(), Color(), Color(), Color(), Color()]
1852
+ self.interval = 0
1853
+
1854
+
1855
+ @classmethod
1856
+ def getSize(cls):
1857
+ return (Color.getSize() * 5) + 2
1858
+
1859
+
1860
+ def toArray(self):
1861
+ dataArray = bytearray()
1862
+ for color in self.colors:
1863
+ dataArray.extend(color.toArray())
1864
+ dataArray.extend(pack('<H', self.interval))
1865
+ return dataArray
1866
+
1867
+
1868
+ @classmethod
1869
+ def parse(cls, dataArray):
1870
+ data = LightIndividual()
1871
+
1872
+ if len(dataArray) != cls.getSize():
1873
+ return None
1874
+
1875
+ indexStart = 0
1876
+ for i in range(5):
1877
+ indexEnd = indexStart + Color.getSize()
1878
+ data.colors[i] = Color.parse(dataArray[indexStart:indexEnd])
1879
+ indexStart = indexEnd
1880
+
1881
+ data.interval, = unpack('<H', dataArray[indexStart:indexStart + 2])
1882
+
1883
+ return data
1884
+
1885
+
1886
+ # CDEP LED packet commands End
1887
+
1888
+
1889
+
1649
1890
  # Light End
1650
1891
 
1651
1892
 
@@ -87,4 +87,10 @@ class Parser:
87
87
  self.d[DataType.InformationAssembledForController] = InformationAssembledForController.parse
88
88
  self.d[DataType.InformationAssembledForEntry] = InformationAssembledForEntry.parse
89
89
 
90
+ # CDEP LED packet commands
91
+ self.d[DataType.LED_rainbow] = None
92
+ self.d[DataType.LED_flicker] = None
93
+ self.d[DataType.LED_random] = None
94
+ self.d[DataType.LED_individual] = None
95
+
90
96
 
@@ -79,7 +79,8 @@ class ModelNumber(Enum):
79
79
  Drone_12_Drone_P1 = 0x000C1002 # Drone_12_Drone_P1 (CDE JROTC ed.)
80
80
  Drone_8_Drone_P3 = 0x00081003 # Drone_8_Drone_P3 CoDrone EDU (IMU)
81
81
 
82
- Drone_12_Controller_P10 = 0x000C200A # CDEP Controller
82
+ Drone_12_Controller_P10 = 0x000C200A # CDEP Controller (v1.0)
83
+ Drone_12_Controller_P11 = 0x000C2010 # CDEP Controller (v1.1)
83
84
  Drone_12_Drone_P10 = 0x000C100A # CDEP Drone
84
85
 
85
86
 
@@ -130,7 +131,7 @@ class ModeSystem(Enum):
130
131
 
131
132
 
132
133
  class ModeControlFlight(Enum):
133
-
134
+
134
135
  None_ = 0x00
135
136
 
136
137
  Attitude = 0x10 # 자세 - X,Y는 각도(deg)로 입력받음, Z,Yaw는 속도(m/s)로 입력 받음
@@ -138,8 +139,67 @@ class ModeControlFlight(Enum):
138
139
  Manual = 0x12 # 고도를 수동으로 조종함
139
140
  Rate = 0x13 # Rate - X,Y는 각속도(deg/s)로 입력받음, Z,Yaw는 속도(m/s)로 입력 받음
140
141
  Function = 0x14 # 기능
141
-
142
- EndOfType = 0x15
142
+
143
+ BirdWing = 0x15 # CDEP - bird wing style aircraft
144
+ FixedWing = 0x16 # CDEP - fixed wing aircraft
145
+
146
+ EndOfType = 0x17
147
+
148
+
149
+
150
+ class ModeDrone(Enum):
151
+ # CDEP - drone-side operating mode (Flight / Card-coding / Motion-coding / Piano / Link / Calibration)
152
+
153
+ None_ = 0x00
154
+
155
+ Flight = 0x10
156
+ Card = 0x11
157
+ Motion = 0x12
158
+ Piano = 0x13
159
+
160
+ Link = 0x80
161
+ Calibration = 0x81
162
+
163
+ Error = 0xA0
164
+
165
+ EndOfType = 0xA1
166
+
167
+
168
+
169
+ class ModeController(Enum):
170
+ # CDEP - controller-side operating mode
171
+
172
+ None_ = 0x00
173
+
174
+ Control = 0x10
175
+ Setup = 0x11
176
+
177
+ Link = 0x80
178
+
179
+ Error = 0xA0
180
+
181
+ EndOfType = 0xA1
182
+
183
+
184
+
185
+ class ModeConnection(Enum):
186
+ # CDEP - RF link connection state
187
+
188
+ None_ = 0x00
189
+
190
+ PairingStart = 0x01
191
+ PairingExchange = 0x02
192
+ PairingComplete = 0x03
193
+
194
+ Ready = 0x04
195
+
196
+ ConnectingStart = 0x05
197
+ Connecting = 0x06
198
+ Connected = 0x07
199
+ LostConnection = 0x08
200
+ Disconnected = 0x09
201
+
202
+ EndOfPairing = 0x0A
143
203
 
144
204
 
145
205
 
@@ -199,26 +259,34 @@ class ErrorFlagsForSensor(Enum):
199
259
 
200
260
  RangeGround_NoAnswer = 0x00000100 # No response from Ground distance sensor
201
261
  RangeGround_WrongValue = 0x00000200 # Incorrect value from Ground distance sensor
202
-
262
+
263
+ RangeFront_NoAnswer = 0x00000400 # CDEP - No response from front distance sensor
264
+ RangeFront_WrongValue = 0x00000800 # CDEP - Incorrect value from front distance sensor
265
+
203
266
  Flow_NoAnswer = 0x00001000 # No response from Flow sensor
204
267
  Flow_WrongValue = 0x00002000 # Incorrect value from Flow sensor
205
268
  Flow_CannotRecognizeGroundImage = 0x00004000 # Cannot recognize ground image
206
269
 
270
+ RF_NoAnswer = 0x10000000 # CDEP - No response from RF module
271
+ RF_Paired = 0x20000000 # CDEP - RF paired
272
+ RF_Connected = 0x40000000 # CDEP - RF connected
273
+
207
274
 
208
275
 
209
276
  class ErrorFlagsForState(Enum):
210
277
 
211
278
  None_ = 0x00000000
212
-
279
+
213
280
  NotRegistered = 0x00000001 # Device not registered
214
281
  FlashReadLock_UnLocked = 0x00000002 # Flash memory read lock not engaged
215
282
  BootloaderWriteLock_UnLocked = 0x00000004 # Bootloader write lock not engaged
216
283
  LowBattery = 0x00000008 # Low battery
217
-
284
+
218
285
  TakeoffFailure_CheckPropellerAndMotor = 0x00000010 # Takeoff failure
219
286
  CheckPropellerVibration = 0x00000020 # Propeller vibration detected
220
287
  Attitude_NotStable = 0x00000040 # Attitude is too tilted or inverted
221
-
288
+ Motors_NotGood = 0x00000080 # CDEP - one or more motors not functioning correctly
289
+
222
290
  CanNotFlip_LowBattery = 0x00000100 # Battery below 50%
223
291
  CanNotFlip_TooHeavy = 0x00000200 # Device is too heavy
224
292
 
@@ -269,7 +337,7 @@ class Direction(Enum):
269
337
 
270
338
 
271
339
  class Rotation(Enum):
272
-
340
+
273
341
  None_ = 0x00
274
342
 
275
343
  Clockwise = 0x01
@@ -279,6 +347,22 @@ class Rotation(Enum):
279
347
 
280
348
 
281
349
 
350
+ class MotorPart(Enum):
351
+ # CDEP - Motor::Part::Type. Note: BYROBOT's own docs are internally inconsistent about whether
352
+ # index 0 is the front-left or front-right motor (prose says front-left, enum lists MFR first).
353
+ # This transcribes the literal enum order; verify against real hardware before relying on it.
354
+
355
+ MFR = 0 # Front Right
356
+ MRR = 1 # Rear Right
357
+ MRL = 2 # Rear Left
358
+ MFL = 3 # Front Left
359
+
360
+ EndOfPart = 4
361
+
362
+ All = 5
363
+
364
+
365
+
282
366
  class SensorOrientation(Enum):
283
367
 
284
368
  None_ = 0x00
@@ -29,7 +29,7 @@ awaitable_drone_methods = frozenset({
29
29
  "takeoff", "land", "emergency_stop", "hover", "reset_move",
30
30
  # Movement - basic - 14
31
31
  "go", "move", "turn", "flip", "set_waypoint", "sendLanding", "sendControlWhile",
32
- "move_distance", "move_forward", "move_backward", "move_left", "move_right",
32
+ "move_distance", "move_forward", "move_backward", "move_left", "move_right", "move_upward", "move_downward",
33
33
  "send_absolute_position", "goto_waypoint",
34
34
  # Movement - turns - 4
35
35
  "turn_degree", "turn_direction", "turn_left", "turn_right",
@@ -49,7 +49,7 @@ awaitable_drone_methods = frozenset({
49
49
  "get_color_data", "get_position_data", "get_pos_x", "get_pos_y", "get_pos_z",
50
50
  "get_height", "get_flow_data", "get_flow_velocity_x", "get_flow_velocity_y",
51
51
  "get_state_data", "get_system_state", "get_flight_state", "get_movement_state",
52
- "get_control_speed", "speed_change", "get_motion_data", "get_raw_motion_data",
52
+ "get_control_speed", "speed_change", "control_change", "headless_change", "get_motion_data", "get_raw_motion_data",
53
53
  "get_accel_x", "get_accel_y", "get_accel_z",
54
54
  "get_x_gyro", "get_y_gyro", "get_z_gyro",
55
55
  "get_angle_x", "get_angle_y", "get_angle_z",
@@ -110,6 +110,87 @@ class CodeAnalyzer(ast.NodeVisitor):
110
110
  self.function_aliases[local_name] = alias.name
111
111
  self.generic_visit(node)
112
112
 
113
+
114
+ class DroneUsageAnalyzer(ast.NodeVisitor):
115
+ """
116
+ Standalone AST visitor backing check_drone_usage(). Kept separate from
117
+ CodeAnalyzer (rather than added to it) so transform_code() and
118
+ transform_imported_module() - which run CodeAnalyzer on every execution -
119
+ don't pay for traversal they don't need.
120
+ """
121
+ def __init__(self):
122
+ self.uses_drone = False
123
+ self.instantiates_drone = False
124
+ self.has_pair_or_connect = False
125
+ self.has_close_or_disconnect = False
126
+ self.has_drone_import = False
127
+
128
+ def visit_Assign(self, node):
129
+ if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name):
130
+ if node.value.func.id == 'Drone':
131
+ if node.targets[0].id != 'drone':
132
+ raise ValueError(f"Drone instance must be named 'drone', found '{node.targets[0].id}' instead.")
133
+ self.instantiates_drone = True
134
+ self.generic_visit(node)
135
+
136
+ def visit_ImportFrom(self, node):
137
+ if node.module == 'codrone_edu.drone':
138
+ self.has_drone_import = True
139
+ self.generic_visit(node)
140
+
141
+ def visit_Import(self, node):
142
+ for alias in node.names:
143
+ if alias.name == 'codrone_edu.drone':
144
+ self.has_drone_import = True
145
+ self.generic_visit(node)
146
+
147
+ def visit_Attribute(self, node):
148
+ if isinstance(node.value, ast.Name) and node.value.id == 'drone':
149
+ self.uses_drone = True
150
+ self.generic_visit(node)
151
+
152
+ def visit_Call(self, node):
153
+ if isinstance(node.func, ast.Name) and node.func.id == 'Drone':
154
+ self.instantiates_drone = True
155
+ elif isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.value.id == 'drone':
156
+ if node.func.attr in ('pair', 'connect'):
157
+ self.has_pair_or_connect = True
158
+ elif node.func.attr in ('close', 'disconnect'):
159
+ self.has_close_or_disconnect = True
160
+ self.generic_visit(node)
161
+
162
+ def get_warnings(self):
163
+ """
164
+ Returns a list of warning strings for the most common CoDrone EDU
165
+ setup mistakes: using 'drone.' without creating a Drone() instance,
166
+ without pairing/connecting, without closing/disconnecting, or using
167
+ Drone() without importing it. Returns an empty list when no issues
168
+ are found.
169
+ """
170
+ warnings = []
171
+
172
+ if self.uses_drone and not self.instantiates_drone:
173
+ warnings.append(
174
+ "Your code uses 'drone.' but never creates a Drone() instance. This will not run outside of Python for Robolink. " \
175
+ "Add 'drone = Drone()' before using 'drone'."
176
+ )
177
+ if self.uses_drone and not self.has_pair_or_connect:
178
+ warnings.append(
179
+ "Your code uses 'drone.' but never calls drone.pair() or drone.connect(). This will not run outside of Python for Robolink."
180
+ )
181
+ if self.uses_drone and not self.has_close_or_disconnect:
182
+ warnings.append(
183
+ "Your code uses 'drone.' but never calls drone.close() or drone.disconnect(). This will not run outside of Python for Robolink. " \
184
+ "Add drone.close() or drone.disconnect() at the end of your program to release the connection."
185
+ )
186
+ if self.instantiates_drone and not self.has_drone_import:
187
+ warnings.append(
188
+ "Your code uses 'Drone()' but never imports it. Add 'from codrone_edu.drone import *' at the top of your program."
189
+ )
190
+
191
+ return warnings
192
+
193
+
113
194
  class AsyncTransformer(ast.NodeTransformer):
114
195
  def __init__(self, custom_functions, drone_instance_name, function_aliases=None):
115
196
  self.custom_functions = custom_functions
@@ -590,6 +671,9 @@ class SleepTransformer(ast.NodeTransformer):
590
671
  class CheckInterruptAndGlobalTransformer(ast.NodeTransformer):
591
672
  def __init__(self, convert_global_to_nonlocal=True):
592
673
  self.convert_global_to_nonlocal = convert_global_to_nonlocal
674
+ self._func_depth = 0
675
+ # Only `while True:` loops get the injected sleep
676
+ self.injected_sleep = False
593
677
 
594
678
  def copy_location(self, new_node, old_node):
595
679
  ast.copy_location(new_node, old_node)
@@ -609,6 +693,33 @@ class CheckInterruptAndGlobalTransformer(ast.NodeTransformer):
609
693
  return [node, check_interrupt_call]
610
694
  return super().generic_visit(node)
611
695
 
696
+ def visit_FunctionDef(self, node):
697
+ self._func_depth += 1
698
+ try:
699
+ return self.generic_visit(node)
700
+ finally:
701
+ self._func_depth -= 1
702
+
703
+ def visit_AsyncFunctionDef(self, node):
704
+ self._func_depth += 1
705
+ try:
706
+ return self.generic_visit(node)
707
+ finally:
708
+ self._func_depth -= 1
709
+
710
+ def visit_While(self, node):
711
+ is_while_true = isinstance(node.test, ast.Constant) and node.test.value is True
712
+ node = self.generic_visit(node)
713
+ if self._func_depth > 0 and is_while_true:
714
+ sleep_call = ast.Expr(value=ast.Await(value=ast.Call(
715
+ func=ast.Attribute(value=ast.Name(id='asyncio', ctx=ast.Load()), attr='sleep', ctx=ast.Load()),
716
+ args=[ast.Constant(value=0.0000000001)], keywords=[]
717
+ )))
718
+ sleep_call = self.copy_location(sleep_call, node)
719
+ node.body.append(sleep_call)
720
+ self.injected_sleep = True
721
+ return node
722
+
612
723
  def visit_Global(self, node):
613
724
  # Only convert global to nonlocal if we're in a wrapper function
614
725
  if self.convert_global_to_nonlocal:
@@ -616,6 +727,31 @@ class CheckInterruptAndGlobalTransformer(ast.NodeTransformer):
616
727
  return ast.copy_location(nonlocal_node, node)
617
728
  return node
618
729
 
730
+ def check_drone_usage(code):
731
+ """
732
+ Static, AST-based check for the most common CoDrone EDU setup mistakes:
733
+ using 'drone.' without creating a Drone() instance, without pairing or
734
+ connecting, without closing or disconnecting, or using Drone() without
735
+ importing it. Returns a list of warning strings (empty if no issues are
736
+ found). Like transform_code(), raises ValueError if a Drone() instance is
737
+ assigned to a variable not named 'drone'.
738
+ """
739
+ tree = ast.parse(code)
740
+ analyzer = DroneUsageAnalyzer()
741
+ analyzer.visit(tree)
742
+ return analyzer.get_warnings()
743
+
744
+
745
+ def ensure_asyncio_import(tree):
746
+ """Insert `import asyncio` at module top level if not already present."""
747
+ has_asyncio = any(
748
+ (isinstance(n, ast.Import) and any(alias.name == 'asyncio' for alias in n.names))
749
+ or (isinstance(n, ast.ImportFrom) and n.module == 'asyncio')
750
+ for n in tree.body
751
+ )
752
+ if not has_asyncio:
753
+ tree.body.insert(0, ast.Import(names=[ast.alias(name='asyncio', asname=None)]))
754
+
619
755
  def transform_code(code):
620
756
  # Parse the code into an AST
621
757
  tree = ast.parse(code)
@@ -645,6 +781,9 @@ def transform_code(code):
645
781
  combined_transformer.visit(node)
646
782
  break
647
783
 
784
+ if combined_transformer.injected_sleep:
785
+ ensure_asyncio_import(tree)
786
+
648
787
  # Ensure the tree is correctly fixed
649
788
  ast.fix_missing_locations(tree)
650
789
 
@@ -682,6 +821,9 @@ def transform_imported_module(code):
682
821
  tree = sleep_transformer.visit(tree)
683
822
  tree = combined_transformer.visit(tree)
684
823
 
824
+ if combined_transformer.injected_sleep:
825
+ ensure_asyncio_import(tree)
826
+
685
827
  # Inject checkInterrupt import at the beginning of the module
686
828
  # This is necessary because CheckInterruptTransformer adds checkInterrupt() calls
687
829
  check_interrupt_import = ast.ImportFrom(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codrone_edu
3
- Version: 2.9
3
+ Version: 2.10
4
4
  Summary: Python CoDrone EDU library
5
5
  Home-page:
6
6
  Author:
@@ -22,7 +22,7 @@ Python package for controlling CoDrone EDU.
22
22
 
23
23
  setup(
24
24
  name='codrone_edu',
25
- version='2.9',
25
+ version='2.10',
26
26
  description='Python CoDrone EDU library',
27
27
  url='',
28
28
  author='',
File without changes
File without changes
File without changes
File without changes