# ============================================================
# imu_fusion.py
# Sensor Fusion: IMU + Camera + Latency Compensation
#
# Решает три критических проблемы реального перехвата:
#
# 1) ЛАТЕНТНОСТЬ: между захватом кадра и моментом когда команда
#    дойдёт до моторов проходит 50-120ms. При Vc=20 м/с это
#    1-2.4 метра промаха. Этот модуль экстраполирует состояние
#    цели ВПЕРЁД на величину латентности.
#
# 2) СОБСТВЕННОЕ ДВИЖЕНИЕ: без IMU невозможно отличить движение
#    цели от движения камеры. Optical flow affine — грубое
#    приближение, ломается при тряске и быстрых манёврах.
#    IMU даёт точную ориентацию и скорость перехватчика.
#
# 3) ПРЕДСКАЗАНИЕ ТОЧКИ ВСТРЕЧИ: с IMU мы знаем свою скорость,
#    а с latency compensation — где цель БУДЕТ, а не где она
#    БЫЛА. Это позволяет PN работать корректно.
#
# Поддерживает:
#   - MAVLink ATTITUDE + LOCAL_POSITION_NED (ArduPilot/PX4)
#   - MSP_ATTITUDE + MSP_RAW_IMU (Betaflight/INAV)
#   - Fallback без IMU (только латентность, по Kalman-предсказанию)
# ============================================================

import time
import math
import threading
from collections import deque

import numpy as np

from config import *
from config_intercept import *
from helpers import clamp, box_center, box_wh, clip_box


# ─────────────────────────────────────────────────────────────
# Config (добавить в config_intercept.py)
# ─────────────────────────────────────────────────────────────

# Латентность пайплайна (секунды).
# Измеряется как: время захвата кадра → момент исполнения команды мотором.
# Типичные значения:
#   Camera capture + USB:    15-25ms
#   YOLO inference (async):  8-15ms (но результат приходит на 1-2 кадра позже)
#   ByteTrack + Kalman:      <1ms
#   Guidance compute:        <1ms
#   Serial/MAVLink send:     1-5ms
#   FC processing:           5-15ms
#   ESC + motor response:    10-20ms
#   ИТОГО:                   ~50-80ms для ROI, ~80-120ms для fullscan
LATENCY_FIXED_MS = 65.0               # фиксированная оценка полной латентности
LATENCY_ADAPTIVE = True                # адаптивная оценка (подстраивается)
LATENCY_EXTRA_TERMINAL_MS = 0.0       # доп. латентность в TERMINAL (0 = не добавлять)
LATENCY_MAX_MS = 200.0                # верхний предел (защита от багов)
LATENCY_MIN_MS = 20.0                 # нижний предел

# IMU
IMU_ENABLE = True
IMU_SOURCE = "mavlink"                 # "mavlink" | "msp" | "none"
IMU_MAVLINK_CONNECTION = "udpin:0.0.0.0:14550"  # получаем телеметрию
IMU_MSP_PORT = "/dev/ttyUSB0"
IMU_MSP_BAUD = 115200
IMU_POLL_HZ = 100.0                   # частота опроса IMU

# Сглаживание IMU
IMU_SMOOTH_ALPHA = 0.30               # EMA для скоростей
IMU_GRAVITY = 9.81

# Camera-IMU alignment (поворот IMU относительно камеры)
# Для типичного FPV: камера наклонена вперёд на 25-35°
CAMERA_TILT_DEG = 30.0                # наклон камеры вперёд (градусы)

# Latency measurement (для адаптивной оценки)
LATENCY_MEASURE_ENABLE = True
LATENCY_MEASURE_WINDOW = 30           # окно усреднения

# Debug
DRAW_LATENCY_COMP = True
DRAW_IMU_STATE = True


class IMUState:
    """Текущее состояние IMU перехватчика."""
    __slots__ = [
        "timestamp",
        "roll", "pitch", "yaw",           # радианы
        "roll_rate", "pitch_rate", "yaw_rate",  # рад/с
        "vx", "vy", "vz",                 # м/с, NED frame
        "ax", "ay", "az",                 # м/с², body frame
        "lat", "lon", "alt",              # GPS (если есть)
        "valid",
    ]

    def __init__(self):
        self.timestamp = 0.0
        self.roll = 0.0
        self.pitch = 0.0
        self.yaw = 0.0
        self.roll_rate = 0.0
        self.pitch_rate = 0.0
        self.yaw_rate = 0.0
        self.vx = 0.0
        self.vy = 0.0
        self.vz = 0.0
        self.ax = 0.0
        self.ay = 0.0
        self.az = 0.0
        self.lat = 0.0
        self.lon = 0.0
        self.alt = 0.0
        self.valid = False


