ken910606 4 weeks ago
commit 81de964778

@ -287,9 +287,11 @@ class UDPMavlinkReceiver(threading.Thread):
elif msg_type == "GLOBAL_POSITION_INT":
latitude = msg.lat / 1e7
longitude = msg.lon / 1e7
relative_alt = msg.relative_alt / 1000.0
self.signals.update_signal.emit('gps', drone_id, {
'lat': latitude,
'lon': longitude
'lon': longitude,
'alt': relative_alt,
})
elif msg_type == "GPS_RAW_INT":
@ -499,9 +501,11 @@ class SerialMavlinkReceiver(threading.Thread, JsonTelemetryProcessor):
elif msg_type == "GLOBAL_POSITION_INT":
latitude = msg.lat / 1e7
longitude = msg.lon / 1e7
relative_alt = msg.relative_alt / 1000.0
self.signals.update_signal.emit('gps', drone_id, {
'lat': latitude,
'lon': longitude
'lon': longitude,
'alt': relative_alt,
})
elif msg_type == "GPS_RAW_INT":

@ -83,10 +83,11 @@ class FormationPlanner:
center_lat = sum(pos[0] for pos in drone_gps_positions) / len(drone_gps_positions)
center_lon = sum(pos[1] for pos in drone_gps_positions) / len(drone_gps_positions)
center_alt = sum(pos[2] for pos in drone_gps_positions) / len(drone_gps_positions)
self.current_origin = (center_lat, center_lon, center_alt)
self.converter = CoordinateConverter(center_lat, center_lon, center_alt)
# origin_alt = 0送給飛控的高度透過 MAV_FRAME_GLOBAL_RELATIVE_ALT_INT
# 直接就是「相對 home 高度」,不需要加 GPS 橢球高度偏移
self.current_origin = (center_lat, center_lon, 0.0)
self.converter = CoordinateConverter(center_lat, center_lon, 0.0)
drone_local = [self.converter.gps_to_local(*pos) for pos in drone_gps_positions]
target_local = self.converter.gps_to_local(*target_gps)
@ -206,29 +207,17 @@ class FormationPlanner:
def _calculate_leader_follower(self, drone_positions, route_wps_local, params):
"""
路徑跟隨編隊 虛擬領隊 / 弧長參數化版 (virtual leader / arc-length)
核心想法
- center_path 建好後計算每個 sample 的累積弧長 s
- 每架 drone (lat_k, lon_k) 的隊形偏移
- leader 想成沿 s 參數化的點follower k 的目標 s = s_leader - lon_k
- follower k i waypoint = lookup(s_list[i] - lon_k) + lat_k × perp(tangent)
- 超過 path 起點/終點以 clip 處理避免倒退或衝出
解決前版空間偏移的三個 bug
1. 起點暴衝s<0 clip follower 起點就是 start 加橫向偏移不往後倒退
2. 弧段角度爆炸lon 不再換算成弧度直接沿 polyline lookup
3. straight/arc 切換不連續統一走 polyline 線性內插與 segment 切線
Rank 定序規則
** drone_positions 的輸入順序為準**= GUI 選取順序
drone_positions[0] = rank 0 = leader (lon=0)
drone_positions[1] = rank 1 (lon=5)依此類推
刻意用位置投影自動排序避免浮點噪音造成 run-to-run
leader 漂移以確保整個 mission 像軍隊縱隊那樣順序固定
隊形 (俯視, 以路徑行進方向為前):
D0 (lat=-L, lon=0) front-right (leader)
路徑跟隨編隊 虛擬領隊 / 弧長參數化
center_path = 使用者畫的路徑點直線段連接不插弧
每架 drone 沿弧長 s 參數化做隊形偏移
s_follower = s_leader - lon_k
position = lookup(s_follower) + lat_k × perp(tangent)
s 超出 path 範圍時外推 clip確保每架 drone 起終點不重疊
Rank = 輸入順序GUI 選取順序drone_positions[0] = leader
隊形 (俯視):
D0 (lat=-L, lon=0) leader
D1 (lat=+L, lon=5)
D2 (lat=-L, lon=10)
D3 (lat=+L, lon=15)
@ -237,29 +226,16 @@ class FormationPlanner:
lateral_offset = params.get('lateral_offset', 3.0)
longitudinal_spacing = params.get('longitudinal_spacing', 5.0)
altitude = params.get('altitude', self.base_altitude)
min_inner_radius = params.get('min_inner_radius', 3.0)
arc_resolution = params.get('arc_resolution', 12)
sharp_turn_cos = params.get('sharp_turn_cos_threshold', 0.0) # cos(90°)=0
# Step 1: 建立中心路徑(含圓弧、銳角單點;每點帶累積 s
center_path, barrier_indices = self._build_center_path(
route_wps_local,
formation_max_lateral=lateral_offset,
min_inner_radius=min_inner_radius,
arc_resolution=arc_resolution,
sharp_turn_cos_threshold=sharp_turn_cos,
)
# Step 1: 建立中心路徑(直線段連接,每個中間航點設 barrier
center_path, barrier_indices = self._build_center_path(route_wps_local)
s_list = [pt['s'] for pt in center_path]
s_max = s_list[-1]
n_pts = len(center_path)
def lookup(s_target):
"""
center path 上以弧長 s 回傳 (x, y, tangent_dir)
s<0 clip starts>s_max clip end
tangent 方向取當前 polyline segment 的切線避免對不連續的 dir 做插值
"""
"""在 center path 上以弧長 s 回傳 (x, y, tangent_dir)。超出範圍時沿端點方向外推。"""
if n_pts == 1:
pt = center_path[0]
return pt['x'], pt['y'], pt['dir']
@ -270,7 +246,8 @@ class FormationPlanner:
sdx, sdy = b['x'] - a['x'], b['y'] - a['y']
slen = math.hypot(sdx, sdy)
if slen > 1e-6:
return a['x'], a['y'], (sdx / slen, sdy / slen)
dx, dy = sdx / slen, sdy / slen
return a['x'] + s_target * dx, a['y'] + s_target * dy, (dx, dy)
return a['x'], a['y'], a['dir']
if s_target >= s_max:
@ -279,7 +256,9 @@ class FormationPlanner:
sdx, sdy = b['x'] - a['x'], b['y'] - a['y']
slen = math.hypot(sdx, sdy)
if slen > 1e-6:
return b['x'], b['y'], (sdx / slen, sdy / slen)
dx, dy = sdx / slen, sdy / slen
overshoot = s_target - s_max
return b['x'] + overshoot * dx, b['y'] + overshoot * dy, (dx, dy)
return b['x'], b['y'], b['dir']
# Binary searchs_list[lo] <= s_target < s_list[hi]
@ -308,16 +287,6 @@ class FormationPlanner:
# 避免浮點噪音在投影值相近時翻轉 leader保證 run-to-run 穩定
all_waypoints = [None] * N
# 預先算好路徑終點的切線(給收尾點用)
end = center_path[-1]
end_dx, end_dy = end['dir']
if n_pts >= 2:
a = center_path[-2]
sdx, sdy = end['x'] - a['x'], end['y'] - a['y']
slen = math.hypot(sdx, sdy)
if slen > 1e-6:
end_dx, end_dy = sdx / slen, sdy / slen
for rank in range(N):
lat_sign = -1 if rank % 2 == 0 else 1
lat = lat_sign * lateral_offset
@ -334,46 +303,16 @@ class FormationPlanner:
altitude,
))
# 收尾:全員到 path 終點的 rank 位置lon 歸零)
waypoints.append((
end['x'] + lat * (-end_dy),
end['y'] + lat * end_dx,
altitude,
))
all_waypoints[rank] = waypoints
# barrier 索引仍指向 center_path 內 (range [0, n_pts-1])
# 不觸及末尾收尾點,直接沿用即可
return all_waypoints, barrier_indices
def _build_center_path(self, waypoints,
formation_max_lateral,
min_inner_radius,
arc_resolution,
sharp_turn_cos_threshold):
def _build_center_path(self, waypoints):
"""
建立以圓弧連接直線段的中心路徑
每個中間 waypoint 的處理
1. 計算入向 u_in出向 u_out 的夾角 β = acos(u_in·u_out)
2. cos β < sharp_turn_cos_threshold 銳角只插單點 (hover + 原地轉向)
barrier 放在該點讓隊伍整體停下再繼續
3. 否則 平滑弧
R_base = formation_max_lateral + min_inner_radius
d = R_base × tan(β/2)並以相鄰段長的 45% 為上限
R_actual = d / tan(β/2)
tangent points T_in = WP - u_in·d, T_out = WP + u_out·d
arc center = T_in + R_actual·sign·n_left其中 sign=+1 左轉/CCW-1 右轉/CW
arc theta_in theta_out arc_resolution 等分
path 內容: [T_in(straight,u_in), arc_samples..., T_out(straight,u_out)]
barrier 放在 T_out (轉完彎後的集合點)
path 是一個 list[dict]每個 dict 至少含:
{'seg': 'straight' | 'arc',
'x': float, 'y': float,
'dir': (dx, dy)}
'arc' 類型額外含: 'arc_center', 'arc_R', 'arc_sign', 'theta'
建立中心路徑 直接用路徑點連接不插弧
每個中間航點設為 barrier轉彎集合點讓隊伍到齊後再推進
飛控在 GUIDED 模式下自行處理直線飛行不需要 GUI 預先平滑
Returns:
(path, barrier_indices)
@ -385,129 +324,35 @@ class FormationPlanner:
'seg': 'straight',
'x': waypoints[0][0], 'y': waypoints[0][1],
'dir': (0.0, 1.0),
's': 0.0,
}], []
segments = []
for i in range(num_wps - 1):
dx = waypoints[i + 1][0] - waypoints[i][0]
dy = waypoints[i + 1][1] - waypoints[i][1]
length = math.hypot(dx, dy)
if length < 0.01:
segments.append(((0.0, 1.0), length))
else:
segments.append(((dx / length, dy / length), length))
R_base = formation_max_lateral + min_inner_radius
path = []
barrier_indices = []
# 起點
path.append({
'seg': 'straight',
'x': waypoints[0][0], 'y': waypoints[0][1],
'dir': segments[0][0],
})
for i in range(1, num_wps - 1):
u_in, len_in = segments[i - 1]
u_out, len_out = segments[i]
wp = waypoints[i]
dot = u_in[0] * u_out[0] + u_in[1] * u_out[1]
dot = max(-1.0, min(1.0, dot))
# 銳角hover + 原地轉向
if dot < sharp_turn_cos_threshold:
avg_dx = u_in[0] + u_out[0]
avg_dy = u_in[1] + u_out[1]
avg_len = math.hypot(avg_dx, avg_dy)
if avg_len > 0.01:
avg_dir = (avg_dx / avg_len, avg_dy / avg_len)
else:
avg_dir = u_in
path.append({
'seg': 'straight',
'x': wp[0], 'y': wp[1],
'dir': avg_dir,
})
barrier_indices.append(len(path) - 1)
continue
# 平滑弧
beta = math.acos(dot)
if beta < 1e-4:
# 幾乎直線,不插弧
path.append({
'seg': 'straight',
'x': wp[0], 'y': wp[1],
'dir': u_in,
})
continue
half_tan = math.tan(beta / 2.0)
d_ideal = R_base * half_tan
d_max = 0.45 * min(len_in, len_out)
d = min(d_ideal, d_max)
R_actual = d / half_tan
T_in = (wp[0] - u_in[0] * d, wp[1] - u_in[1] * d)
T_out = (wp[0] + u_out[0] * d, wp[1] + u_out[1] * d)
# +1 CCW (左轉) / -1 CW (右轉)
cross = u_in[0] * u_out[1] - u_in[1] * u_out[0]
sign = 1 if cross > 0 else -1
# 入向左法線
n_left = (-u_in[1], u_in[0])
center = (
T_in[0] + R_actual * sign * n_left[0],
T_in[1] + R_actual * sign * n_left[1],
)
theta_in = math.atan2(T_in[1] - center[1], T_in[0] - center[0])
for i in range(num_wps):
if i < num_wps - 1:
dx = waypoints[i + 1][0] - waypoints[i][0]
dy = waypoints[i + 1][1] - waypoints[i][1]
else:
dx = waypoints[i][0] - waypoints[i - 1][0]
dy = waypoints[i][1] - waypoints[i - 1][1]
# T_in (straight, 方向 u_in)
path.append({
'seg': 'straight',
'x': T_in[0], 'y': T_in[1],
'dir': u_in,
})
length = math.hypot(dx, dy)
if length < 0.01:
direction = (0.0, 1.0)
else:
direction = (dx / length, dy / length)
# 弧段採樣 k=1..arc_resolution-1不含兩端
for k in range(1, arc_resolution):
t = k / arc_resolution
theta = theta_in + sign * beta * t
px = center[0] + R_actual * math.cos(theta)
py = center[1] + R_actual * math.sin(theta)
# 切線: sign × (-sin θ, cos θ)
tx = sign * (-math.sin(theta))
ty = sign * math.cos(theta)
path.append({
'seg': 'arc',
'x': px, 'y': py,
'dir': (tx, ty),
'arc_center': center,
'arc_R': R_actual,
'arc_sign': sign,
'theta': theta,
})
# T_out (straight, 方向 u_out)barrier 放這
path.append({
'seg': 'straight',
'x': T_out[0], 'y': T_out[1],
'dir': u_out,
'x': waypoints[i][0], 'y': waypoints[i][1],
'dir': direction,
})
barrier_indices.append(len(path) - 1)
# 終點
path.append({
'seg': 'straight',
'x': waypoints[-1][0], 'y': waypoints[-1][1],
'dir': segments[-1][0],
})
if 0 < i < num_wps - 1:
barrier_indices.append(len(path) - 1)
# 後處理:為每個 sample 加上累積 polyline 弧長 s
path[0]['s'] = 0.0
for i in range(1, len(path)):
prev = path[i - 1]

Loading…
Cancel
Save