"""
Frida 无线部署器 — 支持 Root / 免Root 双模式 WiFi 直连
=====================================================

核心功能：
- Root 模式：通过 Termux 在手机端启动 frida-server，监听 TCP 端口
- 免Root 模式：将 frida-gadget 注入到目标 APK（微信），通过 TCP 连接
- WiFi 直连：服务器通过手机 IP + 端口直接连接 Frida，无需 USB
- 自动发现：扫描局域网内运行 frida-server 的设备
- 健康检查：定期检测 Frida 连接状态，自动重连

架构：
  服务器 (FastAPI)  ←→  WiFi  ←→  手机 (Termux + frida-server/gadget)
                                      ↓
                                  微信进程 (Frida Hook)

技术栈：Frida 16.5.6 + WebSocket + TCP
"""
import os
import re
import json
import time
import socket
import logging
import asyncio
import threading
import subprocess
from typing import Optional, Dict, Any, List, Literal, Tuple
from dataclasses import dataclass, field, asdict
from datetime import datetime

logger = logging.getLogger(__name__)

# ============================================================
# § 1  数据模型
# ============================================================

DeployMode = Literal["root_server", "gadget", "auto"]

@dataclass
class DeviceConnection:
    """设备连接信息"""
    device_id: str
    ip: str
    frida_port: int = 27042
    mode: DeployMode = "auto"
    status: str = "disconnected"  # disconnected / connecting / connected / error
    wechat_attached: bool = False
    last_heartbeat: Optional[str] = None
    error: Optional[str] = None
    frida_version: Optional[str] = None
    wechat_version: Optional[str] = None
    android_version: Optional[str] = None
    device_model: Optional[str] = None
    connected_at: Optional[str] = None
    reconnect_count: int = 0

    def to_dict(self) -> dict:
        return asdict(self)


@dataclass
class DeployConfig:
    """部署配置"""
    frida_version: str = "16.5.6"
    frida_arch: str = "arm64"  # arm / arm64 / x86 / x86_64
    listen_port: int = 27042
    listen_host: str = "0.0.0.0"
    auto_start: bool = True
    anti_detect: bool = True  # 随机端口 + 进程伪装
    gadget_config: dict = field(default_factory=lambda: {
        "interaction": {
            "type": "listen",
            "address": "0.0.0.0",
            "port": 27042,
            "on_load": "wait"
        }
    })


# ============================================================
# § 2  Frida 无线部署器
# ============================================================

