pyaidrone 2.2__tar.gz → 2.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyaidrone
3
- Version: 2.2
3
+ Version: 2.4
4
4
  Summary: Library for AIDrone Products
5
5
  Home-page: http://www.ir-brain.com
6
6
  Author: IR-Brain
@@ -0,0 +1,236 @@
1
+ import serial
2
+ from time import sleep
3
+ from threading import Thread
4
+ from serial.tools.list_ports import comports
5
+ from pyaidrone.parse import *
6
+ from pyaidrone.packet import *
7
+ from pyaidrone.deflib import *
8
+
9
+
10
+ class AIDrone(Parse, Packet):
11
+ def __init__(self, receiveCallback = None):
12
+ self.serial = None
13
+ self.isThreadRun = False
14
+ self.parse = Parse(AIDRONE)
15
+ self.makepkt = Packet(AIDRONE)
16
+ self.receiveCallback = receiveCallback
17
+ self.makepkt.clearPacket()
18
+ self.posX = 0
19
+ self.posY = 0
20
+ self.rot = 0
21
+ # --- 영상 스트리밍 관련 기본값 ---
22
+ self.stream_host = "192.168.4.1"
23
+ self.stream_port = 80
24
+ self.stream_path = "/?action=stream"
25
+ self._cap = None
26
+
27
+ def receiveHandler(self):
28
+ while self.isThreadRun:
29
+ try:
30
+ readData = self.serial.read(self.serial.in_waiting or 1)
31
+ # [수정] 문자열 "None" 비교 → None 객체 비교
32
+ packet = self.parse.packetArrange(readData)
33
+ if packet is not None:
34
+ if self.receiveCallback is not None:
35
+ self.receiveCallback(packet)
36
+ self.serial.write(self.makepkt.getPacket())
37
+ except serial.SerialException as e:
38
+ # [수정] 시리얼 오류는 치명적 → 스레드 정상 종료
39
+ print(f"[receiveHandler] 시리얼 오류: {e}")
40
+ self.isThreadRun = False
41
+ break
42
+ except Exception as e:
43
+ # [수정] 일시적 오류는 스레드 유지 (break 제거)
44
+ print(f"[receiveHandler] 일시 오류 (계속 실행): {e}")
45
+
46
+
47
+ def Open(self, portName = "None"):
48
+ # [수정] 문자열 "None" 비교 → None 객체 비교
49
+ if portName == "None":
50
+ nodes = comports()
51
+ for node in nodes:
52
+ if "CH340" in node.description:
53
+ portName = node.device
54
+
55
+ if portName == "None":
56
+ print("Can't find Serial Port")
57
+ # [수정] exit() → raise로 변경 (Jupyter/GUI 환경 보호)
58
+ raise ConnectionError("CH340 시리얼 포트를 찾을 수 없습니다.")
59
+ try:
60
+ self.serial = serial.Serial(port=portName, baudrate=19200, timeout=1)
61
+ if self.serial.isOpen():
62
+ self.isThreadRun = True
63
+ self.thread = Thread(target=self.receiveHandler, args=(), daemon=True)
64
+ self.thread.start()
65
+ print("Connected to", portName)
66
+ return True
67
+ else:
68
+ print("Can't open " + portName)
69
+ raise ConnectionError(f"포트를 열 수 없습니다: {portName}")
70
+ except serial.SerialException as e:
71
+ # [수정] 구체적 예외 처리
72
+ print(f"시리얼 포트 오류: {e}")
73
+ raise
74
+ except ConnectionError:
75
+ raise
76
+ except Exception as e:
77
+ print(f"Can't open {portName}: {e}")
78
+ raise ConnectionError(f"포트 연결 실패: {portName}") from e
79
+
80
+
81
+ def Close(self):
82
+ self.isThreadRun = False
83
+ sleep(0.2)
84
+ try:
85
+ # [수정] serial.write 예외처리 추가 (포트 닫힌 상태 보호)
86
+ pkt = self.makepkt.getPacket()
87
+ if (pkt[15] & 0x80) == 0x80:
88
+ self.makepkt.clearPacket()
89
+ self.setOption(0x8000)
90
+ self.serial.write(self.makepkt.getPacket())
91
+ sleep(0.2)
92
+ self.serial.write(self.makepkt.clearPacket())
93
+ sleep(0.2)
94
+ except Exception:
95
+ pass
96
+ finally:
97
+ if self.serial is not None and self.serial.isOpen():
98
+ self.serial.close()
99
+
100
+ def setOption(self, option):
101
+ data = option.to_bytes(2, byteorder="little", signed=False)
102
+ self.makepkt.makePacket(14, data)
103
+
104
+
105
+ def takeoff(self):
106
+ alt = 70
107
+ data = alt.to_bytes(2, byteorder="little", signed=False)
108
+ self.makepkt.makePacket(12, data)
109
+ alt = 0x2F
110
+ data = alt.to_bytes(2, byteorder="little", signed=False)
111
+ self.setOption(0x2F)
112
+
113
+
114
+ def landing(self):
115
+ alt = 0
116
+ data = alt.to_bytes(2, byteorder="little", signed=False)
117
+ self.makepkt.makePacket(12, data)
118
+
119
+
120
+ def altitude(self, alt):
121
+ data = alt.to_bytes(2, byteorder="little", signed=False)
122
+ self.makepkt.makePacket(12, data)
123
+
124
+
125
+ def velocity(self, dir=0, vel=100):
126
+ if dir > 3:
127
+ return
128
+ if dir==1 or dir==3:
129
+ vel *= -1;
130
+ data = vel.to_bytes(2, byteorder="little", signed=True)
131
+ if dir==0 or dir==1:
132
+ self.makepkt.makePacket(8, data)
133
+ else:
134
+ self.makepkt.makePacket(6, data)
135
+ self.setOption(0x0F)
136
+
137
+
138
+ def move(self, dir=0, dist=100):
139
+ if dir > 3:
140
+ return
141
+ if dir==1 or dir==3:
142
+ dist *= -1;
143
+ if dir==0 or dir==1:
144
+ self.posX += dist
145
+ data = self.posX.to_bytes(2, byteorder="little", signed=True)
146
+ self.makepkt.makePacket(8, data)
147
+ else:
148
+ self.posY += dist
149
+ data = self.posY.to_bytes(2, byteorder="little", signed=True)
150
+ self.makepkt.makePacket(6, data)
151
+ self.setOption(0x2F)
152
+
153
+
154
+ def rotation(self, rot=90):
155
+ self.rot += rot
156
+ data = self.rot.to_bytes(2, byteorder="little", signed=True)
157
+ self.makepkt.makePacket(10, data)
158
+
159
+
160
+
161
+ def motor(self, what, speed):
162
+ speed = DefLib.constrain(speed, 100, 0)
163
+ data = speed.to_bytes(2, byteorder="little", signed=True)
164
+ self.makepkt.makePacket(what*2+6, data)
165
+ self.setOption(0x8000)
166
+
167
+
168
+ def emergency(self):
169
+ self.setOption(0x00)
170
+ self.serial.write(self.makepkt.getPacket())
171
+
172
+ # aiDrone.py — AIDrone 클래스 안에 아래 3개 메서드 추가
173
+ def setStreamAddress(self, host: str, port: int = 80, path: str = "/?action=stream"):
174
+ """MJPG-Streamer 주소 구성 요소를 저장합니다."""
175
+ if not isinstance(host, str) or len(host.strip()) == 0:
176
+ raise ValueError("host가 올바르지 않습니다.")
177
+ self.stream_host = host.strip()
178
+ self.stream_port = int(port)
179
+ if not path.startswith("/"):
180
+ path = "/" + path
181
+ self.stream_path = path
182
+ return self._build_stream_url()
183
+
184
+ def _build_stream_url(self):
185
+ """내부 사용: 저장된 host/port/path로 URL 생성."""
186
+ if self.stream_port in (80, None):
187
+ return f"http://{self.stream_host}{self.stream_path}"
188
+ return f"http://{self.stream_host}:{self.stream_port}{self.stream_path}"
189
+
190
+ def streamon(self, host: str = None, port: int = None, path: str = None, return_url: bool = False):
191
+ """
192
+ MJPG-Streamer 스트림을 엽니다.
193
+ - 인자를 주면 해당 값으로 주소를 덮어쓰고, 안 주면 저장된 값 사용.
194
+ - return_url=True 이면 URL 문자열만 반환(캡처는 열지 않음).
195
+ - 기본 URL 예: http://192.168.4.1/?action=stream
196
+ """
197
+ if host is not None or port is not None or path is not None:
198
+ self.setStreamAddress(host or self.stream_host,
199
+ self.stream_port if port is None else port,
200
+ self.stream_path if path is None else path)
201
+
202
+ url = self._build_stream_url()
203
+ if return_url:
204
+ return url
205
+
206
+ # OpenCV는 여기서만 임포트(필요할 때만)
207
+ try:
208
+ import cv2 as cv
209
+ except Exception as e:
210
+ raise RuntimeError(f"OpenCV(cv2) 임포트 실패: {e}")
211
+
212
+ # 이전 cap이 열려 있으면 정리
213
+ self.streamoff()
214
+
215
+ self._cap = cv.VideoCapture(url)
216
+ if not self._cap.isOpened():
217
+ self._cap.release()
218
+ self._cap = None
219
+ raise RuntimeError(f"스트림 열기 실패: {url}")
220
+ return self._cap
221
+
222
+ def streamoff(self):
223
+ """열려 있는 스트림을 닫습니다."""
224
+ if self._cap is not None:
225
+ try:
226
+ self._cap.release()
227
+ except Exception:
228
+ pass
229
+ self._cap = None
230
+
231
+
232
+
233
+
234
+
235
+
236
+
@@ -9,7 +9,7 @@ LEFT = 3
9
9
  class DefLib:
