AegisX86/UnidenR8wlink
GitHub: AegisX86/UnidenR8wlink
一个通过蓝牙 BLE 从 Uniden R8w 雷达探测器读取实时遥测与警报数据的 Python 库,附带完整的协议逆向文档。
Stars: 0 | Forks: 0
# r8link
一个通过 Bluetooth 读取 Uniden R8w 雷达探测器的 Python 库。
LE、警报、频段、频率、信号强度、方向、电压、航向、
速度以及存储的摄像头数据库,以上内容均以 dataclasses 形式封装,只需大约十行
你自己的代码即可实现。
本项目不附属于 Uniden,也未获得其认可。
```
from r8link import R8W
async with R8W("E0:00:00:00:23:D4") as r8:
async for update in r8.updates():
if update.kind == "alerts":
for alert in update.alerts:
print(alert) # KA 33.785 GHz 5/8 front
```
在 [PROTOCOL.md](PROTOCOL.md) 中有一份完整的协议说明:数据包
格式、每个字段的含义,以及其中哪些部分是我推测出来的。
## 这个项目的历程
哇哦,我已经很久没碰这个了。
我在 12 月开始了逆向工程,在 1 月休假期间完成了它,然后它就在 `git.wired/`(我的内部研究网络)上
闲置了七个月,什么也没做。它“能用”,我也用过,但一直没发布。现在把它放到 GitHub 上是因为,让代码在我家里的机器上腐烂对谁都没好处。
这意味着下面的安装说明是刚重新验证过的,而不是凭记忆写的。我在一台干净的 Pi 上从头设置了整个环境,
以检查它们是否仍然有效。有两件事在 1 月份坑了我,我早就忘得一干二净了,而它们在 7
月份又把我坑了。现在它们已经被妥善记录在配对章节中了。此外还有一个 `pair.py` 可以为你完成整个设置,因为显然光是把警告写下来并不足以让我自己遵守它们。
## 环境要求
- 带有 BlueZ 的 Linux。
- Python 3.10+
- `bleak`(会为你自动安装)
- 显然,还需要一台 Uniden R8w
我实际测试的环境,以防万一出问题时你想
了解你与已知良好配置之间的差异:一台使用其板载 Bluetooth 的 Raspberry Pi 4,Debian Trixie (Raspberry Pi OS),Python 3.13,BlueZ
5.82,bleak 3.0.2。
其他 Pi 型号、其他 Debian 版本、其他发行版、带有正常 BLE 适配器的普通桌面电脑:这些应该都没问题。这里没有任何东西是 Pi 专用的。但是“应该没问题”只是我的猜测,而不是我实际运行过,所以如果你的设备出了什么有趣的问题,请提一个 issue,我会研究的。
macOS 是我真正预期会出问题的一个平台。CoreBluetooth 不提供 MAC 地址(你会得到一个系统分配的 UUID),因此这些示例中的所有地址在那里都毫无意义,你需要按名称查找设备。我从未尝试过。欢迎提交 PR。
## 安装说明
```
git clone https://github.com/Aegisx86/UnidenR8wlink
cd UnidenR8wlink
python3 -m venv .venv # Raspberry Pi OS will not let pip touch system Python
source .venv/bin/activate
pip install -e .
```
在较新的系统上,venv 不是可选的。Debian 将系统 Python 标记为外部管理,在接触到这段
代码之前,`pip install .` 就会失败并报错 `error: externally-managed-environment`。使用 `-e` 这样你就可以编辑 `r8link/` 而无需重新安装。
在尝试配对 R8w 之前,先运行测试:
```
pip install pytest && pytest
```
如果它们通过了,说明包导入正常且数据包格式完好,
从这里开始出现的任何问题都是 Bluetooth 的错,
而不是我的。
## 配对
R8w 在响应任何 GATT 请求之前,需要进行系统级的 Bluetooth 配对。如果没有配对,你会连接成功,枚举出所有
characteristics,但随后每次读取都会返回空值或带有
认证失败的提示。手机向你隐藏了这一点,这就是为什么 nRF
Connect 开箱即用,而你的 Python 脚本却不行的原因。
为此提供了一个脚本:
```
python pair.py
```
它会检查那些否则会浪费你一晚上的东西(rfkill、
用户组成员身份、bluetoothd),引导你进入配对模式,重试
配对,随后释放连接,然后使用该库进行连接
并读取真实的遥测数据,这样你就能知道它是真的成功了,而不仅仅是
配对上了。如果你已经知道地址:
```
python pair.py E0:00:00:00:23:D4
```
这种形式也可以作为已配对设备的健康检查。当它提示重新配对时选择
否,它就会直接连接并读取数据。
### 手动配对
如果你不想运行我的脚本而是自己动手,这里有一份指南:
Bluetooth 可能被软阻止了。大多数 Raspberry Pi OS 镜像默认就是这样,
而 `systemd-rfkill` 会为你持久化恢复该状态。
```
rfkill list bluetooth
sudo rfkill unblock bluetooth
```
当硬件明显存在时,`hciconfig -a` 显示 `DOWN` 就是这个
症状。在 `journalctl -u bluetooth` 中出现 `Failed to set mode: Failed (0x03)`
也是症状之一。
你还需要处于 `bluetooth` 组中。BlueZ 的 D-Bus 策略向该组授予了
配对和代理操作的权限。如果没有它,`power on` 可以正常
工作,但随后 `agent on` 或 `pair` 会失败并报错 `AccessDenied` 或直接提示
"Rejected send message",这看起来完全不像是一个权限
问题。
```
sudo usermod -aG bluetooth $USER
```
然后注销并重新登录。组成员身份仅适用于新会话,
因此如果 `groups` 没有列出 `bluetooth`,说明你仍处于旧会话中。
现在开始配对本身。在探测器上:
```
MENU -> BT Pairing -> MENU display shows "Pairing R8W@.."
```
**未配对的 R8w 在你执行此操作之前根本不会进行广播。** 它不会
出现在扫描结果、`bluetoothctl`、nRF Connect 或任何其他
地方。如果你正在扫描但什么也没找到,这几乎总是原因所在。
该窗口会在一段时间后自动关闭,因此请将探测器放在
伸手可及的地方。你可能至少需要重新激活它一次。
然后在机器上操作。请在一个交互式会话中完成此操作。配对代理
必须在配对期间保持存活状态,而作为从你的 shell 中执行的单次命令
`bluetoothctl pair ...` 会在发送完命令的瞬间退出,并连同代理
一起关闭。BlueZ 将此称为 `AuthenticationCanceled`,读起来
就像是探测器拒绝了你一样:
```
bluetoothctl
power on
agent on # 5.82 registers one at startup already,
default-agent # both of these are no-ops now. harmless.
scan on # wait for R8W@.. to appear, note the address
scan off # single-radio adapters cannot scan and connect at once
pair E0:00:00:00:23:D4
```
**如果第一次 `pair` 失败并报错 `AuthenticationFailed`,请再次运行完全相同的
命令。** 我的设备失败了,然后在进行相同的
第二次尝试时立即成功了,这种情况发生了两次,间隔了几个月。我无法解释。在你尝试两次之前,
不要开始拆卸设备。
当 `bluetoothctl` 向你输出整个 GATT 树并提示 `Pairing successful` 时,你就知道它成功了。探测器显示屏上的 Bluetooth 图标是另一种确认方式。
### 然后断开连接
```
disconnect E0:00:00:00:23:D4
exit
```
BlueZ 占用着连接。在你释放它之前,你自己的代码无法获取它,
而且因为探测器在某个设备连接到它时会停止广播,
所以失败的表现是 `BleakDeviceNotFoundError`:一个你在 `bluetoothctl info` 中能看到、但 Python 坚称不存在的设备。
配对是持久化的,你只需做一次。如果以后出了问题,
在双方设备上“忘记”该设备并重新配对即可。
## 运行示例
```
python examples/minimal.py [address] # print alerts as they arrive
python examples/monitor.py [address] # live terminal
```
注意 `minimal.py` 仅在收到警报时打印内容,因此在没有雷达的
桌面上它会连接、打印电压,然后静静地停在那里
看起来像是坏了。它并没有坏。
## 使用说明
```
from r8link import R8W, Command
# 查找一个(需要处于 pairing mode,或者已经配对并处于空闲状态)
devices = await R8W.discover()
async with R8W("E0:00:00:00:23:D4") as r8:
r8.telemetry # latest Telemetry, updates every 1-2s
r8.alerts # list[Alert], empty means clear
r8.telemetry_age # seconds since the last packet
r8.is_connected
await r8.read_poi() # stored cameras and user marks, with coords
await r8.read_device_info() # firmware / software version
await r8.read_raw(uuid) # for poking at the undecoded characteristics
async for update in r8.updates():
... # update.kind is "telemetry" or "alerts"
```
`Alert` 为你提供 `band`、`strength` (1-8)、`frequency_ghz`、`laser_gun`、
`direction_name`、`mute_status`、`is_muted` 和 `description`,这通常是
你想要打印的:雷达显示频率,激光显示枪名,照片雷达频段显示 "Gatso"。
`Telemetry` 为你提供 `voltage`、`heading`、`speed`、`altitude`、
`gps_locked`、`scan_count` 以及当探测器向你发出存储的摄像头警告时的 `poi`。
所有内容都保留了一个带有原始数据包的 `raw` 字段,并且解析器中没有任何内容
会对无法识别的输入抛出异常。未知的激光 ID 会返回
`"unknown laser (23)"`,短缺的数据包会将缺失的字段保留为
`None`。仅仅因为 Uniden 在固件更新中破坏了某些东西,就让仪表盘上运行的程序崩溃
不是一个可接受的故障模式。
### 重新连接
客户端不会自动为你重连。当连接断开时 `updates()` 会返回,由你决定下一步做什么:
```
while True:
try:
async with R8W(ADDRESS) as r8:
async for _ in r8.updates():
draw(r8)
except Exception as exc:
print(exc)
await asyncio.sleep(3)
```
顺便在 `asyncio.run` 周围捕获 `KeyboardInterrupt`。否则 Ctrl-C 会跳过上下文管理器的退出,导致探测器保持半开连接状态,而下次运行时将无法绕过。
### 写入命令
协议中存在静音、用户标记等功能。我从未向真实硬件发送过这些命令,因此它们被置于一个标志之后:
```
async with R8W(ADDRESS, allow_writes=True) as r8:
await r8.send_command(Command.MUTE)
```
如果没有 `allow_writes=True`,你会得到一个 `WritesDisabledError`。如果你在尝试这些操作时以某种方式让一台价值 900 美元的雷达探测器变砖,那不是我的问题。
读取静音状态不需要这些。探测器会在警报数据包中报告它,因此在设备本身上按下静音会立即反映在 `alert.is_muted` 中。
## 状态 / 诚实声明部分
这是通过对 R/Tach Android 应用 (JADX) 进行反编译,加上对一台探测器的实时 BLE 抓包逆向工程得出的。我不是逆向工程专家,这超出了我的专业领域,我是一名计算机工程专业的学生,只是将反编译器对准了一个 APK,然后盯着 Java 代码看直到看懂为止。这里记录的所有内容都与我的设备实际发送的数据相符。这是一个样本量为 1 的测试,我可能在某些地方弄错了。
- **单一设备。** 一台 R8w,一个固件版本。它花了 900 美元,我不打算
为了测试边界情况去买第二台。
- **单一环境配置。** Pi 4,Trixie,BlueZ 5.82,bleak 3.0.2。其他的 Linux 应该
没问题。
- **实践中是只读的。** 写入功能已实现但未经测试,见
上文。
- **对其他型号一无所知。** R4W 或其他任何型号:UUID 和
格式可能完全不同。反编译后的解析器中有一个 `isI9` 标志,暗示各型号之间共享基础设施,这我
无法验证。
- **某些字段是推测出来的。** 两个设置 characteristics 大部分
未解码,`scan_count` 似乎不是一个计数,并且在我拥有的每次抓包中,alert field 1 的读数都是
`00`。所有这些都在 PROTOCOL.md 中提到了。
- **激光路径从未见过真实的数据包。** Gatso 频段、静音代码 3 到 6 以及填充了内容的 POI 数据库也是如此。这些
都是根据反编译的源代码实现的,并且仅针对合成
输入进行了测试。
如果你的设备发送了此解析器解析错误的数据,请带上原始数据包提一个 issue,正是因为这个原因,该字段在每个型号上都存在。更多的数据点会使这份参考变得更好。
## 代码导览
- `r8link/protocol.py` - 所有的解析逻辑。纯函数,没有 bleak,没有
I/O 操作。每个数据包格式都存放在这里,并且可以通过字符串进行测试。
- `r8link/client.py` - bleak 的封装。负责连接、订阅、排队,并交给你
解析后的数据包。包含 UUID 和写命令常量。
- `r8link/errors.py` - 三个异常,其中一个是一篇长达四页的关于
配对的致歉说明。
- `r8link/__init__.py` - 对外公开的接口。
- `pair.py` - 自动化的配置流程,并在最后进行一次读取以证明其
有效。
- `examples/` - `minimal.py` 和仪表盘代码。
- `tests/test_protocol.py` - 针对捕获数据包的解析器测试,不需要
硬件。
## 致谢
- 由 Aigis (P.R_Aigis) 编写,2026
- 基于 [bleak](https://github.com/hbldh/bleak) 构建
- 协议通过 [JADX](https://github.com/skylot/jadx)、btmon 和 nRF connect 破解得出
- 基于 MIT 许可发布,详见 LICENSE
标签:云资产清单, 物联网, 硬件交互, 蓝牙, 计算机取证, 逆向工具, 逆向工程