class WirelessDeployer:
    """
    Frida 无线部署管理器
    
    职责：
    1. 在手机端部署 frida-server（Root）或 frida-gadget（免Root）
    2. 通过 WiFi TCP 连接 Frida
    3. 管理多设备连接池
    4. 自动发现局域网设备
    5. 健康检查与自动重连
    """

    FRIDA_DOWNLOAD_BASE = "https://github.com/frida/frida/releases/download"
    GADGET_DOWNLOAD_BASE = "https://github.com/nickcano/frida-gadget-releases/releases/download"

    def __init__(self, config: Optional[DeployConfig] = None):
        self.config = config or DeployConfig()
        self.connections: Dict[str, DeviceConnection] = {}
        self._lock = threading.Lock()
        self._running = False
        self._health_thread: Optional[threading.Thread] = None

    # ---- 设备连接管理 ----

    def add_device(self, device_id: str, ip: str, port: int = 27042,
                   mode: DeployMode = "auto") -> DeviceConnection:
        """添加设备到连接池"""
        conn = DeviceConnection(
            device_id=device_id,
            ip=ip,
            frida_port=port,
            mode=mode,
        )
        with self._lock:
            self.connections[device_id] = conn
        logger.info(f"设备已添加: {device_id} ({ip}:{port}) 模式={mode}")
        return conn

    def remove_device(self, device_id: str):
        """从连接池移除设备"""
        with self._lock:
            if device_id in self.connections:
                del self.connections[device_id]
                logger.info(f"设备已移除: {device_id}")

    def get_device(self, device_id: str) -> Optional[DeviceConnection]:
        """获取设备连接信息"""
        return self.connections.get(device_id)

    def list_devices(self) -> List[Dict[str, Any]]:
        """列出所有设备"""
        return [conn.to_dict() for conn in self.connections.values()]

    # ---- WiFi 直连 Frida ----

    def connect_device(self, device_id: str) -> Dict[str, Any]:
        """
        通过 WiFi 连接设备的 Frida
        
        流程：
        1. 检查设备 IP 可达性
        2. 尝试 TCP 连接 frida-server 端口
        3. 获取 Frida 设备信息
        4. attach 到微信进程
        """
        conn = self.connections.get(device_id)
        if not conn:
            return {"success": False, "error": f"设备不存在: {device_id}"}

        conn.status = "connecting"
        conn.error = None

        try:
            # Step 1: 检查 TCP 端口可达
            if not self._check_port(conn.ip, conn.frida_port, timeout=5):
                conn.status = "error"
                conn.error = f"Frida 端口不可达: {conn.ip}:{conn.frida_port}"
                return {"success": False, "error": conn.error}

            # Step 2: 通过 Frida API 连接
            import frida
            mgr = frida.get_device_manager()
            device = mgr.add_remote_device(f"{conn.ip}:{conn.frida_port}")

            # Step 3: 获取设备信息
            conn.frida_version = device.query_system_parameters().get("os", {}).get("version", "unknown")
            conn.device_model = device.name
            conn.status = "connected"
            conn.connected_at = datetime.now().isoformat()
            conn.last_heartbeat = conn.connected_at

            # Step 4: 尝试 attach 微信
            try:
                processes = device.enumerate_processes()
                wechat_proc = None
                for proc in processes:
                    if proc.name == "com.tencent.mm" or "微信" in proc.name:
                        wechat_proc = proc
                        break

                if wechat_proc:
                    conn.wechat_attached = True
                    logger.info(f"微信进程已发现: PID={wechat_proc.pid}")
                else:
                    conn.wechat_attached = False
                    logger.warning(f"微信进程未运行，等待启动")
            except Exception as e:
                logger.warning(f"枚举进程失败: {e}")
                conn.wechat_attached = False

            logger.info(f"✅ 设备 WiFi 连接成功: {device_id} ({conn.ip}:{conn.frida_port})")
            return {
                "success": True,
                "device_id": device_id,
                "ip": conn.ip,
                "port": conn.frida_port,
                "mode": conn.mode,
                "wechat_attached": conn.wechat_attached,
                "device_model": conn.device_model,
            }

        except ImportError:
            conn.status = "error"
            conn.error = "frida 未安装"
            return {"success": False, "error": "frida 未安装，请执行: pip install frida-tools"}
        except Exception as e:
            conn.status = "error"
            conn.error = str(e)
            conn.reconnect_count += 1
            logger.error(f"WiFi 连接失败: {device_id} - {e}")
            return {"success": False, "error": str(e)}

    def disconnect_device(self, device_id: str) -> Dict[str, Any]:
        """断开设备连接"""
        conn = self.connections.get(device_id)
        if not conn:
            return {"success": False, "error": f"设备不存在: {device_id}"}

        conn.status = "disconnected"
        conn.wechat_attached = False
        logger.info(f"设备已断开: {device_id}")
        return {"success": True, "device_id": device_id}

    # ---- Root 模式：远程启动 frida-server ----

    def generate_root_deploy_script(self, port: int = 0) -> str:
        """
        生成 Root 模式部署脚本（在 Termux 中执行）
        
        功能：
        - 下载对应架构的 frida-server
        - 以 root 权限启动，监听指定端口
        - 支持反检测（随机端口 + 进程名伪装）
        """
        if port == 0:
            import random
            port = random.randint(10000, 60000) if self.config.anti_detect else 27042

        version = self.config.frida_version
        arch = self.config.frida_arch
        server_name = f"frida-server-{version}-android-{arch}"
        download_url = f"{self.FRIDA_DOWNLOAD_BASE}/{version}/{server_name}.xz"

        # 反检测：伪装进程名
        disguise_name = "app_process64" if self.config.anti_detect else "frida-server"

        script = f"""#!/data/data/com.termux/files/usr/bin/bash
# ================================================================
#  Frida Server 无线部署脚本 (Root 模式)
#  版本: {version} | 架构: {arch} | 端口: {port}
# ================================================================
set -eo pipefail

RED='\\033[0;31m'; GREEN='\\033[0;32m'; NC='\\033[0m'
info()  {{ echo -e "${{GREEN}}[INFO]${{NC}}  $*"; }}
error() {{ echo -e "${{RED}}[ERROR]${{NC}} $*"; }}

# 检查 Root
if ! su -c "id" 2>/dev/null | grep -q "uid=0"; then
    error "需要 Root 权限"
    exit 1
fi
info "Root 权限确认 ✓"

# 检查网络
DEVICE_IP=$(ip route get 1 2>/dev/null | awk '{{print $NF; exit}}' || hostname -I | awk '{{print $1}}')
info "设备 IP: $DEVICE_IP"

# 下载 frida-server
FRIDA_DIR="$HOME/.frida-server"
mkdir -p "$FRIDA_DIR"
FRIDA_BIN="$FRIDA_DIR/{disguise_name}"

if [ ! -f "$FRIDA_BIN" ]; then
    info "下载 frida-server {version} ({arch})..."
    curl -sL "{download_url}" -o "$FRIDA_DIR/frida-server.xz"
    xz -d -f "$FRIDA_DIR/frida-server.xz"
    mv "$FRIDA_DIR/frida-server" "$FRIDA_BIN"
    chmod +x "$FRIDA_BIN"
    info "下载完成 ✓"
else
    info "frida-server 已存在 ✓"
fi

# 停止旧进程
su -c "pkill -f frida-server" 2>/dev/null || true
su -c "pkill -f {disguise_name}" 2>/dev/null || true
sleep 1

# 复制到系统目录并启动
su -c "cp $FRIDA_BIN /data/local/tmp/{disguise_name}"
su -c "chmod 755 /data/local/tmp/{disguise_name}"
su -c "nohup /data/local/tmp/{disguise_name} -l 0.0.0.0:{port} &" 2>/dev/null
sleep 2

# 验证
if su -c "netstat -tlnp 2>/dev/null" | grep -q ":{port}"; then
    info "✅ frida-server 已启动，监听 0.0.0.0:{port}"
    info "📱 连接地址: $DEVICE_IP:{port}"
    echo ""
    echo "在服务器端执行:"
    echo "  frida -H $DEVICE_IP:{port} -n com.tencent.mm"
    echo ""
else
    error "frida-server 启动失败"
    exit 1
fi

# 写入自启动
BOOT_SCRIPT="$HOME/.frida-autostart.sh"
cat > "$BOOT_SCRIPT" << 'AUTOSTART'
#!/data/data/com.termux/files/usr/bin/bash
sleep 10
su -c "nohup /data/local/tmp/{disguise_name} -l 0.0.0.0:{port} &" 2>/dev/null
AUTOSTART
chmod +x "$BOOT_SCRIPT"

# Termux:Boot 自启动
BOOT_DIR="$HOME/.termux/boot"
mkdir -p "$BOOT_DIR"
ln -sf "$BOOT_SCRIPT" "$BOOT_DIR/frida-autostart.sh"
info "开机自启已配置 ✓"
"""
        return script

    # ---- 免Root 模式：Gadget 注入 ----

    def generate_gadget_deploy_script(self, apk_path: str = "", port: int = 0) -> str:
        """
        生成免 Root 模式部署脚本
        
        原理：
        - 将 frida-gadget.so 注入到微信 APK 的 lib 目录
        - 配置 gadget 监听 TCP 端口
        - 重新签名并安装修改后的 APK
        
        注意：免 Root 模式需要重新安装微信，会丢失聊天记录
        """
        if port == 0:
            import random
            port = random.randint(10000, 60000) if self.config.anti_detect else 27042

        version = self.config.frida_version
        arch = self.config.frida_arch
        gadget_name = f"frida-gadget-{version}-android-{arch}.so"
        gadget_url = f"{self.FRIDA_DOWNLOAD_BASE}/{version}/{gadget_name}.xz"

        gadget_config = json.dumps({
            "interaction": {
                "type": "listen",
                "address": "0.0.0.0",
                "port": port,
                "on_load": "wait"
            }
        }, indent=2)

        script = f"""#!/data/data/com.termux/files/usr/bin/bash
# ================================================================
#  Frida Gadget 无线部署脚本 (免Root 模式)
#  版本: {version} | 架构: {arch} | 端口: {port}
# ================================================================
set -eo pipefail

RED='\\033[0;31m'; GREEN='\\033[0;32m'; YELLOW='\\033[0;33m'; NC='\\033[0m'
info()  {{ echo -e "${{GREEN}}[INFO]${{NC}}  $*"; }}
warn()  {{ echo -e "${{YELLOW}}[WARN]${{NC}}  $*"; }}
error() {{ echo -e "${{RED}}[ERROR]${{NC}} $*"; }}

# 安装依赖
pkg install -y apksigner aapt zip unzip curl xz-utils 2>/dev/null || true

# 下载 frida-gadget
GADGET_DIR="$HOME/.frida-gadget"
mkdir -p "$GADGET_DIR"
GADGET_SO="$GADGET_DIR/libfrida-gadget.so"

if [ ! -f "$GADGET_SO" ]; then
    info "下载 frida-gadget {version} ({arch})..."
    curl -sL "{gadget_url}" -o "$GADGET_DIR/gadget.so.xz"
    xz -d -f "$GADGET_DIR/gadget.so.xz"
    mv "$GADGET_DIR/gadget.so" "$GADGET_SO"
    info "下载完成 ✓"
fi

# Gadget 配置文件
cat > "$GADGET_DIR/libfrida-gadget.config.so" << 'GADGET_CFG'
{gadget_config}
GADGET_CFG

APK_PATH="{apk_path}"
if [ -z "$APK_PATH" ]; then
    # 自动查找已安装的微信 APK
    APK_PATH=$(pm path com.tencent.mm 2>/dev/null | head -1 | sed 's/package://')
    if [ -z "$APK_PATH" ]; then
        error "未找到微信 APK，请指定路径"
        exit 1
    fi
fi
info "微信 APK: $APK_PATH"

# 解包
WORK_DIR="$GADGET_DIR/work"
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR"
cp "$APK_PATH" "$WORK_DIR/base.apk"
cd "$WORK_DIR"

# 解压 APK
unzip -q base.apk -d apk_contents

# 注入 gadget
LIB_DIR="apk_contents/lib/{arch.replace('arm64', 'arm64-v8a').replace('arm', 'armeabi-v7a')}"
mkdir -p "$LIB_DIR"
cp "$GADGET_SO" "$LIB_DIR/libfrida-gadget.so"
cp "$GADGET_DIR/libfrida-gadget.config.so" "$LIB_DIR/libfrida-gadget.config.so"

# 修改 AndroidManifest.xml 加载 gadget（smali 注入方式更可靠，这里用简化方案）
# 在 Application 类的 static 块中加载 libfrida-gadget.so
info "注入 frida-gadget 到 lib 目录..."

# 重新打包
cd apk_contents
zip -q -r ../patched.apk .
cd ..

# 签名
info "签名 APK..."
# 生成临时签名密钥
keytool -genkey -v -keystore "$GADGET_DIR/debug.keystore" \\
    -storepass android -alias androiddebugkey -keypass android \\
    -keyalg RSA -keysize 2048 -validity 10000 \\
    -dname "CN=Debug, OU=Debug, O=Debug, L=Debug, S=Debug, C=US" 2>/dev/null || true

apksigner sign --ks "$GADGET_DIR/debug.keystore" \\
    --ks-pass pass:android --key-pass pass:android \\
    --out patched_signed.apk patched.apk

# 安装
warn "⚠️  即将安装修改版微信，原版数据可能丢失"
warn "   建议先备份聊天记录"
info "安装修改版微信..."
pm install -r -d patched_signed.apk

DEVICE_IP=$(ip route get 1 2>/dev/null | awk '{{print $NF; exit}}' || hostname -I | awk '{{print $1}}')
info "✅ Gadget 注入完成"
info "📱 启动微信后，连接地址: $DEVICE_IP:{port}"
echo ""
echo "在服务器端执行:"
echo "  frida -H $DEVICE_IP:{port} -n Gadget"
echo ""
"""
        return script

    # ---- 混合模式：自动检测 Root 并选择最佳方案 ----

    def generate_auto_deploy_script(self, server_url: str, port: int = 0) -> str:
        """
        生成自动检测部署脚本
        
        逻辑：
        1. 检测是否有 Root 权限
        2. Root → 使用 frida-server 模式
        3. 非 Root → 使用 frida-gadget 模式
        4. 自动连接到 SDK 服务器
        """
        if port == 0:
            import random
            port = random.randint(10000, 60000) if self.config.anti_detect else 27042

        version = self.config.frida_version
        arch = self.config.frida_arch

        script = f"""#!/data/data/com.termux/files/usr/bin/bash
# ================================================================
#  工作手机 SDK - Frida 无线自动部署
#  版本: {version} | 端口: {port}
#  服务器: {server_url}
# ================================================================
set -eo pipefail

RED='\\033[0;31m'; GREEN='\\033[0;32m'; YELLOW='\\033[0;33m'; CYAN='\\033[0;36m'; NC='\\033[0m'
info()  {{ echo -e "${{GREEN}}[INFO]${{NC}}  $*"; }}
warn()  {{ echo -e "${{YELLOW}}[WARN]${{NC}}  $*"; }}
error() {{ echo -e "${{RED}}[ERROR]${{NC}} $*"; }}

DEVICE_ID=$(getprop ro.serialno 2>/dev/null || echo "dev-$(date +%s)")
DEVICE_IP=$(ip route get 1 2>/dev/null | awk '{{print $NF; exit}}' || hostname -I | awk '{{print $1}}')
FRIDA_PORT={port}
SERVER_URL="{server_url}"

info "设备ID: $DEVICE_ID"
info "设备IP: $DEVICE_IP"
info "Frida端口: $FRIDA_PORT"

# ---- 检测 Root ----
HAS_ROOT=false
if su -c "id" 2>/dev/null | grep -q "uid=0"; then
    HAS_ROOT=true
    info "✅ Root 权限可用 → 使用 frida-server 模式"
else
    warn "⚠️  无 Root 权限 → 使用 frida-gadget 模式"
fi

if [ "$HAS_ROOT" = true ]; then
    # ========== Root 模式 ==========
    FRIDA_DIR="$HOME/.frida-server"
    mkdir -p "$FRIDA_DIR"
    FRIDA_BIN="$FRIDA_DIR/fs-{version}"

    if [ ! -f "$FRIDA_BIN" ]; then
        info "下载 frida-server {version} ({arch})..."
        curl -sL "{WirelessDeployer.FRIDA_DOWNLOAD_BASE}/{version}/frida-server-{version}-android-{arch}.xz" \\
            -o "$FRIDA_DIR/fs.xz"
        xz -d -f "$FRIDA_DIR/fs.xz"
        mv "$FRIDA_DIR/fs" "$FRIDA_BIN"
        chmod +x "$FRIDA_BIN"
    fi

    # 停止旧进程
    su -c "pkill -f frida-server" 2>/dev/null || true
    sleep 1

    # 启动
    su -c "cp $FRIDA_BIN /data/local/tmp/frida-server"
    su -c "chmod 755 /data/local/tmp/frida-server"
    su -c "nohup /data/local/tmp/frida-server -l 0.0.0.0:$FRIDA_PORT &" 2>/dev/null
    sleep 2

    if su -c "netstat -tlnp 2>/dev/null" | grep -q ":$FRIDA_PORT"; then
        info "✅ frida-server 已启动"
    else
        error "frida-server 启动失败"
        exit 1
    fi

    DEPLOY_MODE="root_server"
else
    # ========== 免Root 模式 ==========
    info "免Root 模式需要重新安装修改版微信"
    info "请先运行 gadget 部署脚本"
    DEPLOY_MODE="gadget"
fi

# ---- 向服务器注册 ----
info "向服务器注册设备..."
REGISTER_DATA=$(cat << EOF
{{
    "device_id": "$DEVICE_ID",
    "ip": "$DEVICE_IP",
    "frida_port": $FRIDA_PORT,
    "mode": "$DEPLOY_MODE",
    "android_version": "$(getprop ro.build.version.release)",
    "device_model": "$(getprop ro.product.model)",
    "frida_version": "{version}"
}}
EOF
)

# 通过 HTTP API 注册
API_URL=$(echo "$SERVER_URL" | sed 's|ws://|http://|' | sed 's|wss://|https://|' | sed 's|/ws/device||')
curl -s -X POST "$API_URL/api/v3/frida/register" \\
    -H "Content-Type: application/json" \\
    -d "$REGISTER_DATA" || warn "服务器注册失败，稍后重试"

info "🎉 部署完成！"
info "连接信息: $DEVICE_IP:$FRIDA_PORT ($DEPLOY_MODE)"
"""
        return script

    # ---- 局域网设备发现 ----

    def discover_devices(self, subnet: str = "", port: int = 27042,
                         timeout: float = 2.0) -> List[Dict[str, Any]]:
        """
        扫描局域网内运行 frida-server 的设备
        
        Args:
            subnet: 子网前缀，如 "192.168.1"，空则自动检测
            port: Frida 端口
            timeout: 每个 IP 的超时时间
        """
        if not subnet:
            subnet = self._detect_subnet()

        if not subnet:
            return []

        found = []
        logger.info(f"扫描子网 {subnet}.0/24 端口 {port}...")

        def _scan_ip(ip: str):
            if self._check_port(ip, port, timeout):
                try:
                    import frida
                    mgr = frida.get_device_manager()
                    device = mgr.add_remote_device(f"{ip}:{port}")
                    params = device.query_system_parameters()
                    found.append({
                        "ip": ip,
                        "port": port,
                        "name": device.name,
                        "os": params.get("os", {}).get("id", "unknown"),
                        "arch": params.get("arch", "unknown"),
                    })
                    logger.info(f"  发现设备: {ip}:{port} ({device.name})")
                except Exception:
                    # 端口开放但不是 Frida
                    pass

        threads = []
        for i in range(1, 255):
            ip = f"{subnet}.{i}"
            t = threading.Thread(target=_scan_ip, args=(ip,))
            t.daemon = True
            threads.append(t)
            t.start()

        for t in threads:
            t.join(timeout=timeout + 1)

        logger.info(f"扫描完成，发现 {len(found)} 个设备")
        return found

    # ---- 健康检查 ----

    def start_health_check(self, interval: int = 30):
        """启动后台健康检查线程"""
        self._running = True
        self._health_thread = threading.Thread(
            target=self._health_check_loop,
            args=(interval,),
            daemon=True,
        )
        self._health_thread.start()
        logger.info(f"健康检查已启动，间隔 {interval}s")

    def stop_health_check(self):
        """停止健康检查"""
        self._running = False
        if self._health_thread:
            self._health_thread.join(timeout=5)
        logger.info("健康检查已停止")

    def _health_check_loop(self, interval: int):
        """健康检查循环"""
        while self._running:
            for device_id, conn in list(self.connections.items()):
                if conn.status == "connected":
                    if not self._check_port(conn.ip, conn.frida_port, timeout=3):
                        conn.status = "disconnected"
                        conn.wechat_attached = False
                        logger.warning(f"设备离线: {device_id}")
                    else:
                        conn.last_heartbeat = datetime.now().isoformat()
                elif conn.status == "disconnected" and conn.reconnect_count < 10:
                    # 尝试自动重连
                    result = self.connect_device(device_id)
                    if result.get("success"):
                        logger.info(f"设备自动重连成功: {device_id}")
            time.sleep(interval)

    # ---- 工具方法 ----

    @staticmethod
    def _check_port(ip: str, port: int, timeout: float = 3.0) -> bool:
        """检查 TCP 端口是否可达"""
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(timeout)
            result = sock.connect_ex((ip, port))
            sock.close()
            return result == 0
        except Exception:
            return False

    @staticmethod
    def _detect_subnet() -> str:
        """自动检测本机子网"""
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            ip = s.getsockname()[0]
            s.close()
            parts = ip.split(".")
            return ".".join(parts[:3])
        except Exception:
            return ""

    def get_status(self) -> Dict[str, Any]:
        """获取部署器状态"""
        connected = sum(1 for c in self.connections.values() if c.status == "connected")
        total = len(self.connections)
        return {
            "total_devices": total,
            "connected_devices": connected,
            "disconnected_devices": total - connected,
            "health_check_running": self._running,
            "config": {
                "frida_version": self.config.frida_version,
                "frida_arch": self.config.frida_arch,
                "anti_detect": self.config.anti_detect,
            },
            "devices": self.list_devices(),
        }


# ============================================================
# § 3  全局单例
# ============================================================

wireless_deployer = WirelessDeployer()