10
10
 
11
11
  @classmethod
12
- def checksum(self, packet):
12
+ def checksum(cls, packet):
13
13
  len = packet[4]
14
14
  sum = 0
15
15
  for n in range(6, len):
@@ -18,7 +18,7 @@ class DefLib:
18
18
 
19
19
 
20
20
  @classmethod
21
- def _print(self, data):
21
+ def _print(cls, data):
22
22
  for n in range(0, len(data)):
23
23
  h = hex(data[n])
24
24
  # print(h, end=" ")
@@ -26,7 +26,7 @@ class DefLib:
26
26
 
27
27
 
28
28
  @classmethod
29
- def constrain(self, val , max, min):
29
+ def constrain(cls, val , max, min):
30
30
  if val > max:
31
31
  val = max
32
32
  if val < min:
@@ -35,7 +35,7 @@ class DefLib:
35
35
 
36
36
 
37
37
  @classmethod
38
- def comp(self, data):
38
+ def comp(cls, data):
39
39
  data = data&0xFF
40
40
  if data < 0:
41
41
  return 256 + data
@@ -14,20 +14,23 @@ class IKeyEvent:
14
14
  self.keyRight = True
15
15
  if key == keyboard.Key.left:
16
16
  self.keyLeft = True
17
- if key == keyboard.KeyCode(char="w"):
17
+
18
+ char = getattr(key, 'char', None)
19
+ if char == "w":
18
20
  self.keyGoUp = True