class LatencyCompensator:
    """
    Компенсация латентности пайплайна.

    Принцип: вместо того чтобы наводиться на позицию цели "сейчас"
    (которая на самом деле была latency_ms миллисекунд назад),
    экстраполируем состояние цели вперёд на величину латентности.

    Использует:
    - Kalman/IMM-предсказание для экстраполяции цели
    - IMU для вычитания собственного движения
    - Адаптивную оценку латентности по корреляции предсказание/факт
    """

    def __init__(self):
        self.latency_sec = float(LATENCY_FIXED_MS) / 1000.0
        self.adaptive = bool(LATENCY_ADAPTIVE)

        # Адаптивная оценка латентности
        self._pred_hist = deque(maxlen=int(LATENCY_MEASURE_WINDOW))
        self._measure_enable = bool(LATENCY_MEASURE_ENABLE)

        # Статистика
        self.compensated_shift_px = 0.0
        self.ego_shift_px = 0.0

    def get_latency_sec(self, phase="TRACK"):
        """Возвращает текущую оценку латентности."""
        lat = self.latency_sec
        if phase == "TERMINAL":
            lat += float(LATENCY_EXTRA_TERMINAL_MS) / 1000.0
        return float(clamp(lat,
                           float(LATENCY_MIN_MS) / 1000.0,
                           float(LATENCY_MAX_MS) / 1000.0))

    def compensate(self, kf_state, imu_state, frame_w, frame_h, phase="TRACK"):
        """
        Компенсирует латентность — предсказывает где цель БУДЕТ
        в момент исполнения команды.

        Args:
            kf_state: dict с полями из Kalman/IMM:
                cx, cy       — текущий центр цели (пиксели)
                vx, vy       — скорость цели (пиксели/сек)
                w, h         — размер bbox
                ax, ay       — ускорение (если IMM CA)
            imu_state: IMUState или None
            frame_w, frame_h: размеры кадра
            phase: текущая фаза FSM

        Returns:
            dict:
                comp_cx, comp_cy   — компенсированный центр цели
                comp_box           — компенсированный bbox [x1,y1,x2,y2]
                ego_dx, ego_dy     — смещение из-за собственного движения
                pred_dx, pred_dy   — смещение из-за движения цели
                latency_sec        — использованная латентность
                shift_px           — полное смещение (пиксели)
        """
        lat = self.get_latency_sec(phase)

        cx = float(kf_state.get("cx", frame_w * 0.5))
        cy = float(kf_state.get("cy", frame_h * 0.5))
        vx = float(kf_state.get("vx", 0.0))
        vy = float(kf_state.get("vy", 0.0))
        ax = float(kf_state.get("ax", 0.0))
        ay = float(kf_state.get("ay", 0.0))
        bw = float(kf_state.get("w", 20.0))
        bh = float(kf_state.get("h", 20.0))

        # ─── 1. Предсказание движения цели ──────────────────────
        # Линейная экстраполяция + ускорение (если есть)
        pred_dx = vx * lat + 0.5 * ax * lat * lat
        pred_dy = vy * lat + 0.5 * ay * lat * lat

        # ─── 2. Вычитание собственного движения ─────────────────
        ego_dx = 0.0
        ego_dy = 0.0

        if imu_state is not None and imu_state.valid:
            ego_dx, ego_dy = self._compute_ego_shift(
                imu_state, lat, frame_w, frame_h
            )

        # ─── 3. Компенсированная позиция ────────────────────────
        comp_cx = cx + pred_dx - ego_dx
        comp_cy = cy + pred_dy - ego_dy

        # Clamp к границам кадра
        comp_cx = float(clamp(comp_cx, 0.0, float(frame_w - 1)))
        comp_cy = float(clamp(comp_cy, 0.0, float(frame_h - 1)))

        # Компенсированный bbox
        comp_box = np.array([
            comp_cx - bw * 0.5,
            comp_cy - bh * 0.5,
            comp_cx + bw * 0.5,
            comp_cy + bh * 0.5,
        ], dtype=np.float32)
        comp_box = clip_box(comp_box, frame_w, frame_h)

        shift = float(np.hypot(pred_dx - ego_dx, pred_dy - ego_dy))
        self.compensated_shift_px = shift
        self.ego_shift_px = float(np.hypot(ego_dx, ego_dy))

        return {
            "comp_cx": float(comp_cx),
            "comp_cy": float(comp_cy),
            "comp_box": comp_box,
            "ego_dx": float(ego_dx),
            "ego_dy": float(ego_dy),
            "pred_dx": float(pred_dx),
            "pred_dy": float(pred_dy),
            "latency_sec": float(lat),
            "shift_px": float(shift),
        }

    def feed_measurement(self, predicted_center, actual_center, dt):
        """
        Для адаптивной оценки латентности.
        Вызывается когда YOLO даёт новую детекцию — сравниваем
        где мы предсказывали цель vs где она реально оказалась.

        Если prediction overshoots — латентность завышена.
        Если undershoots — занижена.
        """
        if not self._measure_enable or not self.adaptive:
            return

        if predicted_center is None or actual_center is None:
            return

        pred = np.array(predicted_center, dtype=np.float32)
        actual = np.array(actual_center, dtype=np.float32)
        error = float(np.linalg.norm(pred - actual))

        self._pred_hist.append({
            "error": error,
            "dt": float(dt),
        })

        # Пока простая эвристика: если средняя ошибка растёт,
        # уменьшаем латентность (overshooting)
        if len(self._pred_hist) >= 10:
            errors = [p["error"] for p in self._pred_hist]
            recent = np.mean(errors[-5:])
            older = np.mean(errors[:5])
            if recent > older * 1.3:
                self.latency_sec *= 0.95
            elif recent < older * 0.7:
                self.latency_sec *= 1.05
            self.latency_sec = float(clamp(
                self.latency_sec,
                float(LATENCY_MIN_MS) / 1000.0,
                float(LATENCY_MAX_MS) / 1000.0,
            ))

    def _compute_ego_shift(self, imu, lat, frame_w, frame_h):
        """
        Вычисляет смещение изображения из-за собственного вращения/движения
        дрона-перехватчика за время lat секунд.

        Используем угловые скорости IMU → пиксельное смещение через
        фокусное расстояние камеры.
        """
        focal = float(CAMERA_FOCAL_LENGTH_PX)
        if focal < 10.0:
            return 0.0, 0.0

        tilt_rad = float(CAMERA_TILT_DEG) * (math.pi / 180.0)

        # Угловые скорости (body frame → camera frame)
        # Камера наклонена вперёд, поэтому rotation mapping:
        # camera_pan  ≈ yaw_rate * cos(tilt) + pitch_rate * sin(tilt)
        # camera_tilt ≈ pitch_rate * cos(tilt) - yaw_rate * sin(tilt)
        cos_t = math.cos(tilt_rad)
        sin_t = math.sin(tilt_rad)

        # Угловая скорость камеры
        cam_pan_rate = imu.yaw_rate * cos_t + imu.pitch_rate * sin_t
        cam_tilt_rate = imu.pitch_rate * cos_t - imu.yaw_rate * sin_t
        cam_roll_rate = imu.roll_rate

        # Angular velocity → pixel shift
        # Для pinhole camera: dx_px ≈ focal * d_angle
        ego_dx = focal * cam_pan_rate * lat    # горизонтальное смещение
        ego_dy = focal * cam_tilt_rate * lat   # вертикальное смещение

        # Roll создаёт вращение вокруг центра — для малых углов:
        # dx_roll ≈ -(y - cy) * roll_rate * lat
        # dy_roll ≈  (x - cx) * roll_rate * lat
        # Это применяется к конкретной точке, здесь пропускаем
        # (применится в compensate() если нужно)

        return float(ego_dx), float(ego_dy)


