#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
激光管理器模块
提供激光控制、校准等功能
"""
import _thread
import json
import os
import binascii
from maix import time
import threading
import config
from logger_manager import logger_manager
import vision


class LaserManager:
    """激光控制管理器（单例）"""
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(LaserManager, cls).__new__(cls)
            cls._instance._initialized = False
        return cls._instance
    
    def __init__(self):
        if self._initialized:
            return
        
        # 私有状态
        self._serial = None  # 激光串口，由 laser_manager 自己持有
        self._calibration_active = False
        self._calibration_result = None
        self._calibration_lock = threading.Lock()
        self._laser_point = None
        self._laser_turned_on = False
        self._last_frame_with_ellipse = None  # 保存绘制了椭圆的图像（用于调试/显示）
        self._initialized = True
    
    # ==================== 状态访问（只读属性）====================
    
    @property
    def logger(self):
        """获取 logger 对象"""
        return logger_manager.logger
    
    @property
    def calibration_active(self):
        """是否正在校准"""
        return self._calibration_active
    
    @property
    def laser_point(self):
        """当前激光点（如果启用硬编码，则返回硬编码值）"""
        # if config.HARDCODE_LASER_POINT:
        #     return config.HARDCODE_LASER_POINT_VALUE
        return self._laser_point
    
    def get_last_frame_with_ellipse(self):
        """
        获取最后一次查找激光点时绘制了椭圆的图像（如果启用椭圆绘制）
        
        Returns:
            MaixPy 图像对象，如果没有则返回 None
        """
        return self._last_frame_with_ellipse
    
    # ==================== 初始化方法 ====================

    def init(self, serial_device=None, baudrate=None):
        """
        初始化激光模块（包括串口）
        初始化完成后主动发送关闭命令，防止 UART 初始化噪声误触发激光
        
        Args:
            serial_device: 串口设备路径，默认使用 config.DISTANCE_SERIAL_DEVICE
            baudrate: 波特率，默认使用 config.DISTANCE_SERIAL_BAUDRATE
        """
        from maix import uart
        device = serial_device or config.DISTANCE_SERIAL_DEVICE
        baud = baudrate or config.DISTANCE_SERIAL_BAUDRATE

        self._serial = uart.UART(device, baud)
        print(f"[LASER] 激光串口初始化完成: device={device}, baudrate={baud}")

        # 等待串口稳定后主动关闭激光，防止初始化噪声误触发
        time.sleep_ms(100)
        try:
            self._serial.read(-1)  # 清空接收缓冲区
        except Exception:
            pass
        self._serial.write(config.LASER_OFF_CMD)
        time.sleep_ms(60)
        try:
            self._serial.read(-1)  # 清空回包
        except Exception:
            pass
        print("[LASER] 已发送关闭命令（防止开机误触发）")

    # ==================== 业务方法 ====================
    
    def load_laser_point(self):
        """加载激光中心点：优先使用本地保存的坐标，其次硬编码值，最后默认值"""
        # 优先：从本地持久化文件加载（由 cmd 201 保存）
        try:
            if "laser_config.json" in os.listdir("/root"):
                with open(config.CONFIG_FILE, "r") as f:
                    data = json.load(f)
                    if isinstance(data, list) and len(data) == 2:
                        self._laser_point = (int(data[0]), int(data[1]))
                        self.logger.info(f"[LASER] 从本地加载激光点: {self._laser_point}")
                        return self._laser_point
        except Exception:
            pass

        # 其次：硬编码值
        if config.HARDCODE_LASER_POINT:
            self._laser_point = config.HARDCODE_LASER_POINT_VALUE
            self.logger.info(f"[LASER] 使用硬编码激光点: {self._laser_point}")
            return self._laser_point

        # 最后：默认值
        self._laser_point = config.DEFAULT_LASER_POINT
        self.logger.info(f"[LASER] 使用默认激光点: {self._laser_point}")
        return self._laser_point
    
    def save_laser_point(self, point):
        """保存激光中心点到配置文件
        如果启用硬编码模式，则不保存（直接返回 True）
        """
        if config.HARDCODE_LASER_POINT:
            # 硬编码模式：不保存到文件，但更新内存中的值（虽然不会被使用）
            self._laser_point = point
            self.logger.info(f"[LASER] 硬编码模式已启用，跳过保存激光点: {point}")
            return True
        
        # 正常模式：保存到配置文件
        try:
            with open(config.CONFIG_FILE, "w") as f:
                json.dump([point[0], point[1]], f)
            self._laser_point = point
            return True
        except Exception as e:
            self.logger.error(f"[LASER] 保存激光点失败: {e}")
            return False
    
    def turn_on_laser(self):
        """发送指令开启激光，并读取回包（部分模块支持）"""
        if self._serial is None:
            self.logger.error("[LASER] 激光串口未初始化，请先调用 init()")
            return None
        
        # 打印调试信息
        self.logger.info(f"[LASER] 发送开启命令: {config.LASER_ON_CMD.hex()}")
        
        # 清空接收缓冲区
        try:
            self._serial.read(-1)  # 清空缓冲区
        except:
            pass
        
        # 发送命令
        written = self._serial.write(config.LASER_ON_CMD)
        self.logger.info(f"[LASER] 写入字节数: {written}")
        
        time.sleep_ms(60)

        # 读取回包
        resp = self._serial.read(len=20, timeout=10)
        if resp:
            self.logger.info(f"[LASER] 收到回包 ({len(resp)}字节): {resp.hex()}")
            if resp == config.LASER_ON_CMD:
                self.logger.info("✅ 激光开启指令已确认")
        else:
            self.logger.warning("🔇 无回包（可能正常或模块不支持回包）")
        self._laser_turned_on = True
        return resp
    
    def turn_off_laser(self):
        """发送指令关闭激光"""
        if self._serial is None:
            self.logger.error("[LASER] 激光串口未初始化，请先调用 init()")
            return None
        
        # 打印调试信息
        self.logger.info(f"[LASER] 发送关闭命令: {config.LASER_OFF_CMD.hex()}")
        
        # 清空接收缓冲区
        try:
            self._serial.read(-1)
        except:
            pass
        
        # 发送命令
        written = self._serial.write(config.LASER_OFF_CMD)
        self.logger.info(f"[LASER] 写入字节数: {written}")

        time.sleep_ms(60)
        
        # 读取回包
        resp = self._serial.read(20)
        if resp:
            self.logger.info(f"[LASER] 收到回包 ({len(resp)}字节): {resp.hex()}")
        else:
            self.logger.warning("🔇 无回包")
        self._laser_turned_on = False
        return resp

    def flash_laser(self, duration_ms=1000):
        """闪一下激光（非阻塞版本）"""
        try:
            # 1. 先打开激光（主线程立刻执行，不等待）
            was_on = self._laser_turned_on
            self.turn_on_laser()

            # 2. 只有之前是激光off的状态下，才会启动一个子线程去处理“延时关闭”的逻辑，否则就保持着
            if not was_on:
                _thread.start_new_thread(self._laser_delay_off, (duration_ms, was_on))

        except Exception as e:
            self.logger.error(f"启动闪激光线程失败: {e}")

    def _laser_delay_off(self, duration_ms, was_on):
        """子线程执行的函数：等待一段时间后关闭激光"""
        try:
            # 这里 sleep 不会阻塞主线程了，只会阻塞这个不起眼的子线程
            time.sleep_ms(duration_ms)
            self.turn_off_laser()
            # print("[LASER] 射箭前激光已亮（调瞄中），保持激光开启")
        except Exception as e:
            # print(f"子线程关闭激光异常: {e}")
            pass
    # def find_red_laser(self, frame, threshold=150, search_radius=50):
    #     """
    #     在图像中心附近查找最亮的红色激光点（基于 RGB 阈值）
        
    #     Args:
    #         frame: 图像帧
    #         threshold: 红色通道阈值（默认150）
    #         search_radius: 搜索半径（像素），从图像中心开始搜索（默认150）
        
    #     Returns:
    #         (x, y) 坐标，如果未找到则返回 None
    #     """
    #     w, h = frame.width(), frame.height()
    #     center_x, center_y = w // 2, h // 2
        
    #     # 只在中心区域搜索
    #     x_min = max(0, center_x - search_radius)
    #     x_max = min(w, center_x + search_radius)
    #     y_min = max(0, center_y - search_radius)
    #     y_max = min(h, center_y + search_radius)
        
    #     img_bytes = frame.to_bytes()
    #     max_score = 0
    #     best_pos = None
        
    #     for y in range(y_min, y_max, 2):
    #         for x in range(x_min, x_max, 2):
    #             idx = (y * w + x) * 3
    #             r, g, b = img_bytes[idx], img_bytes[idx+1], img_bytes[idx+2]
                
    #             # 判断是否为红色或过曝的红色（发白）
    #             is_red = False
    #             is_overexposed_red = False
                
    #             # 情况1：正常红色（r 明显大于 g 和 b）
    #             if r > threshold and r > g * 2 and r > b * 2:
    #                 is_red = True
                
    #             # 情况2：过曝的红色（发白，r, g, b 都接近255，但 r 仍然最大）
    #             # 过曝时，r, g, b 都接近 255，但 r 应该仍然是最高的
    #             elif r > 200 and g > 200 and b > 200:  # 接近白色
    #                 if r >= g and r >= b and (r - g) > 10 and (r - b) > 10:  # r 仍然明显最大
    #                     is_overexposed_red = True
                
    #             if is_red or is_overexposed_red:
    #                 # 计算得分：RGB 总和 + 距离中心权重（越靠近中心得分越高）
    #                 rgb_sum = r + g + b
    #                 # 计算到中心的距离
    #                 dx = x - center_x
    #                 dy = y - center_y
    #                 distance_from_center = (dx * dx + dy * dy) ** 0.5
    #                 # 距离权重：距离越近，权重越高（最大权重为 1.0，距离为 0 时）
    #                 # 当距离为 search_radius 时，权重为 0.5
    #                 distance_weight = 1.0 - (distance_from_center / search_radius) * 0.5
    #                 distance_weight = max(0.5, distance_weight)  # 最小权重 0.5
                    
    #                 # 综合得分：RGB 总和 * 距离权重
    #                 score = rgb_sum * distance_weight
                    
    #                 if score > max_score:
    #                     max_score = score
    #                     best_pos = (x, y)
    #     print("best_pos:", best_pos)
    #     return best_pos
    def _is_point_in_ellipse(self, point, ellipse_params):
        """
        判断点是否在椭圆内
        
        Args:
            point: 点坐标 (x, y)
            ellipse_params: 椭圆参数 ((center_x, center_y), (width, height), angle)
        
        Returns:
            bool: 如果点在椭圆内返回 True，否则返回 False
        """
        if ellipse_params is None:
            return True  # 如果没有椭圆参数，不进行限制
        
        import math
        
        (cx, cy), (width, height), angle = ellipse_params
        px, py = point
        
        # 椭圆半长轴和半短轴
        a = width / 2.0  # 半长轴
        b = height / 2.0  # 半短轴
        
        # 将点坐标平移到椭圆中心
        dx = px - cx
        dy = py - cy
        
        # 旋转坐标系，使椭圆的长轴与x轴对齐
        # angle 是度，需要转换为弧度
        angle_rad = math.radians(angle)
        cos_a = math.cos(angle_rad)
        sin_a = math.sin(angle_rad)
        
        # 旋转后的坐标
        x_rot = dx * cos_a + dy * sin_a
        y_rot = -dx * sin_a + dy * cos_a
        
        # 检查点是否在椭圆内：((x_rot/a)^2 + (y_rot/b)^2) <= 1
        ellipse_value = (x_rot / a) ** 2 + (y_rot / b) ** 2
        
        return ellipse_value <= 1.0
    
    def find_red_laser_with_ellipse(self, frame, threshold=None, search_radius=None, ellipse_params=None):
        """
        使用椭圆拟合方法查找激光点中心（更准确）
        先找到所有红色像素，然后拟合椭圆找到中心
        
        Args:
            frame: 图像帧
            threshold: 红色通道阈值（如果为None，使用config.LASER_DETECTION_THRESHOLD）
            search_radius: 搜索半径（像素），从图像中心开始搜索（如果为None，使用config.LASER_SEARCH_RADIUS）
            ellipse_params: 椭圆参数 ((center_x, center_y), (width, height), angle)，用于限制激光点必须在椭圆内
        
        Returns:
            (x, y) 坐标，如果未找到则返回 None
            注意：如果启用 LASER_DRAW_ELLIPSE，会在原始 frame 上绘制椭圆（会修改输入图像）
        """
        import cv2
        import numpy as np
        from maix import image
        self.logger.debug(f"find_red_laser_with_ellipse start: {time.ticks_ms()}")
        # 使用配置项
        if threshold is None:
            threshold = config.LASER_DETECTION_THRESHOLD
        if search_radius is None:
            search_radius = config.LASER_SEARCH_RADIUS
        
        red_ratio = config.LASER_RED_RATIO
        overexposed_threshold = config.LASER_OVEREXPOSED_THRESHOLD
        overexposed_diff = config.LASER_OVEREXPOSED_DIFF
        
        w, h = frame.width(), frame.height()
        center_x, center_y = w // 2, h // 2
        
        # 转换为 OpenCV 格式
        img_cv = image.image2cv(frame, False, False)
        
        # 只在中心区域搜索
        x_min = max(0, center_x - search_radius)
        x_max = min(w, center_x + search_radius)
        y_min = max(0, center_y - search_radius)
        y_max = min(h, center_y + search_radius)
        
        # 提取ROI区域（只处理搜索区域，而不是整个图像）
        roi = img_cv[y_min:y_max, x_min:x_max]
        if roi.size == 0:
            self.logger.debug("[LASER] ROI区域为空")
            return None
        
        # 分离RGB通道（向量化操作，比循环快得多）
        r_channel = roi[:, :, 0].astype(np.int32)  # 转换为int32避免溢出
        g_channel = roi[:, :, 1].astype(np.int32)
        b_channel = roi[:, :, 2].astype(np.int32)
        
        # 情况1：正常红色判断（向量化）
        # r > threshold and r > g * red_ratio and r > b * red_ratio
        mask_red = (r_channel > threshold) & \
                   (r_channel > (g_channel * red_ratio)) & \
                   (r_channel > (b_channel * red_ratio))
        
        # 情况2：过曝的红色判断（向量化）
        # r > overexposed_threshold and g > overexposed_threshold and b > overexposed_threshold
        # and r >= g and r >= b and (r - g) > overexposed_diff and (r - b) > overexposed_diff
        mask_overexposed = (r_channel > overexposed_threshold) & \
                           (g_channel > overexposed_threshold) & \
                           (b_channel > overexposed_threshold) & \
                           (r_channel >= g_channel) & \
                           (r_channel >= b_channel) & \
                           ((r_channel - g_channel) > overexposed_diff) & \
                           ((r_channel - b_channel) > overexposed_diff)
        
        # 合并两种情况的掩码
        mask_combined = mask_red | mask_overexposed
        
        # 转换为uint8格式
        mask_roi = mask_combined.astype(np.uint8) * 255
        
        self.logger.debug(f"ellipse fitting start: {time.ticks_ms()}")
        # 查找轮廓（只在搜索区域内）
        contours, _ = cv2.findContours(mask_roi, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        self.logger.debug(f"ellipse fitting end: {time.ticks_ms()}")
        if not contours:
            self.logger.debug("[LASER] 未找到红色像素区域")
            return None
        self.logger.debug(f"ellipse filtering start: {time.ticks_ms()}")
        # 找到最大的轮廓（应该是激光点）
        largest_contour = max(contours, key=cv2.contourArea)
        
        # 检查轮廓面积（太小可能是噪声）
        area = cv2.contourArea(largest_contour)
        min_area = config.LASER_MIN_AREA
        if area < min_area:
            self.logger.debug(f"[LASER] 红色区域太小（面积={area:.1f}），可能是噪声（最小={min_area}）")
            return None
        
        # 使用椭圆拟合找到中心
        laser_center = None
        outer_ellipse_params = None  # 外层红色椭圆参数
        inner_ellipse_params = None  # 内层亮度椭圆参数
        
        if len(largest_contour) >= 5:
            # 椭圆拟合需要至少5个点
            # 注意：需要将轮廓坐标转换回全图坐标
            contour_global = largest_contour.copy()
            for i in range(len(contour_global)):
                contour_global[i][0][0] += x_min
                contour_global[i][0][1] += y_min
            
            try:
                # 第一步：拟合外层红色椭圆
                (x_outer, y_outer), (width_outer, height_outer), angle_outer = cv2.fitEllipse(contour_global)
                outer_ellipse_params = ((x_outer, y_outer), (width_outer, height_outer), angle_outer)
                
                self.logger.debug(f"[LASER] 外层红色椭圆拟合成功: 中心=({x_outer:.1f}, {y_outer:.1f}), 尺寸=({width_outer:.1f}, {height_outer:.1f}), 角度={angle_outer:.1f}°, 面积={area:.1f}")
                
                # 第二步：在外层椭圆区域内，找亮度最高的像素
                # 创建外层椭圆的掩码
                outer_ellipse_mask = np.zeros((h, w), dtype=np.uint8)
                cv2.ellipse(outer_ellipse_mask,
                           (int(x_outer), int(y_outer)),
                           (int(width_outer/2), int(height_outer/2)),
                           angle_outer,
                           0, 360,
                           255, -1)  # 填充椭圆区域
                
                # 在外层椭圆区域内，计算每个像素的亮度（RGB总和）
                brightness = (img_cv[:, :, 0].astype(np.int32) + 
                             img_cv[:, :, 1].astype(np.int32) + 
                             img_cv[:, :, 2].astype(np.int32))
                
                # 只考虑外层椭圆区域内的像素
                brightness_masked = np.where(outer_ellipse_mask > 0, brightness, 0)
                
                # 找到亮度阈值（使用区域内亮度的较高百分位，比如80%）
                brightness_values = brightness_masked[brightness_masked > 0]
                if len(brightness_values) > 0:
                    brightness_threshold = np.percentile(brightness_values, 90)  # 取90%分位数
                    
                    # 创建亮度掩码（只保留高亮度像素）
                    brightness_mask = (brightness_masked >= brightness_threshold).astype(np.uint8) * 255
                    
                    # 查找亮度区域的轮廓
                    brightness_contours, _ = cv2.findContours(brightness_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
              
                    # 在 brightness_contours 处理部分，添加 else 分支处理 brightness_area < 1 的情况
                    if brightness_contours:
                        # 找到最大的亮度轮廓
                        largest_brightness_contour = max(brightness_contours, key=cv2.contourArea)
                        brightness_area = cv2.contourArea(largest_brightness_contour)
                        
                        if brightness_area >= 3 and len(largest_brightness_contour) >= 5:
                            # 第三步：拟合内层亮度椭圆
                            try:
                                (x_inner, y_inner), (width_inner, height_inner), angle_inner = cv2.fitEllipse(largest_brightness_contour)
                                inner_ellipse_params = ((x_inner, y_inner), (width_inner, height_inner), angle_inner)
                                laser_center = (float(x_inner), float(y_inner))
                                self.logger.debug(f"[LASER] 内层亮度椭圆拟合成功: 中心=({x_inner:.1f}, {y_inner:.1f}), 尺寸=({width_inner:.1f}, {height_inner:.1f}), 角度={angle_inner:.1f}°, 面积={brightness_area:.1f}")
                            except Exception as e:
                                # 内层椭圆拟合失败，使用质心
                                M = cv2.moments(largest_brightness_contour)
                                if M["m00"] != 0:
                                    cx = M["m10"] / M["m00"]
                                    cy = M["m01"] / M["m00"]
                                    laser_center = (float(cx), float(cy))
                                    self.logger.debug(f"[LASER] 内层亮度椭圆拟合失败，使用质心: {laser_center}, 错误: {e}")
                                else:
                                    # 质心计算失败，使用外层椭圆中心
                                    laser_center = (float(x_outer), float(y_outer))
                                    self.logger.debug(f"[LASER] 内层区域质心计算失败，使用外层椭圆中心: {laser_center}")
                        elif brightness_area >= 1:
                            # 面积太小，使用质心
                            M = cv2.moments(largest_brightness_contour)
                            if M["m00"] != 0:
                                cx = M["m10"] / M["m00"]
                                cy = M["m01"] / M["m00"]
                                laser_center = (float(cx), float(cy))
                                self.logger.debug(f"[LASER] 内层区域质心计算成功: {laser_center}")
                            else:
                                # 质心计算失败，使用外层椭圆中心
                                laser_center = (float(x_outer), float(y_outer))
                                self.logger.debug(f"[LASER] 内层区域质心计算失败，使用外层椭圆中心: {laser_center}")
                        else:
                            # brightness_area < 1，面积太小，直接使用外层椭圆中心
                            laser_center = (float(x_outer), float(y_outer))
                            self.logger.debug(f"[LASER] 内层亮度区域面积太小（{brightness_area:.1f}），使用外层椭圆中心: {laser_center}")
                    else:
                        # 没有找到亮度轮廓，使用外层椭圆中心
                        laser_center = (float(x_outer), float(y_outer))
                        self.logger.debug(f"[LASER] 外层椭圆区域内无有效亮度值，使用外层椭圆中心: {laser_center}")
                else:
                    # 没有亮度值，使用外层椭圆中心
                    laser_center = (float(x_outer), float(y_outer))
                    self.logger.debug(f"[LASER] 外层椭圆区域内无有效亮度值，使用外层椭圆中心: {laser_center}")
                
                # 如果启用绘制椭圆，在图像上绘制
                if config.LASER_DRAW_ELLIPSE:
                    import math
                    # 绘制外层红色椭圆（绿色）
                    cx_outer, cy_outer = int(x_outer), int(y_outer)
                    cv2.ellipse(img_cv, 
                            (cx_outer, cy_outer),
                            (int(width_outer/2), int(height_outer/2)),
                            angle_outer,
                            0, 360,
                            (0, 255, 0),  # 绿色 (RGB格式)
                            2)
                    
                    # 如果找到内层椭圆，绘制内层亮度椭圆（黄色）和中心点（红色）
                    if inner_ellipse_params is not None:
                        (x_inner, y_inner), (width_inner, height_inner), angle_inner = inner_ellipse_params
                        cx_inner, cy_inner = int(x_inner), int(y_inner)
                        # 绘制内层椭圆（黄色）
                        cv2.ellipse(img_cv,
                                (cx_inner, cy_inner),
                                (int(width_inner/2), int(height_inner/2)),
                                angle_inner,
                                0, 360,
                                (255, 255, 0),  # 黄色 (RGB格式)
                                2)
                        # 绘制内层椭圆中心点（红色，较大）
                        cv2.circle(img_cv, (cx_inner, cy_inner), 5, (255, 0, 0), -1)
                    else:
                        # 只绘制外层椭圆中心点（红色）
                        cv2.circle(img_cv, (cx_outer, cy_outer), 3, (255, 0, 0), -1)
                
                    
                    # 将绘制后的图像转换回 MaixPy 格式并保存到实例变量
                    from maix import image
                    self._last_frame_with_ellipse = image.cv2image(img_cv, False, False)
                    
                    if inner_ellipse_params:
                        self.logger.debug(f"[LASER] 已绘制双层椭圆: 外层(绿色)中心=({cx_outer}, {cy_outer}), 内层(黄色)中心=({cx_inner}, {cy_inner})")
                    else:
                        self.logger.debug(f"[LASER] 已绘制外层椭圆: 中心=({cx_outer}, {cy_outer})")
            except Exception as e:
                laser_ellipse_params = None
                # 椭圆拟合失败，使用质心
                M = cv2.moments(contour_global)
                if M["m00"] != 0:
                    cx = M["m10"] / M["m00"]
                    cy = M["m01"] / M["m00"]
                    laser_center = (float(cx), float(cy))
                    self.logger.debug(f"[LASER] 椭圆拟合失败，使用质心: {laser_center}, 错误: {e}")
                    self.logger.debug(f"ellipse filtering start: {time.ticks_ms()}")
        else:
            # 点太少，使用质心
            contour_global = largest_contour.copy()
            for i in range(len(contour_global)):
                contour_global[i][0][0] += x_min
                contour_global[i][0][1] += y_min
            
            M = cv2.moments(contour_global)
            if M["m00"] != 0:
                cx = M["m10"] / M["m00"]
                cy = M["m01"] / M["m00"]
                laser_center = (float(cx), float(cy))
                self.logger.debug(f"[LASER] 点太少（{len(largest_contour)}个），使用质心: {laser_center}")
        
        if laser_center is None:
            # 清除之前保存的椭圆图像
            self._last_frame_with_ellipse = None
            return None
        
        # 检查距离中心是否太远
        final_x, final_y = laser_center
        dx_final = final_x - center_x
        dy_final = final_y - center_y
        distance_from_center_final = (dx_final * dx_final + dy_final * dy_final) ** 0.5
        
        max_distance = config.LASER_MAX_DISTANCE_FROM_CENTER
        if distance_from_center_final > max_distance:
            self.logger.warning(f"[LASER] 激光点距离中心太远: 位置={laser_center}, "
                             f"距离中心={distance_from_center_final:.1f}像素, "
                             f"最大允许距离={max_distance}像素")
            return None
        
        # 检查是否在黄心椭圆范围内（如果启用）
        if config.LASER_REQUIRE_IN_ELLIPSE and ellipse_params is not None:
            if not self._is_point_in_ellipse(laser_center, ellipse_params):
                (ell_center, (ell_width, ell_height), ell_angle) = ellipse_params
                self.logger.warning(f"[LASER] 激光点不在黄心椭圆内: 位置={laser_center}, "
                                 f"椭圆中心={ell_center}, 椭圆尺寸=({ell_width:.1f}, {ell_height:.1f})")
                return None
        
        ellipse_info = ""
        if config.LASER_REQUIRE_IN_ELLIPSE and ellipse_params is not None:
            ellipse_info = f", 椭圆内检查: 通过"
        elif not config.LASER_REQUIRE_IN_ELLIPSE:
            ellipse_info = f", 椭圆检查: 已禁用"
        self.logger.debug(f"[LASER] 找到激光点（椭圆拟合）: 位置={laser_center}, "
                       f"距离中心={distance_from_center_final:.1f}像素{ellipse_info}")
        if config.LASER_DRAW_ELLIPSE and self._last_frame_with_ellipse is not None:
            self.logger.debug(f"[LASER] 已保存绘制了椭圆的图像，可通过 get_last_frame_with_ellipse() 获取")
        
        return laser_center

    def _find_red_laser_brightest(self, frame, threshold=None, search_radius=None, ellipse_params=None):
        """
        在图像中心附近查找最亮的红色激光点（基于 RGB 阈值）
        使用两阶段搜索：先粗搜索找到候选区域，再精细搜索找到最亮点
        如果启用 LASER_REQUIRE_IN_ELLIPSE，只有激光点落在黄心椭圆范围内才算有效
        
        Args:
            frame: 图像帧
            threshold: 红色通道阈值（如果为None，使用config.LASER_DETECTION_THRESHOLD）
            search_radius: 搜索半径（像素），从图像中心开始搜索（如果为None，使用config.LASER_SEARCH_RADIUS）
            ellipse_params: 椭圆参数 ((center_x, center_y), (width, height), angle)，用于限制激光点必须在椭圆内
                           如果 config.LASER_REQUIRE_IN_ELLIPSE 为 False，则忽略此参数
        
        Returns:
            (x, y) 坐标，如果未找到或不在椭圆内（如果启用检查）则返回 None
        """
        # 使用配置项，如果参数未提供则使用默认配置
        if threshold is None:
            threshold = config.LASER_DETECTION_THRESHOLD
        if search_radius is None:
            search_radius = config.LASER_SEARCH_RADIUS
        
        red_ratio = config.LASER_RED_RATIO
        overexposed_threshold = config.LASER_OVEREXPOSED_THRESHOLD
        overexposed_diff = config.LASER_OVEREXPOSED_DIFF
        
        w, h = frame.width(), frame.height()
        center_x, center_y = w // 2, h // 2
        
        # 只在中心区域搜索
        x_min = max(0, center_x - search_radius)
        x_max = min(w, center_x + search_radius)
        y_min = max(0, center_y - search_radius)
        y_max = min(h, center_y + search_radius)
        
        img_bytes = frame.to_bytes()
        max_score = 0
        candidate_pos = None
        
        # 用于调试：记录最接近但未满足条件的点
        best_near_red = None
        best_near_red_score = 0
        best_near_red_rgb = None
        
        # 第一阶段：粗搜索（每2像素采样），找到候选点
        for y in range(y_min, y_max, 2):
            for x in range(x_min, x_max, 2):
                idx = (y * w + x) * 3
                r, g, b = img_bytes[idx], img_bytes[idx+1], img_bytes[idx+2]
                
                # 判断是否为红色或过曝的红色（发白）
                is_red = False
                is_overexposed_red = False
                
                # 情况1：正常红色（使用配置的倍数要求）
                if r > threshold and r > g * red_ratio and r > b * red_ratio:
                    is_red = True
                
                # 情况2：过曝的红色（发白，r, g, b 都接近255，但 r 仍然最大）
                elif r > overexposed_threshold and g > overexposed_threshold and b > overexposed_threshold:
                    if r >= g and r >= b and (r - g) > overexposed_diff and (r - b) > overexposed_diff:
                        is_overexposed_red = True
                
                if is_red or is_overexposed_red:
                    # 计算得分：RGB 总和 + 距离中心权重
                    rgb_sum = r + g + b
                    dx = x - center_x
                    dy = y - center_y
                    distance_from_center = (dx * dx + dy * dy) ** 0.5
                    distance_weight = 1.0 - (distance_from_center / search_radius) * 0.5
                    distance_weight = max(0.5, distance_weight)
                    
                    score = rgb_sum * distance_weight
                    
                    if score > max_score:
                        max_score = score
                        candidate_pos = (x, y)
                else:
                    # 记录最接近但未满足条件的点（用于调试）
                    if r > threshold * 0.8:  # 至少接近阈值
                        rgb_sum = r + g + b
                        # 计算接近度分数
                        ratio_score = min(r / (g + 1), r / (b + 1))  # 避免除零
                        near_score = rgb_sum * ratio_score
                        if near_score > best_near_red_score:
                            best_near_red_score = near_score
                            best_near_red = (x, y)
                            best_near_red_rgb = (r, g, b)
        
        # 如果没有找到候选点，输出调试信息
        if candidate_pos is None:
            if best_near_red:
                self.logger.debug(f"[LASER] 未找到激光点，最接近的点: 位置={best_near_red}, RGB={best_near_red_rgb}, "
                               f"阈值={threshold}, 倍数要求={red_ratio}, r/g={best_near_red_rgb[0]/(best_near_red_rgb[1]+1):.2f}, "
                               f"r/b={best_near_red_rgb[0]/(best_near_red_rgb[2]+1):.2f}")
            else:
                self.logger.debug(f"[LASER] 未找到激光点，搜索区域: ({x_min}, {y_min}) 到 ({x_max}, {y_max}), "
                               f"阈值={threshold}, 倍数要求={red_ratio}")
            return None
        
        # 第二阶段：在候选点周围进行精细搜索（1像素间隔）
        # 在候选点周围 5x5 或 7x7 区域内找最亮的点
        refine_radius = 3  # 精细搜索半径（像素）
        cx, cy = candidate_pos
        
        x_min_fine = max(0, cx - refine_radius)
        x_max_fine = min(w, cx + refine_radius + 1)
        y_min_fine = max(0, cy - refine_radius)
        y_max_fine = min(h, cy + refine_radius + 1)
        
        max_brightness = 0
        best_pos = candidate_pos
        best_rgb = None
        
        # 精细搜索：1像素间隔，只考虑亮度（RGB总和）
        for y in range(y_min_fine, y_max_fine, 1):
            for x in range(x_min_fine, x_max_fine, 1):
                idx = (y * w + x) * 3
                r, g, b = img_bytes[idx], img_bytes[idx+1], img_bytes[idx+2]
                
                # 判断是否为红色或过曝的红色（使用配置的倍数要求）
                is_red = False
                is_overexposed_red = False
                
                if r > threshold and r > g * red_ratio and r > b * red_ratio:
                    is_red = True
                elif r > overexposed_threshold and g > overexposed_threshold and b > overexposed_threshold:
                    if r >= g and r >= b and (r - g) > overexposed_diff and (r - b) > overexposed_diff:
                        is_overexposed_red = True
                
                if is_red or is_overexposed_red:
                    rgb_sum = r + g + b
                    # 精细搜索阶段只考虑亮度，不考虑距离权重
                    if rgb_sum > max_brightness:
                        max_brightness = rgb_sum
                        best_pos = (x, y)
                        best_rgb = (r, g, b)
        
        # 检查找到的激光点是否满足条件
        if best_pos:
            final_x, final_y = best_pos
            dx_final = final_x - center_x
            dy_final = final_y - center_y
            distance_from_center_final = (dx_final * dx_final + dy_final * dy_final) ** 0.5
            
            # 检查1：距离中心是否太远
            max_distance = config.LASER_MAX_DISTANCE_FROM_CENTER
            if distance_from_center_final > max_distance:
                # 距离中心太远，拒绝这个结果
                self.logger.warning(f"[LASER] 找到的激光点距离中心太远: 位置={best_pos}, "
                                 f"距离中心={distance_from_center_final:.1f}像素, "
                                 f"最大允许距离={max_distance}像素, 拒绝此结果")
                return None
            
            # 检查2：是否在黄心椭圆范围内（仅在启用时检查）
            if config.LASER_REQUIRE_IN_ELLIPSE and ellipse_params is not None:
                if not self._is_point_in_ellipse(best_pos, ellipse_params):
                    # 不在椭圆内，拒绝这个结果
                    (ell_center, (ell_width, ell_height), ell_angle) = ellipse_params
                    self.logger.warning(f"[LASER] 找到的激光点不在黄心椭圆内: 位置={best_pos}, "
                                     f"椭圆中心={ell_center}, 椭圆尺寸=({ell_width:.1f}, {ell_height:.1f}), "
                                     f"椭圆角度={ell_angle:.1f}°, 拒绝此结果")
                    return None
            
            # 输出成功找到激光点的日志
            ellipse_info = ""
            if config.LASER_REQUIRE_IN_ELLIPSE and ellipse_params is not None:
                ellipse_info = f", 椭圆内检查: 通过"
            elif not config.LASER_REQUIRE_IN_ELLIPSE:
                ellipse_info = f", 椭圆检查: 已禁用"
            self.logger.debug(f"[LASER] 找到激光点: 位置={best_pos}, RGB={best_rgb}, "
                           f"亮度={max_brightness}, 距离中心={distance_from_center_final:.1f}像素{ellipse_info}, "
                           f"阈值={threshold}, 倍数要求={red_ratio}")
        
        return best_pos

    def find_red_laser(self, frame, threshold=None, search_radius=None, ellipse_params=None):
        """
        查找激光点（支持两种方法：椭圆拟合或最亮点）
        根据 config.LASER_USE_ELLIPSE_FITTING 配置选择使用哪种方法
        
        Args:
            frame: 图像帧
            threshold: 红色通道阈值（如果为None，使用config.LASER_DETECTION_THRESHOLD）
            search_radius: 搜索半径（像素），从图像中心开始搜索（如果为None，使用config.LASER_SEARCH_RADIUS）
            ellipse_params: 椭圆参数 ((center_x, center_y), (width, height), angle)，用于限制激光点必须在椭圆内
        
        Returns:
            (x, y) 坐标，如果未找到则返回 None
        """
        if config.LASER_USE_ELLIPSE_FITTING:
            return self.find_red_laser_with_ellipse(frame, threshold, search_radius, ellipse_params)
        else:
            # 使用原来的最亮点方法
            return self._find_red_laser_brightest(frame, threshold, search_radius, ellipse_params)

    def calibrate_laser_position(self, timeout_ms=8000, check_sharpness=True):
        """
        执行激光校准：循环拍照 → 检测靶心 → 检查激光点清晰度 → 找红点 → 保存坐标
        只有检测到靶心时才读取激光点
        
        Args:
            timeout_ms: 超时时间（毫秒），默认8000ms
            check_sharpness: 是否检查激光点清晰度，默认True
        
        Returns:
            (x, y) 坐标，如果超时或失败则返回 None
        """
        from camera_manager import camera_manager
        # from vision import check_laser_point_sharpness, save_calibration_image, detect_circle_v3
        import vision
        from maix import time
        
        start = time.ticks_ms()
        
        # 注意：使用 abs(time.ticks_diff(start, time.ticks_ms())) 避免负数问题
        while self._calibration_active and abs(time.ticks_diff(start, time.ticks_ms())) < timeout_ms:
            try:
                # 使用全局 camera_manager，线程安全读取
                frame = camera_manager.read_frame()
                
                # 先检测靶心（仅在需要椭圆检查时）
                ellipse_params_temp = None
                center_temp = None
                radius_temp = None
                if config.LASER_REQUIRE_IN_ELLIPSE:
                    # 使用统一的检测接口（支持ArUco和传统方法）
                    result_img_temp, center_temp, radius_temp, method_temp, best_radius1_temp, ellipse_params_temp = vision.detect_target(frame, None)
                    
                    # 只有检测到靶心时才继续处理激光点
                    if center_temp is None or radius_temp is None:
                        self.logger.debug(f"[LASER] 未检测到靶心，跳过")
                        time.sleep_ms(60)
                        continue
                
                # 检测到靶心，继续处理激光点
                # 检查激光点清晰度（可选）
                sharpness_score = None  # 初始化清晰度分数
                if check_sharpness:
                    try:
                        # 使用 check_laser_point_sharpness 检测激光点清晰度
                        # 该函数会自动查找激光点并检测其清晰度
                        # 仅在启用椭圆检查时传入椭圆参数
                        is_sharp, sharpness_score, laser_pos = vision.check_laser_point_sharpness(
                            frame, 
                            laser_point=None,  # 自动查找激光点
                            roi_size=30,
                            threshold=config.IMAGE_SHARPNESS_THRESHOLD,
                            ellipse_params=ellipse_params_temp if config.LASER_REQUIRE_IN_ELLIPSE else None
                        )
                        
                        if laser_pos is None:
                            # 未找到激光点
                            self.logger.debug(f"[LASER] 未找到激光点，跳过")
                            time.sleep_ms(60)
                            continue
                        
                        if not is_sharp:
                            # 激光点模糊
                            self.logger.debug(f"[LASER] 激光点模糊（清晰度: {sharpness_score:.2f}），跳过")
                            time.sleep_ms(60)
                            continue
                        
                        # 激光点清晰，使用找到的激光点位置
                        pos = laser_pos
                        
                    except Exception as e:
                        self.logger.warning(f"[LASER] 激光点清晰度检测失败: {e}，继续处理")
                        # 检测失败时，回退到原来的方法：直接查找激光点
                        # 仅在启用椭圆检查时传入椭圆参数
                        pos = self.find_red_laser(frame, ellipse_params=ellipse_params_temp if config.LASER_REQUIRE_IN_ELLIPSE else None)
                        if pos is None:
                            time.sleep_ms(60)
                            continue
                else:
                    # 不检查清晰度，直接查找激光点
                    # 仅在启用椭圆检查时传入椭圆参数
                    pos = self.find_red_laser(frame, ellipse_params=ellipse_params_temp if config.LASER_REQUIRE_IN_ELLIPSE else None)
                    if pos is None:
                        time.sleep_ms(60)
                        continue
                
                # 找到清晰的激光点，保存校准图像
                if pos:
                    # 保存校准图像（带标注）
                    try:
                        # 如果使用椭圆拟合且启用了椭圆绘制，使用绘制了椭圆的图像
                        frame_to_save = frame
                        if config.LASER_USE_ELLIPSE_FITTING and config.LASER_DRAW_ELLIPSE:
                            frame_with_ellipse = self.get_last_frame_with_ellipse()
                            if frame_with_ellipse is not None:
                                frame_to_save = frame_with_ellipse
                        
                        vision.save_calibration_image(frame_to_save, pos)
                    except Exception as e:
                        self.logger.error(f"[LASER] 保存校准图像失败: {e}")
                    
                    # 设置结果、停止校准、保存坐标
                    self.set_calibration_result(pos)
                    self.stop_calibration()
                    self.save_laser_point(pos)
                    
                    if sharpness_score is not None:
                        self.logger.info(f"✅ 校准成功: {pos} (清晰度: {sharpness_score:.2f}, 靶心: {center_temp}, 半径: {radius_temp})")
                    else:
                        self.logger.info(f"✅ 校准成功: {pos} (靶心: {center_temp}, 半径: {radius_temp})")
                    return pos
                
                # 未找到激光点，继续循环
                time.sleep_ms(60)
                
            except Exception as e:
                self.logger.error(f"[LASER] 校准过程异常: {e}")
                import traceback
                self.logger.error(traceback.format_exc())
                time.sleep_ms(200)
        
        # 超时或校准被停止
        if self._calibration_active:
            self.logger.warning(f"[LASER] 校准超时（{timeout_ms}ms）")
        else:
            self.logger.info("[LASER] 校准已停止")
        
        return None
    
    def start_calibration(self):
        """开始校准（公共方法）"""
        with self._calibration_lock:
            if self._calibration_active:
                return False
            self._calibration_active = True
            self._calibration_result = None
            return True
    
    def stop_calibration(self):
        """停止校准（公共方法）"""
        with self._calibration_lock:
            self._calibration_active = False
    
    def set_calibration_result(self, result):
        """设置校准结果（内部方法）"""
        with self._calibration_lock:
            self._calibration_result = result
    
    def get_calibration_result(self):
        """获取并清除校准结果（内部方法）"""
        with self._calibration_lock:
            result = self._calibration_result
            self._calibration_result = None
            return result
    
    def parse_bcd_distance(self, bcd_bytes: bytes) -> float:
        """将 4 字节 BCD 码转换为距离（米）"""
        if len(bcd_bytes) != 4:
            return 0.0
        try:
            hex_string = binascii.hexlify(bcd_bytes).decode()
            distance_int = int(hex_string)
            return distance_int / 1000.0
        except Exception as e:
            self.logger.error(f"[LASER] BCD 解析失败: {e}")
            return 0.0
    
    def read_distance_from_laser_sensor(self):
        """发送测距指令并返回距离（米）和信号质量
        返回: (distance_m, signal_quality) 元组，失败返回 (0.0, 0)
        """
        if self._serial is None:
            self.logger.error("[LASER] 激光串口未初始化，请先调用 init()")
            return (0.0, 0)
        
        try:
            # 清空缓冲区
            try:
                self._serial.read(-1)
            except:
                pass
            # 打开激光

            self.turn_on_laser()
            self._laser_turned_on = True
            # time.sleep_ms(500) # 需要一定时间让激光稳定
            # 发送测距查询命令
            self._serial.write(config.DISTANCE_QUERY_CMD)
            # time.sleep_ms(500) # 测试结果：这里的等待没有用！
            self.turn_off_laser()
            self._laser_turned_on = False

            # 这里的等待才是有效的！大概350ms能读到数据
            # 循环读取响应，最多等待500ms
            start_time = time.ticks_ms()
            max_wait_ms = 500
            response = None
            
            while True:
                # 检查是否超时
                # 注意：使用 time.ticks_diff(start_time, time.ticks_ms()) 避免负数问题
                elapsed_ms = abs(time.ticks_diff(start_time, time.ticks_ms()))
                if elapsed_ms >= max_wait_ms:
                    self.logger.warning(f"[LASER] 读取超时 ({elapsed_ms}ms)，未收到完整响应")
                    return (0.0, 0)
                
                # 尝试读取数据
                response = self._serial.read(config.DISTANCE_RESPONSE_LEN)
                
                # 如果读到完整数据，立即返回
                if response and len(response) == config.DISTANCE_RESPONSE_LEN:
                    elapsed_ms = abs(time.ticks_diff(start_time, time.ticks_ms()))
                    self.logger.debug(f"[LASER] 收到响应 ({elapsed_ms}ms)")
                    break
                
                # 如果还没超时，短暂等待后继续尝试
                time.sleep_ms(10)  # 每次循环等待10ms，避免CPU占用过高
            
            # 验证响应格式
            if response and len(response) == config.DISTANCE_RESPONSE_LEN:
                if response[3] != 0x20:
                    if response[0] == 0xEE:
                        err_code = (response[7] << 8) | response[8]
                        self.logger.warning(f"[LASER] 模块错误代码: {hex(err_code)}")
                    return (0.0, 0)
                
                # 解析BCD码距离
                bcd_bytes = response[6:10]
                distance_value_m = self.parse_bcd_distance(bcd_bytes)
                signal_quality = (response[10] << 8) | response[11]
                
                self.logger.debug(f"[LASER] 测距成功: {distance_value_m:.3f} m, 信号质量: {signal_quality}")
                return (distance_value_m, signal_quality)
            
            self.logger.warning(f"[LASER] 无效响应: {response.hex() if response else 'None'}")
            return (0.0, 0)
        except Exception as e:
            self.logger.error(f"[LASER] 读取激光测距失败: {e}")
            return (0.0, 0)
    
    def calculate_laser_point_from_distance(self, distance_m):
        """
        根据目标距离动态计算激光点在图像中的坐标
        激光在摄像头下方，所以需要将图像中心的 y 值加上偏移
        
        Args:
            distance_m: 目标距离（米），例如到靶心的距离
        
        Returns:
            (x, y): 激光点在图像中的坐标
        """
        # from vision import estimate_pixel
        
        # 图像中心坐标
        center_x = config.IMAGE_CENTER_X
        center_y = config.IMAGE_CENTER_Y
        
        # 计算激光在摄像头下方的像素偏移（y 方向）
        # 激光在摄像头下方，所以 y 值要增加（向下为正）
        pixel_offset_y = estimate_pixel(config.LASER_CAMERA_OFFSET_CM, distance_m)
        
        # 激光点坐标：x 保持中心，y 加上偏移，
        laser_x = center_x
        laser_y = center_y + int(pixel_offset_y)
        
        self.logger.debug(f"[LASER] 根据距离 {distance_m:.2f}m 计算激光点: ({laser_x}, {laser_y}), 像素偏移: {pixel_offset_y:.2f}")
        
        return (laser_x, laser_y)
    
    def has_calibrated_point(self):
        """检查是否真正校准过（配置文件存在且不是默认值）"""
        if config.HARDCODE_LASER_POINT:
            return False  # 硬编码模式下不算校准
        
        # 检查配置文件是否存在
        if "laser_config.json" not in os.listdir("/root"):
            return False
        
        # 检查当前值是否是默认值
        if self._laser_point == config.DEFAULT_LASER_POINT:
            return False
        
        return self._laser_point is not None

    def compute_laser_position(self, circle_center, laser_point, radius, method, ellipse_params=None):
        """计算激光相对于靶心的偏移量（单位：厘米）
        
        Args:
            circle_center: 靶心中心坐标 (x, y)
            laser_point: 激光点坐标 (x, y)
            radius: 靶心半径（像素）
            method: 检测方法（"模糊" 或其他）
            ellipse_params: 椭圆参数，用于透视校正（可选）
        
        Returns:
            (dx, dy): 激光相对于靶心的偏移量（厘米），如果输入无效则返回 (None, None)
        """
        if not all([circle_center, radius, method]):
            return None, None
        
        cx, cy = circle_center
        lx, ly = laser_point
        
        # 如果有椭圆参数，使用透视校正计算
        if ellipse_params is not None and method != "aruco":
            return self._compute_with_perspective_correction(
                circle_center, laser_point, radius, ellipse_params
            )
        
        # 传统计算方法
        # r = 22.16 * 5
        r = radius * 5
        self.logger.debug(f"compute_laser_position: circle_center: {circle_center} laser_point: {laser_point} radius: {radius} method: {method} r: {r}")
        target_x = (lx-cx)/r*100
        target_y = (ly-cy)/r*100
        self.logger.info(f"lx：{lx} ly: {ly} cx: {cx} cy: {cy} result_x: {target_x} result_y: {-target_y} real_r_x: {lx-cx} real_r_y: {-1*(ly-cy)}")
        return (target_x, -target_y)
    
    def _compute_with_perspective_correction(self, circle_center, laser_point, radius, ellipse_params):
        """
        使用透视校正计算激光偏移
        
        当相机不正对靶子时，圆会变成椭圆。使用椭圆参数进行透视校正，
        将图像坐标转换到物理坐标系，再计算偏移。
        
        Args:
            circle_center: 靶心中心坐标 (x, y)
            laser_point: 激光点坐标 (x, y)
            radius: 靶心半径（像素）
            ellipse_params: ((center_x, center_y), (width, height), angle)
        
        Returns:
            (dx, dy): 校正后的偏移量（厘米）
        """
        import math
        
        try:
            (ex, ey), (width, height), angle = ellipse_params
            cx, cy = circle_center
            lx, ly = laser_point
            
            # 步骤1: 平移到椭圆中心
            dx1 = lx - cx
            dy1 = ly - cy
            
            # 步骤2: 旋转坐标系，使椭圆轴与坐标轴对齐
            angle_rad = math.radians(-angle)
            cos_a = math.cos(angle_rad)
            sin_a = math.sin(angle_rad)
            
            x_rot = dx1 * cos_a - dy1 * sin_a
            y_rot = dx1 * sin_a + dy1 * cos_a
            
            # 步骤3: 归一化到单位圆
            # 椭圆半轴
            a = width / 2.0
            b = height / 2.0
            
            # 归一化坐标
            if a > 0 and b > 0:
                x_norm = x_rot / a
                y_norm = y_rot / b
            else:
                x_norm = x_rot
                y_norm = y_rot
            
            # 步骤4: 计算物理距离
            # 使用平均半径作为参考
            avg_radius = (a + b) / 2.0
            pixels_per_cm = avg_radius / 20.0  # 假设靶心半径20cm对应avg_radius像素
            
            if pixels_per_cm > 0:
                # 归一化距离（单位：靶心半径的倍数）
                norm_distance = math.sqrt(x_norm**2 + y_norm**2)
                # 转换为厘米（假设靶心半径20cm）
                distance_cm = norm_distance * 20.0
                
                # 计算方向
                angle_offset = math.atan2(y_norm, x_norm)
                
                dx_cm = distance_cm * math.cos(angle_offset)
                dy_cm = -distance_cm * math.sin(angle_offset)  # Y轴翻转
                
                self.logger.debug(f"[PERSPECTIVE] 原始偏移: ({dx1:.1f}, {dy1:.1f})px, "
                                f"校正后: ({dx_cm:.2f}, {dy_cm:.2f})cm, "
                                f"椭圆: ({width:.1f}, {height:.1f}), 角度: {angle:.1f}°")
                
                return (dx_cm, dy_cm)
            else:
                # 回退到直接计算
                r = radius * 5
                return ((lx-cx)/r*100, -(ly-cy)/r*100)
                
        except Exception as e:
            self.logger.error(f"[PERSPECTIVE] 透视校正计算失败: {e}")
            # 回退到直接计算
            r = radius * 5
            return ((lx-cx)/r*100, -(ly-cy)/r*100)
        
        # # 根据检测方法动态调整靶心物理半径（简化模型）
        # circle_r = (radius / 4.0) * 20.0 if method == "模糊" else (68 / 16.0) * 20.0
        # dx = lx - cx
        # dy = ly - cy
        # return dx / (circle_r / 100.0), -dy / (circle_r / 100.0)
        
    def quick_measure_distance(self):
        """
        快速激光测距：打开激光 → 测距 → 关闭激光
        激光开启时间最小化（约500-600ms），尽量不让用户觉察到
        返回: (distance_m, signal_quality) 元组，失败返回 (0.0, 0)
        """
        self._laser_turned_on = False
        
        try:

            
            # 等待激光稳定（最小延迟）
            # time.sleep_ms(50)
            
            # 读取距离和信号质量
            result = self.read_distance_from_laser_sensor()
            return result
        except Exception as e:
            self.logger.error(f"[LASER] 快速测距异常: {e}")
            return (0.0, 0)
        finally:
            # 确保激光关闭
            if self._laser_turned_on:
                try:
                    self.turn_off_laser()
                except Exception as e:
                    self.logger.error(f"[LASER] 关闭激光失败: {e}")

    def set_hardcoded_laser_point(self, raw_x, raw_y):
        """
        设置服务下发的硬编码激光点坐标，并保存到本地持久化文件。
        下次启动时 load_laser_point() 会优先使用此保存的值。

        Args:
            raw_x: 服务下发的 x 坐标
            raw_y: 服务下发的 y 坐标

        Returns:
            (int_x, int_y) 元组
        """
        ix = int(raw_x)
        iy = int(raw_y)
        self._laser_point = (ix, iy)
        try:
            with open(config.CONFIG_FILE, "w") as f:
                json.dump([ix, iy], f)
            self.logger.info(f"[LASER] 设置并持久化激光点: ({ix}, {iy})")
        except Exception as e:
            self.logger.error(f"[LASER] 持久化激光点失败: {e}")
        return ix, iy

# 创建全局单例实例
laser_manager = LaserManager()



