#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
网络通信模块
提供TCP通信、数据包打包/解析、队列管理等功能
"""
import json
import re
from math import e
import struct
from maix import time, network, err
import hmac
import hashlib
import ujson
import os
import threading
import socket
import config

from hardware import hardware_manager
from power import get_bus_voltage, voltage_to_percent, is_charging
from logger_manager import logger_manager
from wifi import wifi_manager
import subprocess


def _wifi_tls_would_block(exc):
    """
    非阻塞 TLS 下 recv/send 常抛出 WANT_READ / WANT_WRITE（或等价文案），
    表示需等待对端/内核缓冲区，不是断线。
    """
    try:
        import ssl as _ssl
    except ImportError:
        _ssl = None
    if _ssl is not None and isinstance(exc, _ssl.SSLError):
        err = getattr(exc, "errno", None)
        if err in (
                getattr(_ssl, "SSL_ERROR_WANT_READ", 2),
                getattr(_ssl, "SSL_ERROR_WANT_WRITE", 3),
        ):
            return True
    msg = str(exc).lower()
    if "did not complete" in msg and ("read" in msg or "write" in msg):
        return True
    return False


class NetworkManager:
    """网络通信管理器（单例）"""
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(NetworkManager, cls).__new__(cls)
            cls._instance._initialized = False
        return cls._instance

    def __init__(self):
        if self._initialized:
            return

        # 私有状态
        self._tcp_connected = False
        self._high_send_queue = []
        self._normal_send_queue = []
        self._queue_lock = threading.Lock()
        self._send_event = threading.Event()
        self._uart4g_lock = threading.Lock()
        self._device_id = None
        self._password = None
        self._raw_line_data = []
        self._manual_trigger_flag = False

        # 限制并发命令线程数
        self._cmd_thread_lock = threading.Lock()
        self._cmd_thread_count = 0

        # 网络类型状态
        self._network_type = None  # "wifi" 或 "4G" 或 None
        # 本次上电曾因 WiFi 质量差切换到 4G 后，直至关机不再改回 WiFi
        self._session_force_4g = False

        self._initialized = True

        # 导入 archery_netcore 模块，并检查是否存在 parse_packet 和 make_packet 函数
        try:
            import archery_netcore as _netcore
            self._netcore = _netcore
            if hasattr(self._netcore, "parse_packet") and hasattr(self._netcore, "make_packet") and hasattr(
                    self._netcore, "actions_for_inner_cmd"):
                print("[NET] archery_netcore found")
            else:
                print("[NET] archery_netcore not found parse_packet or make_packet")
                exit(1)
        except Exception:
            print("[NET] import archery_netcore failed")
            exit(1)

        # 服务器相关
        self._server_ip = self._netcore.get_config().get("SERVER_IP")
        self._server_port = self._netcore.get_config().get("SERVER_PORT")

    # ==================== 状态访问（只读属性）====================

    @property
    def logger(self):
        """获取 logger 对象"""
        return logger_manager.logger

    @property
    def tcp_connected(self):
        """TCP连接状态"""
        return self._tcp_connected

    @property
    def device_id(self):
        """设备ID"""
        return self._device_id

    @property
    def password(self):
        """密码"""
        return self._password

    @property
    def has_pending_messages(self):
        """是否有待发送消息"""
        with self._queue_lock:
            return len(self._high_send_queue) > 0 or len(self._normal_send_queue) > 0

    @property
    def manual_trigger_flag(self):
        """手动触发标志"""
        return self._manual_trigger_flag

    @property
    def network_type(self):
        """当前使用的网络类型（"wifi" 或 "4g"）"""
        return self._network_type

    @property
    def wifi_connected(self):
        """WiFi是否已连接"""
        return wifi_manager.wifi_connected

    @property
    def wifi_ip(self):
        """WiFi IP地址"""
        return wifi_manager.wifi_ip

    # ==================== 内部状态管理方法 ====================

    def set_manual_trigger(self, value=True):
        """设置手动触发标志（公共方法）"""
        self._manual_trigger_flag = value

    def clear_manual_trigger(self):
        """清除手动触发标志（公共方法）"""
        self._manual_trigger_flag = False

    def _set_tcp_connected(self, connected):
        """设置TCP连接状态（内部方法）"""
        self._tcp_connected = connected

    def _set_device_info(self, device_id, password):
        """设置设备信息（内部方法）"""
        self._device_id = device_id
        self._password = password

    def _enqueue(self, item, high=False):
        """线程安全地加入队列（内部方法），队列满时丢弃最旧消息"""
        with self._queue_lock:
            if high:
                if len(self._high_send_queue) >= config.MAX_SEND_QUEUE_SIZE:
                    self._high_send_queue.pop(0)
                self._high_send_queue.append(item)
            else:
                if len(self._normal_send_queue) >= config.MAX_SEND_QUEUE_SIZE:
                    self._normal_send_queue.pop(0)
                self._normal_send_queue.append(item)
        self._send_event.set()

    def _dequeue(self):
        """线程安全地从队列取出（内部方法）"""
        with self._queue_lock:
            if self._high_send_queue:
                return self._high_send_queue.pop(0)
            elif self._normal_send_queue:
                return self._normal_send_queue.pop(0)
            return None

    def _set_raw_line_data(self, data):
        """设置原始行数据（内部方法）"""
        self._raw_line_data = data

    def _get_raw_line_data(self):
        """获取原始行数据（内部方法）"""
        return self._raw_line_data

    def get_uart_lock(self):
        """获取UART锁（用于with语句）"""
        return self._uart4g_lock

    def get_queue_lock(self):
        """获取队列锁（用于with语句）"""
        return self._queue_lock

    def _spawn_cmd_thread(self, target, args=()):
        """安全创建命令线程，限制并发数，防止无限创建导致内存耗尽"""
        with self._cmd_thread_lock:
            if self._cmd_thread_count >= config.MAX_CMD_THREADS:
                self.logger.warning(
                    f"[NET] 并发命令线程已达上限({config.MAX_CMD_THREADS})，跳过: {getattr(target, '__name__', str(target))}"
                )
                return False
            self._cmd_thread_count += 1

        def _wrapper(*a):
            try:
                target(*a)
            except Exception as e:
                self.logger.error(f"[NET] 命令线程异常: {e}")
            finally:
                with self._cmd_thread_lock:
                    self._cmd_thread_count -= 1

        import _thread
        _thread.start_new_thread(_wrapper, args)
        return True

    # ==================== 业务方法 ====================

    def read_device_id(self):
        """从 /device_key 文件读取设备唯一 ID，失败则使用默认值"""

        def _set_password_for_device_id(device_id):
            if getattr(config, "USE_TCP_SSL", False):
                iccid = self.get_4g_mccid()
                iccid = iccid if iccid else ""
                print(f"iccid: {iccid}")
                self._password = self._netcore.calculate_tcp_ssl_password(device_id, iccid)
            else:
                self.logger.error("[SSL] TCP SSL NOT enabled! exit!")
                exit(1)

        try:
            with open("/device_key", "r") as f:
                device_id = f.read().strip()
                if device_id:
                    self.logger.debug(f"[INFO] 从 /device_key 读取到 DEVICE_ID: {device_id}")
                    # 设置内部状态
                    self._device_id = device_id
                    _set_password_for_device_id(device_id)
                    return device_id
        except Exception as e:
            self.logger.error(f"[ERROR] 无法读取 /device_key: {e}")

        # 使用默认值
        default_id = "DEFAULT_DEVICE_ID"
        self._device_id = default_id
        _set_password_for_device_id(default_id)
        return default_id

    # ==================== WiFi 管理方法（委托给 wifi_manager）====================

    def is_wifi_connected(self):
        """检查WiFi是否已连接"""
        return wifi_manager.is_wifi_connected()

    def connect_wifi(self, ssid, password, verify_host=None, verify_port=None, persist=True, timeout_s=20):
        """
        连接 Wi-Fi：委托 ``wifi_manager.connect_wifi``。
        未指定 ``verify_host``/``verify_port`` 时，可达性校验使用本管理器配置的 ``_server_ip``/``_server_port``。
        """

        def _verify(ip: str):
            v_host = verify_host if verify_host is not None else self._server_ip
            v_port = verify_port if verify_port is not None else self._server_port
            try:
                v_port_i = int(v_port) if v_port is not None else None
            except (TypeError, ValueError):
                v_port_i = None
            if v_host and v_port_i:
                if not self.is_server_reachable(v_host, v_port_i, timeout=5):
                    return False, f"Target unreachable ({v_host}:{v_port_i})"
            return True, ""

        return wifi_manager.connect_wifi(
            ssid,
            password,
            verify_callback=_verify,
            persist=persist,
            timeout_s=timeout_s,
        )

    def is_server_reachable(self, host, port=80, timeout=5):
        """检查目标主机端口是否可达（用于网络检测）"""
        try:
            addr_info = socket.getaddrinfo(host, port)[0]
            s = socket.socket(addr_info[0], addr_info[1], addr_info[2])
            s.settimeout(timeout)
            s.connect(addr_info[-1])
            s.close()
            return True
        except Exception as e:
            self.logger.warning(f"[NET] 无法连接 {host}:{port} - {e}")
            return False

    # ==================== 网络选择策略 ====================

    def _get_wifi_rssi_dbm(self):
        """获取 WiFi 信号强度（委托给 wifi_manager）"""
        return wifi_manager._get_wifi_rssi_dbm()

    def _measure_wifi_tcp_rtt_ms(self, host, port, samples=3, per_sample_timeout_ms=900):
        """测量 WiFi TCP RTT（委托给 wifi_manager）"""
        return wifi_manager._measure_wifi_tcp_rtt_ms(host, port, samples, per_sample_timeout_ms)

    def _is_wifi_quality_bad(self, wifi_rtt_ms, wifi_rssi_dbm):
        """判断 WiFi 质量是否差（委托给 wifi_manager）"""
        return wifi_manager._is_wifi_quality_bad(wifi_rtt_ms, wifi_rssi_dbm)

    def is_4g_available(self):
        """
        快速判断 4G 是否可用（不做 DNS，仅验证 SIM/附着/有 IP）
        兼容性兜底：若未拿到 IP，再补充设置 APN 并激活 PDP context
        """
        try:
            atc = hardware_manager.at_client
            if atc is None:
                return False

            if not self._uart4g_lock.acquire(timeout=3000):
                self.logger.warning("[4G] 获取 uart4g_lock 超时，跳过 4G 可用性检查")
                return False
            try:
                # 1) SIM 就绪
                r = atc.send("AT+CPIN?", "READY", 3000)
                if "READY" not in r:
                    return False

                # 2) 尝试附着到网络
                # 不同模块返回可能略有差异，所以做宽松解析
                r2 = atc.send("AT+CGATT?", "OK", 3000)
                m = re.search(r"\+CGATT:\s*(\d+)", r2)
                attached = int(m.group(1)) == 1 if m else None
                if attached is False or attached is None:
                    atc.send("AT+CGATT=1", "OK", 5000)

                # 3) 查询 PDP 地址（有 IP 表示网络可用）
                def _extract_ip(resp: str):
                    m3 = re.search(r"(\d{1,3}\.){3}\d{1,3}", resp or "")
                    if not m3:
                        return None
                    ip = m3.group(0)
                    if ip.startswith("0.") or ip.startswith("127."):
                        return None
                    return ip

                r3 = atc.send("AT+CGPADDR=1", "OK", 3000)
                ip = _extract_ip(r3)
                if ip:
                    return True

                # 4) 若没 IP：补充设置 APN + 激活 PDP（某些网络/模组需要这一步）
                atc.send('AT+CGDCONT=1,"IP","CMNET"', "OK", 3000)

                qact_resp = atc.send("AT+CGACT?", "OK", 3000)
                if "+CGACT:" not in (qact_resp or "") or "1,1" not in (qact_resp or ""):
                    atc.send("AT+CGACT=1,1", "OK", 10000)

                r4 = atc.send("AT+CGPADDR=1", "OK", 3000)
                ip2 = _extract_ip(r4)
                if ip2:
                    return True
                return False
            finally:
                self._uart4g_lock.release()
        except Exception:
            return False

    def get_4g_phone_number(self):
        """
        读取 SIM 本机号码（AT+CNUM）。
        典型响应：+CNUM: "","+861442093407954",145
        部分运营商/卡未在卡内写入号码时可能为空。
        Returns:
            str: 号码（含国家码，如 +86138...），失败或未写入时返回 None
        """
        try:
            atc = hardware_manager.at_client
            if atc is None:
                return None
            if not self._uart4g_lock.acquire(timeout=3000):
                self.logger.warning("[4G] get_4g_phone_number 获取锁超时")
                return None
            try:
                resp = atc.send("AT+CNUM", "OK", 3000)
            finally:
                self._uart4g_lock.release()
            if not resp:
                return None
            # 可能多行 +CNUM，取第一个非空号码
            for m in re.finditer(r'\+CNUM:\s*"[^"]*"\s*,\s*"([^"]*)"', resp):
                num = (m.group(1) or "").strip()
                if num:
                    return num
            return None
        except Exception:
            return None

    def get_4g_mccid(self):
        """
        读取 MCCID（AT+MCCID，模组侧命令，常用于 SIM/卡标识类信息）。
        典型响应行：+MCCID: <值> 或 +MCCID: \"...\"
        Returns:
            str: 解析到的字符串；失败时返回 None
        """
        try:
            atc = hardware_manager.at_client
            if atc is None:
                return None
            if not self._uart4g_lock.acquire(timeout=3000):
                self.logger.warning("[4G] get_4g_mccid 获取锁超时")
                return None
            try:
                resp = atc.send("AT+MCCID", "OK", 3000)
            finally:
                self._uart4g_lock.release()
            if not resp or "ERROR" in resp.upper():
                return None
            m = re.search(r"\+MCCID:\s*(.+)", resp, re.IGNORECASE)
            if not m:
                return None
            val = (m.group(1) or "").strip()
            # 去掉行尾 OK 之前可能粘在一起的杂质：只取第一行有效内容
            val = val.split("\r")[0].split("\n")[0].strip()
            val = val.strip('"').strip()
            return val if val else None
        except Exception:
            return None

    def _maybe_add_iccid_to_login(self, login_data):
        """
        若应用目录下尚无 iccid 标记文件，则在登录包中增加 iccid 字段（值为当前卡号）。
        标记文件仅在「本次登录携带了 iccid 且服务器返回登录成功」后创建，见 _create_iccid_marker_file。
        Returns:
            bool: 本次登录是否携带了 iccid（即成功后需要创建标记文件）
        """
        marker_path = os.path.join(config.APP_DIR, "iccid")
        # if os.path.exists(marker_path):
        #     return False #让iccid一直提交
        iccid_val = self.get_4g_mccid()
        login_data["iccid"] = iccid_val if iccid_val is not None else ""
        return True

    def _create_iccid_marker_file(self):
        """登录成功且曾携带 iccid 后创建空标记文件，后续登录不再带 iccid。"""
        marker_path = os.path.join(config.APP_DIR, "iccid")
        if os.path.exists(marker_path):
            return
        try:
            with open(marker_path, "w"):
                pass
        except Exception as e:
            self.logger.warning(f"[NET] 创建 iccid 标记文件失败: {e}")

    def _apply_session_force_4g(self):
        """锁定本次会话为 4G（直到关机，期间不再回切 WiFi）"""
        self._session_force_4g = True
        self._network_type = "4g"

    def select_network(self, prefer_wifi=None):
        """
        自动选择网络（WiFi优先）

        Args:
            prefer_wifi: 是否优先使用WiFi（None表示使用默认策略）

        Returns:
            "wifi" 或 "4g" 或 None（无可用网络）
        """
        if prefer_wifi is None:
            prefer_wifi = wifi_manager.prefer_wifi

        # 本次会话锁定：只要 4G 可用就一直用 4G
        if self._session_force_4g:
            self.logger.info("[NET] 会话锁定 4G：继续使用 4G（跳过 WiFi 质量评估）")
            self._network_type = "4g"
            return "4g"

        host = self._server_ip
        port = self._server_port

        # 1) 开机先尝试 WiFi，并评估质量
        if prefer_wifi and self.is_wifi_connected():
            wifi_rssi_dbm = self._get_wifi_rssi_dbm()
            wifi_rtt_ms = 0
            wifi_reachable = True
            # wifi_rtt_ms, wifi_reachable = self._measure_wifi_tcp_rtt_ms(
            #     host, port,
            #     samples=getattr(config, "WIFI_QUALITY_RTT_SAMPLES", 3),
            #     per_sample_timeout_ms=900,
            # )
            wifi_bad = self._is_wifi_quality_bad(wifi_rtt_ms, wifi_rssi_dbm)

            self.logger.info(
                f"[NET] WiFi质量评估：rtt_ms(median)={wifi_rtt_ms:.1f}, rssi_dbm={wifi_rssi_dbm}, "
                f"reachable={wifi_reachable}, bad={wifi_bad}"
            )

            # 如果质量差且 4G 可用 -> 切换 4G 并锁定本次会话
            if wifi_bad and self.is_4g_available():
                self._apply_session_force_4g()
                self.logger.warning("[NET] WiFi质量差且 4G 可用 -> 切换到 4G并锁定本次会话")
                return "4g"

            # 否则：仍以 WiFi 为主，但如果服务器不可达则回退到 4G
            if wifi_reachable or self.is_server_reachable(host, port, timeout=3):
                self._network_type = "wifi"
                self.logger.info(f"[NET] 选择WiFi网络，IP: {wifi_manager.wifi_ip}")
                try:
                    os.environ["TZ"] = "Asia/Shanghai"
                    os.system("ntpdate pool.ntp.org")
                except Exception:
                    pass
                return "wifi"

            # WiFi可用但服务器不可达
            self.logger.warning("[NET] WiFi可用但服务器不可达，尝试4G")

        # 2) 如果 WiFi 没法用/不想用，回退到 4G
        if self.is_4g_available():
            self._network_type = "4g"
            self.logger.info("[NET] 选择4G网络")
            return "4g"

        # 3) 两者都不可用
        self.logger.error("[NET] WiFi 与 4G 均不可用")
        return None

    def _start_wifi_quality_monitor(self):
        """
        启动 WiFi 质量后台监测线程（委托给 wifi_manager）
        """
        wifi_manager.start_quality_monitor(
            network_type_callback=lambda: self._network_type,
            on_poor_quality_callback=self._switch_to_4g_due_to_poor_wifi
        )

    def _stop_wifi_quality_monitor(self):
        """停止 WiFi 质量监测线程（委托给 wifi_manager）"""
        wifi_manager.stop_quality_monitor()

    def get_wifi_quality_status(self):
        """获取当前 WiFi 质量状态（委托给 wifi_manager）"""
        return wifi_manager.get_wifi_quality_status()

    def _switch_to_4g_due_to_poor_wifi(self):
        """
        由于 WiFi 质量差，切换到 4G 网络
        """
        self.logger.info("[WiFi->4G] 开始切换到 4G 网络")

        # 1. 标记本次上电强制使用 4G
        self._session_force_4g = True

        # 2. 关闭 WiFi socket
        wifi_manager.disconnect_wifi()

        # 3. 重置连接状态
        self._tcp_connected = False
        self._network_type = None  # 清空，让 select_network 重新选择

        # 4. 检查 4G 是否可用
        if self.is_4g_available():
            self._network_type = "4g"
            self.logger.info("[WiFi->4G] 切换成功，将使用 4G 网络")
            return True
        else:
            self.logger.error("[WiFi->4G] 4G 不可用，无法切换")
            # 回退：继续使用 WiFi（虽然质量差）
            self._session_force_4g = False
            return False

    def _cmd200_detect_laser(self):
        """后台线程执行 cmd200 激光检测，避免阻塞主循环"""
        from laser_manager import laser_manager
        try:
            laser_manager.turn_on_laser()
            self.logger.info("[LASER] cmd200 已发送开激光指令")
        except Exception as e:
            self.logger.warning(f"[LASER] cmd200 开激光异常: {e}")

        try:
            from laser_detector import get_stable_laser_point
            time.sleep_ms(500)
            result = get_stable_laser_point(timeout_ms=60000)
            if result:
                x, y = result
                self.safe_enqueue({
                    "cmd": 200,
                    "result": "laser_detect_ok",
                    "x": x,
                    "y": y,
                }, 2)
                self.logger.info(f"[LASER] cmd200 检测结果: ({x}, {y})")
            else:
                self.safe_enqueue({
                    "cmd": 200,
                    "result": "laser_detect_failed",
                }, 2)
                self.logger.warning("[LASER] cmd200 检测失败")
        except Exception as e:
            self.logger.error(f"[LASER] cmd200 检测异常: {e}")

    def _cmd300_ota(self, data_obj):
        """后台线程执行 cmd300 OTA，避免阻塞主循环"""
        hardware_manager.start_idle_timer()
        inner_data = data_obj.get("data", {}) if isinstance(data_obj, dict) else {}
        self.logger.info(f"[New Ota] cmd300 , data: {inner_data}")
        ssid = inner_data.get("ssid")
        password = inner_data.get("password")
        ota_res_url = inner_data.get("url")
        try:
            for _f in ("/etc/wpa_supplicant.conf", "/boot/wpa_supplicant.conf", "/boot/wifi.ssid", "/boot/wifi.pass"):
                try:
                    os.remove(_f)
                except OSError:
                    pass
            w = network.wifi.Wifi()
            e = w.connect(ssid, password, wait=True, timeout=15)
            err.check_raise(e, "connect wifi failed")
            if self.logger:
                self.logger.info(f"[ota] Connect success, got ip{w.get_ip()}")
            self.safe_enqueue(
                {
                    "cmd": 300,
                    "result": "ota start...",
                    "wifi": w.get_ip(),
                },
                2,
            )
            subprocess.run(
                ["sh", "/maixapp/apps/t11/ota_curl.sh", ota_res_url])
            self.safe_enqueue(
                {
                    "cmd": 300,
                    "result": "success",
                    "wifi": w.get_ip(),
                },
                2,
            )
        except Exception as e:
            self.logger.error(f"[ota] cmd300 失败: {e}")
            self.safe_enqueue(
                {
                    "cmd": 300,
                    "result": "ota fail",
                    "reason": str(e),
                },
                2,
            )

    def _cmd600_conn_wifi(self, data_obj):
        hardware_manager.start_idle_timer()
        inner_data = data_obj.get("data", {}) if isinstance(data_obj, dict) else {}
        self.logger.info(f"[conn wifi] cmd600 , data: {inner_data}")
        ssid = inner_data.get("ssid")
        password = inner_data.get("password")
        # 停止旧的WiFi质量监测（无论当前是WiFi还是4G连接）
        self._stop_wifi_quality_monitor()
        try:
            for _f in ("/etc/wpa_supplicant.conf", "/boot/wpa_supplicant.conf", "/boot/wifi.ssid", "/boot/wifi.pass"):
                try:
                    os.remove(_f)
                except OSError:
                    pass
            w = network.wifi.Wifi()
            e = w.connect(ssid, password, wait=True, timeout=15)
            err.check_raise(e, "connect wifi failed")
            if self.logger:
                self.logger.info(f"[ota] Connect success, got ip{w.get_ip()}")
            self.safe_enqueue(
                {
                    "cmd": 600,
                    "result": "success",
                    "wifi": w.get_ip(),
                },
                2,
            )
            self._session_force_4g = False
            self.disconnect_server()
            self._tcp_connected = False
            self._network_type = None
            self.logger.info("[conn wifi] WiFi已连接，等待主循环重新登录")
        except Exception as e:
            self.logger.error(f"cmd600 失败: {e}")
            self.safe_enqueue(
                {
                    "cmd": 600,
                    "result": "conn fail",
                    "reason": str(e),
                },
                2,
            )
            self._switch_to_4g_due_to_poor_wifi()

    def safe_enqueue(self, data_dict, msg_type=2, high=False):
        """线程安全地将消息加入队列（公共方法）"""
        self._enqueue((msg_type, data_dict), high)

    def connect_server(self):
        """
        连接到服务器（自动选择WiFi或4G）

        Returns:
            bool: 是否连接成功
        """
        if self._tcp_connected:
            # 检查当前连接是否仍然有效
            if self._network_type == "wifi":
                return self._check_wifi_connection()
            elif self._network_type == "4g":
                return self._check_4g_connection()
            return False

        # 自动选择网络
        network_type = self.select_network()
        if not network_type:
            return False

        self.logger.info(f"连接到服务器,使用{network_type.upper()}...")

        # 根据网络类型建立TCP连接
        if network_type == "wifi":
            return self._connect_tcp_via_wifi()
        elif network_type == "4g":
            return self._connect_tcp_via_4g()
        return False

    def _check_4g_connection(self):
        """检查4G TCP连接是否仍然有效（通过查询PDP地址验证网络附着状态）"""
        try:
            atc = hardware_manager.at_client
            if atc is None:
                return False
            if not self._uart4g_lock.acquire(timeout=3000):
                # 获取锁超时说明有其他操作在进行，视为连接仍有效
                return True
            try:
                r = atc.send("AT+CGPADDR=1", "OK", 3000)
                m = re.search(r'\+CGPADDR:\s*1,"([^"]+)"', r)
                ip = m.group(1) if m else ""
                if ip and ip != "0.0.0.0":
                    return True
                # 无IP或IP无效，尝试重新激活PDP
                self.logger.warning("[4G-TCP] PDP地址无效，尝试重新激活")
                atc.send("AT+MIPCALL=1,1", "OK", 15000)
                r2 = atc.send("AT+CGPADDR=1", "OK", 3000)
                m2 = re.search(r'\+CGPADDR:\s*1,"([^"]+)"', r2)
                ip2 = m2.group(1) if m2 else ""
                if ip2 and ip2 != "0.0.0.0":
                    return True
                self.logger.error("[4G-TCP] 重新激活PDP仍无有效IP，连接已断开")
                return False
            finally:
                self._uart4g_lock.release()
        except Exception as e:
            self.logger.warning(f"[4G-TCP] 连接检查异常: {e}")
            return True  # 异常时不误判断线

    def _wrap_wifi_tls(self, plain_sock, hostname):
        """
        在已建立的 TCP socket 上做 TLS（WiFi 走主机 ssl 库；4G 仍用模组 AT+SSL）。
        校验与 config.SSL_VERIFY_MODE、SSL_CERT_PATH 一致。
        """
        import ssl

        verify_mode = getattr(config, "SSL_VERIFY_MODE", 0)
        cert_path = getattr(config, "SSL_CERT_PATH", None) or ""
        ca_ok = verify_mode == 1 and cert_path and os.path.exists(cert_path)
        cert_none = getattr(ssl, "CERT_NONE", 0)
        cert_required = getattr(ssl, "CERT_REQUIRED", 2)

        if hasattr(ssl, "SSLContext"):
            try:
                proto = getattr(ssl, "PROTOCOL_TLS_CLIENT", None)
                if proto is None:
                    proto = getattr(ssl, "PROTOCOL_TLS", 0)
                ctx = ssl.SSLContext(proto)
                if ca_ok:
                    ctx.load_verify_locations(cafile=cert_path)
                    ctx.verify_mode = cert_required
                    ctx.check_hostname = True
                else:
                    ctx.check_hostname = False
                    ctx.verify_mode = cert_none
                return ctx.wrap_socket(plain_sock, server_hostname=hostname)
            except Exception as e:
                self.logger.warning(f"[WIFI-TCP] SSLContext.wrap_socket 失败，改用 wrap_socket: {e}")

        if ca_ok:
            try:
                return ssl.wrap_socket(
                    plain_sock,
                    server_hostname=hostname,
                    cert_reqs=cert_required,
                    ca_certs=cert_path,
                )
            except TypeError:
                return ssl.wrap_socket(plain_sock, cert_reqs=cert_required, ca_certs=cert_path)
        try:
            return ssl.wrap_socket(plain_sock, server_hostname=hostname, cert_reqs=cert_none)
        except TypeError:
            return ssl.wrap_socket(plain_sock, cert_reqs=cert_none)

    def _connect_tcp_via_wifi(self):
        """通过WiFi建立TCP连接（USE_TCP_SSL 时在 TCP 之上走 tls）"""
        try:
            # 创建TCP socket
            wifi_manager.wifi_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            wifi_manager.wifi_socket.settimeout(5.0)  # 5秒超时

            # 连接到服务器
            use_ssl = getattr(config, "USE_TCP_SSL", False)
            host = self._server_ip
            port = getattr(config, "TCP_SSL_PORT", 443) if use_ssl else config.SERVER_PORT
            addr_info = socket.getaddrinfo(host, port, socket.AF_INET, socket.SOCK_STREAM)[0]
            wifi_manager.wifi_socket.connect(addr_info[-1])

            if use_ssl:
                wifi_manager.wifi_socket = self._wrap_wifi_tls(wifi_manager.wifi_socket, host)

            # 设置非阻塞模式（用于接收数据）
            wifi_manager.wifi_socket.setblocking(False)
            # 加快消息发送
            wifi_manager.wifi_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

            self._tcp_connected = True
            if use_ssl:
                self.logger.info("[WIFI-TCP] TLS 连接已建立")
            else:
                self.logger.info("[WIFI-TCP] TCP 连接已建立")

            # 启动 WiFi 质量后台检测
            self._start_wifi_quality_monitor()

            return True

        except Exception as e:
            self.logger.error(f"[WIFI-TCP] 连接失败: {e}")

            if wifi_manager.wifi_socket:
                try:
                    wifi_manager.wifi_socket.close()
                except:
                    pass
                wifi_manager.wifi_socket = None

            return False

    def _connect_tcp_via_4g(self):
        """通过4G模块建立TCP连接（支持按手册绑定 SSL）"""
        link_id = getattr(config, "TCP_LINK_ID", 0)
        use_ssl = getattr(config, "USE_TCP_SSL", False)

        host = self._server_ip
        port = getattr(config, "TCP_SSL_PORT", 443) if use_ssl else config.SERVER_PORT
        tail = getattr(config, "MIPOPEN_TAIL", "")
        if not self._uart4g_lock.acquire(timeout=15000):
            self.logger.warning("[4G-TCP] 连接：获取 uart4g_lock 超时")
            return False
        try:
            resp = hardware_manager.at_client.send(f"AT+MIPCLOSE={link_id}", "OK", 1000)
            self.logger.info(f"[4G-TCP] AT+MIPCLOSE={link_id} response: {resp}")

            if use_ssl:
                ok = self._configure_ssl_before_connect(link_id)
                if not ok:
                    return False

            # 按手册：AT+MIPOPEN=1,"TCP","host",443,,0
            # cmd = f'AT+MIPOPEN={link_id},"TCP","{host}",{port}{tail}'
            cmd = f'AT+MIPOPEN={link_id},"TCP","{host}",{port}'
            res = hardware_manager.at_client.send(cmd, "+MIPOPEN", 8000)
            self.logger.info(f"[4G-TCP] {cmd} response: {res}")
        finally:
            self._uart4g_lock.release()
        if f"+MIPOPEN: {link_id},0" in res:
            self._tcp_connected = True
            return True
        return False

    def _check_wifi_connection(self):
        """检查WiFi TCP连接是否仍然有效"""
        if not wifi_manager.wifi_socket:
            return False
        # TLS(ssl.wrap_socket/SSLContext.wrap_socket) 后的 socket 往往不支持 MSG_PEEK/MSG_DONTWAIT。
        # 这种情况下“主动探测”反而容易误报断线；让真正的 send/recv 去判定更稳。
        try:
            if getattr(config, "USE_TCP_SSL", False) or hasattr(wifi_manager.wifi_socket, "cipher"):
                return True
        except Exception:
            # 任何探测异常都不应导致断线清理
            return True
        try:
            # send(b"") 在很多实现里是 no-op，无法可靠探测断线。
            # 用非阻塞 peek 来判断：若对端已关闭，recv 会返回 b""。
            data = wifi_manager.wifi_socket.recv(1, socket.MSG_PEEK | socket.MSG_DONTWAIT)
            if data == b"":
                raise OSError("wifi socket closed")
            return True
        except TypeError as e:
            # 某些实现（尤其是 TLS socket）不支持 flags 参数；不要误判断线
            try:
                self.logger.warning(f"[WIFI-TCP] conncheck flags unsupported (TypeError): {e}")
            except Exception:
                pass
            return True
        except BlockingIOError:
            # 无数据可读但连接仍在（EAGAIN）
            return True
        except OSError as e:
            # 兼容不同平台的 EAGAIN / would block
            err = getattr(e, "errno", None)
            if err in (11, 35, 10035):  # EAGAIN/EWOULDBLOCK on linux/mac/win
                return True

            # 某些平台会把"无数据可读/超时"抛成 socket.timeout / TimeoutError，errno 可能为 None，
            # 这不代表断线：视为 benign，交给真正的 send/recv 去判定断线。
            if (err is None) and (("timed out" in str(e).lower()) or isinstance(e, (TimeoutError, socket.timeout))):
                try:
                    self.logger.warning(f"[WIFI-TCP] conncheck timeout treated as benign: {e}")
                except:
                    pass
                return True

            # 某些嵌入式 socket 实现可能不支持 MSG_PEEK/MSG_DONTWAIT，或返回 EINVAL/ENOTSUP。
            # 这种情况不代表断线：选择"无法检测但不判死"，交给真正的 send/recv 去触发断线处理。
            # 常见：EINVAL(22), ENOTSUP(95), EOPNOTSUPP(95), EINTR(4)
            if err in (4, 22, 95):
                try:
                    self.logger.warning(f"[WIFI-TCP] conncheck unsupported/benign errno={err}: {e}")
                except:
                    pass
                return True

            # 记录真实错误，便于定位
            try:
                self.logger.error(f"[WIFI-TCP] conncheck failed errno={err}: {e}")
            except:
                pass

            # 明确的"连接不可用"错误才判定断线并清理
            # 常见：ENOTCONN(107), ECONNRESET(104), EPIPE(32), EBADF(9)
            if err in (9, 32, 104, 107, 10054, 10057):
                try:
                    wifi_manager.wifi_socket.close()
                except:
                    pass
                wifi_manager.wifi_socket = None
                self._tcp_connected = False
                return False

            # socket已断开或不可用，清理
            try:
                wifi_manager.wifi_socket.close()
            except:
                pass
            wifi_manager.wifi_socket = None
            self._tcp_connected = False
            return False
        except Exception:
            try:
                wifi_manager.wifi_socket.close()
            except:
                pass
            wifi_manager.wifi_socket = None
            self._tcp_connected = False
            return False

    def disconnect_server(self):
        """断开TCP连接"""
        if self._tcp_connected:
            self.logger.info("与服务器断开链接")

        if self._network_type == "wifi":
            self._disconnect_tcp_via_wifi()
        elif self._network_type == "4g":
            self._disconnect_tcp_via_4g()

        self._tcp_connected = False
        self._network_type = None

    def _disconnect_tcp_via_wifi(self):
        """断开 WiFi TCP 连接并停止监测"""
        # 关闭wifi检测
        self._stop_wifi_quality_monitor()

        # 再关闭 socket
        with wifi_manager.wifi_socket_lock:
            if wifi_manager.wifi_socket:
                try:
                    wifi_manager.wifi_socket.close()
                except:
                    pass
                wifi_manager.wifi_socket = None

    def _disconnect_tcp_via_4g(self):
        link_id = getattr(config, "TCP_LINK_ID", 0)
        if not self._uart4g_lock.acquire(timeout=2000):
            self.logger.warning("[4G-TCP] 断开连接：获取 uart4g_lock 超时")
            return
        try:
            hardware_manager.at_client.send(f"AT+MIPCLOSE={link_id}", "OK", 1000)
        finally:
            self._uart4g_lock.release()

    def tcp_send_raw(self, data: bytes, max_retries=2) -> bool:
        """
        统一的TCP发送接口（自动选择WiFi或4G）

        Args:
            data: 要发送的数据
            max_retries: 最大重试次数

        Returns:
            bool: 是否发送成功
        """
        if not self._tcp_connected:
            return False

        # 根据网络类型选择发送方式
        if self._network_type == "wifi":
            # 先快速校验 WiFi socket 是否仍有效，避免卡在半开连接上
            if not self._check_wifi_connection():
                print("_check_wifi_connection failed")
                return False
            return self._tcp_send_raw_via_wifi(data, max_retries)
        elif self._network_type == "4g":
            return self._tcp_send_raw_via_4g(data, max_retries)
        else:
            self.logger.error("[NET] 未选择网络类型，无法发送数据")
            return False

    def _tcp_send_raw_via_wifi(self, data: bytes, max_retries=2) -> bool:
        """通过WiFi socket发送TCP数据"""
        if not wifi_manager.wifi_socket:
            return False

        with wifi_manager.wifi_socket_lock:
            for attempt in range(max_retries):
                try:
                    # 标准 socket 发送
                    total_sent = 0
                    while total_sent < len(data):
                        try:
                            sent = wifi_manager.wifi_socket.send(data[total_sent:])
                        except (BlockingIOError, OSError) as se:
                            if _wifi_tls_would_block(se):
                                time.sleep_ms(2)
                                continue
                            raise
                        if sent == 0:
                            # socket连接已断开
                            self.logger.warning(f"[WIFI-TCP] 发送失败，socket已断开（尝试 {attempt + 1}/{max_retries}）")
                            raise OSError("wifi socket closed (send returned 0)")
                        total_sent += sent

                    if total_sent == len(data):
                        return True

                    # 发送不完整，重试
                    time.sleep_ms(50)

                except OSError as e:
                    self.logger.error(f"[WIFI-TCP] 发送异常: {e}（尝试 {attempt + 1}/{max_retries}）")
                    # 发送异常通常意味着连接已不可用，主动关闭以触发重连
                    try:
                        wifi_manager.wifi_socket.close()
                    except:
                        pass
                    wifi_manager.wifi_socket = None
                    self._tcp_connected = False
                    return False
                except Exception as e:
                    self.logger.error(f"[WIFI-TCP] 未知错误: {e}（尝试 {attempt + 1}/{max_retries}）")
                    try:
                        wifi_manager.wifi_socket.close()
                    except:
                        pass
                    wifi_manager.wifi_socket = None
                    self._tcp_connected = False
                    return False

            return False

    def _tcp_send_raw_via_4g(self, data: bytes, max_retries=2) -> bool:
        link_id = getattr(config, "TCP_LINK_ID", 0)
        if not self._uart4g_lock.acquire(timeout=2000):
            self.logger.warning("[4G-TCP] 获取 uart4g_lock 超时（其他线程持有），跳过本次发送")
            return False
        try:
            for _ in range(max_retries):
                cmd = f'AT+MIPSEND={link_id},{len(data)}'
                if ">" not in hardware_manager.at_client.send(cmd, ">", 2000):
                    time.sleep_ms(50)
                    continue

                total = 0
                while total < len(data):
                    n = hardware_manager.uart4g.write(data[total:])
                    if not n or n < 0:
                        time.sleep_ms(1)
                        continue
                    total += n

                hardware_manager.uart4g.write(b"\x1A")
                with hardware_manager.at_client._q_lock:
                    hardware_manager.at_client._rx = b""
                r = hardware_manager.at_client.send("", "OK", 8000)
                if ("SEND OK" in r) or ("OK" in r) or ("+MIPSEND" in r):
                    return True
                time.sleep_ms(50)
            return False
        finally:
            self._uart4g_lock.release()

    def _configure_ssl_before_connect(self, link_id: int) -> bool:
        """按手册：MSSLCFG(auth) -> (可选) MSSLCERTWR -> MSSLCFG(cert) -> MIPCFG(ssl)"""
        ssl_id = getattr(config, "SSL_ID", 1)
        auth_mode = getattr(config, "SSL_AUTH_MODE", 1)
        verify_mode = getattr(config, "SSL_VERIFY_MODE", 0)

        # 1) 配置认证方式
        r = hardware_manager.at_client.send(f'AT+MSSLCFG="auth",{ssl_id},{auth_mode}', "OK", 3000)
        # r = hardware_manager.at_client.send(f'AT+MSSLCFG="auth",{ssl_id},0', "OK", 3000)
        self.logger.info(f"[4G-TCP] AT+MSSLCFG=\"auth\",{ssl_id},0 response: {r}")
        if "OK" not in r:
            return False

        # 2) 写入根证书（只有 verify_mode=1 才需要）
        if verify_mode == 1:
            cert_filename = getattr(config, "SSL_CERT_FILENAME", None)
            cert_path = getattr(config, "SSL_CERT_PATH", None)
            if not cert_filename or not cert_path:
                return False

            # 读取证书文件（设备侧路径）
            with open(cert_path, "rb") as f:
                cert_data = f.read()

            # 按手册：AT+MSSLCERTWR="file",0,size -> 等待 ">" -> 写入证书内容 -> 等 OK
            r = hardware_manager.at_client.send(f'AT+MSSLCERTWR="{cert_filename}",0,{len(cert_data)}', ">", 5000)
            self.logger.info(f"[4G-TCP] AT+MSSLCERTWR=\"{cert_filename}\",0,{len(cert_data)} response: {r}")
            if ">" not in r:
                return False

            # 直接写原始字节，不要额外拼 \r\n
            hardware_manager.uart4g.write(cert_data)

            r = hardware_manager.at_client.send("", "OK", 8000)
            self.logger.info(f"[4G-TCP] AT+MSSLCERTWR=\"{cert_filename}\",0,{len(cert_data)} response: {r}")
            if "OK" not in r:
                return False

            r = hardware_manager.at_client.send(f'AT+MSSLCHECK="{cert_filename}"', "OK", 8000)
            if "OK" not in r:
                self.logger.error(f"[4G-TCP] MSSLCHECK failed, response: {r}")
                return False
            else:
                self.logger.info(f"[4G-TCP] MSSLCHECK response: {r}")

            r = hardware_manager.at_client.send(f'AT+MSSLLIST=1', "OK", 3000)
            self.logger.info(f"[4G-TCP] AT+MSSLLIST=1 response: {r}")

            r = hardware_manager.at_client.send(f'AT+MSSLCERTRD="{cert_filename}"', "OK", 3000)
            self.logger.info(f"[4G-TCP] AT+MSSLCERTRD=\"{cert_filename}\" response: {r}")

            # 3) 引用根证书
            r = hardware_manager.at_client.send(f'AT+MSSLCFG="cert",{ssl_id},"{cert_filename}"', "OK", 3000)
            if "OK" not in r:
                return False
            else:
                self.logger.info(f"[4G-TCP] MSSLCFG(cert) response: {r}")

        # 4) 绑定 TCP 通道到 ssl_id，并启用
        r = hardware_manager.at_client.send(f'AT+MIPCFG="ssl",{link_id},{ssl_id},1', "OK", 3000)
        self.logger.info(f"[4G-TCP] AT+MIPCFG=\"ssl\",{link_id},{ssl_id},1 response: {r}")
        if "OK" not in r:
            self.logger.error(f"[4G-TCP] MIPCFG(ssl) failed, response: {r}")
            return False

        return True

    def receive_tcp_data_via_wifi(self, timeout_ms=100):
        """
        通过WiFi接收TCP数据

        Args:
            timeout_ms: 超时时间（毫秒）

        Returns:
            bytes: 接收到的数据，如果没有数据则返回 b""
        """
        if not wifi_manager.wifi_socket:
            return b""

        try:
            # 这里保持 socket 为非阻塞模式（连接时已 setblocking(False)）。
            # 不要反复 settimeout()，否则会把 socket 切回"阻塞+超时"，并导致 conncheck 误报 timed out。
            data = wifi_manager.wifi_socket.recv(4096)  # 每次最多接收4KB（无数据会抛 BlockingIOError）
            return data

        except BlockingIOError:
            # 无数据可读是正常的
            return b""
        except OSError as e:
            if _wifi_tls_would_block(e):
                return b""
            # socket错误（连接断开等）
            self.logger.warning(f"[WIFI-TCP] 接收数据失败: {e}")

            # 关闭socket
            try:
                wifi_manager.wifi_socket.close()
            except:
                pass
            wifi_manager.wifi_socket = None
            self._tcp_connected = False

            return b""
        except Exception as e:
            self.logger.error(f"[WIFI-TCP] 接收数据异常: {e}")
            return b""

    def _upload_log_file(self, upload_url, wifi_ssid=None, wifi_password=None, include_rotated=True, max_files=None,
                         archive_format="tgz"):
        """上传日志文件到指定URL

        Args:
            upload_url: 上传目标URL，例如 "https://example.com/upload/"
            wifi_ssid: WiFi SSID（可选，如果未连接WiFi则尝试连接）
            wifi_password: WiFi 密码（可选）
            include_rotated: 是否包含轮转日志（app.log.1 等）
            max_files: 最多打包多少个日志文件（包含 app.log 本身），None=按 backupCount 自动推断
            archive_format: 打包格式：tgz 或 zip

        Note:
            该功能仅在 WiFi 连接时可用，4G 网络暂不支持文件上传
        """
        import requests
        import shutil
        from datetime import datetime
        import glob

        try:
            # 检查 WiFi 连接状态，如果未连接则尝试连接
            if not self.is_wifi_connected():
                if wifi_ssid and wifi_password:
                    self.logger.info(f"[LOG_UPLOAD] WiFi 未连接，尝试连接 WiFi: {wifi_ssid}")
                    self.safe_enqueue({"result": "log_upload_connecting_wifi", "ssid": wifi_ssid}, 2)

                    # 连接前先把“目标上传 URL”作为可达性验证目标，只有验证通过才落盘保存 SSID/PASS
                    try:
                        from urllib.parse import urlparse
                        parsed = urlparse(upload_url)
                        v_host = parsed.hostname
                        v_port = parsed.port or (443 if parsed.scheme == "https" else 80)
                    except Exception:
                        v_host, v_port = None, None
                    ip, error = self.connect_wifi(
                        wifi_ssid,
                        wifi_password,
                        verify_host=v_host,
                        verify_port=v_port,
                        persist=True,
                    )
                    if error:
                        self.logger.error(f"[LOG_UPLOAD] WiFi 连接失败: {error}")
                        self.safe_enqueue({
                            "result": "log_upload_failed",
                            "reason": "wifi_connect_failed",
                            "detail": error
                        }, 2)
                        return

                    self.logger.info(f"[LOG_UPLOAD] WiFi 连接成功，IP: {ip}")
                else:
                    self.logger.warning("[LOG_UPLOAD] WiFi 未连接且未提供 WiFi 凭证，无法上传日志")
                    self.safe_enqueue({
                        "result": "log_upload_failed",
                        "reason": "wifi_not_connected",
                        "detail": "WiFi not connected and no credentials provided"
                    }, 2)
                    return
            else:
                self.logger.info("[LOG_UPLOAD] WiFi 已连接，跳过连接步骤")

            self.logger.info(f"[LOG_UPLOAD] 开始上传日志文件...")

            # 获取日志文件路径
            log_file_path = config.LOG_FILE  # /maixapp/apps/t11/app.log

            if not os.path.exists(log_file_path):
                self.logger.error(f"[LOG_UPLOAD] 日志文件不存在: {log_file_path}")
                self.safe_enqueue({"result": "log_upload_failed", "reason": "log_file_not_found"}, 2)
                return

            # 生成带时间戳的文件名（归档）
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            device_id = self._device_id or "unknown"
            base_name = f"logs_{timestamp}_{device_id}"
            archive_format = (archive_format or "tgz").strip().lower()
            if archive_format not in ("tgz", "zip"):
                archive_format = "tgz"

            # 选择要打包的日志文件
            candidates = [log_file_path]
            if include_rotated:
                candidates = sorted(set(glob.glob(log_file_path + "*")))
            candidates = [p for p in candidates if os.path.isfile(p)]

            # app.log 优先，其余按“数字后缀”排序（app.log.1, app.log.2...）
            def _log_sort_key(p: str):
                if p == log_file_path:
                    return (0, 0, p)
                suffix = p[len(log_file_path):]
                if suffix.startswith("."):
                    try:
                        return (1, int(suffix[1:]), p)
                    except:
                        return (2, 999999, p)
                return (3, 999999, p)

            candidates.sort(key=_log_sort_key)

            # 限制最大文件数（默认：app.log + 轮转数量）
            if max_files is None:
                try:
                    max_files = 1 + int(getattr(config, "LOG_BACKUP_COUNT", 5))
                except:
                    max_files = 6
            try:
                max_files = int(max_files)
            except:
                max_files = 6
            max_files = max(1, min(max_files, 20))
            selected = candidates[:max_files]

            if not selected:
                self.logger.error("[LOG_UPLOAD] 未找到可打包的日志文件")
                self.safe_enqueue({"result": "log_upload_failed", "reason": "no_log_files"}, 2)
                return

            self.logger.info(f"[LOG_UPLOAD] 将打包日志文件数: {len(selected)}")

            # 先 sync，尽量确保日志落盘；再复制快照到 /tmp，避免打包过程中日志被追加导致内容不一致
            os.system("sync")
            temp_dir = "/tmp"
            staging_dir = os.path.join(temp_dir, f"log_upload_{base_name}")
            os.makedirs(staging_dir, exist_ok=True)
            staged_paths = []
            try:
                for p in selected:
                    dst = os.path.join(staging_dir, os.path.basename(p))
                    shutil.copy2(p, dst)
                    staged_paths.append(dst)
            except Exception as e:
                self.logger.error(f"[LOG_UPLOAD] 复制日志快照失败: {e}")
                self.safe_enqueue({"result": "log_upload_failed", "reason": "snapshot_failed", "detail": str(e)[:100]},
                                  2)
                try:
                    shutil.rmtree(staging_dir)
                except:
                    pass
                return

            # 打包压缩
            if archive_format == "zip":
                archive_filename = f"{base_name}.zip"
            else:
                archive_filename = f"{base_name}.tar.gz"
            archive_path = os.path.join(temp_dir, archive_filename)

            try:
                if archive_format == "zip":
                    import zipfile
                    with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
                        for p in staged_paths:
                            zf.write(p, arcname=os.path.basename(p))
                else:
                    import tarfile
                    with tarfile.open(archive_path, "w:gz") as tf:
                        for p in staged_paths:
                            tf.add(p, arcname=os.path.basename(p))
                self.logger.info(f"[LOG_UPLOAD] 日志压缩包已生成: {archive_path}")
            except Exception as e:
                self.logger.error(f"[LOG_UPLOAD] 打包压缩失败: {e}")
                self.safe_enqueue({"result": "log_upload_failed", "reason": "archive_failed", "detail": str(e)[:100]},
                                  2)
                try:
                    shutil.rmtree(staging_dir)
                except:
                    pass
                try:
                    if os.path.exists(archive_path):
                        os.remove(archive_path)
                except:
                    pass
                return
            finally:
                try:
                    shutil.rmtree(staging_dir)
                except:
                    pass

            # 使用 multipart/form-data 上传压缩包
            with open(archive_path, 'rb') as f:
                mime = "application/gzip" if archive_format == "tgz" else "application/zip"
                files = {'file': (archive_filename, f, mime)}

                # 添加额外的头部信息
                headers = {
                    'User-Agent': 'Archery-Device/1.0',
                    'X-Device-ID': device_id,
                }

                # 如果是 ngrok-free.dev，添加绕过警告页面的头
                if 'ngrok-free.dev' in upload_url or 'ngrok.io' in upload_url:
                    headers['ngrok-skip-browser-warning'] = 'true'

                self.logger.info(f"[LOG_UPLOAD] 正在上传到: {upload_url}")

                # 禁用 SSL 警告（用于自签名证书或 SSL 兼容性问题）
                import urllib3
                urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

                # 发送请求，verify=False 跳过 SSL 证书验证（解决 MaixCAM SSL 兼容性问题）
                response = requests.post(upload_url, files=files, headers=headers, timeout=60, verify=False)

                if response.status_code in (200, 201, 204):
                    self.logger.info(f"[LOG_UPLOAD] 上传成功! 状态码: {response.status_code}")
                    self.safe_enqueue({
                        "result": "log_upload_ok",
                        "filename": archive_filename,
                        "status_code": response.status_code
                    }, 2)
                else:
                    self.logger.error(
                        f"[LOG_UPLOAD] 上传失败! 状态码: {response.status_code}, 响应: {response.text[:200]}")
                    self.safe_enqueue({
                        "result": "log_upload_failed",
                        "reason": f"http_{response.status_code}",
                        "detail": response.text[:100]
                    }, 2)

            # 清理临时文件
            try:
                os.remove(archive_path)
                self.logger.debug(f"[LOG_UPLOAD] 临时文件已删除: {archive_path}")
            except Exception as e:
                self.logger.warning(f"[LOG_UPLOAD] 删除临时文件失败: {e}")

        except requests.exceptions.Timeout:
            self.logger.error("[LOG_UPLOAD] 上传超时")
            self.safe_enqueue({"result": "log_upload_failed", "reason": "timeout"}, 2)
        except requests.exceptions.ConnectionError as e:
            self.logger.error(f"[LOG_UPLOAD] 连接失败: {e}")
            self.safe_enqueue({"result": "log_upload_failed", "reason": "connection_error"}, 2)
        except Exception as e:
            self.logger.error(f"[LOG_UPLOAD] 上传异常: {e}")
            self.safe_enqueue({"result": "log_upload_failed", "reason": str(e)[:100]}, 2)

    def _prepare_log_archive(self, include_rotated=True, max_files=None, archive_format="tgz"):
        """准备日志归档压缩包，返回 (archive_path, archive_filename) 或 (None, error_msg)

        Args:
            include_rotated: 是否包含轮转日志
            max_files: 最多打包多少个日志文件
            archive_format: tgz 或 zip
        """
        import shutil
        from datetime import datetime
        import glob

        try:
            log_file_path = config.LOG_FILE
            if not os.path.exists(log_file_path):
                return None, "log_file_not_found"

            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            device_id = self._device_id or "unknown"
            base_name = f"logs_{timestamp}_{device_id}"
            archive_format = (archive_format or "tgz").strip().lower()
            if archive_format not in ("tgz", "zip"):
                archive_format = "tgz"

            candidates = [log_file_path]
            if include_rotated:
                candidates = sorted(set(glob.glob(log_file_path + "*")))
            candidates = [p for p in candidates if os.path.isfile(p)]

            def _log_sort_key(p):
                if p == log_file_path:
                    return (0, 0, p)
                suffix = p[len(log_file_path):]
                if suffix.startswith("."):
                    try:
                        return (1, int(suffix[1:]), p)
                    except:
                        return (2, 999999, p)
                return (3, 999999, p)

            candidates.sort(key=_log_sort_key)

            if max_files is None:
                try:
                    max_files = 1 + int(getattr(config, "LOG_BACKUP_COUNT", 5))
                except:
                    max_files = 6
            try:
                max_files = int(max_files)
            except:
                max_files = 6
            max_files = max(1, min(max_files, 20))
            selected = candidates[:max_files]

            if not selected:
                return None, "no_log_files"

            os.system("sync")
            temp_dir = "/tmp"
            staging_dir = os.path.join(temp_dir, f"log_upload_{base_name}")
            os.makedirs(staging_dir, exist_ok=True)
            staged_paths = []
            try:
                for p in selected:
                    dst = os.path.join(staging_dir, os.path.basename(p))
                    shutil.copy2(p, dst)
                    staged_paths.append(dst)
            except Exception as e:
                try:
                    shutil.rmtree(staging_dir)
                except:
                    pass
                return None, f"snapshot_failed: {e}"

            if archive_format == "zip":
                archive_filename = f"{base_name}.zip"
            else:
                archive_filename = f"{base_name}.tar.gz"
            archive_path = os.path.join(temp_dir, archive_filename)

            try:
                if archive_format == "zip":
                    import zipfile
                    with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
                        for p in staged_paths:
                            zf.write(p, arcname=os.path.basename(p))
                else:
                    import tarfile
                    with tarfile.open(archive_path, "w:gz") as tf:
                        for p in staged_paths:
                            tf.add(p, arcname=os.path.basename(p))
            except Exception as e:
                try:
                    shutil.rmtree(staging_dir)
                except:
                    pass
                try:
                    if os.path.exists(archive_path):
                        os.remove(archive_path)
                except:
                    pass
                return None, f"archive_failed: {e}"
            finally:
                try:
                    shutil.rmtree(staging_dir)
                except:
                    pass

            return archive_path, archive_filename
        except Exception as e:
            return None, f"prepare_exception: {e}"

    def _upload_log_file_v2(self, upload_url, upload_token, key, outlink="", include_rotated=True, max_files=None,
                            archive_format="tgz"):
        """上传日志到 Qiniu（支持 WiFi 和 4G 双路径）

        流程：准备日志归档 -> 自动检测网络 -> WiFi(requests) 或 4G(AT命令) 上传
        """
        import shutil

        # 1) 准备日志归档
        archive_path, info = self._prepare_log_archive(include_rotated, max_files, archive_format)
        if archive_path is None:
            self.logger.error(f"[LOG_UPLOAD] 准备归档失败: {info}")
            self.safe_enqueue({"result": "log_upload_failed", "reason": info}, 2)
            return

        archive_filename = info
        # key 是服务器下发的目录前缀，最终 key = prefix/filename
        qiniu_key = key.rstrip("/") + "/" + archive_filename
        self.logger.info(f"[LOG_UPLOAD] 日志归档已生成: {archive_path}, qiniu_key: {qiniu_key}")

        # 2) 自动检测网络类型
        if self._network_type == "wifi" and self.is_wifi_connected():
            mode = "wifi"
        else:
            mode = "4g"

        self.logger.info(f"[LOG_UPLOAD] Using {mode} path, archive: {archive_path}")

        try:
            if mode == "wifi":
                # ---- WiFi path: 使用 requests 库上传 ----
                import requests
                import urllib3
                urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

                with open(archive_path, 'rb') as f:
                    files = {'file': (archive_filename, f, 'application/octet-stream')}
                    data = {'token': upload_token, 'key': qiniu_key}
                    wifi_upload_url = upload_url.replace('https://', 'http://', 1)
                    self.logger.info(f"[LOG_UPLOAD] WiFi upload URL: {wifi_upload_url}")
                    response = requests.post(wifi_upload_url, files=files, data=data, timeout=120, verify=False)
                    response.raise_for_status()
                    result_json = response.json()
                    uploaded_key = result_json.get('key', qiniu_key)

                self.logger.info(f"[LOG_UPLOAD] WiFi upload ok: key={uploaded_key}")

                access_url = None
                if outlink:
                    access_url = f"https://{outlink}/{uploaded_key}"

                response_data = {
                    "result": "log_upload_ok",
                    "key": uploaded_key,
                    "via": "wifi",
                }
                if access_url:
                    response_data["url"] = access_url

                self.safe_enqueue(response_data, 2)

            else:
                # ---- 4G path: 使用 FourGUploadManager AT命令上传 ----
                import importlib.util
                spec = importlib.util.spec_from_file_location(
                    "four_g_upload_manager",
                    os.path.join(os.path.dirname(__file__), "4g_upload_manager.py")
                )
                upload_module = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(upload_module)
                FourGUploadManager = upload_module.FourGUploadManager

                uploader = FourGUploadManager(hardware_manager.at_client)
                result = uploader.upload_file(archive_path, upload_url, upload_token, qiniu_key)

                if result.get("success"):
                    uploaded_key = result.get("key", qiniu_key)
                    self.logger.info(f"[LOG_UPLOAD] 4G upload ok: key={uploaded_key}")

                    access_url = None
                    if outlink:
                        access_url = f"https://{outlink}/{uploaded_key}"

                    response_data = {
                        "result": "log_upload_ok",
                        "key": uploaded_key,
                        "via": "4g",
                    }
                    if access_url:
                        response_data["url"] = access_url

                    self.safe_enqueue(response_data, 2)
                else:
                    error_msg = result.get("error", "unknown_error")
                    self.logger.error(f"[LOG_UPLOAD] 4G upload failed: {error_msg}")
                    self.safe_enqueue({
                        "result": "log_upload_failed",
                        "reason": error_msg[:100]
                    }, 2)

        except Exception as e:
            self.logger.error(f"[LOG_UPLOAD] upload exception ({mode}): {e}")
            self.safe_enqueue({
                "result": "log_upload_failed",
                "reason": str(e)[:100]
            }, 2)
        finally:
            # 清理临时归档文件
            try:
                if archive_path and os.path.exists(archive_path):
                    os.remove(archive_path)
                    self.logger.debug(f"[LOG_UPLOAD] 临时归档已删除: {archive_path}")
            except Exception as e:
                self.logger.warning(f"[LOG_UPLOAD] 删除临时归档失败: {e}")

    def _upload_image_file(self, image_path, upload_url, upload_token, key, shoot_id, outlink):
        """上传图片文件到指定URL（自动检测网络类型，WiFi使用requests，4G使用AT HTTP命令）

        Args:
            image_path: 本地图片文件路径
            upload_url: 上传目标URL，例如 "https://upload.qiniup.com"
            upload_token: 上传token
            key: 文件key，例如 "shootPic/123456.bmp"
            shoot_id: 射击ID
            outlink: 外链域名（可选，用于构建访问URL）
        """
        # 自动检测网络类型，选择上传路径
        if self._network_type == "wifi" and self.is_wifi_connected():
            mode = "wifi"
        else:
            mode = "4g"

        self.logger.info(f"[IMAGE_UPLOAD] Using {mode} path, image: {image_path}")

        try:
            if mode == "wifi":
                # ---- WiFi path: 使用 requests 库上传 ----
                import requests
                import urllib3
                urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

                with open(image_path, 'rb') as f:
                    files = {'file': (os.path.basename(image_path), f, 'application/octet-stream')}
                    data = {'token': upload_token, 'key': key}
                    # 测试：将HTTPS转为HTTP
                    wifi_upload_url = upload_url.replace('https://', 'http://', 1)
                    self.logger.info(f"[IMAGE_UPLOAD] WiFi upload URL: {wifi_upload_url}")
                    response = requests.post(wifi_upload_url, files=files, data=data, timeout=120, verify=False)
                    response.raise_for_status()
                    result_json = response.json()
                    uploaded_key = result_json.get('key', key)

                self.logger.info(f"[IMAGE_UPLOAD] WiFi upload ok: key={uploaded_key}")

                access_url = None
                if outlink:
                    access_url = f"https://{outlink}/{uploaded_key}"

                response_data = {
                    "result": "image_upload_ok",
                    "shootId": shoot_id,
                    "key": uploaded_key,
                    "via": "wifi",
                }
                if access_url:
                    response_data["url"] = access_url

                self.safe_enqueue(response_data, 2)

            else:
                # ---- 4G path: 使用 FourGUploadManager AT命令上传 ----
                import importlib.util
                spec = importlib.util.spec_from_file_location(
                    "four_g_upload_manager",
                    os.path.join(os.path.dirname(__file__), "4g_upload_manager.py")
                )
                upload_module = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(upload_module)
                FourGUploadManager = upload_module.FourGUploadManager

                # 实例化4G上传管理器
                uploader = FourGUploadManager(hardware_manager.at_client)

                # 执行上传
                result = uploader.upload_image(image_path, upload_url, upload_token, key)

                if result.get("success"):
                    uploaded_key = result.get("key", key)
                    self.logger.info(f"[IMAGE_UPLOAD] 4G upload ok: key={uploaded_key}")

                    access_url = None
                    if outlink:
                        access_url = f"https://{outlink}/{uploaded_key}"

                    response_data = {
                        "result": "image_upload_ok",
                        "shootId": shoot_id,
                        "key": uploaded_key,
                        "via": "4g",
                    }
                    if access_url:
                        response_data["url"] = access_url

                    self.safe_enqueue(response_data, 2)
                else:
                    error_msg = result.get("error", "unknown_error")
                    self.logger.error(f"[IMAGE_UPLOAD] 4G upload failed: {error_msg}")
                    self.safe_enqueue({
                        "result": "image_upload_failed",
                        "shootId": shoot_id,
                        "reason": error_msg[:100]
                    }, 2)

        except Exception as e:
            self.logger.error(f"[IMAGE_UPLOAD] upload exception ({mode}): {e}")
            self.safe_enqueue({
                "result": "image_upload_failed",
                "shootId": shoot_id,
                "reason": str(e)[:100]
            }, 2)

    def tcp_main(self):
        """TCP 主通信循环：登录、心跳、处理指令、发送数据"""
        self.logger.info("[NET] TCP主线程启动")

        send_hartbeat_fail_count = 0
        last_charging_check = 0
        CHARGING_CHECK_INTERVAL = 5000  # 5秒检查一次充电状态

        while True:
            try:
                # 检查充电状态（每5秒检查一次）
                current_time = time.ticks_ms()
                if current_time - last_charging_check > CHARGING_CHECK_INTERVAL:
                    last_charging_check = current_time

                # OTA 期间不要 connect/登录/心跳/发送
                try:
                    from ota_manager import ota_manager
                    if ota_manager.ota_in_progress:
                        time.sleep_ms(200)
                        continue
                except Exception as e:
                    self.logger.error(f"[NET] OTA检查异常: {e}")
                    time.sleep_ms(200)
                    continue

                if not self.connect_server():
                    time.sleep_ms(1000)
                    continue

                # 发送登录包
                vol_val = get_bus_voltage()
                login_data = {
                    "deviceId": self.device_id,
                    "password": self.password,
                    "version": config.APP_VERSION,
                    "vol": vol_val,
                    "vol_per": voltage_to_percent(vol_val)
                }
                iccid_pending_marker = self._maybe_add_iccid_to_login(login_data)
                print(f"login_data: {login_data}")
                # if not self.tcp_send_raw(self.make_packet(1, login_data)):
                if not self.tcp_send_raw(self._netcore.make_packet(1, login_data)):
                    self._tcp_connected = False
                    try:
                        self.disconnect_server()
                    except:
                        pass
                    time.sleep_ms(500)
                    continue

                self.logger.info("➡️ 登录包已发送，等待确认...")
                logged_in = False
                pending_cleared = False
                last_heartbeat_ack_time = time.ticks_ms()
                last_heartbeat_send_time = time.ticks_ms()

                while True:
                    # 如果底层连接已断开，尽快跳出内层循环触发重连/重选网络
                    if not self._tcp_connected:
                        break

                    # OTA 期间暂停 TCP 活动
                    try:
                        from ota_manager import ota_manager
                        if ota_manager.ota_in_progress:
                            time.sleep_ms(200)
                            continue
                    except Exception as e:
                        self.logger.error(f"[NET] OTA检查异常: {e}")
                        time.sleep_ms(200)
                        continue

                    # 接收数据（根据网络类型选择接收方式）
                    # WiFi 粘包：一次 recv 可能含多条完整包；也可能缓冲里已有完整包但本轮 recv 超时为空
                    rx_items = []
                    if self._network_type == "wifi":
                        data = self.receive_tcp_data_via_wifi(timeout_ms=5)
                        if data:
                            # self.logger.info(f"[NET] 接收WiFi数据, {time.time()}")
                            wifi_manager.recv_buffer += data
                        while len(wifi_manager.recv_buffer) >= 12:
                            try:
                                body_len, msg_type, checksum = struct.unpack(">III", wifi_manager.recv_buffer[:12])
                                total_len = 12 + body_len
                                if len(wifi_manager.recv_buffer) >= total_len:
                                    payload = wifi_manager.recv_buffer[:total_len]
                                    wifi_manager.recv_buffer = wifi_manager.recv_buffer[total_len:]
                                    rx_items.append((0, payload))
                                else:
                                    self.logger.info(f"[NET] 接收WiFi数据不完整, {time.time()}")
                                    break
                            except Exception:
                                wifi_manager.recv_buffer = b""
                                self.logger.info(f"[NET] 接收WiFi数据解析失败, {time.time()}")
                                break
                    elif self._network_type == "4g":
                        item = hardware_manager.at_client.pop_tcp_payload()
                        if item:
                            rx_items.append(item)

                    _rx_login_fail = False
                    _rx_skip_tcp_iteration = False
                    if rx_items:
                        self.logger.info(f"total {len(rx_items)} items")
                        for item in rx_items:
                            if isinstance(item, tuple) and len(item) == 2:
                                link_id, payload = item
                            else:
                                link_id, payload = 0, item

                            if not logged_in:
                                try:
                                    self.logger.debug(
                                        f"[TCP] rx link={link_id} len={len(payload)} head={payload[:12].hex()}")
                                except:
                                    pass

                            # msg_type, body = self.parse_packet(payload)
                            msg_type, body = self._netcore.parse_packet(payload)

                            # 处理登录响应
                            if not logged_in and msg_type == 1:
                                if body and body.get("cmd") == 1 and body.get("data") == "登录成功":
                                    logged_in = True
                                    last_heartbeat_ack_time = time.ticks_ms()
                                    self.logger.info("登录成功")
                                    if iccid_pending_marker:
                                        self._create_iccid_marker_file()
                                        iccid_pending_marker = False

                                    # 检查 ota_pending.json
                                    try:
                                        pending_path = f"{config.APP_DIR}/ota_pending.json"
                                        if os.path.exists(pending_path):
                                            try:
                                                with open(pending_path, "r", encoding="utf-8") as f:
                                                    pending_obj = json.load(f)
                                            except:
                                                pending_obj = {}
                                            self.safe_enqueue({"result": "ota_ok", "url": pending_obj.get("url", "")},
                                                              2)
                                            self.logger.info("[OTA] 已上报 ota_ok，等待心跳确认后删除 pending")
                                    except Exception as e:
                                        self.logger.error(f"[OTA] ota_ok 上报失败: {e}")
                                else:
                                    _rx_login_fail = True
                                    break

                            # 处理心跳 ACK
                            elif logged_in and msg_type == 4:
                                last_heartbeat_ack_time = time.ticks_ms()
                                self.logger.debug("✅ 收到心跳确认")

                            # 处理命令40（分片下载）
                            elif logged_in and msg_type == 40:
                                if isinstance(body, dict):
                                    t = body.get('t', 0)
                                    v = body.get('v')
                                    # 如果是第一个分片，清空之前的缓存
                                    if len(self._raw_line_data) == 0 or (
                                            len(self._raw_line_data) > 0 and self._raw_line_data[0].get('v') != v):
                                        self._raw_line_data.clear()
                                    # 或者更简单：每次收到命令40时，如果版本号不同，清空缓存
                                    if len(self._raw_line_data) > 0:
                                        first_v = self._raw_line_data[0].get('v')
                                        if first_v and first_v != v:
                                            self._raw_line_data.clear()
                                    self._raw_line_data.append(body)
                                    if len(self._raw_line_data) >= int(t):
                                        self.logger.info(f"下载完成")
                                        from ota_manager import ota_manager
                                        stock_array = list(map(lambda x: x.get('d'), self._raw_line_data))
                                        local_filename = config.LOCAL_FILENAME
                                        with open(local_filename, 'w', encoding='utf-8') as file:
                                            file.write("\n".join(stock_array))
                                        ota_manager.apply_ota_and_reboot(None, local_filename)
                                    else:
                                        self.safe_enqueue({'data': {'l': len(self._raw_line_data), 'v': v}, 'cmd': 41})
                                    self.logger.info(f"已下载{len(self._raw_line_data)} 全部：{t} 版本：{v}")

                            elif logged_in and msg_type == 100:
                                self.logger.info(f"[IMAGE_UPLOAD] 收到图片上传命令 {body}")
                                if isinstance(body, dict):

                                    upload_url = body.get("uploadUrl")
                                    upload_token = body.get("token")
                                    shoot_id = body.get("shootId")
                                    outlink = body.get("outlink", "")

                                    hardware_manager.start_idle_timer()  # 重新计时

                                    # 验证必需字段
                                    if not upload_url or not upload_token or not shoot_id:
                                        self.logger.error("[IMAGE_UPLOAD] 缺少必需参数: uploadUrl, token 或 shootId")
                                        self.safe_enqueue({"result": "image_upload_failed", "reason": "missing_params"},
                                                          2)
                                    else:
                                        self.logger.info(f"[IMAGE_UPLOAD] 收到图片上传命令，shootId: {shoot_id}")
                                        # 查找文件名中包含 shoot_id 的图片文件（文件名格式：shot_{shoot_id}_*.bmp）
                                        image_extensions = ('.bmp', '.jpg', '.jpeg', '.png')
                                        photo_dir = config.PHOTO_DIR
                                        target_image = None
                                        try:
                                            if os.path.isdir(photo_dir):
                                                # 优先查找文件名中包含 shoot_id 的图片
                                                matched_images = [
                                                    f for f in os.listdir(photo_dir)
                                                    if f.lower().endswith(image_extensions) and shoot_id in f
                                                ]
                                                if matched_images:
                                                    # 按修改时间排序，取最新的匹配文件
                                                    matched_images.sort(
                                                        key=lambda f: os.path.getmtime(os.path.join(photo_dir, f)),
                                                        reverse=True
                                                    )
                                                    target_image = os.path.join(photo_dir, matched_images[0])
                                                    self.logger.info(
                                                        f"[IMAGE_UPLOAD] 找到匹配shootId的图片: {matched_images[0]}")
                                                else:
                                                    self.logger.warning(
                                                        f"[IMAGE_UPLOAD] 未找到包含shootId={shoot_id}的图片文件")
                                        except Exception as e:
                                            self.logger.error(f"[IMAGE_UPLOAD] 查找图片失败: {e}")

                                        if not target_image:
                                            self.logger.error(f"[IMAGE_UPLOAD] 未找到shootId={shoot_id}对应的图片文件")
                                            self.safe_enqueue(
                                                {"result": "image_upload_failed", "reason": "no_image_found",
                                                 "shootId": shoot_id}, 2)
                                        else:
                                            # 构建上传key
                                            ext = os.path.splitext(target_image)[1].lower()
                                            key = f"shootPic/{self.device_id}/{shoot_id}{ext}"
                                            self.logger.info(f"[IMAGE_UPLOAD] 准备上传: {target_image} -> {key}")

                                            # 在新线程中执行上传，避免阻塞主循环
                                            self._spawn_cmd_thread(
                                                self._upload_image_file,
                                                (target_image, upload_url, upload_token, key, shoot_id, outlink)
                                            )
                                            # 立即返回已入队确认
                                            self.safe_enqueue({"result": "image_upload_queued", "shootId": shoot_id}, 2)
                            elif logged_in and msg_type == 101:
                                self.logger.info(f"[LOG_UPLOAD] 收到日志上传命令 {body}")
                                if isinstance(body, dict):

                                    upload_url = body.get("uploadUrl")
                                    upload_token = body.get("token")
                                    key = body.get("key")
                                    outlink = body.get("outlink", "")
                                    include_rotated = body.get("includeRotated", True)
                                    max_files = body.get("maxFiles")
                                    archive_format = body.get("archive", "tgz")

                                    hardware_manager.start_idle_timer()  # 重新计时

                                    # 验证必需字段
                                    if not upload_url or not upload_token or not key:
                                        self.logger.error("[LOG_UPLOAD] 缺少必需参数: uploadUrl, token 或 key")
                                        self.safe_enqueue({"result": "log_upload_failed", "reason": "missing_params"},
                                                          2)
                                    else:
                                        self.logger.info(f"[LOG_UPLOAD] 收到日志上传命令，key: {key}")
                                        # 在新线程中执行上传，避免阻塞主循环
                                        self._spawn_cmd_thread(
                                            self._upload_log_file_v2,
                                            (upload_url, upload_token, key, outlink, include_rotated, max_files,
                                             archive_format)
                                        )
                                        # 立即返回已入队确认
                                        self.safe_enqueue({"result": "log_upload_queued"}, 2)
                            elif logged_in and msg_type == 201:
                                if self.logger:
                                    self.logger.info(f"[LASER] cmd201:{body}")
                                raw_x = body.get("x")
                                raw_y = body.get("y")
                                try:
                                    from laser_manager import laser_manager
                                    ix, iy = laser_manager.set_hardcoded_laser_point(
                                        raw_x, raw_y
                                    )
                                    self.safe_enqueue(
                                        {
                                            "cmd": 201,
                                            "result": "laser_point_set",
                                            "x": ix,
                                            "y": iy,
                                        },
                                        2,
                                    )
                                    self.logger.info(
                                        f"[LASER] cmd201 硬编码激光点=({ix}, {iy})"
                                    )
                                except Exception as e:
                                    self.logger.error(f"[LASER] cmd201 失败: {e}")
                                    self.safe_enqueue(
                                        {
                                            "cmd": 201,
                                            "result": "laser_point_set_failed",
                                            "reason": str(e),
                                        },
                                        2,
                                    )
                                hardware_manager.start_idle_timer()
                            # 处理业务指令
                            elif logged_in and isinstance(body, dict):
                                inner_cmd = None
                                data_obj = body.get("data")
                                if isinstance(data_obj, dict):
                                    inner_cmd = data_obj.get("cmd")
                                    if inner_cmd == 2:  # 开启激光并校准
                                        from laser_manager import laser_manager
                                        if not laser_manager.calibration_active:
                                            laser_manager.turn_on_laser()
                                            time.sleep_ms(100)
                                            hardware_manager.stop_idle_timer()  # 停表
                                            if not config.HARDCODE_LASER_POINT:
                                                laser_manager.start_calibration()
                                                self.safe_enqueue({"result": "calibrating"}, 2)
                                            else:
                                                # 写死的逻辑，不需要校准激光点
                                                self.safe_enqueue({"result": "laser pos set by hard code"}, 2)
                                    elif inner_cmd == 3:  # 关闭激光
                                        from laser_manager import laser_manager
                                        laser_manager.turn_off_laser()
                                        laser_manager.stop_calibration()
                                        hardware_manager.start_idle_timer()  # 开表
                                        self.safe_enqueue({"result": "laser_off"}, 2)
                                    elif inner_cmd == 4:  # 上报电量
                                        voltage = get_bus_voltage()
                                        battery_percent = voltage_to_percent(voltage)
                                        battery_data = {
                                            "battery": battery_percent,
                                            "voltage": round(float(voltage), 3),
                                            "netType": self.network_type,
                                        }
                                        self.safe_enqueue(battery_data, 2)
                                        self.logger.info(f"电量上报: {battery_percent}% 充电: {is_charging()}")
                                        if is_charging():
                                            self.safe_enqueue(
                                                {
                                                    "cmd": 700,
                                                },
                                                2,
                                            )
                                    elif inner_cmd == 700:
                                        self.logger.warning("服务器下发关机！！！")
                                        exit(-1)
                                    elif inner_cmd == 5:  # OTA 升级
                                        inner_data = data_obj.get("data", {}) if isinstance(data_obj, dict) else {}
                                        ssid = inner_data.get("ssid")
                                        password = inner_data.get("password")
                                        ota_url = inner_data.get("url")
                                        mode = (inner_data.get("mode") or "").strip().lower()

                                        if not ota_url:
                                            self.logger.error("ota missing_url")
                                            self.safe_enqueue({"result": "missing_url"}, 2)
                                            _rx_skip_tcp_iteration = True
                                            break

                                        from ota_manager import ota_manager
                                        if ota_manager.update_thread_started:
                                            self.safe_enqueue({"result": "update_already_started"}, 2)
                                            _rx_skip_tcp_iteration = True
                                            break

                                        # 自动判断模式：如果没有明确指定，根据WiFi连接状态和凭证决定
                                        if mode not in ("4g", "wifi"):
                                            self.logger.info("ota missing mode, auto-detecting...")
                                            # 若本次会话已锁定 4G，则 OTA 自动也走 4G，避免后续回切导致体验不一致
                                            if self._session_force_4g:
                                                mode = "4g"
                                                self.logger.info("ota auto-selected: 4g (session locked on 4g)")
                                            else:
                                                # 只有同时满足：WiFi已连接 且 提供了WiFi凭证，才使用WiFi
                                                if self.is_wifi_connected() and ssid and password:
                                                    mode = "wifi"
                                                    self.logger.info(
                                                        "ota auto-selected: wifi (WiFi connected and credentials provided)")
                                                else:
                                                    mode = "4g"
                                                    self.logger.info(
                                                        "ota auto-selected: 4g (WiFi not available or no credentials)")

                                        hardware_manager.stop_idle_timer()  # 停表，注意OTA停表之后，就没有再开表，因为OTA后面会重启，会重新开表

                                        if mode == "4g":
                                            ota_manager._set_ota_url(ota_url)  # 记录 OTA URL，供命令7使用
                                            ota_manager._start_update_thread()
                                            self._spawn_cmd_thread(ota_manager.direct_ota_download_via_4g, (ota_url,))
                                        else:  # mode == "wifi"
                                            if not ssid or not password:
                                                self.logger.error("ota wifi mode requires ssid and password")
                                                self.safe_enqueue({"result": "missing_ssid_or_password"}, 2)
                                            else:
                                                self.logger.info(f"ssid: {ssid}")
                                                self.logger.info(f"password: {password}")
                                                ota_manager._start_update_thread()
                                                self._spawn_cmd_thread(ota_manager.handle_wifi_and_update,
                                                                       (ssid, password, ota_url))
                                    elif inner_cmd == 6:
                                        try:
                                            ip = os.popen(
                                                "ifconfig wlan0 2>/dev/null | grep 'inet ' | awk '{print $2}'").read().strip()
                                            ip = ip if ip else "no_ip"
                                        except:
                                            ip = "error_getting_ip"
                                        self.safe_enqueue({"result": "current_ip", "ip": ip}, 2)
                                    elif inner_cmd == 44:  # 读 4G 本机号码（AT+CNUM）
                                        cnum = self.get_4g_phone_number()
                                        self.logger.info(f"4G 本机号码: {cnum}")
                                        self.safe_enqueue(
                                            {"result": "cnum", "number": cnum if cnum is not None else ""}, 2)
                                    elif inner_cmd == 45:  # 读 MCCID（AT+MCCID）
                                        mccid = self.get_4g_mccid()
                                        self.logger.info(f"4G MCCID: {mccid}")
                                        self.safe_enqueue(
                                            {"result": "mccid", "mccid": mccid if mccid is not None else ""}, 2)
                                    elif inner_cmd == 41:
                                        self.logger.info(f"[TEST] 收到TCP射箭触发命令, {time.time()}")
                                        self._manual_trigger_flag = True
                                        self.safe_enqueue({"result": "trigger_ack"}, 2)
                                        hardware_manager.start_idle_timer()  # 重新计时
                                    elif inner_cmd == 42:  # 关机命令
                                        self.logger.info("[SHUTDOWN] 收到TCP关机命令，准备关机...")
                                        self.safe_enqueue({"result": "shutdown_ack"}, 2)
                                        time.sleep_ms(1000)
                                        self.disconnect_server()
                                        # 尝试关闭4G模块
                                        try:
                                            with self.get_uart_lock():
                                                hardware_manager.at_client.send("AT+CFUN=0", "OK", 5000)
                                        except:
                                            pass
                                        time.sleep_ms(2000)
                                        os.system("sync")  # 刷新文件系统缓存到磁盘，防止数据丢失
                                        time.sleep_ms(500)
                                        # os.system("poweroff")
                                        hardware_manager.power_off()
                                        return
                                    elif inner_cmd == 43:  # 上传日志命令
                                        # 格式: {"cmd":43, "data":{"ssid":"xxx","password":"xxx","url":"xxx", ...}}
                                        inner_data = data_obj.get("data", {})
                                        upload_url = inner_data.get("url")
                                        wifi_ssid = inner_data.get("ssid")
                                        wifi_password = inner_data.get("password")
                                        include_rotated = inner_data.get("include_rotated", True)
                                        max_files = inner_data.get("max_files")
                                        archive_format = inner_data.get("archive", "tgz")  # tgz 或 zip

                                        hardware_manager.start_idle_timer()  # 重新计时

                                        if not upload_url:
                                            self.logger.error("[LOG_UPLOAD] 缺少 url 参数")
                                            self.safe_enqueue({"result": "log_upload_failed", "reason": "missing_url"},
                                                              2)
                                        else:
                                            self.logger.info(f"[LOG_UPLOAD] 收到日志上传命令，目标URL: {upload_url}")
                                        # 在新线程中执行上传，避免阻塞主循环
                                        self._spawn_cmd_thread(
                                            self._upload_log_file,
                                            (upload_url, wifi_ssid, wifi_password, include_rotated, max_files,
                                             archive_format)
                                        )
                                    elif inner_cmd == 200:
                                        self.logger.info("[LASER] cmd200 在后台线程执行检测")
                                        self._spawn_cmd_thread(self._cmd200_detect_laser, ())
                                    elif inner_cmd == 300:
                                        self.logger.info("[New Ota] cmd300 在后台线程执行OTA")
                                        self._spawn_cmd_thread(self._cmd300_ota, (data_obj,))
                                    elif inner_cmd == 600:
                                        self.logger.info("[conn wifi] cmd600 在后台线程执行连接wifi: {data_obj}")
                                        self._spawn_cmd_thread(self._cmd600_conn_wifi, (data_obj,))
                                    elif inner_cmd == 601:
                                        pass
                                else:  # data的结构不是 dict
                                    self.logger.info(f"[NET] body={body}, {time.time()}")
                            else:
                                self.logger.info(f"[NET] 未知数据 {body}, {time.time()}")
                        if _rx_login_fail:
                            break
                        if _rx_skip_tcp_iteration:
                            continue
                    else:
                        time.sleep_ms(5)

                    # 发送队列中的业务数据
                    if logged_in:
                        item = None
                        item_is_high = False
                        # 出队：发送失败时会把 item 放回队首，避免丢数据
                        with self.get_queue_lock():
                            if self._high_send_queue:
                                item = self._high_send_queue.pop(0)
                                item_is_high = True
                            elif self._normal_send_queue:
                                item = self._normal_send_queue.pop(0)
                                item_is_high = False

                        if item:
                            msg_type, data_dict = item
                            pkt = self._netcore.make_packet(msg_type, data_dict)
                            if not self.tcp_send_raw(pkt):
                                # 发送失败：将消息放回队首（队列满则丢弃）
                                with self.get_queue_lock():
                                    if item_is_high:
                                        if len(self._high_send_queue) < config.MAX_SEND_QUEUE_SIZE:
                                            self._high_send_queue.insert(0, item)
                                    else:
                                        if len(self._normal_send_queue) < config.MAX_SEND_QUEUE_SIZE:
                                            self._normal_send_queue.insert(0, item)
                                self._tcp_connected = False
                                try:
                                    self.disconnect_server()
                                except:
                                    pass
                                break

                    # 发送激光校准结果
                    if logged_in:
                        from laser_manager import laser_manager
                        result = laser_manager.get_calibration_result()
                        if result:
                            x, y = result
                            self.safe_enqueue({"result": "ok", "x": x, "y": y}, 2)

                    # 定期发送心跳
                    current_time = time.ticks_ms()
                    if logged_in and current_time - last_heartbeat_send_time > config.HEARTBEAT_INTERVAL * 1000:
                        vol_val = get_bus_voltage()
                        if not self.tcp_send_raw(
                                self._netcore.make_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})):
                            # if not self.tcp_send_raw(self.make_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})):
                            send_hartbeat_fail_count += 1
                            # 短暂波动可能导致一次发送失败：连续失败达到阈值才重连，避免重连风暴
                            self.logger.error(f"心跳发送失败({send_hartbeat_fail_count}/3)，准备重试")
                            if send_hartbeat_fail_count >= 3:
                                self.logger.error("心跳连续失败>=3，准备重连")
                                self._tcp_connected = False
                                try:
                                    self.disconnect_server()
                                except:
                                    pass
                                break
                            else:
                                # 不立即断开，让下一轮心跳再试
                                time.sleep_ms(50)
                                continue
                        else:
                            send_hartbeat_fail_count = 0
                            last_heartbeat_send_time = current_time
                            self.logger.debug("心跳已发送")

                            # 删除 pending 文件（心跳发送成功后）
                            if not pending_cleared:
                                try:
                                    pending_path = f"{config.APP_DIR}/ota_pending.json"
                                    if os.path.exists(pending_path):
                                        try:
                                            os.remove(pending_path)
                                            pending_cleared = True
                                            self.logger.info("[OTA] 心跳发送成功，已删除 ota_pending.json")
                                        except Exception as e:
                                            self.logger.error(f"[OTA] 删除 pending 文件失败: {e}")
                                except Exception as e:
                                    self.logger.error(f"[OTA] 检查 pending 文件时出错: {e}")

                    # 服务器不再发送心跳ACK
                    # if logged_in and current_time - last_heartbeat_ack_time > 1000*60*10:
                    #     self.logger.error("十分钟无心跳ACK，重连")
                    #     break

                    self._send_event.wait(timeout=0.05)  # 0.05秒 = 50ms
                    self._send_event.clear()

                self._tcp_connected = False
                self.logger.error("连接异常，50ms后重连...")
                time.sleep_ms(50)

            except Exception as e:
                # TCP主循环的顶层异常捕获，防止线程静默退出
                self.logger.error(f"[NET] TCP主循环异常: {e}")
                import traceback
                self.logger.error(traceback.format_exc())
                self._tcp_connected = False
                time.sleep_ms(500)  # 等待5秒后重试连接


# 创建全局单例实例
network_manager = NetworkManager()


# ==================== 向后兼容的函数接口 ====================

def tcp_main():
    """TCP主循环（向后兼容接口）"""
    return network_manager.tcp_main()


def read_device_id():
    """读取设备ID（向后兼容接口）"""
    return network_manager.read_device_id()


def safe_enqueue(data_dict, msg_type=2, high=False):
    """线程安全地加入队列（向后兼容接口）"""
    return network_manager.safe_enqueue(data_dict, msg_type, high)


def connect_server():
    """连接服务器（向后兼容接口）"""
    return network_manager.connect_server()


def disconnet_server():
    """断开服务器连接（向后兼容接口）"""
    return network_manager.disconnect_server()


def is_wifi_connected():
    """检查WiFi是否已连接（向后兼容接口）"""
    return network_manager.is_wifi_connected()


def connect_wifi(ssid, password):
    """连接WiFi（向后兼容接口）"""
    return network_manager.connect_wifi(ssid, password)


def is_server_reachable(host, port=80, timeout=5):
    """检查服务器是否可达（向后兼容接口）"""
    return network_manager.is_server_reachable(host, port, timeout)