class IMUReader:
    """
    Асинхронное чтение IMU с полётного контроллера.
    Работает в отдельном потоке.
    """

    def __init__(self):
        self.enabled = bool(IMU_ENABLE) and (IMU_SOURCE != "none")
        self.source = str(IMU_SOURCE).lower()
        self.state = IMUState()
        self._lock = threading.Lock()
        self._running = False
        self._thread = None
        self._conn = None

        # Сглаживание
        self._alpha = float(IMU_SMOOTH_ALPHA)
        self._smooth_vx = 0.0
        self._smooth_vy = 0.0
        self._smooth_vz = 0.0

    def start(self):
        if not self.enabled:
            return
        self._running = True
        self._thread = threading.Thread(target=self._loop, daemon=True)
        self._thread.start()

    def stop(self):
        self._running = False
        if self._thread is not None:
            self._thread.join(timeout=1.0)
        if self._conn is not None:
            try:
                self._conn.close()
            except Exception:
                pass

    def get_state(self):
        """Потокобезопасное чтение последнего состояния IMU."""
        with self._lock:
            s = IMUState()
            for attr in IMUState.__slots__:
                setattr(s, attr, getattr(self.state, attr))
            return s

    def status_line(self):
        if not self.enabled:
            return "IMU disabled"
        if self.state.valid:
            return (f"IMU ready: {self.source} "
                    f"rpy=({math.degrees(self.state.roll):.1f}, "
                    f"{math.degrees(self.state.pitch):.1f}, "
                    f"{math.degrees(self.state.yaw):.1f})")
        return f"IMU not ready: {self.source}"

    def _loop(self):
        """Главный цикл чтения IMU."""
        if self.source == "mavlink":
            self._loop_mavlink()
        elif self.source == "msp":
            self._loop_msp()

    def _loop_mavlink(self):
        """Чтение через MAVLink."""
        try:
            from pymavlink import mavutil
            self._conn = mavutil.mavlink_connection(
                str(IMU_MAVLINK_CONNECTION),
                baud=115200,
            )
        except Exception as e:
            print(f"[imu] MAVLink connection failed: {e}")
            return

        poll_interval = 1.0 / max(1.0, float(IMU_POLL_HZ))

        while self._running:
            try:
                # Читаем ATTITUDE
                msg = self._conn.recv_match(
                    type=["ATTITUDE", "LOCAL_POSITION_NED",
                          "SCALED_IMU2", "HIGHRES_IMU"],
                    blocking=True,
                    timeout=poll_interval,
                )
                if msg is None:
                    continue

                with self._lock:
                    msg_type = msg.get_type()

                    if msg_type == "ATTITUDE":
                        self.state.roll = float(msg.roll)
                        self.state.pitch = float(msg.pitch)
                        self.state.yaw = float(msg.yaw)
                        self.state.roll_rate = float(msg.rollspeed)
                        self.state.pitch_rate = float(msg.pitchspeed)
                        self.state.yaw_rate = float(msg.yawspeed)
                        self.state.timestamp = time.perf_counter()
                        self.state.valid = True

                    elif msg_type == "LOCAL_POSITION_NED":
                        raw_vx = float(msg.vx)
                        raw_vy = float(msg.vy)
                        raw_vz = float(msg.vz)
                        a = self._alpha
                        self._smooth_vx = a * self._smooth_vx + (1 - a) * raw_vx
                        self._smooth_vy = a * self._smooth_vy + (1 - a) * raw_vy
                        self._smooth_vz = a * self._smooth_vz + (1 - a) * raw_vz
                        self.state.vx = self._smooth_vx
                        self.state.vy = self._smooth_vy
                        self.state.vz = self._smooth_vz

                    elif msg_type in ("SCALED_IMU2", "HIGHRES_IMU"):
                        if hasattr(msg, "xacc"):
                            self.state.ax = float(msg.xacc) / 1000.0 * IMU_GRAVITY
                            self.state.ay = float(msg.yacc) / 1000.0 * IMU_GRAVITY
                            self.state.az = float(msg.zacc) / 1000.0 * IMU_GRAVITY

            except Exception:
                time.sleep(poll_interval)

    def _loop_msp(self):
        """Чтение через MSP (Betaflight/INAV)."""
        try:
            import serial
            self._conn = serial.Serial(
                str(IMU_MSP_PORT),
                int(IMU_MSP_BAUD),
                timeout=0.02,
            )
        except Exception as e:
            print(f"[imu] MSP serial open failed: {e}")
            return

        poll_interval = 1.0 / max(1.0, float(IMU_POLL_HZ))

        while self._running:
            try:
                # Запрашиваем MSP_ATTITUDE (108)
                attitude = self._msp_request(108, 6)
                if attitude is not None and len(attitude) >= 6:
                    roll_deci = int.from_bytes(attitude[0:2], 'little', signed=True)
                    pitch_deci = int.from_bytes(attitude[2:4], 'little', signed=True)
                    yaw_deg = int.from_bytes(attitude[4:6], 'little', signed=False)

                    with self._lock:
                        self.state.roll = float(roll_deci) / 10.0 * (math.pi / 180.0)
                        self.state.pitch = float(pitch_deci) / 10.0 * (math.pi / 180.0)
                        self.state.yaw = float(yaw_deg) * (math.pi / 180.0)
                        self.state.timestamp = time.perf_counter()
                        self.state.valid = True

                # Запрашиваем MSP_RAW_IMU (102) для угловых скоростей
                raw_imu = self._msp_request(102, 18)
                if raw_imu is not None and len(raw_imu) >= 18:
                    gx = int.from_bytes(raw_imu[6:8], 'little', signed=True)
                    gy = int.from_bytes(raw_imu[8:10], 'little', signed=True)
                    gz = int.from_bytes(raw_imu[10:12], 'little', signed=True)
                    # Betaflight gyro: raw → °/s зависит от настройки
                    # Типично: raw / 16.4 для ±2000°/s
                    scale = math.pi / (180.0 * 16.4)
                    with self._lock:
                        self.state.roll_rate = float(gx) * scale
                        self.state.pitch_rate = float(gy) * scale
                        self.state.yaw_rate = float(gz) * scale

                time.sleep(poll_interval)

            except Exception:
                time.sleep(poll_interval)

    def _msp_request(self, cmd, expected_len):
        """
        Отправляет MSP запрос и читает ответ.
        Формат MSP v1: $M< [0] [cmd] [checksum]
        Ответ:         $M> [size] [cmd] [data...] [checksum]
        """
        if self._conn is None:
            return None

        # Запрос (пустой payload)
        checksum = 0 ^ cmd
        packet = bytearray([
            ord('$'), ord('M'), ord('<'),
            0,     # size = 0
            cmd,
            checksum & 0xFF,
        ])
        self._conn.write(packet)
        self._conn.flush()

        # Чтение ответа
        header = self._conn.read(5)  # $M> size cmd
        if len(header) < 5:
            return None
        if header[0:3] != b'$M>':
            return None

        size = header[3]
        cmd_resp = header[4]
        if size < expected_len:
            # Читаем что есть + checksum
            data = self._conn.read(size + 1)
            return data[:size] if len(data) >= size else None

        data = self._conn.read(size + 1)  # data + checksum
        if len(data) < size:
            return None
        return data[:size]


