苹果群控USB HID技术教程

苹果群控技术拆解:iOS 免越狱下 USB HID 的原理与 API 完整教程

从原理到代码拆解苹果群控里的 USB HID 链路:电脑如何冒充 HID 设备注入触控、五个 API 快速上手、usbHidEvent 完整函数清单、Python 走 HTTP 接口的写法,附坐标校准、掉线排查等高频坑的处理顺序。

约 21 分钟

一台电脑拖着几十台 iPhone 干活,这套做法叫苹果群控。它不越狱,也不往手机上装东西,靠的是把操作指令从外部注入设备。

适用版本:中控 EasyClick iOS USB 10.7.0+ | 手机系统 iOS 17+ 技术栈:EasyClick iOS USB 中控 + JavaScript 脚本(也支持 Python 等语言走 HTTP 接口) 面向场景:苹果群控(一对多批量控制 iPhone)的底层链路实现 官方文档:https://ieasyclick.com/iosdocs/ | USB HID 函数手册:https://ieasyclick.com/iosdocs/funcs/usb-hid-event-api/

iOS 自动化难在哪,USB HID 凭什么免签名

安卓有 AccessibilityService(无障碍服务),能直接读界面元素、模拟点击;iOS 没这个待遇,系统封闭,没给第三方开放的无障碍接口。所以在 iPhone 上做自动化只有两条路线。第一条是软件注入:装一个代理 App,通过 XCTest 在 App 内部注入指令,问题是需要签名——签完还要装描述文件、手动信任开发者,签名有期限、过期就得重来,App 或系统一升级代理容易失效。第二条是模拟外部输入设备:让电脑或开发板冒充真实的鼠标键盘,通过系统底层输入通道把触摸事件送进去,iOS 无法区分真假,所以更干净。这篇讲的 USB HID 就是第二条里最省事的一种:一根 USB 数据线就够,不用额外买开发板;一台中控电脑能同时带多台 iPhone,设备从三台加到三十台,代码不用改。

HID 是 Human Interface Device(人机接口设备),鼠标、键盘、游戏手柄都属于它。HID 设备通过标准描述符向主机声明自己是什么类型、支持哪些输入输出,这个规范公开,且被包括 iOS 在内的所有主流操作系统原生支持。关键在于,当一个设备通过 USB 声明自己是 HID 键盘时,iOS 会无条件信任它——电脑端软件构造出符合 HID 规范的报文,告诉手机“我刚按下了屏幕坐标 (300, 500) 的位置”,手机的输入系统就把它当成一次真实的触摸事件分发下去,对系统和 App 来说这跟手指点上去没有区别。真手指和假手指,系统分得开吗?分不开,这正是这条路能走通的原因。

同样思路还有另外两种实现方式:

链路 硬件依赖 系统要求 适合场景
USB HID 一根 USB 数据线 iOS 17+,中控 10.7.0+ 最省事,单机快速验证
蓝牙 BLE HID ESP32C3 开发板 iOS 18+ 更优 配合无截图模式可完全绕开屏幕镜像
OTG HID ESP32S3 开发板 通用 想脱离电脑独立运行

三种方式的 API 能力基本一致,区别在链路和硬件依赖。动手前先确认几件事:中控版本要 EasyClick iOS USB 10.7.0 及以上(低版本没有这套 API);手机系统建议 iOS 17 以上,16 及以下的部分机型可能走不通,可用蓝牙或 OTG 替代;数据线插好、手机上点“信任此电脑”、中控里确认已启动桥接;线一定用原装或 MFi 认证的,杂牌线很多只能充电不能传数据,是新手最常踩的坑。USB 设备授权和自动化环境卡住,可以直接对照官方文档:https://ieasyclick.com/iosdocs/advance/ai-agent/prerequisites

五个 API 上手

所有 USB HID 操作都挂在 usbHidEvent 这个对象下面。先看一个能跑的最小示例:

function main() {
    // 1. 开启会话
    let r = usbHidEvent.sessionStart(true);
    if (!(r == null || r === "")) {
        logw("开启会话失败: " + r);
        return;
    }

    // 2. 设置屏幕尺寸,坐标换算依赖这个参数
    r = usbHidEvent.setScreenSize(1170, 2532);
    if (!(r == null || r === "")) {
        logw("设置尺寸失败: " + r);
        return;
    }

    // 3. 点击坐标 (300, 400)
    r = usbHidEvent.clickPoint(300, 400);
    logd("点击结果: " + (r == null || r === "" ? "成功" : r));

    // 4. 输入文字,走剪贴板粘贴,中英文通用
    r = usbHidEvent.inputText("Hello 自动化");
    logd("输入结果: " + (r == null || r === "" ? "成功" : r));

    // 5. 关闭会话
    usbHidEvent.sessionStop();
}
main();

看代码应该注意到了,每个函数都在判断 r == null || r === ""。这是这套 API 的统一约定:返回值是 null 或空字符串表示成功,返回其它字符串就是错误信息,所以写判断不需要 try-catch,直接判断返回值就行。建议封装一个工具函数:

function _ok(r) {
    return r == null || r === "";
}

sessionStart(gate) 的 gate 默认 true,作用是尝试增强兼容模式,低版本系统会自动忽略;会话已存在时会复用,需要彻底重置(断流、触摸失效)时用 sessionRestart,它等价于先 sessionStop 再 sessionStart。坐标上,setScreenSize(w, h) 设的是屏幕像素宽高,脚本坐标、投屏坐标、截图坐标三者统一,投屏里量出来的值可直接写进脚本;但分辨率变化或横竖屏切换后必须重调,否则坐标整体偏移,点歪了先查它有没有设对。

完整函数清单与 HTTP 接口

下面按用途分类整理,方便速查,每个函数的完整参数和示例以官方手册为准:https://ieasyclick.com/iosdocs/funcs/usb-hid-event-api/

会话管理:

函数 说明
sessionStart(gate) 开启会话,已存在则复用
sessionStop() 关闭会话,释放资源
sessionRestart(gate) 强制重建会话,用于排障

屏幕与坐标:

函数 说明
setScreenSize(w, h) 设置屏幕像素宽高

触控操作:

函数 说明
clickPoint(x, y) 单击
doubleClickPoint(x, y) 双击
press(x, y, delay) 长按,delay 为按住毫秒数
swipeToPoint(x1, y1, x2, y2, duration) 从起点滑动到终点
touchDown(x, y) 按下
touchMove(x, y) 移动
touchUp(x, y) 抬起
multiTouch(points, timeout) 按轨迹回放多点触摸

multiTouch 的轨迹点格式里,action 中 0 表示按下、1 表示抬起、2 表示移动,delay 是该点的延迟毫秒数:

let trace = [
    {"action": 0, "x": 100, "y": 500, "delay": 20},
    {"action": 2, "x": 100, "y": 300, "delay": 30},
    {"action": 1, "x": 100, "y": 300, "delay": 20}
];
usbHidEvent.multiTouch(trace, 10000);

它适合做画圈、双指缩放这类复杂手势。文字输入:

函数 行为 使用建议
inputText(text) 统一走剪贴板粘贴 默认用这个,中英文都稳
typeText(text) 可打印英文走键盘逐键,含中文或 emoji 自动改为粘贴 需要模拟真实打字时用
setClipboard(text) 只写入剪贴板,不粘贴 配合组合键手动触发粘贴时用
getClipboard() 读取剪贴板 有已知限制,见踩坑部分

按键操作:

函数 说明
keyPressChar(prefix, code) 字符按键或组合键
keyPress(key) 按下单个键
keyUp() 抬起全部按键
systemKey(key) 系统按键,支持 home、recents、lock

组合键的 prefix 可以是 alt、ctrl、gui、shift、r_ctrl、r_shift,不需要组合时传空字符串,gui 对应 iOS 上的 Command 键,模拟粘贴的写法是:

usbHidEvent.keyPressChar("gui", "v");

音量控制:

