Compare commits
7 Commits
main
...
dev-202609
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0253d1bbf | ||
|
|
f7edfa291d | ||
|
|
1948e4b1ba | ||
|
|
57762e50be | ||
|
|
dd218addce | ||
|
|
8a7cebcf77 | ||
|
|
f729d16ff2 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -5,3 +5,8 @@ mcm-ddc-ble/MDK-ARM/Objects
|
||||
mcm-ddc-ble/MDK-ARM/build.log
|
||||
mcm-ddc-ble/MDK-ARM/build-app2.log
|
||||
mcm-ddc-ble/MDK-ARM/Objects-app2
|
||||
|
||||
# release zips are regenerable via tools/make_release.bat
|
||||
tools/out/mothercup_tools_*.zip
|
||||
tools/python-embed/Lib/site-packages/__pycache__/
|
||||
tools/python-embed/**/__pycache__/
|
||||
|
||||
25
AGENTS.md
25
AGENTS.md
@ -5,7 +5,7 @@
|
||||
本仓库是 **mothercup(母乳杯)** 产品的完整代码仓库,包含三大部分:
|
||||
|
||||
1. **设备固件** — 基于国民技术(Nations)**N32WB031 BLE SoC**(Cortex-M0,64MHz HSI,**板载硅片实测 256KB Flash**(0x01000000~0x0103FFFF,开发日志 §37),RAM 48KB+16KB):
|
||||
- **`mcm-ddc-ble/`** — 唯一活跃的应用固件工程(APP,链接在 APP1 bank `0x01008000`/112KB)。以 SDK rdtss 例程为蓝本,集成 FreeRTOS、UART CLI、BLE 自定义 GATT 服务(CLI 透传 / 设备信息查询 / BLE OTA 双 bank 直写升级)、bootsetting 与 APP_DATA 参数区结构化读写命令。当前版本 **V1.00.23**(`mcm-ddc-ble/inc/app_version.h`)。历史上曾有 `mcm-ddc-04/` 主工程,**已删除**,其功能已全部并入本工程。
|
||||
- **`mcm-ddc-ble/`** — 唯一活跃的应用固件工程(APP,链接在 APP1 bank `0x01008000`/112KB)。以 SDK rdtss 例程为蓝本,集成 FreeRTOS、UART CLI、BLE 自定义 GATT 服务(CLI 透传 / 设备信息查询 / BLE OTA 双 bank 直写升级)、bootsetting 与 APP_DATA 参数区结构化读写命令。当前版本 **V1.00.27**(`mcm-ddc-ble/inc/app_version.h`)。历史上曾有 `mcm-ddc-04/` 主工程,**已删除**,其功能已全部并入本工程。
|
||||
- **`Boot/`** — 自写精简 bootloader(链接在 `0x01000000`/16KB):校验 bootsetting 记录自身完整性(magic + 结构体 CRC32)后按 active bank 的 `start_address` 直接跳转;**不校验镜像 CRC(CRC 校验在 OTA 升级过程中完成)**;记录无效或地址越界回退 APP1。
|
||||
2. **手机 App `SmartAssiter/`** — uni-app **Vue3 + TypeScript** 工程(HBuilderX 项目管理,无 package.json),通过 BLE 连接设备:查看运行参数(温度/电压/转速/bank/堆等)、CLI 终端、参数读写(appget/appset)、OTA 升级页(e0005 锁步,已恢复)。另有登录/注册/用户信息等云端业务页面。
|
||||
3. **PC 工具 `tools/`** — 整片烧录包制作/烧录脚本 + bleak 蓝牙测试客户端。
|
||||
@ -45,7 +45,7 @@ Flash 布局(256KB,`Boot/src/dfu_layout.h` 为唯一权威定义;Boot/boot
|
||||
| APP1 | 0x01008000 | 112KB(0x1C000) |
|
||||
| APP2 | 0x01024000 | 112KB(0x1C000) |
|
||||
|
||||
- **RAM 铁律**:SRAM 为 48KB+16KB,**低 16KB(0x20000000~0x20003FFF)由芯片 ROM/BLE 子系统占用**(rwip 堆描述符、patch 数组、中断中继表,由 ROM 启动代码预备)。**任何工程的 IRAM 执行区必须从 0x20004000 起**(含 Boot 和任何小工具),否则 .data/.bss 初始化冲掉 ROM 数据,BLE init 必然 HardFault(实测根因,开发日志 §21)。注意:mcm-ddc-ble.uvprojx 里 `<IRAM>` 镜像元素显示 0x20000000 是过期显示值,实际生效的是 OCR_RVCT9=0x20004000/0xC000——以 `Listings/mcm-ddc-ble.map` 的 `RW_IRAM1 (Exec base: 0x20004000)` 为准。
|
||||
- **RAM 铁律**:**低 16KB(0x20000000~0x20003FFF)由芯片 ROM/BLE 子系统占用**(rwip 堆描述符、patch 数组、中断中继表,由 ROM 启动代码预备),**任何工程的 IRAM 执行区必须从 0x20004000 起**(含 Boot 和任何小工具),否则 .data/.bss 初始化冲掉 ROM 数据,BLE init 必然 HardFault(实测根因,开发日志 §21)。**顶部同样悬崖(实测 SWD 探明,§47):0x2000C000 起访问即 fault,APP 可用 SRAM 实为 32KB(0x20004000~0x2000BFFF)**——IRAM 配置里的 0xC000(48KB)上限是虚的,**栈顶 `__initial_sp` 必须 < 0x2000C000**;`merge_image.py` 已加硬卡(出包时校验 bin 向量表首字,越线直接失败)。ZI 膨胀(加大缓冲/堆)把栈顶推过线会导致上电即 HardFault/Lockup(V1.00.24 的 2KB DMA ring 事故,heap 已降为 18KB 腾位)。注意:mcm-ddc-ble.uvprojx 里 `<IRAM>` 镜像元素显示 0x20000000 是过期显示值,实际生效的是 OCR_RVCT9=0x20004000/0xC000——以 `Listings/mcm-ddc-ble.map` 的 `RW_IRAM1 (Exec base: 0x20004000)` 为准。
|
||||
- **VTOR**:Cortex-M0 无 SCB->VTOR,用 `PWR->VTOR_REG`(bit31=EN|[30:0]=基址)。规则:**BLE 初始化之前** VTOR 指向 APP 自身向量表(main 开头设 `0x80000000|0x01008000`,USART1 RX 中断需要);**`ns_ble_stack_init()` 之后 VTOR 必须为 0**,此后所有中断经芯片 ROM 跳板 + RAM 中继。**绝对不要在 BLE init 后重设 VTOR**(实测崩溃根因,开发日志 §18)。
|
||||
|
||||
## 固件工程(mcm-ddc-ble)
|
||||
@ -55,7 +55,7 @@ mcm-ddc-ble/
|
||||
├── src/
|
||||
│ ├── main.c # 入口:VTOR 预置 → USART+banner → LED → app_ble_init → app_params_init
|
||||
│ │ # → ns_sleep_lock_acquire(锁睡眠保 SWD) → 建 BLE 调度/LED/CLI 任务 → vTaskStartScheduler
|
||||
│ ├── bsp_usart.c # USART1(PB6/PB7, 115200 8N1):fputc 重定向 + DMA TX(CH1) + RXDNE 中断字节队列 + TX 互斥锁
|
||||
│ ├── bsp_usart.c # USART1(PB6/PB7, 460800 8N1, V1.00.26 起;IWDG 看门狗见 §50):fputc 重定向 + DMA TX(CH1) + RX DMA 循环环形缓冲(CH2, 3KB, IDLE 中断唤醒;擦扇区关中断期间不丢字节) + TX 互斥锁
|
||||
│ ├── app_gpio.c # LED1(PB0)/LED2(PA6)
|
||||
│ ├── app_cli.c # CLI UART 前端:CliTask 行接收/编辑/历史/Tab/Ctrl+Z,提示符 caiic->
|
||||
│ ├── cli_core.c # CLI 核心(命令表 s_cmds[]/handler/分词执行),输出经可切换 cli_out_fn
|
||||
@ -95,7 +95,7 @@ mcm-ddc-ble/
|
||||
"D:\Keil_v5\UV4\UV4.exe" -b caiic_boot.uvprojx -j0 -o build.log # Boot 增量构建
|
||||
```
|
||||
|
||||
要求保持 **0 Error(s), 0 Warning(s)**。当前基线(V1.00.22):`Code=48872 RO-data=4980 RW-data=2076 ZI-data=29116`。
|
||||
要求保持 **0 Error(s), 0 Warning(s)**。当前基线(V1.00.27):`Code=49408 RO-data=5132 RW-data=2092 ZI-data=30140`。
|
||||
|
||||
**固件版本号约定(重要)**:每次修改固件代码必须递增 `mcm-ddc-ble/inc/app_version.h` 中的 `APP_FW_VERSION` 与 `APP_FW_VERSION_NUM`(格式 `0x00MMmmpp`,通常补丁位 +1),用于识别板上实际运行的固件;工程名与出包文件名保持不变。
|
||||
|
||||
@ -113,7 +113,7 @@ mcm-ddc-ble/
|
||||
3. 日常开发调试:Keil F7 编译、F8 下载即可(Boot 不校验镜像 CRC,可直接下载 APP 调试);**Keil 下载选项必须是 "Erase Sectors",整片擦除会杀掉 Boot/bootsetting**(杀掉后需重烧整片包恢复)。板子上烧过其他 0x01000000 起步的程序同样会覆盖 Boot,恢复也是重烧整片包。
|
||||
4. 日常固件升级设计路径是 BLE OTA(手机 App 或 PC bleak 客户端)。
|
||||
|
||||
验证运行:串口助手接 PB6(TX)/PB7(RX),115200 8N1,复位后应看到 banner 与 `caiic->` 提示符(`help` 查看命令);手机应能搜到广播名 `CAIIC-MCM-20260902`。
|
||||
验证运行:串口助手接 PB6(TX)/PB7(RX),460800 8N1(V1.00.26 起),复位后应看到 banner 与 `caiic->` 提示符(`help` 查看命令);手机应能搜到广播名 `CAIIC-MCM-20260902`。
|
||||
|
||||
## BLE 接口与协议
|
||||
|
||||
@ -128,7 +128,7 @@ mcm-ddc-ble/
|
||||
| `...c72ee005` | Read + Write(带响应) | **OTA 专用特征**(V1.00.19 新增):写携带一帧 OTA 传输帧 → 读同一特征取回 OTA_RSP(轮询至非空),见 ble_protocol.md §6.6 |
|
||||
|
||||
- **notify 上行已修复(V1.00.19)**:根因是固件帧 CRC16 漏算最后一个 payload 字节(3+payLen vs 协议规定的 4+payLen),所有帧被静默丢弃,与 notify 硬件链路无关(开发日志 §42)。帧协议通道(e0001/e0002)已实测恢复。App(SmartAssiter)走 `...e0003`/`...e0004` 读写特征 + `...e0005` OTA 特征(App 侧参考 `src/mcm/McmOta.ts`,PC 参考 `tools/ble_ota_update.py`)。
|
||||
- 帧格式:`0xCA | TYPE | SEQ | LEN(LE16) | PAYLOAD | CRC16-CCITT(0x1021/0xFFFF, LE)`,**CRC 覆盖 TYPE..PAYLOAD 共 4+payLen 字节**;OTA 为双 bank 直写、**逐帧锁步 ack**、END 整镜像 zlib CRC32 + 向量表 bank 校验后更新 bootsetting 并复位。OTA 传输层通道无关:BLE e0005 / UART 二进制模式(CLI `ota`)/ 旧帧协议通道,详见 ble_protocol.md §6.6。
|
||||
- 帧格式:`0xCA | TYPE | SEQ | LEN(LE16) | PAYLOAD | CRC16-CCITT(0x1021/0xFFFF, LE)`,**CRC 覆盖 TYPE..PAYLOAD 共 4+payLen 字节**;OTA 为双 bank 直写、END 整镜像 zlib CRC32 + 向量表 bank 校验后更新 bootsetting 并复位。OTA 传输层通道无关:BLE e0005(逐帧锁步)/ UART 二进制模式(CLI `ota`;V1.00.24 起**流式模式**:BEGIN 16B flags、8KB 窗口流水线、扇区边界 ack、go-back-N,旧固件自动回退锁步)/ 旧帧协议通道,详见 ble_protocol.md §6.6/§6.7。
|
||||
- 连接后设备主动发起 MTU=247 交换;失败退化为默认 20B/包,协议按字节流重组,两种情形都正确。
|
||||
- 信息项 id:0x01 固件版本 u32 / 0x02 芯片温度 i16(0.1°C,ADC CH7) / 0x03 风扇转速 u16(0xFFFF=无硬件,`app_fan_get_rpm()` 弱符号,产品板重写即可) / 0x04 电压 u16(mV,ADC CH6) / 0x05 运行时长 u32(s) / 0x06 剩余堆 u32(B) / 0x07 当前运行 bank u8(1=APP1,2=APP2,OTA 选包依据)。
|
||||
- 调试手段:CLI 命令 `blelog on` 后,串口打印每个收/发协议帧 hex dump、连接/断开、CCCD 订阅事件(走 printf,不经 BLE 通道,避免自激)。
|
||||
@ -148,17 +148,20 @@ mcm-ddc-ble/
|
||||
|
||||
## PC 工具(tools/)
|
||||
|
||||
- `merge_image.py` — 纯 Python 无三方依赖;合并 boot+bootsetting(缺省记录,脚本生成)+APP 为整片 hex/bin,同时产出 `_ota.bin`/`_ota.json`(bank1 载荷);`--ota-app2` 追加 bank2 载荷 `_ota_app2.bin/.json`;`--app2 <bin> --with-appdata` 生成双 bank 整包(make_dual_package.bat 使用);自动从 `app_version.h` 取 `APP_FW_VERSION_NUM` 写入清单(含 `target_bank` 字段)
|
||||
- `make_package.bat` / `flash_package.bat` — 一键出包 / NSpyocd 整片烧录(脚本会自动找 Python312 全路径,本机 `python` 可能是商店占位 stub)
|
||||
**自包含生产包(§49)**:tools/ 内嵌 Python 运行时(`python-embed/`,bleak+pyserial 预装)+ NSpyocd 烧录器副本 + DFP pack,`make_release.bat` 一键打 zip 到 `out/mothercup_tools_<时间戳>.zip`——解压到任何 Windows 电脑即可用,无需安装 Python/任何环境。三件套不入库(gitignore):`python-embed/`、`NSpyocd/`、`N32WB03x_DFP.1.4.0.pack`;**bat 文件必须保持 CRLF 行尾**(LF 会导致 cmd 解析错乱)。Python 解析优先级:`python-embed` → `.venv-ble`(开发机自动建)→ 系统 Python。
|
||||
|
||||
- `merge_image.py` — 纯 Python 无三方依赖;合并 boot+bootsetting(缺省记录,脚本生成)+APP 为整片 hex/bin,同时产出 `_ota.bin`/`_ota.json`(bank1 载荷);`--ota-app2` 追加 bank2 载荷 `_ota_app2.bin/.json`;`--app2 <bin> --with-appdata` 生成双 bank 整包(make_dual_package.bat 使用);自动从 `app_version.h` 取 `APP_FW_VERSION_NUM` 写入清单(含 `target_bank` 字段);**出包硬卡:镜像初始 SP 必须在 (0x20004000, 0x2000C000](SRAM 悬崖,§47)**
|
||||
- `make_package.bat` / `flash_package.bat` — 一键出包 / NSpyocd 整片烧录(flash_package.py 流式转发输出并把厂商横幅替换为 "CAIIC NSLINK UMP")
|
||||
- `make_dual_package.bat` — 双 APP 整包(Boot+bootsetting 双 bank+APP_DATA+APP1+APP2,APP2 升一版链接 0x01024000),用于 appsw 切换测试
|
||||
- `ble_cli_test.py` + `ble_cli.bat` — PC 端 bleak 蓝牙 CLI 测试客户端(帧协议模式 + `-rw` 读写特征模式,交互模式 `-i`,venv 在 `tools/.venv-ble`,首次运行 bat 自动创建)
|
||||
- `ble_ota_update.py` + `ble_ota.bat` — BLE OTA 升级器(解析单文件升级包 `mothercup_ble_ota.bin`、按 CUR_BANK 选对侧 bank 载荷、经 OTA 特征 `...e0005` 逐帧锁步、复位检测+重连校验;`--uart COMx` 走串口二进制模式:握手标记进入/ABORT 退出/丢帧锁步重传再同步)。**BLE 与 UART 双通道均已实测通过(§42/§44)**
|
||||
- `ble_cli_test.py` + `ble_cli.bat` — PC 端 bleak 蓝牙 CLI 测试客户端(帧协议模式 + `-rw` 读写特征模式,交互模式 `-i`,venv 在 `tools/.venv-ble`,不入库;首次运行 bat 按 `tools/requirements-ble.txt`(bleak+pyserial)自动创建)
|
||||
- `ble_ota_update.py` + `ble_ota.bat` — BLE OTA 升级器(解析单文件升级包 `mothercup_ble_ota.bin`、按 CUR_BANK 选对侧 bank 载荷、经 OTA 特征 `...e0005` 逐帧锁步、复位检测+重连校验;`--uart COMx` 走串口二进制模式(460800 8N1,V1.00.26 起):握手标记进入/ABORT 退出;V1.00.24 起默认流式(8KB 窗口+扇区 ack+go-back-N),`--lockstep` 或旧固件自动回退锁步)。**BLE 与 UART 双通道均已实测通过(§42/§44),流式见 §47/§48**
|
||||
- `ble_temp_watch.py` — 温度监测小工具
|
||||
|
||||
## 实时架构约定(固件)
|
||||
|
||||
- 使用 FreeRTOS V9.0.0 原生 API,**不使用 cmsis_os 封装**。
|
||||
- 任务:BLE 调度任务(栈 512 字,优先级 2,循环 `rwip_schedule(); vTaskDelay(1ms);`——**ke_msg/ke_timer 只允许在该任务上下文运行**);LED 任务(LED1 闪烁半周期读 APP_DATA 参数 `led1_blink_ms`);CliTask(优先级 3);CLI 输出经环形缓冲由 BLE 任务上下文发 notify(`ble_up.c`)。`configTOTAL_HEAP_SIZE = 20KB`(heap_4)。
|
||||
- **IWDG 看门狗(V1.00.27 起)**:4s 超时,idle hook 仅在 3s 内有任务心跳时喂狗(BLE/LED/CLI 任务循环打点 `app_wdt_heartbeat()`);SWD halt 时冻结(DBG_IWDG_STOP);banner 打印复位原因;`hang` 命令可验证点火。新增任务请在循环里打心跳。
|
||||
- 任务:BLE 调度任务(栈 512 字,优先级 2,循环 `rwip_schedule(); vTaskDelay(1ms);`——**ke_msg/ke_timer 只允许在该任务上下文运行**);LED 任务(LED1 闪烁半周期读 APP_DATA 参数 `led1_blink_ms`);CliTask(优先级 3);CLI 输出经环形缓冲由 BLE 任务上下文发 notify(`ble_up.c`)。`configTOTAL_HEAP_SIZE = 18KB`(heap_4;SRAM 悬崖约束见 Flash/RAM 布局铁律)。
|
||||
- SysTick 由 FreeRTOS port 接管:`main` 中不要手动 `SysTick_Config`,`n32wb03x_it.c` 中不要重复定义 SVC/PendSV/SysTick_Handler。
|
||||
- USART1_IRQn 优先级为 3(最低),因 ISR 内调用 FreeRTOS FromISR API,必须 ≥ `configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY`(=3)。BLE_SW/FIFO IRQ 优先级 0,其 ISR 内禁止调用 FromISR API。
|
||||
- 串口输出并发:多任务/CLI 输出分别持有 `bsp_usart_tx_lock()` 互斥锁;调度器启动前为空操作。不要在中断或多个任务里直接 printf。
|
||||
|
||||
@ -149,7 +149,7 @@ OTA 会话(§6 的 BEGIN/DATA/END/ABORT → OTA_RSP)跑在 0xCA 帧上,帧
|
||||
| 通道 | 下行(主机→设备) | 上行(设备→主机) |
|
||||
|---|---|---|
|
||||
| **BLE OTA 特征** `...e0005` | 每次"写带响应"携带一帧(≤ att_mtu−3) | 写完**读同一特征**取回 OTA_RSP 帧;读应答可能先于设备处理完成返回空,**读需轮询至非空**(擦扇区时数十 ms) |
|
||||
| **UART 串口**(115200 8N1) | CLI 先发文本命令 `ota`,**等到设备回标记行 `[ota] binary mode ON` 再发帧**(握手,防帧被当文本命令吃掉),随后原始帧字节流 | OTA_RSP 帧直接从 TX 发出;**OTA_ABORT 立即退出**回 CLI,10s 无帧兜底退出 |
|
||||
| **UART 串口**(460800 8N1,V1.00.26 起;之前为 115200) | CLI 先发文本命令 `ota`,**等到设备回标记行 `[ota] binary mode ON` 再发帧**(握手,防帧被当文本命令吃掉),随后原始帧字节流 | OTA_RSP 帧直接从 TX 发出;**OTA_ABORT 立即退出**回 CLI,10s 无帧兜底退出 |
|
||||
|
||||
UART 模式切换与容错(V1.00.21 起):
|
||||
|
||||
@ -181,6 +181,42 @@ PC 参考实现:`tools\ble_ota_update.py`(`ble_ota.bat`;BLE 默认,
|
||||
收到 END 应答;收不到应视为链路异常,但仍可按"等待重启后重连复核 CUR_BANK
|
||||
与版本"兜底(PC 工具两种处理都保留)。
|
||||
|
||||
## 6.7 UART 流式传输模式(V1.00.24 起)
|
||||
|
||||
动机:逐帧锁步在 115200 下实测只有 ~1.5 kB/s(每帧一个 RTT + 主机调度
|
||||
抖动)。流式模式让 DATA 帧在窗口内连续下发:**115200 下实测 9.5 kB/s
|
||||
(55.8KB 约 5.9s,0 重传);460800(V1.00.26 起默认)下实测 23 kB/s
|
||||
(约 2.6s)**,只受波特率与扇区擦除时间限制。
|
||||
|
||||
**前提(固件侧)**:UART RX 改为 **DMA 环形缓冲**(2KB,循环模式)——
|
||||
DMA 在关中断的扇区擦除窗口内继续接收字节,这是流式不丢包的关键;
|
||||
CLI 文本输入路径不变(同一 `bsp_usart_read_byte()` 接口,行编辑/历史/
|
||||
Tab 行为完全一致),BLE 下行注入 CLI 走独立的注入队列。
|
||||
|
||||
协商:OTA_BEGIN payload 扩展为 **16B** `{total_size, image_crc32,
|
||||
version, flags u32}`,`flags` bit0 = STREAM。仅 UART 通道生效(BLE 通道
|
||||
应答靠读轮询,仍逐帧锁步)。兼容性:
|
||||
|
||||
- 12B 旧格式 BEGIN = 锁步模式(旧主机不受影响);
|
||||
- 旧固件(< V1.00.24)收到 16B BEGIN 回 `bad_frame`(status=1),主机据此
|
||||
自动回退锁步(PC 工具已实现;`--lockstep` 可强制)。
|
||||
|
||||
流式规则:
|
||||
|
||||
- DATA 帧格式不变(payload ≤480B,参考实现用 240B/帧),主机**连续发,
|
||||
不等逐帧 ack**;在途未确认字节数 ≤ **8KB(窗口)**。
|
||||
- 设备 ack 策略:DATA 仅在**跨越 4KB 扇区边界**(该扇区的惰性擦除此时
|
||||
已完成)或到达最后一字节时回 `OTA_RSP(ok, offset)`。
|
||||
- 丢帧/坏帧恢复(go-back-N):重组器静默丢弃坏帧后,下一帧 offset 必然
|
||||
不匹配,设备**立即**回一帧 `BAD_STATE + 期望 offset`(节流:重新对齐
|
||||
前只回一次);主机回退到该 offset 续传,无需重开会话。
|
||||
- ack 整体丢失:主机 **3s 无 ack 进展** → 回退到最后确认的 offset 重发。
|
||||
- END / ABORT 保持每帧必答(锁步语义不变);10s 空闲兜底退出二进制模式、
|
||||
END 应答送达后再复位等既有机制(V1.00.20/21)不受影响。
|
||||
|
||||
主机参考实现:`tools\ble_ota_update.py` 的 `ota_session_uart_stream()`
|
||||
(`--uart` 默认流式,`--lockstep` 回旧模式)。
|
||||
|
||||
## 6.5 维护命令:bootsetting 与 APP_DATA 参数区(V1.00.09 起,结构化访问;V1.00.10 起参数命令为 appget/appset;V1.00.11 起新增 reset/factory/uartrst/uartinfo)
|
||||
|
||||
bootsetting 与 APP_DATA 保留区(0x01006000/8KB)的读写以 CLI 命令形式提供,
|
||||
@ -196,8 +232,9 @@ bootsetting 与 APP_DATA 保留区(0x01006000/8KB)的读写以 CLI 命令形
|
||||
| `appsw [1\|2]` | 切换运行 bank 并复位(V1.00.15 起):无参切对侧 bank;切前校验 bootsetting 记录、目标 bank 记录一致性,并复算目标镜像 CRC32 与记录比对,不匹配拒绝(坏 bank 需先跑 OTA 写入有效镜像) |
|
||||
| `reset` | 复位单板:应答后延时 200ms 执行 `NVIC_SystemReset()`,Boot 按 active bank 跳转 |
|
||||
| `factory` | 恢复出厂设置:APP_DATA 参数区恢复缺省值并落盘后复位(**不动 bootsetting**) |
|
||||
| `uartrst` | 复位串口:USART1 按 115200 8N1 重新初始化并清空 RX 队列 |
|
||||
| `uartrst` | 复位串口:USART1 按 460800 8N1 重新初始化并清空 RX 队列 |
|
||||
| `uartinfo` | 查询串口参数:引脚(PB6/PB7 AF4)、波特率/数据位/校验/停止位、TX DMA/RX 中断形态 |
|
||||
| `hang` | 挂死 CLI 任务(IWDG 看门狗点火测试,V1.00.27 起;设备 ~4s 后复位) |
|
||||
|
||||
参数记录(APP_DATA 区头,52B):magic `0xCA12DA7A` + layout_ver + 参数字段 +
|
||||
reserved[8] + CRC32;记录无效时固件以缺省值运行,`appset` 时落盘。
|
||||
|
||||
114
docs/开发日志.md
114
docs/开发日志.md
@ -1171,3 +1171,117 @@ APP2 链接的镜像发向 APP1 区。设备端 END 校验(向量表 Reset PC
|
||||
重传/再同步提示前补换行不与进度条混行;END 总结新增
|
||||
`N resent, N resynced` 统计(链路质量一目了然:BLE 实测 0 次,
|
||||
接触不良的串口线 4~6 次)。BEGIN/END 阶段切换增加明示打印。
|
||||
|
||||
## 47. UART OTA 流式传输层 + RX 改 DMA 环形缓冲(2026-09-05,V1.00.24)
|
||||
|
||||
- **动机**:UART OTA 逐帧锁步实测仅 ~1.5 kB/s(55KB 要 39s),瓶颈是每帧
|
||||
一个 RTT(设备处理 + 主机调度抖动),不是波特率。
|
||||
- **UART RX 改 DMA 环形缓冲**(bsp_usart.c):DMA_CH2 循环模式 + 2KB ring,
|
||||
USART1_RX remap;RXDNE 逐字节中断关闭,改开 IDLEF 中断只做信号量唤醒
|
||||
(`bsp_usart_read_byte()` 接口不变,20ms 切片等信号量兜底连续流无 IDLE
|
||||
的情形;BLE 下行注入 CLI 改走独立的 64B 注入队列,优先于 ring 消费)。
|
||||
**关键收益:DMA 在关中断的扇区擦除窗口(数十 ms)内继续收字节**
|
||||
(2KB ≈ 173ms@115200),这是流式不丢包的前提——中断驱动 RX 在擦除期间
|
||||
必丢字节,正是当年只能锁步的根因。CLI 文本模式行为不变。
|
||||
- **流式传输层(ble_protocol.md §6.7)**:OTA_BEGIN 扩展 16B(+flags u32,
|
||||
bit0=STREAM,仅 UART 通道生效);DATA 不再逐帧 ack,只在跨越 4KB 扇区
|
||||
边界(擦除已完成)或最后一字节时回 ack;主机 8KB 窗口流水线发送;
|
||||
丢帧靠既有的 BAD_STATE+期望 offset 立即回执回退续传(go-back-N,设备侧
|
||||
节流为对齐前只报一次);3s 无 ack 进展主机自动回退到最后的 ack 重发;
|
||||
END/ABORT 语义不变。12B BEGIN=锁步,新旧固件/主机任意组合兼容(旧固件
|
||||
对 16B BEGIN 回 bad_frame,PC 工具自动回退;`--lockstep` 强制旧模式)。
|
||||
- PC 工具 `ble_ota_update.py` 新增 `ota_session_uart_stream()`;
|
||||
BLE 路径与 App(e0005)路径完全不受影响。
|
||||
- 预期速率:115200 下 ~10 kB/s(55KB 约 6s,原 39s)。
|
||||
|
||||
### §47 补记:V1.00.24 首版烧录即锁死——SRAM 顶部悬崖(同日下午修复)
|
||||
|
||||
- **现象**:首版 V1.00.24 烧录后板子无 banner 无广播。SWD 连上看:CPU 处于
|
||||
lockup,LR 落在 HardFault_Handler 内(fault 中再 fault)。
|
||||
- **定位**:用 NSpyocd commander 逐地址探测发现 **SRAM 从 0x2000C000 起读即
|
||||
TransferFault**(复位后立刻探也一样),最后一个可访问字是 0x2000BFFC——
|
||||
该 256KB 硅片 APP 可用 SRAM 实为 **32KB(0x20004000~0x2000BFFF)**,
|
||||
顶部 16KB 块并不可用(IRAM 配置里 0xC000=48KB 的上限是虚的)。V1.00.23
|
||||
的栈顶是 0x2000B9D8 恰好在线内;本次新增的 2KB DMA ring 把 ZI 顶高,
|
||||
栈顶漂到 0x2000C1E8 越线 → 上电第一批压栈即炸。**教训:任何 ZI/RW
|
||||
膨胀都必须复查 map 的 `__initial_sp`**。
|
||||
- **修复**:FreeRTOS heap 20KB→18KB(实测余量 12.6KB,够用),栈顶回到
|
||||
0x2000B9E8;`merge_image.py` 增加硬卡:bin 向量表首字(初始 SP)不在
|
||||
(0x20004000, 0x2000C000] 内直接出包失败,距悬崖 <1KB 告警。
|
||||
- **验证**:重烧后 SWD 观测 reset→go→延时,CPU 持续 Running、PC 在 APP1
|
||||
正常代码区推进,HardFault 断点不再命中。
|
||||
- AGENTS.md "RAM 铁律"已改写为双侧悬崖(低 16KB ROM 占用 + 顶部
|
||||
0x2000C000 悬崖)。
|
||||
|
||||
### §47 再补记:流式首测无 ack 的两个叠加 bug(当日下午二修,已实测通过)
|
||||
|
||||
- **现象**:V1.00.25 首版流式 OTA 主机侧 0 ack,反复 stall 回退。
|
||||
- **定位过程**(SWD 在线取证,值得记一笔):commander 读 `s_offset`/
|
||||
`s_erased`/`s_prog` 发现设备把 4800B 突发全部正确收写(环形缓冲内容
|
||||
与主机发送字节流逐字节一致,DMA RX 无丢字节);OTA 状态机正常、erase
|
||||
正常、TX DMA 通道空闲未挂死——唯一没发生的就是 ack 本身。
|
||||
- **根因 1(V1.00.24 版)**:扇区 ack 写成"`s_offset` 恰好是 4096 倍数才
|
||||
ack",240B/帧时偏移序列 4080→4320 永远踩不中边界。
|
||||
- **根因 2(V1.00.25 首版)**:改成跨界判断时用了 `dlen`——但 `dlen`
|
||||
在上面的 4B 对齐暂存逻辑里已被消耗成 0~3 的尾巴,`(s_offset-0)` 与
|
||||
`s_offset` 永远同扇区,ack 永不触发。修复:用帧原始长度 `chunk` 做
|
||||
跨界判断(`app_ota.c ota_on_data`)。
|
||||
- **实测**:双向各跑一次,均 5.9s 完成(**9.5 kB/s,0 rewinds**,锁步时
|
||||
39s/1.4kB/s),复位复核 PASS(APP1→APP2→APP1,版本 V1.00.25)。
|
||||
- 附:flash_package 改 python 包装(tools/flash_package.py)流式转发
|
||||
NSpyocd 输出并替换横幅为 "CAIIC NSLINK UMP";块状读取修进度条 \r
|
||||
不刷新问题;模式替换需在拼接缓冲上做(跨块边界会劈开模式串)。
|
||||
|
||||
## 48. UART 波特率提升 115200→460800(2026-09-05,V1.00.26)
|
||||
|
||||
- 固件 `BSP_USART_BAUDRATE` 460800(64MHz/460800 分频误差 +0.03%);
|
||||
`uartrst`/`uartinfo` 走宏自动跟随,帮助文本同步。
|
||||
- **环形缓冲 2KB→3KB**:460800 下 2KB 只能覆盖 44ms,小于擦扇区关中断
|
||||
窗口(~45ms);3KB ≈ 69ms。注意 SRAM 悬崖(§47):栈顶上移 1KB 至
|
||||
0x2000BDE8,merge_image.py 硬卡给出 <1KB 告警(属预期内)。
|
||||
- PC 工具默认波特率同步(ble_ota_update.py UartTransport、uart_cap.py)。
|
||||
- 实测:流式 OTA 55832B **2.6s(23 kB/s)**,途中丢 1 帧由 go-back-N
|
||||
自动回退续传( rewind 45360→38160),复位复核 PASS;CLI 文本模式
|
||||
在 460800 下正常(devinfo/ota 握手/复位后复核全走文本 CLI)。
|
||||
- 对比:115200 锁步 39s → 115200 流式 5.9s → 460800 流式 2.6s。
|
||||
- 注意:串口终端/工具必须同步改 460800,否则 CLI 全是乱码。
|
||||
|
||||
## 49. tools 自包含生产包(2026-09-05,无固件改动)
|
||||
|
||||
- 目标:tools/ 打包即可发布到任何 Windows 产线电脑,无需安装任何环境。
|
||||
- **内嵌 Python 运行时**:`tools/python-embed/`(python-3.12.10-embed-amd64,
|
||||
华为云镜像下载;python312._pth 启用 Lib/site-packages + import site,
|
||||
bleak 3.0.2/pyserial 3.5 用 pip --target 预装,29MB)。bat 优先级:
|
||||
python-embed → .venv-ble(开发机自动建)→ 系统 Python。
|
||||
- **烧录器收编**:NSpyocd.exe + DFP pack 复制进 tools/(flash_package.py
|
||||
优先用 tools 内副本,找不到才回退仓库 nations-tec 路径)。
|
||||
- **一键打包**:`make_release.bat`(PowerShell Compress-Archive,排除
|
||||
.venv-ble/__pycache__,zip 暂存 %TEMP% 再移入 out/——直接写 out/ 会
|
||||
自读自写报错)。
|
||||
- **坑**:bat 必须 CRLF 行尾——LF-only 时 cmd 对括号块/%~dp0 的解析会
|
||||
错乱(standalone 复测时 venv 分支被误触发且 PYREAL 为空),本次全部
|
||||
转 CRLF。zip 用 Windows 资源管理器/Expand-Archive 解压(Info-ZIP
|
||||
unzip 对 Compress-Archive 的反斜杠分隔符不兼容)。
|
||||
- 验证:zip 解到仓库外独立目录,ble_ota.bat --uart COM4 全流程 PASS
|
||||
(2.2s,27 kB/s,0 重传);flash_package.bat 用包内 NSpyocd 烧录 PASS。
|
||||
|
||||
## 50. 独立看门狗 IWDG(2026-09-05,V1.00.27)
|
||||
|
||||
- 起因:此前 CLI 卡死事故中 `reset` 命令无从执行(任务已死),系统无自救
|
||||
手段。固件原本没有看门狗。
|
||||
- 复位路径复查:当前固件 `reset`/`factory`/`appsw`/OTA 延迟复位均实测正常
|
||||
(NVIC_SystemReset 有效);用户报告的"复位没生效"发生于 CLI 卡死状态,
|
||||
命令根本无法到达执行路径——这正是看门狗的场景。
|
||||
- 设计(main.c / app_wdt.h,驱动 n32wb03x_iwdg.c 已加入全部 4 个 target):
|
||||
- IWDG:LSI 32kHz /128 → 250Hz,reload 1000 = **4s 超时**;
|
||||
- **喂养策略**:FreeRTOS idle hook 只在"3s 内有任务心跳"时喂狗;BLE 调度/
|
||||
LED/CLI 三个任务循环里打点 `app_wdt_heartbeat()`。任何任务死锁/死循环
|
||||
(CLI 独占 CPU 饿死 idle,或全体阻塞 idle 空转无心跳)都会在 ≤4s 复位;
|
||||
- `DBG_ConfigPeriph(DBG_IWDG_STOP, ENABLE)`:SWD halt 时冻结 IWDG,
|
||||
调试会话不会被看门狗打断;
|
||||
- 启动 banner 打印复位原因(RCC IWDGRSTF)并清标志。
|
||||
- 新增调试命令 `hang`(CLI 任务忙等挂起,验证看门狗点火用)。
|
||||
- 实测:正常喂狗系统稳定(12s+ 无重启、CLI 正常);`hang` 后 4.4s 复位,
|
||||
新 banner 打印 "Last reset: IWDG watchdog!"。
|
||||
- 附带修正:`uartinfo` 输出里 RX 描述更新为 DMA ring(此前还是 RXDNE 队列
|
||||
的旧文案)。
|
||||
|
||||
@ -38,7 +38,7 @@ V1.0 / 2026-09-05
|
||||
- **RAM 铁律**:低 16KB(0x20000000~0x20003FFF)被芯片 ROM/BLE 子系统占用,
|
||||
任何工程 IRAM 执行区必须从 **0x20004000** 起(否则 BLE init 必 HardFault)
|
||||
- **SWD 引脚 SWCLK=PA4 / SWDIO=PA5,应用程序绝对禁止占用**
|
||||
- 板载 LED:LED1=PB0、LED2=PA6;USART1:TX=PB6 / RX=PB7(AF4,115200 8N1)
|
||||
- 板载 LED:LED1=PB0、LED2=PA6;USART1:TX=PB6 / RX=PB7(AF4,460800 8N1,V1.00.26 起)
|
||||
- 新增 GPIO 前核对开发日志 §4 引脚分配表
|
||||
- **不要直接写寄存器**;勘误修复必须先在 SDK 源码交叉验证地址用途
|
||||
(曾按勘误表写 AFEC 寄存器导致 BLE 射频挂死,开发日志 §28)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@ -532,6 +532,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_qflash.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>n32wb03x_iwdg.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_iwdg.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
@ -1688,6 +1693,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_qflash.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>n32wb03x_iwdg.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_iwdg.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
@ -2844,6 +2854,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_qflash.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>n32wb03x_iwdg.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_iwdg.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
@ -3709,6 +3724,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_qflash.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>n32wb03x_iwdg.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\..\nations-tec\N32WB03x_SDK_V2.0.0\firmware\n32wb03x_std_periph_driver\src\n32wb03x_iwdg.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
|
||||
@ -27,13 +27,18 @@
|
||||
|
||||
|
||||
#define configUSE_PREEMPTION 1
|
||||
#define configUSE_IDLE_HOOK 0
|
||||
#define configUSE_IDLE_HOOK 1
|
||||
#define configUSE_TICK_HOOK 0
|
||||
#define configCPU_CLOCK_HZ ( SystemCoreClock )
|
||||
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
|
||||
#define configMAX_PRIORITIES ( 7 )
|
||||
#define configMINIMAL_STACK_SIZE ( ( uint16_t ) 128 )
|
||||
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 20 * 1024 ) )
|
||||
/* Usable app SRAM on this silicon is 32KB (0x20004000..0x2000BFFF; accesses
|
||||
* above fault - the top 16KB block is not available to the APP, see dev log
|
||||
* §47). The stack top (__initial_sp) must stay below 0x2000C000, so
|
||||
* RW+ZI+STACK must fit 32KB; heap sized accordingly (12.6KB free was
|
||||
* observed at V1.00.23 with a 20KB heap, so 2KB less is safe). */
|
||||
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 18 * 1024 ) )
|
||||
#define configMAX_TASK_NAME_LEN ( 16 )
|
||||
#define configUSE_TRACE_FACILITY 1
|
||||
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
|
||||
|
||||
@ -9,12 +9,12 @@
|
||||
#define __APP_VERSION_H__
|
||||
|
||||
#ifndef APP_FW_VERSION
|
||||
#define APP_FW_VERSION "V1.00.23"
|
||||
#define APP_FW_VERSION "V1.00.27"
|
||||
#endif
|
||||
|
||||
/* Numeric form used by the BLE info query / OTA records: 0x00MMmmpp */
|
||||
#ifndef APP_FW_VERSION_NUM
|
||||
#define APP_FW_VERSION_NUM 0x00010017u
|
||||
#define APP_FW_VERSION_NUM 0x0001001Bu
|
||||
#endif
|
||||
|
||||
#endif /* __APP_VERSION_H__ */
|
||||
|
||||
32
mcm-ddc-ble/inc/app_wdt.h
Normal file
32
mcm-ddc-ble/inc/app_wdt.h
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @file app_wdt.h
|
||||
* @brief Independent watchdog (IWDG, LSI 32kHz): 4s timeout, fed from the
|
||||
* FreeRTOS idle hook only while at least one application task has
|
||||
* heartbeated within the last 3s. Frozen while the core is halted
|
||||
* (DBG_IWDG_STOP) so SWD debug sessions do not trigger resets.
|
||||
*/
|
||||
#ifndef __APP_WDT_H__
|
||||
#define __APP_WDT_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Report the reset cause (banner) and start the IWDG.
|
||||
* Call once from main() after the USART banner.
|
||||
*/
|
||||
void app_wdt_init(void);
|
||||
|
||||
/**
|
||||
* @brief Mark a task as alive. Called from the BLE schedule / LED / CLI
|
||||
* task loops; the idle hook stops feeding the IWDG when no task
|
||||
* heartbeated for APP_WDT_HB_TIMEOUT_MS.
|
||||
*/
|
||||
void app_wdt_heartbeat(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __APP_WDT_H__ */
|
||||
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file bsp_usart.h
|
||||
* @brief USART1 support: printf retarget (polling TX), DMA TX, RX interrupt queue.
|
||||
* TX=PB6, RX=PB7, 115200 8N1.
|
||||
* @brief USART1 support: printf retarget (polling TX), DMA TX, RX DMA
|
||||
* circular ring + IDLE interrupt. TX=PB6, RX=PB7, 460800 8N1.
|
||||
*/
|
||||
#ifndef __BSP_USART_H__
|
||||
#define __BSP_USART_H__
|
||||
@ -20,12 +20,17 @@ extern "C" {
|
||||
#define BSP_USART_RX_PIN GPIO_PIN_7
|
||||
#define BSP_USART_TX_GPIO_AF GPIO_AF4_USART1
|
||||
#define BSP_USART_RX_GPIO_AF GPIO_AF4_USART1
|
||||
#define BSP_USART_BAUDRATE 115200
|
||||
#define BSP_USART_BAUDRATE 460800
|
||||
|
||||
/* USART1 TX DMA channel (remap required on this chip) */
|
||||
#define BSP_USART_TX_DMA_CH DMA_CH1
|
||||
#define BSP_USART_TX_DMA_TC DMA_FLAG_TC1
|
||||
#define BSP_USART_TX_DMA_REMAP DMA_REMAP_USART1_TX
|
||||
/* USART1 RX DMA channel: circular mode into a 2KB ring, drained by
|
||||
* bsp_usart_read_byte(). DMA keeps sampling while interrupts are off
|
||||
* (flash erase windows during UART OTA streaming). */
|
||||
#define BSP_USART_RX_DMA_CH DMA_CH2
|
||||
#define BSP_USART_RX_DMA_REMAP DMA_REMAP_USART1_RX
|
||||
#define BSP_USART_DAT_ADDR (USART1_BASE + 0x04)
|
||||
|
||||
/* Pass to bsp_usart_read_byte() to block without timeout */
|
||||
@ -40,11 +45,13 @@ void bsp_usart_set_baud(uint32_t baud);
|
||||
/* TX: send a buffer through DMA (polled completion) */
|
||||
void bsp_usart_write_dma(const uint8_t* data, uint16_t len);
|
||||
|
||||
/* RX: fetch one byte received by the RXDNE interrupt; returns 1 on success */
|
||||
/* RX: fetch one received byte. Source order: the inject queue (BLE
|
||||
* downlink feeding the CLI) first, then the DMA RX ring. Returns 1 on
|
||||
* success, 0 on timeout. */
|
||||
int bsp_usart_read_byte(uint8_t* ch, uint32_t timeout_ms);
|
||||
|
||||
/* RX: inject bytes from task context into the same RX queue (BLE downlink
|
||||
* feeds the CLI through this). Bytes are dropped when the queue is full. */
|
||||
/* RX: inject bytes from task context (BLE downlink feeds the CLI through
|
||||
* this). Bytes are dropped when the inject queue is full. */
|
||||
void bsp_usart_rx_inject(const uint8_t* data, uint16_t len);
|
||||
|
||||
/* TX mutex so that DMA bursts and polling printf lines do not interleave.
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
#include "cli_core.h"
|
||||
#include "bsp_usart.h"
|
||||
#include "app_ota.h"
|
||||
#include "app_wdt.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
@ -245,12 +246,14 @@ static void CliTask(void* argument)
|
||||
|
||||
for (;;)
|
||||
{
|
||||
app_wdt_heartbeat();
|
||||
/* OTA binary frame mode: raw bytes to the OTA channel.
|
||||
* otaIdleMs is (re)armed on every entry - a leftover count from a
|
||||
* previous session must not trip an instant idle timeout. */
|
||||
otaIdleMs = 0;
|
||||
while (s_otaMode)
|
||||
{
|
||||
app_wdt_heartbeat();
|
||||
if (bsp_usart_read_byte(&ch, OTA_MODE_POLL_MS) != 0)
|
||||
{
|
||||
if (app_ota_chan_rx_uart(ch) != 0)
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
* @brief OTA receiver implementation (channel-agnostic: BLE OTA
|
||||
* characteristic, UART binary mode, legacy notify frame path).
|
||||
*
|
||||
* Flow: OTA_BEGIN (size/crc/version) -> OTA_DATA* (strictly sequential
|
||||
* offsets, EVERY frame acked with the new offset - lockstep flow control)
|
||||
* -> OTA_END (flush the sub-word tail, verify whole-image CRC32 against
|
||||
* flash, verify the vector table targets the right bank, update
|
||||
* bootsetting, reset). OTA_ABORT aborts the session.
|
||||
* Flow: OTA_BEGIN (size/crc/version[/flags]) -> OTA_DATA* (strictly sequential
|
||||
* offsets; lockstep acks per frame, or sector-boundary acks in UART stream
|
||||
* mode - ble_protocol.md §6.7) -> OTA_END (flush the sub-word tail, verify
|
||||
* whole-image CRC32 against flash, verify the vector table targets the right
|
||||
* bank, update bootsetting, reset). OTA_ABORT aborts the session.
|
||||
*
|
||||
* Direct-write design (no 4KB staging buffer):
|
||||
* - Sectors of the target bank are erased lazily: the first received byte
|
||||
@ -89,6 +89,17 @@ static uint32_t ota_rd32(const uint8_t* p)
|
||||
|
||||
static app_ota_rsp_sink_fn s_rsp_sink;
|
||||
|
||||
static void ota_uart_sink(const uint8_t* frame, uint16_t len);
|
||||
|
||||
/* UART streaming mode (ble_protocol.md §6.7): OTA_BEGIN payload extended
|
||||
* to 16B {size, crc32, version, flags}; flags bit0 = stream. In stream mode
|
||||
* DATA frames are acked only at 4KB sector boundaries (the erase has
|
||||
* completed by then) instead of per-frame lockstep; the host pipelines
|
||||
* writes with an 8KB window and rewinds on BAD_STATE. BLE stays lockstep. */
|
||||
#define OTA_BEGIN_FLAG_STREAM 0x01u
|
||||
static uint8_t s_stream; /* current session is streaming */
|
||||
static uint8_t s_bad_state_sent; /* throttle repeated BAD_STATE reports */
|
||||
|
||||
/**
|
||||
* @brief CRC16-CCITT (poly 0x1021, init 0xFFFF) - same as the frame
|
||||
* protocol's proto_crc16 (app_ble_proto.c).
|
||||
@ -157,6 +168,8 @@ void app_ota_abort(void)
|
||||
s_prog = 0;
|
||||
s_erased = 0;
|
||||
s_word_fill = 0;
|
||||
s_stream = 0;
|
||||
s_bad_state_sent = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -195,14 +208,15 @@ static uint8_t ota_program(const uint8_t* data, uint32_t len)
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* @brief OTA_BEGIN: {total_size u32, image_crc32 u32, version u32}.
|
||||
* @brief OTA_BEGIN: {total_size u32, image_crc32 u32, version u32}
|
||||
* or the extended 16B form {..., flags u32} (ble_protocol.md §6.7).
|
||||
*/
|
||||
static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
{
|
||||
uint32_t total, crc, version;
|
||||
uint32_t total, crc, version, flags;
|
||||
uint32_t self, target;
|
||||
|
||||
if (len != 12u)
|
||||
if (len != 12u && len != 16u)
|
||||
{
|
||||
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_BAD_FRAME, 0);
|
||||
return;
|
||||
@ -211,6 +225,7 @@ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
total = ota_rd32(p);
|
||||
crc = ota_rd32(p + 4);
|
||||
version = ota_rd32(p + 8);
|
||||
flags = (len == 16u) ? ota_rd32(p + 12) : 0;
|
||||
|
||||
/* One session at a time across all channels: a second BEGIN (e.g. BLE
|
||||
* while a UART session runs) is refused instead of corrupting state */
|
||||
@ -257,6 +272,12 @@ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
s_prog = 0;
|
||||
s_erased = 0;
|
||||
s_word_fill = 0;
|
||||
/* Streaming only on the UART channel: its RSP goes straight out on TX
|
||||
* and the DMA ring buffers inbound bytes during flash erases. The BLE
|
||||
* channel is read-polled and stays per-frame lockstep. */
|
||||
s_stream = ((flags & OTA_BEGIN_FLAG_STREAM) != 0u &&
|
||||
s_rsp_sink == ota_uart_sink) ? 1u : 0u;
|
||||
s_bad_state_sent = 0;
|
||||
|
||||
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_OK, 0);
|
||||
}
|
||||
@ -264,11 +285,13 @@ static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
/**
|
||||
* @brief OTA_DATA: {offset u32 + data}. Data must arrive strictly in order.
|
||||
* Payload is programmed to flash immediately (sectors are erased
|
||||
* lazily); EVERY frame is acked with the new offset (lockstep flow
|
||||
* control on the BLE/UART OTA channels; the erase time of a fresh
|
||||
* sector is covered by the ack round trip).
|
||||
* Errors are acked immediately and the state is
|
||||
* kept so the peer can resend.
|
||||
* lazily). Ack policy depends on the session mode:
|
||||
* - lockstep (BLE and legacy hosts): EVERY frame is acked with the
|
||||
* new offset, the ack round trip covers a fresh sector's erase;
|
||||
* - streaming (UART, BEGIN flags bit0): acks only at 4KB sector
|
||||
* boundaries (erase completed) and at the final byte; the host
|
||||
* pipelines frames inside an 8KB window and rewinds to the offset
|
||||
* reported by an immediate BAD_STATE ack when a frame is lost.
|
||||
*/
|
||||
static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
{
|
||||
@ -291,11 +314,21 @@ static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
off = ota_rd32(p);
|
||||
data = p + 4;
|
||||
dlen = (uint32_t)len - 4u;
|
||||
const uint32_t chunk = dlen; /* dlen is consumed down to the sub-word
|
||||
* tail by the staging logic below; the
|
||||
* stream-ack boundary check needs the
|
||||
* original chunk length */
|
||||
|
||||
if (off != s_offset)
|
||||
{
|
||||
/* out-of-order: report the expected offset, keep the session */
|
||||
/* out-of-order: report the expected offset, keep the session.
|
||||
* In stream mode the frames after a lost one ALL mismatch until the
|
||||
* host rewinds - report once, not per frame */
|
||||
if (!s_stream || !s_bad_state_sent)
|
||||
{
|
||||
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_BAD_STATE, s_offset);
|
||||
s_bad_state_sent = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (s_offset + dlen > s_total_size)
|
||||
@ -342,6 +375,22 @@ static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len)
|
||||
app_ota_abort();
|
||||
return;
|
||||
}
|
||||
s_bad_state_sent = 0; /* in-order again: re-arm the error report */
|
||||
if (s_stream)
|
||||
{
|
||||
/* Streaming (UART): ack when a 4KB sector boundary was CROSSED by
|
||||
* this chunk (its lazy erase has completed by then) or on the final
|
||||
* byte. A chunk rarely lands exactly on a boundary (e.g. 240B
|
||||
* frames step 4080->4320), so compare sector indices, not equality.
|
||||
* NOTE: use chunk (the original length), not dlen which the staging
|
||||
* logic has consumed to the sub-word tail. */
|
||||
if (((s_offset - chunk) & ~0xFFFu) != (s_offset & ~0xFFFu) ||
|
||||
s_offset == s_total_size)
|
||||
{
|
||||
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_OK, s_offset);
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* Lockstep ack: every DATA frame is answered with the new offset */
|
||||
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_OK, s_offset);
|
||||
}
|
||||
@ -493,8 +542,6 @@ void app_ota_reset_poll(void)
|
||||
}
|
||||
}
|
||||
|
||||
static void ota_uart_sink(const uint8_t* frame, uint16_t len);
|
||||
|
||||
static uint8_t s_uart_exit_req; /* OTA_ABORT on UART: drop back to CLI now */
|
||||
|
||||
void app_ota_handle_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uint16_t len)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file bsp_usart.c
|
||||
* @brief USART1 init, printf retarget (fputc, polling TX), DMA TX and
|
||||
* RX interrupt byte queue.
|
||||
* RX DMA circular ring (IDLE interrupt wakeup + polled drain).
|
||||
*/
|
||||
#include "bsp_usart.h"
|
||||
#include <stdio.h>
|
||||
@ -11,14 +11,65 @@
|
||||
#include "queue.h"
|
||||
#include "semphr.h"
|
||||
|
||||
#define RX_QUEUE_LENGTH 64
|
||||
#define RX_INJECT_QUEUE_LEN 64 /* BLE downlink -> CLI injection */
|
||||
#define RX_RING_SIZE 3072u /* DMA circular ring: ~69ms @460800,
|
||||
* covers flash-erase interrupt-off
|
||||
* windows (~45ms) during OTA streaming.
|
||||
* NOTE: keep the stack top below the
|
||||
* 0x2000C000 SRAM cliff (dev log §47) -
|
||||
* merge_image.py hard-fails otherwise */
|
||||
#define RX_WAIT_SLICE_MS 20u /* semaphore wait granularity (also the
|
||||
* poll period when the IDLE interrupt
|
||||
* does not fire during a back-to-back
|
||||
* byte stream) */
|
||||
|
||||
static QueueHandle_t s_rxQueue = NULL;
|
||||
static uint8_t s_rxRing[RX_RING_SIZE];
|
||||
static volatile uint16_t s_rxTail; /* software drain position in the ring */
|
||||
static QueueHandle_t s_rxInjectQueue = NULL;
|
||||
static SemaphoreHandle_t s_rxSem = NULL;
|
||||
static SemaphoreHandle_t s_txMutex = NULL;
|
||||
|
||||
/**
|
||||
* @brief Initialize USART1 (115200 8N1) on PB6(TX)/PB7(RX),
|
||||
* DMA TX channel and the RX interrupt queue.
|
||||
* @brief Start the USART1 RX DMA circular channel (shared by init and
|
||||
* baud re-apply).
|
||||
*/
|
||||
static void bsp_usart_rx_dma_start(void)
|
||||
{
|
||||
DMA_InitType DMA_InitStructure;
|
||||
|
||||
DMA_EnableChannel(BSP_USART_RX_DMA_CH, DISABLE);
|
||||
DMA_DeInit(BSP_USART_RX_DMA_CH);
|
||||
DMA_RequestRemap(BSP_USART_RX_DMA_REMAP, DMA, BSP_USART_RX_DMA_CH, ENABLE);
|
||||
|
||||
DMA_InitStructure.PeriphAddr = BSP_USART_DAT_ADDR;
|
||||
DMA_InitStructure.MemAddr = (uint32_t)s_rxRing;
|
||||
DMA_InitStructure.Direction = DMA_DIR_PERIPH_SRC;
|
||||
DMA_InitStructure.BufSize = RX_RING_SIZE;
|
||||
DMA_InitStructure.PeriphInc = DMA_PERIPH_INC_DISABLE;
|
||||
DMA_InitStructure.DMA_MemoryInc = DMA_MEM_INC_ENABLE;
|
||||
DMA_InitStructure.PeriphDataSize = DMA_PERIPH_DATA_SIZE_BYTE;
|
||||
DMA_InitStructure.MemDataSize = DMA_MemoryDataSize_Byte;
|
||||
DMA_InitStructure.CircularMode = DMA_MODE_CIRCULAR;
|
||||
DMA_InitStructure.Priority = DMA_PRIORITY_HIGH;
|
||||
DMA_InitStructure.Mem2Mem = DMA_M2M_DISABLE;
|
||||
DMA_Init(BSP_USART_RX_DMA_CH, &DMA_InitStructure);
|
||||
|
||||
s_rxTail = 0;
|
||||
DMA_EnableChannel(BSP_USART_RX_DMA_CH, ENABLE);
|
||||
USART_EnableDMA(BSP_USARTx, USART_DMAREQ_RX, ENABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief DMA ring head (hardware write position).
|
||||
*/
|
||||
static uint16_t bsp_usart_rx_head(void)
|
||||
{
|
||||
return (uint16_t)(RX_RING_SIZE - DMA_GetCurrDataCounter(BSP_USART_RX_DMA_CH));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize USART1 (460800 8N1) on PB6(TX)/PB7(RX),
|
||||
* DMA TX channel and the RX DMA ring.
|
||||
*/
|
||||
void bsp_usart_init(void)
|
||||
{
|
||||
@ -57,11 +108,13 @@ void bsp_usart_init(void)
|
||||
DMA_RequestRemap(BSP_USART_TX_DMA_REMAP, DMA, BSP_USART_TX_DMA_CH, ENABLE);
|
||||
USART_EnableDMA(BSP_USARTx, USART_DMAREQ_TX, ENABLE);
|
||||
|
||||
/* RX byte queue fed by the RXDNE interrupt */
|
||||
s_rxQueue = xQueueCreate(RX_QUEUE_LENGTH, sizeof(uint8_t));
|
||||
/* RX: DMA circular ring + IDLE interrupt wakeup */
|
||||
s_rxInjectQueue = xQueueCreate(RX_INJECT_QUEUE_LEN, sizeof(uint8_t));
|
||||
s_rxSem = xSemaphoreCreateBinary();
|
||||
s_txMutex = xSemaphoreCreateMutex();
|
||||
bsp_usart_rx_dma_start();
|
||||
|
||||
USART_ConfigInt(BSP_USARTx, USART_INT_RXDNE, ENABLE);
|
||||
USART_ConfigInt(BSP_USARTx, USART_INT_IDLEF, ENABLE);
|
||||
/* Lowest priority: the ISR calls FreeRTOS FromISR APIs */
|
||||
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
|
||||
NVIC_InitStructure.NVIC_IRQChannelPriority = 3;
|
||||
@ -72,10 +125,10 @@ void bsp_usart_init(void)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Re-apply the baud rate at runtime (keeps RX interrupt/DMA setup).
|
||||
* Waits for TX to drain, re-initializes the USART and flushes the
|
||||
* RX queue (stale bytes may have been sampled at the old baud rate).
|
||||
* Used by the "uartrst" CLI command.
|
||||
* @brief Re-apply the baud rate at runtime (keeps RX DMA ring setup).
|
||||
* Waits for TX to drain, re-initializes the USART, restarts the RX
|
||||
* DMA ring and flushes the inject queue (stale bytes may have been
|
||||
* sampled at the old baud rate). Used by the "uartrst" CLI command.
|
||||
*/
|
||||
void bsp_usart_set_baud(uint32_t baud)
|
||||
{
|
||||
@ -97,12 +150,13 @@ void bsp_usart_set_baud(uint32_t baud)
|
||||
USART_Init(BSP_USARTx, &USART_InitStructure);
|
||||
|
||||
/* Drop bytes received at the old baud rate */
|
||||
if (s_rxQueue != NULL && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING)
|
||||
bsp_usart_rx_dma_start();
|
||||
if (s_rxInjectQueue != NULL && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING)
|
||||
{
|
||||
xQueueReset(s_rxQueue);
|
||||
xQueueReset(s_rxInjectQueue);
|
||||
}
|
||||
|
||||
USART_ConfigInt(BSP_USARTx, USART_INT_RXDNE, ENABLE);
|
||||
USART_ConfigInt(BSP_USARTx, USART_INT_IDLEF, ENABLE);
|
||||
USART_Enable(BSP_USARTx, ENABLE);
|
||||
}
|
||||
|
||||
@ -148,37 +202,71 @@ void bsp_usart_write_dma(const uint8_t* data, uint16_t len)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Fetch one byte received by the RX interrupt.
|
||||
* @brief Fetch one received byte. The BLE-injected queue is drained first,
|
||||
* then the DMA RX ring. Blocking waits use the RX semaphore (poked by
|
||||
* the IDLE interrupt) in RX_WAIT_SLICE_MS slices so a continuous
|
||||
* byte stream without idle gaps is still drained in time.
|
||||
* @param ch output byte
|
||||
* @param timeout_ms BSP_USART_WAIT_FOREVER or a timeout in ms
|
||||
* @return 1 on success, 0 on timeout
|
||||
*/
|
||||
int bsp_usart_read_byte(uint8_t* ch, uint32_t timeout_ms)
|
||||
{
|
||||
TickType_t ticks;
|
||||
uint32_t waited = 0;
|
||||
|
||||
if (s_rxQueue == NULL)
|
||||
for (;;)
|
||||
{
|
||||
uint16_t head;
|
||||
|
||||
/* Injected bytes (BLE downlink -> CLI) take priority */
|
||||
if (s_rxInjectQueue != NULL &&
|
||||
xTaskGetSchedulerState() == taskSCHEDULER_RUNNING &&
|
||||
xQueueReceive(s_rxInjectQueue, ch, 0) == pdPASS)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
head = bsp_usart_rx_head();
|
||||
if (s_rxTail != head)
|
||||
{
|
||||
*ch = s_rxRing[s_rxTail];
|
||||
s_rxTail = (uint16_t)((s_rxTail + 1u) % RX_RING_SIZE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (timeout_ms == 0u ||
|
||||
(timeout_ms != BSP_USART_WAIT_FOREVER && waited >= timeout_ms))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (s_rxSem == NULL || xTaskGetSchedulerState() != taskSCHEDULER_RUNNING)
|
||||
{
|
||||
return 0; /* pre-scheduler: non-blocking snapshot only */
|
||||
}
|
||||
|
||||
ticks = (timeout_ms == BSP_USART_WAIT_FOREVER) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
|
||||
return (xQueueReceive(s_rxQueue, ch, ticks) == pdPASS) ? 1 : 0;
|
||||
uint32_t slice = RX_WAIT_SLICE_MS;
|
||||
if (timeout_ms != BSP_USART_WAIT_FOREVER && slice > timeout_ms - waited)
|
||||
{
|
||||
slice = timeout_ms - waited;
|
||||
}
|
||||
(void)xSemaphoreTake(s_rxSem, pdMS_TO_TICKS(slice));
|
||||
waited += slice;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Inject received bytes from task context into the RX queue.
|
||||
* @brief Inject received bytes from task context into the inject queue.
|
||||
* Used by the BLE downlink to feed the CLI. Drops on full queue.
|
||||
*/
|
||||
void bsp_usart_rx_inject(const uint8_t* data, uint16_t len)
|
||||
{
|
||||
if (s_rxQueue == NULL || xTaskGetSchedulerState() != taskSCHEDULER_RUNNING)
|
||||
if (s_rxInjectQueue == NULL || xTaskGetSchedulerState() != taskSCHEDULER_RUNNING)
|
||||
{
|
||||
return;
|
||||
}
|
||||
while (len--)
|
||||
{
|
||||
if (xQueueSend(s_rxQueue, data++, 0) != pdPASS)
|
||||
if (xQueueSend(s_rxInjectQueue, data++, 0) != pdPASS)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -208,21 +296,23 @@ void bsp_usart_tx_unlock(void)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RXDNE interrupt handler, pushes the received byte into the RX queue.
|
||||
* @brief IDLE line interrupt handler: the DMA ring holds the received bytes,
|
||||
* here we only wake a task blocked in bsp_usart_read_byte().
|
||||
* Called from USART1_IRQHandler (see n32wb03x_it.c).
|
||||
*/
|
||||
void bsp_usart_rx_isr_handler(void)
|
||||
{
|
||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||
|
||||
if (USART_GetIntStatus(BSP_USARTx, USART_INT_RXDNE) != RESET)
|
||||
if (USART_GetIntStatus(BSP_USARTx, USART_INT_IDLEF) != RESET)
|
||||
{
|
||||
/* Reading DAT clears the RXDNE flag */
|
||||
uint8_t ch = (uint8_t)USART_ReceiveData(BSP_USARTx);
|
||||
/* IDLEF clears by reading STS (done by GetIntStatus) then DAT;
|
||||
* the data register itself is already drained by the DMA */
|
||||
(void)USART_ReceiveData(BSP_USARTx);
|
||||
|
||||
if (s_rxQueue != NULL)
|
||||
if (s_rxSem != NULL)
|
||||
{
|
||||
xQueueSendFromISR(s_rxQueue, &ch, &xHigherPriorityTaskWoken);
|
||||
xSemaphoreGiveFromISR(s_rxSem, &xHigherPriorityTaskWoken);
|
||||
}
|
||||
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
||||
}
|
||||
|
||||
@ -277,6 +277,24 @@ static void CmdFactory(int argc, char* argv[])
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief "hang": wedge the CLI task in a busy loop so the idle hook stops
|
||||
* feeding the IWDG - watchdog fire test (device resets in ~4s and
|
||||
* the next banner shows "Last reset: IWDG watchdog!").
|
||||
*/
|
||||
static void CmdHang(int argc, char* argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
cli_write("hanging now - watchdog should reset in ~4s ...\r\n");
|
||||
vTaskDelay(pdMS_TO_TICKS(CLI_RESET_DELAY_MS));
|
||||
for (;;)
|
||||
{
|
||||
/* busy: idle task starves, IWDG unfed */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief "uartrst": re-initialize USART1 at the default baud rate and
|
||||
* flush the RX queue (recovery from a wedged/misconfigured UART).
|
||||
@ -302,7 +320,7 @@ static void CmdUartInfo(int argc, char* argv[])
|
||||
cli_write("usart1: TX=PB6 RX=PB7 (AF4)\r\n");
|
||||
cli_printf("baud: %lu, 8 data bits, no parity, 1 stop bit\r\n",
|
||||
(unsigned long)BSP_USART_BAUDRATE);
|
||||
cli_write("flow control: none; tx: DMA CH1 (polled); rx: RXDNE irq queue\r\n");
|
||||
cli_write("flow control: none; tx: DMA CH1 (polled); rx: DMA CH2 ring (3KB, idle irq)\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -340,8 +358,9 @@ static const CliCmd_t s_cmds[] = {
|
||||
{"ota", "ota: UART enters OTA binary frame mode (see help ota)", CmdOta},
|
||||
{"reset", "reset: system reset (reboot)", CmdReset},
|
||||
{"factory", "factory: restore params to defaults and reboot", CmdFactory},
|
||||
{"uartrst", "uartrst: re-init USART1 (115200 8N1), flush rx queue", CmdUartRst},
|
||||
{"uartrst", "uartrst: re-init USART1 (460800 8N1), flush rx queue", CmdUartRst},
|
||||
{"uartinfo", "uartinfo: show USART1 pins/baud/format", CmdUartInfo},
|
||||
{"hang", "hang: wedge the CLI task (IWDG watchdog fire test)", CmdHang},
|
||||
};
|
||||
|
||||
#define CLI_CMD_COUNT (sizeof(s_cmds) / sizeof(s_cmds[0]))
|
||||
|
||||
@ -84,6 +84,8 @@
|
||||
#include "app_ota.h"
|
||||
#include "app_version.h"
|
||||
#include "app_user_config.h"
|
||||
#include "app_wdt.h"
|
||||
#include "n32wb03x_iwdg.h"
|
||||
|
||||
/* Private typedef -----------------------------------------------------------*/
|
||||
/* Private define ------------------------------------------------------------*/
|
||||
@ -105,8 +107,62 @@
|
||||
#define LED_TASK_STACK (128) /* stack in words */
|
||||
#define LED_TASK_PRIORITY (1)
|
||||
|
||||
/* Private constants ---------------------------------------------------------*/
|
||||
/* Private variables ---------------------------------------------------------*/
|
||||
/* Private constants ----------------------------------------------------------*/
|
||||
/* Private variables ----------------------------------------------------------*/
|
||||
|
||||
/* IWDG: LSI 32kHz, /128 prescaler -> 250Hz counter; 1000 counts = 4s */
|
||||
#define APP_WDT_RELOAD 1000u
|
||||
/* the idle hook feeds the IWDG only while some task heartbeated this long */
|
||||
#define APP_WDT_HB_TIMEOUT_MS 3000u
|
||||
|
||||
static volatile TickType_t s_wdt_last_hb;
|
||||
|
||||
/**
|
||||
* @brief Report the reset cause and start the independent watchdog.
|
||||
* IWDG is frozen while the core is halted (debug-friendly).
|
||||
*/
|
||||
void app_wdt_init(void)
|
||||
{
|
||||
if (RCC_GetFlagStatus(RCC_CTRLSTS_FLAG_IWDGRSTF) != RESET)
|
||||
{
|
||||
printf(" Last reset: IWDG watchdog!\n");
|
||||
}
|
||||
RCC_ClrFlag();
|
||||
|
||||
/* Freeze IWDG while the core is halted (SWD debug sessions) */
|
||||
RCC_EnableAPB1PeriphClk(RCC_APB1_PERIPH_PWR, ENABLE);
|
||||
DBG_ConfigPeriph(DBG_IWDG_STOP, ENABLE);
|
||||
|
||||
IWDG_WriteConfig(IWDG_WRITE_ENABLE);
|
||||
IWDG_SetPrescalerDiv(IWDG_PRESCALER_DIV128);
|
||||
IWDG_CntReload(APP_WDT_RELOAD);
|
||||
IWDG_ReloadKey();
|
||||
IWDG_Enable(); /* LSI is enabled by hardware */
|
||||
|
||||
s_wdt_last_hb = 0;
|
||||
}
|
||||
|
||||
void app_wdt_heartbeat(void)
|
||||
{
|
||||
if (xTaskGetSchedulerState() == taskSCHEDULER_RUNNING)
|
||||
{
|
||||
s_wdt_last_hb = xTaskGetTickCount();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief FreeRTOS idle hook: feed the IWDG only while some application task
|
||||
* heartbeated recently. A wedged/deadlocked system (idle spins but no
|
||||
* task advances) resets within the IWDG timeout.
|
||||
*/
|
||||
void vApplicationIdleHook(void)
|
||||
{
|
||||
if ((xTaskGetTickCount() - s_wdt_last_hb) < pdMS_TO_TICKS(APP_WDT_HB_TIMEOUT_MS))
|
||||
{
|
||||
IWDG_ReloadKey();
|
||||
}
|
||||
}
|
||||
|
||||
/* Private function prototypes -----------------------------------------------*/
|
||||
/* Private functions ---------------------------------------------------------*/
|
||||
|
||||
@ -121,6 +177,7 @@ static void ble_schedule_task(void *pvParameters)
|
||||
(void)pvParameters;
|
||||
for (;;)
|
||||
{
|
||||
app_wdt_heartbeat();
|
||||
/*schedule all pending events*/
|
||||
rwip_schedule();
|
||||
/*flush pending CLI uplink frames (notify, paced by cfm)*/
|
||||
@ -147,6 +204,7 @@ static void led_task(void *pvParameters)
|
||||
{
|
||||
period = PARAM_LED1_BLINK_MS_DEF;
|
||||
}
|
||||
app_wdt_heartbeat();
|
||||
LedBlink(LED1_PORT, LED1_PIN);
|
||||
vTaskDelay(pdMS_TO_TICKS(period));
|
||||
}
|
||||
@ -177,6 +235,9 @@ int main(void)
|
||||
printf(" BLE enabled, advertising as \"%s\"\n", CUSTOM_DEVICE_NAME);
|
||||
printf("========================================\n");
|
||||
|
||||
/* reset-cause report + start the independent watchdog (idle-hook fed) */
|
||||
app_wdt_init();
|
||||
|
||||
#if (CFG_APP_NS_IUS)
|
||||
if(CURRENT_APP_START_ADDRESS == NS_APP1_START_ADDRESS){
|
||||
NS_LOG_INFO("application 1 start new ...\r\n");
|
||||
|
||||
BIN
tools/N32WB03x_DFP.1.4.0.pack
Normal file
BIN
tools/N32WB03x_DFP.1.4.0.pack
Normal file
Binary file not shown.
BIN
tools/NSpyocd/NSpyocd.exe
Normal file
BIN
tools/NSpyocd/NSpyocd.exe
Normal file
Binary file not shown.
@ -13,8 +13,10 @@ rem Requires tools\.venv-ble (created automatically on first run).
|
||||
rem ============================================================
|
||||
setlocal
|
||||
set TOOLS=%~dp0
|
||||
set PYEXE=%TOOLS%.venv-ble\Scripts\python.exe
|
||||
|
||||
rem 优先使用随包内嵌的 Python 运行时(tools\python-embed,免安装);
|
||||
rem 没有则回退到自动创建的 .venv-ble(开发机)
|
||||
set PYEXE=%TOOLS%python-embed\python.exe
|
||||
if not exist "%PYEXE%" set PYEXE=%TOOLS%.venv-ble\Scripts\python.exe
|
||||
if not exist "%PYEXE%" (
|
||||
echo [setup] creating venv %TOOLS%.venv-ble ...
|
||||
set PYREAL=
|
||||
@ -24,7 +26,7 @@ if not exist "%PYEXE%" (
|
||||
)
|
||||
if not defined PYREAL set PYREAL=python
|
||||
%PYREAL% -m venv "%TOOLS%.venv-ble" || goto :fail
|
||||
"%PYEXE%" -m pip install --quiet bleak || goto :fail
|
||||
"%PYEXE%" -m pip install --quiet -r "%TOOLS%requirements-ble.txt" || goto :fail
|
||||
)
|
||||
|
||||
set MODE=-rw
|
||||
|
||||
@ -13,8 +13,10 @@ rem Requires tools\.venv-ble (created automatically on first run).
|
||||
rem ============================================================
|
||||
setlocal
|
||||
set TOOLS=%~dp0
|
||||
set PYEXE=%TOOLS%.venv-ble\Scripts\python.exe
|
||||
|
||||
rem 优先使用随包内嵌的 Python 运行时(tools\python-embed,免安装);
|
||||
rem 没有则回退到自动创建的 .venv-ble(开发机)
|
||||
set PYEXE=%TOOLS%python-embed\python.exe
|
||||
if not exist "%PYEXE%" set PYEXE=%TOOLS%.venv-ble\Scripts\python.exe
|
||||
if not exist "%PYEXE%" (
|
||||
echo [setup] creating venv %TOOLS%.venv-ble ...
|
||||
set PYREAL=
|
||||
@ -24,7 +26,7 @@ if not exist "%PYEXE%" (
|
||||
)
|
||||
if not defined PYREAL set PYREAL=python
|
||||
%PYREAL% -m venv "%TOOLS%.venv-ble" || goto :fail
|
||||
"%PYEXE%" -m pip install --quiet bleak || goto :fail
|
||||
"%PYEXE%" -m pip install --quiet -r "%TOOLS%requirements-ble.txt" || goto :fail
|
||||
)
|
||||
|
||||
"%PYEXE%" "%TOOLS%ble_ota_update.py" %1 %2 %3
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ble_ota_update.py - PC-side OTA updater for CAIIC-MCM devices, transport
|
||||
protocol per docs/ble_protocol.md section 6.6 (0xCA frames, OTA_BEGIN/DATA/
|
||||
END/ABORT -> framed OTA_RSP acks).
|
||||
protocol per docs/ble_protocol.md sections 6.6/6.7 (0xCA frames,
|
||||
OTA_BEGIN/DATA/END/ABORT -> framed OTA_RSP acks).
|
||||
|
||||
Two channels:
|
||||
BLE (default): dedicated OTA characteristic ...e0005 - one frame per
|
||||
@ -12,6 +12,10 @@ Two channels:
|
||||
UART (--uart): e.g. --uart COM4 - the CLI command "ota" switches the
|
||||
serial link to binary frame mode (marker-confirmed
|
||||
handshake; OTA_ABORT exits); same frames on the wire.
|
||||
Firmware V1.00.24+ runs STREAM mode here (pipelined
|
||||
DATA inside an 8KB window, sector-boundary acks,
|
||||
go-back-N on loss); older firmware falls back to
|
||||
lockstep automatically, or force it with --lockstep.
|
||||
|
||||
Both channels select the payload by CUR_BANK (info item 0x07, read-only
|
||||
characteristic ...e0004 / CLI "devinfo"): OTA always writes the INACTIVE
|
||||
@ -19,7 +23,7 @@ bank with the image linked for it, from the single-file package
|
||||
(mothercup_ble_ota.bin, 52B header + both payloads).
|
||||
|
||||
usage: ble_ota.bat [mothercup_ble_ota.bin]
|
||||
ble_ota.bat --uart COM4 [mothercup_ble_ota.bin]
|
||||
ble_ota.bat --uart COM4 [--lockstep] [mothercup_ble_ota.bin]
|
||||
|
||||
Success = the device resets after OTA_END and comes back on the new bank
|
||||
with the new version (verified by a second CUR_BANK/FW_VERSION read).
|
||||
@ -51,8 +55,15 @@ XFER_TRIES = 3 # resend a frame whose ack was lost (flaky links)
|
||||
REBOOT_WAIT_S = 20.0
|
||||
|
||||
ST_OK = 0
|
||||
ST_BAD_FRAME = 1
|
||||
ST_BAD_STATE = 2
|
||||
|
||||
# UART stream mode (firmware V1.00.24+, ble_protocol.md §6.7)
|
||||
BEGIN_FLAG_STREAM = 1
|
||||
STREAM_WINDOW = 8192 # unacked bytes in flight
|
||||
STREAM_CHUNK = 240 # DATA payload per frame (device limit 480+4)
|
||||
STREAM_STALL_S = 3.0 # no ack progress -> rewind to last ack
|
||||
|
||||
_seq = [0]
|
||||
|
||||
|
||||
@ -157,7 +168,7 @@ class UartTransport:
|
||||
MARKER_ON = b"[ota] binary mode ON"
|
||||
PROMPT = b"caiic->"
|
||||
|
||||
def __init__(self, port, baud=115200):
|
||||
def __init__(self, port, baud=460800):
|
||||
import serial
|
||||
self.ser = serial.Serial(port, baud, timeout=0.1)
|
||||
self.dec = FrameDecoder()
|
||||
@ -414,6 +425,107 @@ async def ota_session(xfer, chunk, blob, version, target):
|
||||
# Channel frontends
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def ota_session_uart_stream(transport, blob, version, target):
|
||||
"""UART stream mode (firmware V1.00.24+, ble_protocol.md §6.7):
|
||||
extended BEGIN (16B payload, flags=STREAM), then DATA frames are
|
||||
pipelined inside an 8KB window without per-frame acks; the device acks
|
||||
at 4KB sector boundaries (erase completed) and the final byte.
|
||||
A lost/corrupt frame makes the device report BAD_STATE once with the
|
||||
offset it expects - the host rewinds there (go-back-N). A total ack
|
||||
loss is caught by a stall watchdog that also rewinds to the last ack.
|
||||
Raises RuntimeError with 'status=1' when the device rejects the 16B
|
||||
BEGIN (pre-V1.00.24 firmware) so the caller can fall back to lockstep."""
|
||||
rsp = await transport.xfer(encode_frame(TYPE_OTA_BEGIN,
|
||||
struct.pack("<IIII", len(blob), crc32(blob),
|
||||
version, BEGIN_FLAG_STREAM)))
|
||||
frames = FrameDecoder().feed(rsp)
|
||||
if len(frames) != 1 or frames[0][0] != TYPE_OTA_RSP:
|
||||
raise RuntimeError("BEGIN(stream): bad RSP frame: %s" % rsp.hex(" "))
|
||||
echo, status, offset = parse_rsp(frames[0][2])
|
||||
if status != ST_OK:
|
||||
raise RuntimeError("OTA_BEGIN(stream) rejected, status=%d" % status)
|
||||
print("OTA_BEGIN ok (stream mode), pipelining data ...")
|
||||
|
||||
ser = transport.ser
|
||||
dec = transport.dec
|
||||
sent = 0
|
||||
acked = 0
|
||||
rewinds = 0
|
||||
t0 = time.monotonic()
|
||||
last_progress = t0
|
||||
tty = sys.stdout.isatty()
|
||||
while sent < len(blob) or acked < len(blob):
|
||||
# fill the window
|
||||
while sent < len(blob) and sent - acked < STREAM_WINDOW:
|
||||
n = min(STREAM_CHUNK, len(blob) - sent)
|
||||
ser.write(encode_frame(TYPE_OTA_DATA,
|
||||
struct.pack("<I", sent) + blob[sent:sent + n]))
|
||||
sent += n
|
||||
# collect whatever acks arrived (serial timeout = 0.1s)
|
||||
data = await asyncio.to_thread(ser.read, 256)
|
||||
now = time.monotonic()
|
||||
for ftype, seq, payload in dec.feed(data):
|
||||
if ftype != TYPE_OTA_RSP:
|
||||
continue
|
||||
echo, status, offset = parse_rsp(payload)
|
||||
if echo != TYPE_OTA_DATA:
|
||||
continue
|
||||
if status == ST_OK:
|
||||
if offset > acked:
|
||||
acked = offset
|
||||
last_progress = now
|
||||
elif status == ST_BAD_STATE:
|
||||
if sent != offset:
|
||||
rewinds += 1
|
||||
print("\n DATA: lost frame, rewind %d -> %d"
|
||||
% (sent, offset))
|
||||
sent = offset
|
||||
if acked > offset:
|
||||
acked = offset
|
||||
last_progress = now
|
||||
else:
|
||||
raise RuntimeError("OTA_DATA rejected, status=%d "
|
||||
"(device expects offset %d)"
|
||||
% (status, offset))
|
||||
# progress (throttled on tty, one line per 5% otherwise)
|
||||
pct = 100.0 * acked / len(blob)
|
||||
rate = acked / 1024.0 / max(now - t0, 0.001)
|
||||
if tty:
|
||||
print(" %d / %d bytes (%.0f%%, %.1f kB/s, %d rewinds) "
|
||||
% (acked, len(blob), pct, rate, rewinds),
|
||||
end="\r" if acked != len(blob) else "\n", flush=True)
|
||||
elif acked == len(blob) or int(pct // 5) != getattr(
|
||||
ota_session_uart_stream, "_step", -1):
|
||||
ota_session_uart_stream._step = int(pct // 5)
|
||||
print(" %d / %d bytes (%.0f%%, %.1f kB/s, %d rewinds)"
|
||||
% (acked, len(blob), pct, rate, rewinds))
|
||||
# stall watchdog: no ack progress -> the ack itself was lost,
|
||||
# rewind to the last acked offset and resend from there
|
||||
if now - last_progress > STREAM_STALL_S:
|
||||
if sent != acked:
|
||||
rewinds += 1
|
||||
print("\n DATA: ack stall, rewind %d -> %d" % (sent, acked))
|
||||
sent = acked
|
||||
last_progress = now
|
||||
|
||||
print("OTA_END: verify image + switch bank ...")
|
||||
try:
|
||||
rsp = await transport.xfer(encode_frame(TYPE_OTA_END,
|
||||
struct.pack("<I", crc32(blob))))
|
||||
frames = FrameDecoder().feed(rsp)
|
||||
if len(frames) != 1 or frames[0][0] != TYPE_OTA_RSP:
|
||||
raise RuntimeError("END: bad RSP frame: %s" % rsp.hex(" "))
|
||||
echo, status, offset = parse_rsp(frames[0][2])
|
||||
if status != ST_OK:
|
||||
raise RuntimeError("OTA_END rejected, status=%d "
|
||||
"(4=crc_fail, 6=bank_mismatch)" % status)
|
||||
except (asyncio.TimeoutError, OSError) as exc:
|
||||
print("OTA_END ack lost (%s) - waiting for reboot anyway ..." % exc)
|
||||
print("OTA_END done (%.1fs total, %d rewinds), device reboots into "
|
||||
"APP%d ..." % (time.monotonic() - t0, rewinds, target))
|
||||
return True
|
||||
|
||||
|
||||
async def find_device():
|
||||
from bleak import BleakScanner
|
||||
print("scanning for %s* ..." % NAME_PREFIX)
|
||||
@ -490,7 +602,7 @@ async def run_ble(pkg_path):
|
||||
return 1
|
||||
|
||||
|
||||
async def run_uart(port, pkg_path):
|
||||
async def run_uart(port, pkg_path, lockstep=False):
|
||||
version, banks = parse_combo(pkg_path)
|
||||
print("package: version %s, bank1 %dB / bank2 %dB"
|
||||
% (fmt_ver(version), len(banks[1]), len(banks[2])))
|
||||
@ -509,6 +621,16 @@ async def run_uart(port, pkg_path):
|
||||
% (target, len(blob), crc32(blob)))
|
||||
|
||||
try:
|
||||
if lockstep:
|
||||
await ota_session(transport.xfer, 222, blob, version, target)
|
||||
else:
|
||||
try:
|
||||
await ota_session_uart_stream(transport, blob, version, target)
|
||||
except RuntimeError as exc:
|
||||
if "status=1" not in str(exc): # not "bad BEGIN frame"
|
||||
raise
|
||||
print("note: device has no stream mode (pre-V1.00.24), "
|
||||
"falling back to lockstep")
|
||||
await ota_session(transport.xfer, 222, blob, version, target)
|
||||
except Exception as exc:
|
||||
print("FAIL: %s" % exc)
|
||||
@ -539,12 +661,18 @@ async def run_uart(port, pkg_path):
|
||||
|
||||
|
||||
def main():
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
argv = sys.argv[1:]
|
||||
lower = [a.lower() for a in argv]
|
||||
lockstep = "--lockstep" in lower
|
||||
uart = None
|
||||
if "--uart" in sys.argv[1:]:
|
||||
i = sys.argv[1:].index("--uart")
|
||||
uart = sys.argv[1:][i + 1]
|
||||
args = [a for j, a in enumerate(sys.argv[1:]) if j not in (i, i + 1)]
|
||||
skip = -1
|
||||
if "--uart" in lower:
|
||||
i = lower.index("--uart")
|
||||
if i + 1 < len(argv):
|
||||
uart = argv[i + 1]
|
||||
skip = i + 1
|
||||
args = [a for j, a in enumerate(argv)
|
||||
if not a.startswith("--") and j != skip]
|
||||
|
||||
root = os.path.dirname(os.path.abspath(__file__))
|
||||
pkg = args[0] if args else os.path.join(root, "out", "mothercup_ble_ota.bin")
|
||||
@ -552,7 +680,7 @@ def main():
|
||||
print("package not found: %s (run tools\\make_package.bat first)" % pkg)
|
||||
return 1
|
||||
if uart:
|
||||
return asyncio.run(run_uart(uart, pkg))
|
||||
return asyncio.run(run_uart(uart, pkg, lockstep))
|
||||
return asyncio.run(run_ble(pkg))
|
||||
|
||||
|
||||
|
||||
@ -1,45 +1,29 @@
|
||||
@echo off
|
||||
rem ============================================================
|
||||
rem flash_package.bat [image] - erase the chip and program the
|
||||
rem merged package via NS-LINK (NSpyocd).
|
||||
rem merged package via NS-LINK (NSpyocd); thin wrapper over
|
||||
rem flash_package.py (streams output with the CAIIC banner).
|
||||
rem [image] optional, default: tools\out\mothercup_ble_prod.hex
|
||||
rem Connect NS-LINK to SWD (PA4/PA5) and the reset pin first,
|
||||
rem and CLOSE Keil (it holds the probe).
|
||||
rem ============================================================
|
||||
setlocal
|
||||
set ROOT=%~dp0..
|
||||
set PYOCD="%ROOT%\nations-tec\N32WB03x_SDK_V2.0.0\utilities\dfu\NSpyocd\NSpyocd.exe"
|
||||
rem The builtin "n32wb031" target only maps 256KB of flash (fails at
|
||||
rem 0x01040000 on the dual-bank package); the DFP pack target
|
||||
rem "n32wb031keq6_2" maps the full 512KB of the KEQ6-2 we actually use.
|
||||
set PACK="%ROOT%\nations-tec\N32WB03x_DFP.1.4.0.pack"
|
||||
set TARGET=n32wb031keq6_2
|
||||
set IMAGE=%~1
|
||||
if "%IMAGE%"=="" set IMAGE=out\mothercup_ble_prod.hex
|
||||
if not "%IMAGE:~0,1%"=="\" if not "%IMAGE:~1,1%"==":" set IMAGE=%~dp0%IMAGE%
|
||||
|
||||
if not exist "%IMAGE%" (
|
||||
echo %IMAGE% not found - run tools\make_package.bat first
|
||||
rem 优先使用随包内嵌的 Python 运行时(tools\python-embed,免安装)
|
||||
set PYEXE=%~dp0python-embed\python.exe
|
||||
if not exist "%PYEXE%" (
|
||||
set PYEXE=
|
||||
python --version >nul 2>&1 && set PYEXE=python
|
||||
)
|
||||
if not defined PYEXE (
|
||||
if exist "%LOCALAPPDATA%\Programs\Python\Python312\python.exe" (
|
||||
set PYEXE="%LOCALAPPDATA%\Programs\Python\Python312\python.exe"
|
||||
)
|
||||
)
|
||||
if not defined PYEXE (
|
||||
echo python not found - install Python 3 or fix PATH
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
rem -M under-reset: connect while holding the chip in reset
|
||||
rem -f 1000000: 1 MHz SWD clock (slow but tolerant of wiring)
|
||||
echo Erasing chip...
|
||||
%PYOCD% erase --chip --pack %PACK% -M under-reset -f 1000000 -t %TARGET% || goto :fail
|
||||
echo Programming %IMAGE% ...
|
||||
rem -O smart_flash=false: with the pack target the pre-program diff pass
|
||||
rem faults reading 0x01000000; programming+verify itself is fine.
|
||||
%PYOCD% load --pack %PACK% -M under-reset -f 1000000 -t %TARGET% -O smart_flash=false "%IMAGE%" || goto :fail
|
||||
echo Program Finish!
|
||||
goto :eof
|
||||
|
||||
:fail
|
||||
echo.
|
||||
echo *** FLASH FAILED ***
|
||||
echo Troubleshooting:
|
||||
echo 1. Close Keil / any tool holding the probe, replug NS-LINK USB
|
||||
echo 2. List probes: %PYOCD% list
|
||||
echo 3. Check wiring: SWDIO=PA5 SWCLK=PA4 GND and the RESET pin
|
||||
echo 4. If several probes are attached, add -u ^<probe_uid^> to the commands above
|
||||
exit /b 1
|
||||
%PYEXE% "%~dp0flash_package.py" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
|
||||
119
tools/flash_package.py
Normal file
119
tools/flash_package.py
Normal file
@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flash_package.py - erase the chip and program the merged package via
|
||||
NS-LINK (NSpyocd). Streams the vendor tool's output with the vendor
|
||||
banner replaced by the CAIIC one (the banner is baked into the
|
||||
PyInstaller-packed NSpyocd.exe and cannot be edited).
|
||||
|
||||
usage: flash_package.bat [image]
|
||||
[image] optional, default: tools/out/mothercup_ble_prod.hex
|
||||
|
||||
Connect NS-LINK to SWD (PA4/PA5) and the reset pin first, and CLOSE
|
||||
Keil (it holds the probe).
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# NSpyocd is a PyInstaller-packed pyocd: its banner is inside the
|
||||
# compressed bundle, so we rebrand it on the fly. The console encoding
|
||||
# varies (UTF-8 vs GBK), match the byte pattern in both.
|
||||
BANNER_PATTERNS = [
|
||||
("国民技术NSLINK上位机".encode("utf-8"), b"CAIIC NSLINK UMP"),
|
||||
("国民技术NSLINK上位机".encode("gbk"), b"CAIIC NSLINK UMP"),
|
||||
]
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TOOLS = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Prefer the copies shipped inside tools/ (portable production package);
|
||||
# fall back to the repo's nations-tec/ SDK layout (developer machine).
|
||||
_local_pyocd = os.path.join(TOOLS, "NSpyocd", "NSpyocd.exe")
|
||||
_local_pack = os.path.join(TOOLS, "N32WB03x_DFP.1.4.0.pack")
|
||||
PYOCD = _local_pyocd if os.path.isfile(_local_pyocd) else \
|
||||
os.path.join(ROOT, "nations-tec", "N32WB03x_SDK_V2.0.0",
|
||||
"utilities", "dfu", "NSpyocd", "NSpyocd.exe")
|
||||
# The builtin "n32wb031" target only maps 256KB of flash (fails at
|
||||
# 0x01040000 on the dual-bank package); the DFP pack target
|
||||
# "n32wb031keq6_2" maps the full 512KB of the KEQ6-2 we actually use.
|
||||
PACK = _local_pack if os.path.isfile(_local_pack) else \
|
||||
os.path.join(ROOT, "nations-tec", "N32WB03x_DFP.1.4.0.pack")
|
||||
TARGET = "n32wb031keq6_2"
|
||||
|
||||
|
||||
def run_step(args):
|
||||
"""Run one NSpyocd step, streaming output with the banner replaced.
|
||||
Reads in small chunks (not line-buffered) so the tool's \\r-based
|
||||
progress bar redraws live. Returns the process exit code."""
|
||||
proc = subprocess.Popen([PYOCD] + args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT)
|
||||
out = sys.stdout.buffer
|
||||
keep = max(len(p) for p, _ in BANNER_PATTERNS) - 1
|
||||
tail = b""
|
||||
while True:
|
||||
chunk = proc.stdout.read(256)
|
||||
if not chunk:
|
||||
break
|
||||
buf = tail + chunk
|
||||
# replace on the COMBINED buffer first (a pattern straddling the
|
||||
# chunk boundary only exists contiguously here), then hold back
|
||||
# the last maxpat-1 bytes so a pattern split by the next chunk
|
||||
# is still seen whole
|
||||
for old, new in BANNER_PATTERNS:
|
||||
buf = buf.replace(old, new)
|
||||
emit, tail = buf[:-keep], buf[-keep:]
|
||||
out.write(emit)
|
||||
out.flush()
|
||||
for old, new in BANNER_PATTERNS:
|
||||
tail = tail.replace(old, new)
|
||||
out.write(tail)
|
||||
out.flush()
|
||||
return proc.wait()
|
||||
|
||||
|
||||
def main():
|
||||
image = sys.argv[1] if len(sys.argv) > 1 else \
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"out", "mothercup_ble_prod.hex")
|
||||
if not os.path.isfile(image):
|
||||
print("%s not found - run tools\\make_package.bat first" % image)
|
||||
return 1
|
||||
|
||||
# -M under-reset: connect while holding the chip in reset
|
||||
# -f 1000000: 1 MHz SWD clock (slow but tolerant of wiring)
|
||||
print("Erasing chip...", flush=True)
|
||||
rc = run_step(["erase", "--chip", "--pack", PACK, "-M", "under-reset",
|
||||
"-f", "1000000", "-t", TARGET])
|
||||
if rc != 0:
|
||||
return fail()
|
||||
|
||||
print("Programming %s ..." % image, flush=True)
|
||||
# -O smart_flash=false: with the pack target the pre-program diff pass
|
||||
# faults reading 0x01000000; programming+verify itself is fine.
|
||||
rc = run_step(["load", "--pack", PACK, "-M", "under-reset",
|
||||
"-f", "1000000", "-t", TARGET,
|
||||
"-O", "smart_flash=false", image])
|
||||
if rc != 0:
|
||||
return fail()
|
||||
|
||||
print("Program Finish!")
|
||||
return 0
|
||||
|
||||
|
||||
def fail():
|
||||
print()
|
||||
print("*** FLASH FAILED ***")
|
||||
print("Troubleshooting:")
|
||||
print(" 1. Close Keil / any tool holding the probe, replug NS-LINK USB")
|
||||
print(" 2. List probes: \"%s\" list" % PYOCD)
|
||||
print(" 3. Check wiring: SWDIO=PA5 SWCLK=PA4 GND and the RESET pin")
|
||||
print(" 4. If several probes are attached, add -u <probe_uid> "
|
||||
"to the commands above")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -26,8 +26,12 @@ if errorlevel 1 goto :fail
|
||||
findstr /C:"0 Error(s), 0 Warning(s)" "%ROOT%\mcm-ddc-ble\MDK-ARM\build-app2.log" >nul || goto :fail
|
||||
|
||||
echo [4/4] Merging images...
|
||||
rem 优先随包内嵌的 Python 运行时(merge_image.py 只需标准库)
|
||||
set PYEXE=
|
||||
if exist "%~dp0python-embed\python.exe" set PYEXE="%~dp0python-embed\python.exe"
|
||||
if not defined PYEXE (
|
||||
python --version >nul 2>&1 && set PYEXE=python
|
||||
)
|
||||
if not defined PYEXE (
|
||||
if exist "%LOCALAPPDATA%\Programs\Python\Python312\python.exe" (
|
||||
set PYEXE="%LOCALAPPDATA%\Programs\Python\Python312\python.exe"
|
||||
|
||||
11
tools/make_release.bat
Normal file
11
tools/make_release.bat
Normal file
@ -0,0 +1,11 @@
|
||||
@echo off
|
||||
rem ============================================================
|
||||
rem make_release.bat - pack tools\ into out\mothercup_tools_<ts>.zip
|
||||
rem Self-contained production package: embedded Python runtime
|
||||
rem (python-embed\) + NSpyocd flasher included - the target PC
|
||||
rem needs nothing installed. Unzip anywhere and run the .bat files.
|
||||
rem ============================================================
|
||||
setlocal
|
||||
set PS=powershell
|
||||
%PS% -noprofile -command "exit 0" >nul 2>&1 || set PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe
|
||||
%PS% -noprofile -executionpolicy bypass -file "%~dp0make_release.ps1"
|
||||
14
tools/make_release.ps1
Normal file
14
tools/make_release.ps1
Normal file
@ -0,0 +1,14 @@
|
||||
# make_release.ps1 - pack tools/ into a self-contained production zip.
|
||||
# The zip holds the embedded Python runtime (python-embed/, bleak+pyserial
|
||||
# preinstalled) and NSpyocd, so the target PC needs NO installation.
|
||||
# Excludes the dev-only venv and caches; keeps out/ (the firmware images).
|
||||
# NOTE: the zip is staged in $TEMP first - writing it into out/ while
|
||||
# reading out/ would make Compress-Archive read its own half-written file.
|
||||
$tools = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$stamp = Get-Date -Format "yyyyMMdd_HHmm"
|
||||
$stage = Join-Path $env:TEMP "mothercup_tools_$stamp.zip"
|
||||
$final = Join-Path $tools "out\mothercup_tools_$stamp.zip"
|
||||
$items = Get-ChildItem $tools | Where-Object { $_.Name -notin @('.venv-ble', '__pycache__') }
|
||||
Compress-Archive -Path ($items | ForEach-Object { $_.FullName }) -DestinationPath $stage -Force -ErrorAction Stop
|
||||
Move-Item $stage $final -Force
|
||||
Write-Output "packed: $final"
|
||||
@ -175,6 +175,28 @@ def load_bin(path, desc):
|
||||
return f.read()
|
||||
|
||||
|
||||
# Usable app SRAM on the 256KB-flash silicon is 32KB: 0x20004000..0x2000BFFF.
|
||||
# Accesses at/above 0x2000C000 fault (dev log §47), so the image's initial
|
||||
# SP (vector[0]) must stay below it - a full-rebuild size drift once pushed
|
||||
# the stack over this cliff and the board locked up at boot.
|
||||
SRAM_STACK_TOP_LIMIT = 0x2000C000
|
||||
|
||||
|
||||
def check_initial_sp(blob, desc):
|
||||
"""Vector table word 0 = initial MSP. Hard-fail when it lands outside
|
||||
the usable SRAM window."""
|
||||
if len(blob) < 8:
|
||||
return
|
||||
sp = struct.unpack_from("<I", blob, 0)[0]
|
||||
if not (0x20004000 < sp <= SRAM_STACK_TOP_LIMIT):
|
||||
raise SystemExit("ERROR: %s initial SP 0x%08X is outside usable SRAM "
|
||||
"(0x20004001..0x%08X) - shrink ZI (heap/buffers); "
|
||||
"see dev log §47" % (desc, sp, SRAM_STACK_TOP_LIMIT))
|
||||
if sp > SRAM_STACK_TOP_LIMIT - 0x400:
|
||||
print("WARNING: %s initial SP 0x%08X is within 1KB of the SRAM cliff"
|
||||
% (desc, sp))
|
||||
|
||||
|
||||
def read_app_fw_version(root):
|
||||
"""Extract APP_FW_VERSION_NUM from the APP project's app_version.h.
|
||||
Returns None when not found."""
|
||||
@ -252,6 +274,13 @@ def main():
|
||||
elif app2_blob:
|
||||
ota_app2_blob = app2_blob
|
||||
|
||||
# Stack-top cliff guard (usable SRAM ends at 0x2000C000, dev log §47)
|
||||
check_initial_sp(app_blob, "APP1")
|
||||
if app2_blob:
|
||||
check_initial_sp(app2_blob, "APP2")
|
||||
if ota_app2_blob:
|
||||
check_initial_sp(ota_app2_blob, "OTA APP2 payload")
|
||||
|
||||
bs_blob = default_bootsetting(app_blob, app2_blob, version, app2_version)
|
||||
|
||||
segments = [
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1,7 +0,0 @@
|
||||
{
|
||||
"file": "caiic_ble_dual_ota.bin",
|
||||
"target_bank": 1,
|
||||
"size": 55408,
|
||||
"crc32": "0xA87539ED",
|
||||
"version": 65559
|
||||
}
|
||||
Binary file not shown.
@ -1,7 +0,0 @@
|
||||
{
|
||||
"file": "caiic_ble_dual_ota_app2.bin",
|
||||
"target_bank": 2,
|
||||
"size": 55708,
|
||||
"crc32": "0x03825A0D",
|
||||
"version": 65559
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"file": "mothercup_ble_prod_ota.bin",
|
||||
"target_bank": 1,
|
||||
"size": 55408,
|
||||
"crc32": "0xA87539ED",
|
||||
"version": 65559
|
||||
"size": 56100,
|
||||
"crc32": "0xC7CF2A19",
|
||||
"version": 65563
|
||||
}
|
||||
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"file": "mothercup_ble_prod_ota_app2.bin",
|
||||
"target_bank": 2,
|
||||
"size": 55708,
|
||||
"crc32": "0x03825A0D",
|
||||
"version": 65559
|
||||
"size": 56400,
|
||||
"crc32": "0x0198204E",
|
||||
"version": 65563
|
||||
}
|
||||
702
tools/python-embed/LICENSE.txt
Normal file
702
tools/python-embed/LICENSE.txt
Normal file
@ -0,0 +1,702 @@
|
||||
A. HISTORY OF THE SOFTWARE
|
||||
==========================
|
||||
|
||||
Python was created in the early 1990s by Guido van Rossum at Stichting
|
||||
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
|
||||
as a successor of a language called ABC. Guido remains Python's
|
||||
principal author, although it includes many contributions from others.
|
||||
|
||||
In 1995, Guido continued his work on Python at the Corporation for
|
||||
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
|
||||
in Reston, Virginia where he released several versions of the
|
||||
software.
|
||||
|
||||
In May 2000, Guido and the Python core development team moved to
|
||||
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
|
||||
year, the PythonLabs team moved to Digital Creations, which became
|
||||
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
|
||||
https://www.python.org/psf/) was formed, a non-profit organization
|
||||
created specifically to own Python-related Intellectual Property.
|
||||
Zope Corporation was a sponsoring member of the PSF.
|
||||
|
||||
All Python releases are Open Source (see https://opensource.org for
|
||||
the Open Source Definition). Historically, most, but not all, Python
|
||||
releases have also been GPL-compatible; the table below summarizes
|
||||
the various releases.
|
||||
|
||||
Release Derived Year Owner GPL-
|
||||
from compatible? (1)
|
||||
|
||||
0.9.0 thru 1.2 1991-1995 CWI yes
|
||||
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
|
||||
1.6 1.5.2 2000 CNRI no
|
||||
2.0 1.6 2000 BeOpen.com no
|
||||
1.6.1 1.6 2001 CNRI yes (2)
|
||||
2.1 2.0+1.6.1 2001 PSF no
|
||||
2.0.1 2.0+1.6.1 2001 PSF yes
|
||||
2.1.1 2.1+2.0.1 2001 PSF yes
|
||||
2.1.2 2.1.1 2002 PSF yes
|
||||
2.1.3 2.1.2 2002 PSF yes
|
||||
2.2 and above 2.1.1 2001-now PSF yes
|
||||
|
||||
Footnotes:
|
||||
|
||||
(1) GPL-compatible doesn't mean that we're distributing Python under
|
||||
the GPL. All Python licenses, unlike the GPL, let you distribute
|
||||
a modified version without making your changes open source. The
|
||||
GPL-compatible licenses make it possible to combine Python with
|
||||
other software that is released under the GPL; the others don't.
|
||||
|
||||
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
|
||||
because its license has a choice of law clause. According to
|
||||
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
|
||||
is "not incompatible" with the GPL.
|
||||
|
||||
Thanks to the many outside volunteers who have worked under Guido's
|
||||
direction to make these releases possible.
|
||||
|
||||
|
||||
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
|
||||
===============================================================
|
||||
|
||||
Python software and documentation are licensed under the
|
||||
Python Software Foundation License Version 2.
|
||||
|
||||
Starting with Python 3.8.6, examples, recipes, and other code in
|
||||
the documentation are dual licensed under the PSF License Version 2
|
||||
and the Zero-Clause BSD license.
|
||||
|
||||
Some software incorporated into Python is under different licenses.
|
||||
The licenses are listed with code falling under that license.
|
||||
|
||||
|
||||
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
--------------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||
otherwise using this software ("Python") in source or binary form and
|
||||
its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||
distribute, and otherwise use Python alone or in any derivative version,
|
||||
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
|
||||
All Rights Reserved" are retained in Python alone or in any derivative version
|
||||
prepared by Licensee.
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python.
|
||||
|
||||
4. PSF is making Python available to Licensee on an "AS IS"
|
||||
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. Nothing in this License Agreement shall be deemed to create any
|
||||
relationship of agency, partnership, or joint venture between PSF and
|
||||
Licensee. This License Agreement does not grant permission to use PSF
|
||||
trademarks or trade name in a trademark sense to endorse or promote
|
||||
products or services of Licensee, or any third party.
|
||||
|
||||
8. By copying, installing or otherwise using Python, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
||||
-------------------------------------------
|
||||
|
||||
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
||||
|
||||
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
|
||||
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
|
||||
Individual or Organization ("Licensee") accessing and otherwise using
|
||||
this software in source or binary form and its associated
|
||||
documentation ("the Software").
|
||||
|
||||
2. Subject to the terms and conditions of this BeOpen Python License
|
||||
Agreement, BeOpen hereby grants Licensee a non-exclusive,
|
||||
royalty-free, world-wide license to reproduce, analyze, test, perform
|
||||
and/or display publicly, prepare derivative works, distribute, and
|
||||
otherwise use the Software alone or in any derivative version,
|
||||
provided, however, that the BeOpen Python License is retained in the
|
||||
Software, alone or in any derivative version prepared by Licensee.
|
||||
|
||||
3. BeOpen is making the Software available to Licensee on an "AS IS"
|
||||
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
|
||||
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
|
||||
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
|
||||
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
5. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
6. This License Agreement shall be governed by and interpreted in all
|
||||
respects by the law of the State of California, excluding conflict of
|
||||
law provisions. Nothing in this License Agreement shall be deemed to
|
||||
create any relationship of agency, partnership, or joint venture
|
||||
between BeOpen and Licensee. This License Agreement does not grant
|
||||
permission to use BeOpen trademarks or trade names in a trademark
|
||||
sense to endorse or promote products or services of Licensee, or any
|
||||
third party. As an exception, the "BeOpen Python" logos available at
|
||||
http://www.pythonlabs.com/logos.html may be used according to the
|
||||
permissions granted on that web page.
|
||||
|
||||
7. By copying, installing or otherwise using the software, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
|
||||
---------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Corporation for National
|
||||
Research Initiatives, having an office at 1895 Preston White Drive,
|
||||
Reston, VA 20191 ("CNRI"), and the Individual or Organization
|
||||
("Licensee") accessing and otherwise using Python 1.6.1 software in
|
||||
source or binary form and its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, CNRI
|
||||
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||
license to reproduce, analyze, test, perform and/or display publicly,
|
||||
prepare derivative works, distribute, and otherwise use Python 1.6.1
|
||||
alone or in any derivative version, provided, however, that CNRI's
|
||||
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
|
||||
1995-2001 Corporation for National Research Initiatives; All Rights
|
||||
Reserved" are retained in Python 1.6.1 alone or in any derivative
|
||||
version prepared by Licensee. Alternately, in lieu of CNRI's License
|
||||
Agreement, Licensee may substitute the following text (omitting the
|
||||
quotes): "Python 1.6.1 is made available subject to the terms and
|
||||
conditions in CNRI's License Agreement. This Agreement together with
|
||||
Python 1.6.1 may be located on the internet using the following
|
||||
unique, persistent identifier (known as a handle): 1895.22/1013. This
|
||||
Agreement may also be obtained from a proxy server on the internet
|
||||
using the following URL: http://hdl.handle.net/1895.22/1013".
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python 1.6.1 or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python 1.6.1.
|
||||
|
||||
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
|
||||
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. This License Agreement shall be governed by the federal
|
||||
intellectual property law of the United States, including without
|
||||
limitation the federal copyright law, and, to the extent such
|
||||
U.S. federal law does not apply, by the law of the Commonwealth of
|
||||
Virginia, excluding Virginia's conflict of law provisions.
|
||||
Notwithstanding the foregoing, with regard to derivative works based
|
||||
on Python 1.6.1 that incorporate non-separable material that was
|
||||
previously distributed under the GNU General Public License (GPL), the
|
||||
law of the Commonwealth of Virginia shall govern this License
|
||||
Agreement only as to issues arising under or with respect to
|
||||
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
|
||||
License Agreement shall be deemed to create any relationship of
|
||||
agency, partnership, or joint venture between CNRI and Licensee. This
|
||||
License Agreement does not grant permission to use CNRI trademarks or
|
||||
trade name in a trademark sense to endorse or promote products or
|
||||
services of Licensee, or any third party.
|
||||
|
||||
8. By clicking on the "ACCEPT" button where indicated, or by copying,
|
||||
installing or otherwise using Python 1.6.1, Licensee agrees to be
|
||||
bound by the terms and conditions of this License Agreement.
|
||||
|
||||
ACCEPT
|
||||
|
||||
|
||||
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
||||
--------------------------------------------------
|
||||
|
||||
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
|
||||
The Netherlands. All rights reserved.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its
|
||||
documentation for any purpose and without fee is hereby granted,
|
||||
provided that the above copyright notice appear in all copies and that
|
||||
both that copyright notice and this permission notice appear in
|
||||
supporting documentation, and that the name of Stichting Mathematisch
|
||||
Centrum or CWI not be used in advertising or publicity pertaining to
|
||||
distribution of the software without specific, written prior
|
||||
permission.
|
||||
|
||||
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
|
||||
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
|
||||
Additional Conditions for this Windows binary build
|
||||
---------------------------------------------------
|
||||
|
||||
This program is linked with and uses Microsoft Distributable Code,
|
||||
copyrighted by Microsoft Corporation. The Microsoft Distributable Code
|
||||
is embedded in each .exe, .dll and .pyd file as a result of running
|
||||
the code through a linker.
|
||||
|
||||
If you further distribute programs that include the Microsoft
|
||||
Distributable Code, you must comply with the restrictions on
|
||||
distribution specified by Microsoft. In particular, you must require
|
||||
distributors and external end users to agree to terms that protect the
|
||||
Microsoft Distributable Code at least as much as Microsoft's own
|
||||
requirements for the Distributable Code. See Microsoft's documentation
|
||||
(included in its developer tools and on its website at microsoft.com)
|
||||
for specific details.
|
||||
|
||||
Redistribution of the Windows binary build of the Python interpreter
|
||||
complies with this agreement, provided that you do not:
|
||||
|
||||
- alter any copyright, trademark or patent notice in Microsoft's
|
||||
Distributable Code;
|
||||
|
||||
- use Microsoft's trademarks in your programs' names or in a way that
|
||||
suggests your programs come from or are endorsed by Microsoft;
|
||||
|
||||
- distribute Microsoft's Distributable Code to run on a platform other
|
||||
than Microsoft operating systems, run-time technologies or application
|
||||
platforms; or
|
||||
|
||||
- include Microsoft Distributable Code in malicious, deceptive or
|
||||
unlawful programs.
|
||||
|
||||
These restrictions apply only to the Microsoft Distributable Code as
|
||||
defined above, not to Python itself or any programs running on the
|
||||
Python interpreter. The redistribution of the Python interpreter and
|
||||
libraries is governed by the Python Software License included with this
|
||||
file, or by other licenses as marked.
|
||||
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
|
||||
This program, "bzip2", the associated library "libbzip2", and all
|
||||
documentation, are copyright (C) 1996-2019 Julian R Seward. All
|
||||
rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. The origin of this software must not be misrepresented; you must
|
||||
not claim that you wrote the original software. If you use this
|
||||
software in a product, an acknowledgment in the product
|
||||
documentation would be appreciated but is not required.
|
||||
|
||||
3. Altered source versions must be plainly marked as such, and must
|
||||
not be misrepresented as being the original software.
|
||||
|
||||
4. The name of the author may not be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
||||
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Julian Seward, jseward@acm.org
|
||||
bzip2/libbzip2 version 1.0.8 of 13 July 2019
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
|
||||
libffi - Copyright (c) 1996-2022 Anthony Green, Red Hat, Inc and others.
|
||||
See source files for details.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
``Software''), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
This software is copyrighted by the Regents of the University of
|
||||
California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState
|
||||
Corporation and other parties. The following terms apply to all files
|
||||
associated with the software unless explicitly disclaimed in
|
||||
individual files.
|
||||
|
||||
The authors hereby grant permission to use, copy, modify, distribute,
|
||||
and license this software and its documentation for any purpose, provided
|
||||
that existing copyright notices are retained in all copies and that this
|
||||
notice is included verbatim in any distributions. No written agreement,
|
||||
license, or royalty fee is required for any of the authorized uses.
|
||||
Modifications to this software may be copyrighted by their authors
|
||||
and need not follow the licensing terms described here, provided that
|
||||
the new terms are clearly indicated on the first page of each file where
|
||||
they apply.
|
||||
|
||||
IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
|
||||
FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||
ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
|
||||
DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
|
||||
IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
|
||||
NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
|
||||
MODIFICATIONS.
|
||||
|
||||
GOVERNMENT USE: If you are acquiring this software on behalf of the
|
||||
U.S. government, the Government shall have only "Restricted Rights"
|
||||
in the software and related documentation as defined in the Federal
|
||||
Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
|
||||
are acquiring the software on behalf of the Department of Defense, the
|
||||
software shall be classified as "Commercial Computer Software" and the
|
||||
Government shall have only "Restricted Rights" as defined in Clause
|
||||
252.227-7014 (b) (3) of DFARs. Notwithstanding the foregoing, the
|
||||
authors grant the U.S. Government and others acting in its behalf
|
||||
permission to use and distribute the software in accordance with the
|
||||
terms specified in this license.
|
||||
|
||||
This software is copyrighted by the Regents of the University of
|
||||
California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState
|
||||
Corporation, Apple Inc. and other parties. The following terms apply to
|
||||
all files associated with the software unless explicitly disclaimed in
|
||||
individual files.
|
||||
|
||||
The authors hereby grant permission to use, copy, modify, distribute,
|
||||
and license this software and its documentation for any purpose, provided
|
||||
that existing copyright notices are retained in all copies and that this
|
||||
notice is included verbatim in any distributions. No written agreement,
|
||||
license, or royalty fee is required for any of the authorized uses.
|
||||
Modifications to this software may be copyrighted by their authors
|
||||
and need not follow the licensing terms described here, provided that
|
||||
the new terms are clearly indicated on the first page of each file where
|
||||
they apply.
|
||||
|
||||
IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
|
||||
FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||
ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
|
||||
DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
|
||||
IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
|
||||
NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
|
||||
MODIFICATIONS.
|
||||
|
||||
GOVERNMENT USE: If you are acquiring this software on behalf of the
|
||||
U.S. government, the Government shall have only "Restricted Rights"
|
||||
in the software and related documentation as defined in the Federal
|
||||
Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
|
||||
are acquiring the software on behalf of the Department of Defense, the
|
||||
software shall be classified as "Commercial Computer Software" and the
|
||||
Government shall have only "Restricted Rights" as defined in Clause
|
||||
252.227-7013 (b) (3) of DFARs. Notwithstanding the foregoing, the
|
||||
authors grant the U.S. Government and others acting in its behalf
|
||||
permission to use and distribute the software in accordance with the
|
||||
terms specified in this license.
|
||||
|
||||
Copyright (c) 1993-1999 Ioi Kim Lam.
|
||||
Copyright (c) 2000-2001 Tix Project Group.
|
||||
Copyright (c) 2004 ActiveState
|
||||
|
||||
This software is copyrighted by the above entities
|
||||
and other parties. The following terms apply to all files associated
|
||||
with the software unless explicitly disclaimed in individual files.
|
||||
|
||||
The authors hereby grant permission to use, copy, modify, distribute,
|
||||
and license this software and its documentation for any purpose, provided
|
||||
that existing copyright notices are retained in all copies and that this
|
||||
notice is included verbatim in any distributions. No written agreement,
|
||||
license, or royalty fee is required for any of the authorized uses.
|
||||
Modifications to this software may be copyrighted by their authors
|
||||
and need not follow the licensing terms described here, provided that
|
||||
the new terms are clearly indicated on the first page of each file where
|
||||
they apply.
|
||||
|
||||
IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
|
||||
FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||
ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
|
||||
DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
|
||||
IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
|
||||
NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
|
||||
MODIFICATIONS.
|
||||
|
||||
GOVERNMENT USE: If you are acquiring this software on behalf of the
|
||||
U.S. government, the Government shall have only "Restricted Rights"
|
||||
in the software and related documentation as defined in the Federal
|
||||
Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
|
||||
are acquiring the software on behalf of the Department of Defense, the
|
||||
software shall be classified as "Commercial Computer Software" and the
|
||||
Government shall have only "Restricted Rights" as defined in Clause
|
||||
252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, the
|
||||
authors grant the U.S. Government and others acting in its behalf
|
||||
permission to use and distribute the software in accordance with the
|
||||
terms specified in this license.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Parts of this software are based on the Tcl/Tk software copyrighted by
|
||||
the Regents of the University of California, Sun Microsystems, Inc.,
|
||||
and other parties. The original license terms of the Tcl/Tk software
|
||||
distribution is included in the file docs/license.tcltk.
|
||||
|
||||
Parts of this software are based on the HTML Library software
|
||||
copyrighted by Sun Microsystems, Inc. The original license terms of
|
||||
the HTML Library software distribution is included in the file
|
||||
docs/license.html_lib.
|
||||
|
||||
BIN
tools/python-embed/Lib/site-packages/bin/pyserial-miniterm.exe
Normal file
BIN
tools/python-embed/Lib/site-packages/bin/pyserial-miniterm.exe
Normal file
Binary file not shown.
BIN
tools/python-embed/Lib/site-packages/bin/pyserial-ports.exe
Normal file
BIN
tools/python-embed/Lib/site-packages/bin/pyserial-ports.exe
Normal file
Binary file not shown.
@ -0,0 +1 @@
|
||||
pip
|
||||
@ -0,0 +1,132 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: bleak
|
||||
Version: 3.0.2
|
||||
Summary: Bluetooth Low Energy platform Agnostic Klient
|
||||
Author: Henrik Blidh
|
||||
Author-email: Henrik Blidh <henrik.blidh@nedomkull.com>
|
||||
License-Expression: MIT
|
||||
License-File: LICENSE
|
||||
Classifier: Framework :: AsyncIO
|
||||
Classifier: Operating System :: Microsoft :: Windows :: Windows 11
|
||||
Classifier: Operating System :: POSIX :: Linux
|
||||
Classifier: Operating System :: MacOS :: MacOS X
|
||||
Classifier: Operating System :: Android
|
||||
Requires-Dist: async-timeout>=3.0.0 ; python_full_version < '3.11'
|
||||
Requires-Dist: typing-extensions>=4.7.0 ; python_full_version < '3.12'
|
||||
Requires-Dist: pyobjc-core>=10.3 ; sys_platform == 'darwin'
|
||||
Requires-Dist: pyobjc-framework-corebluetooth>=10.3 ; sys_platform == 'darwin'
|
||||
Requires-Dist: pyobjc-framework-libdispatch>=10.3 ; sys_platform == 'darwin'
|
||||
Requires-Dist: winrt-runtime>=3.1 ; sys_platform == 'win32'
|
||||
Requires-Dist: winrt-windows-devices-bluetooth>=3.1 ; sys_platform == 'win32'
|
||||
Requires-Dist: winrt-windows-devices-bluetooth-advertisement>=3.1 ; sys_platform == 'win32'
|
||||
Requires-Dist: winrt-windows-devices-bluetooth-genericattributeprofile>=3.1 ; sys_platform == 'win32'
|
||||
Requires-Dist: winrt-windows-devices-enumeration>=3.1 ; sys_platform == 'win32'
|
||||
Requires-Dist: winrt-windows-devices-radios>=3.1 ; sys_platform == 'win32'
|
||||
Requires-Dist: winrt-windows-foundation>=3.1 ; sys_platform == 'win32'
|
||||
Requires-Dist: winrt-windows-foundation-collections>=3.1 ; sys_platform == 'win32'
|
||||
Requires-Dist: winrt-windows-storage-streams>=3.1 ; sys_platform == 'win32'
|
||||
Requires-Dist: dbus-fast>=1.83.0 ; sys_platform == 'linux'
|
||||
Requires-Dist: bleak-pythonista>=0.1.1 ; extra == 'pythonista'
|
||||
Requires-Python: >=3.10
|
||||
Project-URL: Homepage, https://github.com/hbldh/bleak
|
||||
Project-URL: Documentation, https://bleak.readthedocs.io
|
||||
Project-URL: Changelog, https://github.com/hbldh/bleak/blob/develop/CHANGELOG.rst
|
||||
Project-URL: Support, https://github.com/hbldh/bleak/discussions
|
||||
Project-URL: Issues, https://github.com/hbldh/bleak/issues
|
||||
Provides-Extra: pythonista
|
||||
Description-Content-Type: text/x-rst
|
||||
|
||||
=====
|
||||
Bleak
|
||||
=====
|
||||
|
||||
.. image:: https://github.com/hbldh/bleak/workflows/Build%20and%20Test/badge.svg
|
||||
:target: https://github.com/hbldh/bleak/actions?query=workflow%3A%22Build+and+Test%22
|
||||
:alt: Build and Test
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/bleak.svg
|
||||
:target: https://pypi.python.org/pypi/bleak
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/bleak.svg
|
||||
:target: https://pypi.python.org/pypi/bleak
|
||||
:alt: PyPI - Downloads
|
||||
|
||||
.. image:: https://readthedocs.org/projects/bleak/badge/?version=latest
|
||||
:target: https://bleak.readthedocs.io/en/latest/?badge=latest
|
||||
:alt: Documentation Status
|
||||
|
||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/psf/black
|
||||
|
||||
.. container::
|
||||
|
||||
.. image:: Bleak_logo2.png
|
||||
:target: https://github.com/hbldh/bleak
|
||||
:alt: Bleak Logo
|
||||
|
||||
Bleak is an acronym for Bluetooth Low Energy platform Agnostic Klient.
|
||||
|
||||
* Free software: MIT license
|
||||
* Documentation: https://bleak.readthedocs.io.
|
||||
|
||||
Bleak is a GATT client software, capable of connecting to BLE devices
|
||||
acting as GATT servers. It is designed to provide a asynchronous,
|
||||
cross-platform Python API to connect and communicate with e.g. sensors.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install bleak
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* Supports Windows 11, version 22000 and greater
|
||||
* Supports Linux distributions with BlueZ >= 5.55
|
||||
* OS X/macOS support via Core Bluetooth API, from at least OS X version 10.15
|
||||
* Android backend compatible with python-for-android
|
||||
|
||||
Bleak supports reading, writing and getting notifications from
|
||||
GATT servers, as well as a function for discovering BLE devices.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
To discover Bluetooth devices that can be connected to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
from bleak import BleakScanner
|
||||
|
||||
async def main():
|
||||
devices = await BleakScanner.discover()
|
||||
for d in devices:
|
||||
print(d)
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
Connect to a Bluetooth device and read its model number:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
from bleak import BleakClient
|
||||
|
||||
address = "24:71:89:cc:09:05"
|
||||
MODEL_NBR_UUID = "2A24"
|
||||
|
||||
async def main(address):
|
||||
async with BleakClient(address) as client:
|
||||
model_number = await client.read_gatt_char(MODEL_NBR_UUID)
|
||||
print(f"Model Number: {model_number.decode()}")
|
||||
|
||||
asyncio.run(main(address))
|
||||
|
||||
DO NOT NAME YOUR SCRIPT ``bleak.py``! It will cause a circular import error.
|
||||
|
||||
See examples folder for more code, for instance example code for connecting to a
|
||||
`TI SensorTag CC2650 <http://www.ti.com/ww/en/wireless_connectivity/sensortag/>`_
|
||||
@ -0,0 +1,97 @@
|
||||
bleak-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
bleak-3.0.2.dist-info/METADATA,sha256=-J8V2qKsJBXOvDOVL73TrXv4U67jyiDmbopo7BXa9AE,4698
|
||||
bleak-3.0.2.dist-info/RECORD,,
|
||||
bleak-3.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
bleak-3.0.2.dist-info/WHEEL,sha256=q5IF0q2xCp3ktUFRCVWsQLjl2ChNlWXBJtnI1LCGdJ8,80
|
||||
bleak-3.0.2.dist-info/licenses/LICENSE,sha256=xAKaK2Ozgkq2i-hB9BSt293iDLec2-Jy-oDAsqvmU3Q,1072
|
||||
bleak/__init__.py,sha256=ht6iQq9epvqQ7hz6A8KlQcw7fvT0efdqNvtaxTJyg2M,37965
|
||||
bleak/__pycache__/__init__.cpython-312.pyc,,
|
||||
bleak/__pycache__/_compat.cpython-312.pyc,,
|
||||
bleak/__pycache__/assigned_numbers.cpython-312.pyc,,
|
||||
bleak/__pycache__/exc.cpython-312.pyc,,
|
||||
bleak/__pycache__/uuids.cpython-312.pyc,,
|
||||
bleak/_compat.py,sha256=Vd1TpIlUIlg33OAsJjWy1H-SNZYrf7PkjPI2IFqfI60,988
|
||||
bleak/args/__init__.py,sha256=B60fxxXFh_1F0t56BQl0i2rYW2iQaMhMWk7CPEAGG4s,594
|
||||
bleak/args/__pycache__/__init__.cpython-312.pyc,,
|
||||
bleak/args/__pycache__/bluez.cpython-312.pyc,,
|
||||
bleak/args/__pycache__/corebluetooth.cpython-312.pyc,,
|
||||
bleak/args/__pycache__/winrt.cpython-312.pyc,,
|
||||
bleak/args/bluez.py,sha256=FXldHcGZ4aFtKA70kwX39dLBJNUJhk0p2gJobpx8vko,4122
|
||||
bleak/args/corebluetooth.py,sha256=HUlwGXtw2M6F_mGj07obYlwUALnWqR7o8BmDWKzuKuk,1226
|
||||
bleak/args/winrt.py,sha256=RPdm8I316SFKbkf_8w_FSb7uTKuD5wXHLGe6bcii97k,974
|
||||
bleak/assigned_numbers.py,sha256=jC3uihHGvozGmCdIZ34zwk-jeDyYj65shj8RvtI-lZw,2504
|
||||
bleak/backends/__init__.py,sha256=PSTvMjyTVGzk92mtxVW5Q7wlKcvocL9pK4y834eeK_8,1798
|
||||
bleak/backends/__pycache__/__init__.cpython-312.pyc,,
|
||||
bleak/backends/__pycache__/_manufacturers.cpython-312.pyc,,
|
||||
bleak/backends/__pycache__/_utils.cpython-312.pyc,,
|
||||
bleak/backends/__pycache__/characteristic.cpython-312.pyc,,
|
||||
bleak/backends/__pycache__/client.cpython-312.pyc,,
|
||||
bleak/backends/__pycache__/descriptor.cpython-312.pyc,,
|
||||
bleak/backends/__pycache__/device.cpython-312.pyc,,
|
||||
bleak/backends/__pycache__/scanner.cpython-312.pyc,,
|
||||
bleak/backends/__pycache__/service.cpython-312.pyc,,
|
||||
bleak/backends/_manufacturers.py,sha256=NjCqNI7SCtSzNf5OmGiFM7ayq9t_5eKNecMnyF4kehU,67669
|
||||
bleak/backends/_utils.py,sha256=KkWbkLbOhawaQ-3iKcmvJVen2Adt4QxstcsvE0l-mII,1047
|
||||
bleak/backends/bluezdbus/__init__.py,sha256=isYF5VYdJI9EUUD9Z0AbQ3WDv4IBuaLBv2epV2DS8FM,21
|
||||
bleak/backends/bluezdbus/__pycache__/__init__.cpython-312.pyc,,
|
||||
bleak/backends/bluezdbus/__pycache__/advertisement_monitor.cpython-312.pyc,,
|
||||
bleak/backends/bluezdbus/__pycache__/client.cpython-312.pyc,,
|
||||
bleak/backends/bluezdbus/__pycache__/defs.cpython-312.pyc,,
|
||||
bleak/backends/bluezdbus/__pycache__/manager.cpython-312.pyc,,
|
||||
bleak/backends/bluezdbus/__pycache__/scanner.cpython-312.pyc,,
|
||||
bleak/backends/bluezdbus/__pycache__/signals.cpython-312.pyc,,
|
||||
bleak/backends/bluezdbus/__pycache__/utils.cpython-312.pyc,,
|
||||
bleak/backends/bluezdbus/__pycache__/version.cpython-312.pyc,,
|
||||
bleak/backends/bluezdbus/advertisement_monitor.py,sha256=hfwHUzA8kNCv9OyUJSAUJ2mU0Kayd2yplfhLVL2ief4,3974
|
||||
bleak/backends/bluezdbus/client.py,sha256=72X6l4RBBSHBDVyeyi9jMQ4pnX9CeLr3Uw-B1XiIz5I,39932
|
||||
bleak/backends/bluezdbus/defs.py,sha256=OwPfz0ZlMx7vBWI-8t_1tfvqu05PG8dgUG_c2JMtXSY,4373
|
||||
bleak/backends/bluezdbus/manager.py,sha256=Kn4g7zqWsBroPfX8v8IwiEnv6qtikhaSx2ymmighO9s,44905
|
||||
bleak/backends/bluezdbus/scanner.py,sha256=u3NJaQWRBQqYWRXr_Pe8lu_LoETdm1R0Oh2MGdRfzzo,8234
|
||||
bleak/backends/bluezdbus/signals.py,sha256=2iOPVBtHSQBIACqGhC5v8Bk_oFgkPUuR6kd845sIM2w,6252
|
||||
bleak/backends/bluezdbus/utils.py,sha256=eZNw8GK5WlMVbGJ3NdloSgC3twpl5y6Y7VL1eA-QfMI,4933
|
||||
bleak/backends/bluezdbus/version.py,sha256=GQbwpYMBfv8LCGpEjynUyvk1sIudceLz_FNgVfxrE4I,1957
|
||||
bleak/backends/characteristic.py,sha256=jhVkm_Ce3REfmKcjArDHUOTdWxfAugfSr0EcZY6BALc,5112
|
||||
bleak/backends/client.py,sha256=_wSF61BwfMWO0kzgcQjhLBinxZKK1fZZiyyX2LjTrGE,8442
|
||||
bleak/backends/corebluetooth/CentralManagerDelegate.py,sha256=3YvbHNQ3jVJ4JqVJK7ujWy2ZsROwBnpawTjSf1tZfJM,14226
|
||||
bleak/backends/corebluetooth/PeripheralDelegate.py,sha256=KdcZ2KbSmiJvULmFqdpT1fZZeHATzz5T7rnoHA8kn0c,24743
|
||||
bleak/backends/corebluetooth/__init__.py,sha256=e3h7IH-yrKjTvT4WoK2g-Azrewm18Rg0ln6VsmZ3hbU,284
|
||||
bleak/backends/corebluetooth/__pycache__/CentralManagerDelegate.cpython-312.pyc,,
|
||||
bleak/backends/corebluetooth/__pycache__/PeripheralDelegate.cpython-312.pyc,,
|
||||
bleak/backends/corebluetooth/__pycache__/__init__.cpython-312.pyc,,
|
||||
bleak/backends/corebluetooth/__pycache__/client.cpython-312.pyc,,
|
||||
bleak/backends/corebluetooth/__pycache__/scanner.cpython-312.pyc,,
|
||||
bleak/backends/corebluetooth/__pycache__/utils.cpython-312.pyc,,
|
||||
bleak/backends/corebluetooth/client.py,sha256=HIcltkuujrV7X027AzuzUWqWcJxAYf6F_Qw5t8kpul8,14472
|
||||
bleak/backends/corebluetooth/scanner.py,sha256=taMAE0h2DzeypUC7HJehgFg5XNfho6RqjyvCUQYU-sI,6554
|
||||
bleak/backends/corebluetooth/utils.py,sha256=EhtJKMbLvBfBO_yVdYF4PZ4EN8drBpKZlzf38h3iVm8,3602
|
||||
bleak/backends/descriptor.py,sha256=ZxSU30c7aN_N6Bd_vVjF9OmmDvDsRdQ-kEODgTNHsUI,4501
|
||||
bleak/backends/device.py,sha256=ip3sfYapIlwwl2VcSD8F4l2v9WV5PBEg2hgSqk1re3Y,1230
|
||||
bleak/backends/p4android/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
bleak/backends/p4android/__pycache__/__init__.cpython-312.pyc,,
|
||||
bleak/backends/p4android/__pycache__/client.cpython-312.pyc,,
|
||||
bleak/backends/p4android/__pycache__/defs.cpython-312.pyc,,
|
||||
bleak/backends/p4android/__pycache__/scanner.cpython-312.pyc,,
|
||||
bleak/backends/p4android/__pycache__/utils.cpython-312.pyc,,
|
||||
bleak/backends/p4android/client.py,sha256=WCgYmdwOPSGNeKJFXH21j7tVkroIQ5lRle8cXtMroQA,17814
|
||||
bleak/backends/p4android/defs.py,sha256=pO4gpTx8tu2n7kXgV5zDWwd26_Hh-myPAy73TYQsS-4,3283
|
||||
bleak/backends/p4android/java/com/github/hbldh/bleak/PythonBluetoothGattCallback.java,sha256=2bZPGMQB3EZt7uW2EV7ST2EVwtWnDa04iTOtOWHuWqc,2897
|
||||
bleak/backends/p4android/java/com/github/hbldh/bleak/PythonScanCallback.java,sha256=vtiCeSA4Sn8ScjqDiCgXG21v1ADQD8SEekRdJQ3QAB8,909
|
||||
bleak/backends/p4android/recipes/bleak/__init__.py,sha256=pnT6pxILzrII_cl8sAFIN-_it4dPRbXbW5mfsx4sotA,2022
|
||||
bleak/backends/p4android/recipes/bleak/__pycache__/__init__.cpython-312.pyc,,
|
||||
bleak/backends/p4android/recipes/bleak/__pycache__/fix_setup.cpython-312.pyc,,
|
||||
bleak/backends/p4android/recipes/bleak/fix_setup.py,sha256=KMfVhr4Um91aKxBAYxXIGcjH98CiTFXBRJwEAwUJriA,244
|
||||
bleak/backends/p4android/scanner.py,sha256=cXCMTlYRotPFUBzlauYebfSy0FmHKuMEzS9edYsYG-0,10842
|
||||
bleak/backends/p4android/utils.py,sha256=mkrjzJBHnkUri41Yl6qgKjllz-LXQqNULMj6DW5HDwo,3330
|
||||
bleak/backends/scanner.py,sha256=zryWb4-YwRoEmVV1fc93u_qtiAOVl-ku8nP-Mlg650k,10184
|
||||
bleak/backends/service.py,sha256=hhjKGkmfgsKoNSi3Iwhef5UWF8PC5-zhWDi0nB0IFeI,7576
|
||||
bleak/backends/winrt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
bleak/backends/winrt/__pycache__/__init__.cpython-312.pyc,,
|
||||
bleak/backends/winrt/__pycache__/client.cpython-312.pyc,,
|
||||
bleak/backends/winrt/__pycache__/scanner.cpython-312.pyc,,
|
||||
bleak/backends/winrt/__pycache__/util.cpython-312.pyc,,
|
||||
bleak/backends/winrt/client.py,sha256=5QiEcCYjNZvYF6nvIGxatNEinJPo9ed34YT_bIdMMtQ,43313
|
||||
bleak/backends/winrt/scanner.py,sha256=Y6nlml8jTO_2Ci6JjsZpeLdeAMuAOXxE458hHg-H68E,13046
|
||||
bleak/backends/winrt/util.py,sha256=bNT0ivkVEoO4oteB1cRBBTADHolluccx7AEVgjLa4RM,6486
|
||||
bleak/exc.py,sha256=voa8y6eWdkvMJcEKAO7rZqgAgDgGtCEOM5rZ3JECVq4,11504
|
||||
bleak/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
bleak/uuids.py,sha256=ind-60OjNfoEXLzhCfYJWTGhLuTgwJuN0vLPnvZL8Oc,49038
|
||||
@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: uv 0.11.8
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
@ -0,0 +1,11 @@
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020, Henrik Blidh
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
1008
tools/python-embed/Lib/site-packages/bleak/__init__.py
Normal file
1008
tools/python-embed/Lib/site-packages/bleak/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
27
tools/python-embed/Lib/site-packages/bleak/_compat.py
Normal file
27
tools/python-embed/Lib/site-packages/bleak/_compat.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""
|
||||
Python version compatibility imports.
|
||||
|
||||
These will be removed when support for older Python versions is dropped.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from async_timeout import timeout as timeout
|
||||
from typing_extensions import Never as Never
|
||||
from typing_extensions import Self as Self
|
||||
from typing_extensions import TypeVarTuple as TypeVarTuple
|
||||
from typing_extensions import Unpack as Unpack
|
||||
from typing_extensions import assert_never as assert_never
|
||||
else:
|
||||
from asyncio import timeout as timeout # noqa: F401
|
||||
from typing import Never as Never # noqa: F401
|
||||
from typing import Self as Self # noqa: F401
|
||||
from typing import TypeVarTuple as TypeVarTuple # noqa: F401
|
||||
from typing import Unpack as Unpack # noqa: F401
|
||||
from typing import assert_never as assert_never # noqa: F401
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
from typing_extensions import override as override
|
||||
else:
|
||||
from typing import override as override # noqa: F401
|
||||
21
tools/python-embed/Lib/site-packages/bleak/args/__init__.py
Normal file
21
tools/python-embed/Lib/site-packages/bleak/args/__init__.py
Normal file
@ -0,0 +1,21 @@
|
||||
import sys
|
||||
from collections.abc import Sized
|
||||
from typing import Protocol
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
# mypy and pyright object to Buffer being both ABC and Protocol. Work around
|
||||
# by just inheriting from Protocol here. typing_extensions.Buffer is just
|
||||
# abc.ABC.
|
||||
class BufferProtocol(Protocol):
|
||||
def __buffer__(self, flags: int, /) -> memoryview: ...
|
||||
|
||||
else:
|
||||
from collections.abc import Buffer as BufferProtocol
|
||||
|
||||
|
||||
class SizedBuffer(BufferProtocol, Sized, Protocol):
|
||||
"""
|
||||
Protocol for types that are both Buffer and Sized.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
"""
|
||||
150
tools/python-embed/Lib/site-packages/bleak/args/bluez.py
Normal file
150
tools/python-embed/Lib/site-packages/bleak/args/bluez.py
Normal file
@ -0,0 +1,150 @@
|
||||
"""
|
||||
-----------------------
|
||||
BlueZ backend arguments
|
||||
-----------------------
|
||||
"""
|
||||
|
||||
from typing import NamedTuple, TypedDict, Union
|
||||
|
||||
from bleak.assigned_numbers import AdvertisementDataType
|
||||
|
||||
|
||||
class BlueZDiscoveryFilters(TypedDict, total=False):
|
||||
"""
|
||||
Dictionary of arguments for the ``org.bluez.Adapter1.SetDiscoveryFilter``
|
||||
D-Bus method.
|
||||
|
||||
https://github.com/bluez/bluez/blob/master/doc/org.bluez.Adapter.rst#void-setdiscoveryfilterdict-filter
|
||||
"""
|
||||
|
||||
UUIDs: list[str]
|
||||
"""
|
||||
Filter by service UUIDs, empty means match _any_ UUID.
|
||||
|
||||
Normally, the ``service_uuids`` argument of :class:`bleak.BleakScanner`
|
||||
is used instead.
|
||||
"""
|
||||
RSSI: int
|
||||
"""
|
||||
RSSI threshold value.
|
||||
"""
|
||||
Pathloss: int
|
||||
"""
|
||||
Pathloss threshold value.
|
||||
"""
|
||||
Transport: str
|
||||
"""
|
||||
Transport parameter determines the type of scan.
|
||||
|
||||
This should not be used since it is required to be set to ``"le"``.
|
||||
"""
|
||||
DuplicateData: bool
|
||||
"""
|
||||
Disables duplicate detection of advertisement data.
|
||||
|
||||
This does not affect the ``Filter Duplicates`` parameter of the ``LE Set Scan Enable``
|
||||
HCI command to the Bluetooth adapter!
|
||||
|
||||
Although the default value for BlueZ is ``True``, Bleak sets this to ``False`` by default.
|
||||
"""
|
||||
Discoverable: bool
|
||||
"""
|
||||
Make adapter discoverable while discovering,
|
||||
if the adapter is already discoverable setting
|
||||
this filter won't do anything.
|
||||
"""
|
||||
Pattern: str
|
||||
"""
|
||||
Discover devices where the pattern matches
|
||||
either the prefix of the address or
|
||||
device name which is convenient way to limited
|
||||
the number of device objects created during a
|
||||
discovery.
|
||||
"""
|
||||
|
||||
|
||||
class OrPattern(NamedTuple):
|
||||
"""
|
||||
BlueZ advertisement monitor or-pattern.
|
||||
|
||||
https://github.com/bluez/bluez/blob/master/doc/org.bluez.AdvertisementMonitor.rst#arrayuint8-uint8-arraybyte-patterns-read-only-optional
|
||||
"""
|
||||
|
||||
start_position: int
|
||||
ad_data_type: AdvertisementDataType
|
||||
content_of_pattern: bytes
|
||||
|
||||
|
||||
# Windows has a similar structure, so we allow generic tuple for cross-platform compatibility
|
||||
OrPatternLike = Union[OrPattern, tuple[int, AdvertisementDataType, bytes]]
|
||||
|
||||
|
||||
class BlueZScannerArgs(TypedDict, total=False):
|
||||
"""
|
||||
:class:`BleakScanner` args that are specific to the BlueZ backend.
|
||||
"""
|
||||
|
||||
adapter: str
|
||||
"""
|
||||
Bluetooth adapter to use for discovery, e.g. "hci0".
|
||||
|
||||
.. tip:: If you have multiple Bluetooth adapters, they may not always be
|
||||
assigned the same ``hciX`` name across reboots. In that case, you can
|
||||
use udev to look up the name based on other properties like the USB
|
||||
vendor and product ID.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
|
||||
filters: BlueZDiscoveryFilters
|
||||
"""
|
||||
Filters to pass to the adapter SetDiscoveryFilter D-Bus method.
|
||||
|
||||
Only used for active scanning.
|
||||
"""
|
||||
|
||||
or_patterns: list[OrPatternLike]
|
||||
"""
|
||||
Or patterns to pass to the AdvertisementMonitor1 D-Bus interface.
|
||||
|
||||
Only used for passive scanning.
|
||||
"""
|
||||
|
||||
|
||||
class BlueZClientArgs(TypedDict, total=False):
|
||||
"""
|
||||
:class:`bleak.BleakClient` args that are specific to the BlueZ backend.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
|
||||
adapter: str
|
||||
"""
|
||||
Bluetooth adapter to use for connection, e.g. "hci0".
|
||||
|
||||
.. tip:: If you have multiple Bluetooth adapters, they may not always be
|
||||
assigned the same ``hciX`` name across reboots. In that case, you can
|
||||
use udev to look up the name based on other properties like the USB
|
||||
vendor and product ID.
|
||||
"""
|
||||
|
||||
|
||||
class BlueZNotifyArgs(TypedDict, total=False):
|
||||
"""
|
||||
:meth:`bleak.BleakClient.start_notify` method args that are specific to the
|
||||
BlueZ backend.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
"""
|
||||
|
||||
use_start_notify: bool
|
||||
"""
|
||||
If false, use the "AcquireNotify" D-Bus method instead of "StartNotify" to
|
||||
subscribe to notifications. The default is to use "StartNotify" for better
|
||||
compatibility with most BLE devices.
|
||||
|
||||
see :ref:`linux-start-notify` for more details.
|
||||
|
||||
.. versionchanged:: 3.0.2
|
||||
The default value was changed from ``False`` to ``True``.
|
||||
"""
|
||||
@ -0,0 +1,41 @@
|
||||
"""
|
||||
-------------------------------
|
||||
CoreBluetooth backend arguments
|
||||
-------------------------------
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Optional, TypedDict
|
||||
|
||||
|
||||
class CBScannerArgs(TypedDict, total=False):
|
||||
"""
|
||||
Platform-specific :class:`BleakScanner` args for the CoreBluetooth backend.
|
||||
"""
|
||||
|
||||
use_bdaddr: bool
|
||||
"""
|
||||
If true, use Bluetooth address instead of UUID.
|
||||
|
||||
.. warning:: This uses an undocumented IOBluetooth API to get the Bluetooth
|
||||
address and may break in the future macOS releases. `It is known to not
|
||||
work on macOS 10.15 <https://github.com/hbldh/bleak/issues/1286>`_.
|
||||
"""
|
||||
|
||||
|
||||
NotificationDiscriminator = Callable[[bytes], bool]
|
||||
|
||||
|
||||
class CBStartNotifyArgs(TypedDict, total=False):
|
||||
"""CoreBluetooth backend-specific dictionary of arguments for the
|
||||
:meth:`bleak.BleakClient.start_notify` method.
|
||||
"""
|
||||
|
||||
notification_discriminator: Optional[NotificationDiscriminator]
|
||||
"""
|
||||
A function that takes a single argument of a characteristic value
|
||||
and returns ``True`` if the value is from a notification or
|
||||
``False`` if the value is from a read response.
|
||||
|
||||
.. seealso:: :ref:`cb-notification-discriminator` for more info.
|
||||
"""
|
||||
32
tools/python-embed/Lib/site-packages/bleak/args/winrt.py
Normal file
32
tools/python-embed/Lib/site-packages/bleak/args/winrt.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""
|
||||
-----------------------
|
||||
WinRT backend arguments
|
||||
-----------------------
|
||||
"""
|
||||
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
|
||||
class WinRTClientArgs(TypedDict, total=False):
|
||||
"""
|
||||
Windows-specific arguments for :class:`BleakClient`.
|
||||
"""
|
||||
|
||||
address_type: Literal["public", "random"]
|
||||
"""
|
||||
Can either be ``"public"`` or ``"random"``, depending on the required address
|
||||
type needed to connect to your device.
|
||||
"""
|
||||
|
||||
use_cached_services: bool
|
||||
"""
|
||||
``True`` allows Windows to fetch the services, characteristics and descriptors
|
||||
from the Windows cache instead of reading them from the device. Can be very
|
||||
much faster for known, unchanging devices, but not recommended for DIY peripherals
|
||||
where the GATT layout can change between connections.
|
||||
|
||||
``False`` will force the attribute database to be read from the remote device
|
||||
instead of using the OS cache.
|
||||
|
||||
If omitted, the OS Bluetooth stack will do what it thinks is best.
|
||||
"""
|
||||
@ -0,0 +1,93 @@
|
||||
"""
|
||||
Bluetooth Assigned Numbers
|
||||
--------------------------
|
||||
|
||||
This module contains useful assigned numbers from the Bluetooth spec.
|
||||
|
||||
See <https://www.bluetooth.com/specifications/assigned-numbers/>.
|
||||
"""
|
||||
|
||||
from enum import IntEnum
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class AdvertisementDataType(IntEnum):
|
||||
"""
|
||||
Generic Access Profile advertisement data types.
|
||||
|
||||
`Source <https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Generic%20Access%20Profile.pdf>`.
|
||||
|
||||
.. versionadded:: 0.15
|
||||
"""
|
||||
|
||||
FLAGS = 0x01
|
||||
INCOMPLETE_LIST_SERVICE_UUID16 = 0x02
|
||||
COMPLETE_LIST_SERVICE_UUID16 = 0x03
|
||||
INCOMPLETE_LIST_SERVICE_UUID32 = 0x04
|
||||
COMPLETE_LIST_SERVICE_UUID32 = 0x05
|
||||
INCOMPLETE_LIST_SERVICE_UUID128 = 0x06
|
||||
COMPLETE_LIST_SERVICE_UUID128 = 0x07
|
||||
SHORTENED_LOCAL_NAME = 0x08
|
||||
COMPLETE_LOCAL_NAME = 0x09
|
||||
TX_POWER_LEVEL = 0x0A
|
||||
CLASS_OF_DEVICE = 0x0D
|
||||
|
||||
SERVICE_DATA_UUID16 = 0x16
|
||||
SERVICE_DATA_UUID32 = 0x20
|
||||
SERVICE_DATA_UUID128 = 0x21
|
||||
|
||||
MANUFACTURER_SPECIFIC_DATA = 0xFF
|
||||
|
||||
|
||||
# NOTE: these must match BlueZ name mapping
|
||||
CharacteristicPropertyName = Literal[
|
||||
"broadcast",
|
||||
"read",
|
||||
"write-without-response",
|
||||
"write",
|
||||
"notify",
|
||||
"indicate",
|
||||
"authenticated-signed-writes",
|
||||
"extended-properties",
|
||||
"reliable-write",
|
||||
"writable-auxiliaries",
|
||||
"encrypt-read",
|
||||
"encrypt-write",
|
||||
# "encrypt-notify" and "encrypt-indicate" are server-only
|
||||
"encrypt-authenticated-read",
|
||||
"encrypt-authenticated-write",
|
||||
# "encrypt-authenticated-notify", "encrypt-authenticated-indicate",
|
||||
# "secure-read", "secure-write", "secure-notify", "secure-indicate"
|
||||
# are server-only
|
||||
"authorize",
|
||||
]
|
||||
|
||||
CHARACTERISTIC_PROPERTIES: dict[int, CharacteristicPropertyName] = {
|
||||
0x1: "broadcast",
|
||||
0x2: "read",
|
||||
0x4: "write-without-response",
|
||||
0x8: "write",
|
||||
0x10: "notify",
|
||||
0x20: "indicate",
|
||||
0x40: "authenticated-signed-writes",
|
||||
0x80: "extended-properties",
|
||||
0x100: "reliable-write",
|
||||
0x200: "writable-auxiliaries",
|
||||
}
|
||||
|
||||
|
||||
def gatt_char_props_to_strs(
|
||||
props: int,
|
||||
) -> frozenset[CharacteristicPropertyName]:
|
||||
"""
|
||||
Convert a GATT characteristic properties bitmask to a set of strings.
|
||||
|
||||
Args:
|
||||
props: The GATT characteristic properties bitmask.
|
||||
|
||||
Returns:
|
||||
A set of strings representing the GATT characteristic properties.
|
||||
"""
|
||||
return frozenset(
|
||||
CHARACTERISTIC_PROPERTIES[i] for i in (1 << n for n in range(16)) if props & i
|
||||
)
|
||||
@ -0,0 +1,75 @@
|
||||
"""
|
||||
Communicating with Bluetooth hardware requires calling OS-specific APIs. These
|
||||
are abstracted as "backends" in Bleak.
|
||||
|
||||
The backend will be automatically selected based on the operating system Bleak
|
||||
is running on. In some cases, this may also depend on a specific runtime, like
|
||||
Pythonista on iOS.
|
||||
"""
|
||||
|
||||
import enum
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from bleak.exc import BleakError
|
||||
|
||||
|
||||
class BleakBackend(str, enum.Enum):
|
||||
"""
|
||||
Identifiers for available built-in Bleak backends.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
P4ANDROID = "p4android"
|
||||
"""
|
||||
Python for Android backend.
|
||||
"""
|
||||
|
||||
BLUEZ_DBUS = "bluez_dbus"
|
||||
"""
|
||||
BlueZ D-Bus backend for Linux.
|
||||
"""
|
||||
|
||||
PYTHONISTA_CB = "pythonista_cb"
|
||||
"""
|
||||
Pythonista CoreBluetooth backend for iOS and macOS.
|
||||
"""
|
||||
|
||||
CORE_BLUETOOTH = "core_bluetooth"
|
||||
"""
|
||||
CoreBluetooth backend for macOS.
|
||||
"""
|
||||
|
||||
WIN_RT = "win_rt"
|
||||
"""
|
||||
Windows Runtime backend for Windows.
|
||||
"""
|
||||
|
||||
|
||||
def get_default_backend() -> BleakBackend:
|
||||
"""
|
||||
Returns the preferred backend for the current platform/environment.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
if os.environ.get("P4A_BOOTSTRAP") is not None:
|
||||
return BleakBackend.P4ANDROID
|
||||
|
||||
if platform.system() == "Linux":
|
||||
return BleakBackend.BLUEZ_DBUS
|
||||
|
||||
if sys.platform == "ios" and "Pythonista3.app" in sys.executable:
|
||||
# Must be resolved before checking for "Darwin" (macOS),
|
||||
# as both the Pythonista app for iOS and macOS
|
||||
# return "Darwin" from platform.system()
|
||||
return BleakBackend.PYTHONISTA_CB
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
return BleakBackend.CORE_BLUETOOTH
|
||||
|
||||
if platform.system() == "Windows":
|
||||
return BleakBackend.WIN_RT
|
||||
|
||||
raise BleakError(f"Unsupported platform: {platform.system()}")
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,38 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from typing import Any, ParamSpec, TypeVar
|
||||
|
||||
from bleak._compat import TypeVarTuple, Unpack
|
||||
|
||||
_Ts = TypeVarTuple("_Ts")
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def try_call_soon_threadsafe(
|
||||
event_loop: asyncio.AbstractEventLoop,
|
||||
callback: Callable[[Unpack[_Ts]], Any],
|
||||
*args: Unpack[_Ts],
|
||||
) -> None:
|
||||
"""Call a callback on the event loop thread, handling closed loop errors gracefully."""
|
||||
try:
|
||||
event_loop.call_soon_threadsafe(callback, *args)
|
||||
except RuntimeError:
|
||||
# Likely caused by loop being closed
|
||||
logger.debug("unraisable exception", exc_info=True) # pragma: no cover
|
||||
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
_TReturn = TypeVar("_TReturn")
|
||||
|
||||
|
||||
def external_thread_callback(func: Callable[_P, _TReturn]) -> Callable[_P, _TReturn]:
|
||||
"""
|
||||
Decorator for callbacks invoked from external non-Python thread.
|
||||
|
||||
This is a no-op placeholder that can be overridden in tests to enable
|
||||
coverage tracing for external threads.
|
||||
"""
|
||||
return func
|
||||
@ -0,0 +1 @@
|
||||
"""BlueZ backend."""
|
||||
@ -0,0 +1,131 @@
|
||||
"""
|
||||
Advertisement Monitor
|
||||
---------------------
|
||||
|
||||
This module contains types associated with the BlueZ D-Bus `advertisement
|
||||
monitor api <https://github.com/bluez/bluez/blob/master/doc/org.bluez.AdvertisementMonitor.rst>`.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "linux":
|
||||
assert False, "This backend is only available on Linux"
|
||||
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, no_type_check
|
||||
from warnings import warn
|
||||
|
||||
from dbus_fast import PropertyAccess
|
||||
from dbus_fast.service import ServiceInterface, dbus_property, method
|
||||
|
||||
from bleak.args.bluez import OrPattern as _OrPattern
|
||||
from bleak.args.bluez import OrPatternLike as _OrPatternLike
|
||||
from bleak.backends.bluezdbus import defs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEPRECATED: dict[str, Any] = {
|
||||
"OrPattern": _OrPattern,
|
||||
"OrPatternLike": _OrPatternLike,
|
||||
}
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
if value := _DEPRECATED.get(name):
|
||||
warn(
|
||||
f"importing {name} from bleak.backends.bluezdbus.advertisement_monitor is deprecated, use bleak.args.bluez instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return value
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
class AdvertisementMonitor(ServiceInterface):
|
||||
"""
|
||||
Implementation of the org.bluez.AdvertisementMonitor1 D-Bus interface.
|
||||
|
||||
The BlueZ advertisement monitor API design seems to be just for device
|
||||
presence (is it in range or out of range), but this isn't really what
|
||||
we want in Bleak, we want to monitor changes in advertisement data, just
|
||||
like in active scanning.
|
||||
|
||||
So the only thing we are using here is the "or_patterns" since it is
|
||||
currently required, but really we don't need that either. Hopefully an
|
||||
"all" "Type" could be added to BlueZ in the future.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
or_patterns: Iterable[_OrPatternLike],
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
or_patterns:
|
||||
List of or patterns that will be returned by the ``Patterns`` property.
|
||||
"""
|
||||
super().__init__(defs.ADVERTISEMENT_MONITOR_INTERFACE)
|
||||
# dbus_fast marshaling requires list instead of tuple
|
||||
self._or_patterns = [list(p) for p in or_patterns]
|
||||
|
||||
@method()
|
||||
def Release(self):
|
||||
logger.debug("Release")
|
||||
|
||||
@method()
|
||||
def Activate(self):
|
||||
logger.debug("Activate")
|
||||
|
||||
# REVISIT: mypy is broke, so we have to add redundant @no_type_check
|
||||
# https://github.com/python/mypy/issues/6583
|
||||
|
||||
@method()
|
||||
@no_type_check
|
||||
def DeviceFound(self, device: "o"): # noqa: F821
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("DeviceFound %s", device)
|
||||
|
||||
@method()
|
||||
@no_type_check
|
||||
def DeviceLost(self, device: "o"): # noqa: F821
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("DeviceLost %s", device)
|
||||
|
||||
@dbus_property(PropertyAccess.READ)
|
||||
@no_type_check
|
||||
def Type(self) -> "s": # noqa: F821
|
||||
# this is currently the only type supported in BlueZ
|
||||
return "or_patterns"
|
||||
|
||||
@dbus_property(PropertyAccess.READ, disabled=True)
|
||||
@no_type_check
|
||||
def RSSILowThreshold(self) -> "n": # noqa: F821
|
||||
...
|
||||
|
||||
@dbus_property(PropertyAccess.READ, disabled=True)
|
||||
@no_type_check
|
||||
def RSSIHighThreshold(self) -> "n": # noqa: F821
|
||||
...
|
||||
|
||||
@dbus_property(PropertyAccess.READ, disabled=True)
|
||||
@no_type_check
|
||||
def RSSILowTimeout(self) -> "q": # noqa: F821
|
||||
...
|
||||
|
||||
@dbus_property(PropertyAccess.READ, disabled=True)
|
||||
@no_type_check
|
||||
def RSSIHighTimeout(self) -> "q": # noqa: F821
|
||||
...
|
||||
|
||||
@dbus_property(PropertyAccess.READ, disabled=True)
|
||||
@no_type_check
|
||||
def RSSISamplingPeriod(self) -> "q": # noqa: F821
|
||||
...
|
||||
|
||||
@dbus_property(PropertyAccess.READ)
|
||||
@no_type_check
|
||||
def Patterns(self) -> "a(yyay)": # noqa: F821
|
||||
return self._or_patterns
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,158 @@
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
from bleak.assigned_numbers import CharacteristicPropertyName
|
||||
|
||||
# DBus Interfaces
|
||||
OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager"
|
||||
PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties"
|
||||
|
||||
# Bluez specific DBUS
|
||||
BLUEZ_SERVICE = "org.bluez"
|
||||
ADAPTER_INTERFACE = "org.bluez.Adapter1"
|
||||
ADVERTISEMENT_MONITOR_INTERFACE = "org.bluez.AdvertisementMonitor1"
|
||||
ADVERTISEMENT_MONITOR_MANAGER_INTERFACE = "org.bluez.AdvertisementMonitorManager1"
|
||||
DEVICE_INTERFACE = "org.bluez.Device1"
|
||||
BATTERY_INTERFACE = "org.bluez.Battery1"
|
||||
|
||||
# GATT interfaces
|
||||
GATT_MANAGER_INTERFACE = "org.bluez.GattManager1"
|
||||
GATT_PROFILE_INTERFACE = "org.bluez.GattProfile1"
|
||||
GATT_SERVICE_INTERFACE = "org.bluez.GattService1"
|
||||
GATT_CHARACTERISTIC_INTERFACE = "org.bluez.GattCharacteristic1"
|
||||
GATT_DESCRIPTOR_INTERFACE = "org.bluez.GattDescriptor1"
|
||||
|
||||
# BlueZ error names
|
||||
BLUEZ_ERROR_DOES_NOT_EXIST = "org.bluez.Error.DoesNotExist"
|
||||
BLUEZ_ERROR_FAILED = "org.bluez.Error.Failed"
|
||||
BLUEZ_ERROR_IMPROPERLY_CONFIGURED = "org.bluez.Error.ImproperlyConfigured"
|
||||
BLUEZ_ERROR_IN_PROGRESS = "org.bluez.Error.InProgress"
|
||||
BLUEZ_ERROR_INVALID_ARGUMENT = "org.bluez.Error.InvalidArguments"
|
||||
BLUEZ_ERROR_INVALID_OFFSET = "org.bluez.Error.InvalidOffset"
|
||||
BLUEZ_ERROR_INVALID_VALUE_LENGTH = "org.bluez.Error.InvalidValueLength"
|
||||
BLUEZ_ERROR_NOT_AUTHORIZED = "org.bluez.Error.NotAuthorized"
|
||||
BLUEZ_ERROR_NOT_PERMITTED = "org.bluez.Error.NotPermitted"
|
||||
BLUEZ_ERROR_NOT_READY = "org.bluez.Error.NotReady"
|
||||
BLUEZ_ERROR_NOT_SUPPORTED = "org.bluez.Error.NotSupported"
|
||||
|
||||
# D-Bus properties for interfaces
|
||||
# https://github.com/bluez/bluez/blob/master/doc/org.bluez.Adapter.rst
|
||||
|
||||
|
||||
class Adapter1(TypedDict):
|
||||
Address: str
|
||||
Name: str
|
||||
Alias: str
|
||||
Class: int
|
||||
Powered: bool
|
||||
Discoverable: bool
|
||||
Pairable: bool
|
||||
PairableTimeout: int
|
||||
DiscoverableTimeout: int
|
||||
Discovering: int
|
||||
UUIDs: list[str]
|
||||
Modalias: str
|
||||
Roles: list[str]
|
||||
ExperimentalFeatures: list[str]
|
||||
|
||||
|
||||
# https://github.com/bluez/bluez/blob/master/doc/org.bluez.AdvertisementMonitor.rst
|
||||
|
||||
|
||||
class AdvertisementMonitor1(TypedDict):
|
||||
Type: str
|
||||
RSSILowThreshold: int
|
||||
RSSIHighThreshold: int
|
||||
RSSILowTimeout: int
|
||||
RSSIHighTimeout: int
|
||||
RSSISamplingPeriod: int
|
||||
Patterns: list[tuple[int, int, bytes]]
|
||||
|
||||
|
||||
# https://github.com/bluez/bluez/blob/master/doc/org.bluez.AdvertisementMonitorManager.rst
|
||||
|
||||
|
||||
class AdvertisementMonitorManager1(TypedDict):
|
||||
SupportedMonitorTypes: list[str]
|
||||
SupportedFeatures: list[str]
|
||||
|
||||
|
||||
# https://github.com/bluez/bluez/blob/master/doc/org.bluez.Battery.rst
|
||||
|
||||
|
||||
class Battery1(TypedDict):
|
||||
SupportedMonitorTypes: list[str]
|
||||
SupportedFeatures: list[str]
|
||||
|
||||
|
||||
# https://github.com/bluez/bluez/blob/master/doc/org.bluez.Device.rst
|
||||
|
||||
|
||||
class Device1(TypedDict):
|
||||
Address: str
|
||||
AddressType: str
|
||||
Name: str
|
||||
Icon: str
|
||||
Class: int
|
||||
Appearance: int
|
||||
UUIDs: list[str]
|
||||
Paired: bool
|
||||
Bonded: bool
|
||||
Connected: bool
|
||||
Trusted: bool
|
||||
Blocked: bool
|
||||
WakeAllowed: bool
|
||||
Alias: str
|
||||
Adapter: str
|
||||
LegacyPairing: bool
|
||||
Modalias: str
|
||||
RSSI: int
|
||||
TxPower: int
|
||||
ManufacturerData: dict[int, bytes]
|
||||
ServiceData: dict[str, bytes]
|
||||
ServicesResolved: bool
|
||||
AdvertisingFlags: bytes
|
||||
AdvertisingData: dict[int, bytes]
|
||||
|
||||
|
||||
# https://github.com/bluez/bluez/blob/master/doc/org.bluez.GattService.rst
|
||||
|
||||
|
||||
class GattService1(TypedDict):
|
||||
UUID: str
|
||||
Primary: bool
|
||||
Device: str
|
||||
Includes: list[str]
|
||||
# Handle is server-only and not available in Bleak
|
||||
|
||||
|
||||
class GattCharacteristic1(TypedDict):
|
||||
UUID: str
|
||||
Service: str
|
||||
Value: bytes
|
||||
WriteAcquired: bool
|
||||
NotifyAcquired: bool
|
||||
Notifying: bool
|
||||
Flags: list[CharacteristicPropertyName]
|
||||
# "MTU" property was added in BlueZ 5.62.
|
||||
# It may missing when operating with an older stack.
|
||||
MTU: int
|
||||
# Handle is server-only and not available in Bleak
|
||||
|
||||
|
||||
class GattDescriptor1(TypedDict):
|
||||
UUID: str
|
||||
Characteristic: str
|
||||
Value: bytes
|
||||
Flags: list[
|
||||
Literal[
|
||||
"read",
|
||||
"write",
|
||||
"encrypt-read",
|
||||
"encrypt-write",
|
||||
"encrypt-authenticated-read",
|
||||
"encrypt-authenticated-write",
|
||||
# "secure-read" and "secure-write" are server-only and not available in Bleak
|
||||
"authorize",
|
||||
]
|
||||
]
|
||||
# Handle is server-only and not available in Bleak
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,239 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "linux":
|
||||
assert False, "This backend is only available on Linux"
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any, Literal, Optional
|
||||
from warnings import warn
|
||||
|
||||
from dbus_fast import Variant
|
||||
|
||||
from bleak._compat import override
|
||||
from bleak.args.bluez import BlueZDiscoveryFilters as _BlueZDiscoveryFilters
|
||||
from bleak.args.bluez import BlueZScannerArgs as _BlueZScannerArgs
|
||||
from bleak.backends.bluezdbus.defs import Device1
|
||||
from bleak.backends.bluezdbus.manager import get_global_bluez_manager
|
||||
from bleak.backends.scanner import (
|
||||
AdvertisementData,
|
||||
AdvertisementDataCallback,
|
||||
BaseBleakScanner,
|
||||
)
|
||||
from bleak.exc import BleakError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_DEPRECATED: dict[str, Any] = {
|
||||
"BlueZDiscoveryFilters": _BlueZDiscoveryFilters,
|
||||
"BlueZScannerArgs": _BlueZScannerArgs,
|
||||
}
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
if value := _DEPRECATED.get(name):
|
||||
warn(
|
||||
f"importing {name} from bleak.backends.bluezdbus.scanner is deprecated, use bleak.args.bluez instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return value
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
class BleakScannerBlueZDBus(BaseBleakScanner):
|
||||
"""The native Linux Bleak BLE Scanner.
|
||||
|
||||
For possible values for `filters`, see the parameters to the
|
||||
``SetDiscoveryFilter`` method in the `BlueZ docs
|
||||
<https://github.com/bluez/bluez/blob/master/doc/org.bluez.Adapter.rst#void-setdiscoveryfilterdict-filter>`_
|
||||
|
||||
Args:
|
||||
detection_callback:
|
||||
Optional function that will be called each time a device is
|
||||
discovered or advertising data has changed.
|
||||
service_uuids:
|
||||
Optional list of service UUIDs to filter on. Only advertisements
|
||||
containing this advertising data will be received. Specifying this
|
||||
also enables scanning while the screen is off on Android.
|
||||
scanning_mode:
|
||||
Set to ``"passive"`` to avoid the ``"active"`` scanning mode.
|
||||
**bluez:
|
||||
Dictionary of arguments specific to the BlueZ backend.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detection_callback: Optional[AdvertisementDataCallback],
|
||||
service_uuids: Optional[list[str]],
|
||||
scanning_mode: Literal["active", "passive"],
|
||||
*,
|
||||
bluez: _BlueZScannerArgs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(detection_callback, service_uuids)
|
||||
|
||||
self._scanning_mode = scanning_mode
|
||||
self._adapter = bluez.get("adapter")
|
||||
|
||||
# callback from manager for stopping scanning if it has been started
|
||||
self._stop: Optional[Callable[[], Coroutine[Any, Any, None]]] = None
|
||||
|
||||
# Discovery filters
|
||||
|
||||
self._filters: dict[str, Variant] = {}
|
||||
|
||||
self._filters["Transport"] = Variant("s", "le")
|
||||
self._filters["DuplicateData"] = Variant("b", False)
|
||||
|
||||
if self._service_uuids:
|
||||
self._filters["UUIDs"] = Variant("as", self._service_uuids)
|
||||
|
||||
filters = bluez.get("filters")
|
||||
|
||||
if filters is not None:
|
||||
self.set_scanning_filter(filters=filters)
|
||||
|
||||
self._or_patterns = bluez.get("or_patterns")
|
||||
|
||||
if self._scanning_mode == "passive" and service_uuids:
|
||||
logger.warning(
|
||||
"service uuid filtering is not implemented for passive scanning, use bluez or_patterns as a workaround"
|
||||
)
|
||||
|
||||
if self._scanning_mode == "passive" and not self._or_patterns:
|
||||
raise BleakError("passive scanning mode requires bluez or_patterns")
|
||||
|
||||
@override
|
||||
async def start(self) -> None:
|
||||
manager = await get_global_bluez_manager()
|
||||
|
||||
if self._adapter:
|
||||
adapter_path = f"/org/bluez/{self._adapter}"
|
||||
else:
|
||||
adapter_path = manager.get_default_adapter()
|
||||
|
||||
self.seen_devices = {}
|
||||
|
||||
if self._scanning_mode == "passive":
|
||||
assert self._or_patterns is not None # should be checked in __init__
|
||||
|
||||
self._stop = await manager.passive_scan(
|
||||
adapter_path,
|
||||
self._or_patterns,
|
||||
self._handle_advertising_data,
|
||||
self._handle_device_removed,
|
||||
)
|
||||
else:
|
||||
self._stop = await manager.active_scan(
|
||||
adapter_path,
|
||||
self._filters,
|
||||
self._handle_advertising_data,
|
||||
self._handle_device_removed,
|
||||
)
|
||||
|
||||
@override
|
||||
async def stop(self) -> None:
|
||||
if self._stop:
|
||||
# avoid reentrancy
|
||||
stop, self._stop = self._stop, None
|
||||
|
||||
await stop()
|
||||
|
||||
def set_scanning_filter(self, **kwargs: Any) -> None:
|
||||
"""Sets OS level scanning filters for the BleakScanner.
|
||||
|
||||
For possible values for `filters`, see the parameters to the
|
||||
``SetDiscoveryFilter`` method in the `BlueZ docs
|
||||
<https://github.com/bluez/bluez/blob/master/doc/org.bluez.Adapter.rst#void-setdiscoveryfilterdict-filter>`_
|
||||
|
||||
See variant types here: <https://python-dbus-next.readthedocs.io/en/latest/type-system/>
|
||||
|
||||
Keyword Args:
|
||||
filters (dict): A dict of filters to be applied on discovery.
|
||||
|
||||
"""
|
||||
for k, v in kwargs.get("filters", {}).items():
|
||||
if k == "UUIDs":
|
||||
self._filters[k] = Variant("as", v)
|
||||
elif k == "RSSI":
|
||||
self._filters[k] = Variant("n", v)
|
||||
elif k == "Pathloss":
|
||||
self._filters[k] = Variant("n", v)
|
||||
elif k == "Transport":
|
||||
self._filters[k] = Variant("s", v)
|
||||
elif k == "DuplicateData":
|
||||
self._filters[k] = Variant("b", v)
|
||||
elif k == "Discoverable":
|
||||
self._filters[k] = Variant("b", v)
|
||||
elif k == "Pattern":
|
||||
self._filters[k] = Variant("s", v)
|
||||
else:
|
||||
logger.warning("Filter '%s' is not currently supported.", k)
|
||||
|
||||
# Helper methods
|
||||
|
||||
def _handle_advertising_data(self, path: str, props: Device1) -> None:
|
||||
"""
|
||||
Handles advertising data received from the BlueZ manager instance.
|
||||
|
||||
Args:
|
||||
path: The D-Bus object path of the device.
|
||||
props: The D-Bus object properties of the device.
|
||||
"""
|
||||
_service_uuids = props.get("UUIDs", [])
|
||||
|
||||
if not self.is_allowed_uuid(_service_uuids):
|
||||
return
|
||||
|
||||
# Get all the information wanted to pack in the advertisement data
|
||||
_local_name = props.get("Name")
|
||||
_manufacturer_data = {
|
||||
k: bytes(v) for k, v in props.get("ManufacturerData", {}).items()
|
||||
}
|
||||
_service_data = {k: bytes(v) for k, v in props.get("ServiceData", {}).items()}
|
||||
|
||||
# Get tx power data
|
||||
tx_power = props.get("TxPower")
|
||||
|
||||
# Pack the advertisement data
|
||||
advertisement_data = AdvertisementData(
|
||||
local_name=_local_name,
|
||||
manufacturer_data=_manufacturer_data,
|
||||
service_data=_service_data,
|
||||
service_uuids=_service_uuids,
|
||||
tx_power=tx_power,
|
||||
rssi=props.get("RSSI", -127),
|
||||
platform_data=(path, props),
|
||||
)
|
||||
|
||||
device = self.create_or_update_device(
|
||||
path,
|
||||
props["Address"],
|
||||
# BlueZ generates a name based on the address if no name is available.
|
||||
# To match other backends, we replace this with None.
|
||||
(
|
||||
None
|
||||
if props["Alias"] == props["Address"].replace(":", "-")
|
||||
else props["Alias"]
|
||||
),
|
||||
{"path": path, "props": props},
|
||||
advertisement_data,
|
||||
)
|
||||
|
||||
self.call_detection_callbacks(device, advertisement_data)
|
||||
|
||||
def _handle_device_removed(self, device_path: str) -> None:
|
||||
"""
|
||||
Handles a device being removed from BlueZ.
|
||||
"""
|
||||
try:
|
||||
del self.seen_devices[device_path]
|
||||
except KeyError:
|
||||
# The device will not have been added to self.seen_devices if no
|
||||
# advertising data was received, so this is expected to happen
|
||||
# occasionally.
|
||||
pass
|
||||
@ -0,0 +1,212 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "linux":
|
||||
assert False, "This backend is only available on Linux"
|
||||
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
from dbus_fast.aio.message_bus import MessageBus
|
||||
from dbus_fast.errors import InvalidObjectPathError
|
||||
from dbus_fast.message import Message
|
||||
from dbus_fast.validators import (
|
||||
assert_interface_name_valid,
|
||||
assert_member_name_valid,
|
||||
assert_object_path_valid,
|
||||
)
|
||||
|
||||
# TODO: this stuff should be improved and submitted upstream to dbus-next
|
||||
# https://github.com/altdesktop/python-dbus-next/issues/53
|
||||
|
||||
_message_types = ["signal", "method_call", "method_return", "error"]
|
||||
|
||||
|
||||
class InvalidMessageTypeError(TypeError):
|
||||
def __init__(self, type: str):
|
||||
super().__init__(f"invalid message type: {type}")
|
||||
|
||||
|
||||
def is_message_type_valid(type: str) -> bool:
|
||||
"""Whether this is a valid message type.
|
||||
|
||||
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules
|
||||
|
||||
:param type: The message type to validate.
|
||||
:type name: str
|
||||
|
||||
:returns: Whether the name is a valid message type.
|
||||
:rtype: bool
|
||||
"""
|
||||
return type in _message_types
|
||||
|
||||
|
||||
def assert_bus_name_valid(type: str) -> None:
|
||||
"""Raise an error if this is not a valid message type.
|
||||
|
||||
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules
|
||||
|
||||
:param type: The message type to validate.
|
||||
:type name: str
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidMessageTypeError` - If this is not a valid message type.
|
||||
"""
|
||||
if not is_message_type_valid(type):
|
||||
raise InvalidMessageTypeError(type)
|
||||
|
||||
|
||||
class MatchRules:
|
||||
"""D-Bus signal match rules.
|
||||
|
||||
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
type: str = "signal",
|
||||
sender: Optional[str] = None,
|
||||
interface: Optional[str] = None,
|
||||
member: Optional[str] = None,
|
||||
path: Optional[str] = None,
|
||||
path_namespace: Optional[str] = None,
|
||||
destination: Optional[str] = None,
|
||||
arg0namespace: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
assert_bus_name_valid(type)
|
||||
self.type: str = type
|
||||
|
||||
if sender:
|
||||
assert_bus_name_valid(sender)
|
||||
self.sender: Optional[str] = sender
|
||||
else:
|
||||
self.sender = None
|
||||
|
||||
if interface:
|
||||
assert_interface_name_valid(interface)
|
||||
self.interface: Optional[str] = interface
|
||||
else:
|
||||
self.interface = None
|
||||
|
||||
if member:
|
||||
assert_member_name_valid(member)
|
||||
self.member: Optional[str] = member
|
||||
else:
|
||||
self.member = None
|
||||
|
||||
if path:
|
||||
assert_object_path_valid(path)
|
||||
self.path: Optional[str] = path
|
||||
else:
|
||||
self.path = None
|
||||
|
||||
if path_namespace:
|
||||
assert_object_path_valid(path_namespace)
|
||||
self.path_namespace: Optional[str] = path_namespace
|
||||
else:
|
||||
self.path_namespace = None
|
||||
|
||||
if path and path_namespace:
|
||||
raise TypeError(
|
||||
"message rules cannot have both 'path' and 'path_namespace' at the same time"
|
||||
)
|
||||
|
||||
if destination:
|
||||
assert_bus_name_valid(destination)
|
||||
self.destination: Optional[str] = destination
|
||||
else:
|
||||
self.destination = None
|
||||
|
||||
if arg0namespace:
|
||||
assert_bus_name_valid(arg0namespace)
|
||||
self.arg0namespace: Optional[str] = arg0namespace
|
||||
else:
|
||||
self.arg0namespace = None
|
||||
|
||||
if kwargs:
|
||||
for k, v in kwargs.items():
|
||||
if re.match(r"^arg\d+$", k):
|
||||
if not isinstance(v, str):
|
||||
raise TypeError(f"kwarg '{k}' must have a str value")
|
||||
elif re.match(r"^arg\d+path$", k):
|
||||
if not isinstance(v, str):
|
||||
raise InvalidObjectPathError(v)
|
||||
assert_object_path_valid(v[:-1] if v.endswith("/") else v)
|
||||
else:
|
||||
raise ValueError("kwargs must be in the form 'arg0' or 'arg0path'")
|
||||
self.args: Optional[dict[str, str]] = kwargs
|
||||
else:
|
||||
self.args = None
|
||||
|
||||
@staticmethod
|
||||
def parse(rules: str) -> MatchRules:
|
||||
return MatchRules(**dict(r.split("=") for r in rules.split(",")))
|
||||
|
||||
def __str__(self) -> str:
|
||||
rules = [f"type={self.type}"]
|
||||
|
||||
if self.sender:
|
||||
rules.append(f"sender={self.sender}")
|
||||
|
||||
if self.interface:
|
||||
rules.append(f"interface={self.interface}")
|
||||
|
||||
if self.member:
|
||||
rules.append(f"member={self.member}")
|
||||
|
||||
if self.path:
|
||||
rules.append(f"path={self.path}")
|
||||
|
||||
if self.path_namespace:
|
||||
rules.append(f"path_namespace={self.path_namespace}")
|
||||
|
||||
if self.destination:
|
||||
rules.append(f"destination={self.destination}")
|
||||
|
||||
if self.args:
|
||||
for k, v in self.args.items():
|
||||
rules.append(f"{k}={v}")
|
||||
|
||||
if self.arg0namespace:
|
||||
rules.append(f"arg0namespace={self.arg0namespace}")
|
||||
|
||||
return ",".join(rules)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"MatchRules({self})"
|
||||
|
||||
|
||||
async def add_match(bus: MessageBus, rules: MatchRules) -> Message:
|
||||
"""Calls org.freedesktop.DBus.AddMatch using ``rules``."""
|
||||
reply = await bus.call(
|
||||
Message(
|
||||
destination="org.freedesktop.DBus",
|
||||
interface="org.freedesktop.DBus",
|
||||
path="/org/freedesktop/DBus",
|
||||
member="AddMatch",
|
||||
signature="s",
|
||||
body=[str(rules)],
|
||||
)
|
||||
)
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
async def remove_match(bus: MessageBus, rules: MatchRules) -> Message:
|
||||
"""Calls org.freedesktop.DBus.RemoveMatch using ``rules``."""
|
||||
reply = await bus.call(
|
||||
Message(
|
||||
destination="org.freedesktop.DBus",
|
||||
interface="org.freedesktop.DBus",
|
||||
path="/org/freedesktop/DBus",
|
||||
member="RemoveMatch",
|
||||
signature="s",
|
||||
body=[str(rules)],
|
||||
)
|
||||
)
|
||||
|
||||
return reply
|
||||
@ -0,0 +1,147 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "linux":
|
||||
assert False, "This backend is only available on Linux"
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from dbus_fast.auth import AuthExternal
|
||||
from dbus_fast.constants import MessageType
|
||||
from dbus_fast.message import Message
|
||||
|
||||
from bleak.backends.bluezdbus import defs
|
||||
from bleak.exc import (
|
||||
BleakDBusError,
|
||||
BleakError,
|
||||
BleakGATTProtocolError,
|
||||
BleakGATTProtocolErrorCode,
|
||||
)
|
||||
|
||||
|
||||
def assert_reply(reply: Message) -> None:
|
||||
"""Checks that a D-Bus message is a valid reply.
|
||||
|
||||
Raises:
|
||||
BleakDBusError: if the message type is ``MessageType.ERROR``
|
||||
AssertionError: if the message type is not ``MessageType.METHOD_RETURN``
|
||||
"""
|
||||
if reply.message_type == MessageType.ERROR:
|
||||
assert reply.error_name
|
||||
raise BleakDBusError(reply.error_name, reply.body)
|
||||
assert reply.message_type == MessageType.METHOD_RETURN
|
||||
|
||||
|
||||
def assert_gatt_reply(reply: Message, start_notify: bool = False) -> None:
|
||||
"""
|
||||
Checks that a D-Bus message is a valid reply.
|
||||
|
||||
Like :func:`assert_reply`, but has special handling for GATT protocol errors.
|
||||
|
||||
Args:
|
||||
reply: The D-Bus message to check.
|
||||
start_notify: Whether this reply is for a StartNotify call.
|
||||
|
||||
Raises:
|
||||
BleakGATTProtocolError: for specific GATT protocol errors.
|
||||
BleakDBusError: if the message type is ``MessageType.ERROR``
|
||||
AssertionError: if the message type is not ``MessageType.METHOD_RETURN``
|
||||
"""
|
||||
|
||||
# BlueZ has specific errors for some GATT protocol errors, so we
|
||||
# have to turn them back into the generic BleakGATTProtocolError
|
||||
# with the correct code. See create_gatt_dbus_error() in BlueZ source.
|
||||
|
||||
if reply.error_name == defs.BLUEZ_ERROR_NOT_PERMITTED:
|
||||
# Same error is used for both read and write not permitted, so we have
|
||||
# to use the message to discriminate.
|
||||
if reply.body and reply.body[0] == "Read not permitted":
|
||||
raise BleakGATTProtocolError(BleakGATTProtocolErrorCode.READ_NOT_PERMITTED)
|
||||
|
||||
if reply.body and reply.body[0] == "Write not permitted":
|
||||
raise BleakGATTProtocolError(BleakGATTProtocolErrorCode.WRITE_NOT_PERMITTED)
|
||||
|
||||
# REVISIT: could also be "Not paired" which could be any of:
|
||||
# INSUFFICIENT_AUTHENTICATION, INSUFFICIENT_ENCRYPTION, or
|
||||
# INSUFFICIENT_ENCRYPTION_KEY_SIZE
|
||||
|
||||
# "StartNotify" will return BLUEZ_ERROR_NOT_SUPPORTED if the characteristic
|
||||
# does not support notifications before even trying, so it is not a GATT
|
||||
# error in this case.
|
||||
if not start_notify and reply.error_name == defs.BLUEZ_ERROR_NOT_SUPPORTED:
|
||||
raise BleakGATTProtocolError(BleakGATTProtocolErrorCode.REQUEST_NOT_SUPPORTED)
|
||||
|
||||
if reply.error_name == defs.BLUEZ_ERROR_NOT_AUTHORIZED:
|
||||
raise BleakGATTProtocolError(
|
||||
BleakGATTProtocolErrorCode.INSUFFICIENT_AUTHORIZATION
|
||||
)
|
||||
|
||||
if reply.error_name == defs.BLUEZ_ERROR_INVALID_ARGUMENT:
|
||||
if reply.body and reply.body[0] == "Invalid offset":
|
||||
raise BleakGATTProtocolError(BleakGATTProtocolErrorCode.INVALID_OFFSET)
|
||||
|
||||
if reply.body and reply.body[0] == "Invalid Length":
|
||||
raise BleakGATTProtocolError(
|
||||
BleakGATTProtocolErrorCode.INVALID_ATTRIBUTE_VALUE_LENGTH
|
||||
)
|
||||
|
||||
if reply.error_name == defs.BLUEZ_ERROR_IMPROPERLY_CONFIGURED:
|
||||
raise BleakGATTProtocolError(
|
||||
BleakGATTProtocolErrorCode.CCCD_IMPROPERLY_CONFIGURED
|
||||
)
|
||||
|
||||
if (
|
||||
reply.error_name == defs.BLUEZ_ERROR_FAILED
|
||||
and reply.body
|
||||
and (
|
||||
# Unfortunately, BlueZ makes us scrape the string to get the
|
||||
# error code in this case.
|
||||
match := re.match(
|
||||
r"^Operation failed with ATT error: (0x[0-9a-fA-F]+)",
|
||||
reply.body[0],
|
||||
)
|
||||
)
|
||||
):
|
||||
raise BleakGATTProtocolError(
|
||||
BleakGATTProtocolErrorCode(int(match.group(1), 16))
|
||||
)
|
||||
|
||||
assert_reply(reply)
|
||||
|
||||
|
||||
def extract_service_handle_from_path(path: str) -> int:
|
||||
try:
|
||||
return int(path[-4:], 16)
|
||||
except Exception as e:
|
||||
raise BleakError(f"Could not parse service handle from path: {path}") from e
|
||||
|
||||
|
||||
def device_path_from_characteristic_path(characteristic_path: str) -> str:
|
||||
"""
|
||||
Scrape the device path from a D-Bus characteristic path.
|
||||
|
||||
Args:
|
||||
characteristic_path: The D-Bus object path of the characteristic.
|
||||
|
||||
Returns:
|
||||
A D-Bus object path of the device.
|
||||
"""
|
||||
# /org/bluez/hci1/dev_FA_23_9D_AA_45_46/service000c/char000d
|
||||
return characteristic_path[:-21]
|
||||
|
||||
|
||||
def get_dbus_authenticator() -> Optional[AuthExternal]:
|
||||
uid = None
|
||||
try:
|
||||
uid = int(os.environ.get("BLEAK_DBUS_AUTH_UID", ""))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
auth = None
|
||||
if uid is not None:
|
||||
auth = AuthExternal(uid=uid)
|
||||
|
||||
return auth
|
||||
@ -0,0 +1,60 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "linux":
|
||||
assert False, "This backend is only available on Linux"
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _get_bluetoothctl_version() -> Optional[re.Match[bytes]]:
|
||||
"""Get the version of bluetoothctl."""
|
||||
with contextlib.suppress(Exception):
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"bluetoothctl", "--version", stdout=asyncio.subprocess.PIPE
|
||||
)
|
||||
assert proc.stdout
|
||||
out = await proc.stdout.read()
|
||||
version = re.search(b"(\\d+).(\\d+)", out.strip(b"'"))
|
||||
await proc.wait()
|
||||
return version
|
||||
return None
|
||||
|
||||
|
||||
class BlueZFeatures:
|
||||
"""Check which features are supported by the BlueZ backend."""
|
||||
|
||||
checked_bluez_version = False
|
||||
supported_version = True
|
||||
_check_bluez_event: Optional[asyncio.Event] = None
|
||||
|
||||
@classmethod
|
||||
async def check_bluez_version(cls) -> None:
|
||||
"""Check the bluez version."""
|
||||
if cls._check_bluez_event:
|
||||
# If there is already a check in progress
|
||||
# it wins, wait for it instead
|
||||
await cls._check_bluez_event.wait()
|
||||
return
|
||||
cls._check_bluez_event = asyncio.Event()
|
||||
version_output = await _get_bluetoothctl_version()
|
||||
if version_output:
|
||||
major, minor = tuple(map(int, version_output.groups()))
|
||||
cls.supported_version = major == 5 and minor >= 55
|
||||
else:
|
||||
# Its possible they may be running inside a container where
|
||||
# bluetoothctl is not available and they only have access to the
|
||||
# BlueZ D-Bus API.
|
||||
logger.warning(
|
||||
"Could not determine BlueZ version, bluetoothctl not available, assuming 5.55+"
|
||||
)
|
||||
|
||||
cls._check_bluez_event.set()
|
||||
cls.checked_bluez_version = True
|
||||
@ -0,0 +1,157 @@
|
||||
# Created on 2019-03-19 by hbldh <henrik.blidh@nedomkull.com>
|
||||
"""
|
||||
Interface class for the Bleak representation of a GATT Characteristic
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
from uuid import UUID
|
||||
|
||||
from bleak.assigned_numbers import CharacteristicPropertyName
|
||||
from bleak.backends.descriptor import BleakGATTDescriptor
|
||||
from bleak.uuids import normalize_uuid_str, uuidstr_to_str
|
||||
|
||||
# to prevent circular import
|
||||
if TYPE_CHECKING:
|
||||
from bleak.backends.service import BleakGATTService
|
||||
|
||||
|
||||
class GattCharacteristicsFlags(enum.Enum):
|
||||
broadcast = 0x0001
|
||||
read = 0x0002
|
||||
write_without_response = 0x0004
|
||||
write = 0x0008
|
||||
notify = 0x0010
|
||||
indicate = 0x0020
|
||||
authenticated_signed_writes = 0x0040
|
||||
extended_properties = 0x0080
|
||||
reliable_write = 0x0100
|
||||
writable_auxiliaries = 0x0200
|
||||
|
||||
|
||||
class BleakGATTCharacteristic:
|
||||
"""The Bleak representation of a GATT Characteristic"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
obj: Any,
|
||||
handle: int,
|
||||
uuid: str,
|
||||
properties: list[CharacteristicPropertyName],
|
||||
max_write_without_response_size: Callable[[], int],
|
||||
service: BleakGATTService,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
obj:
|
||||
A platform-specific object for this characteristic.
|
||||
max_write_without_response_size:
|
||||
The maximum size in bytes that can be written to the
|
||||
characteristic in a single write without response command.
|
||||
service:
|
||||
The service this characteristic belongs to.
|
||||
"""
|
||||
self.obj = obj
|
||||
self._handle = handle
|
||||
self._uuid = uuid
|
||||
self._properties = properties
|
||||
self._max_write_without_response_size = max_write_without_response_size
|
||||
self._service = service
|
||||
self._descriptors: dict[int, BleakGATTDescriptor] = {}
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.uuid} (Handle: {self.handle}): {self.description}"
|
||||
|
||||
@property
|
||||
def service_uuid(self) -> str:
|
||||
"""The UUID of the Service containing this characteristic"""
|
||||
return self._service.uuid
|
||||
|
||||
@property
|
||||
def service_handle(self) -> int:
|
||||
"""The integer handle of the Service containing this characteristic"""
|
||||
return self._service.handle
|
||||
|
||||
@property
|
||||
def handle(self) -> int:
|
||||
"""The handle for this characteristic"""
|
||||
return self._handle
|
||||
|
||||
@property
|
||||
def uuid(self) -> str:
|
||||
"""The UUID for this characteristic"""
|
||||
return self._uuid
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""Description for this characteristic"""
|
||||
return uuidstr_to_str(self.uuid)
|
||||
|
||||
@property
|
||||
def properties(self) -> list[CharacteristicPropertyName]:
|
||||
"""Properties of this characteristic"""
|
||||
return self._properties
|
||||
|
||||
@property
|
||||
def max_write_without_response_size(self) -> int:
|
||||
"""
|
||||
Gets the maximum size in bytes that can be used for the *data* argument
|
||||
of :meth:`BleakClient.write_gatt_char()` when ``response=False``.
|
||||
|
||||
In rare cases, a device may take a long time to update this value, so
|
||||
reading this property may return the default value of ``20`` and reading
|
||||
it again after a some time may return the expected higher value.
|
||||
|
||||
If you *really* need to wait for a higher value, you can do something
|
||||
like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async with asyncio.timeout(10):
|
||||
while char.max_write_without_response_size == 20:
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
.. warning:: Linux quirk: For BlueZ versions < 5.62, this property
|
||||
will always return ``20``.
|
||||
|
||||
.. versionadded:: 0.16
|
||||
"""
|
||||
|
||||
# for backwards compatibility
|
||||
if isinstance(self._max_write_without_response_size, int):
|
||||
return self._max_write_without_response_size
|
||||
|
||||
return self._max_write_without_response_size()
|
||||
|
||||
@property
|
||||
def descriptors(self) -> list[BleakGATTDescriptor]:
|
||||
"""List of descriptors for this service"""
|
||||
return list(self._descriptors.values())
|
||||
|
||||
def get_descriptor(
|
||||
self, specifier: Union[int, str, UUID]
|
||||
) -> Union[BleakGATTDescriptor, None]:
|
||||
"""Get a descriptor by handle (int) or UUID (str or uuid.UUID)"""
|
||||
if isinstance(specifier, int):
|
||||
return self._descriptors.get(specifier)
|
||||
|
||||
uuid = normalize_uuid_str(str(specifier))
|
||||
for descriptor in self._descriptors.values():
|
||||
if descriptor.uuid == uuid:
|
||||
return descriptor
|
||||
|
||||
return None
|
||||
|
||||
def add_descriptor(self, descriptor: BleakGATTDescriptor) -> None:
|
||||
"""Add a :py:class:`~BleakGATTDescriptor` to the characteristic.
|
||||
|
||||
Should not be used by end user, but rather by `bleak` itself.
|
||||
"""
|
||||
if descriptor.handle in self._descriptors:
|
||||
raise ValueError(
|
||||
f"Descriptor with handle {descriptor.handle} already exists"
|
||||
)
|
||||
|
||||
self._descriptors[descriptor.handle] = descriptor
|
||||
258
tools/python-embed/Lib/site-packages/bleak/backends/client.py
Normal file
258
tools/python-embed/Lib/site-packages/bleak/backends/client.py
Normal file
@ -0,0 +1,258 @@
|
||||
# Created on 2018-04-23 by hbldh <henrik.blidh@nedomkull.com>
|
||||
"""
|
||||
Base class for backend clients.
|
||||
"""
|
||||
import abc
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from bleak.args import SizedBuffer
|
||||
from bleak.backends import BleakBackend, get_default_backend
|
||||
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||
from bleak.backends.descriptor import BleakGATTDescriptor
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.service import BleakGATTServiceCollection
|
||||
from bleak.exc import BleakError
|
||||
|
||||
NotifyCallback = Callable[[bytearray], None]
|
||||
|
||||
|
||||
class BaseBleakClient(abc.ABC):
|
||||
"""The Client Interface for Bleak Backend implementations to implement.
|
||||
|
||||
The documentation of this interface should thus be safe to use as a reference for your implementation.
|
||||
|
||||
Args:
|
||||
address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it.
|
||||
|
||||
Keyword Args:
|
||||
timeout (float): Timeout for required ``discover`` call.
|
||||
disconnected_callback (callable): Callback that will be scheduled in the
|
||||
event loop when the client is disconnected. The callable must take one
|
||||
argument, which will be this client object.
|
||||
"""
|
||||
|
||||
def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs: Any):
|
||||
if isinstance(address_or_ble_device, BLEDevice):
|
||||
self.address = address_or_ble_device.address
|
||||
else:
|
||||
self.address = address_or_ble_device
|
||||
|
||||
self.services: Optional[BleakGATTServiceCollection] = None
|
||||
|
||||
self._timeout = kwargs["timeout"]
|
||||
self._disconnected_callback: Optional[Callable[[], None]] = kwargs.get(
|
||||
"disconnected_callback"
|
||||
)
|
||||
|
||||
# NB: this is not marked as @abc.abstractmethod because that would break
|
||||
# 3rd-party backends. We might change this in the future to make it required.
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""See :meth:`bleak.BleakClient.name`."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def mtu_size(self) -> int:
|
||||
"""Gets the negotiated MTU."""
|
||||
raise NotImplementedError
|
||||
|
||||
# Connectivity methods
|
||||
|
||||
def set_disconnected_callback(
|
||||
self, callback: Optional[Callable[[], None]], **kwargs: Any
|
||||
) -> None:
|
||||
"""Set the disconnect callback.
|
||||
The callback will only be called on unsolicited disconnect event.
|
||||
|
||||
Set the callback to ``None`` to remove any existing callback.
|
||||
|
||||
Args:
|
||||
callback: callback to be called on disconnection.
|
||||
|
||||
"""
|
||||
self._disconnected_callback = callback
|
||||
|
||||
@abc.abstractmethod
|
||||
async def connect(self, pair: bool, **kwargs: Any) -> None:
|
||||
"""Connect to the specified GATT server.
|
||||
|
||||
Args:
|
||||
pair (bool): If the client should attempt to pair with the
|
||||
peripheral before connecting if it is not already paired.
|
||||
|
||||
Backends that can't implement this should make an appropriate
|
||||
log message and ignore the parameter.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def disconnect(self) -> None:
|
||||
"""Disconnect from the specified GATT server."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def pair(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Pair with the peripheral."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def unpair(self) -> None:
|
||||
"""Unpair with the peripheral."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def is_connected(self) -> bool:
|
||||
"""Check connection status between this client and the server.
|
||||
|
||||
Returns:
|
||||
Boolean representing connection status.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
# I/O methods
|
||||
|
||||
@abc.abstractmethod
|
||||
async def read_gatt_char(
|
||||
self,
|
||||
characteristic: BleakGATTCharacteristic,
|
||||
*,
|
||||
use_cached: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> bytearray:
|
||||
"""Perform read operation on the specified GATT characteristic.
|
||||
|
||||
Args:
|
||||
characteristic (BleakGATTCharacteristic): The characteristic to read from.
|
||||
use_cached: If True, read the cached value instead of performing a read from the device.
|
||||
|
||||
Returns:
|
||||
(bytearray) The read data.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def read_gatt_descriptor(
|
||||
self,
|
||||
descriptor: BleakGATTDescriptor,
|
||||
*,
|
||||
use_cached: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> bytearray:
|
||||
"""Perform read operation on the specified GATT descriptor.
|
||||
|
||||
Args:
|
||||
descriptor: The descriptor to read from.
|
||||
use_cached: If True, read the cached value instead of performing a read from the device.
|
||||
|
||||
Returns:
|
||||
The read data.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def write_gatt_char(
|
||||
self, characteristic: BleakGATTCharacteristic, data: SizedBuffer, response: bool
|
||||
) -> None:
|
||||
"""
|
||||
Perform a write operation on the specified GATT characteristic.
|
||||
|
||||
Args:
|
||||
characteristic: The characteristic to write to.
|
||||
data: The data to send.
|
||||
response: If write-with-response operation should be done.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def write_gatt_descriptor(
|
||||
self, descriptor: BleakGATTDescriptor, data: SizedBuffer
|
||||
) -> None:
|
||||
"""Perform a write operation on the specified GATT descriptor.
|
||||
|
||||
Args:
|
||||
descriptor: The descriptor to read from.
|
||||
data: The data to send (any bytes-like object).
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def start_notify(
|
||||
self,
|
||||
characteristic: BleakGATTCharacteristic,
|
||||
callback: NotifyCallback,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Activate notifications/indications on a characteristic.
|
||||
|
||||
Implementers should call the OS function to enable notifications or
|
||||
indications on the characteristic.
|
||||
|
||||
To keep things the same cross-platform, notifications should be preferred
|
||||
over indications if possible when a characteristic supports both.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None:
|
||||
"""Deactivate notification/indication on a specified characteristic.
|
||||
|
||||
Args:
|
||||
characteristic (BleakGATTCharacteristic): The characteristic to deactivate
|
||||
notification/indication on.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def get_platform_client_backend_type() -> tuple[type[BaseBleakClient], BleakBackend]:
|
||||
"""
|
||||
Gets the platform-specific :class:`BaseBleakClient` type.
|
||||
"""
|
||||
backend = get_default_backend()
|
||||
match backend:
|
||||
case BleakBackend.P4ANDROID:
|
||||
from bleak.backends.p4android.client import (
|
||||
BleakClientP4Android, # type: ignore
|
||||
)
|
||||
|
||||
return (BleakClientP4Android, backend) # type: ignore
|
||||
|
||||
case BleakBackend.BLUEZ_DBUS:
|
||||
from bleak.backends.bluezdbus.client import (
|
||||
BleakClientBlueZDBus, # type: ignore
|
||||
)
|
||||
|
||||
return (BleakClientBlueZDBus, backend) # type: ignore
|
||||
|
||||
case BleakBackend.PYTHONISTA_CB:
|
||||
try:
|
||||
from bleak_pythonista import BleakClientPythonistaCB # type: ignore
|
||||
|
||||
return (BleakClientPythonistaCB, backend) # type: ignore
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"Ensure you have `bleak-pythonista` package installed."
|
||||
) from e
|
||||
|
||||
case BleakBackend.CORE_BLUETOOTH:
|
||||
from bleak.backends.corebluetooth.client import (
|
||||
BleakClientCoreBluetooth, # type: ignore
|
||||
)
|
||||
|
||||
return (BleakClientCoreBluetooth, backend) # type: ignore
|
||||
|
||||
case BleakBackend.WIN_RT:
|
||||
from bleak.backends.winrt.client import BleakClientWinRT # type: ignore
|
||||
|
||||
return (BleakClientWinRT, backend) # type: ignore
|
||||
|
||||
case _:
|
||||
raise BleakError(f"Unsupported backend: {backend}")
|
||||
@ -0,0 +1,402 @@
|
||||
# Created on June, 25 2019 by kevincar <kevincarrolldavis@gmail.com>
|
||||
"""
|
||||
CentralManagerDelegate will implement the CBCentralManagerDelegate protocol to
|
||||
manage CoreBluetooth services and resources on the Central End
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "darwin":
|
||||
assert False, "This backend is only available on macOS"
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Optional, TypedDict, cast
|
||||
|
||||
import objc
|
||||
from CoreBluetooth import (
|
||||
CBUUID,
|
||||
CBCentralManager,
|
||||
CBManagerAuthorizationDenied,
|
||||
CBManagerAuthorizationRestricted,
|
||||
CBManagerStatePoweredOff,
|
||||
CBManagerStatePoweredOn,
|
||||
CBManagerStateResetting,
|
||||
CBManagerStateUnauthorized,
|
||||
CBManagerStateUnsupported,
|
||||
CBPeripheral,
|
||||
)
|
||||
from Foundation import (
|
||||
NSUUID,
|
||||
NSArray,
|
||||
NSData,
|
||||
NSDictionary,
|
||||
NSError,
|
||||
NSNumber,
|
||||
NSObject,
|
||||
NSString,
|
||||
)
|
||||
from libdispatch import DISPATCH_QUEUE_SERIAL, dispatch_queue_create
|
||||
|
||||
from bleak._compat import Self
|
||||
from bleak._compat import timeout as async_timeout
|
||||
from bleak.backends._utils import external_thread_callback, try_call_soon_threadsafe
|
||||
from bleak.backends.corebluetooth.utils import cb_manager_state_message
|
||||
from bleak.exc import (
|
||||
BleakBluetoothNotAvailableError,
|
||||
BleakBluetoothNotAvailableReason,
|
||||
BleakError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
CBCentralManagerDelegate = objc.protocolNamed("CBCentralManagerDelegate")
|
||||
|
||||
|
||||
DisconnectCallback = Callable[[], None]
|
||||
|
||||
|
||||
class CBAdvertisementData(TypedDict, total=False):
|
||||
kCBAdvDataLocalName: NSString
|
||||
kCBAdvDataManufacturerData: NSData
|
||||
kCBAdvDataServiceData: dict[CBUUID, NSData]
|
||||
kCBAdvDataServiceUUIDs: NSArray[CBUUID]
|
||||
kCBAdvertisementDataOverflowServiceUUIDsKey: NSArray[CBUUID]
|
||||
kCBAdvDataTxPowerLevel: NSNumber
|
||||
kCBAdvertisementDataIsConnectable: NSNumber
|
||||
kCBAdvDataOverflowServiceUUIDs: NSArray[CBUUID]
|
||||
|
||||
|
||||
class ObjcCentralManagerDelegate(NSObject, protocols=[CBCentralManagerDelegate]):
|
||||
"""
|
||||
CoreBluetooth central manager delegate for bridging callbacks to asyncio.
|
||||
"""
|
||||
|
||||
def initWithPyDelegate_(
|
||||
self, py_delegate: weakref.ReferenceType["CentralManagerDelegate"]
|
||||
) -> Optional[Self]:
|
||||
"""macOS init function for NSObject"""
|
||||
self = objc.super(ObjcCentralManagerDelegate, self).init() # type: ignore[assignment]
|
||||
|
||||
if self is None:
|
||||
return None # pragma: no cover
|
||||
|
||||
self.py_delegate = py_delegate
|
||||
|
||||
return self
|
||||
|
||||
# Protocol Functions
|
||||
|
||||
@external_thread_callback
|
||||
def centralManagerDidUpdateState_(self, centralManager: CBCentralManager) -> None:
|
||||
logger.debug("centralManagerDidUpdateState_")
|
||||
logger.debug(cb_manager_state_message(centralManager.state()))
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_update_state_event.set,
|
||||
)
|
||||
|
||||
@external_thread_callback
|
||||
def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(
|
||||
self,
|
||||
central: CBCentralManager,
|
||||
peripheral: CBPeripheral,
|
||||
advertisementData: NSDictionary[str, Any],
|
||||
RSSI: NSNumber,
|
||||
) -> None:
|
||||
logger.debug("centralManager_didDiscoverPeripheral_advertisementData_RSSI_")
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_discover_peripheral,
|
||||
central,
|
||||
peripheral,
|
||||
advertisementData,
|
||||
RSSI,
|
||||
)
|
||||
|
||||
@external_thread_callback
|
||||
def centralManager_didConnectPeripheral_(
|
||||
self, central: CBCentralManager, peripheral: CBPeripheral
|
||||
) -> None:
|
||||
logger.debug("centralManager_didConnectPeripheral_")
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_connect_peripheral,
|
||||
central,
|
||||
peripheral,
|
||||
)
|
||||
|
||||
@external_thread_callback
|
||||
def centralManager_didFailToConnectPeripheral_error_(
|
||||
self,
|
||||
centralManager: CBCentralManager,
|
||||
peripheral: CBPeripheral,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
logger.debug("centralManager_didFailToConnectPeripheral_error_")
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_fail_to_connect_peripheral,
|
||||
centralManager,
|
||||
peripheral,
|
||||
error,
|
||||
)
|
||||
|
||||
@external_thread_callback
|
||||
def centralManager_didDisconnectPeripheral_error_(
|
||||
self,
|
||||
central: CBCentralManager,
|
||||
peripheral: CBPeripheral,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
logger.debug("centralManager_didDisconnectPeripheral_error_")
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_disconnect_peripheral,
|
||||
central,
|
||||
peripheral,
|
||||
error,
|
||||
)
|
||||
|
||||
|
||||
class CentralManagerDelegate:
|
||||
"""
|
||||
macOS conforming python class for managing the CentralManger for BLE
|
||||
|
||||
Before this object can be used, the :method:`wait_until_ready` method has to
|
||||
be called. This can take a while, when the OS asks the user for permissions.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""macOS init function for NSObject"""
|
||||
delegate = ObjcCentralManagerDelegate.alloc().initWithPyDelegate_(
|
||||
weakref.ref(self)
|
||||
)
|
||||
assert delegate is not None
|
||||
self.objc_delegate = delegate
|
||||
|
||||
self.event_loop = asyncio.get_running_loop()
|
||||
self._connect_futures: dict[NSUUID, asyncio.Future[bool]] = {}
|
||||
|
||||
self.callbacks: dict[
|
||||
int,
|
||||
Callable[[CBPeripheral, CBAdvertisementData, NSNumber], None],
|
||||
] = {}
|
||||
self._disconnect_callbacks: dict[NSUUID, DisconnectCallback] = {}
|
||||
self._disconnect_futures: dict[NSUUID, asyncio.Future[None]] = {}
|
||||
|
||||
self.did_update_state_event = asyncio.Event()
|
||||
self.central_manager = CBCentralManager.alloc().initWithDelegate_queue_(
|
||||
self.objc_delegate,
|
||||
dispatch_queue_create(b"bleak.corebluetooth", DISPATCH_QUEUE_SERIAL),
|
||||
)
|
||||
|
||||
# User defined functions
|
||||
@objc.python_method
|
||||
async def wait_until_ready(self):
|
||||
# According to CoreBluetooth docs, it is not valid to call CBCentral
|
||||
# methods until the centralManagerDidUpdateState_() delegate method
|
||||
# is called and the current state is CBManagerStatePoweredOn.
|
||||
# Wait until the callback occurs. This normally should not take too long,
|
||||
# but if the app currently has no permission to access the Bluetooth peripheral,
|
||||
# there is automatically a dialog shown by the OS. The user has to accept or deny
|
||||
# the Bluetooth access. This may take infinite time until the user clicks something.
|
||||
await self.did_update_state_event.wait()
|
||||
|
||||
state = self.central_manager.state()
|
||||
if state == CBManagerStateUnsupported:
|
||||
raise BleakBluetoothNotAvailableError(
|
||||
"Bluetooth is unsupported",
|
||||
BleakBluetoothNotAvailableReason.NO_BLUETOOTH,
|
||||
)
|
||||
elif state == CBManagerStateUnauthorized:
|
||||
authorization = self.central_manager.authorization()
|
||||
if authorization == CBManagerAuthorizationDenied:
|
||||
raise BleakBluetoothNotAvailableError(
|
||||
"Bluetooth access is denied by the user for the current application. Check macOS privacy settings.",
|
||||
BleakBluetoothNotAvailableReason.DENIED_BY_USER,
|
||||
)
|
||||
elif authorization == CBManagerAuthorizationRestricted:
|
||||
raise BleakBluetoothNotAvailableError(
|
||||
"Bluetooth access is restricted for the current application, e.g. by parental controls. Ask the admin to remove this restriction.",
|
||||
BleakBluetoothNotAvailableReason.DENIED_BY_SYSTEM,
|
||||
)
|
||||
else:
|
||||
raise BleakBluetoothNotAvailableError(
|
||||
"Bluetooth is not authorized for an unknown reason. Check macOS privacy settings.",
|
||||
BleakBluetoothNotAvailableReason.DENIED_BY_UNKNOWN,
|
||||
)
|
||||
elif state == CBManagerStatePoweredOff:
|
||||
raise BleakBluetoothNotAvailableError(
|
||||
"Bluetooth device is turned off",
|
||||
BleakBluetoothNotAvailableReason.POWERED_OFF,
|
||||
)
|
||||
elif state == CBManagerStateResetting:
|
||||
raise BleakBluetoothNotAvailableError(
|
||||
"Connection to the Bluetooth system service was lost. Currently trying to reconnect...",
|
||||
BleakBluetoothNotAvailableReason.UNKNOWN,
|
||||
)
|
||||
elif state != CBManagerStatePoweredOn:
|
||||
raise BleakBluetoothNotAvailableError(
|
||||
"Bluetooth state is unknown",
|
||||
BleakBluetoothNotAvailableReason.UNKNOWN,
|
||||
)
|
||||
|
||||
async def start_scan(self, service_uuids: Optional[list[str]]) -> None:
|
||||
_service_uuids = (
|
||||
NSArray[CBUUID]
|
||||
.alloc()
|
||||
.initWithArray_(list(map(CBUUID.UUIDWithString_, service_uuids)))
|
||||
if service_uuids
|
||||
else None
|
||||
)
|
||||
|
||||
self.central_manager.scanForPeripheralsWithServices_options_(
|
||||
_service_uuids, None
|
||||
)
|
||||
|
||||
async def stop_scan(self) -> None:
|
||||
self.central_manager.stopScan()
|
||||
|
||||
async def connect(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
disconnect_callback: DisconnectCallback,
|
||||
timeout: float = 10.0,
|
||||
) -> None:
|
||||
try:
|
||||
self._disconnect_callbacks[peripheral.identifier()] = disconnect_callback
|
||||
future = self.event_loop.create_future()
|
||||
|
||||
self._connect_futures[peripheral.identifier()] = future
|
||||
try:
|
||||
self.central_manager.connectPeripheral_options_(peripheral, None)
|
||||
async with async_timeout(timeout):
|
||||
await future
|
||||
finally:
|
||||
del self._connect_futures[peripheral.identifier()]
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.debug(f"Connection timed out after {timeout} seconds.")
|
||||
del self._disconnect_callbacks[peripheral.identifier()]
|
||||
future = self.event_loop.create_future()
|
||||
|
||||
self._disconnect_futures[peripheral.identifier()] = future
|
||||
try:
|
||||
self.central_manager.cancelPeripheralConnection_(peripheral)
|
||||
await future
|
||||
finally:
|
||||
del self._disconnect_futures[peripheral.identifier()]
|
||||
|
||||
raise
|
||||
|
||||
async def disconnect(self, peripheral: CBPeripheral) -> None:
|
||||
future = self.event_loop.create_future()
|
||||
|
||||
self._disconnect_futures[peripheral.identifier()] = future
|
||||
try:
|
||||
self.central_manager.cancelPeripheralConnection_(peripheral)
|
||||
await future
|
||||
finally:
|
||||
del self._disconnect_futures[peripheral.identifier()]
|
||||
|
||||
# Protocol Functions
|
||||
|
||||
def did_discover_peripheral(
|
||||
self,
|
||||
central: CBCentralManager,
|
||||
peripheral: CBPeripheral,
|
||||
advertisementData: NSDictionary[str, Any],
|
||||
RSSI: NSNumber,
|
||||
) -> None:
|
||||
# Note: this function might be called several times for same device.
|
||||
# This can happen for instance when an active scan is done, and the
|
||||
# second call with contain the data from the BLE scan response.
|
||||
# Example a first time with the following keys in advertisementData:
|
||||
# ['kCBAdvDataLocalName', 'kCBAdvDataIsConnectable', 'kCBAdvDataChannel']
|
||||
# ... and later a second time with other keys (and values) such as:
|
||||
# ['kCBAdvDataServiceUUIDs', 'kCBAdvDataIsConnectable', 'kCBAdvDataChannel']
|
||||
#
|
||||
# i.e it is best not to trust advertisementData for later use and data
|
||||
# from it should be copied.
|
||||
#
|
||||
# This behaviour could be affected by the
|
||||
# CBCentralManagerScanOptionAllowDuplicatesKey global setting.
|
||||
|
||||
uuid_string = peripheral.identifier().UUIDString()
|
||||
|
||||
for callback in self.callbacks.values():
|
||||
callback(peripheral, cast(CBAdvertisementData, advertisementData), RSSI)
|
||||
|
||||
logger.debug(
|
||||
"Discovered device %s: %s @ RSSI: %d (kCBAdvData %r) and Central: %r",
|
||||
uuid_string,
|
||||
peripheral.name(),
|
||||
RSSI,
|
||||
advertisementData.keys(),
|
||||
central,
|
||||
)
|
||||
|
||||
def did_connect_peripheral(
|
||||
self, central: CBCentralManager, peripheral: CBPeripheral
|
||||
) -> None:
|
||||
future = self._connect_futures.get(peripheral.identifier(), None)
|
||||
if future is not None:
|
||||
future.set_result(True)
|
||||
|
||||
def did_fail_to_connect_peripheral(
|
||||
self,
|
||||
centralManager: CBCentralManager,
|
||||
peripheral: CBPeripheral,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
future = self._connect_futures.get(peripheral.identifier(), None)
|
||||
if future is not None:
|
||||
if error is not None:
|
||||
future.set_exception(BleakError(f"failed to connect: {error}"))
|
||||
else:
|
||||
future.set_result(False)
|
||||
|
||||
def did_disconnect_peripheral(
|
||||
self,
|
||||
central: CBCentralManager,
|
||||
peripheral: CBPeripheral,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
logger.debug("Peripheral Device disconnected!")
|
||||
|
||||
future = self._disconnect_futures.get(peripheral.identifier(), None)
|
||||
if future is not None:
|
||||
if error is not None:
|
||||
future.set_exception(BleakError(f"disconnect failed: {error}"))
|
||||
else:
|
||||
future.set_result(None)
|
||||
|
||||
callback = self._disconnect_callbacks.pop(peripheral.identifier(), None)
|
||||
|
||||
if callback is not None:
|
||||
callback()
|
||||
@ -0,0 +1,727 @@
|
||||
"""
|
||||
|
||||
PeripheralDelegate
|
||||
|
||||
Created by kevincar <kevincarrolldavis@gmail.com>
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "darwin":
|
||||
assert False, "This backend is only available on macOS"
|
||||
|
||||
import asyncio
|
||||
import itertools
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, Optional
|
||||
|
||||
import objc
|
||||
from CoreBluetooth import (
|
||||
CBUUID,
|
||||
CBATTErrorDomain,
|
||||
CBCharacteristic,
|
||||
CBCharacteristicWriteType,
|
||||
CBCharacteristicWriteWithResponse,
|
||||
CBDescriptor,
|
||||
CBPeripheral,
|
||||
CBService,
|
||||
)
|
||||
from Foundation import NSUUID, NSArray, NSData, NSError, NSNumber, NSObject
|
||||
|
||||
from bleak._compat import Self
|
||||
from bleak._compat import timeout as async_timeout
|
||||
from bleak.args.corebluetooth import NotificationDiscriminator
|
||||
from bleak.backends._utils import external_thread_callback, try_call_soon_threadsafe
|
||||
from bleak.backends.client import NotifyCallback
|
||||
from bleak.exc import BleakError, BleakGATTProtocolError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CBPeripheralDelegate = objc.protocolNamed("CBPeripheralDelegate")
|
||||
|
||||
|
||||
class ObjcPeripheralDelegate(NSObject, protocols=[CBPeripheralDelegate]):
|
||||
"""
|
||||
CoreBluetooth peripheral manager delegate for bridging callbacks to asyncio.
|
||||
"""
|
||||
|
||||
def initWithPyDelegate_(
|
||||
self, py_delegate: weakref.ReferenceType["PeripheralDelegate"]
|
||||
) -> Optional[Self]:
|
||||
"""macOS init function for NSObject"""
|
||||
self = objc.super(ObjcPeripheralDelegate, self).init() # type: ignore[assignment]
|
||||
|
||||
if self is None:
|
||||
return None # pragma: no cover
|
||||
|
||||
self.py_delegate = py_delegate
|
||||
|
||||
return self
|
||||
|
||||
# Protocol Functions
|
||||
|
||||
@external_thread_callback
|
||||
def peripheral_didDiscoverServices_(
|
||||
self, peripheral: CBPeripheral, error: Optional[NSError]
|
||||
) -> None:
|
||||
logger.debug("peripheral_didDiscoverServices_")
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_discover_services,
|
||||
peripheral,
|
||||
peripheral.services(),
|
||||
error,
|
||||
)
|
||||
|
||||
@external_thread_callback
|
||||
def peripheral_didDiscoverIncludedServicesForService_error_( # pragma: no cover
|
||||
self, peripheral: CBPeripheral, service: CBService, error: Optional[NSError]
|
||||
) -> None:
|
||||
logger.debug("peripheral_didDiscoverIncludedServicesForService_error_")
|
||||
# Currently not used in Bleak
|
||||
|
||||
@external_thread_callback
|
||||
def peripheral_didDiscoverCharacteristicsForService_error_(
|
||||
self, peripheral: CBPeripheral, service: CBService, error: Optional[NSError]
|
||||
) -> None:
|
||||
logger.debug("peripheral_didDiscoverCharacteristicsForService_error_")
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_discover_characteristics_for_service,
|
||||
peripheral,
|
||||
service,
|
||||
service.characteristics(),
|
||||
error,
|
||||
)
|
||||
|
||||
@external_thread_callback
|
||||
def peripheral_didDiscoverDescriptorsForCharacteristic_error_(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
characteristic: CBCharacteristic,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
logger.debug("peripheral_didDiscoverDescriptorsForCharacteristic_error_")
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_discover_descriptors_for_characteristic,
|
||||
peripheral,
|
||||
characteristic,
|
||||
error,
|
||||
)
|
||||
|
||||
@external_thread_callback
|
||||
def peripheral_didUpdateValueForCharacteristic_error_(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
characteristic: CBCharacteristic,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
logger.debug("peripheral_didUpdateValueForCharacteristic_error_")
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_update_value_for_characteristic,
|
||||
peripheral,
|
||||
characteristic,
|
||||
characteristic.value(),
|
||||
error,
|
||||
)
|
||||
|
||||
@external_thread_callback
|
||||
def peripheral_didUpdateValueForDescriptor_error_(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
descriptor: CBDescriptor,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
logger.debug("peripheral_didUpdateValueForDescriptor_error_")
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_update_value_for_descriptor,
|
||||
peripheral,
|
||||
descriptor,
|
||||
descriptor.value(),
|
||||
error,
|
||||
)
|
||||
|
||||
@external_thread_callback
|
||||
def peripheral_didWriteValueForCharacteristic_error_(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
characteristic: CBCharacteristic,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
logger.debug("peripheral_didWriteValueForCharacteristic_error_")
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_write_value_for_characteristic,
|
||||
peripheral,
|
||||
characteristic,
|
||||
error,
|
||||
)
|
||||
|
||||
@external_thread_callback
|
||||
def peripheral_didWriteValueForDescriptor_error_(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
descriptor: CBDescriptor,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
logger.debug("peripheral_didWriteValueForDescriptor_error_")
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_write_value_for_descriptor,
|
||||
peripheral,
|
||||
descriptor,
|
||||
error,
|
||||
)
|
||||
|
||||
@external_thread_callback
|
||||
def peripheralIsReadyToSendWriteWithoutResponse_( # pragma: no cover
|
||||
self, peripheral: CBPeripheral
|
||||
) -> None:
|
||||
logger.debug("peripheralIsReadyToSendWriteWithoutResponse_")
|
||||
# Currently not used in Bleak
|
||||
|
||||
@external_thread_callback
|
||||
def peripheral_didUpdateNotificationStateForCharacteristic_error_(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
characteristic: CBCharacteristic,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
logger.debug("peripheral_didUpdateNotificationStateForCharacteristic_error_")
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_update_notification_for_characteristic,
|
||||
peripheral,
|
||||
characteristic,
|
||||
error,
|
||||
)
|
||||
|
||||
@external_thread_callback
|
||||
def peripheral_didReadRSSI_error_(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
rssi: NSNumber,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
logger.debug("peripheral_didReadRSSI_error_")
|
||||
|
||||
if (py_delegate := self.py_delegate()) is None:
|
||||
return
|
||||
|
||||
try_call_soon_threadsafe(
|
||||
py_delegate.event_loop,
|
||||
py_delegate.did_read_rssi,
|
||||
peripheral,
|
||||
int(rssi),
|
||||
error,
|
||||
)
|
||||
|
||||
@external_thread_callback
|
||||
def peripheralDidUpdateName_( # pragma: no cover
|
||||
self, peripheral: CBPeripheral
|
||||
) -> None:
|
||||
logger.debug("peripheralDidUpdateName_")
|
||||
logger.debug(
|
||||
f"name of {peripheral.identifier()} changed to {peripheral.name()}"
|
||||
)
|
||||
# Currently not used in Bleak
|
||||
|
||||
@external_thread_callback
|
||||
def peripheral_didModifyServices_( # pragma: no cover
|
||||
self, peripheral: CBPeripheral, invalidatedServices: NSArray[CBService]
|
||||
) -> None:
|
||||
logger.debug("peripheral_didModifyServices_")
|
||||
logger.debug(
|
||||
f"{peripheral.identifier()} invalidated services: {invalidatedServices}"
|
||||
)
|
||||
# Currently not used in Bleak
|
||||
|
||||
|
||||
class PeripheralDelegate:
|
||||
"""macOS conforming python class for managing the PeripheralDelegate for BLE"""
|
||||
|
||||
def __init__(self, peripheral: CBPeripheral) -> None:
|
||||
delegate = ObjcPeripheralDelegate.alloc().initWithPyDelegate_(weakref.ref(self))
|
||||
assert delegate is not None
|
||||
self.objc_delegate = delegate
|
||||
|
||||
self.peripheral = peripheral
|
||||
self.peripheral.setDelegate_(self.objc_delegate)
|
||||
|
||||
self.event_loop = asyncio.get_running_loop()
|
||||
self._services_discovered_future = self.event_loop.create_future()
|
||||
|
||||
self._service_characteristic_discovered_futures: dict[
|
||||
int, asyncio.Future[NSArray[CBCharacteristic]]
|
||||
] = {}
|
||||
self._characteristic_descriptor_discover_futures: dict[
|
||||
int, asyncio.Future[None]
|
||||
] = {}
|
||||
|
||||
self._characteristic_read_futures: dict[int, asyncio.Future[NSData]] = {}
|
||||
self._characteristic_write_futures: dict[int, asyncio.Future[None]] = {}
|
||||
|
||||
self._descriptor_read_futures: dict[int, asyncio.Future[NSObject]] = {}
|
||||
self._descriptor_write_futures: dict[int, asyncio.Future[None]] = {}
|
||||
|
||||
self._characteristic_notify_change_futures: dict[int, asyncio.Future[None]] = {}
|
||||
self._characteristic_notify_callbacks: dict[int, NotifyCallback] = {}
|
||||
self._characteristic_notification_discriminators: dict[
|
||||
int, Optional[NotificationDiscriminator]
|
||||
] = {}
|
||||
|
||||
self._read_rssi_futures: dict[NSUUID, asyncio.Future[int]] = {}
|
||||
|
||||
def futures(self) -> Iterable[asyncio.Future[Any]]:
|
||||
"""
|
||||
Gets all futures for this delegate.
|
||||
|
||||
These can be used to handle any pending futures when a peripheral is disconnected.
|
||||
"""
|
||||
services_discovered_future = (
|
||||
(self._services_discovered_future,)
|
||||
if hasattr(self, "_services_discovered_future")
|
||||
else ()
|
||||
)
|
||||
|
||||
return itertools.chain(
|
||||
services_discovered_future,
|
||||
self._service_characteristic_discovered_futures.values(),
|
||||
self._characteristic_descriptor_discover_futures.values(),
|
||||
self._characteristic_read_futures.values(),
|
||||
self._characteristic_write_futures.values(),
|
||||
self._descriptor_read_futures.values(),
|
||||
self._descriptor_write_futures.values(),
|
||||
self._characteristic_notify_change_futures.values(),
|
||||
self._read_rssi_futures.values(),
|
||||
)
|
||||
|
||||
async def discover_services(
|
||||
self, services: Optional[NSArray[CBUUID]] = None
|
||||
) -> NSArray[CBService]:
|
||||
future = self.event_loop.create_future()
|
||||
|
||||
self._services_discovered_future = future
|
||||
try:
|
||||
self.peripheral.discoverServices_(services)
|
||||
return await future
|
||||
finally:
|
||||
del self._services_discovered_future
|
||||
|
||||
async def discover_characteristics(
|
||||
self, service: CBService
|
||||
) -> NSArray[CBCharacteristic]:
|
||||
future = self.event_loop.create_future()
|
||||
|
||||
self._service_characteristic_discovered_futures[service.startHandle()] = future
|
||||
try:
|
||||
self.peripheral.discoverCharacteristics_forService_(None, service)
|
||||
return await future
|
||||
finally:
|
||||
del self._service_characteristic_discovered_futures[service.startHandle()]
|
||||
|
||||
async def discover_descriptors(
|
||||
self, characteristic: CBCharacteristic
|
||||
) -> NSArray[CBDescriptor]:
|
||||
future = self.event_loop.create_future()
|
||||
|
||||
self._characteristic_descriptor_discover_futures[characteristic.handle()] = (
|
||||
future
|
||||
)
|
||||
try:
|
||||
self.peripheral.discoverDescriptorsForCharacteristic_(characteristic)
|
||||
await future
|
||||
finally:
|
||||
del self._characteristic_descriptor_discover_futures[
|
||||
characteristic.handle()
|
||||
]
|
||||
|
||||
return characteristic.descriptors()
|
||||
|
||||
async def read_characteristic(
|
||||
self,
|
||||
characteristic: CBCharacteristic,
|
||||
use_cached: bool,
|
||||
timeout: int = 20,
|
||||
) -> NSData:
|
||||
value = characteristic.value()
|
||||
if value is not None and use_cached:
|
||||
return value
|
||||
|
||||
future = self.event_loop.create_future()
|
||||
|
||||
self._characteristic_read_futures[characteristic.handle()] = future
|
||||
|
||||
try:
|
||||
self.peripheral.readValueForCharacteristic_(characteristic)
|
||||
async with async_timeout(timeout):
|
||||
return await future
|
||||
finally:
|
||||
del self._characteristic_read_futures[characteristic.handle()]
|
||||
|
||||
async def read_descriptor(
|
||||
self, descriptor: CBDescriptor, use_cached: bool = True
|
||||
) -> Any:
|
||||
value = descriptor.value()
|
||||
if value is not None and use_cached:
|
||||
return value
|
||||
|
||||
future = self.event_loop.create_future()
|
||||
|
||||
self._descriptor_read_futures[descriptor.handle()] = future
|
||||
try:
|
||||
self.peripheral.readValueForDescriptor_(descriptor)
|
||||
return await future
|
||||
finally:
|
||||
del self._descriptor_read_futures[descriptor.handle()]
|
||||
|
||||
async def write_characteristic(
|
||||
self,
|
||||
characteristic: CBCharacteristic,
|
||||
value: NSData,
|
||||
response: CBCharacteristicWriteType,
|
||||
) -> None:
|
||||
# in CoreBluetooth there is no indication of success or failure of
|
||||
# CBCharacteristicWriteWithoutResponse
|
||||
if response == CBCharacteristicWriteWithResponse:
|
||||
future = self.event_loop.create_future()
|
||||
|
||||
self._characteristic_write_futures[characteristic.handle()] = future
|
||||
try:
|
||||
self.peripheral.writeValue_forCharacteristic_type_(
|
||||
value, characteristic, response
|
||||
)
|
||||
await future
|
||||
finally:
|
||||
del self._characteristic_write_futures[characteristic.handle()]
|
||||
else:
|
||||
self.peripheral.writeValue_forCharacteristic_type_(
|
||||
value, characteristic, response
|
||||
)
|
||||
|
||||
async def write_descriptor(self, descriptor: CBDescriptor, value: NSData) -> None:
|
||||
future = self.event_loop.create_future()
|
||||
|
||||
self._descriptor_write_futures[descriptor.handle()] = future
|
||||
try:
|
||||
self.peripheral.writeValue_forDescriptor_(value, descriptor)
|
||||
await future
|
||||
finally:
|
||||
del self._descriptor_write_futures[descriptor.handle()]
|
||||
|
||||
async def start_notifications(
|
||||
self,
|
||||
characteristic: CBCharacteristic,
|
||||
callback: NotifyCallback,
|
||||
notification_discriminator: Optional[NotificationDiscriminator] = None,
|
||||
) -> None:
|
||||
c_handle = characteristic.handle()
|
||||
if c_handle in self._characteristic_notify_callbacks:
|
||||
raise ValueError("Characteristic notifications already started")
|
||||
|
||||
self._characteristic_notify_callbacks[c_handle] = callback
|
||||
self._characteristic_notification_discriminators[c_handle] = (
|
||||
notification_discriminator
|
||||
)
|
||||
|
||||
future = self.event_loop.create_future()
|
||||
|
||||
self._characteristic_notify_change_futures[c_handle] = future
|
||||
try:
|
||||
self.peripheral.setNotifyValue_forCharacteristic_(True, characteristic)
|
||||
await future
|
||||
finally:
|
||||
del self._characteristic_notify_change_futures[c_handle]
|
||||
|
||||
async def stop_notifications(self, characteristic: CBCharacteristic) -> None:
|
||||
c_handle = characteristic.handle()
|
||||
if c_handle not in self._characteristic_notify_callbacks:
|
||||
raise ValueError("Characteristic notification never started")
|
||||
|
||||
future = self.event_loop.create_future()
|
||||
|
||||
self._characteristic_notify_change_futures[c_handle] = future
|
||||
try:
|
||||
self.peripheral.setNotifyValue_forCharacteristic_(False, characteristic)
|
||||
await future
|
||||
finally:
|
||||
del self._characteristic_notify_change_futures[c_handle]
|
||||
|
||||
self._characteristic_notify_callbacks.pop(c_handle)
|
||||
self._characteristic_notification_discriminators.pop(c_handle)
|
||||
|
||||
async def read_rssi(self) -> int:
|
||||
future = self.event_loop.create_future()
|
||||
|
||||
self._read_rssi_futures[self.peripheral.identifier()] = future
|
||||
try:
|
||||
self.peripheral.readRSSI()
|
||||
return await future
|
||||
finally:
|
||||
del self._read_rssi_futures[self.peripheral.identifier()]
|
||||
|
||||
# Protocol Functions
|
||||
|
||||
def did_discover_services(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
services: NSArray[CBService],
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
future = self._services_discovered_future
|
||||
if error is not None:
|
||||
exception = BleakError(f"Failed to discover services {error}")
|
||||
future.set_exception(exception)
|
||||
else:
|
||||
logger.debug("Services discovered")
|
||||
future.set_result(services)
|
||||
|
||||
def did_discover_characteristics_for_service(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
service: CBService,
|
||||
characteristics: NSArray[CBCharacteristic],
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
future = self._service_characteristic_discovered_futures.get(
|
||||
service.startHandle()
|
||||
)
|
||||
if not future:
|
||||
logger.debug(
|
||||
f"Unexpected event didDiscoverCharacteristicsForService for {service.startHandle()}"
|
||||
)
|
||||
return
|
||||
if error is not None:
|
||||
exception = BleakError(
|
||||
f"Failed to discover characteristics for service {service.startHandle()}: {error}"
|
||||
)
|
||||
future.set_exception(exception)
|
||||
else:
|
||||
logger.debug("Characteristics discovered")
|
||||
future.set_result(characteristics)
|
||||
|
||||
def did_discover_descriptors_for_characteristic(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
characteristic: CBCharacteristic,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
future = self._characteristic_descriptor_discover_futures.get(
|
||||
characteristic.handle()
|
||||
)
|
||||
if not future:
|
||||
logger.warning(
|
||||
f"Unexpected event didDiscoverDescriptorsForCharacteristic for {characteristic.handle()}"
|
||||
)
|
||||
return
|
||||
if error is not None:
|
||||
exception = BleakError(
|
||||
f"Failed to discover descriptors for characteristic {characteristic.handle()}: {error}"
|
||||
)
|
||||
future.set_exception(exception)
|
||||
else:
|
||||
logger.debug(f"Descriptor discovered {characteristic.handle()}")
|
||||
future.set_result(None)
|
||||
|
||||
def did_update_value_for_characteristic(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
characteristic: CBCharacteristic,
|
||||
value: Optional[NSData],
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
c_handle = characteristic.handle()
|
||||
|
||||
future = self._characteristic_read_futures.get(c_handle)
|
||||
|
||||
# If error is set, then we know this was a read response.
|
||||
# Otherwise, if there is a pending read request, we can't tell if this is a read response or notification.
|
||||
# If the user provided a notification discriminator, we can use that to
|
||||
# identify if this callback is due to a notification by analyzing the value.
|
||||
# If not, and there is a future (pending read request), we assume it is a read response but can't know for sure.
|
||||
if not error:
|
||||
assert value is not None
|
||||
|
||||
notification_discriminator = (
|
||||
self._characteristic_notification_discriminators.get(c_handle)
|
||||
)
|
||||
if not future or (
|
||||
notification_discriminator and notification_discriminator(bytes(value))
|
||||
):
|
||||
notify_callback = self._characteristic_notify_callbacks.get(c_handle)
|
||||
|
||||
if notify_callback:
|
||||
notify_callback(bytearray(value))
|
||||
return
|
||||
|
||||
if not future:
|
||||
logger.warning(
|
||||
"Unexpected event didUpdateValueForCharacteristic for 0x%04x with value: %r and error: %r",
|
||||
c_handle,
|
||||
value,
|
||||
error,
|
||||
)
|
||||
return
|
||||
|
||||
if error is not None:
|
||||
exception = (
|
||||
BleakGATTProtocolError(error.code())
|
||||
if error.domain() == CBATTErrorDomain
|
||||
else BleakError(f"Failed to read characteristic {c_handle}: {error}")
|
||||
)
|
||||
future.set_exception(exception)
|
||||
else:
|
||||
logger.debug("Read characteristic value")
|
||||
assert value is not None
|
||||
future.set_result(value)
|
||||
|
||||
def did_update_value_for_descriptor(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
descriptor: CBDescriptor,
|
||||
value: Optional[Any],
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
future = self._descriptor_read_futures.get(descriptor.handle())
|
||||
if not future:
|
||||
logger.warning("Unexpected event didUpdateValueForDescriptor")
|
||||
return
|
||||
if error is not None:
|
||||
exception = (
|
||||
BleakGATTProtocolError(error.code())
|
||||
if error.domain() == CBATTErrorDomain
|
||||
else BleakError(
|
||||
f"Failed to read descriptor {descriptor.handle()}: {error}"
|
||||
)
|
||||
)
|
||||
future.set_exception(exception)
|
||||
else:
|
||||
logger.debug("Read descriptor value")
|
||||
assert value is not None
|
||||
future.set_result(value)
|
||||
|
||||
def did_write_value_for_characteristic(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
characteristic: CBCharacteristic,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
future = self._characteristic_write_futures.get(characteristic.handle(), None)
|
||||
if not future:
|
||||
return # event only expected on write with response
|
||||
if error is not None:
|
||||
exception = (
|
||||
BleakGATTProtocolError(error.code())
|
||||
if error.domain() == CBATTErrorDomain
|
||||
else BleakError(
|
||||
f"Failed to write characteristic {characteristic.handle()}: {error}"
|
||||
)
|
||||
)
|
||||
future.set_exception(exception)
|
||||
else:
|
||||
logger.debug("Write Characteristic Value")
|
||||
future.set_result(None)
|
||||
|
||||
def did_write_value_for_descriptor(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
descriptor: CBDescriptor,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
future = self._descriptor_write_futures.get(descriptor.handle())
|
||||
if not future:
|
||||
logger.warning("Unexpected event didWriteValueForDescriptor")
|
||||
return
|
||||
if error is not None:
|
||||
exception = (
|
||||
BleakGATTProtocolError(error.code())
|
||||
if error.domain() == CBATTErrorDomain
|
||||
else BleakError(
|
||||
f"Failed to write descriptor {descriptor.handle()}: {error}"
|
||||
)
|
||||
)
|
||||
|
||||
future.set_exception(exception)
|
||||
else:
|
||||
logger.debug("Write Descriptor Value")
|
||||
future.set_result(None)
|
||||
|
||||
def did_update_notification_for_characteristic(
|
||||
self,
|
||||
peripheral: CBPeripheral,
|
||||
characteristic: CBCharacteristic,
|
||||
error: Optional[NSError],
|
||||
) -> None:
|
||||
c_handle = characteristic.handle()
|
||||
future = self._characteristic_notify_change_futures.get(c_handle)
|
||||
if not future:
|
||||
logger.warning(
|
||||
"Unexpected event didUpdateNotificationStateForCharacteristic"
|
||||
)
|
||||
return
|
||||
if error is not None:
|
||||
exception = BleakError(
|
||||
f"Failed to update the notification status for characteristic {c_handle}: {error}"
|
||||
)
|
||||
future.set_exception(exception)
|
||||
else:
|
||||
logger.debug("Character Notify Update")
|
||||
future.set_result(None)
|
||||
|
||||
def did_read_rssi(
|
||||
self, peripheral: CBPeripheral, rssi: int, error: Optional[NSError]
|
||||
) -> None:
|
||||
future = self._read_rssi_futures.get(peripheral.identifier(), None)
|
||||
|
||||
if not future:
|
||||
logger.warning("Unexpected event did_read_rssi")
|
||||
return
|
||||
|
||||
if error is not None:
|
||||
exception = BleakError(f"Failed to read RSSI: {error}")
|
||||
future.set_exception(exception)
|
||||
else:
|
||||
future.set_result(rssi)
|
||||
@ -0,0 +1,14 @@
|
||||
# Created on 2017-11-19 by hbldh <henrik.blidh@nedomkull.com>
|
||||
"""
|
||||
__init__.py
|
||||
"""
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "darwin":
|
||||
assert False, "This backend is only available on macOS"
|
||||
|
||||
import objc
|
||||
|
||||
objc.options.verbose = True
|
||||
@ -0,0 +1,402 @@
|
||||
# Created on 2019-06-26 by kevincar <kevincarrolldavis@gmail.com>
|
||||
"""
|
||||
BLE Client for CoreBluetooth on macOS
|
||||
"""
|
||||
|
||||
import functools
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "darwin":
|
||||
assert False, "This backend is only available on macOS"
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from CoreBluetooth import (
|
||||
CBUUID,
|
||||
CBCharacteristicWriteWithoutResponse,
|
||||
CBCharacteristicWriteWithResponse,
|
||||
CBPeripheral,
|
||||
CBPeripheralStateConnected,
|
||||
)
|
||||
from Foundation import NSArray, NSData
|
||||
|
||||
from bleak import BleakScanner
|
||||
from bleak._compat import override
|
||||
from bleak.args import SizedBuffer
|
||||
from bleak.args.corebluetooth import CBStartNotifyArgs
|
||||
from bleak.assigned_numbers import gatt_char_props_to_strs
|
||||
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||
from bleak.backends.client import BaseBleakClient, NotifyCallback
|
||||
from bleak.backends.corebluetooth.CentralManagerDelegate import CentralManagerDelegate
|
||||
from bleak.backends.corebluetooth.PeripheralDelegate import PeripheralDelegate
|
||||
from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth
|
||||
from bleak.backends.corebluetooth.utils import (
|
||||
cb_uuid_to_str,
|
||||
is_descriptor_nsnumber,
|
||||
is_descriptor_nsstring,
|
||||
)
|
||||
from bleak.backends.descriptor import BleakGATTDescriptor
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection
|
||||
from bleak.exc import BleakDeviceNotFoundError, BleakError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BleakClientCoreBluetooth(BaseBleakClient):
|
||||
"""CoreBluetooth class interface for BleakClient
|
||||
|
||||
Args:
|
||||
address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it.
|
||||
services: Optional set of service UUIDs that will be used.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
address_or_ble_device: Union[BLEDevice, str],
|
||||
services: Optional[set[str]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(address_or_ble_device, **kwargs)
|
||||
|
||||
self._peripheral: Optional[CBPeripheral] = None
|
||||
self._delegate: Optional[PeripheralDelegate] = None
|
||||
self._central_manager_delegate: Optional[CentralManagerDelegate] = None
|
||||
|
||||
if isinstance(address_or_ble_device, BLEDevice):
|
||||
(
|
||||
self._peripheral,
|
||||
self._central_manager_delegate,
|
||||
) = address_or_ble_device.details
|
||||
|
||||
self._requested_services = (
|
||||
NSArray[CBUUID]
|
||||
.alloc()
|
||||
.initWithArray_(list(map(CBUUID.UUIDWithString_, services)))
|
||||
if services
|
||||
else None
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"BleakClientCoreBluetooth ({self.address})"
|
||||
|
||||
@override
|
||||
async def connect(self, pair: bool, **kwargs: Any) -> None:
|
||||
"""Connect to a specified Peripheral
|
||||
|
||||
Keyword Args:
|
||||
timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call.
|
||||
"""
|
||||
if pair:
|
||||
logger.debug("Explicit pairing is not available in CoreBluetooth.")
|
||||
|
||||
timeout = kwargs.get("timeout", self._timeout)
|
||||
if self._peripheral is None:
|
||||
device = await BleakScanner.find_device_by_address(
|
||||
self.address, timeout=timeout, backend=BleakScannerCoreBluetooth
|
||||
)
|
||||
|
||||
if device:
|
||||
self._peripheral, self._central_manager_delegate = device.details
|
||||
else:
|
||||
raise BleakDeviceNotFoundError(
|
||||
self.address, f"Device with address {self.address} was not found"
|
||||
)
|
||||
|
||||
if self._delegate is None:
|
||||
assert self._peripheral is not None
|
||||
self._delegate = PeripheralDelegate(self._peripheral)
|
||||
|
||||
def disconnect_callback() -> None:
|
||||
# Ensure that `get_services` retrieves services again, rather
|
||||
# than using the cached object
|
||||
self.services = None
|
||||
|
||||
assert self._delegate is not None
|
||||
|
||||
# If there are any pending futures waiting for delegate callbacks, we
|
||||
# need to raise an exception since the callback will no longer be
|
||||
# called because the device is disconnected.
|
||||
for future in self._delegate.futures():
|
||||
try:
|
||||
future.set_exception(BleakError("disconnected"))
|
||||
except asyncio.InvalidStateError:
|
||||
# the future was already done
|
||||
pass
|
||||
|
||||
if self._disconnected_callback:
|
||||
self._disconnected_callback()
|
||||
|
||||
manager = self._central_manager_delegate
|
||||
assert manager is not None
|
||||
logger.debug("CentralManagerDelegate at %r", manager)
|
||||
logger.debug("Connecting to BLE device @ %s", self.address)
|
||||
assert self._peripheral is not None
|
||||
await manager.connect(self._peripheral, disconnect_callback, timeout=timeout)
|
||||
|
||||
# Now get services
|
||||
await self._get_services()
|
||||
|
||||
@override
|
||||
async def disconnect(self) -> None:
|
||||
"""Disconnect from the peripheral device"""
|
||||
if (
|
||||
self._peripheral is None
|
||||
or self._peripheral.state() != CBPeripheralStateConnected
|
||||
):
|
||||
return
|
||||
|
||||
assert self._central_manager_delegate
|
||||
await self._central_manager_delegate.disconnect(self._peripheral)
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_connected(self) -> bool:
|
||||
"""Checks for current active connection"""
|
||||
return (
|
||||
False
|
||||
if self._peripheral is None
|
||||
else self._peripheral.state() == CBPeripheralStateConnected
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def name(self) -> str:
|
||||
"""Get the name of the connected peripheral"""
|
||||
if self._peripheral is None:
|
||||
raise BleakError("Not connected")
|
||||
return self._peripheral.name()
|
||||
|
||||
@property
|
||||
@override
|
||||
def mtu_size(self) -> int:
|
||||
"""Get ATT MTU size for active connection"""
|
||||
# Use type CBCharacteristicWriteWithoutResponse to get maximum write
|
||||
# value length based on the negotiated ATT MTU size. Add the ATT header
|
||||
# length (+3) to get the actual ATT MTU size.
|
||||
assert self._peripheral
|
||||
return (
|
||||
self._peripheral.maximumWriteValueLengthForType_(
|
||||
CBCharacteristicWriteWithoutResponse
|
||||
)
|
||||
+ 3
|
||||
)
|
||||
|
||||
@override
|
||||
async def pair(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Attempt to pair with a peripheral.
|
||||
|
||||
Raises:
|
||||
NotImplementedError:
|
||||
This is not available on macOS since there is not explicit API
|
||||
to do a pairing. Instead, the docs state that it "auto-pairs",
|
||||
when trying to read a characteristic that requires encryption.
|
||||
|
||||
Reference:
|
||||
|
||||
- `Apple Docs <https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/BestPracticesForSettingUpYourIOSDeviceAsAPeripheral/BestPracticesForSettingUpYourIOSDeviceAsAPeripheral.html#//apple_ref/doc/uid/TP40013257-CH5-SW1>`_
|
||||
- `Stack Overflow post #1 <https://stackoverflow.com/questions/25254932/can-you-pair-a-bluetooth-le-device-in-an-ios-app>`_
|
||||
- `Stack Overflow post #2 <https://stackoverflow.com/questions/47546690/ios-bluetooth-pairing-request-dialog-can-i-know-the-users-choice>`_
|
||||
"""
|
||||
raise NotImplementedError("Pairing is not available in Core Bluetooth.")
|
||||
|
||||
@override
|
||||
async def unpair(self) -> None:
|
||||
"""
|
||||
Remove pairing information for a peripheral.
|
||||
|
||||
Raises:
|
||||
NotImplementedError:
|
||||
This is not available on macOS since there is not explicit API
|
||||
to do a pairing.
|
||||
"""
|
||||
raise NotImplementedError("Pairing is not available in Core Bluetooth.")
|
||||
|
||||
async def _get_services(self) -> BleakGATTServiceCollection:
|
||||
"""Get all services registered for this GATT server.
|
||||
|
||||
Returns:
|
||||
A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree.
|
||||
|
||||
"""
|
||||
if self.services is not None:
|
||||
return self.services
|
||||
|
||||
services = BleakGATTServiceCollection()
|
||||
|
||||
logger.debug("Retrieving services...")
|
||||
assert self._delegate
|
||||
assert self._peripheral
|
||||
cb_services = await self._delegate.discover_services(self._requested_services)
|
||||
|
||||
for service in cb_services:
|
||||
serv = BleakGATTService(
|
||||
service, service.startHandle(), cb_uuid_to_str(service.UUID())
|
||||
)
|
||||
services.add_service(serv)
|
||||
|
||||
serviceUUID = service.UUID().UUIDString()
|
||||
logger.debug("Retrieving characteristics for service %s", serviceUUID)
|
||||
characteristics = await self._delegate.discover_characteristics(service)
|
||||
|
||||
for characteristic in characteristics:
|
||||
cUUID = characteristic.UUID().UUIDString()
|
||||
logger.debug("Retrieving descriptors for characteristic %s", cUUID)
|
||||
|
||||
char = BleakGATTCharacteristic(
|
||||
characteristic,
|
||||
characteristic.handle(),
|
||||
cb_uuid_to_str(characteristic.UUID()),
|
||||
list(gatt_char_props_to_strs(characteristic.properties())),
|
||||
functools.partial(
|
||||
self._peripheral.maximumWriteValueLengthForType_,
|
||||
CBCharacteristicWriteWithoutResponse,
|
||||
),
|
||||
serv,
|
||||
)
|
||||
services.add_characteristic(char)
|
||||
|
||||
descriptors = await self._delegate.discover_descriptors(characteristic)
|
||||
for descriptor in descriptors:
|
||||
desc = BleakGATTDescriptor(
|
||||
descriptor,
|
||||
int(descriptor.handle()),
|
||||
cb_uuid_to_str(descriptor.UUID()),
|
||||
char,
|
||||
)
|
||||
services.add_descriptor(desc)
|
||||
|
||||
logger.debug("Services resolved for %s", str(self))
|
||||
self.services = services
|
||||
return self.services
|
||||
|
||||
@override
|
||||
async def read_gatt_char(
|
||||
self,
|
||||
characteristic: BleakGATTCharacteristic,
|
||||
*,
|
||||
use_cached: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> bytearray:
|
||||
"""Perform read operation on the specified GATT characteristic.
|
||||
|
||||
Args:
|
||||
characteristic (BleakGATTCharacteristic): The characteristic to read from.
|
||||
|
||||
Returns:
|
||||
(bytearray) The read data.
|
||||
|
||||
"""
|
||||
assert self._delegate
|
||||
output = await self._delegate.read_characteristic(
|
||||
characteristic.obj, use_cached=use_cached
|
||||
)
|
||||
value = bytearray(output)
|
||||
logger.debug("Read Characteristic %s: %r", characteristic.uuid, value)
|
||||
return value
|
||||
|
||||
@override
|
||||
async def read_gatt_descriptor(
|
||||
self,
|
||||
descriptor: BleakGATTDescriptor,
|
||||
*,
|
||||
use_cached: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> bytearray:
|
||||
"""Perform read operation on the specified GATT descriptor.
|
||||
|
||||
Args:
|
||||
handle (int): The handle of the descriptor to read from.
|
||||
use_cached (bool): `False` forces Windows to read the value from the
|
||||
device again and not use its own cached value. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
(bytearray) The read data.
|
||||
"""
|
||||
assert self._delegate
|
||||
output = await self._delegate.read_descriptor(
|
||||
descriptor.obj, use_cached=use_cached
|
||||
)
|
||||
|
||||
if is_descriptor_nsnumber(output, descriptor.uuid):
|
||||
value = bytearray(int(output).to_bytes(2, byteorder="little"))
|
||||
elif is_descriptor_nsstring(output, descriptor.uuid):
|
||||
value = bytearray(output.encode())
|
||||
else:
|
||||
value = bytearray(output)
|
||||
|
||||
logger.debug("Read Descriptor %d : %r", descriptor.handle, value)
|
||||
return value
|
||||
|
||||
@override
|
||||
async def write_gatt_char(
|
||||
self, characteristic: BleakGATTCharacteristic, data: SizedBuffer, response: bool
|
||||
) -> None:
|
||||
value = NSData.alloc().initWithBytes_length_(data, len(data))
|
||||
assert self._delegate
|
||||
await self._delegate.write_characteristic(
|
||||
characteristic.obj,
|
||||
value,
|
||||
(
|
||||
CBCharacteristicWriteWithResponse
|
||||
if response
|
||||
else CBCharacteristicWriteWithoutResponse
|
||||
),
|
||||
)
|
||||
logger.debug(f"Write Characteristic {characteristic.uuid} : {data}")
|
||||
|
||||
@override
|
||||
async def write_gatt_descriptor(
|
||||
self, descriptor: BleakGATTDescriptor, data: SizedBuffer
|
||||
) -> None:
|
||||
"""Perform a write operation on the specified GATT descriptor.
|
||||
|
||||
Args:
|
||||
descriptor: The descriptor to read from.
|
||||
data: The data to send (any bytes-like object).
|
||||
|
||||
"""
|
||||
assert self._delegate
|
||||
value = NSData.alloc().initWithBytes_length_(data, len(data))
|
||||
await self._delegate.write_descriptor(descriptor.obj, value)
|
||||
logger.debug("Write Descriptor %d : %r", descriptor.handle, data)
|
||||
|
||||
@override
|
||||
async def start_notify(
|
||||
self,
|
||||
characteristic: BleakGATTCharacteristic,
|
||||
callback: NotifyCallback,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Activate notifications/indications on a characteristic.
|
||||
"""
|
||||
assert self._delegate is not None
|
||||
|
||||
cb: CBStartNotifyArgs = kwargs["cb"]
|
||||
|
||||
await self._delegate.start_notifications(
|
||||
characteristic.obj,
|
||||
callback,
|
||||
cb.get("notification_discriminator"),
|
||||
)
|
||||
|
||||
@override
|
||||
async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None:
|
||||
"""Deactivate notification/indication on a specified characteristic.
|
||||
|
||||
Args:
|
||||
characteristic (BleakGATTCharacteristic: The characteristic to deactivate
|
||||
notification/indication on.
|
||||
"""
|
||||
assert self._delegate
|
||||
await self._delegate.stop_notifications(characteristic.obj)
|
||||
|
||||
async def get_rssi(self) -> int:
|
||||
"""To get RSSI value in dBm of the connected Peripheral"""
|
||||
assert self._delegate
|
||||
return int(await self._delegate.read_rssi())
|
||||
@ -0,0 +1,180 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "darwin":
|
||||
assert False, "This backend is only available on macOS"
|
||||
|
||||
import logging
|
||||
from typing import Any, Literal, Optional, cast
|
||||
from warnings import warn
|
||||
|
||||
import objc
|
||||
from CoreBluetooth import CBPeripheral
|
||||
from Foundation import NSBundle, NSNumber
|
||||
|
||||
from bleak._compat import override
|
||||
from bleak.args.corebluetooth import CBScannerArgs as _CBScannerArgs
|
||||
from bleak.backends.corebluetooth.CentralManagerDelegate import (
|
||||
CBAdvertisementData,
|
||||
CentralManagerDelegate,
|
||||
)
|
||||
from bleak.backends.corebluetooth.utils import (
|
||||
cb_uuid_to_str,
|
||||
to_optional_int,
|
||||
to_optional_str,
|
||||
)
|
||||
from bleak.backends.scanner import (
|
||||
AdvertisementData,
|
||||
AdvertisementDataCallback,
|
||||
BaseBleakScanner,
|
||||
)
|
||||
from bleak.exc import BleakError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
if name == "CBScannerArgs":
|
||||
warn(
|
||||
"importing CBScannerArgs from bleak.backends.corebluetooth.scanner is deprecated, use bleak.args.corebluetooth instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return _CBScannerArgs
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
class BleakScannerCoreBluetooth(BaseBleakScanner):
|
||||
"""The native macOS Bleak BLE Scanner.
|
||||
|
||||
Documentation:
|
||||
https://developer.apple.com/documentation/corebluetooth/cbcentralmanager
|
||||
|
||||
CoreBluetooth doesn't explicitly use Bluetooth addresses to identify peripheral
|
||||
devices because private devices may obscure their Bluetooth addresses. To cope
|
||||
with this, CoreBluetooth utilizes UUIDs for each peripheral. Bleak uses
|
||||
this for the BLEDevice address on macOS.
|
||||
|
||||
Args:
|
||||
detection_callback:
|
||||
Optional function that will be called each time a device is
|
||||
discovered or advertising data has changed.
|
||||
service_uuids:
|
||||
Optional list of service UUIDs to filter on. Only advertisements
|
||||
containing this advertising data will be received. Required on
|
||||
macOS >= 12.0, < 12.3 (unless you create an app with ``py2app``).
|
||||
scanning_mode:
|
||||
Set to ``"passive"`` to avoid the ``"active"`` scanning mode. Not
|
||||
supported on macOS! Will raise :class:`BleakError` if set to
|
||||
``"passive"``
|
||||
**timeout (float):
|
||||
The scanning timeout to be used, in case of missing
|
||||
``stopScan_`` method.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detection_callback: Optional[AdvertisementDataCallback],
|
||||
service_uuids: Optional[list[str]],
|
||||
scanning_mode: Literal["active", "passive"],
|
||||
*,
|
||||
cb: _CBScannerArgs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(detection_callback, service_uuids)
|
||||
|
||||
self._use_bdaddr = cb.get("use_bdaddr", False)
|
||||
|
||||
if scanning_mode == "passive":
|
||||
raise BleakError("macOS does not support passive scanning")
|
||||
|
||||
self._manager = CentralManagerDelegate()
|
||||
self._timeout: float = kwargs.get("timeout", 5.0)
|
||||
if (
|
||||
objc.macos_available(12, 0)
|
||||
and not objc.macos_available(12, 3)
|
||||
and not self._service_uuids
|
||||
):
|
||||
# See https://github.com/hbldh/bleak/issues/720
|
||||
if NSBundle.mainBundle().bundleIdentifier() == "org.python.python":
|
||||
logger.error(
|
||||
"macOS 12.0, 12.1 and 12.2 require non-empty service_uuids kwarg, otherwise no advertisement data will be received"
|
||||
)
|
||||
|
||||
@override
|
||||
async def start(self) -> None:
|
||||
await self._manager.wait_until_ready()
|
||||
|
||||
self.seen_devices = {}
|
||||
|
||||
def callback(
|
||||
peripheral: CBPeripheral, adv_data: CBAdvertisementData, rssi: NSNumber
|
||||
) -> None:
|
||||
|
||||
service_uuids = [
|
||||
cb_uuid_to_str(u) for u in adv_data.get("kCBAdvDataServiceUUIDs", [])
|
||||
]
|
||||
|
||||
if not self.is_allowed_uuid(service_uuids):
|
||||
return
|
||||
|
||||
# Process service data
|
||||
service_data = {
|
||||
cb_uuid_to_str(k): bytes(v)
|
||||
for k, v in adv_data.get("kCBAdvDataServiceData", {}).items()
|
||||
}
|
||||
|
||||
# Process manufacturer data into a more friendly format
|
||||
manufacturer_binary_data = adv_data.get("kCBAdvDataManufacturerData")
|
||||
manufacturer_data: dict[int, bytes] = {}
|
||||
if manufacturer_binary_data:
|
||||
manufacturer_id = int.from_bytes(
|
||||
manufacturer_binary_data[0:2], byteorder="little"
|
||||
)
|
||||
manufacturer_value = bytes(manufacturer_binary_data[2:])
|
||||
manufacturer_data[manufacturer_id] = manufacturer_value
|
||||
|
||||
advertisement_data = AdvertisementData(
|
||||
local_name=to_optional_str(adv_data.get("kCBAdvDataLocalName")),
|
||||
manufacturer_data=manufacturer_data,
|
||||
service_data=service_data,
|
||||
service_uuids=service_uuids,
|
||||
tx_power=to_optional_int(adv_data.get("kCBAdvDataTxPowerLevel")),
|
||||
rssi=int(rssi),
|
||||
platform_data=(peripheral, adv_data, rssi),
|
||||
)
|
||||
|
||||
if self._use_bdaddr:
|
||||
# HACK: retrieveAddressForPeripheral_ is undocumented but seems to do the trick
|
||||
address_bytes = cast(
|
||||
Optional[bytes],
|
||||
self._manager.central_manager.retrieveAddressForPeripheral_(peripheral), # type: ignore
|
||||
)
|
||||
if address_bytes is None:
|
||||
logger.debug(
|
||||
"Could not get Bluetooth address for %s. Ignoring this device.",
|
||||
peripheral.identifier().UUIDString(),
|
||||
)
|
||||
return
|
||||
address = address_bytes.hex(":").upper()
|
||||
else:
|
||||
address = peripheral.identifier().UUIDString()
|
||||
|
||||
device = self.create_or_update_device(
|
||||
peripheral.identifier().UUIDString(),
|
||||
address,
|
||||
peripheral.name(),
|
||||
(peripheral, self._manager),
|
||||
advertisement_data,
|
||||
)
|
||||
|
||||
self.call_detection_callbacks(device, advertisement_data)
|
||||
|
||||
self._manager.callbacks[id(self)] = callback
|
||||
await self._manager.start_scan(self._service_uuids)
|
||||
|
||||
@override
|
||||
async def stop(self) -> None:
|
||||
await self._manager.stop_scan()
|
||||
self._manager.callbacks.pop(id(self), None)
|
||||
@ -0,0 +1,123 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "darwin":
|
||||
assert False, "This backend is only available on macOS"
|
||||
|
||||
from typing import Any, Optional, TypeGuard, overload
|
||||
|
||||
from CoreBluetooth import (
|
||||
CBUUID,
|
||||
CBManagerState,
|
||||
CBManagerStatePoweredOff,
|
||||
CBManagerStatePoweredOn,
|
||||
CBManagerStateResetting,
|
||||
CBManagerStateUnauthorized,
|
||||
CBManagerStateUnknown,
|
||||
CBManagerStateUnsupported,
|
||||
CBUUIDCharacteristicExtendedPropertiesString,
|
||||
CBUUIDCharacteristicUserDescriptionString,
|
||||
CBUUIDClientCharacteristicConfigurationString,
|
||||
CBUUIDServerCharacteristicConfigurationString,
|
||||
)
|
||||
from Foundation import NSNumber, NSString
|
||||
|
||||
from bleak.uuids import normalize_uuid_str
|
||||
|
||||
|
||||
def cb_uuid_to_str(uuid: CBUUID) -> str:
|
||||
"""Converts a CoreBluetooth UUID to a Python string.
|
||||
|
||||
If ``uuid`` is a 16-bit UUID, it is assumed to be a Bluetooth GATT UUID
|
||||
(``0000xxxx-0000-1000-8000-00805f9b34fb``).
|
||||
|
||||
Args
|
||||
uuid: The UUID.
|
||||
|
||||
Returns:
|
||||
The UUID as a lower case Python string (``xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx``)
|
||||
"""
|
||||
return normalize_uuid_str(uuid.UUIDString())
|
||||
|
||||
|
||||
@overload
|
||||
def to_optional_str(value: NSString) -> str: ...
|
||||
@overload
|
||||
def to_optional_str(value: None) -> None: ...
|
||||
|
||||
|
||||
def to_optional_str(value: Optional[NSString]) -> Optional[str]:
|
||||
"""Converts an NSString to a Python string or None.
|
||||
|
||||
Args:
|
||||
value: The NSString or None.
|
||||
|
||||
Returns:
|
||||
The Python string or None.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return str(value)
|
||||
|
||||
|
||||
@overload
|
||||
def to_optional_int(value: NSNumber) -> int: ...
|
||||
@overload
|
||||
def to_optional_int(value: None) -> None: ...
|
||||
|
||||
|
||||
def to_optional_int(value: Optional[NSNumber]) -> Optional[int]:
|
||||
"""Converts an NSNumber to a Python int or None.
|
||||
|
||||
Args:
|
||||
value: The NSNumber or None.
|
||||
|
||||
Returns:
|
||||
The Python int or None.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return int(value)
|
||||
|
||||
|
||||
# Most descriptors are returned as NSData (raw bytes), but some of them
|
||||
# are returned as NSNumber or NSString.
|
||||
# See: https://developer.apple.com/documentation/corebluetooth/characteristic-descriptors
|
||||
_DESCRIPTOR_TYPE_NSNUMBER = (
|
||||
normalize_uuid_str(CBUUIDCharacteristicExtendedPropertiesString), # 0x2900
|
||||
normalize_uuid_str(CBUUIDClientCharacteristicConfigurationString), # 0x2902
|
||||
normalize_uuid_str(CBUUIDServerCharacteristicConfigurationString), # 0x2903
|
||||
)
|
||||
_DESCRIPTOR_TYPE_NSSTRING = (
|
||||
normalize_uuid_str(CBUUIDCharacteristicUserDescriptionString), # 0x2901
|
||||
)
|
||||
|
||||
|
||||
def is_descriptor_nsnumber(value: Any, descriptor_uuid: str) -> TypeGuard[NSNumber]:
|
||||
"""Check if descriptor value is returned as NSNumber by CoreBluetooth."""
|
||||
return descriptor_uuid in _DESCRIPTOR_TYPE_NSNUMBER
|
||||
|
||||
|
||||
def is_descriptor_nsstring(value: Any, descriptor_uuid: str) -> TypeGuard[NSString]:
|
||||
"""Check if descriptor value is returned as NSString by CoreBluetooth."""
|
||||
return descriptor_uuid in _DESCRIPTOR_TYPE_NSSTRING
|
||||
|
||||
|
||||
_CB_MANAGER_STATE_MESSAGE = {
|
||||
CBManagerStateUnknown: "Bluetooth in unknown state",
|
||||
CBManagerStateResetting: "Bluetooth is resetting",
|
||||
CBManagerStateUnsupported: "Bluetooth is unsupported",
|
||||
CBManagerStateUnauthorized: "Bluetooth is unauthorized",
|
||||
CBManagerStatePoweredOff: "Bluetooth powered off",
|
||||
CBManagerStatePoweredOn: "Bluetooth powered on",
|
||||
}
|
||||
|
||||
|
||||
def cb_manager_state_message(state: CBManagerState) -> str:
|
||||
"""Get log message for CoreBluetooth manager state."""
|
||||
return _CB_MANAGER_STATE_MESSAGE.get(
|
||||
state, f"Unknown CBManagerState value: {state}"
|
||||
)
|
||||
@ -0,0 +1,154 @@
|
||||
# Created on 2019-03-19 by hbldh <henrik.blidh@nedomkull.com>
|
||||
"""
|
||||
Interface class for the Bleak representation of a GATT Descriptor
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from bleak.uuids import normalize_uuid_16
|
||||
|
||||
# avoid circular import
|
||||
if TYPE_CHECKING:
|
||||
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||
|
||||
|
||||
_descriptor_descriptions = {
|
||||
normalize_uuid_16(0x2905): [
|
||||
"Characteristic Aggregate Format",
|
||||
"org.bluetooth.descriptor.gatt.characteristic_aggregate_format",
|
||||
"0x2905",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x2900): [
|
||||
"Characteristic Extended Properties",
|
||||
"org.bluetooth.descriptor.gatt.characteristic_extended_properties",
|
||||
"0x2900",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x2904): [
|
||||
"Characteristic Presentation Format",
|
||||
"org.bluetooth.descriptor.gatt.characteristic_presentation_format",
|
||||
"0x2904",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x2901): [
|
||||
"Characteristic User Description",
|
||||
"org.bluetooth.descriptor.gatt.characteristic_user_description",
|
||||
"0x2901",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x2902): [
|
||||
"Client Characteristic Configuration",
|
||||
"org.bluetooth.descriptor.gatt.client_characteristic_configuration",
|
||||
"0x2902",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x290B): [
|
||||
"Environmental Sensing Configuration",
|
||||
"org.bluetooth.descriptor.es_configuration",
|
||||
"0x290B",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x290C): [
|
||||
"Environmental Sensing Measurement",
|
||||
"org.bluetooth.descriptor.es_measurement",
|
||||
"0x290C",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x290D): [
|
||||
"Environmental Sensing Trigger Setting",
|
||||
"org.bluetooth.descriptor.es_trigger_setting",
|
||||
"0x290D",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x2907): [
|
||||
"External Report Reference",
|
||||
"org.bluetooth.descriptor.external_report_reference",
|
||||
"0x2907",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x2909): [
|
||||
"Number of Digitals",
|
||||
"org.bluetooth.descriptor.number_of_digitals",
|
||||
"0x2909",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x2908): [
|
||||
"Report Reference",
|
||||
"org.bluetooth.descriptor.report_reference",
|
||||
"0x2908",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x2903): [
|
||||
"Server Characteristic Configuration",
|
||||
"org.bluetooth.descriptor.gatt.server_characteristic_configuration",
|
||||
"0x2903",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x290E): [
|
||||
"Time Trigger Setting",
|
||||
"org.bluetooth.descriptor.time_trigger_setting",
|
||||
"0x290E",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x2906): [
|
||||
"Valid Range",
|
||||
"org.bluetooth.descriptor.valid_range",
|
||||
"0x2906",
|
||||
"GSS",
|
||||
],
|
||||
normalize_uuid_16(0x290A): [
|
||||
"Value Trigger Setting",
|
||||
"org.bluetooth.descriptor.value_trigger_setting",
|
||||
"0x290A",
|
||||
"GSS",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class BleakGATTDescriptor:
|
||||
"""The Bleak representation of a GATT Descriptor"""
|
||||
|
||||
def __init__(
|
||||
self, obj: Any, handle: int, uuid: str, characteristic: BleakGATTCharacteristic
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
obj: The backend-specific object for the descriptor.
|
||||
handle: The handle of the descriptor.
|
||||
uuid: The UUID of the descriptor.
|
||||
characteristic: The characteristic that this descriptor belongs to.
|
||||
"""
|
||||
self.obj = obj
|
||||
self._handle = handle
|
||||
self._uuid = uuid
|
||||
self._characteristic = characteristic
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.uuid} (Handle: {self.handle}): {self.description}"
|
||||
|
||||
@property
|
||||
def characteristic_uuid(self) -> str:
|
||||
"""UUID for the characteristic that this descriptor belongs to"""
|
||||
return self._characteristic.uuid
|
||||
|
||||
@property
|
||||
def characteristic_handle(self) -> int:
|
||||
"""handle for the characteristic that this descriptor belongs to"""
|
||||
return self._characteristic.handle
|
||||
|
||||
@property
|
||||
def uuid(self) -> str:
|
||||
"""UUID for this descriptor"""
|
||||
return self._uuid
|
||||
|
||||
@property
|
||||
def handle(self) -> int:
|
||||
"""Integer handle for this descriptor"""
|
||||
return self._handle
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""A text description of what this descriptor represents"""
|
||||
return _descriptor_descriptions.get(self.uuid, ["Unknown"])[0]
|
||||
@ -0,0 +1,39 @@
|
||||
# Created on 2018-04-23 by hbldh <henrik.blidh@nedomkull.com>
|
||||
"""
|
||||
Wrapper class for Bluetooth LE servers returned from calling
|
||||
:py:meth:`bleak.discover`.
|
||||
"""
|
||||
|
||||
|
||||
from typing import Any, Optional
|
||||
from warnings import warn
|
||||
|
||||
|
||||
class BLEDevice:
|
||||
"""
|
||||
A simple wrapper class representing a BLE server detected during scanning.
|
||||
"""
|
||||
|
||||
__slots__ = ("address", "name", "details")
|
||||
|
||||
def __init__(self, address: str, name: Optional[str], details: Any, **kwargs: Any):
|
||||
#: The Bluetooth address of the device on this machine (UUID on macOS).
|
||||
self.address = address
|
||||
#: The operating system name of the device (not necessarily the local name
|
||||
#: from the advertising data), suitable for display to the user.
|
||||
self.name = name
|
||||
#: The OS native details required for connecting to the device.
|
||||
self.details = details
|
||||
|
||||
if kwargs:
|
||||
warn(
|
||||
"Passing additional arguments for BLEDevice is deprecated and has no effect.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.address}: {self.name}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"BLEDevice({self.address}, {self.name})"
|
||||
@ -0,0 +1,528 @@
|
||||
"""
|
||||
BLE Client for python-for-android
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "android":
|
||||
assert False, "This backend is only available on Android"
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import uuid
|
||||
import warnings
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from android.broadcast import BroadcastReceiver
|
||||
from jnius import java_method
|
||||
|
||||
from bleak._compat import override
|
||||
from bleak.assigned_numbers import gatt_char_props_to_strs
|
||||
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||
from bleak.backends.client import BaseBleakClient, NotifyCallback
|
||||
from bleak.backends.descriptor import BleakGATTDescriptor
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.p4android import defs, utils
|
||||
from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection
|
||||
from bleak.exc import BleakError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BleakClientP4Android(BaseBleakClient):
|
||||
"""A python-for-android Bleak Client
|
||||
|
||||
Args:
|
||||
address_or_ble_device:
|
||||
The Bluetooth address of the BLE peripheral to connect to or the
|
||||
:class:`BLEDevice` object representing it.
|
||||
services:
|
||||
Optional set of services UUIDs to filter.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
address_or_ble_device: Union[BLEDevice, str],
|
||||
services: Optional[set[uuid.UUID]],
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(address_or_ble_device, **kwargs)
|
||||
self._requested_services = (
|
||||
set(map(defs.UUID.fromString, services)) if services else None
|
||||
)
|
||||
self.__gatt = None
|
||||
self.__mtu = 23
|
||||
|
||||
self.__callbacks = None
|
||||
|
||||
# Connectivity methods
|
||||
|
||||
@override
|
||||
async def connect(self, pair: bool, **kwargs) -> None:
|
||||
"""Connect to the specified GATT server."""
|
||||
if pair:
|
||||
logger.warning("Pairing during connect is not implemented on Android")
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
adapter = defs.BluetoothAdapter.getDefaultAdapter()
|
||||
if adapter is None:
|
||||
raise BleakError("Bluetooth is not supported on this hardware platform")
|
||||
if adapter.getState() != defs.BluetoothAdapter.STATE_ON:
|
||||
raise BleakError("Bluetooth is not turned on")
|
||||
|
||||
self.__device = adapter.getRemoteDevice(self.address)
|
||||
|
||||
self.__callbacks = _PythonBluetoothGattCallback(self, loop)
|
||||
|
||||
self._subscriptions = {}
|
||||
|
||||
logger.debug(f"Connecting to BLE device @ {self.address}")
|
||||
|
||||
(self.__gatt,) = await self.__callbacks.perform_and_wait(
|
||||
dispatchApi=self.__device.connectGatt,
|
||||
dispatchParams=(
|
||||
defs.context,
|
||||
False,
|
||||
self.__callbacks.java,
|
||||
defs.BluetoothDevice.TRANSPORT_LE,
|
||||
),
|
||||
resultApi="onConnectionStateChange",
|
||||
resultExpected=(defs.BluetoothProfile.STATE_CONNECTED,),
|
||||
return_indicates_status=False,
|
||||
)
|
||||
|
||||
try:
|
||||
logger.debug("Connection successful.")
|
||||
|
||||
# unlike other backends, Android doesn't automatically negotiate
|
||||
# the MTU, so we request the largest size possible like BlueZ
|
||||
logger.debug("requesting mtu...")
|
||||
(self.__mtu,) = await self.__callbacks.perform_and_wait(
|
||||
dispatchApi=self.__gatt.requestMtu,
|
||||
dispatchParams=(517,),
|
||||
resultApi="onMtuChanged",
|
||||
)
|
||||
|
||||
logger.debug("discovering services...")
|
||||
await self.__callbacks.perform_and_wait(
|
||||
dispatchApi=self.__gatt.discoverServices,
|
||||
dispatchParams=(),
|
||||
resultApi="onServicesDiscovered",
|
||||
)
|
||||
|
||||
await self._get_services()
|
||||
except BaseException:
|
||||
# if connecting is canceled or one of the above fails, we need to
|
||||
# disconnect
|
||||
try:
|
||||
await self.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
@override
|
||||
async def disconnect(self) -> None:
|
||||
"""Disconnect from the specified GATT server."""
|
||||
logger.debug("Disconnecting from BLE device...")
|
||||
if self.__gatt is None:
|
||||
# No connection exists. Either one hasn't been created or
|
||||
# we have already called disconnect and closed the gatt
|
||||
# connection.
|
||||
logger.debug("already disconnected")
|
||||
return
|
||||
|
||||
# Try to disconnect the actual device/peripheral
|
||||
try:
|
||||
await self.__callbacks.perform_and_wait(
|
||||
dispatchApi=self.__gatt.disconnect,
|
||||
dispatchParams=(),
|
||||
resultApi="onConnectionStateChange",
|
||||
resultExpected=(defs.BluetoothProfile.STATE_DISCONNECTED,),
|
||||
unless_already=True,
|
||||
return_indicates_status=False,
|
||||
)
|
||||
self.__gatt.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Attempt to disconnect device failed: {e}")
|
||||
|
||||
self.__gatt = None
|
||||
self.__callbacks = None
|
||||
|
||||
# Reset all stored services.
|
||||
self.services = None
|
||||
|
||||
@override
|
||||
async def pair(self, *args, **kwargs) -> None:
|
||||
"""Pair with the peripheral.
|
||||
|
||||
You can use ConnectDevice method if you already know the MAC address of the device.
|
||||
Else you need to StartDiscovery, Trust, Pair and Connect in sequence.
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
bondedFuture = loop.create_future()
|
||||
|
||||
def handleBondStateChanged(context, intent):
|
||||
bond_state = intent.getIntExtra(defs.BluetoothDevice.EXTRA_BOND_STATE, -1)
|
||||
if bond_state == -1:
|
||||
loop.call_soon_threadsafe(
|
||||
bondedFuture.set_exception,
|
||||
BleakError(f"Unexpected bond state {bond_state}"),
|
||||
)
|
||||
elif bond_state == defs.BluetoothDevice.BOND_NONE:
|
||||
loop.call_soon_threadsafe(
|
||||
bondedFuture.set_exception,
|
||||
BleakError(
|
||||
f"Device with address {self.address} could not be paired with."
|
||||
),
|
||||
)
|
||||
elif bond_state == defs.BluetoothDevice.BOND_BONDED:
|
||||
loop.call_soon_threadsafe(bondedFuture.set_result, True)
|
||||
|
||||
receiver = BroadcastReceiver(
|
||||
handleBondStateChanged,
|
||||
actions=[defs.BluetoothDevice.ACTION_BOND_STATE_CHANGED],
|
||||
)
|
||||
receiver.start()
|
||||
try:
|
||||
# See if it is already paired.
|
||||
bond_state = self.__device.getBondState()
|
||||
if bond_state == defs.BluetoothDevice.BOND_BONDED:
|
||||
return
|
||||
elif bond_state == defs.BluetoothDevice.BOND_NONE:
|
||||
logger.debug(f"Pairing to BLE device @ {self.address}")
|
||||
if not self.__device.createBond():
|
||||
raise BleakError(
|
||||
f"Could not initiate bonding with device @ {self.address}"
|
||||
)
|
||||
await bondedFuture
|
||||
finally:
|
||||
await receiver.stop()
|
||||
|
||||
@override
|
||||
async def unpair(self) -> None:
|
||||
"""Unpair with the peripheral."""
|
||||
warnings.warn(
|
||||
"Unpairing is seemingly unavailable in the Android API at the moment."
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_connected(self) -> bool:
|
||||
"""Check connection status between this client and the server.
|
||||
|
||||
Returns:
|
||||
Boolean representing connection status.
|
||||
|
||||
"""
|
||||
return (
|
||||
self.__callbacks is not None
|
||||
and self.__callbacks.states["onConnectionStateChange"][1]
|
||||
== defs.BluetoothProfile.STATE_CONNECTED
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def mtu_size(self) -> int:
|
||||
return self.__mtu
|
||||
|
||||
# GATT services methods
|
||||
|
||||
async def _get_services(self) -> BleakGATTServiceCollection:
|
||||
"""Get all services registered for this GATT server.
|
||||
|
||||
Returns:
|
||||
A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree.
|
||||
|
||||
"""
|
||||
if self.services is not None:
|
||||
return self.services
|
||||
|
||||
services = BleakGATTServiceCollection()
|
||||
|
||||
logger.debug("Get Services...")
|
||||
for java_service in self.__gatt.getServices():
|
||||
if (
|
||||
self._requested_services is not None
|
||||
and java_service.getUuid() not in self._requested_services
|
||||
):
|
||||
continue
|
||||
|
||||
service = BleakGATTService(
|
||||
java_service,
|
||||
java_service.getInstanceId(),
|
||||
java_service.getUuid().toString(),
|
||||
)
|
||||
services.add_service(service)
|
||||
|
||||
for java_characteristic in java_service.getCharacteristics():
|
||||
|
||||
characteristic = BleakGATTCharacteristic(
|
||||
java_characteristic,
|
||||
java_characteristic.getInstanceId(),
|
||||
java_characteristic.getUuid().toString(),
|
||||
gatt_char_props_to_strs(java_characteristic.getProperties()),
|
||||
lambda: self.__mtu - 3,
|
||||
service,
|
||||
)
|
||||
services.add_characteristic(characteristic)
|
||||
|
||||
for descriptor_index, java_descriptor in enumerate(
|
||||
java_characteristic.getDescriptors()
|
||||
):
|
||||
|
||||
descriptor = BleakGATTDescriptor(
|
||||
java_descriptor,
|
||||
characteristic.handle + 1 + descriptor_index,
|
||||
java_descriptor.getUuid().toString(),
|
||||
characteristic,
|
||||
)
|
||||
services.add_descriptor(descriptor)
|
||||
|
||||
self.services = services
|
||||
return self.services
|
||||
|
||||
# IO methods
|
||||
|
||||
@override
|
||||
async def read_gatt_char(
|
||||
self,
|
||||
characteristic: BleakGATTCharacteristic,
|
||||
*,
|
||||
use_cached: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> bytearray:
|
||||
"""Perform read operation on the specified GATT characteristic.
|
||||
|
||||
Args:
|
||||
characteristic (BleakGATTCharacteristic): The characteristic to read from.
|
||||
|
||||
Returns:
|
||||
(bytearray) The read data.
|
||||
|
||||
"""
|
||||
if use_cached:
|
||||
logger.debug(
|
||||
"Reading cached characteristic values is not implemented on Android"
|
||||
)
|
||||
|
||||
(value,) = await self.__callbacks.perform_and_wait(
|
||||
dispatchApi=self.__gatt.readCharacteristic,
|
||||
dispatchParams=(characteristic.obj,),
|
||||
resultApi=("onCharacteristicRead", characteristic.handle),
|
||||
)
|
||||
value = bytearray(value)
|
||||
logger.debug(
|
||||
f"Read Characteristic {characteristic.uuid} | {characteristic.handle}: {value}"
|
||||
)
|
||||
return value
|
||||
|
||||
@override
|
||||
async def read_gatt_descriptor(
|
||||
self,
|
||||
descriptor: BleakGATTDescriptor,
|
||||
*,
|
||||
use_cached: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> bytearray:
|
||||
"""Perform read operation on the specified GATT descriptor.
|
||||
|
||||
Args:
|
||||
descriptor: The descriptor to read from.
|
||||
use_cached: Whether to use cached value.
|
||||
|
||||
Returns:
|
||||
The read data.
|
||||
"""
|
||||
if use_cached:
|
||||
logger.debug(
|
||||
"Reading cached descriptor values is not implemented on Android"
|
||||
)
|
||||
|
||||
(value,) = await self.__callbacks.perform_and_wait(
|
||||
dispatchApi=self.__gatt.readDescriptor,
|
||||
dispatchParams=(descriptor.obj,),
|
||||
resultApi=("onDescriptorRead", descriptor.uuid),
|
||||
)
|
||||
value = bytearray(value)
|
||||
|
||||
logger.debug(
|
||||
f"Read Descriptor {descriptor.uuid} | {descriptor.handle}: {value}"
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
@override
|
||||
async def write_gatt_char(
|
||||
self, characteristic: BleakGATTCharacteristic, data: bytearray, response: bool
|
||||
) -> None:
|
||||
if response:
|
||||
characteristic.obj.setWriteType(
|
||||
defs.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
||||
)
|
||||
else:
|
||||
characteristic.obj.setWriteType(
|
||||
defs.BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
||||
)
|
||||
|
||||
characteristic.obj.setValue(data)
|
||||
|
||||
await self.__callbacks.perform_and_wait(
|
||||
dispatchApi=self.__gatt.writeCharacteristic,
|
||||
dispatchParams=(characteristic.obj,),
|
||||
resultApi=("onCharacteristicWrite", characteristic.handle),
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Write Characteristic {characteristic.uuid} | {characteristic.handle}: {data}"
|
||||
)
|
||||
|
||||
@override
|
||||
async def write_gatt_descriptor(
|
||||
self,
|
||||
desc_specifier: Union[BleakGATTDescriptor, str, uuid.UUID],
|
||||
data: bytearray,
|
||||
) -> None:
|
||||
"""Perform a write operation on the specified GATT descriptor.
|
||||
|
||||
Args:
|
||||
desc_specifier (BleakGATTDescriptor, str or UUID): The descriptor to write
|
||||
to, specified by either UUID or directly by the
|
||||
BleakGATTDescriptor object representing it.
|
||||
data (bytes or bytearray): The data to send.
|
||||
|
||||
"""
|
||||
if not isinstance(desc_specifier, BleakGATTDescriptor):
|
||||
descriptor = self.services.get_descriptor(desc_specifier)
|
||||
else:
|
||||
descriptor = desc_specifier
|
||||
|
||||
if not descriptor:
|
||||
raise BleakError(f"Descriptor {desc_specifier} was not found!")
|
||||
|
||||
descriptor.obj.setValue(data)
|
||||
|
||||
await self.__callbacks.perform_and_wait(
|
||||
dispatchApi=self.__gatt.writeDescriptor,
|
||||
dispatchParams=(descriptor.obj,),
|
||||
resultApi=("onDescriptorWrite", descriptor.uuid),
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Write Descriptor {descriptor.uuid} | {descriptor.handle}: {data}"
|
||||
)
|
||||
|
||||
@override
|
||||
async def start_notify(
|
||||
self,
|
||||
characteristic: BleakGATTCharacteristic,
|
||||
callback: NotifyCallback,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""
|
||||
Activate notifications/indications on a characteristic.
|
||||
"""
|
||||
self._subscriptions[characteristic.handle] = callback
|
||||
|
||||
assert self.__gatt is not None
|
||||
|
||||
if not self.__gatt.setCharacteristicNotification(characteristic.obj, True):
|
||||
raise BleakError(
|
||||
f"Failed to enable notification for characteristic {characteristic.uuid}"
|
||||
)
|
||||
|
||||
await self.write_gatt_descriptor(
|
||||
characteristic.get_descriptor(
|
||||
defs.CLIENT_CHARACTERISTIC_CONFIGURATION_UUID
|
||||
),
|
||||
defs.BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE,
|
||||
)
|
||||
|
||||
@override
|
||||
async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None:
|
||||
"""Deactivate notification/indication on a specified characteristic.
|
||||
|
||||
Args:
|
||||
characteristic (BleakGATTCharacteristic): The characteristic to deactivate
|
||||
notification/indication on,.
|
||||
|
||||
"""
|
||||
await self.write_gatt_descriptor(
|
||||
characteristic.get_descriptor(
|
||||
defs.CLIENT_CHARACTERISTIC_CONFIGURATION_UUID
|
||||
),
|
||||
defs.BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE,
|
||||
)
|
||||
|
||||
if not self.__gatt.setCharacteristicNotification(characteristic.obj, False):
|
||||
raise BleakError(
|
||||
f"Failed to disable notification for characteristic {characteristic.uuid}"
|
||||
)
|
||||
del self._subscriptions[characteristic.handle]
|
||||
|
||||
|
||||
class _PythonBluetoothGattCallback(utils.AsyncJavaCallbacks):
|
||||
__javainterfaces__ = [
|
||||
"com.github.hbldh.bleak.PythonBluetoothGattCallback$Interface"
|
||||
]
|
||||
|
||||
def __init__(self, client, loop):
|
||||
super().__init__(loop)
|
||||
self._client = client
|
||||
self.java = defs.PythonBluetoothGattCallback(self)
|
||||
|
||||
def result_state(self, status, resultApi, *data):
|
||||
if status == defs.BluetoothGatt.GATT_SUCCESS:
|
||||
failure_str = None
|
||||
else:
|
||||
failure_str = defs.GATT_STATUS_STRINGS.get(status, status)
|
||||
self._loop.call_soon_threadsafe(
|
||||
self._result_state_unthreadsafe, failure_str, resultApi, data
|
||||
)
|
||||
|
||||
@java_method("(II)V")
|
||||
def onConnectionStateChange(self, status, new_state):
|
||||
try:
|
||||
self.result_state(status, "onConnectionStateChange", new_state)
|
||||
except BleakError:
|
||||
pass
|
||||
if (
|
||||
new_state == defs.BluetoothProfile.STATE_DISCONNECTED
|
||||
and self._client._disconnected_callback is not None
|
||||
):
|
||||
self._client._disconnected_callback()
|
||||
|
||||
@java_method("(II)V")
|
||||
def onMtuChanged(self, mtu, status):
|
||||
self.result_state(status, "onMtuChanged", mtu)
|
||||
|
||||
@java_method("(I)V")
|
||||
def onServicesDiscovered(self, status):
|
||||
self.result_state(status, "onServicesDiscovered")
|
||||
|
||||
@java_method("(I[B)V")
|
||||
def onCharacteristicChanged(self, handle, value):
|
||||
self._loop.call_soon_threadsafe(
|
||||
self._client._subscriptions[handle], bytearray(value.tolist())
|
||||
)
|
||||
|
||||
@java_method("(II[B)V")
|
||||
def onCharacteristicRead(self, handle, status, value):
|
||||
self.result_state(
|
||||
status, ("onCharacteristicRead", handle), bytes(value.tolist())
|
||||
)
|
||||
|
||||
@java_method("(II)V")
|
||||
def onCharacteristicWrite(self, handle, status):
|
||||
self.result_state(status, ("onCharacteristicWrite", handle))
|
||||
|
||||
@java_method("(Ljava/lang/String;I[B)V")
|
||||
def onDescriptorRead(self, uuid, status, value):
|
||||
self.result_state(status, ("onDescriptorRead", uuid), bytes(value.tolist()))
|
||||
|
||||
@java_method("(Ljava/lang/String;I)V")
|
||||
def onDescriptorWrite(self, uuid, status):
|
||||
self.result_state(status, ("onDescriptorWrite", uuid))
|
||||
@ -0,0 +1,85 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "android":
|
||||
assert False, "This backend is only available on Android"
|
||||
|
||||
import enum
|
||||
|
||||
from jnius import autoclass, cast
|
||||
|
||||
import bleak.exc
|
||||
from bleak.uuids import normalize_uuid_16
|
||||
|
||||
# caching constants avoids unnecessary extra use of the jni-python interface, which can be slow
|
||||
|
||||
List = autoclass("java.util.ArrayList")
|
||||
UUID = autoclass("java.util.UUID")
|
||||
BluetoothAdapter = autoclass("android.bluetooth.BluetoothAdapter")
|
||||
ScanCallback = autoclass("android.bluetooth.le.ScanCallback")
|
||||
ScanFilter = autoclass("android.bluetooth.le.ScanFilter")
|
||||
ScanFilterBuilder = autoclass("android.bluetooth.le.ScanFilter$Builder")
|
||||
ScanSettings = autoclass("android.bluetooth.le.ScanSettings")
|
||||
ScanSettingsBuilder = autoclass("android.bluetooth.le.ScanSettings$Builder")
|
||||
BluetoothDevice = autoclass("android.bluetooth.BluetoothDevice")
|
||||
BluetoothGatt = autoclass("android.bluetooth.BluetoothGatt")
|
||||
BluetoothGattCharacteristic = autoclass("android.bluetooth.BluetoothGattCharacteristic")
|
||||
BluetoothGattDescriptor = autoclass("android.bluetooth.BluetoothGattDescriptor")
|
||||
BluetoothProfile = autoclass("android.bluetooth.BluetoothProfile")
|
||||
|
||||
PythonActivity = autoclass("org.kivy.android.PythonActivity")
|
||||
ParcelUuid = autoclass("android.os.ParcelUuid")
|
||||
activity = cast("android.app.Activity", PythonActivity.mActivity)
|
||||
context = cast("android.content.Context", activity.getApplicationContext())
|
||||
|
||||
ScanResult = autoclass("android.bluetooth.le.ScanResult")
|
||||
|
||||
BLEAK_JNI_NAMESPACE = "com.github.hbldh.bleak"
|
||||
PythonScanCallback = autoclass(BLEAK_JNI_NAMESPACE + ".PythonScanCallback")
|
||||
PythonBluetoothGattCallback = autoclass(
|
||||
BLEAK_JNI_NAMESPACE + ".PythonBluetoothGattCallback"
|
||||
)
|
||||
|
||||
|
||||
class ScanFailed(enum.IntEnum):
|
||||
ALREADY_STARTED = ScanCallback.SCAN_FAILED_ALREADY_STARTED
|
||||
APPLICATION_REGISTRATION_FAILED = (
|
||||
ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED
|
||||
)
|
||||
FEATURE_UNSUPPORTED = ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED
|
||||
INTERNAL_ERROR = ScanCallback.SCAN_FAILED_INTERNAL_ERROR
|
||||
|
||||
|
||||
GATT_SUCCESS = 0x0000
|
||||
# TODO: we may need different lookups, e.g. one for bleak.exc.CONTROLLER_ERROR_CODES
|
||||
GATT_STATUS_STRINGS = {
|
||||
# https://developer.android.com/reference/android/bluetooth/BluetoothGatt
|
||||
# https://android.googlesource.com/platform/external/bluetooth/bluedroid/+/5738f83aeb59361a0a2eda2460113f6dc9194271/stack/include/gatt_api.h
|
||||
# https://android.googlesource.com/platform/system/bt/+/master/stack/include/gatt_api.h
|
||||
# https://www.bluetooth.com/specifications/bluetooth-core-specification/
|
||||
**bleak.exc.PROTOCOL_ERROR_CODES,
|
||||
0x007F: "Too Short",
|
||||
0x0080: "No Resources",
|
||||
0x0081: "Internal Error",
|
||||
0x0082: "Wrong State",
|
||||
0x0083: "DB Full",
|
||||
0x0084: "Busy",
|
||||
0x0085: "Error",
|
||||
0x0086: "Command Started",
|
||||
0x0087: "Illegal Parameter",
|
||||
0x0088: "Pending",
|
||||
0x0089: "Auth Failure",
|
||||
0x008A: "More",
|
||||
0x008B: "Invalid Configuration",
|
||||
0x008C: "Service Started",
|
||||
0x008D: "Encrypted No MITM",
|
||||
0x008E: "Not Encrypted",
|
||||
0x008F: "Congested",
|
||||
0x0090: "Duplicate Reg",
|
||||
0x0091: "Already Open",
|
||||
0x0092: "Cancel",
|
||||
0x0101: "Failure",
|
||||
}
|
||||
|
||||
CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = normalize_uuid_16(0x2902)
|
||||
@ -0,0 +1,84 @@
|
||||
package com.github.hbldh.bleak;
|
||||
|
||||
import java.net.ConnectException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCallback;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattDescriptor;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
|
||||
|
||||
public final class PythonBluetoothGattCallback extends BluetoothGattCallback
|
||||
{
|
||||
public interface Interface
|
||||
{
|
||||
public void onConnectionStateChange(int status, int newState);
|
||||
public void onMtuChanged(int mtu, int status);
|
||||
public void onServicesDiscovered(int status);
|
||||
public void onCharacteristicChanged(int handle, byte[] value);
|
||||
public void onCharacteristicRead(int handle, int status, byte[] value);
|
||||
public void onCharacteristicWrite(int handle, int status);
|
||||
public void onDescriptorRead(String uuid, int status, byte[] value);
|
||||
public void onDescriptorWrite(String uuid, int status);
|
||||
}
|
||||
private Interface callback;
|
||||
|
||||
public PythonBluetoothGattCallback(Interface pythonCallback)
|
||||
{
|
||||
callback = pythonCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
|
||||
{
|
||||
callback.onConnectionStateChange(status, newState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status)
|
||||
{
|
||||
callback.onMtuChanged(mtu, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered(BluetoothGatt gatt, int status)
|
||||
{
|
||||
callback.onServicesDiscovered(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)
|
||||
{
|
||||
callback.onCharacteristicRead(characteristic.getInstanceId(), status, characteristic.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)
|
||||
{
|
||||
callback.onCharacteristicWrite(characteristic.getInstanceId(), status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)
|
||||
{
|
||||
callback.onCharacteristicChanged(characteristic.getInstanceId(), characteristic.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status)
|
||||
{
|
||||
callback.onDescriptorRead(descriptor.getUuid().toString(), status, descriptor.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status)
|
||||
{
|
||||
callback.onDescriptorWrite(descriptor.getUuid().toString(), status);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package com.github.hbldh.bleak;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.bluetooth.le.ScanCallback;
|
||||
import android.bluetooth.le.ScanResult;
|
||||
|
||||
public final class PythonScanCallback extends ScanCallback
|
||||
{
|
||||
public interface Interface
|
||||
{
|
||||
public void onScanFailed(int code);
|
||||
public void onScanResult(ScanResult result);
|
||||
}
|
||||
private Interface callback;
|
||||
|
||||
public PythonScanCallback(Interface pythonCallback)
|
||||
{
|
||||
callback = pythonCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBatchScanResults(List<ScanResult> results)
|
||||
{
|
||||
for (ScanResult result : results) {
|
||||
callback.onScanResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanFailed(int errorCode)
|
||||
{
|
||||
callback.onScanFailed(errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanResult(int callbackType, ScanResult result)
|
||||
{
|
||||
callback.onScanResult(result);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import os
|
||||
from os.path import join
|
||||
|
||||
import sh
|
||||
from pythonforandroid.recipe import PythonRecipe
|
||||
from pythonforandroid.toolchain import info, shprint
|
||||
|
||||
|
||||
class BleakRecipe(PythonRecipe):
|
||||
version = None # Must be none for p4a to correctly clone repo
|
||||
fix_setup_py_version = "bleak develop branch"
|
||||
url = "git+https://github.com/hbldh/bleak.git"
|
||||
name = "bleak"
|
||||
|
||||
depends = ["pyjnius"]
|
||||
call_hostpython_via_targetpython = False
|
||||
|
||||
fix_setup_filename = "fix_setup.py"
|
||||
|
||||
def prepare_build_dir(self, arch):
|
||||
super().prepare_build_dir(arch) # Unpack the url file to the get_build_dir
|
||||
build_dir = self.get_build_dir(arch)
|
||||
|
||||
setup_py_path = join(build_dir, "setup.py")
|
||||
if not os.path.exists(setup_py_path):
|
||||
# Perform the p4a temporary fix
|
||||
# At the moment, p4a recipe installing requires setup.py to be present
|
||||
# So, we create a setup.py file only for android
|
||||
|
||||
fix_setup_py_path = join(self.get_recipe_dir(), self.fix_setup_filename)
|
||||
with open(fix_setup_py_path, "r") as f:
|
||||
contents = f.read()
|
||||
|
||||
# Write to the correct location and fill in the version number
|
||||
with open(setup_py_path, "w") as f:
|
||||
f.write(contents.replace("[VERSION]", self.fix_setup_py_version))
|
||||
else:
|
||||
info("setup.py found in bleak directory, are you installing older version?")
|
||||
|
||||
def get_recipe_env(self, arch=None, with_flags_in_cc=True):
|
||||
env = super().get_recipe_env(arch, with_flags_in_cc)
|
||||
# to find jnius and identify p4a
|
||||
env["PYJNIUS_PACKAGES"] = self.ctx.get_site_packages_dir(arch)
|
||||
return env
|
||||
|
||||
def postbuild_arch(self, arch):
|
||||
super().postbuild_arch(arch)
|
||||
|
||||
info("Copying java files")
|
||||
dest_dir = self.ctx.javaclass_dir
|
||||
path = join(
|
||||
self.get_build_dir(arch.arch), "bleak", "backends", "p4android", "java", "."
|
||||
)
|
||||
|
||||
shprint(sh.cp, "-a", path, dest_dir)
|
||||
|
||||
|
||||
recipe = BleakRecipe()
|
||||
@ -0,0 +1,10 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
VERSION = "[VERSION]" # Version will be filled in by the bleak recipe
|
||||
NAME = "bleak"
|
||||
|
||||
setup(
|
||||
name=NAME,
|
||||
version=VERSION,
|
||||
packages=find_packages(exclude=("tests", "examples", "docs")),
|
||||
)
|
||||
@ -0,0 +1,293 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "android":
|
||||
assert False, "This backend is only available on Android"
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import warnings
|
||||
from typing import Literal, Optional
|
||||
|
||||
from android.broadcast import BroadcastReceiver
|
||||
from android.permissions import Permission, request_permissions
|
||||
from jnius import cast, java_method
|
||||
|
||||
from bleak._compat import override
|
||||
from bleak._compat import timeout as async_timeout
|
||||
from bleak.backends.p4android import defs, utils
|
||||
from bleak.backends.scanner import (
|
||||
AdvertisementData,
|
||||
AdvertisementDataCallback,
|
||||
BaseBleakScanner,
|
||||
)
|
||||
from bleak.exc import BleakError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BleakScannerP4Android(BaseBleakScanner):
|
||||
"""
|
||||
The python-for-android Bleak BLE Scanner.
|
||||
|
||||
Args:
|
||||
detection_callback:
|
||||
Optional function that will be called each time a device is
|
||||
discovered or advertising data has changed.
|
||||
service_uuids:
|
||||
Optional list of service UUIDs to filter on. Only advertisements
|
||||
containing this advertising data will be received. Specifying this
|
||||
also enables scanning while the screen is off on Android.
|
||||
scanning_mode:
|
||||
Set to ``"passive"`` to avoid the ``"active"`` scanning mode.
|
||||
"""
|
||||
|
||||
__scanner = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detection_callback: Optional[AdvertisementDataCallback],
|
||||
service_uuids: Optional[list[str]],
|
||||
scanning_mode: Literal["active", "passive"],
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(detection_callback, service_uuids)
|
||||
|
||||
if scanning_mode == "passive":
|
||||
self.__scan_mode = defs.ScanSettings.SCAN_MODE_OPPORTUNISTIC
|
||||
else:
|
||||
self.__scan_mode = defs.ScanSettings.SCAN_MODE_LOW_LATENCY
|
||||
|
||||
self.__javascanner = None
|
||||
self.__callback = None
|
||||
|
||||
@override
|
||||
async def start(self) -> None:
|
||||
if BleakScannerP4Android.__scanner is not None:
|
||||
raise BleakError("A BleakScanner is already scanning on this adapter.")
|
||||
|
||||
logger.debug("Starting BTLE scan")
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
if self.__javascanner is None:
|
||||
if self.__callback is None:
|
||||
self.__callback = _PythonScanCallback(self, loop)
|
||||
|
||||
permission_acknowledged = loop.create_future()
|
||||
|
||||
def handle_permissions(permissions, grantResults):
|
||||
if any(grantResults):
|
||||
loop.call_soon_threadsafe(
|
||||
permission_acknowledged.set_result, grantResults
|
||||
)
|
||||
else:
|
||||
loop.call_soon_threadsafe(
|
||||
permission_acknowledged.set_exception(
|
||||
BleakError("User denied access to " + str(permissions))
|
||||
)
|
||||
)
|
||||
|
||||
request_permissions(
|
||||
[
|
||||
Permission.ACCESS_FINE_LOCATION,
|
||||
Permission.ACCESS_COARSE_LOCATION,
|
||||
"android.permission.ACCESS_BACKGROUND_LOCATION",
|
||||
],
|
||||
handle_permissions,
|
||||
)
|
||||
await permission_acknowledged
|
||||
|
||||
adapter = defs.BluetoothAdapter.getDefaultAdapter()
|
||||
if adapter is None:
|
||||
raise BleakError("Bluetooth is not supported on this hardware platform")
|
||||
if adapter.getState() != defs.BluetoothAdapter.STATE_ON:
|
||||
raise BleakError("Bluetooth is not turned on")
|
||||
|
||||
self.__javascanner = adapter.getBluetoothLeScanner()
|
||||
|
||||
BleakScannerP4Android.__scanner = self
|
||||
|
||||
filters = cast("java.util.List", defs.List())
|
||||
if self._service_uuids:
|
||||
for uuid in self._service_uuids:
|
||||
filters.add(
|
||||
defs.ScanFilterBuilder()
|
||||
.setServiceUuid(defs.ParcelUuid.fromString(uuid))
|
||||
.build()
|
||||
)
|
||||
|
||||
scanfuture = self.__callback.perform_and_wait(
|
||||
dispatchApi=self.__javascanner.startScan,
|
||||
dispatchParams=(
|
||||
filters,
|
||||
defs.ScanSettingsBuilder()
|
||||
.setScanMode(self.__scan_mode)
|
||||
.setReportDelay(0)
|
||||
.setPhy(defs.ScanSettings.PHY_LE_ALL_SUPPORTED)
|
||||
.setNumOfMatches(defs.ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
|
||||
.setMatchMode(defs.ScanSettings.MATCH_MODE_AGGRESSIVE)
|
||||
.setCallbackType(defs.ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
|
||||
.build(),
|
||||
self.__callback.java,
|
||||
),
|
||||
resultApi="onScan",
|
||||
return_indicates_status=False,
|
||||
)
|
||||
self.__javascanner.flushPendingScanResults(self.__callback.java)
|
||||
|
||||
try:
|
||||
async with async_timeout(0.2):
|
||||
await scanfuture
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
pass
|
||||
except BleakError as bleakerror:
|
||||
await self.stop()
|
||||
if bleakerror.args != (
|
||||
"onScan",
|
||||
"SCAN_FAILED_APPLICATION_REGISTRATION_FAILED",
|
||||
):
|
||||
raise bleakerror
|
||||
else:
|
||||
# there might be a clearer solution to this if android source and vendor
|
||||
# documentation are reviewed for the meaning of the error
|
||||
# https://stackoverflow.com/questions/27516399/solution-for-ble-scans-scan-failed-application-registration-failed
|
||||
warnings.warn(
|
||||
"BT API gave SCAN_FAILED_APPLICATION_REGISTRATION_FAILED. Resetting adapter."
|
||||
)
|
||||
|
||||
def handlerWaitingForState(state, stateFuture):
|
||||
def handleAdapterStateChanged(context, intent):
|
||||
adapter_state = intent.getIntExtra(
|
||||
defs.BluetoothAdapter.EXTRA_STATE,
|
||||
defs.BluetoothAdapter.STATE_ERROR,
|
||||
)
|
||||
if adapter_state == defs.BluetoothAdapter.STATE_ERROR:
|
||||
loop.call_soon_threadsafe(
|
||||
stateOffFuture.set_exception,
|
||||
BleakError(f"Unexpected adapter state {adapter_state}"),
|
||||
)
|
||||
elif adapter_state == state:
|
||||
loop.call_soon_threadsafe(
|
||||
stateFuture.set_result, adapter_state
|
||||
)
|
||||
|
||||
return handleAdapterStateChanged
|
||||
|
||||
logger.info(
|
||||
"disabling bluetooth adapter to handle SCAN_FAILED_APPLICATION_REGSTRATION_FAILED ..."
|
||||
)
|
||||
stateOffFuture = loop.create_future()
|
||||
receiver = BroadcastReceiver(
|
||||
handlerWaitingForState(
|
||||
defs.BluetoothAdapter.STATE_OFF, stateOffFuture
|
||||
),
|
||||
actions=[defs.BluetoothAdapter.ACTION_STATE_CHANGED],
|
||||
)
|
||||
receiver.start()
|
||||
try:
|
||||
adapter.disable()
|
||||
await stateOffFuture
|
||||
finally:
|
||||
receiver.stop()
|
||||
|
||||
logger.info("re-enabling bluetooth adapter ...")
|
||||
stateOnFuture = loop.create_future()
|
||||
receiver = BroadcastReceiver(
|
||||
handlerWaitingForState(
|
||||
defs.BluetoothAdapter.STATE_ON, stateOnFuture
|
||||
),
|
||||
actions=[defs.BluetoothAdapter.ACTION_STATE_CHANGED],
|
||||
)
|
||||
receiver.start()
|
||||
try:
|
||||
adapter.enable()
|
||||
await stateOnFuture
|
||||
finally:
|
||||
receiver.stop()
|
||||
logger.debug("restarting scan ...")
|
||||
|
||||
return await self.start()
|
||||
|
||||
@override
|
||||
async def stop(self) -> None:
|
||||
if self.__javascanner is not None:
|
||||
logger.debug("Stopping BTLE scan")
|
||||
self.__javascanner.stopScan(self.__callback.java)
|
||||
BleakScannerP4Android.__scanner = None
|
||||
self.__javascanner = None
|
||||
else:
|
||||
logger.debug("BTLE scan already stopped")
|
||||
|
||||
def _handle_scan_result(self, result) -> None:
|
||||
native_device = result.getDevice()
|
||||
record = result.getScanRecord()
|
||||
|
||||
service_uuids = record.getServiceUuids()
|
||||
if service_uuids is not None:
|
||||
service_uuids = [service_uuid.toString() for service_uuid in service_uuids]
|
||||
|
||||
if not self.is_allowed_uuid(service_uuids):
|
||||
return
|
||||
|
||||
manufacturer_data = record.getManufacturerSpecificData()
|
||||
manufacturer_data = {
|
||||
manufacturer_data.keyAt(index): bytes(manufacturer_data.valueAt(index))
|
||||
for index in range(manufacturer_data.size())
|
||||
}
|
||||
|
||||
service_data = {
|
||||
entry.getKey().toString(): bytes(entry.getValue())
|
||||
for entry in record.getServiceData().entrySet()
|
||||
}
|
||||
tx_power = record.getTxPowerLevel()
|
||||
|
||||
# change "not present" value to None to match other backends
|
||||
if tx_power == -2147483648: # Integer#MIN_VALUE
|
||||
tx_power = None
|
||||
|
||||
advertisement = AdvertisementData(
|
||||
local_name=record.getDeviceName(),
|
||||
manufacturer_data=manufacturer_data,
|
||||
service_data=service_data,
|
||||
service_uuids=service_uuids,
|
||||
tx_power=tx_power,
|
||||
rssi=result.getRssi(),
|
||||
platform_data=(result,),
|
||||
)
|
||||
|
||||
device = self.create_or_update_device(
|
||||
native_device.getAddress(),
|
||||
native_device.getAddress(),
|
||||
native_device.getName(),
|
||||
native_device,
|
||||
advertisement,
|
||||
)
|
||||
|
||||
self.call_detection_callbacks(device, advertisement)
|
||||
|
||||
|
||||
class _PythonScanCallback(utils.AsyncJavaCallbacks):
|
||||
__javainterfaces__ = ["com.github.hbldh.bleak.PythonScanCallback$Interface"]
|
||||
|
||||
def __init__(self, scanner: BleakScannerP4Android, loop: asyncio.AbstractEventLoop):
|
||||
super().__init__(loop)
|
||||
self._scanner = scanner
|
||||
self.java = defs.PythonScanCallback(self)
|
||||
|
||||
def result_state(self, status_str, name, *data):
|
||||
self._loop.call_soon_threadsafe(
|
||||
self._result_state_unthreadsafe, status_str, name, data
|
||||
)
|
||||
|
||||
@java_method("(I)V")
|
||||
def onScanFailed(self, errorCode):
|
||||
self.result_state(defs.ScanFailed(errorCode).name, "onScan")
|
||||
|
||||
@java_method("(Landroid/bluetooth/le/ScanResult;)V")
|
||||
def onScanResult(self, result):
|
||||
self._loop.call_soon_threadsafe(self._scanner._handle_scan_result, result)
|
||||
|
||||
if "onScan" not in self.states:
|
||||
self.result_state(None, "onScan", result)
|
||||
@ -0,0 +1,99 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "android":
|
||||
assert False, "This backend is only available on Android"
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from jnius import PythonJavaClass
|
||||
|
||||
from bleak.exc import BleakError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncJavaCallbacks(PythonJavaClass):
|
||||
__javacontext__ = "app"
|
||||
|
||||
def __init__(self, loop: asyncio.AbstractEventLoop):
|
||||
self._loop = loop
|
||||
self.states = {}
|
||||
self.futures = {}
|
||||
|
||||
@staticmethod
|
||||
def _if_expected(result, expected):
|
||||
if result[: len(expected)] == expected[:]:
|
||||
return result[len(expected) :]
|
||||
else:
|
||||
return None
|
||||
|
||||
async def perform_and_wait(
|
||||
self,
|
||||
dispatchApi,
|
||||
dispatchParams,
|
||||
resultApi,
|
||||
resultExpected=(),
|
||||
unless_already=False,
|
||||
return_indicates_status=True,
|
||||
):
|
||||
result2 = None
|
||||
if unless_already:
|
||||
if resultApi in self.states:
|
||||
result2 = self._if_expected(self.states[resultApi][1:], resultExpected)
|
||||
result1 = True
|
||||
|
||||
if result2 is not None:
|
||||
logger.debug(
|
||||
f"Not waiting for android api {resultApi} because found {resultExpected}"
|
||||
)
|
||||
else:
|
||||
logger.debug(f"Waiting for android api {resultApi}")
|
||||
|
||||
state = self._loop.create_future()
|
||||
self.futures[resultApi] = state
|
||||
result1 = dispatchApi(*dispatchParams)
|
||||
if return_indicates_status and not result1:
|
||||
del self.futures[resultApi]
|
||||
raise BleakError(f"api call failed, not waiting for {resultApi}")
|
||||
data = await state
|
||||
result2 = self._if_expected(data, resultExpected)
|
||||
if result2 is None:
|
||||
raise BleakError("Expected", resultExpected, "got", data)
|
||||
|
||||
logger.debug(f"{resultApi} succeeded {result2}")
|
||||
|
||||
if return_indicates_status:
|
||||
return result2
|
||||
else:
|
||||
return (result1, *result2)
|
||||
|
||||
def _result_state_unthreadsafe(self, failure_str, source, data):
|
||||
logger.debug(f"Java state transfer {source} error={failure_str} data={data}")
|
||||
self.states[source] = (failure_str, *data)
|
||||
future = self.futures.get(source, None)
|
||||
if future is not None and not future.done():
|
||||
if failure_str is None:
|
||||
future.set_result(data)
|
||||
else:
|
||||
future.set_exception(BleakError(source, failure_str, *data))
|
||||
else:
|
||||
if failure_str is not None:
|
||||
# an error happened with nothing waiting for it
|
||||
exception = BleakError(source, failure_str, *data)
|
||||
namedfutures = [
|
||||
namedfuture
|
||||
for namedfuture in self.futures.items()
|
||||
if not namedfuture[1].done()
|
||||
]
|
||||
if len(namedfutures):
|
||||
# send it on existing requests
|
||||
for name, future in namedfutures:
|
||||
warnings.warn(f"Redirecting error without home to {name}")
|
||||
future.set_exception(exception)
|
||||
else:
|
||||
# send it on the event thread
|
||||
raise exception
|
||||
324
tools/python-embed/Lib/site-packages/bleak/backends/scanner.py
Normal file
324
tools/python-embed/Lib/site-packages/bleak/backends/scanner.py
Normal file
@ -0,0 +1,324 @@
|
||||
import abc
|
||||
import asyncio
|
||||
import inspect
|
||||
from collections.abc import Callable, Coroutine, Hashable
|
||||
from typing import Any, NamedTuple, Optional
|
||||
|
||||
from bleak.backends import BleakBackend, get_default_backend
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.exc import BleakError
|
||||
|
||||
# prevent tasks from being garbage collected
|
||||
_background_tasks: set[asyncio.Task[None]] = set()
|
||||
|
||||
|
||||
class AdvertisementData(NamedTuple):
|
||||
"""
|
||||
Wrapper around the advertisement data that each platform returns upon discovery
|
||||
"""
|
||||
|
||||
local_name: Optional[str]
|
||||
"""
|
||||
The local name of the device or ``None`` if not included in advertising data.
|
||||
"""
|
||||
|
||||
manufacturer_data: dict[int, bytes]
|
||||
"""
|
||||
Dictionary of manufacturer data in bytes from the received advertisement data or empty dict if not present.
|
||||
|
||||
The keys are Bluetooth SIG assigned Company Identifiers and the values are bytes.
|
||||
|
||||
https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/
|
||||
"""
|
||||
|
||||
service_data: dict[str, bytes]
|
||||
"""
|
||||
Dictionary of service data from the received advertisement data or empty dict if not present.
|
||||
"""
|
||||
|
||||
service_uuids: list[str]
|
||||
"""
|
||||
List of service UUIDs from the received advertisement data or empty list if not present.
|
||||
"""
|
||||
|
||||
tx_power: Optional[int]
|
||||
"""
|
||||
TX Power Level of the remote device from the received advertising data or ``None`` if not present.
|
||||
|
||||
.. versionadded:: 0.17
|
||||
"""
|
||||
|
||||
rssi: int
|
||||
"""
|
||||
The Radio Receive Signal Strength (RSSI) in dBm.
|
||||
|
||||
.. versionadded:: 0.19
|
||||
"""
|
||||
|
||||
platform_data: tuple[Any, ...]
|
||||
"""
|
||||
Tuple of platform specific data.
|
||||
|
||||
This is not a stable API. The actual values may change between releases.
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
kwargs: list[str] = []
|
||||
if self.local_name:
|
||||
kwargs.append(f"local_name={repr(self.local_name)}")
|
||||
if self.manufacturer_data:
|
||||
kwargs.append(f"manufacturer_data={repr(self.manufacturer_data)}")
|
||||
if self.service_data:
|
||||
kwargs.append(f"service_data={repr(self.service_data)}")
|
||||
if self.service_uuids:
|
||||
kwargs.append(f"service_uuids={repr(self.service_uuids)}")
|
||||
if self.tx_power is not None:
|
||||
kwargs.append(f"tx_power={repr(self.tx_power)}")
|
||||
kwargs.append(f"rssi={repr(self.rssi)}")
|
||||
return f"AdvertisementData({', '.join(kwargs)})"
|
||||
|
||||
|
||||
AdvertisementDataCallback = Callable[
|
||||
[BLEDevice, AdvertisementData],
|
||||
Optional[Coroutine[Any, Any, None]],
|
||||
]
|
||||
"""
|
||||
Type alias for callback called when advertisement data is received.
|
||||
"""
|
||||
|
||||
AdvertisementDataFilter = Callable[
|
||||
[BLEDevice, AdvertisementData],
|
||||
bool,
|
||||
]
|
||||
"""
|
||||
Type alias for an advertisement data filter function.
|
||||
|
||||
Implementations should return ``True`` for matches, otherwise ``False``.
|
||||
"""
|
||||
|
||||
|
||||
class BaseBleakScanner(abc.ABC):
|
||||
"""
|
||||
Interface for Bleak Bluetooth LE Scanners
|
||||
|
||||
Args:
|
||||
detection_callback:
|
||||
Optional function that will be called each time a device is
|
||||
discovered or advertising data has changed.
|
||||
service_uuids:
|
||||
Optional list of service UUIDs to filter on. Only advertisements
|
||||
containing this advertising data will be received.
|
||||
"""
|
||||
|
||||
seen_devices: dict[str, tuple[BLEDevice, AdvertisementData]]
|
||||
"""
|
||||
Map of device identifier to BLEDevice and most recent advertisement data.
|
||||
|
||||
The key is a backend-specific identifier for the device.
|
||||
|
||||
This map must be cleared when scanning starts.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detection_callback: Optional[AdvertisementDataCallback],
|
||||
service_uuids: Optional[list[str]],
|
||||
):
|
||||
self._ad_callbacks: dict[
|
||||
Hashable, Callable[[BLEDevice, AdvertisementData], None]
|
||||
] = {}
|
||||
"""
|
||||
List of callbacks to call when an advertisement is received.
|
||||
"""
|
||||
|
||||
if detection_callback is not None:
|
||||
self.register_detection_callback(detection_callback)
|
||||
|
||||
self._service_uuids: Optional[list[str]] = (
|
||||
[u.lower() for u in service_uuids] if service_uuids is not None else None
|
||||
)
|
||||
|
||||
self.seen_devices = {}
|
||||
|
||||
def register_detection_callback(
|
||||
self, callback: Optional[AdvertisementDataCallback]
|
||||
) -> Callable[[], None]:
|
||||
"""
|
||||
Register a callback that is called when an advertisement event from the
|
||||
OS is received.
|
||||
|
||||
The ``callback`` is a function or coroutine that takes two arguments: :class:`BLEDevice`
|
||||
and :class:`AdvertisementData`.
|
||||
|
||||
Args:
|
||||
callback: A function, coroutine or ``None``.
|
||||
|
||||
Returns:
|
||||
A method that can be called to unregister the callback.
|
||||
"""
|
||||
error_text = "callback must be callable with 2 parameters"
|
||||
|
||||
if not callable(callback):
|
||||
raise TypeError(error_text)
|
||||
|
||||
handler_signature = inspect.signature(callback)
|
||||
|
||||
if len(handler_signature.parameters) != 2:
|
||||
raise TypeError(error_text)
|
||||
|
||||
if inspect.iscoroutinefunction(callback):
|
||||
|
||||
def detection_callback(s: BLEDevice, d: AdvertisementData) -> None:
|
||||
task = asyncio.create_task(callback(s, d))
|
||||
_background_tasks.add(task)
|
||||
task.add_done_callback(_background_tasks.discard)
|
||||
|
||||
else:
|
||||
detection_callback = callback # type: ignore
|
||||
|
||||
token = object()
|
||||
|
||||
self._ad_callbacks[token] = detection_callback
|
||||
|
||||
def remove() -> None:
|
||||
self._ad_callbacks.pop(token, None)
|
||||
|
||||
return remove
|
||||
|
||||
def is_allowed_uuid(self, service_uuids: Optional[list[str]]) -> bool:
|
||||
"""
|
||||
Check if the advertisement data contains any of the service UUIDs
|
||||
matching the filter. If no filter is set, this will always return
|
||||
``True``.
|
||||
|
||||
Args:
|
||||
service_uuids: The service UUIDs from the advertisement data.
|
||||
|
||||
Returns:
|
||||
``True`` if the advertisement data should be allowed or ``False``
|
||||
if the advertisement data should be filtered out.
|
||||
"""
|
||||
# Backends will make best effort to filter out advertisements that
|
||||
# don't match the service UUIDs, but if other apps are scanning at the
|
||||
# same time or something like that, we may still receive advertisements
|
||||
# that don't match. So we need to do more filtering here to get the
|
||||
# expected behavior.
|
||||
|
||||
if not self._service_uuids:
|
||||
# if there is no filter, everything is allowed
|
||||
return True
|
||||
|
||||
if not service_uuids:
|
||||
# if there is a filter the advertisement data doesn't contain any
|
||||
# service UUIDs, filter it out
|
||||
return False
|
||||
|
||||
for uuid in service_uuids:
|
||||
if uuid in self._service_uuids:
|
||||
# match was found, keep this advertisement
|
||||
return True
|
||||
|
||||
# there were no matching service uuids, filter this one out
|
||||
return False
|
||||
|
||||
def call_detection_callbacks(
|
||||
self, device: BLEDevice, advertisement_data: AdvertisementData
|
||||
) -> None:
|
||||
"""
|
||||
Calls all registered detection callbacks.
|
||||
|
||||
Backend implementations should call this method when an advertisement
|
||||
event is received from the OS.
|
||||
"""
|
||||
|
||||
for callback in self._ad_callbacks.values():
|
||||
callback(device, advertisement_data)
|
||||
|
||||
def create_or_update_device(
|
||||
self,
|
||||
key: str,
|
||||
address: str,
|
||||
name: Optional[str],
|
||||
details: Any,
|
||||
adv: AdvertisementData,
|
||||
) -> BLEDevice:
|
||||
"""
|
||||
Creates or updates a device in :attr:`seen_devices`.
|
||||
|
||||
Args:
|
||||
key: A backend-specific identifier for the device.
|
||||
address: The Bluetooth address of the device (UUID on macOS).
|
||||
name: The OS display name for the device.
|
||||
details: The platform-specific handle for the device.
|
||||
adv: The most recent advertisement data received.
|
||||
|
||||
Returns:
|
||||
The updated device.
|
||||
"""
|
||||
|
||||
try:
|
||||
device, _ = self.seen_devices[key]
|
||||
|
||||
device.name = name
|
||||
except KeyError:
|
||||
device = BLEDevice(address, name, details)
|
||||
|
||||
self.seen_devices[key] = (device, adv)
|
||||
|
||||
return device
|
||||
|
||||
@abc.abstractmethod
|
||||
async def start(self) -> None:
|
||||
"""Start scanning for devices"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def stop(self) -> None:
|
||||
"""Stop scanning for devices"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def get_platform_scanner_backend_type() -> tuple[type[BaseBleakScanner], BleakBackend]:
|
||||
"""
|
||||
Gets the platform-specific :class:`BaseBleakScanner` type.
|
||||
"""
|
||||
backend = get_default_backend()
|
||||
match backend:
|
||||
case BleakBackend.P4ANDROID:
|
||||
from bleak.backends.p4android.scanner import (
|
||||
BleakScannerP4Android, # type: ignore
|
||||
)
|
||||
|
||||
return (BleakScannerP4Android, backend) # type: ignore
|
||||
|
||||
case BleakBackend.BLUEZ_DBUS:
|
||||
from bleak.backends.bluezdbus.scanner import (
|
||||
BleakScannerBlueZDBus, # type: ignore
|
||||
)
|
||||
|
||||
return (BleakScannerBlueZDBus, backend) # type: ignore
|
||||
|
||||
case BleakBackend.PYTHONISTA_CB:
|
||||
try:
|
||||
from bleak_pythonista import BleakScannerPythonistaCB # type: ignore
|
||||
|
||||
return (BleakScannerPythonistaCB, backend) # type: ignore
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"Ensure you have `bleak-pythonista` package installed."
|
||||
) from e
|
||||
|
||||
case BleakBackend.CORE_BLUETOOTH:
|
||||
from bleak.backends.corebluetooth.scanner import (
|
||||
BleakScannerCoreBluetooth, # type: ignore
|
||||
)
|
||||
|
||||
return (BleakScannerCoreBluetooth, backend) # type: ignore
|
||||
|
||||
case BleakBackend.WIN_RT:
|
||||
from bleak.backends.winrt.scanner import BleakScannerWinRT # type: ignore
|
||||
|
||||
return (BleakScannerWinRT, backend) # type: ignore
|
||||
|
||||
case _:
|
||||
raise BleakError(f"Unsupported backend: {backend}")
|
||||
218
tools/python-embed/Lib/site-packages/bleak/backends/service.py
Normal file
218
tools/python-embed/Lib/site-packages/bleak/backends/service.py
Normal file
@ -0,0 +1,218 @@
|
||||
# Created on 2019-03-19 by hbldh <henrik.blidh@nedomkull.com>
|
||||
"""
|
||||
Gatt Service Collection class and interface class for the Bleak representation of a GATT Service.
|
||||
"""
|
||||
import logging
|
||||
from collections.abc import Iterator
|
||||
from typing import Any, Optional, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||
from bleak.backends.descriptor import BleakGATTDescriptor
|
||||
from bleak.exc import BleakError
|
||||
from bleak.uuids import normalize_uuid_str, uuidstr_to_str
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BleakGATTService:
|
||||
"""The Bleak representation of a GATT Service."""
|
||||
|
||||
def __init__(self, obj: Any, handle: int, uuid: str) -> None:
|
||||
self.obj = obj
|
||||
self._handle = handle
|
||||
self._uuid = uuid
|
||||
self._characteristics: dict[int, BleakGATTCharacteristic] = {}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.uuid} (Handle: {self.handle}): {self.description}"
|
||||
|
||||
@property
|
||||
def handle(self) -> int:
|
||||
"""The handle of this service"""
|
||||
return self._handle
|
||||
|
||||
@property
|
||||
def uuid(self) -> str:
|
||||
"""The UUID to this service"""
|
||||
return self._uuid
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""String description for this service"""
|
||||
return uuidstr_to_str(self.uuid)
|
||||
|
||||
@property
|
||||
def characteristics(self) -> list[BleakGATTCharacteristic]:
|
||||
"""List of characteristics for this service"""
|
||||
return list(self._characteristics.values())
|
||||
|
||||
def add_characteristic(self, characteristic: BleakGATTCharacteristic) -> None:
|
||||
"""Add a :py:class:`~BleakGATTCharacteristic` to the service.
|
||||
|
||||
Should not be used by end user, but rather by `bleak` itself.
|
||||
"""
|
||||
if characteristic.handle in self._characteristics:
|
||||
raise BleakError(
|
||||
"The characteristic '%s' is already present in this BleakGATTService!",
|
||||
characteristic.handle,
|
||||
)
|
||||
|
||||
self._characteristics[characteristic.handle] = characteristic
|
||||
|
||||
def get_characteristic(
|
||||
self, uuid: Union[str, UUID]
|
||||
) -> Union[BleakGATTCharacteristic, None]:
|
||||
"""Get a characteristic by UUID.
|
||||
|
||||
Args:
|
||||
uuid: The UUID to match.
|
||||
|
||||
Returns:
|
||||
The first characteristic matching ``uuid`` or ``None`` if no
|
||||
matching characteristic was found.
|
||||
"""
|
||||
uuid = normalize_uuid_str(str(uuid))
|
||||
|
||||
try:
|
||||
return next(
|
||||
filter(lambda x: x.uuid == uuid, self._characteristics.values())
|
||||
)
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
|
||||
class BleakGATTServiceCollection:
|
||||
"""Simple data container for storing the peripheral's service complement."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.__services: dict[int, BleakGATTService] = {}
|
||||
self.__characteristics: dict[int, BleakGATTCharacteristic] = {}
|
||||
self.__descriptors: dict[int, BleakGATTDescriptor] = {}
|
||||
|
||||
def __getitem__(
|
||||
self, item: Union[str, int, UUID]
|
||||
) -> Optional[
|
||||
Union[BleakGATTService, BleakGATTCharacteristic, BleakGATTDescriptor]
|
||||
]:
|
||||
"""Get a service, characteristic or descriptor from uuid or handle"""
|
||||
return (
|
||||
self.get_service(item)
|
||||
or self.get_characteristic(item)
|
||||
or self.get_descriptor(cast(int, item))
|
||||
)
|
||||
|
||||
def __iter__(self) -> Iterator[BleakGATTService]:
|
||||
"""Returns an iterator over all BleakGATTService objects"""
|
||||
return iter(self.services.values())
|
||||
|
||||
@property
|
||||
def services(self) -> dict[int, BleakGATTService]:
|
||||
"""Returns dictionary of handles mapping to BleakGATTService"""
|
||||
return self.__services
|
||||
|
||||
@property
|
||||
def characteristics(self) -> dict[int, BleakGATTCharacteristic]:
|
||||
"""Returns dictionary of handles mapping to BleakGATTCharacteristic"""
|
||||
return self.__characteristics
|
||||
|
||||
@property
|
||||
def descriptors(self) -> dict[int, BleakGATTDescriptor]:
|
||||
"""Returns a dictionary of integer handles mapping to BleakGATTDescriptor"""
|
||||
return self.__descriptors
|
||||
|
||||
def add_service(self, service: BleakGATTService) -> None:
|
||||
"""Add a :py:class:`~BleakGATTService` to the service collection.
|
||||
|
||||
Should not be used by end user, but rather by `bleak` itself.
|
||||
"""
|
||||
if service.handle not in self.__services:
|
||||
self.__services[service.handle] = service
|
||||
else:
|
||||
logger.error(
|
||||
"The service '%s' is already present in this BleakGATTServiceCollection!",
|
||||
service.handle,
|
||||
)
|
||||
|
||||
def get_service(
|
||||
self, specifier: Union[int, str, UUID]
|
||||
) -> Optional[BleakGATTService]:
|
||||
"""Get a service by handle (int) or UUID (str or uuid.UUID)"""
|
||||
if isinstance(specifier, int):
|
||||
return self.services.get(specifier)
|
||||
|
||||
uuid = normalize_uuid_str(str(specifier))
|
||||
|
||||
x = list(
|
||||
filter(
|
||||
lambda x: x.uuid == uuid,
|
||||
self.services.values(),
|
||||
)
|
||||
)
|
||||
|
||||
if len(x) > 1:
|
||||
raise BleakError(
|
||||
"Multiple Services with this UUID, refer to your desired service by the `handle` attribute instead."
|
||||
)
|
||||
|
||||
return x[0] if x else None
|
||||
|
||||
def add_characteristic(self, characteristic: BleakGATTCharacteristic) -> None:
|
||||
"""Add a :py:class:`~BleakGATTCharacteristic` to the service collection.
|
||||
|
||||
Should not be used by end user, but rather by `bleak` itself.
|
||||
"""
|
||||
if characteristic.handle not in self.__characteristics:
|
||||
self.__characteristics[characteristic.handle] = characteristic
|
||||
self.__services[characteristic.service_handle].add_characteristic(
|
||||
characteristic
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
"The characteristic '%s' is already present in this BleakGATTServiceCollection!",
|
||||
characteristic.handle,
|
||||
)
|
||||
|
||||
def get_characteristic(
|
||||
self, specifier: Union[int, str, UUID]
|
||||
) -> Optional[BleakGATTCharacteristic]:
|
||||
"""Get a characteristic by handle (int) or UUID (str or uuid.UUID)"""
|
||||
if isinstance(specifier, int):
|
||||
return self.characteristics.get(specifier)
|
||||
|
||||
uuid = normalize_uuid_str(str(specifier))
|
||||
|
||||
# Assume uuid usage.
|
||||
x = list(
|
||||
filter(
|
||||
lambda x: x.uuid == uuid,
|
||||
self.characteristics.values(),
|
||||
)
|
||||
)
|
||||
|
||||
if len(x) > 1:
|
||||
raise BleakError(
|
||||
"Multiple Characteristics with this UUID, refer to your desired characteristic by the `handle` attribute instead."
|
||||
)
|
||||
|
||||
return x[0] if x else None
|
||||
|
||||
def add_descriptor(self, descriptor: BleakGATTDescriptor) -> None:
|
||||
"""Add a :py:class:`~BleakGATTDescriptor` to the service collection.
|
||||
|
||||
Should not be used by end user, but rather by `bleak` itself.
|
||||
"""
|
||||
if descriptor.handle not in self.__descriptors:
|
||||
self.__descriptors[descriptor.handle] = descriptor
|
||||
self.__characteristics[descriptor.characteristic_handle].add_descriptor(
|
||||
descriptor
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
"The descriptor '%s' is already present in this BleakGATTServiceCollection!",
|
||||
descriptor.handle,
|
||||
)
|
||||
|
||||
def get_descriptor(self, handle: int) -> Optional[BleakGATTDescriptor]:
|
||||
"""Get a descriptor by integer handle"""
|
||||
return self.descriptors.get(handle)
|
||||
1198
tools/python-embed/Lib/site-packages/bleak/backends/winrt/client.py
Normal file
1198
tools/python-embed/Lib/site-packages/bleak/backends/winrt/client.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,341 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "win32":
|
||||
assert False, "This backend is only available on Windows"
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Literal, NamedTuple, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from winrt.windows.devices.bluetooth import BluetoothAdapter
|
||||
from winrt.windows.devices.bluetooth.advertisement import (
|
||||
BluetoothLEAdvertisementReceivedEventArgs,
|
||||
BluetoothLEAdvertisementType,
|
||||
BluetoothLEAdvertisementWatcher,
|
||||
BluetoothLEAdvertisementWatcherStatus,
|
||||
BluetoothLEAdvertisementWatcherStoppedEventArgs,
|
||||
BluetoothLEScanningMode,
|
||||
)
|
||||
from winrt.windows.devices.radios import RadioState
|
||||
from winrt.windows.foundation import EventRegistrationToken
|
||||
|
||||
from bleak._compat import override
|
||||
from bleak.assigned_numbers import AdvertisementDataType
|
||||
from bleak.backends.scanner import (
|
||||
AdvertisementData,
|
||||
AdvertisementDataCallback,
|
||||
BaseBleakScanner,
|
||||
)
|
||||
from bleak.backends.winrt.util import assert_mta
|
||||
from bleak.exc import (
|
||||
BleakBluetoothNotAvailableError,
|
||||
BleakBluetoothNotAvailableReason,
|
||||
BleakError,
|
||||
)
|
||||
from bleak.uuids import normalize_uuid_str
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _format_bdaddr(a: int) -> str:
|
||||
return ":".join(f"{x:02X}" for x in a.to_bytes(6, byteorder="big"))
|
||||
|
||||
|
||||
def _format_event_args(e: BluetoothLEAdvertisementReceivedEventArgs) -> str:
|
||||
try:
|
||||
return f"{_format_bdaddr(e.bluetooth_address)}: {e.advertisement.local_name}"
|
||||
except Exception:
|
||||
return _format_bdaddr(e.bluetooth_address)
|
||||
|
||||
|
||||
class RawAdvData(NamedTuple):
|
||||
"""
|
||||
Platform-specific advertisement data.
|
||||
|
||||
Windows does not combine advertising data with type SCAN_RSP with other
|
||||
advertising data like other platforms, so we have to do it ourselves.
|
||||
"""
|
||||
|
||||
adv: Optional[BluetoothLEAdvertisementReceivedEventArgs]
|
||||
"""
|
||||
The advertisement data received from the BluetoothLEAdvertisementWatcher.Received event.
|
||||
"""
|
||||
scan: Optional[BluetoothLEAdvertisementReceivedEventArgs]
|
||||
"""
|
||||
The scan response for the same device as *adv*.
|
||||
"""
|
||||
|
||||
|
||||
class BleakScannerWinRT(BaseBleakScanner):
|
||||
"""The native Windows Bleak BLE Scanner.
|
||||
|
||||
Implemented using `Python/WinRT <https://github.com/Microsoft/xlang/tree/master/src/package/pywinrt/projection/>`_.
|
||||
|
||||
Args:
|
||||
detection_callback:
|
||||
Optional function that will be called each time a device is
|
||||
discovered or advertising data has changed.
|
||||
service_uuids:
|
||||
Optional list of service UUIDs to filter on. Only advertisements
|
||||
containing this advertising data will be received.
|
||||
scanning_mode:
|
||||
Set to ``"passive"`` to avoid the ``"active"`` scanning mode.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detection_callback: Optional[AdvertisementDataCallback],
|
||||
service_uuids: Optional[list[str]],
|
||||
scanning_mode: Literal["active", "passive"],
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(detection_callback, service_uuids)
|
||||
|
||||
self.watcher: Optional[BluetoothLEAdvertisementWatcher] = None
|
||||
self._advertisement_pairs: dict[str, RawAdvData] = {}
|
||||
self._stopped_event: Optional[asyncio.Event] = None
|
||||
|
||||
# case insensitivity is for backwards compatibility on Windows only
|
||||
if scanning_mode.lower() == "passive":
|
||||
self._scanning_mode = BluetoothLEScanningMode.PASSIVE
|
||||
else:
|
||||
self._scanning_mode = BluetoothLEScanningMode.ACTIVE
|
||||
|
||||
# Unfortunately, due to the way Windows handles filtering, we can't
|
||||
# make use of the service_uuids filter here. If we did we would only
|
||||
# get the advertisement data or the scan data, but not both, so would
|
||||
# miss out on other essential data. Advanced users can pass their own
|
||||
# filters though if they want to.
|
||||
self._signal_strength_filter = kwargs.get("SignalStrengthFilter", None)
|
||||
self._advertisement_filter = kwargs.get("AdvertisementFilter", None)
|
||||
|
||||
self._received_token: Optional[EventRegistrationToken] = None
|
||||
self._stopped_token: Optional[EventRegistrationToken] = None
|
||||
|
||||
def _received_handler(
|
||||
self,
|
||||
sender: BluetoothLEAdvertisementWatcher,
|
||||
event_args: BluetoothLEAdvertisementReceivedEventArgs,
|
||||
):
|
||||
"""Callback for AdvertisementWatcher.Received"""
|
||||
# TODO: Cannot check for if sender == self.watcher in winrt?
|
||||
logger.debug("Received %s.", _format_event_args(event_args))
|
||||
|
||||
# REVISIT: if scanning filters with BluetoothSignalStrengthFilter.OutOfRangeTimeout
|
||||
# are in place, an RSSI of -127 means that the device has gone out of range and should
|
||||
# be removed from the list of seen devices instead of processing the advertisement data.
|
||||
# https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothsignalstrengthfilter.outofrangetimeout
|
||||
|
||||
bdaddr = _format_bdaddr(event_args.bluetooth_address)
|
||||
|
||||
# Unlike other platforms, Windows does not combine advertising data for
|
||||
# us (regular advertisement + scan response) so we have to do it manually.
|
||||
|
||||
# get the previous advertising data/scan response pair or start a new one
|
||||
raw_data = self._advertisement_pairs.get(bdaddr, RawAdvData(None, None))
|
||||
|
||||
# update the advertising data depending on the advertising data type
|
||||
if event_args.advertisement_type == BluetoothLEAdvertisementType.SCAN_RESPONSE:
|
||||
raw_data = RawAdvData(raw_data.adv, event_args)
|
||||
else:
|
||||
raw_data = RawAdvData(event_args, raw_data.scan)
|
||||
|
||||
self._advertisement_pairs[bdaddr] = raw_data
|
||||
|
||||
uuids: list[str] = []
|
||||
mfg_data = {}
|
||||
service_data = {}
|
||||
local_name = None
|
||||
tx_power = None
|
||||
|
||||
for args in filter(lambda d: d is not None, raw_data):
|
||||
assert args
|
||||
|
||||
for u in args.advertisement.service_uuids:
|
||||
uuids.append(str(u))
|
||||
|
||||
for m in args.advertisement.manufacturer_data:
|
||||
mfg_data[m.company_id] = bytes(m.data)
|
||||
|
||||
# local name is empty string rather than None if not present
|
||||
if args.advertisement.local_name:
|
||||
local_name = args.advertisement.local_name
|
||||
|
||||
try:
|
||||
if args.transmit_power_level_in_dbm is not None:
|
||||
tx_power = args.transmit_power_level_in_dbm
|
||||
except AttributeError:
|
||||
# the transmit_power_level_in_d_bm property was introduce in
|
||||
# Windows build 19041 so we have a fallback for older versions
|
||||
for section in args.advertisement.get_sections_by_type(
|
||||
AdvertisementDataType.TX_POWER_LEVEL
|
||||
):
|
||||
tx_power = bytes(section.data)[0]
|
||||
|
||||
# Decode service data
|
||||
for section in args.advertisement.get_sections_by_type(
|
||||
AdvertisementDataType.SERVICE_DATA_UUID16
|
||||
):
|
||||
data = bytes(section.data)
|
||||
service_data[normalize_uuid_str(f"{data[1]:02x}{data[0]:02x}")] = data[
|
||||
2:
|
||||
]
|
||||
for section in args.advertisement.get_sections_by_type(
|
||||
AdvertisementDataType.SERVICE_DATA_UUID32
|
||||
):
|
||||
data = bytes(section.data)
|
||||
service_data[
|
||||
normalize_uuid_str(
|
||||
f"{data[3]:02x}{data[2]:02x}{data[1]:02x}{data[0]:02x}"
|
||||
)
|
||||
] = data[4:]
|
||||
for section in args.advertisement.get_sections_by_type(
|
||||
AdvertisementDataType.SERVICE_DATA_UUID128
|
||||
):
|
||||
data = bytes(section.data)
|
||||
service_data[str(UUID(bytes=bytes(data[15::-1])))] = data[16:]
|
||||
|
||||
if not self.is_allowed_uuid(uuids):
|
||||
return
|
||||
|
||||
# Use the BLEDevice to populate all the fields for the advertisement data to return
|
||||
advertisement_data = AdvertisementData(
|
||||
local_name=local_name,
|
||||
manufacturer_data=mfg_data,
|
||||
service_data=service_data,
|
||||
service_uuids=uuids,
|
||||
tx_power=tx_power,
|
||||
rssi=event_args.raw_signal_strength_in_dbm,
|
||||
platform_data=(sender, raw_data),
|
||||
)
|
||||
|
||||
device = self.create_or_update_device(
|
||||
bdaddr, bdaddr, local_name, raw_data, advertisement_data
|
||||
)
|
||||
|
||||
self.call_detection_callbacks(device, advertisement_data)
|
||||
|
||||
def _stopped_handler(
|
||||
self,
|
||||
sender: BluetoothLEAdvertisementWatcher,
|
||||
e: BluetoothLEAdvertisementWatcherStoppedEventArgs,
|
||||
) -> None:
|
||||
logger.debug(
|
||||
"%s devices found. Watcher status: %r.",
|
||||
len(self.seen_devices),
|
||||
sender.status,
|
||||
)
|
||||
assert self._stopped_event
|
||||
self._stopped_event.set()
|
||||
|
||||
@override
|
||||
async def start(self) -> None:
|
||||
if self.watcher:
|
||||
raise BleakError("Scanner already started")
|
||||
|
||||
# Callbacks for WinRT async methods will never happen in STA mode if
|
||||
# there is nothing pumping a Windows message loop.
|
||||
await assert_mta()
|
||||
|
||||
# TODO: need to fix return type of get_default_async() in PyWinRT
|
||||
adapter = await BluetoothAdapter.get_default_async()
|
||||
if adapter is None: # pyright: ignore[reportUnnecessaryComparison]
|
||||
raise BleakBluetoothNotAvailableError(
|
||||
"No Bluetooth adapter found",
|
||||
BleakBluetoothNotAvailableReason.NO_BLUETOOTH,
|
||||
)
|
||||
|
||||
if not adapter.is_central_role_supported:
|
||||
raise BleakBluetoothNotAvailableError(
|
||||
"BLE 'central' role not supported on this adapter",
|
||||
BleakBluetoothNotAvailableReason.NO_BLE_CENTRAL_ROLE,
|
||||
)
|
||||
|
||||
radio = await adapter.get_radio_async()
|
||||
if radio.state != RadioState.ON:
|
||||
raise BleakBluetoothNotAvailableError(
|
||||
"Bluetooth radio is not powered on. Turn on Bluetooth and try again.",
|
||||
BleakBluetoothNotAvailableReason.POWERED_OFF,
|
||||
)
|
||||
|
||||
# start with fresh list of discovered devices
|
||||
self.seen_devices = {}
|
||||
self._advertisement_pairs.clear()
|
||||
|
||||
self.watcher = BluetoothLEAdvertisementWatcher()
|
||||
self.watcher.scanning_mode = self._scanning_mode
|
||||
# BlueZ and CoreBluetooth don't allow controlling this and always enabled it, so do the same here
|
||||
try:
|
||||
self.watcher.allow_extended_advertisements = True
|
||||
except AttributeError:
|
||||
logger.warning(
|
||||
"Extended advertisements are not available in this OS Version."
|
||||
)
|
||||
|
||||
event_loop = asyncio.get_running_loop()
|
||||
self._stopped_event = asyncio.Event()
|
||||
|
||||
def on_received(
|
||||
sender: BluetoothLEAdvertisementWatcher,
|
||||
args: BluetoothLEAdvertisementReceivedEventArgs,
|
||||
) -> None:
|
||||
event_loop.call_soon_threadsafe(self._received_handler, sender, args)
|
||||
|
||||
self._received_token = self.watcher.add_received(on_received)
|
||||
|
||||
def on_stopped(
|
||||
sender: BluetoothLEAdvertisementWatcher,
|
||||
args: BluetoothLEAdvertisementWatcherStoppedEventArgs,
|
||||
) -> None:
|
||||
event_loop.call_soon_threadsafe(self._stopped_handler, sender, args)
|
||||
|
||||
self._stopped_token = self.watcher.add_stopped(on_stopped)
|
||||
|
||||
if self._signal_strength_filter is not None:
|
||||
self.watcher.signal_strength_filter = self._signal_strength_filter
|
||||
if self._advertisement_filter is not None:
|
||||
self.watcher.advertisement_filter = self._advertisement_filter
|
||||
|
||||
self.watcher.start()
|
||||
|
||||
# no events for status changes, so we have to poll :-(
|
||||
while self.watcher.status == BluetoothLEAdvertisementWatcherStatus.CREATED:
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
if self.watcher.status == BluetoothLEAdvertisementWatcherStatus.ABORTED:
|
||||
raise BleakError("Failed to start scanner. Is Bluetooth turned on?")
|
||||
|
||||
if self.watcher.status != BluetoothLEAdvertisementWatcherStatus.STARTED:
|
||||
raise BleakError(f"Unexpected watcher status: {self.watcher.status.name}")
|
||||
|
||||
@override
|
||||
async def stop(self) -> None:
|
||||
assert self.watcher
|
||||
assert self._stopped_event
|
||||
assert self._received_token
|
||||
assert self._stopped_token
|
||||
|
||||
self.watcher.stop()
|
||||
|
||||
if self.watcher.status == BluetoothLEAdvertisementWatcherStatus.STOPPING:
|
||||
await self._stopped_event.wait()
|
||||
else:
|
||||
logger.debug(
|
||||
"skipping waiting for stop because status is %r",
|
||||
self.watcher.status,
|
||||
)
|
||||
|
||||
try:
|
||||
self.watcher.remove_received(self._received_token)
|
||||
self.watcher.remove_stopped(self._stopped_token)
|
||||
except Exception as e:
|
||||
logger.debug("Could not remove event handlers: %s", e)
|
||||
|
||||
self._stopped_token = None
|
||||
self._received_token = None
|
||||
|
||||
self.watcher = None
|
||||
@ -0,0 +1,224 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.platform != "win32":
|
||||
assert False, "This backend is only available on Windows"
|
||||
|
||||
import asyncio
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
from enum import IntEnum
|
||||
|
||||
from bleak._compat import timeout as async_timeout
|
||||
from bleak.exc import BleakError
|
||||
|
||||
|
||||
def _check_result(result: int, func: Any, args: Any) -> Any:
|
||||
if not result:
|
||||
raise ctypes.WinError()
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def check_hresult(result: int, func: Any, args: Any) -> Any:
|
||||
if result:
|
||||
raise ctypes.WinError(result)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
# not defined in wintypes
|
||||
_UINT_PTR = wintypes.WPARAM
|
||||
|
||||
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-timerproc
|
||||
_TIMERPROC = ctypes.WINFUNCTYPE(
|
||||
None, wintypes.HWND, _UINT_PTR, wintypes.UINT, wintypes.DWORD
|
||||
)
|
||||
|
||||
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-settimer
|
||||
_SET_TIMER_PROTOTYPE = ctypes.WINFUNCTYPE(
|
||||
_UINT_PTR, wintypes.HWND, _UINT_PTR, wintypes.UINT, _TIMERPROC
|
||||
)
|
||||
_SET_TIMER_PARAM_FLAGS = (
|
||||
(1, "hwnd", None),
|
||||
(1, "nidevent"),
|
||||
(1, "uelapse"),
|
||||
(1, "lptimerfunc", None),
|
||||
)
|
||||
_SetTimer = _SET_TIMER_PROTOTYPE(
|
||||
("SetTimer", ctypes.windll.user32), _SET_TIMER_PARAM_FLAGS
|
||||
)
|
||||
_SetTimer.errcheck = _check_result # type: ignore[assignment]
|
||||
|
||||
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-killtimer
|
||||
_KILL_TIMER_PROTOTYPE = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, _UINT_PTR)
|
||||
_KILL_TIMER_PARAM_FLAGS = (
|
||||
(1, "hwnd", None),
|
||||
(1, "uidevent"),
|
||||
)
|
||||
_KillTimer = _KILL_TIMER_PROTOTYPE(
|
||||
("KillTimer", ctypes.windll.user32), _KILL_TIMER_PARAM_FLAGS
|
||||
)
|
||||
|
||||
# https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cogetapartmenttype
|
||||
_CO_GET_APARTMENT_TYPE_PROTOTYPE = ctypes.WINFUNCTYPE(
|
||||
ctypes.c_int,
|
||||
ctypes.POINTER(ctypes.c_int),
|
||||
ctypes.POINTER(ctypes.c_int),
|
||||
)
|
||||
_CO_GET_APARTMENT_TYPE_PARAM_FLAGS = (
|
||||
(1, "papttype", None),
|
||||
(1, "paptqualifier", None),
|
||||
)
|
||||
_CoGetApartmentType = _CO_GET_APARTMENT_TYPE_PROTOTYPE(
|
||||
("CoGetApartmentType", ctypes.windll.ole32), _CO_GET_APARTMENT_TYPE_PARAM_FLAGS
|
||||
)
|
||||
_CoGetApartmentType.errcheck = check_hresult # type: ignore[assignment]
|
||||
|
||||
_CO_E_NOTINITIALIZED = -2147221008
|
||||
|
||||
|
||||
# https://learn.microsoft.com/en-us/windows/win32/api/objidl/ne-objidl-apttype
|
||||
class _AptType(IntEnum):
|
||||
CURRENT = -1
|
||||
STA = 0
|
||||
MTA = 1
|
||||
NA = 2
|
||||
MAIN_STA = 3
|
||||
|
||||
|
||||
# https://learn.microsoft.com/en-us/windows/win32/api/objidl/ne-objidl-apttypequalifier
|
||||
class _AptQualifierType(IntEnum):
|
||||
NONE = 0
|
||||
IMPLICIT_MTA = 1
|
||||
NA_ON_MTA = 2
|
||||
NA_ON_STA = 3
|
||||
NA_ON_IMPLICIT_STA = 4
|
||||
NA_ON_MAIN_STA = 5
|
||||
APPLICATION_STA = 6
|
||||
RESERVED_1 = 7
|
||||
|
||||
|
||||
def _get_apartment_type() -> tuple[_AptType, _AptQualifierType]:
|
||||
"""
|
||||
Calls CoGetApartmentType to get the current apartment type and qualifier.
|
||||
|
||||
Returns:
|
||||
The current apartment type and qualifier.
|
||||
Raises:
|
||||
OSError: If the call to CoGetApartmentType fails.
|
||||
"""
|
||||
api_type = ctypes.c_int()
|
||||
api_type_qualifier = ctypes.c_int()
|
||||
_CoGetApartmentType(ctypes.byref(api_type), ctypes.byref(api_type_qualifier))
|
||||
return _AptType(api_type.value), _AptQualifierType(api_type_qualifier.value)
|
||||
|
||||
|
||||
async def assert_mta() -> None:
|
||||
"""
|
||||
Asserts that the current apartment type is MTA.
|
||||
|
||||
Raises:
|
||||
BleakError:
|
||||
If the current apartment type is not MTA and there is no Windows
|
||||
message loop running.
|
||||
|
||||
.. versionadded:: 0.22
|
||||
|
||||
.. versionchanged:: 0.22.2
|
||||
|
||||
Function is now async and will not raise if the current apartment type
|
||||
is STA and the Windows message loop is running.
|
||||
"""
|
||||
if hasattr(allow_sta, "_allowed"):
|
||||
return
|
||||
|
||||
try:
|
||||
apt_type, _ = _get_apartment_type()
|
||||
except OSError as e:
|
||||
# All is OK if not initialized yet. WinRT will initialize it.
|
||||
if e.winerror == _CO_E_NOTINITIALIZED:
|
||||
return
|
||||
|
||||
raise
|
||||
|
||||
if apt_type == _AptType.MTA:
|
||||
# if we get here, WinRT probably set the apartment type to MTA and all
|
||||
# is well, we don't need to check again
|
||||
setattr(allow_sta, "_allowed", True)
|
||||
return
|
||||
|
||||
event = asyncio.Event()
|
||||
|
||||
def wait_event(hwnd: int, uidevent: int, uelapse: int, dwtime: int) -> None:
|
||||
event.set()
|
||||
|
||||
# have to keep a reference to the callback or it will be garbage collected
|
||||
# before it is called
|
||||
callback = _TIMERPROC(wait_event)
|
||||
|
||||
# set a timer to see if we get a callback to ensure the windows event loop
|
||||
# is running
|
||||
timer = _SetTimer(None, 1, 0, callback)
|
||||
|
||||
try:
|
||||
async with async_timeout(0.5):
|
||||
await event.wait()
|
||||
except asyncio.TimeoutError:
|
||||
raise BleakError(
|
||||
"Thread is configured for Windows GUI but callbacks are not working."
|
||||
+ (
|
||||
" Suspect unwanted side effects from importing 'pythoncom'."
|
||||
if "pythoncom" in sys.modules
|
||||
else ""
|
||||
)
|
||||
)
|
||||
else:
|
||||
# if the windows event loop is running, we assume it is going to keep
|
||||
# running and we don't need to check again
|
||||
setattr(allow_sta, "_allowed", True)
|
||||
finally:
|
||||
_KillTimer(None, timer)
|
||||
|
||||
|
||||
def allow_sta() -> None:
|
||||
"""
|
||||
Suppress check for MTA thread type and allow STA.
|
||||
|
||||
Bleak will hang forever if the current thread is not MTA - unless there is
|
||||
a Windows event loop running that is properly integrated with asyncio in
|
||||
Python.
|
||||
|
||||
If your program meets that condition, you must call this function do disable
|
||||
the check for MTA. If your program doesn't have a graphical user interface
|
||||
you probably shouldn't call this function. and use ``uninitialize_sta()``
|
||||
instead.
|
||||
|
||||
.. versionadded:: 0.22.1
|
||||
"""
|
||||
setattr(allow_sta, "_allowed", True)
|
||||
|
||||
|
||||
def uninitialize_sta():
|
||||
"""
|
||||
Uninitialize the COM library on the current thread if it was not initialized
|
||||
as MTA.
|
||||
|
||||
This is intended to undo the implicit initialization of the COM library as STA
|
||||
by packages like pywin32.
|
||||
|
||||
It should be called as early as possible in your application after the
|
||||
offending package has been imported.
|
||||
|
||||
.. versionadded:: 0.22
|
||||
"""
|
||||
|
||||
try:
|
||||
_get_apartment_type()
|
||||
except OSError as e:
|
||||
# All is OK if not initialized yet. WinRT will initialize it.
|
||||
if e.winerror == _CO_E_NOTINITIALIZED:
|
||||
return
|
||||
else:
|
||||
ctypes.windll.ole32.CoUninitialize()
|
||||
344
tools/python-embed/Lib/site-packages/bleak/exc.py
Normal file
344
tools/python-embed/Lib/site-packages/bleak/exc.py
Normal file
@ -0,0 +1,344 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import uuid
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
|
||||
class BleakError(Exception):
|
||||
"""Base Exception for bleak."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BleakBluetoothNotAvailableReason(enum.Enum):
|
||||
"""
|
||||
Reasons for Bluetooth not being available.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
NO_BLUETOOTH = enum.auto()
|
||||
"""
|
||||
The system does not support Bluetooth. I.e. there is no Bluetooth radio.
|
||||
"""
|
||||
|
||||
NO_BLE_CENTRAL_ROLE = enum.auto()
|
||||
"""
|
||||
The Bluetooth radio does not support the Central role. (E.g. classic-only adapters.)
|
||||
"""
|
||||
|
||||
POWERED_OFF = enum.auto()
|
||||
"""
|
||||
Bluetooth is not currently available because the radio is turned off.
|
||||
"""
|
||||
|
||||
DENIED_BY_USER = enum.auto()
|
||||
"""
|
||||
The user denied permission for the app to use Bluetooth when prompted.
|
||||
"""
|
||||
|
||||
DENIED_BY_SYSTEM = enum.auto()
|
||||
"""
|
||||
Using Bluetooth was denied by the system. E.g. because of a system administrator policy.
|
||||
"""
|
||||
|
||||
DENIED_BY_UNKNOWN = enum.auto()
|
||||
"""
|
||||
Permission to use Bluetooth was denied for an unknown reason.
|
||||
"""
|
||||
|
||||
UNKNOWN = enum.auto()
|
||||
"""
|
||||
Bluetooth is not available for an unknown reason.
|
||||
"""
|
||||
|
||||
|
||||
class BleakBluetoothNotAvailableError(BleakError):
|
||||
"""
|
||||
Exception which is raised if the Bluetooth access is not available for some reason.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
def __init__(self, msg: str, reason: BleakBluetoothNotAvailableReason) -> None:
|
||||
super().__init__(msg, reason)
|
||||
|
||||
@property
|
||||
def reason(self) -> BleakBluetoothNotAvailableReason:
|
||||
"""
|
||||
Gets the reason why Bluetooth is not available.
|
||||
"""
|
||||
return self.args[1]
|
||||
|
||||
|
||||
class BleakCharacteristicNotFoundError(BleakError):
|
||||
"""
|
||||
Exception which is raised if a device does not support a characteristic.
|
||||
|
||||
.. versionadded:: 0.22
|
||||
"""
|
||||
|
||||
char_specifier: Union[int, str, uuid.UUID]
|
||||
|
||||
def __init__(self, char_specifier: Union[int, str, uuid.UUID]) -> None:
|
||||
"""
|
||||
Args:
|
||||
characteristic (str): handle or UUID of the characteristic which was not found
|
||||
"""
|
||||
super().__init__(f"Characteristic {char_specifier} was not found!")
|
||||
self.char_specifier = char_specifier
|
||||
|
||||
|
||||
class BleakDeviceNotFoundError(BleakError):
|
||||
"""
|
||||
Exception which is raised if a device can not be found by ``connect``, ``pair`` and ``unpair``.
|
||||
This is the case if the OS Bluetooth stack has never seen this device or it was removed and forgotten.
|
||||
|
||||
.. versionadded:: 0.19
|
||||
"""
|
||||
|
||||
identifier: str
|
||||
|
||||
def __init__(self, identifier: str, *args: object) -> None:
|
||||
"""
|
||||
Args:
|
||||
identifier (str): device identifier (Bluetooth address or UUID) of the device which was not found
|
||||
"""
|
||||
super().__init__(*args)
|
||||
self.identifier = identifier
|
||||
|
||||
|
||||
class BleakDBusError(BleakError):
|
||||
"""Specialized exception type for D-Bus errors."""
|
||||
|
||||
def __init__(self, dbus_error: str, error_body: list[Any]):
|
||||
"""
|
||||
Args:
|
||||
dbus_error (str): The D-Bus error, e.g. ``org.freedesktop.DBus.Error.UnknownObject``.
|
||||
error_body (list): Body of the D-Bus error, sometimes containing error description or details.
|
||||
"""
|
||||
super().__init__(dbus_error, *error_body)
|
||||
|
||||
@property
|
||||
def dbus_error(self) -> str:
|
||||
"""Gets the D-Bus error name, e.g. ``org.freedesktop.DBus.Error.UnknownObject``."""
|
||||
return self.args[0]
|
||||
|
||||
@property
|
||||
def dbus_error_details(self) -> Optional[str]:
|
||||
"""Gets the optional D-Bus error details, e.g. 'Invalid UUID'."""
|
||||
if len(self.args) > 1:
|
||||
details = self.args[1]
|
||||
# Some error descriptions can be further parsed to be even more helpful
|
||||
if "ATT error: 0x" in details:
|
||||
more_detail = PROTOCOL_ERROR_CODES.get(
|
||||
int(details.rsplit("x")[1], 16), "Unknown code"
|
||||
)
|
||||
details += f" ({more_detail})"
|
||||
return details
|
||||
return None
|
||||
|
||||
def __str__(self) -> str:
|
||||
name = f"[{self.dbus_error}]"
|
||||
details = self.dbus_error_details
|
||||
return (name + " " + details) if details else name
|
||||
|
||||
|
||||
CONTROLLER_ERROR_CODES = {
|
||||
0x00: "Success",
|
||||
0x01: "Unknown HCI Command",
|
||||
0x02: "Unknown Connection Identifier",
|
||||
0x03: "Hardware Failure",
|
||||
0x04: "Page Timeout",
|
||||
0x05: "Authentication Failure",
|
||||
0x06: "PIN or Key Missing",
|
||||
0x07: "Memory Capacity Exceeded",
|
||||
0x08: "Connection Timeout",
|
||||
0x09: "Connection Limit Exceeded",
|
||||
0x0A: "Synchronous Connection Limit To A Device Exceeded",
|
||||
0x0B: "Connection Already Exists",
|
||||
0x0C: "Command Disallowed",
|
||||
0x0D: "Connection Rejected due to Limited Resources",
|
||||
0x0E: "Connection Rejected Due To Security Reasons",
|
||||
0x0F: "Connection Rejected due to Unacceptable BD_ADDR",
|
||||
0x10: "Connection Accept Timeout Exceeded",
|
||||
0x11: "Unsupported Feature or Parameter Value",
|
||||
0x12: "Invalid HCI Command Parameters",
|
||||
0x13: "Remote User Terminated Connection",
|
||||
0x14: "Remote Device Terminated Connection due to Low Resources",
|
||||
0x15: "Remote Device Terminated Connection due to Power Off",
|
||||
0x16: "Connection Terminated By Local Host",
|
||||
0x17: "Repeated Attempts",
|
||||
0x18: "Pairing Not Allowed",
|
||||
0x19: "Unknown LMP PDU",
|
||||
0x1A: "Unsupported Remote Feature / Unsupported LMP Feature",
|
||||
0x1B: "SCO Offset Rejected",
|
||||
0x1C: "SCO Interval Rejected",
|
||||
0x1D: "SCO Air Mode Rejected",
|
||||
0x1E: "Invalid LMP Parameters / Invalid LL Parameters",
|
||||
0x1F: "Unspecified Error",
|
||||
0x20: "Unsupported LMP Parameter Value / Unsupported LL Parameter Value",
|
||||
0x21: "Role Change Not Allowed",
|
||||
0x22: "LMP Response Timeout / LL Response Timeout",
|
||||
0x23: "LMP Error Transaction Collision / LL Procedure Collision",
|
||||
0x24: "LMP PDU Not Allowed",
|
||||
0x25: "Encryption Mode Not Acceptable",
|
||||
0x26: "Link Key cannot be Changed",
|
||||
0x27: "Requested QoS Not Supported",
|
||||
0x28: "Instant Passed",
|
||||
0x29: "Pairing With Unit Key Not Supported",
|
||||
0x2A: "Different Transaction Collision",
|
||||
0x2B: "Reserved for future use",
|
||||
0x2C: "QoS Unacceptable Parameter",
|
||||
0x2D: "QoS Rejected",
|
||||
0x2E: "Channel Classification Not Supported",
|
||||
0x2F: "Insufficient Security",
|
||||
0x30: "Parameter Out Of Mandatory Range",
|
||||
0x31: "Reserved for future use",
|
||||
0x32: "Role Switch Pending",
|
||||
0x33: "Reserved for future use",
|
||||
0x34: "Reserved Slot Violation",
|
||||
0x35: "Role Switch Failed",
|
||||
0x36: "Extended Inquiry Response Too Large",
|
||||
0x37: "Secure Simple Pairing Not Supported By Host",
|
||||
0x38: "Host Busy - Pairing",
|
||||
0x39: "Connection Rejected due to No Suitable Channel Found",
|
||||
0x3A: "Controller Busy",
|
||||
0x3B: "Unacceptable Connection Parameters",
|
||||
0x3C: "Advertising Timeout",
|
||||
0x3D: "Connection Terminated due to MIC Failure",
|
||||
0x3E: "Connection Failed to be Established / Synchronization Timeout",
|
||||
0x3F: "MAC Connection Failed",
|
||||
0x40: "Coarse Clock Adjustment Rejected but Will Try to Adjust Using Clock",
|
||||
0x41: "Type0 Submap Not Defined",
|
||||
0x42: "Unknown Advertising Identifier",
|
||||
0x43: "Limit Reached",
|
||||
0x44: "Operation Cancelled by Host",
|
||||
0x45: "Packet Too Long",
|
||||
}
|
||||
|
||||
# as defined in Bluetooth Core Specification v5.2, volume 3, part F, section 3.4.1.1, table 3.4.
|
||||
PROTOCOL_ERROR_CODES = {
|
||||
0x01: "Invalid Handle",
|
||||
0x02: "Read Not Permitted",
|
||||
0x03: "Write Not Permitted",
|
||||
0x04: "Invalid PDU",
|
||||
0x05: "Insufficient Authentication",
|
||||
0x06: "Request Not Supported",
|
||||
0x07: "Invalid Offset",
|
||||
0x08: "Insufficient Authorization",
|
||||
0x09: "Prepare Queue Full",
|
||||
0x0A: "Attribute Not Found",
|
||||
0x0B: "Attribute Not Long",
|
||||
0x0C: "Insufficient Encryption Key Size",
|
||||
0x0D: "Invalid Attribute Value Length",
|
||||
0x0E: "Unlikely Error",
|
||||
0x0F: "Insufficient Encryption",
|
||||
0x10: "Unsupported Group Type",
|
||||
0x11: "Insufficient Resource",
|
||||
0x12: "Database Out Of Sync",
|
||||
0x13: "Value Not Allowed",
|
||||
0x80: "Application-specific Error 0x80",
|
||||
0x81: "Application-specific Error 0x81",
|
||||
0x82: "Application-specific Error 0x82",
|
||||
0x83: "Application-specific Error 0x83",
|
||||
0x84: "Application-specific Error 0x84",
|
||||
0x85: "Application-specific Error 0x85",
|
||||
0x86: "Application-specific Error 0x86",
|
||||
0x87: "Application-specific Error 0x87",
|
||||
0x88: "Application-specific Error 0x88",
|
||||
0x89: "Application-specific Error 0x89",
|
||||
0x8A: "Application-specific Error 0x8A",
|
||||
0x8B: "Application-specific Error 0x8B",
|
||||
0x8C: "Application-specific Error 0x8C",
|
||||
0x8D: "Application-specific Error 0x8D",
|
||||
0x8E: "Application-specific Error 0x8E",
|
||||
0x8F: "Application-specific Error 0x8F",
|
||||
0x90: "Application-specific Error 0x90",
|
||||
0x91: "Application-specific Error 0x91",
|
||||
0x92: "Application-specific Error 0x92",
|
||||
0x93: "Application-specific Error 0x93",
|
||||
0x94: "Application-specific Error 0x94",
|
||||
0x95: "Application-specific Error 0x95",
|
||||
0x96: "Application-specific Error 0x96",
|
||||
0x97: "Application-specific Error 0x97",
|
||||
0x98: "Application-specific Error 0x98",
|
||||
0x99: "Application-specific Error 0x99",
|
||||
0x9A: "Application-specific Error 0x9A",
|
||||
0x9B: "Application-specific Error 0x9B",
|
||||
0x9C: "Application-specific Error 0x9C",
|
||||
0x9D: "Application-specific Error 0x9D",
|
||||
0x9E: "Application-specific Error 0x9E",
|
||||
0x9F: "Application-specific Error 0x9F",
|
||||
0xFC: "Write Request Rejected",
|
||||
0xFD: "Client Characteristic Configuration Descriptor Improperly Configured",
|
||||
0xFE: "Procedure Already in Progress",
|
||||
0xFF: "Out of Range",
|
||||
}
|
||||
|
||||
|
||||
class BleakGATTProtocolErrorCode(enum.IntEnum):
|
||||
"""
|
||||
Enumeration of GATT protocol error codes.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
|
||||
INVALID_HANDLE = 0x01
|
||||
READ_NOT_PERMITTED = 0x02
|
||||
WRITE_NOT_PERMITTED = 0x03
|
||||
INVALID_PDU = 0x04
|
||||
INSUFFICIENT_AUTHENTICATION = 0x05
|
||||
REQUEST_NOT_SUPPORTED = 0x06
|
||||
INVALID_OFFSET = 0x07
|
||||
INSUFFICIENT_AUTHORIZATION = 0x08
|
||||
PREPARE_QUEUE_FULL = 0x09
|
||||
ATTRIBUTE_NOT_FOUND = 0x0A
|
||||
ATTRIBUTE_NOT_LONG = 0x0B
|
||||
INSUFFICIENT_ENCRYPTION_KEY_SIZE = 0x0C
|
||||
INVALID_ATTRIBUTE_VALUE_LENGTH = 0x0D
|
||||
UNLIKELY_ERROR = 0x0E
|
||||
INSUFFICIENT_ENCRYPTION = 0x0F
|
||||
UNSUPPORTED_GROUP_TYPE = 0x10
|
||||
INSUFFICIENT_RESOURCE = 0x11
|
||||
DATABASE_OUT_OF_SYNC = 0x12
|
||||
VALUE_NOT_ALLOWED = 0x13
|
||||
WRITE_REQUEST_REJECTED = 0xFC
|
||||
CCCD_IMPROPERLY_CONFIGURED = 0xFD
|
||||
PROCEDURE_ALREADY_IN_PROGRESS = 0xFE
|
||||
OUT_OF_RANGE = 0xFF
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value: Any) -> BleakGATTProtocolErrorCode | None:
|
||||
try:
|
||||
obj = int.__new__(cls, value)
|
||||
except TypeError: # pragma: no cover
|
||||
return None
|
||||
|
||||
obj._value_ = value
|
||||
obj._name_ = f"{cls.__name__}[{value}]"
|
||||
return obj
|
||||
|
||||
|
||||
class BleakGATTProtocolError(BleakError):
|
||||
"""
|
||||
Exception which is raised if a GATT protocol error occurs.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
|
||||
def __init__(self, error_code: int) -> None:
|
||||
"""
|
||||
Args:
|
||||
error_code (int): The GATT protocol error code.
|
||||
"""
|
||||
error_message = PROTOCOL_ERROR_CODES.get(error_code, "Unknown code")
|
||||
super().__init__(error_code, f"GATT Protocol Error: {error_message}")
|
||||
|
||||
@property
|
||||
def code(self) -> BleakGATTProtocolErrorCode:
|
||||
"""
|
||||
Gets the GATT protocol error code.
|
||||
"""
|
||||
return BleakGATTProtocolErrorCode(self.args[0])
|
||||
0
tools/python-embed/Lib/site-packages/bleak/py.typed
Normal file
0
tools/python-embed/Lib/site-packages/bleak/py.typed
Normal file
1271
tools/python-embed/Lib/site-packages/bleak/uuids.py
Normal file
1271
tools/python-embed/Lib/site-packages/bleak/uuids.py
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user