pyaidrone 2.0__tar.gz → 2.1__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
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pyaidrone
3
- Version: 2.0
3
+ Version: 2.1
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,397 @@
1
+ """
2
+ AIDrone ↔ MAVLink Bridge (GCS/QGroundControl 연동용)
3
+
4
+ 요구사항:
5
+ pip install pymavlink pyserial
6
+
7
+ 사용 예:
8
+ python aidrone_mavlink_bridge.py --port /dev/ttyACM0 --mav-in udp:0.0.0.0:14550 --mav-out udp:127.0.0.1:14551
9
+
10
+ 설명:
11
+ - MAVLink 입력(명령): COMMAND_LONG(ARM/TAKEOFF/LAND), MANUAL_CONTROL(조이스틱), HEARTBEAT
12
+ - MAVLink 출력(텔레메트리): HEARTBEAT, SYS_STATUS(배터리), ATTITUDE(roll/pitch/yaw), LOCAL_POSITION_NED(간단 위치), DISTANCE_SENSOR(ToF)
13
+ """
14
+
15
+ import argparse
16
+ import math
17
+ import time
18
+ import threading
19
+ from typing import Optional
20
+
21
+ from pymavlink import mavutil
22
+
23
+ # pyaidrone 기반
24
+ from pyaidrone.aiDrone import AIDrone # takeoff/landing/velocity/rotation/altitude/Open/Close :contentReference[oaicite:3]{index=3}
25
+ from pyaidrone.sensor import AIDroneSensor # 수신 패킷을 state로 파싱 :contentReference[oaicite:4]{index=4}
26
+ from pyaidrone.deflib import FRONT, BACK, RIGHT, LEFT # 방향 상수 :contentReference[oaicite:5]{index=5}
27
+
28
+
29
+ def clamp(v, lo, hi):
30
+ return lo if v < lo else hi if v > hi else v
31
+
32
+
33
+ class AIDroneMavlinkBridge:
34
+ """
35
+ pyaidrone(AIDrone)와 MAVLink(GCS)를 연결하는 브릿지.
36
+ - 드론 시리얼은 이 프로세스만 단독으로 엽니다.
37
+ - MAVLink는 UDP(권장)로 GCS와 연결합니다.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ serial_port: str,
43
+ mav_in: str,
44
+ mav_out: str,
45
+ sysid: int = 1,
46
+ compid: int = 1,
47
+ telemetry_hz: float = 10.0,
48
+ ):
49
+ self.serial_port = serial_port
50
+ self.sysid = sysid
51
+ self.compid = compid
52
+
53
+ # AIDrone + 센서 파서
54
+ self.sensor = AIDroneSensor()
55
+ self.last_state = self.sensor.state.copy()
56
+ self.last_state_ts = 0.0
57
+
58
+ self.drone = AIDrone(receiveCallback=self._on_drone_packet)
59
+
60
+ # MAVLink I/O
61
+ self.mav_in = mavutil.mavlink_connection(mav_in, source_system=sysid, source_component=compid)
62
+ self.mav_out = mavutil.mavlink_connection(mav_out, source_system=sysid, source_component=compid)
63
+
64
+ self._stop = False
65
+ self._armed = False # AIDrone가 명시적 arm API가 없어서 내부 상태로만 관리
66
+ self._last_hb = 0.0
67
+ self._telemetry_period = 1.0 / max(1.0, telemetry_hz)
68
+
69
+ # MANUAL_CONTROL 유실 시 자동 정지(안전)
70
+ self._last_manual_ts = 0.0
71
+ self.manual_timeout_s = 0.7 # 이 시간 동안 조이스틱 명령이 없으면 정지
72
+
73
+ # yaw 누적(센서에 yaw가 없으면 “추정치”로 MAVLink에만 표시)
74
+ self._yaw_est = 0.0
75
+ self._yaw_rate_last = 0.0
76
+ self._yaw_est_ts = time.time()
77
+
78
+ # ---------------------------
79
+ # Drone callback
80
+ # ---------------------------
81
+ def _on_drone_packet(self, packet):
82
+ st = self.sensor.parse(packet)
83
+ if st:
84
+ self.last_state = st
85
+ self.last_state_ts = time.time()
86
+
87
+ # ---------------------------
88
+ # Lifecycle
89
+ # ---------------------------
90
+ def start(self):
91
+ ok = self.drone.Open(self.serial_port)
92
+ if not ok:
93
+ raise RuntimeError("AIDrone serial open failed")
94
+
95
+ t_rx = threading.Thread(target=self._rx_loop, daemon=True)
96
+ t_tx = threading.Thread(target=self._tx_loop, daemon=True)
97
+ t_safe = threading.Thread(target=self._safety_loop, daemon=True)
98
+
99
+ t_rx.start()
100
+ t_tx.start()
101
+ t_safe.start()
102
+
103
+ print("[Bridge] running. Ctrl+C to stop.")
104
+ try:
105
+ while True:
106
+ time.sleep(0.2)
107
+ except KeyboardInterrupt:
108
+ self._stop = True
109
+ time.sleep(0.3)
110
+ finally:
111
+ try:
112
+ self.drone.Close()
113
+ except Exception:
114
+ pass
115
+
116
+ # ---------------------------
117
+ # MAVLink RX (GCS -> Drone)
118
+ # ---------------------------
119
+ def _rx_loop(self):
120
+ while not self._stop:
121
+ msg = self.mav_in.recv_match(blocking=True, timeout=0.5)
122
+ if msg is None:
123
+ continue
124
+
125
+ mtype = msg.get_type()
126
+ if mtype == "BAD_DATA":
127
+ continue
128
+
129
+ if mtype == "COMMAND_LONG":
130
+ self._handle_command_long(msg)
131
+ elif mtype == "MANUAL_CONTROL":
132
+ self._handle_manual_control(msg)
133
+ elif mtype == "HEARTBEAT":
134
+ # 필요시 링크 감지용으로만 사용
135
+ pass
136
+
137
+ def _ack_command(self, command: int, result: int):
138
+ self.mav_out.mav.command_ack_send(command, result, 0, 0, 0, 0)
139
+
140
+ def _handle_command_long(self, msg):
141
+ cmd = int(msg.command)
142
+
143
+ # ARM/DISARM: 실제 AIDrone에 arm 개념이 없으니 내부 플래그만 관리
144
+ if cmd == mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM:
145
+ arm = (msg.param1 >= 0.5)
146
+ self._armed = bool(arm)
147
+ self._ack_command(cmd, mavutil.mavlink.MAV_RESULT_ACCEPTED)
148
+ return
149
+
150
+ # TAKEOFF
151
+ if cmd == mavutil.mavlink.MAV_CMD_NAV_TAKEOFF:
152
+ try:
153
+ self.drone.takeoff()
154
+ self._armed = True
155
+ self._ack_command(cmd, mavutil.mavlink.MAV_RESULT_ACCEPTED)
156
+ except Exception:
157
+ self._ack_command(cmd, mavutil.mavlink.MAV_RESULT_FAILED)
158
+ return
159
+
160
+ # LAND
161
+ if cmd == mavutil.mavlink.MAV_CMD_NAV_LAND:
162
+ try:
163
+ self.drone.landing()
164
+ self._ack_command(cmd, mavutil.mavlink.MAV_RESULT_ACCEPTED)
165
+ except Exception:
166
+ self._ack_command(cmd, mavutil.mavlink.MAV_RESULT_FAILED)
167
+ return
168
+
169
+ # EMERGENCY STOP (지원하고 싶으면 MAV_CMD_DO_FLIGHTTERMINATION 등으로 매핑 가능)
170
+ if cmd == mavutil.mavlink.MAV_CMD_DO_FLIGHTTERMINATION:
171
+ try:
172
+ self.drone.emergency()
173
+ self._armed = False
174
+ self._ack_command(cmd, mavutil.mavlink.MAV_RESULT_ACCEPTED)
175
+ except Exception:
176
+ self._ack_command(cmd, mavutil.mavlink.MAV_RESULT_FAILED)
177
+ return
178
+
179
+ self._ack_command(cmd, mavutil.mavlink.MAV_RESULT_UNSUPPORTED)
180
+
181
+ def _handle_manual_control(self, msg):
182
+ """
183
+ MANUAL_CONTROL: 조이스틱 입력
184
+ x: -1000..1000 (pitch: 전/후)
185
+ y: -1000..1000 (roll: 좌/우)
186
+ z: 0..1000 (throttle)
187
+ r: -1000..1000 (yaw)
188
+ """
189
+ self._last_manual_ts = time.time()
190
+
191
+ # arming 안됐으면 제어 무시(안전)
192
+ if not self._armed:
193
+ return
194
+
195
+ x = int(msg.x) # forward/back
196
+ y = int(msg.y) # left/right
197
+ z = int(msg.z) # throttle
198
+ r = int(msg.r) # yaw
199
+
200
+ # 1) 전/후, 좌/우는 AIDrone.velocity(dir, vel 0~100)로 매핑
201
+ # AIDrone.velocity는 dir에 따라 부호를 내부에서 처리함(앞/뒤, 우/좌). :contentReference[oaicite:6]{index=6}
202
+ forward = clamp(x / 1000.0, -1.0, 1.0)
203
+ lateral = clamp(y / 1000.0, -1.0, 1.0)
204
+
205
+ # deadzone
206
+ dead = 0.12
207
+ if abs(forward) < dead:
208
+ forward = 0.0
209
+ if abs(lateral) < dead:
210
+ lateral = 0.0
211
+
212
+ vel_f = int(abs(forward) * 100)
213
+ vel_l = int(abs(lateral) * 100)
214
+
215
+ # 전/후
216
+ if vel_f > 0:
217
+ self.drone.velocity(FRONT if forward > 0 else BACK, vel_f)
218
+
219
+ # 좌/우: pyaidrone에서 RIGHT=2, LEFT=3 :contentReference[oaicite:7]{index=7}
220
+ if vel_l > 0:
221
+ self.drone.velocity(RIGHT if lateral > 0 else LEFT, vel_l)
222
+
223
+ # 둘 다 0이면 정지(velocity에 0 넣기)
224
+ if vel_f == 0 and vel_l == 0:
225
+ self.drone.velocity(FRONT, 0)
226
+
227
+ # 2) yaw: rotation(rot)으로 매핑 (상대 회전)
228
+ yaw_in = clamp(r / 1000.0, -1.0, 1.0)
229
+ if abs(yaw_in) >= 0.15:
230
+ # 입력 크기에 따라 작은 각도로 “자주” 회전
231
+ step_deg = int(yaw_in * 8) # 1회에 최대 ±8도
232
+ if step_deg != 0:
233
+ self.drone.rotation(step_deg)
234
+ self._yaw_rate_last = yaw_in
235
+
236
+ # 3) throttle: altitude(alt)로 매핑
237
+ # pyaidrone의 takeoff는 alt=70을 사용(단위는 cm로 보임) :contentReference[oaicite:8]{index=8}
238
+ # z는 0..1000 이므로 50~150cm로 스케일(원하는 범위로 조정 가능)
239
+ alt_cm = int(50 + (clamp(z / 1000.0, 0.0, 1.0) * 100)) # 50~150
240
+ try:
241
+ self.drone.altitude(alt_cm)
242
+ except Exception:
243
+ pass
244
+
245
+ # ---------------------------
246
+ # Safety: MANUAL 끊기면 정지
247
+ # ---------------------------
248
+ def _safety_loop(self):
249
+ while not self._stop:
250
+ if self._armed and (time.time() - self._last_manual_ts) > self.manual_timeout_s:
251
+ try:
252
+ # 정지
253
+ self.drone.velocity(FRONT, 0)
254
+ except Exception:
255
+ pass
256
+ time.sleep(0.1)
257
+
258
+ # ---------------------------
259
+ # MAVLink TX (Drone -> GCS)
260
+ # ---------------------------
261
+ def _tx_loop(self):
262
+ while not self._stop:
263
+ now = time.time()
264
+
265
+ # yaw 추정 업데이트(센서 yaw가 없으므로 r 입력 기반 아주 단순 적분)
266
+ dt = max(0.0, now - self._yaw_est_ts)
267
+ self._yaw_est_ts = now
268
+ # yaw_rate_last는 -1..1, 초당 약 30deg로 가정(표시용)
269
+ self._yaw_est += math.radians(30.0) * float(self._yaw_rate_last) * dt
270
+ # -pi..pi로 정규화
271
+ self._yaw_est = (self._yaw_est + math.pi) % (2 * math.pi) - math.pi
272
+ self._yaw_rate_last *= 0.85 # 서서히 감쇠
273
+
274
+ # 1Hz heartbeat
275
+ if now - self._last_hb >= 1.0:
276
+ self._send_heartbeat()
277
+ self._last_hb = now
278
+
279
+ # telemetry
280
+ st = self.last_state
281
+
282
+ self._send_sys_status(st)
283
+ self._send_attitude(st)
284
+ self._send_local_position(st)
285
+ self._send_distance_sensor(st)
286
+
287
+ time.sleep(self._telemetry_period)
288
+
289
+ def _send_heartbeat(self):
290
+ base_mode = 0
291
+ if self._armed:
292
+ base_mode |= mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED
293
+
294
+ self.mav_out.mav.heartbeat_send(
295
+ mavutil.mavlink.MAV_TYPE_QUADROTOR,
296
+ mavutil.mavlink.MAV_AUTOPILOT_GENERIC,
297
+ base_mode,
298
+ 0, # custom_mode
299
+ mavutil.mavlink.MAV_STATE_ACTIVE
300
+ )
301
+
302
+ def _send_sys_status(self, st: dict):
303
+ # battery: pyaidrone sensor는 battery(%)를 제공 :contentReference[oaicite:9]{index=9}
304
+ batt_rem = int(st.get("battery", 0))
305
+ batt_rem = clamp(batt_rem, 0, 100)
306
+
307
+ # 전압은 센서에서 직접 안 주므로 -1(unknown). 남은%만 전달.
308
+ self.mav_out.mav.sys_status_send(
309
+ 0, 0, 0, # sensors present/enabled/health
310
+ 0, # load
311
+ 0, # voltage_battery(mV) unknown
312
+ -1, # current_battery(cA) unknown
313
+ batt_rem,
314
+ 0, 0, 0, 0, 0, 0
315
+ )
316
+
317
+ def _send_attitude(self, st: dict):
318
+ # sensor.py: roll/pitch가 short로 들어오며 단위는 보드 정의(대개 degree 또는 0.1deg일 가능성)
319
+ # 여기서는 “degree”라고 가정하고 rad로 변환(필요시 스케일 조정)
320
+ roll_raw = float(st.get("roll", 0))
321
+ pitch_raw = float(st.get("pitch", 0))
322
+
323
+ # 스케일 추정: 만약 값이 너무 크면(예: 300~500) 0.1deg일 가능성 → 0.1 배
324
+ if abs(roll_raw) > 180 or abs(pitch_raw) > 180:
325
+ roll_raw *= 0.1
326
+ pitch_raw *= 0.1
327
+
328
+ roll = math.radians(roll_raw)
329
+ pitch = math.radians(pitch_raw)
330
+ yaw = float(self._yaw_est)
331
+
332
+ self.mav_out.mav.attitude_send(
333
+ int(time.time() * 1000),
334
+ roll, pitch, yaw,
335
+ 0.0, 0.0, 0.0
336
+ )
337
+
338
+ def _send_local_position(self, st: dict):
339
+ # sensor.py: pos_x/pos_y(cm), height(cm) 제공 :contentReference[oaicite:10]{index=10}
340
+ px_cm = float(st.get("pos_x", 0))
341
+ py_cm = float(st.get("pos_y", 0))
342
+ h_cm = float(st.get("height", 0))
343
+
344
+ x = px_cm / 100.0
345
+ y = py_cm / 100.0
346
+ z = -(h_cm / 100.0) # NED에서 z는 Down(+). 높이는 Up이므로 음수
347
+
348
+ # 속도는 센서에 없으니 0
349
+ self.mav_out.mav.local_position_ned_send(
350
+ int(time.time() * 1000),
351
+ x, y, z,
352
+ 0.0, 0.0, 0.0
353
+ )
354
+
355
+ def _send_distance_sensor(self, st: dict):
356
+ # tof(mm) 제공 :contentReference[oaicite:11]{index=11}
357
+ tof_mm = int(st.get("tof", 0))
358
+ if tof_mm <= 0:
359
+ return
360
+
361
+ # MAVLink DISTANCE_SENSOR: cm 단위로 쓰는 경우도 있지만 메시지는 mm 기반(min/max는 cm 단위 필드 아님)
362
+ # 여기서는 current_distance를 mm로 그대로 넣고, min/max는 적당히 설정
363
+ self.mav_out.mav.distance_sensor_send(
364
+ int(time.time() * 1000),
365
+ 50, # min_distance (cm) - 센서 최소
366
+ 2000, # max_distance (cm) - 센서 최대(대략)
367
+ tof_mm, # current_distance (mm)
368
+ mavutil.mavlink.MAV_DISTANCE_SENSOR_LASER,
369
+ 0, # id
370
+ mavutil.mavlink.MAV_SENSOR_ROTATION_PITCH_270, # 아래 방향
371
+ 0 # covariance
372
+ )
373
+
374
+
375
+ def main():
376
+ ap = argparse.ArgumentParser()
377
+ ap.add_argument("--port", required=True, help="AIDrone serial port (e.g., COM3 or /dev/ttyACM0)")
378
+ ap.add_argument("--mav-in", default="udp:0.0.0.0:14550", help="MAVLink input (GCS -> bridge)")
379
+ ap.add_argument("--mav-out", default="udp:127.0.0.1:14551", help="MAVLink output (bridge -> GCS)")
380
+ ap.add_argument("--sysid", type=int, default=1)
381
+ ap.add_argument("--compid", type=int, default=1)
382
+ ap.add_argument("--hz", type=float, default=10.0)
383
+ args = ap.parse_args()
384
+
385
+ bridge = AIDroneMavlinkBridge(
386
+ serial_port=args.port,
387
+ mav_in=args.mav_in,
388
+ mav_out=args.mav_out,
389
+ sysid=args.sysid,
390
+ compid=args.compid,
391
+ telemetry_hz=args.hz,
392
+ )
393
+ bridge.start()
394
+
395
+
396
+ if __name__ == "__main__":
397
+ main()
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pyaidrone
3
- Version: 2.0
3
+ Version: 2.1
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
 
@@ -3,6 +3,7 @@ setup.cfg
3
3
  setup.py
4
4
  pyaidrone/__init__.py
5
5
  pyaidrone/aiDrone.py
6
+ pyaidrone/aidrone_mavlink_bridge.py
6
7
  pyaidrone/deflib.py
7
8
  pyaidrone/edu.py
8
9
  pyaidrone/ikeyevent.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="pyaidrone",
5
- version="2.0",
5
+ version="2.1",
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 형식임을 지정
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
File without changes