把健康数据采集器流放到手机上

我写了一个用 ADB 和 uiautomator 自动采集小米运动健康 App 数据的小工具,Go 写的,几百行代码。它能解锁手机、启动 App、爬取屏幕上的控件树、把步数睡眠心率之类的数字刮下来,传到 Cloudflare D1。

问题是它只能在家跑——Mac 开着,手机连着同一个 WiFi,ADB 通过局域网无线调试连接。出了家门,这条链路就断了。像养了一只只认家门的猫。

解决思路很朴素:既然手机上能装 Termux,Termux 里能装 ADB,那让程序直接在手机上跑,ADB 连接自己,不就不需要 Mac 了吗。

听起来只差一步。实际上,这一步里埋着好几个坑。

代码改动:四行

Go 代码的改动小到令人不安。在 adb/adb.go 的设备检测函数开头加一个环境变量覆盖:

1
2
3
4
5
// 显式指定设备(Termux 用 localhost:PORT)
if s := os.Getenv("MIHEALTH_ADB_SERIAL"); s != "" {
    deviceSerial = s
    return
}

加上 import 里补一个 "os",总共四行。其他文件一个字没改。

真正的麻烦不在代码里。

交叉编译三连坑

本以为在 Mac 上交叉编译一个 ARM64 二进制推到手机就完事了。结果连续撞了三面墙。

第一面墙:Android 不认静态二进制

1
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-s -w" -o mihealth-data-termux .

推到手机一跑:unexpected e_type: 2

Android 从 API 21 开始强制要求 PIE(Position Independent Executable)。Go 默认产出的静态二进制是 ET_EXEC(类型 2),Android 内核直接拒绝加载。

第二面墙:TLS 对齐

加上 -buildmode=pie

1
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -buildmode=pie -ldflags="-s -w" -o mihealth-data-termux .

这次报错更晦涩:executable's TLS segment is underaligned: alignment is 8, needs to be at least 64 for ARM64 Bionic

Go 的 PIE 模式产出的二进制,线程局部存储段只做了 8 字节对齐,而 Android 的 Bionic 链接器要求 64 字节。这是 Go 工具链和 Android 运行时之间的一个裂缝,夹在中间的人只能干瞪眼。

第三面墙:SELinux

换成 GOOS=android

1
GOOS=android GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-s -w" -o mihealth-data-termux .

二进制终于能启动了。程序跑起来,连接 ADB,准备 dump UI——然后:fork/exec /data/data/com.termux/files/usr/bin/adb: permission denied

GOOS=android 编译的二进制使用 /system/bin/linker64,运行时拿到的是系统级 SELinux 上下文,没有权限访问 Termux 的私有目录。程序能跑,但它调用的 adb 在 Termux 的地盘里,它够不着。

三次编译,三种死法。每一种都合理,每一种都让人无话可说。

最终方案:在 Termux 上编译

1
2
pkg install golang
CGO_ENABLED=0 GOTOOLCHAIN=local go build -o $HOME/bin/mihealth-data .

最笨的办法。在目标环境里装 Go,把源码传过去,本地编译。产出的二进制天然拥有正确的链接器、正确的 SELinux 上下文、正确的一切。

有时候绕了一大圈,发现最短的路就是不绕。

ADB 连接自己

Termux 里的 ADB 通过无线调试连接手机自身。但无线调试的端口是随机分配的,重启就变。试过 getprop 读系统属性——没有。试过 /proc/net/tcp 扫端口——权限拒绝。Android 把这些信息藏得很深。

最后用了一个笨办法:

1
2
adb tcpip 5555          # 让 adbd 在固定端口监听
adb connect localhost:5555  # 连接自身

重启后需要重新执行一次 adb tcpip 5555,但至少端口是确定的。在不确定性中找到一个锚点,够用了。

配对过程也有讲究:Android 无线调试的配对码页面一旦切走就失效。需要用分屏模式——上半屏看配对码,下半屏在 Termux 里输入。两个世界同时存在于一块屏幕上。

Termux 环境踩坑清单

症状解决方案
/sdcard/ noexec复制到 sdcard 的二进制无法执行termux-setup-storage 后通过 ~/storage/shared/ 访问,cp 到 Termux home 再执行
termux-services 不生效sv-enable crond 报 file does not exist安装后必须重启 Termux
Go 版本不匹配go.mod requires go >= 1.25.7 (running 1.25.6)go.mod 降版本,或加 GOTOOLCHAIN=local
CGO 编译失败clang: No such file or directoryCGO_ENABLED=0 禁用 CGO
手机打不出 ASCII ~全角 不被 bash 识别$HOME 代替 ~
配对码切走就失效无线调试页面离开即刷新分屏模式同时操作
端口自动探测不可行getprop 和 /proc/net/tcp 都不可用adb tcpip 5555 设固定端口
git clone 私有仓库手机上打 Personal Access Token 太痛苦Termux 生成 SSH key,添加到 GitHub

每一行都是实际踩过的。

定时任务

Termux 端负责采集,每天两次:

1
2
3
4
5
6
7
8
pkg install cronie termux-services
# 重启 Termux 后
sv-enable crond
sv up crond
termux-wake-lock  # 防后台被杀

echo "30 7 * * * $HOME/bin/run-mihealth.sh >> $HOME/mihealth-cron.log 2>&1
30 17 * * * $HOME/bin/run-mihealth.sh >> $HOME/mihealth-cron.log 2>&1" | crontab -

Mac 上的 OpenClaw Health Agent 在 7:40 和 17:40 从 D1 API 读取数据,结合七天历史做趋势分析,生成飞书卡片推送。采集和分析完全解耦——手机负责干活,Agent 负责思考。

手机 Termux cron 7:30/17:30 → ADB 本机 → 采集 → Cloudflare D1
                                                        ↓
Agent cron 7:40/17:40 → 读 API → 7天趋势分析 → 飞书卡片

几条经验

关于交叉编译:Go 交叉编译到 Android/Termux,如果程序需要 fork/exec 其他 Termux 工具,交叉编译走不通。SELinux 会在你以为万事大吉的时候拦住你。老实在目标环境编译。如果是纯计算型二进制(不 fork 其他程序),GOOS=android 可行。

关于 Termux:它比想象中能干,也比想象中脆弱。能装 Go、能跑 cron、能连 ADB——但 Android 系统随时可能杀后台。termux-wake-lock + 关闭电池优化 + 锁定最近任务,三件套缺一不可。

关于自动化的边界:ADB + uiautomator 理论上可以操控任何 App。但像微信这样使用自定义渲染的 App,控件树里能拿到的信息远不如原生 UI 丰富。健康类 App 多用标准 Android 控件,是自动化的理想对象。

一个工具从依赖笔记本电脑到能在口袋里自己运行,改的代码不过四行。真正的工作量在于和运行环境的反复交涉——编译器、链接器、安全策略、文件权限、端口发现,每一层都有自己的规矩。《庄子》里说"庖丁解牛",讲的是顺着纹理走。这些纹理不在文档里写着,只有刀碰上去才知道。