本文的所有代码示例都是基于 Go 语言的
引言
由于近期有对手机自动化的业务需求,也为了增强自己的 USB 开发能力,我学习了 Scrcpy (v1.25) 的源码,借用了 Scrcpy 服务器以实现手机屏幕的网络串流。
但在控制这一块,犯了难。
Scrcpy 的做法是,将捕获的鼠标和键盘事件通过 Socket 连接传送到 Scrcpy 服务器,由服务器利用 Android Shell 比较高的权限注入控制事件。
这种做法存在弊端:对于部分正在收紧 USB 调试 权限的设备来说,比如小米手机 你背叛了发烧友 ,需要开启更加细分的 USB 调试(安全设置) 选项,在这个过程中需要给手机联网、登录小米账号、反复输入小米账号密码三次。对于不联网的自动化测试机来说,这是不能接受的。
通过查阅,我注意到 Scrcpy 提供了 OTG MODE
选项。这个模式不需要启用 USB 调试 ,就算是在权限收紧后的小米手机上也可用。在激活 OTG MODE
之后,手机上直接显示了一个实体键盘的图标。
这激发了我浓厚的兴趣:这是怎么做到的?一番查阅,我看到了这个博客,文章介绍了一种 HID over AOAv2 的方案,本质就是利用谷歌提供的 Android 开放配件协议 (AOA) 实现 USB 连接设备的主从机转换,从而使电脑 (主机) 能够为 Android (从机) 注册 HID 设备并发送 HID 事件。
一番折腾和踩坑过后,我搞定并使用 Go 重新实现了 OTG MODE
,现在可以在这里写一些内容记录一下了。
这里要特别感谢
前置工作
基础知识
要再现本文的技术,你至少需要:
- Go 语言基础
- CGO 编译环境
- Linux 操作基础
- 能根据协议标准进行报文的组织和发送
要深入理解本文的内容,你还需要理解:
- USB 开发基础
- HID 协议
- Android 开放配件协议
环境配置
要运行本文的代码,你需要使用原生 Linux 系统 (虚拟机是不可行的) ,最好使用某个发行版 (例如 Ubuntu) 以防止出现内核不完整的问题。
在 Linux 系统中,你需要安装如下环境:
- Go v1.20+
- gousb
- libusb-1.0.0-dev
- gcc 编译套件
环境配置教程不在本文范围内,这里提供一个十全大补丸以供参考:
sudo apt-get install build-essential
sudo apt-get install pkg-config
sudo apt-get install libusb-1.0.0-dev
wget https://go.dev/dl/go1.20.4.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.4.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go get -u github.com/google/gousb
正文
第一阶段: 获取 USB 设备
首先我们要获取全部的已连接 usb 设备,再筛选出其中的 Android 手机。
这一步没什么好的办法,只能通过 USB-IF 组织提供的厂商 Vendor ID 列表来匹配查询,这里举例筛选小米手机,则 Vendor ID 是 0x2717
:
func GetDevice() *gousb.Device {
devices, err := gousb.NewContext().OpenDevices(func(desc *gousb.DeviceDesc) bool {
if desc.Vendor == 0x2717 {
return true
}
return false
})
if err != nil || len(devices) == 0 {
panic(err)
}
return devices[0]
}
当然,据说也可以通过 Android Device Serial 来匹配 USB 设备,但我没有深究,业务上同时只会连接一台手机。
获取到 USB 设备后我们可以打印它的厂商验证一下:
func main() {
dev := GetDevice()
defer dev.Close()
manu, _ := dev.Manufacturer()
fmt.Println(manu) // Xiaomi
}
可以发现打印出的厂商名字正是 Xiaomi ,这就是我们要的那台设备了。
第二阶段: 验证 Protocol 版本
Android 开放配件协议 (AOA) 有两个版本,一个是 v1 ,一个是 v2 。
只有 v2 版本的协议才能支持我们要实现的 HID 设备注册,所以验证设备的 Protocol 是很重要的。
而验证 Protocol ,其实就是根据 AOA 协议发送命令并接受返回数据,校验一下,代码如下:
func getProtocol(dev *gousb.Device) (uint16, error) {
if dev == nil {
return 0, errors.New("ErrorNoAccessoryDevice")
}
var data = make([]byte, 2)
RTypeIN := gousb.ControlIn | gousb.ControlVendor
GetProtocol := 51
n, err := dev.Control(RTypeIN, GetProtocol, 0, 0, data)
if err != nil {
return 0, err
}
if n != 2 {
return 0, errors.New("ErrorFailedToGetProtocol")
}
return (uint16(data[1])<<8 | uint16(data[0])), nil
}
我们将获取到的小米手机设备传入并检验一下,函数返回值为 2 ,说明支持的正是 AOAv2 协议。
这里要说明一下, AOAv2 协议是向下兼容 AOAv1 协议的,如果你希望实现其它的功能,请自己阅读上面提供的官方文档。本文只讲述如何实现 HID 设备。
暂无评论内容