pyaidrone 2.0__tar.gz → 2.2__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.
- {pyaidrone-2.0 → pyaidrone-2.2}/PKG-INFO +13 -2
- pyaidrone-2.2/pyaidrone/aiDrone.py +352 -0
- pyaidrone-2.2/pyaidrone/aidrone_mavlink_bridge.py +397 -0
- pyaidrone-2.2/pyaidrone/edu.py +192 -0
- {pyaidrone-2.0 → pyaidrone-2.2}/pyaidrone/vision_ai.py +6 -2
- {pyaidrone-2.0 → pyaidrone-2.2}/pyaidrone.egg-info/PKG-INFO +13 -2
- {pyaidrone-2.0 → pyaidrone-2.2}/pyaidrone.egg-info/SOURCES.txt +1 -0
- {pyaidrone-2.0 → pyaidrone-2.2}/setup.py +1 -1
- pyaidrone-2.0/pyaidrone/aiDrone.py +0 -221
- pyaidrone-2.0/pyaidrone/edu.py +0 -188
- {pyaidrone-2.0 → pyaidrone-2.2}/README.md +0 -0
- {pyaidrone-2.0 → pyaidrone-2.2}/pyaidrone/__init__.py +0 -0
- {pyaidrone-2.0 → pyaidrone-2.2}/pyaidrone/deflib.py +0 -0
- {pyaidrone-2.0 → pyaidrone-2.2}/pyaidrone/ikeyevent.py +0 -0
- {pyaidrone-2.0 → pyaidrone-2.2}/pyaidrone/packet.py +0 -0
- {pyaidrone-2.0 → pyaidrone-2.2}/pyaidrone/parse.py +0 -0
- {pyaidrone-2.0 → pyaidrone-2.2}/pyaidrone/sensor.py +0 -0
- {pyaidrone-2.0 → pyaidrone-2.2}/pyaidrone.egg-info/dependency_links.txt +0 -0
- {pyaidrone-2.0 → pyaidrone-2.2}/pyaidrone.egg-info/requires.txt +0 -0
- {pyaidrone-2.0 → pyaidrone-2.2}/pyaidrone.egg-info/top_level.txt +0 -0
- {pyaidrone-2.0 → pyaidrone-2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pyaidrone
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2
|
|
4
4
|
Summary: Library for AIDrone Products
|
|
5
5
|
Home-page: http://www.ir-brain.com
|
|
6
6
|
Author: IR-Brain
|
|
@@ -10,6 +10,17 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Requires-Python: >=3.6
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: pyserial>=3.4
|
|
14
|
+
Requires-Dist: pynput>=1.7.3
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: requires-dist
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
Dynamic: summary
|
|
13
24
|
|
|
14
25
|
## install pyaidrone
|
|
15
26
|
|
|
@@ -0,0 +1,352 @@
|
|
|
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()
|