19
- if key == keyboard.KeyCode(char="x"):
21
+ if char == "x":
20
22
  self.keyGoDown = True
21
- if key == keyboard.KeyCode(char="a"):
23
+ if char == "a":
22
24
  self.keyLTurn = True
23
- if key == keyboard.KeyCode(char="d"):
25
+ if char == "d":
24
26
  self.keyRTurn = True
25
- if key == keyboard.KeyCode(char="r"):
27
+ if char == "r":
26
28
  self.keyRecording = True
27
- if key == keyboard.KeyCode(char="s"):
29
+ if char == "s":
28
30
  self.keyPicture = True
29
- if key == keyboard.KeyCode(char="t"):
31
+ if char == "t":
30
32
  self.keyTracking = True
33
+
31
34
  if key == keyboard.Key.esc:
32
35
  self.keyEsc = True
33
36
 
@@ -44,20 +47,23 @@ class IKeyEvent:
44
47
  self.keyRight = False
45
48
  if key == keyboard.Key.left:
46
49
  self.keyLeft = False
47
- if key == keyboard.KeyCode(char="w"):
50
+
51
+ char = getattr(key, 'char', None)
52
+ if char == "w":
48
53
  self.keyGoUp = False
49
- if key == keyboard.KeyCode(char="x"):
54
+ if char == "x":
50
55
  self.keyGoDown = False