class SensorFusion:
    """
    Высокоуровневый модуль: объединяет IMU + Latency Compensation.

    Вызывается один раз в каждом кадре main loop.
    Принимает текущее состояние трекера и выдаёт
    компенсированную позицию/bbox для наведения.
    """

    def __init__(self):
        self.imu_reader = IMUReader()
        self.latency_comp = LatencyCompensator()
        self.enabled = True

    def start(self):
        self.imu_reader.start()

    def stop(self):
        self.imu_reader.stop()

    def status_line(self):
        imu_status = self.imu_reader.status_line()
        lat = self.latency_comp.latency_sec * 1000.0
        return f"Sensor fusion: latency={lat:.0f}ms | {imu_status}"

    def update(self, kf, locked_box, pred_box, frame_w, frame_h,
               phase="TRACK", confirmed=False):
        """
        Полный пайплайн компенсации.

        Args:
            kf:          Kalman8D или IMMFilter (с .x, .initialized)
            locked_box:  текущий locked bbox или None
            pred_box:    Kalman-предсказанный bbox или None
            frame_w, frame_h: размеры effective frame
            phase:       фаза FSM
            confirmed:   цель подтверждена

        Returns:
            dict:
                comp_center  — компенсированный центр [cx, cy]
                comp_box     — компенсированный bbox [x1, y1, x2, y2]
                raw_center   — исходный центр (без компенсации)
                imu_valid    — IMU доступен
                latency_ms   — использованная латентность
                shift_px     — смещение компенсации
                ego_speed    — скорость дрона (м/с)
                ego_yaw_rate — скорость рыскания (°/с)
        """
        result = {
            "comp_center": None,
            "comp_box": None,
            "raw_center": None,
            "imu_valid": False,
            "latency_ms": 0.0,
            "shift_px": 0.0,
            "ego_speed": 0.0,
            "ego_yaw_rate": 0.0,
        }

        # Определяем исходную позицию цели
        ref_box = locked_box if locked_box is not None else pred_box
        if ref_box is None or not kf.initialized:
            return result

        center = box_center(ref_box)
        w, h = box_wh(ref_box)
        result["raw_center"] = center.copy()

        # Состояние из Kalman/IMM
        kf_state = {
            "cx": float(center[0]),
            "cy": float(center[1]),
            "vx": float(kf.x[4, 0]),
            "vy": float(kf.x[5, 0]),
            "w": float(w),
            "h": float(h),
            "ax": 0.0,
            "ay": 0.0,
        }

        # Если IMM — берём ускорение из CA-модели
        if hasattr(kf, 'get_acceleration'):
            ax, ay = kf.get_acceleration()
            kf_state["ax"] = ax
            kf_state["ay"] = ay

        # Читаем IMU
        imu_state = self.imu_reader.get_state() if self.imu_reader.enabled else None
        result["imu_valid"] = (imu_state is not None and imu_state.valid)

        if imu_state is not None and imu_state.valid:
            speed = float(np.hypot(imu_state.vx, imu_state.vy))
            result["ego_speed"] = speed
            result["ego_yaw_rate"] = float(
                math.degrees(imu_state.yaw_rate)
            )

        # Компенсация латентности
        comp = self.latency_comp.compensate(
            kf_state, imu_state, frame_w, frame_h, phase
        )

        result["comp_center"] = np.array(
            [comp["comp_cx"], comp["comp_cy"]], dtype=np.float32
        )
        result["comp_box"] = comp["comp_box"]
        result["latency_ms"] = comp["latency_sec"] * 1000.0
        result["shift_px"] = comp["shift_px"]

        return result

    def feed_detection(self, predicted_center, actual_center, dt):
        """Для адаптивной калибровки латентности."""
        self.latency_comp.feed_measurement(predicted_center, actual_center, dt)

    def draw_overlay(self, frame_bgr, result, sx, sy):
        """Визуализация компенсации."""
        if not DRAW_LATENCY_COMP:
            return

        import cv2

        raw = result.get("raw_center")
        comp_center = result.get("comp_center")
        shift = result.get("shift_px", 0.0)
        lat = result.get("latency_ms", 0.0)

        if raw is not None and comp_center is not None:
            # Линия от raw к compensated
            rx = int(float(raw[0]) / max(1e-6, sx))
            ry = int(float(raw[1]) / max(1e-6, sy))
            cx = int(float(comp_center[0]) / max(1e-6, sx))
            cy = int(float(comp_center[1]) / max(1e-6, sy))

            color = (255, 128, 0)  # оранжевый
            cv2.arrowedLine(frame_bgr, (rx, ry), (cx, cy),
                            color, 2, cv2.LINE_AA)
            cv2.circle(frame_bgr, (cx, cy), 5, color, -1)

        # Текст
        y0 = 320
        imu_str = "IMU:OK" if result.get("imu_valid") else "IMU:---"
        ego = result.get("ego_speed", 0.0)
        yaw_r = result.get("ego_yaw_rate", 0.0)

        txt = f"LAT: {lat:.0f}ms  shift={shift:.1f}px  {imu_str}"
        cv2.putText(frame_bgr, txt, (20, y0),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 128, 0), 2)

        if DRAW_IMU_STATE and result.get("imu_valid"):
            txt2 = f"EGO: v={ego:.1f}m/s  yaw_r={yaw_r:.1f}d/s"
            cv2.putText(frame_bgr, txt2, (20, y0 + 25),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 128, 0), 2)


