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
标签:云资产清单, 物联网, 硬件交互, 蓝牙, 计算机取证, 逆向工具, 逆向工程