苹果群控USB HID技术教程

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

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

约 23 分钟

适用版本:中控 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 自动化到底难在哪

做过安卓自动化的同学应该知道,安卓那边有 AccessibilityService(无障碍服务),可以直接读取界面元素、模拟点击,开发体验相当舒服。

iOS 就没这个待遇。系统封闭,没有开放的无障碍接口给第三方调用。

所以想在 iPhone 上做自动化,不管是单机脚本还是苹果群控这种一对多的批量场景,绕来绕去只有两条技术路线:

第一条是软件注入。装一个代理 App,通过 XCTest 框架在 App 内部注入测试指令。这条路能用,但问题不少:需要签名,签完还要装描述文件、手动信任开发者;签名有期限,过期就得重来;App 或系统一升级,代理容易失效。

第二条是模拟外部输入设备。让电脑或者一块开发板冒充真实的鼠标键盘,通过系统底层的输入通道把触摸事件送进去。iOS 无法区分这是真实外设还是程序注入,所以这条路更干净。

这篇要讲的 USB HID,就是第二条路线里最省事的一种:只要一根 USB 数据线,不需要额外买开发板。

对做苹果群控的人来说,这条路最有价值的地方在于:一台中控电脑可以同时带多台 iPhone,每台都按同样的方式注入触控,脚本逻辑完全一致。设备从三台加到三十台,代码不用改。


二、USB HID 的原理拆解

HID 是 Human Interface Device 的缩写,即人机接口设备。你手上的鼠标、键盘、游戏手柄,都属于 HID 设备。

协议层面,HID 设备通过标准描述符向主机声明自己是什么类型的设备、支持哪些输入输出。这个规范是公开的,而且被所有主流操作系统原生支持,包括 iOS。

关键在于,当一个设备通过 USB 声明自己是 HID 键盘时,iOS 会无条件信任它,把它当成真实的键盘。

这就给了一条巧妙的路子。电脑端软件构造出符合 HID 规范的报文,告诉手机“我刚按下了屏幕坐标 (300, 500) 的位置”,手机的输入系统就会把它当成一次真实的触摸事件分发下去。

对系统和 App 来说,这跟用户手指点上去没有区别。

和其他两条 HID 链路的对比

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

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

三种方式的 API 能力基本一致,区别在链路和硬件依赖。选哪种后面会讲。


三、环境准备

动手之前,先确认这几件事:

  • 中控版本:USB HID 功能需要 EasyClick iOS USB 中控 10.7.0 及以上。低版本没有这套 API。
  • 手机系统:建议 iOS 17 以上。16 及以下的部分机型可能走不通这条路,可以用蓝牙或 OTG 替代。
  • 连接状态:USB 数据线插好,手机上点“信任此电脑”,中控里确认已经启动桥接。
  • 数据线:一定用原装或者 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 的参数

sessionStart(gate) 有一个可选参数 gate,默认 true,作用是尝试增强兼容模式。低版本系统会自动忽略这个参数,所以一般不用管,保持默认即可。

会话已经存在时会复用,不会重复创建。如果需要彻底重置(比如断流、触摸失效),用 sessionRestart,它等价于先 sessionStopsessionStart,比单独重开更彻底。

坐标系统

setScreenSize(w, h) 设置的是屏幕像素宽高,单位是像素。

这里有个关键点:脚本坐标、投屏画面坐标、截图像素坐标,三者是统一的。

也就是说,你在投屏界面上量出来的坐标,可以直接写进脚本。

但要注意:分辨率变化或者横竖屏切换之后,必须重新调用 setScreenSize,否则坐标会整体偏移。这是最高频的坑之一。


五、API 完整清单

下面按用途分类整理,方便速查。每个函数的完整参数说明和示例代码以官方手册为准: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) 系统按键,支持 homerecentslock

组合键的 prefix 可以是 altctrlguishiftr_ctrlr_shift,不需要组合时传空字符串。

模拟粘贴的写法:

// gui 对应 iOS 上的 Command 键
usbHidEvent.keyPressChar("gui", "v");

音量控制

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

六、进阶:用 Python 走 HTTP 接口

如果不写 JavaScript,也可以用其它语言通过 HTTP 对接中控。所有接口都是 POST,请求体是 JSON。完整的请求参数、返回字段和 Python / Node.js / cURL / C# 四种示例见官方开放接口文档:https://ieasyclick.com/iosdocs/advance/openapi/usbhid

地址前缀是中控地址,默认 http://127.0.0.1:8019,路径与脚本函数一一对应:

脚本函数 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 是错误信息。

Python 示例

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();

几点说明:

  • 每步都判断返回值:一旦某一步失败,后面的操作就是在错误的前提下继续,可能导致更糟的结果。早失败早退出,日志也更好定位。
  • 等待时间要随机:固定间隔本身就是机器特征。真人操作时,看页面可能花 2 秒,也可能花 10 秒。用随机函数包一层,能显著降低被识别的概率。
  • 坐标靠截图量:用投屏界面截图量,量出来的值可以直接用。别照搬别人脚本里的数字,机型不同坐标系就不同。

八、踩坑记录

这些都是我在实际使用中踩过的,按出现频率排序。

坐标整体偏移

最常见的原因是没调 setScreenSize,或者横竖屏切换之后忘了重新调。

还有一个隐蔽原因是设备型号不统一。不同机型分辨率不同,一套坐标在 iPhone 11 上是对的,换到 iPhone 12 就偏了。批量场景下建议用同型号设备。

设备频繁掉线

按这个顺序排查:数据线(换原装线)、USB 口供电(换主板直出的口)、集线器(用带独立电源的)、设备数量(单个集线器负载不超过 7 到 8 台)。

粘贴英文时多出空格

这是 iOS 的键盘设置问题。到手机的设置 → 通用 → 键盘,关闭“智能标点”,问题就消失了。

getClipboard 有已知限制

这个函数经 CoreDevice 直读,setClipboard 写入之后马上读通常没问题。

但如果读的是手机上手动长按复制的内容,在部分 iOS 版本(比如 26.x)上会超时甚至读不到,严重时会把设备的剪贴板服务卡住,需要重启手机才能恢复。

所以不要依赖它去读取人手复制的内容。 目前更稳的做法是用快捷指令绕,或者干脆改成让脚本自己写入再读。

系统半夜自动更新导致脚本失效

跑自动化的设备一定要关掉自动更新。一夜之间系统升级,第二天可能整套流程都跑不起来。这个设置是必须做的,不是可选项。


九、三种 HID 方案怎么选

最后回到选型问题。

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 。配置步骤也各有一篇教程,搜「蓝牙 BLE 教程」「OTG 教程」就能找到。

三条路线怎么取舍,站内有一篇专门做的横向对比,看那张表比看文字快:三条 HID 路线横向对比


十、小结

梳理一下这篇的几个要点:

USB HID 的核心思路是让电脑冒充 HID 设备,通过系统输入通道注入触摸事件,iOS 无法区分真假,所以不需要越狱也不需要签名。

这也让它成了苹果群控里最省事的一条免越狱链路:不用买蓝牙板或 OTG 板,一根数据线就能跑通,脚本还能在多台设备上复用。

API 层面就记住三件事:会话要先开,屏幕尺寸要设对,返回值 null 或空字符串表示成功。

排查问题的顺序基本固定:先看物理连接,再看参数设置,最后才怀疑代码逻辑。


参考文档

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

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


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

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

想要真实跑起来?

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

访问 EasyClick 官网 →