函数 说明
volumeUp() 音量加
volumeDown() 音量减
mute() 静音

不写 JavaScript,也可以用其它语言通过 HTTP 对接中控。所有接口都是 POST、请求体是 JSON,地址前缀默认 http://127.0.0.1:8019,路径与脚本函数一一对应,完整的请求参数、返回字段和 Python / Node.js / cURL / C# 四种示例见官方文档:https://ieasyclick.com/iosdocs/advance/openapi/usbhid

脚本函数 HTTP 路径
sessionStart /openapi/usbhidSessionStart
sessionStop /openapi/usbhidSessionStop
sessionRestart /openapi/usbhidSessionRestart
setScreenSize /openapi/usbhidSetScreenSize
clickPoint /openapi/usbhidClickPoint
doubleClickPoint /openapi/usbhidDoubleClickPoint
press /openapi/usbhidPress
swipeToPoint /openapi/usbhidSwipeToPoint
touchDown/Move/Up /openapi/usbhidTouchDown 等
multiTouch /openapi/usbhidMultiTouch
inputText /openapi/usbhidInputText
typeText /openapi/usbhidTypeText
systemKey /openapi/usbhidSystemKey
keyPressChar /openapi/usbhidKeyPressChar
keyPress/keyUp /openapi/usbhidKeyPress、/openapi/usbhidKeyUp
volumeUp/Down/mute /openapi/usbhidVolumeUp 等
setClipboard /openapi/usbhidSetClipboard
getClipboard /openapi/usbhidGetClipboard

HTTP 接口返回的是 JSON:

{
  "code": 0,
  "msg": "",
  "data": ""
}

code 为 0 表示成功,非 0 时 msg 是错误信息。

import requests

BASE = "http://127.0.0.1:8019"
DEVICE_ID = "你的设备 ID"  # 从设备列表接口获取

def call(path, body=None):
    r = requests.post(f"{BASE}{path}", json=body or {}, timeout=30)
    data = r.json()
    if data.get("code") != 0:
        raise RuntimeError(f"{path} 失败: {data.get('msg')}")
    return data

# 开会话
call("/openapi/usbhidSessionStart", {"deviceId": DEVICE_ID, "gate": True})

# 设置屏幕尺寸
call("/openapi/usbhidSetScreenSize", {"deviceId": DEVICE_ID, "w": 1170, "h": 2532})

# 点击
call("/openapi/usbhidClickPoint", {"deviceId": DEVICE_ID, "x": 300, "y": 400})

# 输入文字
call("/openapi/usbhidInputText", {"deviceId": DEVICE_ID, "text": "Hello"})

# 关闭会话
call("/openapi/usbhidSessionStop", {"deviceId": DEVICE_ID})

这种方式适合把自动化能力接进已有的 Python 系统,比如电商后台、测试平台、运维工单。

一个完整流程,和踩过的坑

假设需求是:打开某个 App,等页面加载完成,点击一个按钮,输入内容并提交,然后回到桌面。

function _ok(r) {
    return r == null || r === "";
}

// 随机等待,避免机械节奏
function randSleep(minSec, maxSec) {
    let ms = (minSec + Math.random() * (maxSec - minSec)) * 1000;
    sleep(parseInt(ms));
}

function main() {
    let r = usbHidEvent.sessionStart(true);
    if (!_ok(r)) {
        logw("会话开启失败: " + r);
        return;
    }

    // 屏幕尺寸要与当前实际分辨率一致
    r = usbHidEvent.setScreenSize(1170, 2532);
    if (!_ok(r)) {
        logw("设置尺寸失败: " + r);
        return;
    }

    // 步骤一:回到桌面,确保起点一致
    usbHidEvent.systemKey("home");
    randSleep(1, 2);

    // 步骤二:打开目标 App(假设图标在第一屏第二个位置)
    r = usbHidEvent.clickPoint(400, 780);
    if (!_ok(r)) {
        logw("打开 App 失败: " + r);
        return;
    }
    randSleep(4, 7);  // 等页面加载

    // 步骤三:点击输入框
    r = usbHidEvent.clickPoint(585, 1200);
    if (!_ok(r)) {
        logw("点击输入框失败: " + r);
        return;
    }
    randSleep(1, 2);

    // 步骤四:输入内容
    r = usbHidEvent.inputText("这是自动化写入的内容");
    if (!_ok(r)) {
        logw("输入失败: " + r);
        return;
    }
    randSleep(1, 2);

    // 步骤五:点击提交按钮
    r = usbHidEvent.clickPoint(585, 1600);
    if (!_ok(r)) {
        logw("提交失败: " + r);
        return;
    }
    randSleep(2, 4);

    // 步骤六:回到桌面
    usbHidEvent.systemKey("home");

    usbHidEvent.sessionStop();
    logd("流程执行完成");
}
main();

