pyaidrone 2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyaidrone
3
- Version: 2.1
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
@@ -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()
@@ -0,0 +1,192 @@
1
+ import cv2
2
+ import time
3
+ import numpy as np
4
+ import os
5
+ from pyaidrone.aiDrone import AIDrone
6
+ from pyaidrone.vision_ai import TFLiteDetector, yolo_decode, draw_box_xywh, largest_contour, contour_centroid
7
+ from pyaidrone.deflib import *
8
+
9
+ class EduAIDrone:
10
+ """
11
+ AI 교육을 위해 복잡한 기능을 단순화한 통합 API 클래스
12
+ """
13
+ def __init__(self, port="COM3", use_simulation=False, model_path=None, labels_path=None):
14
+ self.use_simulation = use_simulation
15
+ self.last_frame = None
16
+ self.height = 100
17
+ self.cap = None
18
+ self.stream_url = None # 수정: self.stream.url 오타 수정
19
+
20
+ # 1. 시뮬레이션 모드 전용 설정 (지연 로딩 적용)
21
+ if self.use_simulation:
22
+ try:
23
+ import rclpy
24
+ from rclpy.node import Node
25
+ from geometry_msgs.msg import Twist
26
+ from sensor_msgs.msg import Image
27
+ from cv_bridge import CvBridge
28
+
29
+ if not rclpy.ok(): rclpy.init()
30
+ self.node = Node('edu_drone_node')
31
+ self.cmd_vel_pub = self.node.create_publisher(Twist, '/cmd_vel', 10)
32
+ self.bridge = CvBridge()
33
+ self.sim_frame = None
34
+ self.image_sub = self.node.create_subscription(Image, '/camera', self._gz_image_cb, 10)
35
+ print("🎮 ROS2 Gazebo 모드 활성화됨")
36
+ except ImportError:
37
+ print("❌ ROS2 환경을 찾을 수 없습니다. 실기체 모드로 전환합니다.")
38
+ self.use_simulation = False
39
+
40
+ # 2. 하드웨어 드론 인터페이스 초기화
41
+ self.aidrone = AIDrone()
42
+ self.port = port
43
+
44
+ # 3. AI 탐지기 및 라벨 설정
45
+ self.detector = TFLiteDetector(model_path) if model_path else None
46
+ self.labels = []
47
+ if labels_path and os.path.exists(labels_path):
48
+ with open(labels_path, 'r', encoding='utf-8') as f:
49
+ self.labels = [line.strip() for line in f.readlines()]
50
+
51
+ def _gz_image_cb(self, msg):
52
+ """Gazebo 카메라 데이터 수신 콜백"""
53
+ self.sim_frame = self.bridge.imgmsg_to_cv2(msg, "bgr8")
54
+
55
+ def connect(self):
56
+ """드론 연결 (실기체인 경우 시리얼 오픈)"""
57
+ if self.use_simulation:
58
+ return True
59
+ return self.aidrone.Open(self.port)
60
+
61
+ def update_screen(self, window_name="AI Drone Edu"):
62
+ """영상 업데이트 (시뮬레이션 토픽 또는 실기체 MJPG 스트림)"""
63
+ if self.use_simulation:
64
+ import rclpy
65
+ rclpy.spin_once(self.node, timeout_sec=0)
66
+ if self.sim_frame is not None:
67
+ self.last_frame = cv2.resize(self.sim_frame, (640, 480))
68
+ return self.last_frame
69
+ else:
70
+ if self.cap is None: return None
71
+ ret, frame = self.cap.read()
72
+ if ret and frame is not None:
73
+ self.last_frame = cv2.resize(frame, (640, 480))
74
+ return self.last_frame
75
+ return None
76
+
77
+ def takeoff(self):
78
+ print("🚀 이륙합니다...")
79
+ if not self.use_simulation:
80
+ self.aidrone.takeoff()
81
+ time.sleep(2)
82
+
83
+ def land(self):
84
+ print("🛬 착륙합니다...")
85
+ if self.use_simulation:
86
+ from geometry_msgs.msg import Twist
87
+ self.cmd_vel_pub.publish(Twist())
88
+ else:
89
+ self.aidrone.landing()
90
+
91
+ def move(self, direction, speed=100):
92
+ if self.use_simulation:
93
+ from geometry_msgs.msg import Twist
94
+ msg = Twist()
95
+ val = speed / 100.0
96
+ if direction == 'front': msg.linear.x = val
97
+ elif direction == 'back': msg.linear.x = -val
98
+ elif direction == 'left': msg.linear.y = val
99
+ elif direction == 'right': msg.linear.y = -val
100
+ self.cmd_vel_pub.publish(msg)
101
+ else:
102
+ dir_map = {'front': FRONT, 'back': BACK, 'right': RIGHT, 'left': LEFT}
103
+ if direction in dir_map:
104
+ self.aidrone.velocity(dir_map[direction], speed)
105
+ self.aidrone.setOption(0x0F)
106
+
107
+ def turn(self, angle):
108
+ if self.use_simulation:
109
+ from geometry_msgs.msg import Twist
110
+ msg = Twist()
111
+ msg.angular.z = float(angle) * (np.pi / 180.0)
112
+ self.cmd_vel_pub.publish(msg)
113
+ else:
114
+ self.aidrone.rotation(angle)
115
+
116
+ def stop(self):
117
+ if self.use_simulation:
118
+ from geometry_msgs.msg import Twist
119
+ self.cmd_vel_pub.publish(Twist())
120
+ else:
121
+ self.aidrone.velocity(FRONT, 0)
122
+ self.aidrone.setOption(0x0F)
123
+
124
+ def find_color(self, color="red"):
125
+ if self.last_frame is None: return None
126
+ hsv = cv2.cvtColor(self.last_frame, cv2.COLOR_BGR2HSV)
127
+ ranges = {
128
+ "red": [(0, 150, 50), (10, 255, 255)],
129
+ "blue": [(100, 150, 50), (140, 255, 255)],
130
+ "green": [(40, 100, 50), (80, 255, 255)]
131
+ }
132
+ low, high = ranges.get(color, ranges["red"])
133
+ mask = cv2.inRange(hsv, np.array(low), np.array(high))
134
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
135
+ big_c = largest_contour(contours)
136
+ if big_c is not None:
137
+ return contour_centroid(big_c)
138
+ return None
139
+
140
+ def find_object(self, target_name, threshold=0.5):
141
+ if not self.detector or self.last_frame is None: return None
142
+ results = self.detector.infer(self.last_frame, yolo_decode)
143
+ for res in results:
144
+ name = self.labels[res.class_id] if self.labels else f"ID:{res.class_id}"
145
+ if name == target_name and res.score > threshold:
146
+ return ((res.box[0] + res.box[2]) / 2, (res.box[1] + res.box[3]) / 2)
147
+ return None
148
+
149
+ def read_qr(self):
150
+ if self.last_frame is None: return None
151
+ data, _, _ = cv2.QRCodeDetector().detectAndDecode(self.last_frame)
152
+ return data if data else None
153
+
154
+ def follow_target(self, error_x, error_y):
155
+ """오차값을 보고 드론을 자동으로 회전 및 고도 조절"""
156
+ yaw_step = int(error_x * 0.15)
157
+
158
+ if self.use_simulation:
159
+ self.turn(yaw_step)
160
+ else:
161
+ self.aidrone.rotation(yaw_step)
162
+
163
+ throttle_change = int(error_y * 0.2)
164
+ self.height = max(50, min(150, self.height + throttle_change))
165
+
166
+ if self.use_simulation:
167
+ from geometry_msgs.msg import Twist
168
+ msg = Twist()
169
+ # 고도 조절 시 기존 회전 성분 유지 필요 시 여기에 추가 로직 필요
170
+ msg.linear.z = float(throttle_change / 100.0)
171
+ self.cmd_vel_pub.publish(msg)
172
+ else:
173
+ self.aidrone.altitude(self.height)
174
+
175
+ def start_stream(self, url):
176
+ self.stream_url = url
177
+ self.cap = cv2.VideoCapture(url)
178
+ return self.cap.isOpened()
179
+
180
+ def reconnect_stream(self):
181
+ """스트림 재연결 (중복 제거 및 통합)"""
182
+ if self.stream_url is None: return False
183
+ if self.cap: self.cap.release()
184
+ self.cap = None
185
+ time.sleep(1)
186
+ return self.start_stream(self.stream_url)
187
+
188
+ def save_image(self, frame, folder="captured_images"):
189
+ if not os.path.exists(folder): os.makedirs(folder)
190
+ timestamp = time.strftime("%H%M%S")
191
+ filename = f"{folder}/target_{timestamp}.jpg"
192
+ cv2.imwrite(filename, frame)
@@ -142,8 +142,12 @@ class VisionTracker:
142
142
 
