lgnap/connectiq-tethered-rig
GitHub: lgnap/connectiq-tethered-rig
一套纯 Python 工具,让开发者在没有实体手机的情况下测试 Connect IQ 手表应用与配套手机应用之间的通信。
Stars: 0 | Forks: 0
# connectiq-tethered-rig
**在没有实体手机的情况下测试 Connect IQ 手表应用。**
如果你开发了一个与配套手机应用通信的 Connect IQ 应用,但你自己并没有目标手表,那么模拟器是你唯一的选择——而且开箱即用时,它不会触发你的任何手机消息。本项目补齐了缺失的另一半:一个可以在你笔记本上运行的手机端环境,以及一种在 SDK Bug 导致正常通信路径崩溃的情况下,依然能从模拟器中提取出消息的方法。
无需 Android。无需模拟器。无需 Garmin Connect Mobile。甚至可以不用 adb。
```
$ python3 -m ciq_rig.mock_phone --send-object '{"type":"ping","n":1}'
[14:22:05] listening on 127.0.0.1:7381 — waiting for the simulator
[14:22:11] simulator connected from ('127.0.0.1', 56548)
[14:22:11] sent 44 bytes: {'type': 'ping', 'n': 1}
[14:22:11] 39 bytes in: 000024abcdabcd...
[14:22:11] decoded: {'type': 'pong', 'n': 1}
```
## 一切的核心事实
**模拟器不负责监听,而是主动发起连接。** 它自身的错误提示已经说明了这一点:
`adb forward tcp:7381 tcp:7381` 将*主机*的 7381 端口转发到手机的同一端口。因此,模拟器在你的 PC 上拨号连接 `127.0.0.1:7381`,adb 仅仅是搬运数据流。而真正调用 `ServerSocket(7381)` 和 `accept()` 的对等端是**手机**。
只要你自己监听 7381 端口,你*就*相当于那台手机。
```
┌─────────────────┐ connects to ┌──────────────────┐
│ CIQ simulator │ ─────────────────▶ │ mock_phone.py │
│ (your app) │ 127.0.0.1:7381 │ (you) │
│ │ ◀───────────────── │ │
└─────────────────┘ pushes messages └──────────────────┘
```
你甚至不需要 adb:既然没有转发*目标*,只需进行监听,模拟器就会直接连接到你。
## 功能说明
| 模块 | 作用 |
|---|---|
| `ciq_rig.wire` | 用纯 Python 编解码 Connect IQ 对象格式 —— 无需 Java,无需 SDK jar |
| `ciq_rig.mock_phone` | 在 7381 端口模拟手机的 TCP server |
| `ciq_rig.log_relay` | 通过模拟器控制台,将手表发往手机的消息提取出来 |
`wire` 是开发耗时最长、最具复用价值的部分:`serialize()` 所产生的字节,与真实配套应用通过网络发送的字节数据完全一致(已通过抓包逐字节验证);而 `deserialize()` 则能将这些字节读取回来,因此你也可以将其直接应用于真实应用的流量,单纯地进行*读取*。
在自行编写相关代码前,有一点必须了解:**该格式没有长度前缀。** 对象是自定界的,并且是连续写入的。添加前缀会导致模拟器拒绝该消息并重置连接,这种现象看起来完全像是传输故障,但实际上并非如此。
## 快速开始
```
git clone https://github.com/lgnap/connectiq-tethered-rig && cd connectiq-tethered-rig
python3 -m unittest discover -s tests # 24 tests, no simulator needed
# 1. 充当手机 — 首先,始终如此
python3 -m ciq_rig.mock_phone --send-object '{"type":"ping"}'
# 2. 启动 simulator,然后:菜单 "adb Connection" > Start
# 3. 加载你的 app
monkeydo bin/YourApp.prg fr965
```
**启动顺序至关重要且不可随意猜测。** 详情请参阅 [docs/recipe.md](docs/recipe.md) —— 文中包含了三个足以让你耗费整个下午排查的陷阱。
## 另一半挑战:将消息从模拟器中提取出来
上文内容仅涵盖了“手机 -> 手表”方向。反向传输则是更为困难的一半,且由于上游问题而无法使用:`Communications.transmit` **会导致模拟器发生段错误 (segfault)**,在 Garmin 官方的 `samples/Comm` 示例中加上一行代码即可复现。此时 socket 无法传输任何字节。这是在 SDK **8.3.0** 中引入的回归问题,在 9.2.0 版本中依然存在,在 Linux *和* macOS 上均有发生——Garmin 已通过三个独立的工单确认了此问题,但在随后的四个次要版本中仍未修复。**8.2.3 是目前已知最后的可用版本。** 详细信息及三个相关工单请见:[docs/known-issues.md](docs/known-issues.md),以及我们自己的 [bug 报告][bug]。
既然如此,请为这个方向换一条通道。`System.println` 依然可用,其输出会显示在 monkeydo 控制台中——只需加上编译注解,就不会影响最终发布版本:
```
// (:commMuted) — test-rig build only; the shipping build calls transmit for real
function emitToPhone(payload) {
Sys.println("[phone-out] " + toJson(payload));
}
```
`ciq_rig.log_relay` 会实时跟踪该控制台,并将每一个对象交给你。你可以选择自行读取:
```
monkeydo bin/YourApp-rig.prg fr965 2>&1 | tee /tmp/monkeydo.log &
python3 -m ciq_rig.log_relay /tmp/monkeydo.log --print
```
……或者,如果你的手机端是一个**真实的 Android 应用**,可以通过调试广播接收器(broadcast receiver)将它们直接投递到该应用中。这样一来,注入的消息就能与 SDK 本身发出的消息走完全相同的代码执行路径:
```
python3 -m ciq_rig.log_relay /tmp/monkeydo.log \
--action com.example.app.WATCH_IN \
--component com.example.app/com.example.app.WatchBridgeReceiver
```
后一种方式隐藏了一个在自行编写代码前必须留意的陷阱:`adb shell` 会将其参数重新拼接成单个字符串,交由**设备端**的 shell 再次进行解析。因此,未加引号的 JSON payload 到达时会支离破碎;而如果 payload 中包含 `$(...)`,甚至会在设备上被执行。`log_relay` 已经针对这第二次 shell 解析进行了正确的引号转义;具体请参见 `adb_broadcast_command`。
完整细节,以及第二个 Bug——一个专门针对 Android 配套应用造成隐患的静默 Bug——请参见 [docs/known-issues.md](docs/known-issues.md)。
## 验证内容及验证方式
所谓“在我自己的编码器上测试通过”并不能作为证据:
| 组件 | 验证方式 |
|---|---|
| `wire` | 通过代理从**真实配套应用**中捕获的字节。解码后得到预期值,重新编码后能够**精确**还原捕获的字节——包括内部偏移量和广度优先顺序。 |
| `mock_phone` | 在 `venu3` 上使用**真实的 Connect IQ 模拟器**验证:推送的对象能够在手表上正常渲染,全程无需 Android、无需模拟器,也无需 `adb forward`。 |
| `log_relay` | 使用**真实的 monkeydo 控制台日志**(包含其中的引号转义),随后将其投递到**真实的 Android 应用**中,并已通过该应用自身的日志确认。 |
组帧(Framing)正是这些检查所发现的问题所在。早期版本基于反编译结果,曾错误地使用了长度前缀进行消息组帧;但抓包结果推翻了这一点。事实证明,对象是自定界的。
## 文档
- [docs/wire-format.md](docs/wire-format.md) —— 逐字节解析对象格式
- [docs/recipe.md](docs/recipe.md) —— 启动顺序与避坑指南
- [docs/known-issues.md](docs/known-issues.md) —— 模拟器段错误,以及 `NetworkOnMainThreadException`
## 状态与范围
本项目目前每天都被用于在开发者本人并未拥有的设备上,开发一款真实的手表应用。这是一个开发工具,而非一个完整的产品:它仅模拟*传输层*,不涉及配对、蓝牙或电源管理。
本项目提取自一套实际运行中的私有工具链,因此代码经过了实战检验,但其打包封装尚处于早期阶段。
欢迎提交 Issue 和 PR —— 特别期待来自其他 SDK 版本和其他主机平台的测试反馈。
## 许可证
Apache-2.0。请参阅 [LICENSE](LICENSE) 和 [NOTICE](NOTICE)。
本项目不隶属于 Garmin Ltd.,未获其认可或赞助。项目不包含任何 Garmin 的代码或二进制文件;你需要自行提供 SDK。其网络传输协议格式是通过观察得出的,并为了实现互操作性而独立实现。
标签:Garmin, Python, 可穿戴设备, 无后门, 测试工具, 移动开发, 网络调试, 自动化, 逆向工具