三点说明:每步都判断返回值,某一步失败后继续就是在错误前提下跑,早失败早退出、日志也更好定位;等待时间要随机,固定间隔本身就是机器特征;坐标靠投屏截图量,别照搬别人脚本里的数字,机型不同坐标系就不同。

踩过的坑按频率排。坐标整体偏移,多是没调 setScreenSize,或横竖屏切换后忘了重调,还有个隐蔽原因是机型不统一——一套坐标在 iPhone 11 上对、换到 iPhone 12 就偏,批量场景建议用同型号。设备频繁掉线,按顺序排查:换原装线、USB 口换主板直出的、集线器用带独立电源的、单个集线器负载不超过 7-8 台。粘贴英文多出空格,是 iOS 键盘设置问题,到设置 → 通用 → 键盘关掉“智能标点”即可。getClipboard 有已知限制:setClipboard 写入后马上读通常没问题,但读手机上手动长按复制的内容,在部分 iOS 版本(如 26.x)会超时甚至读不到,严重时卡住剪贴板服务、需重启手机,别依赖它读人手复制的内容。系统半夜自动更新也会让脚本失效,跑自动化的设备一定要关掉自动更新。

最后是选型。USB HID 适合快速验证和单机场景,一根线就能跑,缺点是必须连着电脑;蓝牙 BLE HID 需要一块 ESP32C3 开发板,几十块钱,独特优势是能配合无自动化截图模式、整个链路不走屏幕镜像,代价是投屏帧率不高、配置更繁琐;OTG HID 用 ESP32S3 直连手机,可脱离电脑运行。先用起来建议从 USB HID 入门,跑通再按需升级。后两条链路的文档官方站都有:蓝牙 BLE 看 https://ieasyclick.com/iosdocs/funcs/ble-event-api ,OTG HID 看 https://ieasyclick.com/iosdocs/funcs/otg-event-api 。三条路线怎么取舍,站内有一篇横向对比,看那张表比看文字快:三条 HID 路线横向对比。

把要点捋一遍。USB HID 的核心思路是让电脑冒充 HID 设备、通过系统输入通道注入触摸事件,iOS 无法区分真假,所以不需越狱也不需签名,这也让它成了苹果群控里最省事的免越狱链路——不用买蓝牙板或 OTG 板,一根数据线就能跑通,脚本还能在多台设备上复用。API 层面记住三件事:会话先开、屏幕尺寸设对、返回值 null 或空字符串表示成功。排查顺序基本固定:先看物理连接,再看参数设置,最后才怀疑代码逻辑。

文中涉及的接口和配置,官方文档都有完整说明:

站内也写过几篇相关的,视角不同,可以对着看:

本文基于 EasyClick iOS USB 版开发文档整理,代码片段可直接在中控脚本编辑器中运行。


关于 EasyClick:手机自动化 AI 智能体平台,覆盖安卓免 root、iOS 免越狱、鸿蒙 Next 三大生态,提供脚本开发、苹果群控、本地中控投屏与云控系统。→ 了解全部产品

想要真实跑起来?

本文介绍的方案均可在 EasyClick 手机自动化平台落地。官网提供完整文档、开发工具与群控云控产品,免费体验。

访问 EasyClick 官网 →