143
143
  roi = (x_roi, y_roi, w_roi, h_roi)
144
144
 
145
- # 🔧 5. 추적기 생성 및 초기화
146
- self.tracker = cv2.TrackerCSRT_create()
145
+ # 🔧 5. 추적기 생성 및 초기화
146
+ if hasattr(cv2, 'legacy'):
147
+ self.tracker = cv2.legacy.TrackerCSRT_create()
148
+ else:
149
+ self.tracker = cv2.TrackerCSRT_create()
150
+
147
151
  success = self.tracker.init(frame, roi)
148
152
 
149
153
  if success:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyaidrone
3
- Version: 2.1
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
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="pyaidrone",
5
- version="2.1",
5
+ version="2.2",
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,221 +0,0 @@
1
- import serial
2
- import binascii
3
- import math
4
- from time import sleep
5
- import random
6
- from operator import eq
7
- from threading import Thread
8
- from serial.tools.list_ports import comports
9
- from pyaidrone.parse import *
10
- from pyaidrone.packet import *
11
- from pyaidrone.deflib import *
12
-
13
-
14
- class AIDrone(Parse, Packet):
15
- def __init__(self, receiveCallback = None):
16
- self.serial = None
17
- self.isThreadRun = False
18
- self.parse = Parse(AIDRONE)
19
- self.makepkt = Packet(AIDRONE)
20
- self.receiveCallback = receiveCallback
21
- self.makepkt.clearPacket()
22
- self.posX = 0
23
- self.posY = 0
24
- self.rot = 0
25
- # --- 영상 스트리밍 관련 기본값 ---
26
- self.stream_host = "192.168.4.1"
27
- self.stream_port = 80
28
- self.stream_path = "/?action=stream"
29
- self._cap = None
30
-
31
- def receiveHandler(self):
32
- while self.isThreadRun:
33
- readData = self.serial.read(self.serial.in_waiting or 1)
34
- packet = self.parse.packetArrange(readData)
35
- if not eq(packet, "None"):
36
- if self.receiveCallback != None:
37
- self.receiveCallback(packet)
38
- self.serial.write(self.makepkt.getPacket())
39
-
40
-
41
- def Open(self, portName = "None"):
42
- if eq(portName, "None"):
43
- nodes = comports()
44
- for node in nodes:
45
- if "CH340" in node.description:
46
- portName = node.device
47
-
48
- if eq(portName, "None"):
49
- print("Can't find Serial Port")
50
- exit()
51
- return False
52
- try:
53
- self.serial = serial.Serial(port=portName, baudrate=19200, timeout=1)
54
- if self.serial.isOpen():
55
- self.isThreadRun = True
56
- self.thread = Thread(target=self.receiveHandler, args=(), daemon=True)
57
- self.thread.start()
58
- print("Connected to", portName)
59
- return True
60
- else:
61
- print("Can't open " + portName)
62
- exit()
63
- return False
64
- except:
65
- print("Can't open " + portName)
66
- exit()
67
- return False
68
-
69
-
70
- def Close(self):
71
- self.isThreadRun = False
72
- sleep(0.2)
73
- pkt = self.makepkt.getPacket()
74
- if (pkt[15]&0x80) == 0x80:
75
- self.makepkt.clearPacket()
76
- self.setOption(0x8000)
77
- self.serial.write(self.makepkt.getPacket())
78
- sleep(0.2)
79
- self.serial.write(self.makepkt.clearPacket())
80
- sleep(0.2)
81
- if self.serial != None:
82
- if self.serial.isOpen() == True:
83
- self.serial.close()
84
-
85
- def setOption(self, option):
86
- data = option.to_bytes(2, byteorder="little", signed=False)
87
- self.makepkt.makePacket(14, data)
88
-
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
- alt = 0x2F
95
- data = alt.to_bytes(2, byteorder="little", signed=False)
96
- self.setOption(0x2F)
97
-
98
-
99
- def landing(self):
100
- alt = 0
101
- data = alt.to_bytes(2, byteorder="little", signed=False)
102
- self.makepkt.makePacket(12, data)
103
-
104
-
105
- def altitude(self, alt):
106
- data = alt.to_bytes(2, byteorder="little", signed=False)
107
- self.makepkt.makePacket(12, data)
108
-
109
-
110
- def velocity(self, dir=0, vel=100):
111
- if dir > 3:
112
- return
113
- if dir==1 or dir==3:
114
- vel *= -1;
115
- data = vel.to_bytes(2, byteorder="little", signed=True)
116
- if dir==0 or dir==1:
117
- self.makepkt.makePacket(8, data)
118
- else:
119
- self.makepkt.makePacket(6, data)
120
- self.setOption(0x0F)
121
-
122
-
123
- def move(self, dir=0, dist=100):
124
- if dir > 3:
125
- return
126
- if dir==1 or dir==3:
127
- dist *= -1;
128
- if dir==0 or dir==1:
129
- self.posX += dist
130
- data = self.posX.to_bytes(2, byteorder="little", signed=True)
131
- self.makepkt.makePacket(8, data)
132
- else:
133
- self.posY += dist
134
- data = self.posY.to_bytes(2, byteorder="little", signed=True)
135
- self.makepkt.makePacket(6, data)
136
- self.setOption(0x2F)
137
-
138
-
139
- def rotation(self, rot=90):
140
- self.rot += rot
141
- data = self.rot.to_bytes(2, byteorder="little", signed=True)
142
- self.makepkt.makePacket(10, data)
143
-
144
-
145
-
146
- def motor(self, what, speed):
147
- speed = DefLib.constrain(speed, 100, 0)
148
- data = speed.to_bytes(2, byteorder="little", signed=True)
149
- self.makepkt.makePacket(what*2+6, data)
150
- self.setOption(0x8000)
151
-
152
-
153
- def emergency(self):
154
- self.setOption(0x00)
155
- self.serial.write(self.makepkt.getPacket())
156
-
157
- # aiDrone.py — AIDrone 클래스 안에 아래 3개 메서드 추가
158
- def setStreamAddress(self, host: str, port: int = 80, path: str = "/?action=stream"):
159
- """MJPG-Streamer 주소 구성 요소를 저장합니다."""
160
- if not isinstance(host, str) or len(host.strip()) == 0:
161
- raise ValueError("host가 올바르지 않습니다.")
162
- self.stream_host = host.strip()
163
- self.stream_port = int(port)
164
- if not path.startswith("/"):
165
- path = "/" + path
166
- self.stream_path = path
167
- return self._build_stream_url()
168
-
169
- def _build_stream_url(self):
170
- """내부 사용: 저장된 host/port/path로 URL 생성."""
171
- if self.stream_port in (80, None):
172
- return f"http://{self.stream_host}{self.stream_path}"
173
- return f"http://{self.stream_host}:{self.stream_port}{self.stream_path}"
174
-
175
- def streamon(self, host: str = None, port: int = None, path: str = None, return_url: bool = False):
176
- """
177
- MJPG-Streamer 스트림을 엽니다.
178
- - 인자를 주면 해당 값으로 주소를 덮어쓰고, 안 주면 저장된 값 사용.
179
- - return_url=True 이면 URL 문자열만 반환(캡처는 열지 않음).
180
- - 기본 URL 예: http://192.168.4.1/?action=stream
181
- """
182
- if host is not None or port is not None or path is not None:
183
- self.setStreamAddress(host or self.stream_host,
184
- self.stream_port if port is None else port,
185
- self.stream_path if path is None else path)
186
-
187
- url = self._build_stream_url()
188
- if return_url:
189
- return url
190
-
191
- # OpenCV는 여기서만 임포트(필요할 때만)
192
- try:
193
- import cv2 as cv
194
- except Exception as e:
195
- raise RuntimeError(f"OpenCV(cv2) 임포트 실패: {e}")
196
-
197
- # 이전 cap이 열려 있으면 정리
198
- self.streamoff()
199
-
200
- self._cap = cv.VideoCapture(url)
201
- if not self._cap.isOpened():
202
- self._cap.release()
203
- self._cap = None
204
- raise RuntimeError(f"스트림 열기 실패: {url}")
205
- return self._cap
206
-
207
- def streamoff(self):
208
- """열려 있는 스트림을 닫습니다."""
209
- if self._cap is not None:
210
- try:
211
- self._cap.release()
212
- except Exception:
213
- pass
214
- self._cap = None
215
-
216
-
217
-
218
-
219
-
220
-
221
-
@@ -1,188 +0,0 @@
1
- import cv2
2
- import time
3
- import numpy as np
4
- from pyaidrone.aiDrone import AIDrone
5
- from pyaidrone.vision_ai import TFLiteDetector, yolo_decode, draw_box_xywh, largest_contour, contour_centroid
6
- from pyaidrone.deflib import *
7
-
8
- class EduAIDrone:
9
- """
10
- AI 교육을 위해 복잡한 기능을 단순화한 통합 API 클래스
11
- """
12
- def __init__(self, port="COM3", model_path=None, labels_path=None):
13
- # 드론 객체 및 기본 설정
14
- self.aidrone = AIDrone()
15
- self.port = port
16
- self.detector = TFLiteDetector(model_path) if model_path else None
17
- self.labels = []
18
- if labels_path:
19
- with open(labels_path, 'r', encoding='utf-8') as f:
20
- self.labels = [line.strip() for line in f.readlines()]
21
-
22
- self.cap = None
23
- self.last_frame = None
24
- self.height = 100 # 기본 유지 고도
25
- self.stream_url = None
26
-
27
- # --- 연결 및 영상 관리 ---
28
- def connect(self):
29
- """드론 연결 및 초기 세팅"""
30
- if self.aidrone.Open(self.port):
31
- self.aidrone.setOption(0)
32
- print(f"✅ 연결 성공: {self.port}")
33
- return True
34
- return False
35
-
36
- def start_stream(self, url="http://192.168.4.1/?action=stream"):
37
- self.stream_url = url # ← URL 저장
38
- self.cap = cv2.VideoCapture(url)
39
-
40
- if self.cap.isOpened():
41
- self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # ← 버퍼 설정
42
- print(f"✅ 스트림 연결: {url}")
43
- return True
44
- else:
45
- print(f"❌ 스트림 연결 실패: {url}")
46
- return False
47
-
48
- def update_screen(self, window_name="AI Drone Edu"):
49
- """안전한 프레임 업데이트"""
50
- # 1. cap이 None인지 확인
51
- if self.cap is None:
52
- print("⚠️ 스트림이 연결되지 않았습니다")
53
- return None
54
-
55
- # 2. AttributeError 처리
56
- try:
57
- ret, frame = self.cap.read()
58
- except AttributeError:
59
- print("❌ cap.read() 에러")
60
- self.cap = None
61
- return None
62
-
63
- # 3. 프레임 검증
64
- if not ret or frame is None:
65
- return None
66
-
67
- # 4. 안전한 리사이즈
68
- try:
69
- if frame.size == 0: # ← 크기 체크 추가!
70
- return None
71
-
72
- self.last_frame = cv2.resize(frame, (640, 480))
73
- return self.last_frame
74
- except Exception as e:
75
- print(f"⚠️ 리사이즈 에러: {e}")
76
- return None
77
-
78
- # --- 단순 제어 명령어 ---
79
- def takeoff(self):
80
- print("🚀 이륙합니다..."); self.aidrone.takeoff(); time.sleep(2)
81
-
82
- def land(self):
83
- print("🛬 착륙합니다..."); self.aidrone.landing()
84
-
85
- def move(self, direction, speed=100):
86
- """방향: 'front', 'back', 'left', 'right'"""
87
- dir_map = {'front': FRONT, 'back': BACK, 'right': RIGHT, 'left': LEFT}
88
- if direction in dir_map:
89
- self.aidrone.velocity(dir_map[direction], speed)
90
-
91
- def set_height(self, cm):
92
- """고도 설정 (50~150cm 추천)"""
93
- self.height = max(50, min(150, cm))
94
- self.aidrone.altitude(self.height)
95
-
96
- def turn(self, angle):
97
- """회전: 양수(우회전), 음수(좌회전)"""
98
- self.aidrone.rotation(angle)
99
-
100
- def stop(self):
101
- """모든 이동 정지 (호버링)"""
102
- self.aidrone.velocity(FRONT, 0)
103
- self.aidrone.velocity(RIGHT, 0)
104
-
105
- # --- AI 인지 기능 ---
106
- def find_color(self, color="red"):
107
- """색상을 찾아 화면에 표시하고 좌표 반환"""
108
- if self.last_frame is None: return None
109
- hsv = cv2.cvtColor(self.last_frame, cv2.COLOR_BGR2HSV)
110
-
111
- # 교육용 프리셋
112
- ranges = {
113
- "red": [(0, 150, 50), (10, 255, 255)],
114
- "blue": [(100, 150, 50), (140, 255, 255)],
115
- "green": [(40, 100, 50), (80, 255, 255)]
116
- }
117
- low, high = ranges.get(color, ranges["red"])
118
- mask = cv2.inRange(hsv, np.array(low), np.array(high))
119
- contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
120
- big_c = largest_contour(contours)
121
-
122
- if big_c is not None:
123
- cv2.drawContours(self.last_frame, [big_c], -1, (0, 255, 0), 2)
124
- return contour_centroid(big_c)
125
- return None
126
-
127
- def find_object(self, target_name, threshold=0.5):
128
- """YOLO 모델로 사물 찾기 (통합된 vision_ai 로직 활용)"""
129
- if not self.detector or self.last_frame is None: return None
130
-
131
- # 1. vision_ai에 내장된 정식 yolo_decode를 사용하여 추론
132
- results = self.detector.infer(self.last_frame, yolo_decode)
133
-
134
- # 2. 결과 분석
135
- for res in results:
136
- # 라벨 리스트가 있다면 이름으로 비교, 없으면 ID로 비교
137
- name = self.labels[res.class_id] if self.labels else f"ID:{res.class_id}"
138
-
139
- if name == target_name and res.score > threshold:
140
- # 3. 화면에 인식 결과 그리기 (xyxy -> xywh 변환 후 그리기)
141
- x1, y1, x2, y2 = res.box
142
- w, h = x2 - x1, y2 - y1
143
- draw_box_xywh(self.last_frame, (x1, y1, w, h), label=f"{name} {int(res.score*100)}%")
144
-
145
- # 4. 물체의 중심 좌표 반환 (학생들이 제어에 사용하기 위함)
146
- return ((x1 + x2) / 2, (y1 + y2) / 2)
147
-
148
- return None
149
-
150
- def read_qr(self):
151
- """QR 코드 텍스트 읽기"""
152
- if self.last_frame is None: return None
153
- data, _, _ = cv2.QRCodeDetector().detectAndDecode(self.last_frame)
154
- return data if data else None
155
-
156
- # 오차값을 바탕으로 드론이 알아서 회전하고 고도를 조절
157
- def follow_target(self, error_x, error_y):
158
- """오차값을 보고 드론을 자동으로 회전 및 고도 조절"""
159
- # 1. 좌우 회전 (Yaw) 제어
160
- yaw = int(error_x * 0.15)
161
- self.aidrone.rotation(yaw)
162
-
163
- # 2. 상하 고도 (Throttle) 제어
164
- throttle_change = int(error_y * 0.2)
165
- self.height = max(50, min(150, self.height + throttle_change))
166
- self.aidrone.altitude(self.height)
167
-
168
- # 조건이 맞으면 사진을 저장.
169
- def save_image(self, frame, folder="captured_images"):
170
- """사진을 저장하고 기존 폴더가 없으면 생성"""
171
- import os
172
- if not os.path.exists(folder): os.makedirs(folder)
173
-
174
- timestamp = time.strftime("%H%M%S")
175
- filename = f"{folder}/target_{timestamp}.jpg"
176
- cv2.imwrite(filename, frame)
177
-
178
- def reconnect_stream(self):
179
- """스트림 재연결"""
180
- if self.stream_url is None:
181
- return False
182
-
183
- if self.cap:
184
- self.cap.release()
185
- self.cap = None
186
-
187
- time.sleep(1)
188
- return self.start_stream(self.stream_url)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes