farm-sdk 2026.11.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- farm_sdk-2026.11.8/MANIFEST.in +2 -0
- farm_sdk-2026.11.8/PKG-INFO +174 -0
- farm_sdk-2026.11.8/README.md +152 -0
- farm_sdk-2026.11.8/examples/device_query.py +26 -0
- farm_sdk-2026.11.8/examples/match_target.py +27 -0
- farm_sdk-2026.11.8/examples/offline_match.py +20 -0
- farm_sdk-2026.11.8/examples/template_assets.py +27 -0
- farm_sdk-2026.11.8/examples/watch_templates.py +27 -0
- farm_sdk-2026.11.8/pyproject.toml +30 -0
- farm_sdk-2026.11.8/setup.cfg +4 -0
- farm_sdk-2026.11.8/src/farm/__init__.py +16 -0
- farm_sdk-2026.11.8/src/farm/client.py +126 -0
- farm_sdk-2026.11.8/src/farm/contracts.py +33 -0
- farm_sdk-2026.11.8/src/farm/decoder.py +95 -0
- farm_sdk-2026.11.8/src/farm/dependencies.py +11 -0
- farm_sdk-2026.11.8/src/farm/device.py +399 -0
- farm_sdk-2026.11.8/src/farm/errors.py +57 -0
- farm_sdk-2026.11.8/src/farm/matcher.py +61 -0
- farm_sdk-2026.11.8/src/farm/models.py +106 -0
- farm_sdk-2026.11.8/src/farm/net.py +67 -0
- farm_sdk-2026.11.8/src/farm/ocr.py +212 -0
- farm_sdk-2026.11.8/src/farm/ocr_consensus.py +157 -0
- farm_sdk-2026.11.8/src/farm/ocr_sampling.py +115 -0
- farm_sdk-2026.11.8/src/farm/ocr_workers.py +39 -0
- farm_sdk-2026.11.8/src/farm/templates.py +143 -0
- farm_sdk-2026.11.8/src/farm/vision.py +349 -0
- farm_sdk-2026.11.8/src/farm/watch.py +115 -0
- farm_sdk-2026.11.8/src/farm_sdk.egg-info/PKG-INFO +174 -0
- farm_sdk-2026.11.8/src/farm_sdk.egg-info/SOURCES.txt +38 -0
- farm_sdk-2026.11.8/src/farm_sdk.egg-info/dependency_links.txt +1 -0
- farm_sdk-2026.11.8/src/farm_sdk.egg-info/requires.txt +13 -0
- farm_sdk-2026.11.8/src/farm_sdk.egg-info/top_level.txt +1 -0
- farm_sdk-2026.11.8/tests/test_assets.py +78 -0
- farm_sdk-2026.11.8/tests/test_basic.py +116 -0
- farm_sdk-2026.11.8/tests/test_net.py +134 -0
- farm_sdk-2026.11.8/tests/test_ocr.py +105 -0
- farm_sdk-2026.11.8/tests/test_ocr_capacity.py +151 -0
- farm_sdk-2026.11.8/tests/test_ocr_consensus.py +382 -0
- farm_sdk-2026.11.8/tests/test_vision.py +409 -0
- farm_sdk-2026.11.8/tests/test_vision_performance.py +199 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: farm-sdk
|
|
3
|
+
Version: 2026.11.8
|
|
4
|
+
Summary: Python client for Farm device automation
|
|
5
|
+
Project-URL: Homepage, https://github.com/liuzhuogood/Farm
|
|
6
|
+
Project-URL: Documentation, https://github.com/liuzhuogood/Farm/blob/main/packages/python-sdk/README.md
|
|
7
|
+
Project-URL: Repository, https://github.com/liuzhuogood/Farm
|
|
8
|
+
Project-URL: Issues, https://github.com/liuzhuogood/Farm/issues
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: httpx<1,>=0.28
|
|
12
|
+
Requires-Dist: websocket-client<2,>=1.8
|
|
13
|
+
Requires-Dist: av<20,>=14
|
|
14
|
+
Requires-Dist: opencv-python-headless<5,>=4.10
|
|
15
|
+
Requires-Dist: numpy<3,>=1.26
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: websockets<18,>=14; extra == "test"
|
|
18
|
+
Provides-Extra: vision
|
|
19
|
+
Requires-Dist: av<20,>=14; extra == "vision"
|
|
20
|
+
Requires-Dist: opencv-python-headless<5,>=4.10; extra == "vision"
|
|
21
|
+
Requires-Dist: numpy<3,>=1.26; extra == "vision"
|
|
22
|
+
|
|
23
|
+
# Farm Python SDK
|
|
24
|
+
|
|
25
|
+
`farm-sdk`(版本与根目录 VERSION 一致),导入名 `farm`,支持 Python 3.10+。HTTP 使用 HTTPX,Vision 使用 websocket-client;视频在调用方进程通过 PyAV 解码、OpenCV 匹配,无需运行独立匹配工程。远程操作要求主服务和设备可达。
|
|
26
|
+
|
|
27
|
+
## 安装
|
|
28
|
+
|
|
29
|
+
从 PyPI 安装:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python -m pip install --upgrade farm-sdk
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
也可从仓库根目录安装本地源码:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m pip install ./packages/python-sdk
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
核心依赖包含 HTTPX、websocket-client、PyAV、OpenCV (opencv-python-headless) 与 NumPy,开箱即用支持模板资产、设备列表、ADB、页面尺寸、视频取帧与模板匹配。基础 import 保持懒加载,仅在实际取帧与匹配时加载 PyAV、OpenCV 与 NumPy。仍保留 `[vision]` 兼容标识。
|
|
42
|
+
|
|
43
|
+
构建并安装 wheel:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m pip install build
|
|
47
|
+
python -m build packages/python-sdk
|
|
48
|
+
python -m pip install --force-reinstall 'packages/python-sdk/dist/farm_sdk-2026.11.8-py3-none-any.whl[vision]'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
安装后脚本可以放在其他目录运行。外部脚本和全局匹配服务使用其 Python 环境中安装的 SDK。系统配置的 Python 卡片通过 pip 安装同版本 SDK 及识图依赖;服务仍需手动启动。
|
|
52
|
+
|
|
53
|
+
## 匹配与操作
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from farm import Client
|
|
57
|
+
|
|
58
|
+
with Client('http://HOST:PORT') as client:
|
|
59
|
+
templates = client.templates.list(serial='DEVICE_SERIAL')
|
|
60
|
+
with client.device('DEVICE_SERIAL') as device:
|
|
61
|
+
frame = device.frame(timeout=10)
|
|
62
|
+
with client.startVideoStream('DEVICE_SERIAL', timeout=15) as stream:
|
|
63
|
+
first_frame = stream.first_frame
|
|
64
|
+
latest_frame = stream.current_frame(timeout=10)
|
|
65
|
+
matches = device.match('CONFIRMED_TEMPLATE_ID', timeout=10)
|
|
66
|
+
print(matches)
|
|
67
|
+
# 只有显式动作才操作设备;先确认目标模板 ID。
|
|
68
|
+
target = device.wait_for('CONFIRMED_TEMPLATE_ID', timeout=10)
|
|
69
|
+
result = device.tap_match(target, max_age_ms=600)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`frame()` 等待调用后到达的新画面。重放的缓存关键帧只初始化解码,不满足取帧请求;静止画面可能超时。`match()` 正常未命中返回 `[]`,`wait_for()` 到期抛 `MatchTimeout`。仅支持模板主图片与 `TM_CCOEFF_NORMED`;未知算法、禁用模板、其他设备模板明确报错。多参考图不会自动聚合。
|
|
73
|
+
|
|
74
|
+
`client.startVideoStream(serial)`(亦可写作 `start_video_stream(serial)`)返回上下文管理器,进入时连接视频流并验证首帧,提供 `first_frame` 与 `current_frame(timeout=...)`;退出时关闭本次连接。`sync_vision_frame=True` 会把首帧上传至设备总状态帧缓存。
|
|
75
|
+
|
|
76
|
+
OCR 可直接用于脚本判断当前画面或等待文字出现:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
result = device.wait_for_text("登录", timeout=15, interval=0.5)
|
|
80
|
+
print(result.matched, result.text)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`wait_for_text()` 每隔 `interval` 秒识别一次,找到包含文字的结果后返回 `OCRResult`;超过 `timeout` 秒抛出 `MatchTimeout`。`device.ocr(keywords="登录", timeout=10)` 只识别一次当前画面。两种方法都可通过 `bbox`、`engine` 等参数限定识别范围或引擎。
|
|
84
|
+
|
|
85
|
+
`Frame.image` 为只读 uint8 BGR ndarray。`Match` 带模板 ID、名称、置信度、归一化框及 `center`、来源设备/会话/连接/重置代数、帧 ID 和本地接收时间。`tap_match()` 拒绝旧连接、旧配置与过期结果;仍不能消除画面变化与点击之间的竞态。
|
|
86
|
+
|
|
87
|
+
| API | 约定 |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `client.devices.list()` | 精确解析 `/api/devices` 数组 |
|
|
90
|
+
| `client.templates.list/get/create/update/delete` | 创建和更新图片使用 bytes,SDK 编码为 `image_base64` |
|
|
91
|
+
| `client.templates.image/add_image` | 主 PNG 下载、multipart `image` 参考图上传 |
|
|
92
|
+
| `client.templates.import_zip/export_zip` | ZIP bytes;导入使用 multipart `file` + `serial` |
|
|
93
|
+
| `device.adb(args, timeout=30)` | 明确参数数组;不含 adb 或宿主/设备选择参数;检查真实退出状态 |
|
|
94
|
+
| `device.page_info(timeout=10)` | 返回正整数 width/height |
|
|
95
|
+
| `device.ocr(keywords=None, timeout=10)` | 识别当前画面;`keywords` 可筛选文字框,返回 `OCRResult` |
|
|
96
|
+
| `device.wait_for_text(text, timeout=10, interval=0.5)` | 轮询 OCR 直到找到文字并返回结果;总期限到达时抛 `MatchTimeout` |
|
|
97
|
+
| `device.tap(point)` / `swipe(start, end, duration_ms=300)` | 归一化坐标 0..1;滑动时长为毫秒 |
|
|
98
|
+
| `device.key(keycode, meta_state=0)` / `text(text)` | uint32 按键;文本最多 300 UTF-8 字节,具体字符输入能力需设备验证 |
|
|
99
|
+
| `Matcher.match_image(image, template, ...)` | 内存 BGR 图像离线匹配,无网络,返回归一化框与分数 |
|
|
100
|
+
| `device.watch_templates(publish=True, max_fps=5, timeout=10)` | 使用上下文;按指定频率处理当前画面,每批有总期限;每设备一个 watch |
|
|
101
|
+
|
|
102
|
+
`watch_templates(publish=True)` 将结果回传网页播放器,空结果与关闭都会清框;异常中断时服务端 TTL 兜住残留显示。每 10 秒刷新模板;刷新失败中断 watch,不以旧缓存继续。完整编码包持续解码,只保存最新画面,不因慢匹配堆积历史帧。BGR 图像在取帧时按需转换,同一保留帧最多转换一次,颜色转换在接收线程与其状态锁之外执行。每个 Device 一个收包线程,动作结果按 request_id 分发。
|
|
103
|
+
|
|
104
|
+
静止画面没有新视频帧时,watch 复用最后一张有效画面的匹配结果并刷新绿框;图像的 `frame_id`、`received_at` 不变,`tap_match` 仍拒绝超过 600ms 的图像。连续新帧的完整像素相同且模板不变时,也可复用匹配位置,结果来源绑定实际新帧;像素、模板配置、旋转或会话变化都会重新计算。只缓存一张已匹配图像及一组结果,不缩小图像或近似判断像素。旋转、编码器重置、断流和关闭使画面缓存失效。`frame`、`match`、`wait_for` 继续等待新帧。
|
|
105
|
+
|
|
106
|
+
Vision 订阅携带 `current_frame=true`,服务器回放有大小上限的完整关键帧与增量帧链,并在末包标记 `snapshot_complete=true`,用于静止时启动 watch。未标记完整性的回放只用于初始化解码器;缓存超限则等待新关键帧。回放匹配结果标记 `replayed=True`,不能用于点击。连接每 5 秒发送心跳,5 秒内没有响应会结束检测;停止发布后,绿框按 TTL 清除。
|
|
107
|
+
|
|
108
|
+
## 异常与资源
|
|
109
|
+
|
|
110
|
+
Client、Device、watch 使用 `with` 或显式 `close()`,可重复关闭。断流终止当前连接,由调用方重新创建设备对象;关闭会唤醒等待者并回收线程。标准 logging 不修改 root logger、不记录凭据、图片或输入文本。
|
|
111
|
+
|
|
112
|
+
超时单位为秒,范围 `(0, 600]`。视觉方法有总期限,覆盖模板请求、连接、取帧和匹配。ADB 保留 v0.1 的命令期限及传输余量。`ProtocolError` 表示确切字段/格式错误,不猜替代字段;`HTTPError` 保留 status/code/detail,`ConnectionError` 表示连接问题。`ActionError.result` 保留 accepted/executed/error_code;`ActionTimeout` 表示执行结果未知。动作和 ADB 不自动重试。
|
|
113
|
+
|
|
114
|
+
默认验证 TLS,可通过 `Client(verify='CA_PATH', headers=...)` 配置 CA 和明确请求头;不读取未知凭据。每次发布和点击都校验会话与帧来源,点击 expires_ms 在发送锁内按剩余期限计算。
|
|
115
|
+
|
|
116
|
+
需要经代理访问服务时,一次性配置即可覆盖全部通道:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
Client('https://HOST:PORT', proxy='http://127.0.0.1:7890')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
也可用环境变量 `FARM_PROXY` 指定(取值还支持 `env`,表示交给运行环境决定)。该设置统一作用于 HTTP 请求、Vision WebSocket 与 `device.ocr` 的图片上传;不配置时代理一律不生效,行为与旧版一致。典型场景:macOS 按代码签名授予本地网络访问权,adhoc 签名的 Python 直连内网报 `Errno 65`,把内网流量交给本机代理进程即可恢复。
|
|
123
|
+
|
|
124
|
+
## 示例与验证
|
|
125
|
+
|
|
126
|
+
- [device_query.py](examples/device_query.py):读取型号。
|
|
127
|
+
- [template_assets.py](examples/template_assets.py):列出模板或上传 PNG 创建模板。
|
|
128
|
+
- [match_target.py](examples/match_target.py):单次匹配;显式 `--tap` 才等待并点击。
|
|
129
|
+
- [watch_templates.py](examples/watch_templates.py):持续发布,Ctrl+C 退出清框。
|
|
130
|
+
- [offline_match.py](examples/offline_match.py):两个本地图像离线匹配。
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
python packages/python-sdk/examples/match_target.py DEVICE_SERIAL TEMPLATE_ID --base-url http://HOST:PORT
|
|
134
|
+
python -m pip install './packages/python-sdk[vision,test]'
|
|
135
|
+
python -m unittest discover -s packages/python-sdk/tests -v
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
本版在 macOS 验证了真实 H.264 受控服务、任务执行、包安装与小米手机只读取帧/匹配。真机点击等待用户指定目标;浏览器绿框和 Windows/Linux 原生链路仍待验收。macOS 的当前 PyAV/OpenCV wheel 同时加载时出现 libavdevice Objective-C 类重复警告;测试未出现故障,跨平台稳定性仍须原生验证。
|
|
139
|
+
|
|
140
|
+
## 版本与发布
|
|
141
|
+
|
|
142
|
+
SDK 版本与仓库根目录 `VERSION` 一致,Python 包版本去掉 `v` 前缀。修改 `VERSION` 后运行 `node scripts/sync-version.mjs`,同时更新主程序与 SDK 版本。
|
|
143
|
+
|
|
144
|
+
GitHub Actions 的 `Publish Python SDK` 工作流在主分支的版本、SDK 或发布配置变更时运行,也支持 `v*` 标签和手动触发;构建检查及安装测试通过后发布未发布的版本,已存在的完整版本会跳过。仓库需在 Settings → Secrets and variables → Actions 配置 `PYPI_API_TOKEN`,值为有此项目发布权限的 PyPI API Token。只修改 SDK 而不更新版本号不会覆盖 PyPI 上已有的版本。
|
|
145
|
+
|
|
146
|
+
## OCR 引擎
|
|
147
|
+
|
|
148
|
+
`device.ocr()` 自动按 Indra OCR、macOCR、PaddleOCR 顺序尝试;服务识别成功但没有文字时不会降级。使用前在 Farm 系统配置中启动本地 Indra OCR。显式指定引擎时只调用该服务。
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
result = device.ocr(engine="indraocr", mark=True)
|
|
152
|
+
# 远程脚本可指定可访问的服务地址:
|
|
153
|
+
result = device.ocr(indraocr_url="http://your-host:8032/upload")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`INDRAOCR_URL` 环境变量也可设置 Indra 地址,默认 `http://127.0.0.1:8032/upload`。已有 `MACOCR_URL`、`MACOCR_AUTH`、`PADDLEOCR_URL` 及其关键字参数继续有效。
|
|
157
|
+
|
|
158
|
+
`engine="parallel"` 同轮并行调用上述三个配置引擎,最多三个在途请求;每个引擎单独最多等待 5 秒,并受调用剩余 `timeout` 预算限制。部分引擎失败、忙或超时时,返回可用引擎的融合结果,并在 `fallback_reason` 中列出原因;全部失败时抛出 `RuntimeError`。成功识别为空仍属于有效结果。`auto` 保持原有串行优先级,显式引擎只调用指定服务。
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
# 单帧、多引擎投票
|
|
162
|
+
result = device.ocr(engine="parallel", timeout=10)
|
|
163
|
+
# 500ms 内连续获取 Vision 新帧,再综合各轮结果
|
|
164
|
+
result = device.ocr(engine="parallel", stabilization_ms=500, timeout=10)
|
|
165
|
+
print(result.text, result.fallback_reason)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`stabilization_ms` 必须是 `0..5000` 的整数(不接受布尔值或小数),默认 `0` 保持原有单帧行为;正值适用于所有引擎模式,要求 Vision 有持续的新帧,不复用回放或旧帧。采样逐轮串行,轮次启动间隔至少 50ms,最多 101 轮。窗口结束后不再启动识别,已经启动的一轮可在 `timeout` 总预算内完成;取帧、各轮及 auto 降级共享剩余预算。窗口内没有有效样本或识别超时会抛错,不足多轮时可以返回单轮有效结果。
|
|
169
|
+
|
|
170
|
+
融合按位置 IoU ≥ 0.3 分组去重,每轮每引擎最多一票,跨轮每轮最多一票;文字按票数决定,同票采用更新观察,坐标仅取获胜文字框的中位数,不跨引擎比较置信度。时序结果的位置必须出现在最新轮并至少覆盖半数轮,输出文字也必须在最新轮出现,避免旧页面文字残留。关键词筛选在时序融合之后执行。新采样路径按实际像素裁剪区域映射回原图,小数 ROI 的取整和边缘裁剪不会沿用原始 bbox 造成偏差。
|
|
171
|
+
|
|
172
|
+
并行 OCR 与新采样路径共用进程内的 6 worker 执行池,最多接收 6 个未完成任务,满载立即报 `OCR workers busy`,不会无界排队。SDK 使用同步 HTTP 上传;等待预算到期后立即返回或抛错,丢弃迟到结果。已进入底层 HTTP 的请求无法强制中断,会由其 socket 超时结束;这些残留任务仍持有容量,直到实际结束才释放,也不会追加采样或发布标记。未开始的任务执行前会检查期限,过期不会发起上传。
|
|
173
|
+
|
|
174
|
+
`OCRResult.samples` 是有效采样轮数,`engines_used` 列出成功返回结果的引擎(空识别也计入,失败引擎不计入)。新增字段位于构造参数末尾,默认分别为 `1`、`[]`,已有位置参数构造方式不变。
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Farm Python SDK
|
|
2
|
+
|
|
3
|
+
`farm-sdk`(版本与根目录 VERSION 一致),导入名 `farm`,支持 Python 3.10+。HTTP 使用 HTTPX,Vision 使用 websocket-client;视频在调用方进程通过 PyAV 解码、OpenCV 匹配,无需运行独立匹配工程。远程操作要求主服务和设备可达。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
从 PyPI 安装:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
python -m pip install --upgrade farm-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
也可从仓库根目录安装本地源码:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python -m pip install ./packages/python-sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
核心依赖包含 HTTPX、websocket-client、PyAV、OpenCV (opencv-python-headless) 与 NumPy,开箱即用支持模板资产、设备列表、ADB、页面尺寸、视频取帧与模板匹配。基础 import 保持懒加载,仅在实际取帧与匹配时加载 PyAV、OpenCV 与 NumPy。仍保留 `[vision]` 兼容标识。
|
|
20
|
+
|
|
21
|
+
构建并安装 wheel:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python -m pip install build
|
|
25
|
+
python -m build packages/python-sdk
|
|
26
|
+
python -m pip install --force-reinstall 'packages/python-sdk/dist/farm_sdk-2026.11.8-py3-none-any.whl[vision]'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
安装后脚本可以放在其他目录运行。外部脚本和全局匹配服务使用其 Python 环境中安装的 SDK。系统配置的 Python 卡片通过 pip 安装同版本 SDK 及识图依赖;服务仍需手动启动。
|
|
30
|
+
|
|
31
|
+
## 匹配与操作
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from farm import Client
|
|
35
|
+
|
|
36
|
+
with Client('http://HOST:PORT') as client:
|
|
37
|
+
templates = client.templates.list(serial='DEVICE_SERIAL')
|
|
38
|
+
with client.device('DEVICE_SERIAL') as device:
|
|
39
|
+
frame = device.frame(timeout=10)
|
|
40
|
+
with client.startVideoStream('DEVICE_SERIAL', timeout=15) as stream:
|
|
41
|
+
first_frame = stream.first_frame
|
|
42
|
+
latest_frame = stream.current_frame(timeout=10)
|
|
43
|
+
matches = device.match('CONFIRMED_TEMPLATE_ID', timeout=10)
|
|
44
|
+
print(matches)
|
|
45
|
+
# 只有显式动作才操作设备;先确认目标模板 ID。
|
|
46
|
+
target = device.wait_for('CONFIRMED_TEMPLATE_ID', timeout=10)
|
|
47
|
+
result = device.tap_match(target, max_age_ms=600)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`frame()` 等待调用后到达的新画面。重放的缓存关键帧只初始化解码,不满足取帧请求;静止画面可能超时。`match()` 正常未命中返回 `[]`,`wait_for()` 到期抛 `MatchTimeout`。仅支持模板主图片与 `TM_CCOEFF_NORMED`;未知算法、禁用模板、其他设备模板明确报错。多参考图不会自动聚合。
|
|
51
|
+
|
|
52
|
+
`client.startVideoStream(serial)`(亦可写作 `start_video_stream(serial)`)返回上下文管理器,进入时连接视频流并验证首帧,提供 `first_frame` 与 `current_frame(timeout=...)`;退出时关闭本次连接。`sync_vision_frame=True` 会把首帧上传至设备总状态帧缓存。
|
|
53
|
+
|
|
54
|
+
OCR 可直接用于脚本判断当前画面或等待文字出现:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
result = device.wait_for_text("登录", timeout=15, interval=0.5)
|
|
58
|
+
print(result.matched, result.text)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`wait_for_text()` 每隔 `interval` 秒识别一次,找到包含文字的结果后返回 `OCRResult`;超过 `timeout` 秒抛出 `MatchTimeout`。`device.ocr(keywords="登录", timeout=10)` 只识别一次当前画面。两种方法都可通过 `bbox`、`engine` 等参数限定识别范围或引擎。
|
|
62
|
+
|
|
63
|
+
`Frame.image` 为只读 uint8 BGR ndarray。`Match` 带模板 ID、名称、置信度、归一化框及 `center`、来源设备/会话/连接/重置代数、帧 ID 和本地接收时间。`tap_match()` 拒绝旧连接、旧配置与过期结果;仍不能消除画面变化与点击之间的竞态。
|
|
64
|
+
|
|
65
|
+
| API | 约定 |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `client.devices.list()` | 精确解析 `/api/devices` 数组 |
|
|
68
|
+
| `client.templates.list/get/create/update/delete` | 创建和更新图片使用 bytes,SDK 编码为 `image_base64` |
|
|
69
|
+
| `client.templates.image/add_image` | 主 PNG 下载、multipart `image` 参考图上传 |
|
|
70
|
+
| `client.templates.import_zip/export_zip` | ZIP bytes;导入使用 multipart `file` + `serial` |
|
|
71
|
+
| `device.adb(args, timeout=30)` | 明确参数数组;不含 adb 或宿主/设备选择参数;检查真实退出状态 |
|
|
72
|
+
| `device.page_info(timeout=10)` | 返回正整数 width/height |
|
|
73
|
+
| `device.ocr(keywords=None, timeout=10)` | 识别当前画面;`keywords` 可筛选文字框,返回 `OCRResult` |
|
|
74
|
+
| `device.wait_for_text(text, timeout=10, interval=0.5)` | 轮询 OCR 直到找到文字并返回结果;总期限到达时抛 `MatchTimeout` |
|
|
75
|
+
| `device.tap(point)` / `swipe(start, end, duration_ms=300)` | 归一化坐标 0..1;滑动时长为毫秒 |
|
|
76
|
+
| `device.key(keycode, meta_state=0)` / `text(text)` | uint32 按键;文本最多 300 UTF-8 字节,具体字符输入能力需设备验证 |
|
|
77
|
+
| `Matcher.match_image(image, template, ...)` | 内存 BGR 图像离线匹配,无网络,返回归一化框与分数 |
|
|
78
|
+
| `device.watch_templates(publish=True, max_fps=5, timeout=10)` | 使用上下文;按指定频率处理当前画面,每批有总期限;每设备一个 watch |
|
|
79
|
+
|
|
80
|
+
`watch_templates(publish=True)` 将结果回传网页播放器,空结果与关闭都会清框;异常中断时服务端 TTL 兜住残留显示。每 10 秒刷新模板;刷新失败中断 watch,不以旧缓存继续。完整编码包持续解码,只保存最新画面,不因慢匹配堆积历史帧。BGR 图像在取帧时按需转换,同一保留帧最多转换一次,颜色转换在接收线程与其状态锁之外执行。每个 Device 一个收包线程,动作结果按 request_id 分发。
|
|
81
|
+
|
|
82
|
+
静止画面没有新视频帧时,watch 复用最后一张有效画面的匹配结果并刷新绿框;图像的 `frame_id`、`received_at` 不变,`tap_match` 仍拒绝超过 600ms 的图像。连续新帧的完整像素相同且模板不变时,也可复用匹配位置,结果来源绑定实际新帧;像素、模板配置、旋转或会话变化都会重新计算。只缓存一张已匹配图像及一组结果,不缩小图像或近似判断像素。旋转、编码器重置、断流和关闭使画面缓存失效。`frame`、`match`、`wait_for` 继续等待新帧。
|
|
83
|
+
|
|
84
|
+
Vision 订阅携带 `current_frame=true`,服务器回放有大小上限的完整关键帧与增量帧链,并在末包标记 `snapshot_complete=true`,用于静止时启动 watch。未标记完整性的回放只用于初始化解码器;缓存超限则等待新关键帧。回放匹配结果标记 `replayed=True`,不能用于点击。连接每 5 秒发送心跳,5 秒内没有响应会结束检测;停止发布后,绿框按 TTL 清除。
|
|
85
|
+
|
|
86
|
+
## 异常与资源
|
|
87
|
+
|
|
88
|
+
Client、Device、watch 使用 `with` 或显式 `close()`,可重复关闭。断流终止当前连接,由调用方重新创建设备对象;关闭会唤醒等待者并回收线程。标准 logging 不修改 root logger、不记录凭据、图片或输入文本。
|
|
89
|
+
|
|
90
|
+
超时单位为秒,范围 `(0, 600]`。视觉方法有总期限,覆盖模板请求、连接、取帧和匹配。ADB 保留 v0.1 的命令期限及传输余量。`ProtocolError` 表示确切字段/格式错误,不猜替代字段;`HTTPError` 保留 status/code/detail,`ConnectionError` 表示连接问题。`ActionError.result` 保留 accepted/executed/error_code;`ActionTimeout` 表示执行结果未知。动作和 ADB 不自动重试。
|
|
91
|
+
|
|
92
|
+
默认验证 TLS,可通过 `Client(verify='CA_PATH', headers=...)` 配置 CA 和明确请求头;不读取未知凭据。每次发布和点击都校验会话与帧来源,点击 expires_ms 在发送锁内按剩余期限计算。
|
|
93
|
+
|
|
94
|
+
需要经代理访问服务时,一次性配置即可覆盖全部通道:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
Client('https://HOST:PORT', proxy='http://127.0.0.1:7890')
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
也可用环境变量 `FARM_PROXY` 指定(取值还支持 `env`,表示交给运行环境决定)。该设置统一作用于 HTTP 请求、Vision WebSocket 与 `device.ocr` 的图片上传;不配置时代理一律不生效,行为与旧版一致。典型场景:macOS 按代码签名授予本地网络访问权,adhoc 签名的 Python 直连内网报 `Errno 65`,把内网流量交给本机代理进程即可恢复。
|
|
101
|
+
|
|
102
|
+
## 示例与验证
|
|
103
|
+
|
|
104
|
+
- [device_query.py](examples/device_query.py):读取型号。
|
|
105
|
+
- [template_assets.py](examples/template_assets.py):列出模板或上传 PNG 创建模板。
|
|
106
|
+
- [match_target.py](examples/match_target.py):单次匹配;显式 `--tap` 才等待并点击。
|
|
107
|
+
- [watch_templates.py](examples/watch_templates.py):持续发布,Ctrl+C 退出清框。
|
|
108
|
+
- [offline_match.py](examples/offline_match.py):两个本地图像离线匹配。
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python packages/python-sdk/examples/match_target.py DEVICE_SERIAL TEMPLATE_ID --base-url http://HOST:PORT
|
|
112
|
+
python -m pip install './packages/python-sdk[vision,test]'
|
|
113
|
+
python -m unittest discover -s packages/python-sdk/tests -v
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
本版在 macOS 验证了真实 H.264 受控服务、任务执行、包安装与小米手机只读取帧/匹配。真机点击等待用户指定目标;浏览器绿框和 Windows/Linux 原生链路仍待验收。macOS 的当前 PyAV/OpenCV wheel 同时加载时出现 libavdevice Objective-C 类重复警告;测试未出现故障,跨平台稳定性仍须原生验证。
|
|
117
|
+
|
|
118
|
+
## 版本与发布
|
|
119
|
+
|
|
120
|
+
SDK 版本与仓库根目录 `VERSION` 一致,Python 包版本去掉 `v` 前缀。修改 `VERSION` 后运行 `node scripts/sync-version.mjs`,同时更新主程序与 SDK 版本。
|
|
121
|
+
|
|
122
|
+
GitHub Actions 的 `Publish Python SDK` 工作流在主分支的版本、SDK 或发布配置变更时运行,也支持 `v*` 标签和手动触发;构建检查及安装测试通过后发布未发布的版本,已存在的完整版本会跳过。仓库需在 Settings → Secrets and variables → Actions 配置 `PYPI_API_TOKEN`,值为有此项目发布权限的 PyPI API Token。只修改 SDK 而不更新版本号不会覆盖 PyPI 上已有的版本。
|
|
123
|
+
|
|
124
|
+
## OCR 引擎
|
|
125
|
+
|
|
126
|
+
`device.ocr()` 自动按 Indra OCR、macOCR、PaddleOCR 顺序尝试;服务识别成功但没有文字时不会降级。使用前在 Farm 系统配置中启动本地 Indra OCR。显式指定引擎时只调用该服务。
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
result = device.ocr(engine="indraocr", mark=True)
|
|
130
|
+
# 远程脚本可指定可访问的服务地址:
|
|
131
|
+
result = device.ocr(indraocr_url="http://your-host:8032/upload")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`INDRAOCR_URL` 环境变量也可设置 Indra 地址,默认 `http://127.0.0.1:8032/upload`。已有 `MACOCR_URL`、`MACOCR_AUTH`、`PADDLEOCR_URL` 及其关键字参数继续有效。
|
|
135
|
+
|
|
136
|
+
`engine="parallel"` 同轮并行调用上述三个配置引擎,最多三个在途请求;每个引擎单独最多等待 5 秒,并受调用剩余 `timeout` 预算限制。部分引擎失败、忙或超时时,返回可用引擎的融合结果,并在 `fallback_reason` 中列出原因;全部失败时抛出 `RuntimeError`。成功识别为空仍属于有效结果。`auto` 保持原有串行优先级,显式引擎只调用指定服务。
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# 单帧、多引擎投票
|
|
140
|
+
result = device.ocr(engine="parallel", timeout=10)
|
|
141
|
+
# 500ms 内连续获取 Vision 新帧,再综合各轮结果
|
|
142
|
+
result = device.ocr(engine="parallel", stabilization_ms=500, timeout=10)
|
|
143
|
+
print(result.text, result.fallback_reason)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`stabilization_ms` 必须是 `0..5000` 的整数(不接受布尔值或小数),默认 `0` 保持原有单帧行为;正值适用于所有引擎模式,要求 Vision 有持续的新帧,不复用回放或旧帧。采样逐轮串行,轮次启动间隔至少 50ms,最多 101 轮。窗口结束后不再启动识别,已经启动的一轮可在 `timeout` 总预算内完成;取帧、各轮及 auto 降级共享剩余预算。窗口内没有有效样本或识别超时会抛错,不足多轮时可以返回单轮有效结果。
|
|
147
|
+
|
|
148
|
+
融合按位置 IoU ≥ 0.3 分组去重,每轮每引擎最多一票,跨轮每轮最多一票;文字按票数决定,同票采用更新观察,坐标仅取获胜文字框的中位数,不跨引擎比较置信度。时序结果的位置必须出现在最新轮并至少覆盖半数轮,输出文字也必须在最新轮出现,避免旧页面文字残留。关键词筛选在时序融合之后执行。新采样路径按实际像素裁剪区域映射回原图,小数 ROI 的取整和边缘裁剪不会沿用原始 bbox 造成偏差。
|
|
149
|
+
|
|
150
|
+
并行 OCR 与新采样路径共用进程内的 6 worker 执行池,最多接收 6 个未完成任务,满载立即报 `OCR workers busy`,不会无界排队。SDK 使用同步 HTTP 上传;等待预算到期后立即返回或抛错,丢弃迟到结果。已进入底层 HTTP 的请求无法强制中断,会由其 socket 超时结束;这些残留任务仍持有容量,直到实际结束才释放,也不会追加采样或发布标记。未开始的任务执行前会检查期限,过期不会发起上传。
|
|
151
|
+
|
|
152
|
+
`OCRResult.samples` 是有效采样轮数,`engines_used` 列出成功返回结果的引擎(空识别也计入,失败引擎不计入)。新增字段位于构造参数末尾,默认分别为 `1`、`[]`,已有位置参数构造方式不变。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
from farm import Client
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
parser = argparse.ArgumentParser(description="查询设备型号")
|
|
8
|
+
parser.add_argument("serial", help="ADB 设备序列号")
|
|
9
|
+
parser.add_argument("--base-url", default=os.environ.get("FARM_API_URL", ""))
|
|
10
|
+
parser.add_argument("--token", default=os.environ.get("FARM_API_TOKEN", ""))
|
|
11
|
+
options = parser.parse_args()
|
|
12
|
+
if not options.base_url:
|
|
13
|
+
raise SystemExit("请通过 --base-url 指定 Farm 服务地址")
|
|
14
|
+
headers = {}
|
|
15
|
+
if options.token:
|
|
16
|
+
headers["Authorization"] = "Bearer " + options.token
|
|
17
|
+
|
|
18
|
+
with Client(options.base_url, headers=headers) as client:
|
|
19
|
+
with client.device(options.serial) as device:
|
|
20
|
+
print("步骤 1:读取设备型号", flush=True)
|
|
21
|
+
result = device.adb(["shell", "getprop", "ro.product.model"], timeout=10)
|
|
22
|
+
print(result.stdout, end="", flush=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Match a confirmed template; --tap explicitly opts in to a device action."""
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
from farm import Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
parser = argparse.ArgumentParser(description='匹配指定模板')
|
|
9
|
+
parser.add_argument('serial')
|
|
10
|
+
parser.add_argument('template_id')
|
|
11
|
+
parser.add_argument('--base-url', default=os.environ.get('FARM_API_URL', ''))
|
|
12
|
+
parser.add_argument('--token', default=os.environ.get('FARM_API_TOKEN', ''))
|
|
13
|
+
parser.add_argument('--timeout', type=float, default=10)
|
|
14
|
+
parser.add_argument('--tap', action='store_true', help='等待目标后点击匹配中心')
|
|
15
|
+
options = parser.parse_args()
|
|
16
|
+
headers = {'Authorization': 'Bearer ' + options.token} if options.token else {}
|
|
17
|
+
with Client(options.base_url, headers=headers) as client:
|
|
18
|
+
with client.device(options.serial) as device:
|
|
19
|
+
if options.tap:
|
|
20
|
+
target = device.wait_for(options.template_id, timeout=options.timeout)
|
|
21
|
+
print(device.tap_match(target), flush=True)
|
|
22
|
+
else:
|
|
23
|
+
print(device.match(options.template_id, timeout=options.timeout), flush=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == '__main__':
|
|
27
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from farm import Matcher
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
parser = argparse.ArgumentParser(description='本地图像离线匹配')
|
|
7
|
+
parser.add_argument('image')
|
|
8
|
+
parser.add_argument('template')
|
|
9
|
+
parser.add_argument('--threshold', type=float, default=.88)
|
|
10
|
+
options = parser.parse_args()
|
|
11
|
+
import cv2
|
|
12
|
+
image, template = cv2.imread(options.image), cv2.imread(options.template)
|
|
13
|
+
if image is None or template is None:
|
|
14
|
+
raise SystemExit('无法读取图像')
|
|
15
|
+
matches = Matcher().match_image(image, template, threshold=options.threshold)
|
|
16
|
+
print([(m.confidence, m.center) for m in matches])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == '__main__':
|
|
20
|
+
main()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from farm import Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
parser = argparse.ArgumentParser(description='列出设备模板,或从 PNG 创建模板')
|
|
9
|
+
parser.add_argument('serial')
|
|
10
|
+
parser.add_argument('--base-url', default=os.environ.get('FARM_API_URL', ''))
|
|
11
|
+
parser.add_argument('--token', default=os.environ.get('FARM_API_TOKEN', ''))
|
|
12
|
+
parser.add_argument('--image', type=Path)
|
|
13
|
+
parser.add_argument('--name')
|
|
14
|
+
options = parser.parse_args()
|
|
15
|
+
if options.image and not options.name:
|
|
16
|
+
parser.error('--image 需要 --name')
|
|
17
|
+
headers = {'Authorization': 'Bearer ' + options.token} if options.token else {}
|
|
18
|
+
with Client(options.base_url, headers=headers) as client:
|
|
19
|
+
if options.image:
|
|
20
|
+
print(client.templates.create(options.name, options.image.read_bytes(), serial=options.serial))
|
|
21
|
+
else:
|
|
22
|
+
for template in client.templates.list(serial=options.serial):
|
|
23
|
+
print(template.id, template.name, template.enabled, template.method)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == '__main__':
|
|
27
|
+
main()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Publish matching results; context exit clears the last batch."""
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
from farm import Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
parser = argparse.ArgumentParser(description='持续匹配并发布绿框')
|
|
9
|
+
parser.add_argument('serial')
|
|
10
|
+
parser.add_argument('--base-url', default=os.environ.get('FARM_API_URL', 'http://127.0.0.1:8080'))
|
|
11
|
+
parser.add_argument('--token', default=os.environ.get('FARM_API_TOKEN', ''))
|
|
12
|
+
parser.add_argument('--max-fps', type=float, default=5)
|
|
13
|
+
parser.add_argument('--timeout', type=float, default=10)
|
|
14
|
+
options = parser.parse_args()
|
|
15
|
+
headers = {'Authorization': 'Bearer ' + options.token} if options.token else {}
|
|
16
|
+
try:
|
|
17
|
+
with Client(options.base_url, headers=headers) as client:
|
|
18
|
+
with client.device(options.serial) as device:
|
|
19
|
+
with device.watch_templates(publish=True, max_fps=options.max_fps, timeout=options.timeout) as watch:
|
|
20
|
+
for matches in watch:
|
|
21
|
+
print([(m.template_id, m.confidence, m.center) for m in matches], flush=True)
|
|
22
|
+
except KeyboardInterrupt:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == '__main__':
|
|
27
|
+
main()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "farm-sdk"
|
|
7
|
+
version = "2026.11.8"
|
|
8
|
+
description = "Python client for Farm device automation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"httpx>=0.28,<1",
|
|
13
|
+
"websocket-client>=1.8,<2",
|
|
14
|
+
"av>=14,<20",
|
|
15
|
+
"opencv-python-headless>=4.10,<5",
|
|
16
|
+
"numpy>=1.26,<3",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
test = ["websockets>=14,<18"]
|
|
21
|
+
vision = ["av>=14,<20", "opencv-python-headless>=4.10,<5", "numpy>=1.26,<3"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/liuzhuogood/Farm"
|
|
25
|
+
Documentation = "https://github.com/liuzhuogood/Farm/blob/main/packages/python-sdk/README.md"
|
|
26
|
+
Repository = "https://github.com/liuzhuogood/Farm"
|
|
27
|
+
Issues = "https://github.com/liuzhuogood/Farm/issues"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Farm SDK: vision dependencies are loaded only when needed."""
|
|
2
|
+
from .client import Client
|
|
3
|
+
from .device import Device
|
|
4
|
+
from .matcher import Matcher
|
|
5
|
+
from .models import ActionResult, CommandResult, DeviceInfo, Frame, ImportResult, Match, PageInfo, Template
|
|
6
|
+
from .ocr import OCRBox, OCRResult
|
|
7
|
+
from .errors import (ActionError, ActionTimeout, CommandError, ConnectionError, FrameTimeout,
|
|
8
|
+
HTTPError, MatchTimeout, MissingDependency, ProtocolError, SDKError,
|
|
9
|
+
TemplateDisabled, UnsupportedTemplateMethod)
|
|
10
|
+
|
|
11
|
+
__version__ = "2026.11.8"
|
|
12
|
+
__all__ = ["Client", "Device", "Matcher", "ActionResult", "CommandResult", "DeviceInfo", "Frame",
|
|
13
|
+
"ImportResult", "Match", "PageInfo", "Template", "OCRBox", "OCRResult",
|
|
14
|
+
"ActionError", "ActionTimeout",
|
|
15
|
+
"CommandError", "ConnectionError", "FrameTimeout", "HTTPError", "MatchTimeout",
|
|
16
|
+
"MissingDependency", "ProtocolError", "SDKError", "TemplateDisabled", "UnsupportedTemplateMethod"]
|