agrotechsimapi 1.0.0__py3-none-any.whl
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.
- agrotechsimapi/__init__.py +10 -0
- agrotechsimapi/client.py +374 -0
- agrotechsimapi/high_level_client.py +695 -0
- agrotechsimapi/pid.py +59 -0
- agrotechsimapi/signal_utils.py +7 -0
- agrotechsimapi/utils/__init__.py +7 -0
- agrotechsimapi/utils/aruco_marker_recognizer.py +132 -0
- agrotechsimapi/utils/recognition_setting.py +28 -0
- agrotechsimapi/utils/utils.py +48 -0
- agrotechsimapi/utils/vision.py +168 -0
- agrotechsimapi/video_pb2.py +40 -0
- agrotechsimapi/video_pb2_grpc.py +97 -0
- agrotechsimapi-1.0.0.dist-info/METADATA +443 -0
- agrotechsimapi-1.0.0.dist-info/RECORD +19 -0
- agrotechsimapi-1.0.0.dist-info/WHEEL +5 -0
- agrotechsimapi-1.0.0.dist-info/top_level.txt +2 -0
- modules/camera_driver.py +42 -0
- modules/input_driver.py +133 -0
- modules/lidar_driver.py +57 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
from inavmspapi import MultirotorControl
|
|
2
|
+
from inavmspapi.transmitter import TCPTransmitter
|
|
3
|
+
from inavmspapi.msp_codes import MSPCodes
|
|
4
|
+
from agrotechsimapi.client import SimClient
|
|
5
|
+
|
|
6
|
+
from agrotechsimapi.pid import PID
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
|
|
9
|
+
from agrotechsimapi.utils.utils import LoopingTimer, sim_to_api_distance, vel_to_rc_signal
|
|
10
|
+
from agrotechsimapi.utils.vision import process_aruco, process_blob, resolution_changes
|
|
11
|
+
|
|
12
|
+
from transforms3d.euler import quat2euler
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
import math
|
|
16
|
+
import threading # Добавлен импорт
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HighLevelSimClient:
|
|
28
|
+
def __init__(self):
|
|
29
|
+
|
|
30
|
+
self.__alt_pid = PID(2.5, 0.015, 7.5)
|
|
31
|
+
|
|
32
|
+
self._odom = (0.0, 0.0)
|
|
33
|
+
|
|
34
|
+
self._altitude = 0.0
|
|
35
|
+
|
|
36
|
+
self._target_height = 0.0
|
|
37
|
+
|
|
38
|
+
self._armed_flag = False
|
|
39
|
+
self._poshold_flag = False
|
|
40
|
+
|
|
41
|
+
self._sim_img = None
|
|
42
|
+
self._blob_img = None
|
|
43
|
+
self._aruco_img = None
|
|
44
|
+
self._aruco_data = []
|
|
45
|
+
self._camera_pose_aruco_data = []
|
|
46
|
+
self._blob_data = []
|
|
47
|
+
|
|
48
|
+
self._odom0_xy = (0.0, 0.0) # (x0, y0) в мировой СК на момент «сброса одометрии»
|
|
49
|
+
|
|
50
|
+
self._sim_kinematics = None
|
|
51
|
+
|
|
52
|
+
self._sim_ultrasonic = None
|
|
53
|
+
|
|
54
|
+
# Блокировка для безопасного доступа к клиенту из нескольких потоков
|
|
55
|
+
self._client_lock = threading.Lock()
|
|
56
|
+
|
|
57
|
+
print("process started")
|
|
58
|
+
|
|
59
|
+
def connect(self, ip, port):
|
|
60
|
+
self.__HOST = ip
|
|
61
|
+
self.__SIM_PORT = 8080
|
|
62
|
+
self.__TCP_PORT = 5762
|
|
63
|
+
self.__TCP_ADDRESS = (self.__HOST, self.__TCP_PORT)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
self.__tcp_transmitter = TCPTransmitter(self.__TCP_ADDRESS)
|
|
68
|
+
self.__tcp_transmitter.connect()
|
|
69
|
+
self._control = MultirotorControl(self.__tcp_transmitter)
|
|
70
|
+
time.sleep(2)
|
|
71
|
+
|
|
72
|
+
# Инициализируем клиент с блокировкой
|
|
73
|
+
with self._client_lock:
|
|
74
|
+
self._client = SimClient(address=self.__HOST, port=self.__SIM_PORT)
|
|
75
|
+
|
|
76
|
+
self._height_timer = LoopingTimer(interval=1/50, callback=self.calculate_height_rc, name="height_timer")
|
|
77
|
+
self._rc_timer = LoopingTimer(interval=1/50, callback=self.transmit_rc_to_sim, name="rc_timer")
|
|
78
|
+
self._sim_kinematics_timer = LoopingTimer(interval=1/50, callback=self.sim_kinematics_callback, name="sim_kinematics_timer")
|
|
79
|
+
self._image_processing_timer = LoopingTimer(interval=1/10, callback=self.image_processing_callback, name="image_processing_timer") #new
|
|
80
|
+
|
|
81
|
+
self.sim_kinematics_callback()
|
|
82
|
+
|
|
83
|
+
self.initDrone()
|
|
84
|
+
|
|
85
|
+
self._sim_kinematics_timer.start()
|
|
86
|
+
self._height_timer.start()
|
|
87
|
+
self._rc_timer.start()
|
|
88
|
+
self._image_processing_timer.start()
|
|
89
|
+
|
|
90
|
+
time.sleep(1)
|
|
91
|
+
self._armed_flag = True
|
|
92
|
+
self._arm_data = 2000
|
|
93
|
+
time.sleep(1)
|
|
94
|
+
self._poshold_flag = True
|
|
95
|
+
self._nav_mode = 1500
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def disconnect(self):
|
|
99
|
+
self._sim_kinematics_timer.stop()
|
|
100
|
+
self._height_timer.stop()
|
|
101
|
+
self._rc_timer.stop()
|
|
102
|
+
self._image_processing_timer.stop() #new
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _world_xy(self, kin=None) -> tuple[float, float]:
|
|
106
|
+
"""Мировые координаты XY в твоей API-конвенции."""
|
|
107
|
+
if kin is None:
|
|
108
|
+
kin = self.get_sim_kinematics()
|
|
109
|
+
x_w = sim_to_api_distance(kin["location"][0])
|
|
110
|
+
y_w = sim_to_api_distance(kin["location"][1])
|
|
111
|
+
return x_w, y_w
|
|
112
|
+
|
|
113
|
+
def _odom_xy(self, kin=None) -> tuple[float, float]:
|
|
114
|
+
"""Одометрия (XY), отн. сохранённого нуля. Если ноль не задан — возвращаем мировые XY."""
|
|
115
|
+
x_w, y_w = self._world_xy(kin)
|
|
116
|
+
if self._odom0_xy == (0.0, 0.0):
|
|
117
|
+
return x_w, y_w
|
|
118
|
+
x0, y0 = self._odom0_xy
|
|
119
|
+
return x_w - x0, y_w - y0
|
|
120
|
+
|
|
121
|
+
def getHeightRange(self):
|
|
122
|
+
return self._altitude
|
|
123
|
+
|
|
124
|
+
def getHeightBarometer(self):
|
|
125
|
+
return self._altitude
|
|
126
|
+
|
|
127
|
+
def getArm(self):
|
|
128
|
+
return self._armed_flag
|
|
129
|
+
|
|
130
|
+
def setZeroOdomOpticflow(self) -> bool:
|
|
131
|
+
"""
|
|
132
|
+
Запомнить текущие мировые координаты (x0,y0) как ноль одометрии.
|
|
133
|
+
Дальше frame='odom' трактуется как координаты относительно этого нуля.
|
|
134
|
+
"""
|
|
135
|
+
kin = self.get_sim_kinematics()
|
|
136
|
+
if kin is None:
|
|
137
|
+
raise RuntimeError("Нет кэша кинематики: setZeroOdomOpticflow() вызван слишком рано")
|
|
138
|
+
self._odom0_xy = self._world_xy(kin)
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
def getUltrasonic(self):
|
|
142
|
+
return self._sim_ultrasonic
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def calculate_height_rc(self):
|
|
147
|
+
self._altitude = sim_to_api_distance(self.get_sim_kinematics()["location"][2])
|
|
148
|
+
|
|
149
|
+
error = self._target_height - self._altitude
|
|
150
|
+
# print(f"target height: {self._target_height}, error: {error}")
|
|
151
|
+
|
|
152
|
+
self.__alt_pid.update_control(error)
|
|
153
|
+
alt_rc_output = self.__alt_pid.get_control()
|
|
154
|
+
|
|
155
|
+
throttle_output = self.clamp_rc(1500 + alt_rc_output * 100)
|
|
156
|
+
|
|
157
|
+
if self._armed_flag and self._poshold_flag:
|
|
158
|
+
self._throttle_data = throttle_output
|
|
159
|
+
|
|
160
|
+
def getRPY(self):
|
|
161
|
+
kin = self.get_sim_kinematics()
|
|
162
|
+
qx, qy, qz, qw = kin["orientation"]
|
|
163
|
+
roll, pitch, yaw = quat2euler((qw, qx, qy, qz), axes='sxyz')
|
|
164
|
+
return [roll, pitch, yaw]
|
|
165
|
+
|
|
166
|
+
def go_to_xy(self, frame: str, x: float, y: float) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Блокирующий полёт в точку.
|
|
169
|
+
frame: 'odom'|'map' (абсолют) или 'base_link'|'body' (относительный сдвиг в корпусной СК: x вперёд, y влево).
|
|
170
|
+
Возвращает True (достиг) / False (таймаут).
|
|
171
|
+
"""
|
|
172
|
+
# ---- алиасы кадров ----
|
|
173
|
+
if frame == "odom":
|
|
174
|
+
use_body_shift = False
|
|
175
|
+
use_odom_frame = True
|
|
176
|
+
elif frame == "base_link":
|
|
177
|
+
use_body_shift = True
|
|
178
|
+
use_odom_frame = False
|
|
179
|
+
else:
|
|
180
|
+
raise ValueError("frame must be 'odom' or 'base_link'")
|
|
181
|
+
|
|
182
|
+
# ===== ХАРДКОД НАСТРОЕК =====
|
|
183
|
+
PERIOD = 1/50.0 # цикл управления, c
|
|
184
|
+
X_THR = 0.2 # допуск по оси вперёд (м)
|
|
185
|
+
Y_THR = 0.2 # допуск по оси влево (м)
|
|
186
|
+
RC_PER_MPS = 100.0 # м/с -> PWM вокруг 1500
|
|
187
|
+
VMAX_AXIS = 0.3 # ограничение скорости на ось, м/с
|
|
188
|
+
|
|
189
|
+
K_LIMIT = 0.5
|
|
190
|
+
|
|
191
|
+
T_MIN = 1.0 # минимум таймаута, c
|
|
192
|
+
T_MAX = 30.0 # максимум таймаута, c
|
|
193
|
+
# PID-коэффициенты по осям (скорости в м/с)
|
|
194
|
+
PITCH_KP, PITCH_KI, PITCH_KD = 0.05, 0.0001, 0.1 # вперёд (err_x) 0.05, 0.0002, 0.1
|
|
195
|
+
ROLL_KP, ROLL_KI, ROLL_KD = 0.05, 0.0001, 0.1 # влево (err_y) 0.05, 0.0002, 0.1
|
|
196
|
+
|
|
197
|
+
# PITCH_KP, PITCH_KI, PITCH_KD = 0.15, 0.0, 0.0 # вперёд (err_x)
|
|
198
|
+
# ROLL_KP, ROLL_KI, ROLL_KD = 0.15, 0.0, 0.0 # влево (err_y)
|
|
199
|
+
|
|
200
|
+
FF_PITCH = FF_ROLL = 0.105
|
|
201
|
+
|
|
202
|
+
# PITCH_I_LIMIT = K_LIMIT * VMAX_AXIS / PITCH_KI
|
|
203
|
+
# ROLL_I_LIMIT = K_LIMIT * VMAX_AXIS / ROLL_KI
|
|
204
|
+
|
|
205
|
+
PITCH_I_LIMIT = None
|
|
206
|
+
ROLL_I_LIMIT = None
|
|
207
|
+
# ===========================
|
|
208
|
+
|
|
209
|
+
# --- PIDs ---
|
|
210
|
+
pid_pitch = PID(PITCH_KP, PITCH_KI, PITCH_KD, max_control=VMAX_AXIS, i_limit=PITCH_I_LIMIT)
|
|
211
|
+
pid_roll = PID(ROLL_KP, ROLL_KI, ROLL_KD, max_control=VMAX_AXIS, i_limit=ROLL_I_LIMIT)
|
|
212
|
+
pid_pitch.reset(); pid_roll.reset()
|
|
213
|
+
first_iter = True
|
|
214
|
+
|
|
215
|
+
mono = time.monotonic
|
|
216
|
+
next_t = mono()
|
|
217
|
+
|
|
218
|
+
# --- текущая поза (мировая) ---
|
|
219
|
+
kin = self.get_sim_kinematics()
|
|
220
|
+
cx, cy = self._world_xy(kin)
|
|
221
|
+
|
|
222
|
+
qx, qy, qz, qw = kin["orientation"]
|
|
223
|
+
_, _, yaw = quat2euler((qw, qx, qy, qz), axes='sxyz')
|
|
224
|
+
|
|
225
|
+
# --- цель в МИРОВОЙ СК ---
|
|
226
|
+
if not use_body_shift:
|
|
227
|
+
# odom → world: просто добавляем сохранённый сдвиг
|
|
228
|
+
if use_odom_frame and (self._odom0_xy is not None):
|
|
229
|
+
x0, y0 = self._odom0_xy
|
|
230
|
+
tgt_x, tgt_y = x0 + float(x), y0 + float(y)
|
|
231
|
+
else:
|
|
232
|
+
# если ноль не задан — трактуем как world как есть
|
|
233
|
+
tgt_x, tgt_y = float(x), float(y)
|
|
234
|
+
else:
|
|
235
|
+
# base_link → world (как у тебя было, без изменений)
|
|
236
|
+
tgt_x = cx + math.cos(yaw) * x - math.sin(yaw) * y
|
|
237
|
+
tgt_y = cy + math.sin(yaw) * x + math.cos(yaw) * y
|
|
238
|
+
|
|
239
|
+
# --- стартовая дистанция и таймаут ---
|
|
240
|
+
dx0 = tgt_x - cx
|
|
241
|
+
dy0 = tgt_y - cy # инверсия мирового y, как в файле
|
|
242
|
+
dist0 = math.hypot(dx0, dy0)
|
|
243
|
+
timeout = max(T_MIN, min((dist0 / 0.1) * 2.0, T_MAX))
|
|
244
|
+
deadline = mono() + timeout
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
while True:
|
|
248
|
+
kin = self.get_sim_kinematics() # кэш!
|
|
249
|
+
x_w = sim_to_api_distance(kin["location"][0])
|
|
250
|
+
y_w = sim_to_api_distance(kin["location"][1])
|
|
251
|
+
|
|
252
|
+
qx, qy, qz, qw = kin["orientation"]
|
|
253
|
+
_, _, yaw = quat2euler((qw, qx, qy, qz), axes='sxyz') # как в файле
|
|
254
|
+
# print(yaw)
|
|
255
|
+
# yaw = -yaw
|
|
256
|
+
# print(x_w, y_w, yaw)
|
|
257
|
+
|
|
258
|
+
# yaw += 1.57
|
|
259
|
+
# print(yaw)
|
|
260
|
+
# print()
|
|
261
|
+
|
|
262
|
+
# --- мировая разность до цели ---
|
|
263
|
+
dx = tgt_x - x_w
|
|
264
|
+
dy = tgt_y - y_w # инвертируем мировой y (как в файле)
|
|
265
|
+
|
|
266
|
+
# print(f"dx: {dx}, dy: {dy}")
|
|
267
|
+
|
|
268
|
+
# --- локальная ошибка (корпусная СК: x вперёд, y влево) ---
|
|
269
|
+
err_x = math.cos(yaw) * dx + math.sin(yaw) * dy
|
|
270
|
+
err_y = -math.sin(yaw) * dx + math.cos(yaw) * dy
|
|
271
|
+
|
|
272
|
+
# print(f"err_x: {err_x}, err_y: {err_y}")
|
|
273
|
+
|
|
274
|
+
# --- условие достижения (по осям) ---
|
|
275
|
+
if abs(err_x) < X_THR and abs(err_y) < Y_THR:
|
|
276
|
+
self._rpy_vel_data = (1500, 1500, 1500)
|
|
277
|
+
return True
|
|
278
|
+
|
|
279
|
+
# --- таймаут ---
|
|
280
|
+
if mono() > deadline:
|
|
281
|
+
self._rpy_vel_data = (1500, 1500, 1500)
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
sign_pitch = 1
|
|
285
|
+
sign_roll = 1
|
|
286
|
+
|
|
287
|
+
if err_x > 0:
|
|
288
|
+
sign_pitch = 1
|
|
289
|
+
elif err_x < 0:
|
|
290
|
+
sign_pitch = -1
|
|
291
|
+
else:
|
|
292
|
+
sign_pitch = 0
|
|
293
|
+
|
|
294
|
+
if err_y > 0:
|
|
295
|
+
sign_roll = 1
|
|
296
|
+
elif err_y < 0:
|
|
297
|
+
sign_roll = -1
|
|
298
|
+
else:
|
|
299
|
+
sign_roll = 0
|
|
300
|
+
|
|
301
|
+
# print()
|
|
302
|
+
# print(FF_PITCH * sign_roll)
|
|
303
|
+
# print(pid_roll.current_error * pid_roll.kp)
|
|
304
|
+
# print(pid_roll.integral * pid_roll.ki)
|
|
305
|
+
# print(pid_roll.derivative * pid_roll.kd)
|
|
306
|
+
# print(pid_roll.get_control())
|
|
307
|
+
|
|
308
|
+
# --- PID по осям с порогом (как generate_control в файле) ---
|
|
309
|
+
# вперёд (pitch)
|
|
310
|
+
if abs(err_x) > X_THR:
|
|
311
|
+
pid_pitch.update_control(err_x, reset_prev=first_iter)
|
|
312
|
+
v_fwd = max(-VMAX_AXIS, min(FF_PITCH * sign_pitch + pid_pitch.get_control(), VMAX_AXIS))
|
|
313
|
+
|
|
314
|
+
else:
|
|
315
|
+
v_fwd = 0.0
|
|
316
|
+
|
|
317
|
+
# влево (roll)
|
|
318
|
+
if abs(err_y) > Y_THR:
|
|
319
|
+
pid_roll.update_control(err_y, reset_prev=first_iter)
|
|
320
|
+
v_left = -max(-VMAX_AXIS, min(FF_ROLL * sign_roll + pid_roll.get_control(), VMAX_AXIS))
|
|
321
|
+
else:
|
|
322
|
+
v_left = 0.0
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
first_iter = False
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# --- PWM маппинг (вперёд=+pitch, влево=+roll) ---
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
pitch_pwm = vel_to_rc_signal(v_fwd)
|
|
335
|
+
roll_pwm = vel_to_rc_signal(v_left)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# print(f"###########################")
|
|
341
|
+
# print(v_fwd)
|
|
342
|
+
# print(v_left)
|
|
343
|
+
# print()
|
|
344
|
+
|
|
345
|
+
self._rpy_vel_data = (roll_pwm, pitch_pwm, 1500)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# периодичность
|
|
350
|
+
next_t += PERIOD
|
|
351
|
+
time.sleep(max(0.0, next_t - mono()))
|
|
352
|
+
finally:
|
|
353
|
+
# стоп по XY в любом случае
|
|
354
|
+
self._rpy_vel_data = (1500, 1500, 1500)
|
|
355
|
+
|
|
356
|
+
def getOdomOpticflow(self):
|
|
357
|
+
kin = self.get_sim_kinematics()
|
|
358
|
+
x, y = self._world_xy(kin)
|
|
359
|
+
last_x, last_y = self._odom0_xy
|
|
360
|
+
odom_x = x - last_x
|
|
361
|
+
odom_y = y - last_y
|
|
362
|
+
return [odom_x, odom_y, self._altitude]
|
|
363
|
+
|
|
364
|
+
def gotoXYdrone(self, x, y):
|
|
365
|
+
return self.go_to_xy("base_link", x, y)
|
|
366
|
+
|
|
367
|
+
def gotoXYodom(self, x, y):
|
|
368
|
+
return self.go_to_xy("odom", x, y)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
def _wrap_pi(a: float) -> float:
|
|
373
|
+
"""Нормализация угла в интервал [-pi, pi)."""
|
|
374
|
+
return (a + math.pi) % (2 * math.pi) - math.pi
|
|
375
|
+
|
|
376
|
+
def _get_yaw_cw(self) -> float:
|
|
377
|
+
"""
|
|
378
|
+
Текущий курс в РАДИАНАХ с положительным направлением ПО ЧАСОВОЙ.
|
|
379
|
+
IMU/сим даёт yaw CCW -> инвертируем знак.
|
|
380
|
+
"""
|
|
381
|
+
kin = self.get_sim_kinematics() # ДОЛЖЕН быть кэш (см. твой sim_kinematics_callback)
|
|
382
|
+
qx, qy, qz, qw = kin["orientation"] # (x,y,z,w)
|
|
383
|
+
_, _, yaw_ccw = quat2euler((qw, qx, qy, qz), axes='sxyz')
|
|
384
|
+
return -yaw_ccw # переводим в CW
|
|
385
|
+
|
|
386
|
+
def setYaw(self, yaw: float) -> bool:
|
|
387
|
+
"""
|
|
388
|
+
БЛОКИРУЮЩИЙ поворот до абсолютного угла в ГЛОБАЛЬНОЙ СК.
|
|
389
|
+
Вход: yaw — радианы, ПО ЧАСОВОЙ (CW), нормализуется в [-pi, pi).
|
|
390
|
+
Возврат: True (достиг в допуске) / False (таймаут).
|
|
391
|
+
"""
|
|
392
|
+
# ===== жёсткие настройки =====
|
|
393
|
+
PERIOD = 1/20.0 # 20 Гц
|
|
394
|
+
TOL = 0.025 # рад, допуск по углу
|
|
395
|
+
MAX_TIME = 10.0 # сек, таймаут
|
|
396
|
+
MAX_RATE = 0.75 # рад/с, ограничение выхода PID
|
|
397
|
+
KP, KI, KD = 3.0, 0.001, 0.30 # PID по курсу
|
|
398
|
+
I_LIMIT = None # можно поставить число (напр. 600) для ограничения интеграла
|
|
399
|
+
# =============================
|
|
400
|
+
|
|
401
|
+
pid = PID(KP, KI, KD, max_control=MAX_RATE, i_limit=I_LIMIT)
|
|
402
|
+
pid.reset()
|
|
403
|
+
|
|
404
|
+
mono = time.monotonic
|
|
405
|
+
next_t = mono()
|
|
406
|
+
deadline = mono() + MAX_TIME
|
|
407
|
+
|
|
408
|
+
# цель сразу нормализуем в CW
|
|
409
|
+
goal = self._wrap_pi(yaw)
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
while True:
|
|
413
|
+
curr = self._get_yaw_cw() # текущее CW
|
|
414
|
+
err = self._wrap_pi(goal - curr) # кратчайшая дуга
|
|
415
|
+
|
|
416
|
+
# завершение
|
|
417
|
+
if abs(err) < TOL:
|
|
418
|
+
r, p, _ = self._rpy_vel_data
|
|
419
|
+
self._rpy_vel_data = (r, p, 1500) # отпустить руддер
|
|
420
|
+
return True
|
|
421
|
+
if mono() > deadline:
|
|
422
|
+
r, p, _ = self._rpy_vel_data
|
|
423
|
+
self._rpy_vel_data = (r, p, 1500)
|
|
424
|
+
return False
|
|
425
|
+
|
|
426
|
+
# PID -> угловая скорость (рад/с), внутри уже есть сатурация MAX_RATE
|
|
427
|
+
pid.update_control(err)
|
|
428
|
+
rate_cw = pid.get_control()
|
|
429
|
+
|
|
430
|
+
# рад/с -> PWM (если «вправо» у тебя даёт yaw<1500 — просто поставь минус)
|
|
431
|
+
yaw_pwm = vel_to_rc_signal(rate_cw) # или vel_to_rc_signal(-rate_cw), если знак канала другой
|
|
432
|
+
|
|
433
|
+
# пишем только yaw
|
|
434
|
+
r, p, _ = self._rpy_vel_data
|
|
435
|
+
self._rpy_vel_data = (r, p, yaw_pwm) # оставь именно (r, p, yaw_pwm)
|
|
436
|
+
|
|
437
|
+
next_t += PERIOD
|
|
438
|
+
time.sleep(max(0.0, next_t - mono()))
|
|
439
|
+
finally:
|
|
440
|
+
# гарантированно нейтраль по yaw
|
|
441
|
+
r, p, _ = self._rpy_vel_data
|
|
442
|
+
self._rpy_vel_data = (r, p, 1500)
|
|
443
|
+
|
|
444
|
+
def _wrap(self, a: float) -> float: # на будущее (не обязателен)
|
|
445
|
+
return (a + math.pi) % (2*math.pi) - math.pi
|
|
446
|
+
|
|
447
|
+
def _get_height(self) -> float:
|
|
448
|
+
# Текущая высота из твоего кэша (calculate_height_rc её обновляет)
|
|
449
|
+
return float(self._altitude)
|
|
450
|
+
|
|
451
|
+
def _clamp_h(self, h: float, lo: float, hi: float) -> float:
|
|
452
|
+
return max(lo, min(h, hi))
|
|
453
|
+
|
|
454
|
+
def _sleep_until(self, t_deadline: float, period: float) -> bool:
|
|
455
|
+
# true -> живём; false -> вышел таймаут
|
|
456
|
+
now = time.monotonic()
|
|
457
|
+
if now >= t_deadline:
|
|
458
|
+
return False
|
|
459
|
+
time.sleep(max(0.0, period - (time.monotonic() - now)))
|
|
460
|
+
return True
|
|
461
|
+
|
|
462
|
+
def takeoff(self) -> bool:
|
|
463
|
+
"""
|
|
464
|
+
ВЗЛЁТ: мгновенно кидаем целевую высоту takeoff_h и ждём достижения.
|
|
465
|
+
Блокирующий. True/False = достиг/таймаут.
|
|
466
|
+
"""
|
|
467
|
+
# ---- жёсткие настройки ----
|
|
468
|
+
PERIOD = 0.05 # 20 Гц
|
|
469
|
+
MIN_H = 0.00
|
|
470
|
+
TAKEOFF_H = 1.00
|
|
471
|
+
MAX_H = 5.00
|
|
472
|
+
REACH_COEF = 0.70 # считаем «достигли», если >= 70% цели
|
|
473
|
+
TIMEOUT_COEF = 10.0 # с/м
|
|
474
|
+
# ---------------------------
|
|
475
|
+
|
|
476
|
+
h_now = self._get_height()
|
|
477
|
+
tgt = self._clamp_h(TAKEOFF_H, MIN_H, MAX_H)
|
|
478
|
+
|
|
479
|
+
# Мгновенно ставим финальный сетпоинт
|
|
480
|
+
self.set_target_height(tgt)
|
|
481
|
+
|
|
482
|
+
# Таймаут пропорционален требуемому подъёму (как в твоём ROS-коде)
|
|
483
|
+
climb = max(0.0, tgt - h_now)
|
|
484
|
+
deadline = time.monotonic() + (TIMEOUT_COEF * climb if climb > 0.0 else TIMEOUT_COEF)
|
|
485
|
+
|
|
486
|
+
while True:
|
|
487
|
+
h = self._get_height()
|
|
488
|
+
if h >= REACH_COEF * tgt:
|
|
489
|
+
return True
|
|
490
|
+
if time.monotonic() >= deadline:
|
|
491
|
+
return False
|
|
492
|
+
if not self._sleep_until(deadline, PERIOD):
|
|
493
|
+
return False
|
|
494
|
+
|
|
495
|
+
def boarding(self) -> bool:
|
|
496
|
+
"""
|
|
497
|
+
ПОСАДКА: плавно снижаем целевой сетпоинт до MIN_H лесенкой.
|
|
498
|
+
Блокирующий. True/False = завершил/таймаут.
|
|
499
|
+
"""
|
|
500
|
+
# ---- жёсткие настройки ----
|
|
501
|
+
PERIOD = 0.05 # 20 Гц
|
|
502
|
+
MIN_H = 0.00
|
|
503
|
+
MAX_H = 5.00
|
|
504
|
+
STEP = 0.01 # м за шаг сетпоинта
|
|
505
|
+
TIMEOUT_MIN = 15.0 # базовый таймаут
|
|
506
|
+
TIMEOUT_COEF = 10.0 # доп. таймаут пропорционален высоте
|
|
507
|
+
# ---------------------------
|
|
508
|
+
|
|
509
|
+
# Стартуем от текущего командного сетпоинта (если он меньше факта — ок)
|
|
510
|
+
curr_cmd = float(getattr(self, "_target_height", 0.0))
|
|
511
|
+
curr_cmd = self._clamp_h(curr_cmd, MIN_H, MAX_H)
|
|
512
|
+
|
|
513
|
+
total_drop = max(0.0, curr_cmd - MIN_H)
|
|
514
|
+
deadline = time.monotonic() + TIMEOUT_MIN + TIMEOUT_COEF * total_drop
|
|
515
|
+
|
|
516
|
+
while curr_cmd > MIN_H:
|
|
517
|
+
curr_cmd = max(MIN_H, curr_cmd - STEP)
|
|
518
|
+
self.set_target_height(curr_cmd)
|
|
519
|
+
if time.monotonic() >= deadline:
|
|
520
|
+
return False
|
|
521
|
+
if not self._sleep_until(deadline, PERIOD):
|
|
522
|
+
return False
|
|
523
|
+
|
|
524
|
+
# На земле — успех (ждать фактического нуля не обязательно: контур дотянет сам)
|
|
525
|
+
return True
|
|
526
|
+
|
|
527
|
+
def setHeight(self, target_height: float) -> bool:
|
|
528
|
+
"""
|
|
529
|
+
Установить высоту.
|
|
530
|
+
- Если цель ВЫШЕ текущей фактической высоты → сразу ставим конечный сетпоинт и ждём достижения.
|
|
531
|
+
- Если цель НИЖЕ текущей фактической высоты → плавно опускаем сетпоинт лесенкой до цели.
|
|
532
|
+
Блокирующий. True/False = достиг/таймаут/пределы.
|
|
533
|
+
"""
|
|
534
|
+
# ---- жёсткие настройки ----
|
|
535
|
+
PERIOD = 0.05
|
|
536
|
+
MIN_H = 0.00
|
|
537
|
+
MAX_H = 5.00
|
|
538
|
+
STEP = 0.01
|
|
539
|
+
REACH_COEF = 0.70
|
|
540
|
+
TIMEOUT_MIN = 15.0
|
|
541
|
+
TIMEOUT_COEF = 10.0
|
|
542
|
+
# ---------------------------
|
|
543
|
+
|
|
544
|
+
tgt = self._clamp_h(float(target_height), MIN_H, MAX_H)
|
|
545
|
+
h0 = self._get_height()
|
|
546
|
+
|
|
547
|
+
# ВВЕРХ: сразу кидаем финальный сетпоинт
|
|
548
|
+
if tgt >= h0:
|
|
549
|
+
self.set_target_height(tgt)
|
|
550
|
+
deadline = time.monotonic() + TIMEOUT_MIN + TIMEOUT_COEF * max(0.0, tgt - h0)
|
|
551
|
+
while True:
|
|
552
|
+
h = self._get_height()
|
|
553
|
+
if h >= REACH_COEF * tgt:
|
|
554
|
+
return True
|
|
555
|
+
if time.monotonic() >= deadline:
|
|
556
|
+
return False
|
|
557
|
+
if not self._sleep_until(deadline, PERIOD):
|
|
558
|
+
return False
|
|
559
|
+
|
|
560
|
+
# ВНИЗ: плавная лесенка от текущего командного
|
|
561
|
+
curr_cmd = float(getattr(self, "_target_height", h0))
|
|
562
|
+
curr_cmd = self._clamp_h(curr_cmd, MIN_H, MAX_H)
|
|
563
|
+
deadline = time.monotonic() + TIMEOUT_MIN + TIMEOUT_COEF * max(0.0, curr_cmd - tgt)
|
|
564
|
+
|
|
565
|
+
while curr_cmd > tgt:
|
|
566
|
+
curr_cmd = max(tgt, curr_cmd - STEP)
|
|
567
|
+
self.set_target_height(curr_cmd)
|
|
568
|
+
if time.monotonic() >= deadline:
|
|
569
|
+
return False
|
|
570
|
+
if not self._sleep_until(deadline, PERIOD):
|
|
571
|
+
return False
|
|
572
|
+
|
|
573
|
+
return True
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def sim_kinematics_callback(self):
|
|
577
|
+
# Используем блокировку при обращении к клиенту
|
|
578
|
+
with self._client_lock:
|
|
579
|
+
self._sim_kinematics = self._client.get_kinametics_data()
|
|
580
|
+
self._sim_img = self._client.get_camera_capture(camera_id=1) #выбор камеры 0-передняя 1-нижняя
|
|
581
|
+
self._sim_ultrasonic = self._client.get_range_data(rangefinder_id = 0, range_min = 0.15, range_max = 4, is_clear = True, range_error = 0.0003) * 100
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def get_sim_kinematics(self):
|
|
585
|
+
return self._sim_kinematics
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def transmit_rc_to_sim(self):
|
|
589
|
+
roll, pitch, yaw = self._rpy_vel_data
|
|
590
|
+
raw_rc = [roll, pitch, self._throttle_data, yaw, self._arm_data, self._fliyng_mode, self._nav_mode]
|
|
591
|
+
raw_rc = self.clamp_rc_list(raw_rc)
|
|
592
|
+
msg = self._control.send_RAW_RC(raw_rc)
|
|
593
|
+
data_handler = self._control.receive_msg()
|
|
594
|
+
|
|
595
|
+
def setVelXY(self, x, y):
|
|
596
|
+
rpy = self._rpy_vel_data
|
|
597
|
+
rc_x = vel_to_rc_signal(x)
|
|
598
|
+
rc_y = vel_to_rc_signal(y)
|
|
599
|
+
self._rpy_vel_data = (rc_y, rc_x, rpy[2])
|
|
600
|
+
|
|
601
|
+
def setVelXYYaw(self, x, y, yaw):
|
|
602
|
+
rc_x = vel_to_rc_signal(x)
|
|
603
|
+
rc_y = vel_to_rc_signal(y)
|
|
604
|
+
rc_yaw = vel_to_rc_signal(yaw)
|
|
605
|
+
self._rpy_vel_data = (rc_y, rc_x, rc_yaw)
|
|
606
|
+
|
|
607
|
+
def armDrone(self):
|
|
608
|
+
self._armed_flag = True
|
|
609
|
+
self._arm_data = 2000
|
|
610
|
+
|
|
611
|
+
def disarmDrone(self):
|
|
612
|
+
self._armed_flag = False
|
|
613
|
+
self._arm_data = 1000
|
|
614
|
+
|
|
615
|
+
def initDrone(self):
|
|
616
|
+
self._rpy_vel_data = (1500, 1500, 1500)
|
|
617
|
+
self._throttle_data = 1000
|
|
618
|
+
self._arm_data = 1000
|
|
619
|
+
self._fliyng_mode = 1000
|
|
620
|
+
self._nav_mode = 1000
|
|
621
|
+
|
|
622
|
+
def posholdOn(self):
|
|
623
|
+
self._poshold_flag = True
|
|
624
|
+
self._nav_mode = 1500
|
|
625
|
+
|
|
626
|
+
def posholdOff(self):
|
|
627
|
+
self._poshold_flag = False
|
|
628
|
+
self._nav_mode = 1000
|
|
629
|
+
|
|
630
|
+
def clamp_rc(self, data):
|
|
631
|
+
return max(min(data, 2000), 1000)
|
|
632
|
+
|
|
633
|
+
def clamp_rc_list(self, data: Iterable):
|
|
634
|
+
return [self.clamp_rc(rc) for rc in data]
|
|
635
|
+
|
|
636
|
+
def round_data(self, iterable):
|
|
637
|
+
return map(lambda x: round(x, 3), iterable)
|
|
638
|
+
|
|
639
|
+
def set_target_height(self, height):
|
|
640
|
+
self._target_height = height
|
|
641
|
+
self.__alt_pid.reset()
|
|
642
|
+
|
|
643
|
+
#new
|
|
644
|
+
def getImage(self):
|
|
645
|
+
return self._sim_img
|
|
646
|
+
|
|
647
|
+
def getArucos(self):
|
|
648
|
+
return self._aruco_data
|
|
649
|
+
|
|
650
|
+
def getCameraPoseAruco(self):
|
|
651
|
+
return self._camera_pose_aruco_data
|
|
652
|
+
|
|
653
|
+
def getBlobs(self):
|
|
654
|
+
return self._blob_data
|
|
655
|
+
|
|
656
|
+
def getBlobsImage(self):
|
|
657
|
+
return self._blob_img
|
|
658
|
+
|
|
659
|
+
def getArucosImage(self):
|
|
660
|
+
return self._aruco_img
|
|
661
|
+
|
|
662
|
+
def image_processing_callback(self):
|
|
663
|
+
# Обработка изображения не требует доступа к клиенту,
|
|
664
|
+
# используем уже полученное изображение из кэша
|
|
665
|
+
|
|
666
|
+
sim_image = self._sim_img.copy() if self._sim_img is not None else None
|
|
667
|
+
camera_img = resolution_changes(sim_image, (320, 240))
|
|
668
|
+
|
|
669
|
+
img_aruco = camera_img.copy() if self._sim_img is not None else None
|
|
670
|
+
img_blob = camera_img.copy() if self._sim_img is not None else None
|
|
671
|
+
|
|
672
|
+
if img_aruco is not None:
|
|
673
|
+
self._aruco_data, self._camera_pose_aruco_data, aruco_img = process_aruco(img_aruco)
|
|
674
|
+
|
|
675
|
+
if aruco_img is None:
|
|
676
|
+
self._aruco_img = sim_image
|
|
677
|
+
else:
|
|
678
|
+
self._aruco_img = resolution_changes(aruco_img, (640, 480))
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
if img_blob is not None:
|
|
682
|
+
self._blob_data, blob_img = process_blob(img_blob)
|
|
683
|
+
if blob_img is None:
|
|
684
|
+
self._blob_img = sim_image
|
|
685
|
+
else:
|
|
686
|
+
self._blob_img = resolution_changes(blob_img, (640, 480))
|
|
687
|
+
|
|
688
|
+
def setDiod(self, r, g, b):
|
|
689
|
+
# Используем блокировку при обращении к клиенту
|
|
690
|
+
with self._client_lock:
|
|
691
|
+
return self._client.set_Diod(float(r), float(g), float(b))
|
|
692
|
+
|
|
693
|
+
def setShoot(self, time):
|
|
694
|
+
with self._client_lock:
|
|
695
|
+
return self._client.call_event_action()
|