码到成功
atxserver 技术博客

这时候,atxserver 的位置就出来了。
它更像一层“设备管理平台”,不是直接替你点按钮的执行器,而是把下面几件事情整理成一套秩序:
- 哪些设备在线
- 哪些设备正在被谁使用
- 设备侧暴露了哪些地址给自动化层
- provider 怎么把真机接进来
- 自动化脚本怎么拿到一台能用的机器
如果把 uiautomator2 看成“操作设备的手”,那 atxserver 更像“安排这只手该去摸哪台机器、怎么摸、摸完怎么还回去”的管理层。
先把 atxserver 的角色摆正
在 openatx 这套体系里,atxserver2 的定位很明确:它是移动设备管理平台。
它自己并不直接替你完成所有设备动作,而是把设备状态、占用关系、远程接入信息、provider 来源这些东西统一收进来,再通过 Web 界面和 REST API 暴露出去。
所以这套链路里,常见组件大概是这样分工的:
atxserver2:设备管理和占用调度层atxserver2-android-provider:把 Android 设备接入平台atx-agent:跑在设备侧的 HTTP 服务uiautomator2:Python 自动化客户端
这四个东西一旦拆开理解,很多之前看起来有点拧巴的概念就顺了。
为什么 atxserver 值得单独聊
如果你只是本机挂一台手机,写个脚本点点页面,确实不一定需要它。
但一旦场景开始往下面这些方向走,atxserver 的价值会非常明显:
- 设备变多了,不能再靠口头喊“这台先别动”
- 多个人同时跑脚本,需要设备占用机制
- 远程调试时,需要知道一台设备对应哪些接入地址
- 想把设备安装、冷却、自检这些动作交给 provider
- 希望平台层知道“设备在线但不可用”和“完全空闲可拿来跑任务”的区别
它解决的不是“单次点击怎么做”,而是“设备资源怎么管”。
一条比较顺的心智模型
把 atxserver 想成一个中枢,整个流程会比较好记:
设备接入进来以后,provider 会把设备能力和来源信息报给平台;平台把设备放进列表,记录是否在线、是否正在使用、是否处于清理状态;你的 Python 脚本再通过平台 API 拿到一台空闲机器,占用它,读取 source 信息,最后才进入真正的自动化阶段。
这条线里,平台层做的是编排,设备侧做的是执行。
atxserver 里最值得理解的几个字段
官方 API 文档里,设备列表和设备详情接口暴露了几个很关键的状态位:
present:设备是否在线using:设备是否已被占用colding:设备是否处于清理、自检或暂不可占用状态properties:品牌、型号、系统版本等静态信息source:平台给你返回的接入地址集合
这里最有价值的是 source。
对于 Android 设备,常见会看到这些字段:
urlatxAgentAddressremoteConnectAddresswhatsInputAddress
这些字段不是摆设,而是把后续链路全接起来的钥匙:
url:provider 的入口,适合做安装、冷却等设备管理动作atxAgentAddress:设备侧atx-agent的访问地址,偏自动化执行链remoteConnectAddress:可用于adb connectwhatsInputAddress:更多是远程输入场景需要,自动化脚本不一定每次都用
也就是说,atxserver 的真正价值之一,就是它不只告诉你“有一台设备”,还告诉你“这台设备该怎么接、从哪儿接、接上以后还能干什么”。
provider 为什么是这套系统里特别关键的一层
如果没有 provider,平台就只是一个空壳子。
atxserver2-android-provider 做的事情,本质上是:
- 发现连接到宿主机上的 Android 设备
- 把必要资源推送到手机上
- 把设备状态和 source 信息同步给平台
- 提供安装应用、冷却设备等面向平台的操作入口
所以 provider 非常像“平台和真机之间的翻译层”。
这也是为什么不少人第一次看这套体系时会有点迷糊:他们以为 atxserver 一启动就该看见设备,实际上真正把设备搬进平台视野里的,是 provider。
用 API 看 atxserver,思路会更干净
如果你把 Web 页面先放一边,只看 API,整个模型会非常清楚。
常见的几类接口大概是:
- 获取当前用户信息:
GET /api/v1/user - 获取设备列表:
GET /api/v1/devices - 获取单台设备:
GET /api/v1/devices/{udid} - 占用设备:
POST /api/v1/user/devices - 更新活跃时间:
GET /api/v1/user/devices/{udid}/active - 获取当前用户占用的设备详情:
GET /api/v1/user/devices/{udid} - 释放设备:
DELETE /api/v1/user/devices/{udid}
你会发现,它的设计思路其实很稳:
- 平台先判断设备能不能被拿
- 用户拿到设备以后,才会获得更完整的 source 信息
- 占用期间可以持续刷新活跃时间
- 脚本跑完以后,再把设备释放回池子
这就是很典型的“资源池”味道,而不是“我看见设备就直接上手抢”。
一个实用的 Python 例子:从平台拿一台空闲设备
这段代码的目标很朴素:
- 从平台拿一台 Android 空闲机
- 占用它
- 读取它的 source 信息
- 最后把设备还回去
from __future__ import annotations
import requests
SERVER_URL = "http://127.0.0.1:4000"
TOKEN = "replace-with-your-token"
def get_headers() -> dict[str, str]:
return {"Authorization": f"Bearer {TOKEN}"}
def pick_one_device() -> dict:
resp = requests.get(
f"{SERVER_URL}/api/v1/devices",
headers=get_headers(),
params={"platform": "android", "usable": "true"},
timeout=10,
)
resp.raise_for_status()
payload = resp.json()
devices = payload.get("devices", [])
if not devices:
raise RuntimeError("no usable android device found")
return devices[0]
def acquire_device(udid: str) -> None:
resp = requests.post(
f"{SERVER_URL}/api/v1/user/devices",
headers=get_headers(),
json={"udid": udid, "idleTimeout": 600},
timeout=10,
)
resp.raise_for_status()
def get_my_device_detail(udid: str) -> dict:
resp = requests.get(
f"{SERVER_URL}/api/v1/user/devices/{udid}",
headers=get_headers(),
timeout=10,
)
resp.raise_for_status()
return resp.json()["device"]
def release_device(udid: str) -> None:
resp = requests.delete(
f"{SERVER_URL}/api/v1/user/devices/{udid}",
headers=get_headers(),
timeout=10,
)
resp.raise_for_status()
if __name__ == "__main__":
device = pick_one_device()
udid = device["udid"]
acquire_device(udid)
detail = get_my_device_detail(udid)
print("picked device:", detail["properties"])
print("source:", detail["source"])
release_device(udid)
这段代码虽然不复杂,但已经把 atxserver 的主价值跑出来了:
- 不是直接盲连设备
- 先问平台有没有可用资源
- 真正拿到设备后,再读取 source 细节
- 用完及时释放
如果你有多台机器,这种写法会比硬编码 serial 舒服很多。
Python 再往前走一步:拿到 source 后接自动化
光拿到设备还不够,真正有意思的是后半段。
Android 设备详情里常见会返回 remoteConnectAddress,这个地址就是给 adb connect 用的。你可以先通过它把设备接进当前执行环境,再把 uiautomator2 接上去。
from __future__ import annotations
import subprocess
import uiautomator2 as u2
def connect_via_remote_adb(remote_addr: str):
subprocess.run(["adb", "connect", remote_addr], check=True)
return u2.connect(remote_addr)
def smoke_test(d):
print("device info:", d.info)
d.app_start("com.android.settings")
d(text="网络和互联网").wait(timeout=5)
d.press("back")
remote_addr = "10.0.0.1:20002"
d = connect_via_remote_adb(remote_addr)
smoke_test(d)
这时候你会明显感受到 atxserver 的好处:
自动化脚本不需要自己维护一份“今天哪台机器能跑、哪台机器被占了、哪台机器该连哪个地址”的表,平台已经把这些信息整理好了。
脚本只需要:
- 向平台要一台设备
- 读 source
- 接自动化客户端
链路干净得多。
provider 的 source.url 也很有价值
有些动作并不适合塞进自动化客户端里,比如安装应用、触发设备冷却、做 provider 级别的设备动作。这时 source.url 就很重要。
官方 API 文档里给过一个很实用的思路:通过 provider URL 做应用安装。
如果你要用 Python 包一层,大概可以写成这样:
from __future__ import annotations
import requests
def install_apk_via_provider(provider_url: str, udid: str, apk_url: str):
resp = requests.post(
f"{provider_url}/app/install",
params={"udid": udid},
data={"url": apk_url, "launch": "true"},
timeout=120,
)
resp.raise_for_status()
return resp.json()
result = install_apk_via_provider(
provider_url="http://10.0.1.1:3500",
udid="demo-device-udid",
apk_url="https://example.com/app.apk",
)
print(result)
这个设计挺聪明的一点在于:
- 平台负责告诉你 provider 在哪
- provider 负责把安装动作落到具体设备
- 自动化客户端不用什么事都揽过来
职责边界一下就清楚了。
一旦设备开始多人共用,几个细节就会变得很重要
真正把 atxserver 用起来以后,最容易出问题的反而不是“能不能连上”,而是资源管理的边角。
别跳过占用和释放
如果脚本直接拿 source 去连,而不先走占用接口,平台上的 using 状态就不准。久而久之,设备列表看起来全在线,真正拿来跑却互相撞。
活跃时间不是摆设
官方 API 里有更新 active 时间的接口,这类设计不是多余。跑长任务时,如果平台依赖空闲超时自动释放设备,你就得认真考虑“什么时候刷新活跃状态”。
present、using、colding 不能混着看
这三个状态一起看才有意义:
present=true只是在线using=false只是没人占- 还得
colding=false才算真的可用
很多脚本只判断在线,结果老是抢到正在清理中的设备,问题就出在这里。
平台层和执行层不要互相越位
atxserver 适合做资源池、权限、占用、provider 路由。
uiautomator2 适合做点击、等待、截图、输入、断言。
两层如果边界不清,代码很容易长成一坨:平台脚本里夹 UI 操作,自动化脚本里又偷偷写资源调度,后面会非常难收。
如果你要维护这套体系,最值得保留的是什么
哪怕你后面未必继续用完整的 openatx 组合,atxserver 这个设计思路本身还是很有价值。
它提醒了一件经常被低估的事:
当自动化规模上来以后,问题已经不再只是“脚本能不能点到控件”,而是“设备资源是不是被有序管理”。
这也是它最值得学的地方:
- 用平台层抽象设备资源
- 用 provider 把物理设备接入平台
- 用 source 信息把设备管理和自动化执行衔接起来
- 用占用、活跃、释放把多人协作管住