51
- if key == keyboard.KeyCode(char="a"):
56
+ if char == "a":
52
57
  self.keyLTurn = False
53
- if key == keyboard.KeyCode(char="d"):
58
+ if char == "d":
54
59
  self.keyRTurn = False
55
- if key == keyboard.KeyCode(char="r"):
60
+ if char == "r":
56
61
  self.keyRecording = False
57
- if key == keyboard.KeyCode(char="s"):
62
+ if char == "s":
58
63
  self.keyPicture = False
59
- if key == keyboard.KeyCode(char="t"):
64
+ if char == "t":
60
65
  self.keyTracking = False
66
+
61
67
  if key == keyboard.Key.esc:
62
68
  self.keyEsc = False
63
69
 
@@ -41,5 +41,5 @@ class Parse:
41
41
  chksum = DefLib.checksum(self.packet)
42
42
  if chksum == self.packet[5]:
43
43
  return self.packet
44
- return "None"
44
+ return None
45
45
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyaidrone
3
- Version: 2.2
3
+ Version: 2.4
4
4
  Summary: Library for AIDrone Products
5
5
  Home-page: http://www.ir-brain.com
6
6
  Author: IR-Brain
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="pyaidrone",
5
- version="2.2",
5
+ version="2.4",
6
6
  description="Library for AIDrone Products",
7
7
  long_description=open("README.md").read(), # README.md 내용을 long_description으로 사용
8
8
  long_description_content_type="text/markdown", # README 파일이 markdown 형식임을 지정