# =============================================================
# ИНТЕГРАЦИЯ В MAIN.PY
# =============================================================
#
# 1. Импорт:
#    from imu_fusion import SensorFusion
#
# 2. Инициализация (после guidance_ctrl):
#    sensor_fusion = SensorFusion()
#    sensor_fusion.start()
#    print(sensor_fusion.status_line())
#
# 3. В главном цикле, ПОСЛЕ guidance_state и ПЕРЕД autopilot:
#
#    fusion_result = sensor_fusion.update(
#        kf=kf,
#        locked_box=locked_box_eff,
#        pred_box=pred_box_eff,
#        frame_w=ew,
#        frame_h=eh,
#        phase=intercept_params.get("phase", "TRACK") if intercept_params else "TRACK",
#        confirmed=confirmed,
#    )
#
#    # Используем компенсированную позицию для PN и autopilot:
#    if fusion_result["comp_center"] is not None:
#        # Передаём в PN вместо raw center
#        target_center_for_pn = fusion_result["comp_center"]
#        target_box_for_guidance = fusion_result["comp_box"]
#
# 4. При получении новой YOLO-детекции — адаптивная калибровка:
#    if have_yolo and chosen_valid:
#        sensor_fusion.feed_detection(
#            predicted_center=box_center(pred_box_eff) if pred_box_eff is not None else None,
#            actual_center=box_center(chosen),
#            dt=dt,
#        )
#
# 5. Отрисовка:
#    sensor_fusion.draw_overlay(frame_orig, fusion_result, sx, sy)
#
# 6. Cleanup:
#    sensor_fusion.stop()
# =============================================================
