pyaidrone 2.3__tar.gz → 2.5__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.3 → pyaidrone-2.5}/PKG-INFO +2 -13
- pyaidrone-2.5/pyaidrone/aiDrone.py +282 -0
- pyaidrone-2.5/pyaidrone/deflib.py +41 -0
- {pyaidrone-2.3 → pyaidrone-2.5}/pyaidrone/ikeyevent.py +2 -2
- {pyaidrone-2.3 → pyaidrone-2.5}/pyaidrone/packet.py +5 -2
- pyaidrone-2.5/pyaidrone/sensor.py +69 -0
- {pyaidrone-2.3 → pyaidrone-2.5}/pyaidrone/vision_ai.py +10 -2
- {pyaidrone-2.3 → pyaidrone-2.5}/pyaidrone.egg-info/PKG-INFO +2 -13
- {pyaidrone-2.3 → pyaidrone-2.5}/setup.py +1 -1
- pyaidrone-2.3/pyaidrone/aiDrone.py +0 -352
- pyaidrone-2.3/pyaidrone/deflib.py +0 -44
- pyaidrone-2.3/pyaidrone/sensor.py +0 -58
- {pyaidrone-2.3 → pyaidrone-2.5}/README.md +0 -0
- {pyaidrone-2.3 → pyaidrone-2.5}/pyaidrone/__init__.py +0 -0
- {pyaidrone-2.3 → pyaidrone-2.5}/pyaidrone/aidrone_mavlink_bridge.py +0 -0
- {pyaidrone-2.3 → pyaidrone-2.5}/pyaidrone/edu.py +0 -0
- {pyaidrone-2.3 → pyaidrone-2.5}/pyaidrone/parse.py +0 -0
- {pyaidrone-2.3 → pyaidrone-2.5}/pyaidrone.egg-info/SOURCES.txt +0 -0
- {pyaidrone-2.3 → pyaidrone-2.5}/pyaidrone.egg-info/dependency_links.txt +0 -0
- {pyaidrone-2.3 → pyaidrone-2.5}/pyaidrone.egg-info/requires.txt +0 -0
- {pyaidrone-2.3 → pyaidrone-2.5}/pyaidrone.egg-info/top_level.txt +0 -0
- {pyaidrone-2.3 → pyaidrone-2.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: pyaidrone
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5
|
|
4
4
|
Summary: Library for AIDrone Products
|
|
5
5
|
Home-page: http://www.ir-brain.com
|
|
6
6
|
Author: IR-Brain
|
|
@@ -10,17 +10,6 @@ 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
|
|
24
13
|
|
|
25
14
|
## install pyaidrone
|
|
26
15
|
|
|
@@ -0,0 +1,282 @@
|
|
|
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
|
+
if portName is None:
|
|
49
|
+
nodes = comports()
|
|
50
|
+
for node in nodes:
|
|
51
|
+
if "CH340" in node.description:
|
|
52
|
+
portName = node.device
|
|
53
|
+
|
|
54
|
+
if portName is None:
|
|
55
|
+
print("Can't find Serial Port")
|
|
56
|
+
raise ConnectionError("CH340 시리얼 포트를 찾을 수 없습니다.")
|
|
57
|
+
try:
|
|
58
|
+
self.serial = serial.Serial(port=portName, baudrate=19200, timeout=1)
|
|
59
|
+
if self.serial.isOpen():
|
|
60
|
+
self.isThreadRun = True
|
|
61
|
+
self.thread = Thread(target=self.receiveHandler, args=(), daemon=True)
|
|
62
|
+
self.thread.start()
|
|
63
|
+
print("Connected to", portName)
|
|
64
|
+
return True
|
|
65
|
+
else:
|
|
66
|
+
print("Can't open " + portName)
|
|
67
|
+
raise ConnectionError(f"포트를 열 수 없습니다: {portName}")
|
|
68
|
+
except serial.SerialException as e:
|
|
69
|
+
# [수정] 구체적 예외 처리
|
|
70
|
+
print(f"시리얼 포트 오류: {e}")
|
|
71
|
+
raise
|
|
72
|
+
except ConnectionError:
|
|
73
|
+
raise
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"Can't open {portName}: {e}")
|
|
76
|
+
raise ConnectionError(f"포트 연결 실패: {portName}") from e
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def Close(self):
|
|
80
|
+
self.isThreadRun = False
|
|
81
|
+
sleep(0.2)
|
|
82
|
+
try:
|
|
83
|
+
# [수정] serial.write 예외처리 추가 (포트 닫힌 상태 보호)
|
|
84
|
+
pkt = self.makepkt.getPacket()
|
|
85
|
+
if (pkt[15] & 0x80) == 0x80:
|
|
86
|
+
self.makepkt.clearPacket()
|
|
87
|
+
self.setOption(0x8000)
|
|
88
|
+
self.serial.write(self.makepkt.getPacket())
|
|
89
|
+
sleep(0.2)
|
|
90
|
+
self.serial.write(self.makepkt.clearPacket())
|
|
91
|
+
sleep(0.2)
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
finally:
|
|
95
|
+
if self.serial is not None and self.serial.isOpen():
|
|
96
|
+
self.serial.close()
|
|
97
|
+
|
|
98
|
+
def setOption(self, option):
|
|
99
|
+
data = option.to_bytes(2, byteorder="little", signed=False)
|
|
100
|
+
self.makepkt.makePacket(14, data)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def takeoff(self):
|
|
104
|
+
alt = 70
|
|
105
|
+
data = alt.to_bytes(2, byteorder="little", signed=False)
|
|
106
|
+
self.makepkt.makePacket(12, data)
|
|
107
|
+
self.setOption(0x2F)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def landing(self):
|
|
111
|
+
alt = 0
|
|
112
|
+
data = alt.to_bytes(2, byteorder="little", signed=False)
|
|
113
|
+
self.makepkt.makePacket(12, data)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def altitude(self, alt):
|
|
117
|
+
data = alt.to_bytes(2, byteorder="little", signed=False)
|
|
118
|
+
self.makepkt.makePacket(12, data)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def velocity(self, dir=0, vel=100):
|
|
122
|
+
if dir > 3:
|
|
123
|
+
return
|
|
124
|
+
if dir==1 or dir==3:
|
|
125
|
+
vel *= -1;
|
|
126
|
+
data = vel.to_bytes(2, byteorder="little", signed=True)
|
|
127
|
+
if dir==0 or dir==1:
|
|
128
|
+
self.makepkt.makePacket(8, data)
|
|
129
|
+
else:
|
|
130
|
+
self.makepkt.makePacket(6, data)
|
|
131
|
+
self.setOption(0x0F)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def move(self, dir=0, dist=100):
|
|
135
|
+
if dir > 3:
|
|
136
|
+
return
|
|
137
|
+
if dir==1 or dir==3:
|
|
138
|
+
dist *= -1;
|
|
139
|
+
if dir==0 or dir==1:
|
|
140
|
+
self.posX += dist
|
|
141
|
+
data = self.posX.to_bytes(2, byteorder="little", signed=True)
|
|
142
|
+
self.makepkt.makePacket(8, data)
|
|
143
|
+
else:
|
|
144
|
+
self.posY += dist
|
|
145
|
+
data = self.posY.to_bytes(2, byteorder="little", signed=True)
|
|
146
|
+
self.makepkt.makePacket(6, data)
|
|
147
|
+
self.setOption(0x2F)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def rotation(self, rot=90):
|
|
151
|
+
self.rot += rot
|
|
152
|
+
# 프로토콜: YAW는 절대 회전각 -180 ~ 180 degree
|
|
153
|
+
if self.rot > 180:
|
|
154
|
+
self.rot -= 360
|
|
155
|
+
elif self.rot < -180:
|
|
156
|
+
self.rot += 360
|
|
157
|
+
data = self.rot.to_bytes(2, byteorder="little", signed=True)
|
|
158
|
+
self.makepkt.makePacket(10, data)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def motor(self, what, speed):
|
|
163
|
+
speed = DefLib.constrain(speed, 100, 0)
|
|
164
|
+
data = speed.to_bytes(2, byteorder="little", signed=True)
|
|
165
|
+
self.makepkt.makePacket(what*2+6, data)
|
|
166
|
+
self.setOption(0x8000)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def emergency(self):
|
|
170
|
+
self.setOption(0x00)
|
|
171
|
+
self.serial.write(self.makepkt.getPacket())
|
|
172
|
+
|
|
173
|
+
def requestSensorData(self, data_type: int, sig_mode: int = None) -> None:
|
|
174
|
+
"""
|
|
175
|
+
드론에 센서 데이터 출력을 요청합니다.
|
|
176
|
+
|
|
177
|
+
data_type 9 : 비행정보 요청 (roll / pitch / height / pos_x / pos_y)
|
|
178
|
+
sig_mode — 기본값 1 (출력 시작)
|
|
179
|
+
data_type 10 : 센서·제어 신호 요청
|
|
180
|
+
sig_mode — SIG 값 (POS_SIGNAL=0x06, 기타 0x01~0x05)
|
|
181
|
+
기본값 0x06 (flow_x, flow_y, tof 포함)
|
|
182
|
+
|
|
183
|
+
프로토콜 참고 (AIDrone Protocol.pdf):
|
|
184
|
+
TYPE(9) — Command Packet B2: 26 A8 14 B2 08 CS 09 MODE
|
|
185
|
+
TYPE(10) — Command Packet B1: 26 A8 14 B1 08 CS 0A SIG
|
|
186
|
+
"""
|
|
187
|
+
if self.serial is None or not self.serial.isOpen():
|
|
188
|
+
return
|
|
189
|
+
if data_type == 9:
|
|
190
|
+
mode = 1 if sig_mode is None else (sig_mode & 0xFF)
|
|
191
|
+
pkt = bytearray([0x26, 0xA8, 0x14, 0xB2, 0x08, 0x00, 0x09, mode])
|
|
192
|
+
pkt[5] = sum(pkt[4:]) & 0xFF
|
|
193
|
+
self.serial.write(pkt)
|
|
194
|
+
elif data_type == 10:
|
|
195
|
+
sig = 0x06 if sig_mode is None else (sig_mode & 0xFF)
|
|
196
|
+
pkt = bytearray([0x26, 0xA8, 0x14, 0xB1, 0x08, 0x00, 0x0A, sig])
|
|
197
|
+
pkt[5] = sum(pkt[4:]) & 0xFF
|
|
198
|
+
self.serial.write(pkt)
|
|
199
|
+
|
|
200
|
+
def stopSensorData(self, data_type: int) -> None:
|
|
201
|
+
"""
|
|
202
|
+
드론에 센서 데이터 출력 중지를 요청합니다.
|
|
203
|
+
|
|
204
|
+
data_type 9 : 비행정보 출력 중지 (MODE=0)
|
|
205
|
+
data_type 10 : 센서·제어 신호 출력 중지 (STOP_SIGNAL=0x00)
|
|
206
|
+
"""
|
|
207
|
+
if self.serial is None or not self.serial.isOpen():
|
|
208
|
+
return
|
|
209
|
+
if data_type == 9:
|
|
210
|
+
pkt = bytearray([0x26, 0xA8, 0x14, 0xB2, 0x08, 0x00, 0x09, 0x00])
|
|
211
|
+
pkt[5] = sum(pkt[4:]) & 0xFF
|
|
212
|
+
self.serial.write(pkt)
|
|
213
|
+
elif data_type == 10:
|
|
214
|
+
pkt = bytearray([0x26, 0xA8, 0x14, 0xB1, 0x08, 0x00, 0x0A, 0x00])
|
|
215
|
+
pkt[5] = sum(pkt[4:]) & 0xFF
|
|
216
|
+
self.serial.write(pkt)
|
|
217
|
+
|
|
218
|
+
# aiDrone.py — AIDrone 클래스 안에 아래 3개 메서드 추가
|
|
219
|
+
def setStreamAddress(self, host: str, port: int = 80, path: str = "/?action=stream"):
|
|
220
|
+
"""MJPG-Streamer 주소 구성 요소를 저장합니다."""
|
|
221
|
+
if not isinstance(host, str) or len(host.strip()) == 0:
|
|
222
|
+
raise ValueError("host가 올바르지 않습니다.")
|
|
223
|
+
self.stream_host = host.strip()
|
|
224
|
+
self.stream_port = int(port)
|
|
225
|
+
if not path.startswith("/"):
|
|
226
|
+
path = "/" + path
|
|
227
|
+
self.stream_path = path
|
|
228
|
+
return self._build_stream_url()
|
|
229
|
+
|
|
230
|
+
def _build_stream_url(self):
|
|
231
|
+
"""내부 사용: 저장된 host/port/path로 URL 생성."""
|
|
232
|
+
if self.stream_port in (80, None):
|
|
233
|
+
return f"http://{self.stream_host}{self.stream_path}"
|
|
234
|
+
return f"http://{self.stream_host}:{self.stream_port}{self.stream_path}"
|
|
235
|
+
|
|
236
|
+
def streamon(self, host: str = None, port: int = None, path: str = None, return_url: bool = False):
|
|
237
|
+
"""
|
|
238
|
+
MJPG-Streamer 스트림을 엽니다.
|
|
239
|
+
- 인자를 주면 해당 값으로 주소를 덮어쓰고, 안 주면 저장된 값 사용.
|
|
240
|
+
- return_url=True 이면 URL 문자열만 반환(캡처는 열지 않음).
|
|
241
|
+
- 기본 URL 예: http://192.168.4.1/?action=stream
|
|
242
|
+
"""
|
|
243
|
+
if host is not None or port is not None or path is not None:
|
|
244
|
+
self.setStreamAddress(host or self.stream_host,
|
|
245
|
+
self.stream_port if port is None else port,
|
|
246
|
+
self.stream_path if path is None else path)
|
|
247
|
+
|
|
248
|
+
url = self._build_stream_url()
|
|
249
|
+
if return_url:
|
|
250
|
+
return url
|
|
251
|
+
|
|
252
|
+
# OpenCV는 여기서만 임포트(필요할 때만)
|
|
253
|
+
try:
|
|
254
|
+
import cv2 as cv
|
|
255
|
+
except Exception as e:
|
|
256
|
+
raise RuntimeError(f"OpenCV(cv2) 임포트 실패: {e}")
|
|
257
|
+
|
|
258
|
+
# 이전 cap이 열려 있으면 정리
|
|
259
|
+
self.streamoff()
|
|
260
|
+
|
|
261
|
+
self._cap = cv.VideoCapture(url)
|
|
262
|
+
if not self._cap.isOpened():
|
|
263
|
+
self._cap.release()
|
|
264
|
+
self._cap = None
|
|
265
|
+
raise RuntimeError(f"스트림 열기 실패: {url}")
|
|
266
|
+
return self._cap
|
|
267
|
+
|
|
268
|
+
def streamoff(self):
|
|
269
|
+
"""열려 있는 스트림을 닫습니다."""
|
|
270
|
+
if self._cap is not None:
|
|
271
|
+
try:
|
|
272
|
+
self._cap.release()
|
|
273
|
+
except Exception:
|
|
274
|
+
pass
|
|
275
|
+
self._cap = None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
AIDRONE = 1
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
FRONT = 0
|
|
5
|
+
BACK = 1
|
|
6
|
+
RIGHT = 2
|
|
7
|
+
LEFT = 3
|
|
8
|
+
|
|
9
|
+
class DefLib:
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def checksum(cls, packet):
|
|
13
|
+
length = packet[4]
|
|
14
|
+
total = 0
|
|
15
|
+
for n in range(6, length):
|
|
16
|
+
total += packet[n]
|
|
17
|
+
return total & 0xFF
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def _print(cls, data):
|
|
22
|
+
for n in range(0, len(data)):
|
|
23
|
+
h = hex(data[n])
|
|
24
|
+
# print(h, end=" ")
|
|
25
|
+
# print("")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def constrain(cls, val, val_max, val_min):
|
|
30
|
+
if val > val_max:
|
|
31
|
+
val = val_max
|
|
32
|
+
if val < val_min:
|
|
33
|
+
val = val_min
|
|
34
|
+
return val
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def comp(cls, data):
|
|
39
|
+
# &0xFF 결과는 항상 0~255이므로 부호 분기 불필요
|
|
40
|
+
return data & 0xFF
|
|
41
|
+
|
|
@@ -83,8 +83,8 @@ class IKeyEvent:
|
|
|
83
83
|
self.keyTracking = False
|
|
84
84
|
self.keyEsc = False
|
|
85
85
|
|
|
86
|
-
listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release)
|
|
87
|
-
listener.start()
|
|
86
|
+
self.listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release)
|
|
87
|
+
self.listener.start()
|
|
88
88
|
|
|
89
89
|
def isKeyEnterPressed(self):
|
|
90
90
|
return self.keyEnter
|
|
@@ -12,8 +12,11 @@ class Packet:
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def makePacket(self, start, data):
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
end = start + len(data)
|
|
16
|
+
if end > len(self.packet):
|
|
17
|
+
raise ValueError(f"makePacket: 범위 초과 (start={start}, len={len(data)}, packet={len(self.packet)})")
|
|
18
|
+
for n in range(start, end):
|
|
19
|
+
self.packet[n] = data[n - start]
|
|
17
20
|
self.packet[5] = DefLib.checksum(self.packet)
|
|
18
21
|
# DefLib._print(self.packet)
|
|
19
22
|
return self.packet
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#AIDrone sensor 라이브러리 (26.01.15)
|
|
2
|
+
import struct
|
|
3
|
+
from pyaidrone.deflib import DefLib
|
|
4
|
+
|
|
5
|
+
class AIDroneSensor:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
# 센서 데이터 및 비행 상태 통합 저장소
|
|
8
|
+
self.state = {
|
|
9
|
+
'battery': 0, # 배터리 잔량 (%)
|
|
10
|
+
'tof': 0, # VL53L01X 고도 센서 값 (mm)
|
|
11
|
+
'flow_x': 0, # PMW3901 광학 흐름 센서 X축 변화량
|
|
12
|
+
'flow_y': 0, # PMW3901 광학 흐름 센서 Y축 변화량
|
|
13
|
+
'pos_x': 0, # 이륙 지점 기준 X 좌표 (cm)
|
|
14
|
+
'pos_y': 0, # 이륙 지점 기준 Y 좌표 (cm)
|
|
15
|
+
'height': 0, # 드론이 계산한 현재 높이 (cm)
|
|
16
|
+
'roll': 0, # 현재 좌우 기울기
|
|
17
|
+
'pitch': 0, # 현재 앞뒤 기울기
|
|
18
|
+
'ready': False, # 비행 준비 상태
|
|
19
|
+
'emergency': False # 응급 정지 상태 여부
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def parse(self, packet):
|
|
23
|
+
"""
|
|
24
|
+
pyaidrone의 parse.py에서 수신된 패킷을 분석하여 상태를 업데이트합니다.
|
|
25
|
+
|
|
26
|
+
Output Packet 구조 (parse.py가 헤더 4바이트를 저장하지 않으므로 offset 주의):
|
|
27
|
+
packet[4] = LEN
|
|
28
|
+
packet[5] = CS
|
|
29
|
+
packet[6] = 0x01 (고정값)
|
|
30
|
+
packet[7] = OPT1
|
|
31
|
+
packet[8] = OPT2
|
|
32
|
+
packet[9] = BAT (배터리 %)
|
|
33
|
+
packet[10] = INFO[0] (TYPE 바이트)
|
|
34
|
+
packet[11+]= INFO 데이터
|
|
35
|
+
"""
|
|
36
|
+
if packet is None:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
# parse.py에서 체크섬 검증 후 전달된 패킷이므로 헤더 재확인 불필요.
|
|
40
|
+
# OPT1: packet[7], BAT: packet[9], INFO: packet[10+]
|
|
41
|
+
opt1 = packet[7]
|
|
42
|
+
self.state['battery'] = packet[9]
|
|
43
|
+
self.state['emergency'] = bool(opt1 & 0x01)
|
|
44
|
+
self.state['ready'] = bool(opt1 & 0x02)
|
|
45
|
+
|
|
46
|
+
# INFO 데이터 구간 (packet[10]부터 시작, info[0]이 TYPE 바이트)
|
|
47
|
+
info = packet[10:]
|
|
48
|
+
|
|
49
|
+
# 2. 센서 신호 파싱 (OPT1 BIT2 활성화 시 - POS_SIGNAL 0x06)
|
|
50
|
+
if opt1 & 0x04:
|
|
51
|
+
data_type = info[0]
|
|
52
|
+
if data_type == 0x06:
|
|
53
|
+
# D1~D4 슬롯에서 Flow 및 ToF 데이터 추출 (Little Endian short)
|
|
54
|
+
self.state['flow_x'] = struct.unpack('<h', info[1:3])[0]
|
|
55
|
+
self.state['flow_y'] = struct.unpack('<h', info[3:5])[0]
|
|
56
|
+
self.state['tof'] = struct.unpack('<h', info[5:7])[0]
|
|
57
|
+
|
|
58
|
+
# 3. 비행 정보 파싱 (OPT1 BIT5 활성화 시 - 비행 정보 응답, TYPE=9)
|
|
59
|
+
if opt1 & 0x20:
|
|
60
|
+
# 기울기, 높이, 절대 위치(pos_x, pos_y) 추출
|
|
61
|
+
# info[0]=TYPE(9), info[1:3]=A1(roll), info[3:5]=A2(pitch),
|
|
62
|
+
# info[5:7]=H(height), info[7:9]=POS1(x), info[9:11]=POS2(y)
|
|
63
|
+
self.state['roll'] = struct.unpack('<h', info[1:3])[0]
|
|
64
|
+
self.state['pitch'] = struct.unpack('<h', info[3:5])[0]
|
|
65
|
+
self.state['height'] = struct.unpack('<h', info[5:7])[0]
|
|
66
|
+
self.state['pos_x'] = struct.unpack('<h', info[7:9])[0]
|
|
67
|
+
self.state['pos_y'] = struct.unpack('<h', info[9:11])[0]
|
|
68
|
+
|
|
69
|
+
return self.state
|
|
@@ -78,7 +78,13 @@ def yolo_decode(outputs, img_shape, input_shape, r, pad, threshold=0.4):
|
|
|
78
78
|
|
|
79
79
|
class TFLiteDetector:
|
|
80
80
|
def __init__(self, model_path: str):
|
|
81
|
+
# 초기화 실패 시 infer()에서 명확한 오류를 내기 위해 미리 None 설정
|
|
82
|
+
self.interpreter = None
|
|
83
|
+
self.input_details = None
|
|
84
|
+
self.output_details = None
|
|
85
|
+
|
|
81
86
|
# 윈도우 환경(TensorFlow)과 라즈베리파이(tflite-runtime) 자동 선택
|
|
87
|
+
tflite = None
|
|
82
88
|
try:
|
|
83
89
|
import tensorflow.lite as tflite
|
|
84
90
|
except ImportError:
|
|
@@ -87,14 +93,16 @@ class TFLiteDetector:
|
|
|
87
93
|
except ImportError:
|
|
88
94
|
print("❌ TFLite를 실행할 라이브러리가 없습니다.")
|
|
89
95
|
return
|
|
90
|
-
|
|
96
|
+
|
|
91
97
|
self.interpreter = tflite.Interpreter(model_path=model_path)
|
|
92
98
|
self.interpreter.allocate_tensors()
|
|
93
99
|
self.input_details = self.interpreter.get_input_details()
|
|
94
100
|
self.output_details = self.interpreter.get_output_details()
|
|
95
|
-
|
|
101
|
+
|
|
96
102
|
def infer(self, frame, decode_func, threshold=0.4):
|
|
97
103
|
"""edu.py에서 사용하는 명칭인 'infer'로 유지"""
|
|
104
|
+
if self.interpreter is None:
|
|
105
|
+
raise RuntimeError("TFLiteDetector: TFLite 라이브러리가 없어 초기화되지 않았습니다.")
|
|
98
106
|
ih, iw = self.input_details[0]['shape'][1:3]
|
|
99
107
|
img, r, pad = letterbox(frame, (ih, iw))
|
|
100
108
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: pyaidrone
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5
|
|
4
4
|
Summary: Library for AIDrone Products
|
|
5
5
|
Home-page: http://www.ir-brain.com
|
|
6
6
|
Author: IR-Brain
|
|
@@ -10,17 +10,6 @@ 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
|
|
24
13
|
|
|
25
14
|
## install pyaidrone
|
|
26
15
|
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="pyaidrone",
|
|
5
|
-
version="2.
|
|
5
|
+
version="2.5",
|
|
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 packet is not 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()
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
AIDRONE = 1
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
FRONT = 0
|
|
5
|
-
BACK = 1
|
|
6
|
-
RIGHT = 2
|
|
7
|
-
LEFT = 3
|
|
8
|
-
|
|
9
|
-
class DefLib:
|
|
10
|
-
|
|
11
|
-
@classmethod
|
|
12
|
-
def checksum(cls, packet):
|
|
13
|
-
len = packet[4]
|
|
14
|
-
sum = 0
|
|
15
|
-
for n in range(6, len):
|
|
16
|
-
sum += packet[n]
|
|
17
|
-
return sum & 0xFF
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@classmethod
|
|
21
|
-
def _print(cls, data):
|
|
22
|
-
for n in range(0, len(data)):
|
|
23
|
-
h = hex(data[n])
|
|
24
|
-
# print(h, end=" ")
|
|
25
|
-
print("")
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@classmethod
|
|
29
|
-
def constrain(cls, val , max, min):
|
|
30
|
-
if val > max:
|
|
31
|
-
val = max
|
|
32
|
-
if val < min:
|
|
33
|
-
val = min
|
|
34
|
-
return val
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@classmethod
|
|
38
|
-
def comp(cls, data):
|
|
39
|
-
data = data&0xFF
|
|
40
|
-
if data < 0:
|
|
41
|
-
return 256 + data
|
|
42
|
-
else:
|
|
43
|
-
return data
|
|
44
|
-
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
#AIDrone sensor 라이브러리 (26.01.15)
|
|
2
|
-
import struct
|
|
3
|
-
from pyaidrone.deflib import DefLib
|
|
4
|
-
|
|
5
|
-
class AIDroneSensor:
|
|
6
|
-
def __init__(self):
|
|
7
|
-
# 센서 데이터 및 비행 상태 통합 저장소
|
|
8
|
-
self.state = {
|
|
9
|
-
'battery': 0, # 배터리 잔량 (%)
|
|
10
|
-
'tof': 0, # VL53L01X 고도 센서 값 (mm)
|
|
11
|
-
'flow_x': 0, # PMW3901 광학 흐름 센서 X축 변화량
|
|
12
|
-
'flow_y': 0, # PMW3901 광학 흐름 센서 Y축 변화량
|
|
13
|
-
'pos_x': 0, # 이륙 지점 기준 X 좌표 (cm)
|
|
14
|
-
'pos_y': 0, # 이륙 지점 기준 Y 좌표 (cm)
|
|
15
|
-
'height': 0, # 드론이 계산한 현재 높이 (cm)
|
|
16
|
-
'roll': 0, # 현재 좌우 기울기
|
|
17
|
-
'pitch': 0, # 현재 앞뒤 기울기
|
|
18
|
-
'ready': False, # 비행 준비 상태
|
|
19
|
-
'emergency': False # 응급 정지 상태 여부
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
def parse(self, packet):
|
|
23
|
-
"""
|
|
24
|
-
pyaidrone의 parse.py에서 수신된 패킷을 분석하여 상태를 업데이트합니다.
|
|
25
|
-
"""
|
|
26
|
-
if packet is None or packet == "None":
|
|
27
|
-
return None
|
|
28
|
-
|
|
29
|
-
# 패킷 헤더 및 타입 확인 (0xA2: Output Packet)
|
|
30
|
-
if (packet[3] & 0xF0) == 0xA0:
|
|
31
|
-
# 1. 기본 상태 정보 추출 (배터리 및 옵션 비트)
|
|
32
|
-
opt1 = packet[6]
|
|
33
|
-
self.state['battery'] = packet[8]
|
|
34
|
-
self.state['emergency'] = bool(opt1 & 0x01)
|
|
35
|
-
self.state['ready'] = bool(opt1 & 0x02)
|
|
36
|
-
|
|
37
|
-
# INFO 데이터 구간 (packet[9]부터 시작)
|
|
38
|
-
info = packet[9:]
|
|
39
|
-
|
|
40
|
-
# 2. 센서 신호 파싱 (OPT1 BIT2 활성화 시 - POS_SIGNAL 0x06)
|
|
41
|
-
if opt1 & 0x04:
|
|
42
|
-
data_type = info[0]
|
|
43
|
-
if data_type == 0x06:
|
|
44
|
-
# D1~D4 슬롯에서 Flow 및 ToF 데이터 추출 (Little Endian short)
|
|
45
|
-
self.state['flow_x'] = struct.unpack('<h', info[1:3])[0]
|
|
46
|
-
self.state['flow_y'] = struct.unpack('<h', info[3:5])[0]
|
|
47
|
-
self.state['tof'] = struct.unpack('<h', info[5:7])[0]
|
|
48
|
-
|
|
49
|
-
# 3. 비행 정보 파싱 (OPT1 BIT5 활성화 시 - 비행 정보 응답)
|
|
50
|
-
if opt1 & 0x20:
|
|
51
|
-
# 기울기, 높이, 그리고 요청하신 절대 위치(pos_x, pos_y) 추출
|
|
52
|
-
self.state['roll'] = struct.unpack('<h', info[1:3])[0]
|
|
53
|
-
self.state['pitch'] = struct.unpack('<h', info[3:5])[0]
|
|
54
|
-
self.state['height'] = struct.unpack('<h', info[5:7])[0]
|
|
55
|
-
self.state['pos_x'] = struct.unpack('<h', info[7:9])[0]
|
|
56
|
-
self.state['pos_y'] = struct.unpack('<h', info[9:11])[0]
|
|
57
|
-
|
|
58
|
-
return self.state
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|