趣智校园热水洗澡 BLE 控制系统是一个面向课程作业和实验演示的 Windows PC 端工具,用于展示校园热水/智能淋浴设备的 BLE/GATT 通信闭环:扫描设备、连接、枚举 GATT、写入授权控制指令、订阅 notify 状态反馈,并把全过程记录为可审计日志。
本项目只用于本人授权设备、课程演示和 BLE 通信模型学习。项目不包含云端破解、账号绕过、动态 token 逆向、暴力枚举指令或批量控制能力。
| 能力 | 状态 | 说明 |
|---|---|---|
| BLE 扫描 | 已完成 | 支持扫描附近 BLE 设备,记录 Name / Address / RSSI / Service UUIDs。 |
| GATT 枚举 | 已完成 | 支持连接目标设备并导出 Characteristic 清单。 |
| Profile 配置 | 已完成 | 服务 UUID、控制特征、notify 特征、start/stop 指令全部配置化。 |
| 控制命令 | 已完成 | 支持 start/stop,控制前校验 profile、状态机和 allowlist。 |
| Notify 监听 | 已完成 | 订阅 notify 并记录原始 hex 数据。 |
| JSONL 日志 | 已完成 | 每条日志带 run_id、运行模式、profile 路径、BLE 模式等上下文。 |
| Tkinter UI | 已完成 | 提供扫描、连接、开始、停止、断开、GATT 显示和日志窗口。 |
| Mock 模式 | 已完成 | 无蓝牙硬件时可完整跑通离线闭环。 |
| 自动停止 | 已完成 | start 后可按 max_demo_seconds 自动 stop。 |
| 安全限制 | 已完成 | require_allowlist=true 时必须命中 allowlist 才能写控制指令。 |
用户操作 / CLI
|
v
src/smartshower/main.py tools/*.py
CLI 入口 扫描、GATT、notify 辅助脚本
|
v
src/smartshower/app_controller.py
控制状态机:scan -> connect -> discover -> start -> notify -> stop -> disconnect
|
+--> src/smartshower/protocol/profile.py
| 读取 YAML profile,校验必填字段和 allowlist
|
+--> src/smartshower/protocol/parser.py
| notify 原始 bytes -> 可读 hex 事件
|
+--> src/smartshower/ble/manager.py
| BLE provider 选择:real / mock / mock_fallback
| |
| +--> src/smartshower/ble/real.py
| | 基于 bleak 操作真实 BLE 设备
| |
| +--> src/smartshower/ble/mock.py
| 本地模拟设备,用于离线验收和 CI
|
+--> src/smartshower/utils/logger.py
JSONL 审计日志,安全序列化复杂对象
UI 路径使用 src/smartshower/ui/app.py。UI 保持一个长期后台 asyncio loop,避免真实 BleakClient 在多个事件循环之间切换。
- Windows 10/11
- Python 3.11+
- 蓝牙适配器可用且已开启
- 真实设备验收时需要本人授权的趣智校园热水/智能淋浴设备
核心依赖:
bleak:BLE 扫描、连接、GATT 读写、notifyPyYAML:profile 配置读取pytest:自动化测试,安装开发依赖时使用
推荐使用可编辑安装:
cd D:\desktop\quzhi\smartshower-controller
python -m venv .venv
.\.venv\Scripts\Activate
python -m pip install -U pip
pip install -e .[dev]也可以只安装运行依赖:
pip install -r requirements.txt安装完成后可以使用两个入口:
python main.py --help
smartshower --help无蓝牙环境或课堂预演时,先使用 mock 模式确认程序闭环稳定。
$env:SMARTSHOWER_SIMULATE_BLE = "1"
pytest -q
python tools\scan_ble.py --seconds 0.1 --out artifacts\logs
python tools\gatt_dump.py --address "AA:BB:CC:DD:EE:01" --out artifacts\logs
python main.py demo --address "AA:BB:CC:DD:EE:01" --profile profiles\shower_profile.mock.yaml --demo-seconds 1预期结果:
pytest -q输出36 passed。artifacts\logs\scan_*.json里出现Shower-DEV-Mock。artifacts\logs\gatt_AA-BB-CC-DD-EE-01.json里出现 control 和 notify 特征。logs\session.jsonl包含完整事件链:
connect_success -> discover_chars -> gatt_validation_ok -> command_start -> notify -> command_stop -> disconnect
真实硬件验收前,先准备本地 profile:
Copy-Item profiles\shower_profile.example.yaml profiles\shower_profile.local.yaml
notepad profiles\shower_profile.local.yaml然后按以下顺序执行。
python tools\scan_ble.py --seconds 10 --out logs记录候选设备的 address、name 和 rssi。如果设备名不明显,优先根据 RSSI、出现时间和宿舍设备状态变化判断。
python tools\gatt_dump.py --address "AA:BB:CC:DD:EE:FF" --timeout 20 --out logs在输出 JSON 中寻找:
- control 特征:
properties包含write或write-without-response - notify 特征:
properties包含notify - 对应 service UUID
把确认后的 UUID 和抓包得到的 start/stop 十六进制指令写入 profiles\shower_profile.local.yaml。
python main.py ui --profile profiles\shower_profile.local.yamlUI 中按顺序执行:
扫描 -> 选择目标设备 -> 连接 -> 查看 GATT -> 开始 -> 接收 notify -> 停止/自动停止 -> 断开
python main.py demo --address "AA:BB:CC:DD:EE:FF" --profile profiles\shower_profile.local.yaml --demo-seconds 15| 文件 | 用途 | 是否应提交 |
|---|---|---|
profiles/shower_profile.example.yaml |
真实设备空模板 | 是 |
profiles/shower_profile.mock.yaml |
离线 mock 演示配置 | 是 |
profiles/shower_profile.local.yaml |
本机真实设备配置 | 否,已被 .gitignore 忽略 |
device:
name_keywords:
- "Shower"
address_allowlist:
- "AA:BB:CC:DD:EE:FF"
min_rssi: -85
safety:
require_allowlist: true
confirm_before_start: true
max_demo_seconds: 20
auto_stop: true
gatt:
service_uuid: ""
control_char_uuid: ""
notify_char_uuid: ""
commands:
start:
hex: ""
write_response: true
stop:
hex: ""
write_response: true字段说明:
device.name_keywords:扫描结果过滤关键字。device.address_allowlist:允许写入控制指令的设备地址。device.min_rssi:候选设备最低信号强度。safety.require_allowlist:为true时,allowlist 不能为空且必须命中。safety.confirm_before_start:UI 中 start 前是否弹确认框。safety.max_demo_seconds:自动停止倒计时。gatt.service_uuid:目标服务 UUID。gatt.control_char_uuid:写控制命令的特征 UUID。gatt.notify_char_uuid:订阅状态反馈的特征 UUID。commands.start.hex/commands.stop.hex:授权会话中确认过的十六进制指令。commands.*.write_response:是否使用 write with response。
python main.py scan --seconds 10
python main.py gatt --address "AA:BB:CC:DD:EE:FF" --timeout 20
python main.py demo --address "AA:BB:CC:DD:EE:FF" --profile profiles\shower_profile.local.yaml --demo-seconds 15
python main.py ui --profile profiles\shower_profile.local.yamlsmartshower scan --seconds 10
smartshower gatt --address "AA:BB:CC:DD:EE:FF"
smartshower demo --address "AA:BB:CC:DD:EE:FF" --profile profiles\shower_profile.local.yaml
smartshower ui --profile profiles\shower_profile.local.yamlpython tools\scan_ble.py --seconds 10 --out logs
python tools\gatt_dump.py --address "AA:BB:CC:DD:EE:FF" --timeout 20 --out logs
python tools\notify_probe.py --address "AA:BB:CC:DD:EE:FF" --profile profiles\shower_profile.local.yaml --seconds 20
python tools\android_hci.py status
python tools\android_hci.py bugreport --out logs\android_bugreports --name quzhi_ble_session
python tools\android_hci.py extract logs\android_bugreports\bugreport.zip --out logs\android_bugreports\hci
python tools\android_hci.py analyze logs\android_bugreports\hci\BT_HCI.cfa.curf --out logs\android_bugreports\gatt_writes.csv
python tools\android_hci.py compare --session open_close=logs\android_bugreports\gatt_writes.ascii_messages.txt --out logs\android_bugreports\session_comparison.md
python tools\replay_log.py logs\session.jsonl当需要从本人授权的官方 App 会话中核对 BLE/GATT 流程时,使用 Android 真机和 USB ADB:
- 手机开启“开发者选项”“USB 调试”和 Bluetooth HCI snoop log。
- 电脑端确认
python tools\android_hci.py status显示设备为device。 - 断开本项目 UI,避免电脑占用 BLE。
- 手机使用官方 App 正常连接设备并执行授权操作。
- 运行
python tools\android_hci.py bugreport --out logs\android_bugreports --name quzhi_ble_session。 - 使用
extract提取 btsnoop/CFA 日志,再使用analyze导出 ATT write CSV 和 ASCII 消息摘要。 - 多次采集后使用
compare对比各会话,例如baseline、open_hold、close。
android_hci.py analyze 会优先使用内置 btsnoop 解析器;如果遇到不支持的格式且本机安装了 Wireshark tshark,会回退到 tshark 过滤。默认关注 ATT handles 0x0013/0x0014/0x0015/0x0018。
如果日志显示控制 payload 包含动态 token、签名、授权时间或计费上下文,不应把它当成固定 start/stop 指令复用。
日志格式为 JSONL,一行一条事件。典型字段:
| 字段 | 含义 |
|---|---|
time |
本地时间,毫秒精度 |
run_id |
单次运行 ID |
event |
事件名 |
mode |
cli_demo、ui、notify_probe 等 |
profile_path |
本次使用的 profile |
ble_mode |
real、mock 或 mock_fallback |
address |
设备地址 |
char_uuid |
notify/control 特征 |
raw_hex |
notify 原始十六进制 |
主要日志文件:
logs/session.jsonl:CLI demo 闭环日志logs/ui_session.jsonl:UI 操作日志logs/notify_probe.jsonl:notify 探测日志logs/scan_*.json:扫描结果logs/gatt_*.json:GATT 枚举结果
logs/ 默认被 .gitignore 忽略,避免误提交真实设备地址和控制指令。
运行全部测试:
$env:SMARTSHOWER_SIMULATE_BLE = "1"
pytest -q当前 v1.0.1 覆盖 36 个测试,主要验证:
- profile 读取、默认值和 allowlist 地址规范化
- hex 编码/解码
- notify 解析
- controller 状态机
- allowlist 空列表拒绝策略
- GATT service/property 校验
- notify sender 安全序列化
- JSONL logger 上下文和复杂对象序列化
- UI 按钮状态规则
- mock BLE manager 选择
- scan/gatt 工具脚本输出文件
- Android HCI bugreport 提取、ATT write 解析和会话对比
本项目强制遵守以下边界:
- 只针对本人授权设备。
- 不破解云端账号、签名、token 或加密算法。
- 不暴力枚举控制指令。
- 不支持批量控制。
require_allowlist=true时,非 allowlist 设备禁止写入控制命令。- profile 不完整时,UI 和 controller 都会阻止 start/stop。
- 真实设备配置和日志默认不提交。
如果发现设备协议依赖动态 token、签名或强加密,本项目应降级为:
扫描 + GATT 枚举 + notify 监听 + 日志分析 + 安全降级演示
smartshower-controller/
├── README.md
├── pyproject.toml
├── requirements.txt
├── main.py
├── profiles/
│ ├── shower_profile.example.yaml
│ ├── shower_profile.mock.yaml
│ └── shower_profile.local.yaml
├── src/
│ └── smartshower/
│ ├── main.py
│ ├── config.py
│ ├── app_controller.py
│ ├── ble/
│ ├── protocol/
│ ├── ui/
│ └── utils/
├── tools/
│ ├── scan_ble.py
│ ├── gatt_dump.py
│ ├── notify_probe.py
│ ├── android_hci.py
│ └── replay_log.py
└── tests/
├── test_app_controller.py
├── test_ble_manager.py
├── test_config.py
├── test_hex_utils.py
├── test_logger.py
├── test_parser.py
├── test_profile.py
├── test_tools_cli.py
└── test_ui_state.py
- 确认 Windows 蓝牙已开启。
- 确认设备处于广播或可连接状态。
- 缩短 PC 和设备距离。
- 关闭其他正在占用蓝牙连接的 App。
- 先运行 mock 验收确认程序本身无问题。
- 确认 address 来自最新扫描结果。
- 尝试重新扫描后再连接。
- 确认设备没有被手机 App 或其他客户端占用。
- 增加
--timeout 20或更高。
- 检查
service_uuid、control_char_uuid、notify_char_uuid是否复制完整。 - control 特征必须包含
write或write-without-response。 - notify 特征必须包含
notify。 - UUID 比较不区分大小写,但不能缺字符。
- 检查 profile 是否完整。
- 检查当前状态是否已经
GATT_READY。 require_allowlist=true时,确认address_allowlist包含当前设备地址。- 检查 start/stop hex 是否为空。
- 确认 notify 特征正确。
- 确认设备需要先 start 才会上报状态。
- 使用
tools\notify_probe.py单独验证 notify。
v1.0.1 是课程验收版的补充发布,重点加强真实授权会话的证据采集、协议核对和远程仓库可发现性:
- 新增
tools/android_hci.py,支持 ADB 设备状态检查、bugreport 采集、HCI/btsnoop 候选日志提取、ATT write CSV 导出和 ASCII 消息摘要。 - 新增 BLE 协议帧解码与多会话对比,便于判断 start/stop 流程是否包含动态 token、签名或会话字段。
- 扫描候选设备时支持按 allowlist 地址优先匹配,即使设备广播名不可识别也能在授权地址范围内筛选。
- README 补充 Android 官方 App 会话采集流程、HCI 分析命令和安全降级说明。
- 自动化测试扩展到 36 个用例。
v1.0.0 是课程验收版首个稳定发布:
- 完成 BLE scan/connect/GATT/write/notify/disconnect 闭环。
- 完成 mock 离线演示路径。
- 完成 Tkinter UI。
- 完成 allowlist 安全策略。
- 完成 JSONL 审计日志。
- 完成 26 个自动化测试。
- 完成 Python 包入口
smartshower。
真机控制仍依赖用户从本人授权会话中确认 UUID 和 start/stop 指令,并正确填写 profiles/shower_profile.local.yaml。