@@ -1,352 +0,0 @@
1
- import serial
2
- import binascii
3
- import math
4
- from time import sleep
5
- import random
6
- import platform
7
- import subprocess
8
- from operator import eq
9
- from threading import Thread
10
- from serial.tools.list_ports import comports
11
- from pyaidrone.parse import *
12
- from pyaidrone.packet import *
13
- from pyaidrone.deflib import *
14
-
15
-
16
- class AIDrone(Parse, Packet):
17
- def __init__(self, receiveCallback=None, index=0):
18
- self.serial = None
19
- self.isThreadRun = False
20
- self.parse = Parse(AIDRONE)
21
- self.makepkt = Packet(AIDRONE)
22
- self.receiveCallback = receiveCallback
23
- self.makepkt.clearPacket()
24
- self.posX = 0
25
- self.posY = 0
26
- self.rot = 0
27
- self.portIndex = index # 멀티 연결 시 드론 번호 식별용
28
- # --- 영상 스트리밍 관련 기본값 ---
29
- self.stream_host = "192.168.4.1"
30
- self.stream_port = 80
31
- self.stream_path = "/?action=stream"
32
- self._cap = None
33
-
34
- def receiveHandler(self):
35
- while self.isThreadRun:
36
- readData = self.serial.read(self.serial.in_waiting or 1)
37
- packet = self.parse.packetArrange(readData)
38
- if not eq(packet, "None"):
39
- if self.receiveCallback is not None:
40
- self.receiveCallback(packet)
41
- self.serial.write(self.makepkt.getPacket())
42
-
43
- def Open(self, portName="None"):
44
- if eq(portName, "None"):
45
- nodes = comports()
46
- for node in nodes:
47
- if "CH340" in node.description:
48
- portName = node.device
49
-
50
- if eq(portName, "None"):
51
- print("Can't find Serial Port")
52
- exit()
53
- return False
54
- try:
55
- self.serial = serial.Serial(port=portName, baudrate=19200, timeout=1)
56
- if self.serial.isOpen():
57
- self.isThreadRun = True
58
- self.thread = Thread(target=self.receiveHandler, args=(), daemon=True)
59
- self.thread.start()
60
- print(f"[Drone #{self.portIndex}] Connected to {portName}")
61
- return True
62
- else:
63
- print("Can't open " + portName)
64
- exit()
65
- return False
66
- except Exception:
67
- print("Can't open " + portName)
68
- exit()
69
- return False
70
-
71
- def Close(self):
72
- self.isThreadRun = False
73
- sleep(0.2)
74
- pkt = self.makepkt.getPacket()
75
- if (pkt[15] & 0x80) == 0x80:
76
- self.makepkt.clearPacket()
77
- self.setOption(0x8000)
78
- self.serial.write(self.makepkt.getPacket())
79
- sleep(0.2)
80
- self.serial.write(self.makepkt.clearPacket())
81
- sleep(0.2)
82
- if self.serial is not None:
83
- if self.serial.isOpen():
84
- self.serial.close()
85
-
86
- def setOption(self, option):
87
- data = option.to_bytes(2, byteorder="little", signed=False)
88
- self.makepkt.makePacket(14, data)
89
-
90
- def takeoff(self):
91
- alt = 70
92
- data = alt.to_bytes(2, byteorder="little", signed=False)
93
- self.makepkt.makePacket(12, data)
94
- self.setOption(0x2F)
95
-
96
- def landing(self):
97
- alt = 0
98
- data = alt.to_bytes(2, byteorder="little", signed=False)
99
- self.makepkt.makePacket(12, data)
100
-
101
- def altitude(self, alt):
102
- data = alt.to_bytes(2, byteorder="little", signed=False)
103
- self.makepkt.makePacket(12, data)
104
-
105
- def velocity(self, dir=0, vel=100):
106
- if dir > 3:
107
- return
108
- if dir == 1 or dir == 3:
109
- vel *= -1
110
- data = vel.to_bytes(2, byteorder="little", signed=True)
111
- if dir == 0 or dir == 1:
112
- self.makepkt.makePacket(8, data)
113
- else:
114
- self.makepkt.makePacket(6, data)
115
- self.setOption(0x0F)
116
-
117
- def move(self, dir=0, dist=100):
118
- if dir > 3:
119
- return
120
- if dir == 1 or dir == 3:
121
- dist *= -1
122
- if dir == 0 or dir == 1:
123
- self.posX += dist
124
- data = self.posX.to_bytes(2, byteorder="little", signed=True)
125
- self.makepkt.makePacket(8, data)
126
- else:
127
- self.posY += dist
128
- data = self.posY.to_bytes(2, byteorder="little", signed=True)
129
- self.makepkt.makePacket(6, data)
130
- self.setOption(0x2F)
131
-
132
- def rotation(self, rot=90):
133
- self.rot += rot
134
- data = self.rot.to_bytes(2, byteorder="little", signed=True)
135
- self.makepkt.makePacket(10, data)
136
-
137
- def motor(self, what, speed):
138
- speed = DefLib.constrain(speed, 100, 0)
139
- data = speed.to_bytes(2, byteorder="little", signed=True)
140
- self.makepkt.makePacket(what * 2 + 6, data)
141
- self.setOption(0x8000)
142
-
143
- def emergency(self):
144
- self.setOption(0x00)
145
- self.serial.write(self.makepkt.getPacket())
146
-
147
- # ── 스트리밍 관련 ──────────────────────────────────────────────────────────
148
- def setStreamAddress(self, host: str, port: int = 80, path: str = "/?action=stream"):
149
- """MJPG-Streamer 주소 구성 요소를 저장합니다."""
150
- if not isinstance(host, str) or len(host.strip()) == 0:
151
- raise ValueError("host가 올바르지 않습니다.")
152
- self.stream_host = host.strip()
153
- self.stream_port = int(port)
154
- if not path.startswith("/"):
155
- path = "/" + path
156
- self.stream_path = path
157
- return self._build_stream_url()
158
-
159
- def _build_stream_url(self):
160
- """내부 사용: 저장된 host/port/path로 URL 생성."""
161
- if self.stream_port in (80, None):
162
- return f"http://{self.stream_host}{self.stream_path}"
163
- return f"http://{self.stream_host}:{self.stream_port}{self.stream_path}"
164
-
165
- def streamon(self, host: str = None, port: int = None, path: str = None, return_url: bool = False):
166
- """
167
- MJPG-Streamer 스트림을 엽니다.
168
- - 인자를 주면 해당 값으로 주소를 덮어쓰고, 안 주면 저장된 값 사용.
169
- - return_url=True 이면 URL 문자열만 반환(캡처는 열지 않음).
170
- """
171
- if host is not None or port is not None or path is not None:
172
- self.setStreamAddress(
173
- host or self.stream_host,
174
- self.stream_port if port is None else port,
175
- self.stream_path if path is None else path,
176
- )
177
- url = self._build_stream_url()
178
- if return_url:
179
- return url
180
- try:
181
- import cv2 as cv
182
- except Exception as e:
183
- raise RuntimeError(f"OpenCV(cv2) 임포트 실패: {e}")
184
- self.streamoff()
185
- self._cap = cv.VideoCapture(url)
186
- if not self._cap.isOpened():
187
- self._cap.release()
188
- self._cap = None
189
- raise RuntimeError(f"스트림 열기 실패: {url}")
190
- return self._cap
191
-
192
- def streamoff(self):
193
- """열려 있는 스트림을 닫습니다."""
194
- if self._cap is not None:
195
- try:
196
- self._cap.release()
197
- except Exception:
198
- pass
199
- self._cap = None
200
-
201
- # ── 센서 데이터 요청 ────────────────────────────────────────────────────────
202
- def requestSensorData(self, type_num, mode=1):
203
- """
204
- 드론에게 센서 및 비행 정보 출력을 요청합니다.
205
- type_num: 9 (비행 정보), 10 (센서 신호)
206
- mode: 1 (출력 시작), 0 (출력 중지)
207
- """
208
- if type_num == 9:
209
- data = mode.to_bytes(1, byteorder="little")
210
- self.makepkt.packet[3] = 0xB2
211
- self.makepkt.makePacket(7, data)
212
- elif type_num == 10:
213
- data = mode.to_bytes(1, byteorder="little")
214
- self.makepkt.packet[3] = 0xB1
215
- self.makepkt.makePacket(7, data)
216
-
217
-
218
- # ══════════════════════════════════════════════════════════════════════════════
219
- # 멀티 드론 헬퍼 함수
220
- # ══════════════════════════════════════════════════════════════════════════════
221
-
222
- def getWindowsPortList():
223
- """Windows에서 CH340 포트 목록 반환."""
224
- nodes = comports()
225
- portList = [node.device for node in nodes if "CH340" in node.description]
226
- if not portList:
227
- print("Can't find Serial Port (CH340)")
228
- return None
229
- print(f"발견된 포트: {portList}")
230
- return portList
231
-
232
-
233
- def getLinuxPortList():
234
- """Linux에서 /dev/ttyUSB* 포트 목록 반환."""
235
- try:
236
- output = subprocess.check_output("ls /dev/ttyUSB*", shell=True)
237
- return tuple(output.decode().strip().split())
238
- except subprocess.CalledProcessError:
239
- print("Can't find Serial Port (/dev/ttyUSB*)")
240
- return None
241
-
242
-
243
- def AIDroneMultiOpen(nameList=None, receiveCallback=None):
244
- """
245
- 여러 대의 AIDrone을 동시에 연결합니다.
246
-
247
- Parameters
248
- ----------
249
- nameList : list[str] | None
250
- 포트 이름 목록. None이면 OS에 맞게 자동 탐색.
251
- receiveCallback : callable | None
252
- 수신 콜백. 호출 시 첫 번째 인자로 AIDrone 인스턴스가 전달됩니다.
253
-
254
- Returns
255
- -------
256
- list[AIDrone] | None
257
- 연결된 드론 객체 목록.
258
-
259
- Example
260
- -------
261
- >>> drones = AIDroneMultiOpen()
262
- >>> multi_takeoff(drones)
263
- >>> sleep(3)
264
- >>> multi_landing(drones)
265
- >>> AIDroneMultiClose(drones)
266
- """
267
- if nameList is None:
268
- if platform.system() == "Linux":
269
- nameList = getLinuxPortList()
270
- else:
271
- nameList = getWindowsPortList()
272
-
273
- if not nameList:
274
- return None
275
-
276
- howMany = min(len(nameList), 10) # 최대 10대 제한
277
- drones = []
278
-
279
- for n in range(howMany):
280
- drone = AIDrone(receiveCallback, n)
281
- if drone.Open(nameList[n]):
282
- drones.append(drone)
283
- else:
284
- print(f"[Drone #{n}] 연결 실패 → 전체 연결 중단")
285
- AIDroneMultiClose(drones)
286
- return None
287
-
288
- print(f"총 {len(drones)}대 드론 연결 완료.")
289
- return drones
290
-
291
-
292
- def AIDroneMultiClose(drones):
293
- """연결된 모든 드론을 안전하게 닫습니다."""
294
- if not drones:
295
- return
296
- # 착륙 명령 먼저 전송
297
- for drone in drones:
298
- drone.landing()
299
- sleep(0.5)
300
- for drone in drones:
301
- drone.Close()
302
- print("모든 드론 연결 종료.")
303
-
304
-
305
- # ── 멀티 제어 함수 ─────────────────────────────────────────────────────────────
306
-
307
- def multi_takeoff(drones):
308
- """모든 드론 이륙."""
309
- for drone in drones:
310
- drone.takeoff()
311
-
312
-
313
- def multi_landing(drones):
314
- """모든 드론 착륙."""
315
- for drone in drones:
316
- drone.landing()
317
-
318
-
319
- def multi_altitude(drones, alt):
320
- """모든 드론 고도 설정."""
321
- for drone in drones:
322
- drone.altitude(alt)
323
-
324
-
325
- def multi_move(drones, dir=0, dist=100):
326
- """모든 드론 이동."""
327
- for drone in drones:
328
- drone.move(dir, dist)
329
-
330
-
331
- def multi_velocity(drones, dir=0, vel=100):
332
- """모든 드론 속도 제어."""
333
- for drone in drones:
334
- drone.velocity(dir, vel)
335
-
336
-
337
- def multi_rotation(drones, rot=90):
338
- """모든 드론 회전."""
339
- for drone in drones:
340
- drone.rotation(rot)
341
-
342
-
343
- def multi_motor(drones, what, speed):
344
- """모든 드론 모터 제어."""
345
- for drone in drones:
346
- drone.motor(what, speed)
347
-
348
-
349
- def multi_emergency(drones):
350
- """모든 드론 긴급 정지."""
351
- for drone in drones:
352
- drone.emergency()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes