|
|
|
|
@ -182,9 +182,9 @@ DEFAULT_REASSIGN_COOLDOWN = 2.0 # slot 交換最短間隔 (s);安全時
|
|
|
|
|
DEFAULT_REPLAN_MOVE_M = 1.0 # 敵機移動 >= 1m 觸發更新
|
|
|
|
|
DEFAULT_REPLAN_CAP_SEC = 0.4 # 最慢 0.4 秒更新一次瞬時目標
|
|
|
|
|
DEFAULT_LEAD_TIME_CAP = 4.0
|
|
|
|
|
DEFAULT_FRIENDLY_SPEED = 5.0
|
|
|
|
|
DEFAULT_FRIENDLY_SPEED = 3.0
|
|
|
|
|
DEFAULT_ENEMY_VEL_WINDOW = 1.0
|
|
|
|
|
DEFAULT_TARGET_LOST_TIMEOUT = 2.0
|
|
|
|
|
DEFAULT_TARGET_LOST_TIMEOUT = 5.0
|
|
|
|
|
|
|
|
|
|
# assignment cost
|
|
|
|
|
DEFAULT_CROSSING_PENALTY = 20000.0
|
|
|
|
|
@ -437,9 +437,13 @@ class MissionOrchestrator(QObject):
|
|
|
|
|
|
|
|
|
|
gps = self._gps_provider()
|
|
|
|
|
epos = gps.get(self.enemy_id)
|
|
|
|
|
if not epos:
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
if not epos or not self._position_is_fresh(epos, now):
|
|
|
|
|
self.status_message.emit("敵機定位遺失,無法開始包圍")
|
|
|
|
|
return
|
|
|
|
|
if self._position_alt(epos) is None:
|
|
|
|
|
self.status_message.emit("敵機相對 Home 高度尚未收到,無法開始包圍")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
friendly_ids = self._unique_keep_order(self.a_drone_ids + self.b_drone_ids)
|
|
|
|
|
if not friendly_ids:
|
|
|
|
|
@ -467,7 +471,7 @@ class MissionOrchestrator(QObject):
|
|
|
|
|
self._motion_dirs = {}
|
|
|
|
|
self._slot_groups = {} # {slot_index: "A"/"B"}
|
|
|
|
|
self._vertical_hold_ids = set()
|
|
|
|
|
self._last_enemy_seen_at = time.monotonic()
|
|
|
|
|
self._last_enemy_seen_at = self._position_received_at(epos, now)
|
|
|
|
|
self._update_enemy_velocity(epos)
|
|
|
|
|
|
|
|
|
|
center = self._planning_center(gps, epos, reset=True)
|
|
|
|
|
@ -558,7 +562,8 @@ class MissionOrchestrator(QObject):
|
|
|
|
|
def _tick_watch(self):
|
|
|
|
|
gps = self._gps_provider()
|
|
|
|
|
epos = gps.get(self.enemy_id)
|
|
|
|
|
if not epos:
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
if not epos or not self._position_is_fresh(epos, now):
|
|
|
|
|
self._detect_since = 0.0
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
@ -566,13 +571,12 @@ class MissionOrchestrator(QObject):
|
|
|
|
|
nearest_d = None
|
|
|
|
|
for did in self.a_drone_ids:
|
|
|
|
|
pos = gps.get(did)
|
|
|
|
|
if not pos:
|
|
|
|
|
if not pos or not self._position_is_fresh(pos, now):
|
|
|
|
|
continue
|
|
|
|
|
d = _haversine(epos['lat'], epos['lon'], pos['lat'], pos['lon'])
|
|
|
|
|
if nearest_d is None or d < nearest_d:
|
|
|
|
|
nearest_id, nearest_d = did, d
|
|
|
|
|
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
if nearest_d is not None and nearest_d <= self.r_detect:
|
|
|
|
|
if self._detect_since == 0.0:
|
|
|
|
|
self._detect_since = now
|
|
|
|
|
@ -589,11 +593,12 @@ class MissionOrchestrator(QObject):
|
|
|
|
|
epos = gps.get(self.enemy_id)
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
|
|
|
|
|
if not epos:
|
|
|
|
|
if (not epos or not self._position_is_fresh(epos, now)
|
|
|
|
|
or self._position_alt(epos) is None):
|
|
|
|
|
self._handle_enemy_missing(gps, now)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self._last_enemy_seen_at = now
|
|
|
|
|
self._last_enemy_seen_at = self._position_received_at(epos, now)
|
|
|
|
|
self._update_enemy_velocity(epos)
|
|
|
|
|
|
|
|
|
|
if self._positions_for(self._friendly_ids, gps) is None:
|
|
|
|
|
@ -614,11 +619,12 @@ class MissionOrchestrator(QObject):
|
|
|
|
|
epos = gps.get(self.enemy_id)
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
|
|
|
|
|
if not epos:
|
|
|
|
|
if (not epos or not self._position_is_fresh(epos, now)
|
|
|
|
|
or self._position_alt(epos) is None):
|
|
|
|
|
self._handle_enemy_missing(gps, now)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self._last_enemy_seen_at = now
|
|
|
|
|
self._last_enemy_seen_at = self._position_received_at(epos, now)
|
|
|
|
|
self._update_enemy_velocity(epos)
|
|
|
|
|
|
|
|
|
|
if self._positions_for(self._friendly_ids, gps) is None:
|
|
|
|
|
@ -638,13 +644,15 @@ class MissionOrchestrator(QObject):
|
|
|
|
|
def _tick_target_lost(self):
|
|
|
|
|
gps = self._gps_provider()
|
|
|
|
|
epos = gps.get(self.enemy_id)
|
|
|
|
|
if not epos:
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
if (not epos or not self._position_is_fresh(epos, now)
|
|
|
|
|
or self._position_alt(epos) is None):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 目標重新出現:不沿用過期軌跡,重新估速並回 CONVERGE。
|
|
|
|
|
self._enemy_hist = []
|
|
|
|
|
self._enemy_vel = (0.0, 0.0)
|
|
|
|
|
self._last_enemy_seen_at = time.monotonic()
|
|
|
|
|
self._last_enemy_seen_at = self._position_received_at(epos, now)
|
|
|
|
|
self._update_enemy_velocity(epos)
|
|
|
|
|
self._set_phase(MissionPhase.CONVERGE)
|
|
|
|
|
self.status_message.emit("敵機定位恢復,重新建立包圍軌跡")
|
|
|
|
|
@ -667,14 +675,18 @@ class MissionOrchestrator(QObject):
|
|
|
|
|
if self._merged_ex is None:
|
|
|
|
|
return
|
|
|
|
|
targets = {}
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
for did in self._friendly_ids:
|
|
|
|
|
p = gps.get(did)
|
|
|
|
|
if not p:
|
|
|
|
|
if not p or not self._position_is_fresh(p, now):
|
|
|
|
|
continue
|
|
|
|
|
current_alt = self._position_alt(p)
|
|
|
|
|
if current_alt is None:
|
|
|
|
|
continue
|
|
|
|
|
targets[did] = (
|
|
|
|
|
p['lat'],
|
|
|
|
|
p['lon'],
|
|
|
|
|
max(self.min_alt_floor, float(p.get('alt', self.formation_alt))),
|
|
|
|
|
max(self.min_alt_floor, current_alt),
|
|
|
|
|
)
|
|
|
|
|
if len(targets) == len(self._friendly_ids):
|
|
|
|
|
self._merged_ex.update_plan(self._targets_to_plan(targets))
|
|
|
|
|
@ -844,6 +856,35 @@ class MissionOrchestrator(QObject):
|
|
|
|
|
return None
|
|
|
|
|
return alt if math.isfinite(alt) else None
|
|
|
|
|
|
|
|
|
|
def _position_is_fresh(self, pos, now=None):
|
|
|
|
|
"""檢查 GPS 水平位置是否仍在可接受的遙測時間內。"""
|
|
|
|
|
if not pos:
|
|
|
|
|
return False
|
|
|
|
|
updated_at = pos.get('_gps_updated_at')
|
|
|
|
|
# 保留舊 gps_provider/測試資料的相容性;GUI 實飛資料一定帶時間戳。
|
|
|
|
|
if updated_at is None:
|
|
|
|
|
return True
|
|
|
|
|
try:
|
|
|
|
|
updated_at = float(updated_at)
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
return False
|
|
|
|
|
if not math.isfinite(updated_at):
|
|
|
|
|
return False
|
|
|
|
|
now = time.monotonic() if now is None else float(now)
|
|
|
|
|
age = now - updated_at
|
|
|
|
|
return -0.1 <= age <= self.target_lost_timeout_sec
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _position_received_at(pos, now):
|
|
|
|
|
"""取得最後一筆水平定位的 monotonic 時間,舊資料則使用目前時間。"""
|
|
|
|
|
try:
|
|
|
|
|
updated_at = float(pos.get('_gps_updated_at'))
|
|
|
|
|
except (AttributeError, TypeError, ValueError):
|
|
|
|
|
return now
|
|
|
|
|
if not math.isfinite(updated_at) or updated_at > now + 0.1:
|
|
|
|
|
return now
|
|
|
|
|
return updated_at
|
|
|
|
|
|
|
|
|
|
def _cross_layer_vertical_hold_ids(self, gps):
|
|
|
|
|
"""
|
|
|
|
|
找出必須先原地垂直分層的 A/B 友機。
|
|
|
|
|
@ -1111,7 +1152,9 @@ class MissionOrchestrator(QObject):
|
|
|
|
|
acc += seg
|
|
|
|
|
|
|
|
|
|
lat, lon = _m_to_ll(chosen[0], chosen[1], center[0], center[1])
|
|
|
|
|
cur_alt = float(gps[did].get('alt', self.formation_alt))
|
|
|
|
|
cur_alt = self._position_alt(gps[did])
|
|
|
|
|
if cur_alt is None:
|
|
|
|
|
return None
|
|
|
|
|
target_alt = slots[slot_index][2]
|
|
|
|
|
# 高度用距離比例平滑,但近場直接交給 slot。
|
|
|
|
|
frac = min(1.0, want / max(1e-6, traj['length']))
|
|
|
|
|
@ -1317,7 +1360,9 @@ class MissionOrchestrator(QObject):
|
|
|
|
|
|
|
|
|
|
lat, lon = _m_to_ll(tx, ty, ref_lat, ref_lon)
|
|
|
|
|
target_alt = self._layer_alt(group)
|
|
|
|
|
cur_alt = float(p.get('alt', target_alt))
|
|
|
|
|
cur_alt = self._position_alt(p)
|
|
|
|
|
if cur_alt is None:
|
|
|
|
|
return None
|
|
|
|
|
alt = cur_alt + 0.35 * (target_alt - cur_alt)
|
|
|
|
|
raw[did] = (lat, lon, max(self.min_alt_floor, alt))
|
|
|
|
|
|
|
|
|
|
@ -1360,10 +1405,10 @@ class MissionOrchestrator(QObject):
|
|
|
|
|
tx = x + ox * self.recovery_spread_step
|
|
|
|
|
ty = y + oy * self.recovery_spread_step
|
|
|
|
|
lat, lon = _m_to_ll(tx, ty, ref_lat, ref_lon)
|
|
|
|
|
alt = max(
|
|
|
|
|
self.min_alt_floor,
|
|
|
|
|
float(gps[did].get('alt', self.formation_alt)),
|
|
|
|
|
)
|
|
|
|
|
current_alt = self._position_alt(gps[did])
|
|
|
|
|
if current_alt is None:
|
|
|
|
|
return None
|
|
|
|
|
alt = max(self.min_alt_floor, current_alt)
|
|
|
|
|
targets[did] = (lat, lon, alt)
|
|
|
|
|
return targets
|
|
|
|
|
|
|
|
|
|
@ -1776,11 +1821,20 @@ class MissionOrchestrator(QObject):
|
|
|
|
|
|
|
|
|
|
def _positions_for(self, drone_ids, gps):
|
|
|
|
|
out = []
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
for did in drone_ids:
|
|
|
|
|
p = gps.get(did)
|
|
|
|
|
if not p:
|
|
|
|
|
if not p or not self._position_is_fresh(p, now):
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
lat = float(p['lat'])
|
|
|
|
|
lon = float(p['lon'])
|
|
|
|
|
except (KeyError, TypeError, ValueError):
|
|
|
|
|
return None
|
|
|
|
|
alt = self._position_alt(p)
|
|
|
|
|
if not math.isfinite(lat) or not math.isfinite(lon) or alt is None:
|
|
|
|
|
return None
|
|
|
|
|
out.append((p['lat'], p['lon'], p.get('alt', 0.0)))
|
|
|
|
|
out.append((lat, lon, alt))
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|