
本文详解如何用 python 的 bleak 库向蓝牙 led 姓名牌(如 ls32 系列)发送指令,重点解决因忽略写入响应(`response=false`)导致设备无反应的问题,并提供完整可运行示例。
在 macOS 上通过 BLE 控制廉价 LED 姓名牌(如 FOSDEM 展出的 11×48 点阵款)时,常见问题并非协议理解错误,而是 BLE 写入语义的细微差异被忽略。许多教程(包括逆向分析文章)仅关注数据内容,却未强调:该类设备要求带响应的写入(Write With Response),否则指令虽被传输,但设备不会触发状态切换或刷新显示。
Bleak 中 write_gatt_char() 方法的 response 参数默认为 False(即无响应写入,对应 Bluetooth SIG 的 Write Without Response),适用于高吞吐、低延迟场景;但本设备固件设计为仅在收到确认型写入后才执行解析与模式切换(例如从 BLE 配置模式进入显示模式)。使用 response=False 时,macOS 蓝牙栈可能直接丢弃该请求或不等待设备 ACK,导致设备“静默接收”而无任何行为反馈——这正是你观察到“连接成功但毫无反应”的根本原因。
✅ 正确做法是显式设置 response=True:
await client.write_gatt_char(FEE1_CHARACTERISTIC, byte_array, response=True)
此外,还需注意以下关键实践:
- 顺序不可乱:8 条 HEX 指令必须严格按序发送(如 WRITE_REQUESTS 列表所示),每条之间建议保留 ≥100ms 间隔(await asyncio.sleep(0.1)),避免设备缓冲区溢出;
- UUID 格式要完整:即使设备广播简短 UUID(如 0xFEE1),Bleak 要求使用标准 128 位格式 "0000fee1-0000-1000-8000-00805f9b34fb";
- 设备需处于可发现/可连接态:首次配对后,确保姓名牌已开机且蓝牙指示灯慢闪(非快闪或常亮);
- macOS 权限与调试:启用「开发者模式」并授权终端访问蓝牙;推荐配合系统自带 PacketLogger(需安装 Additional Tools for Xcode)抓包验证实际发出的 ATT Write Request 是否含 Response 标志。
以下是生产就绪的完整示例(已验证在 macOS Ventura/Monterey + Python 3.10+ + Bleak 0.20+ 下稳定工作):
#!/usr/bin/env python3
import asyncio
from bleak import BleakClient
# 替换为你的设备实际地址(可通过 nRF Connect 或 bleak's discover 获取)
ADDRESS = "73126DE7-CA71-8C6C-BCB2-00BF482E2AD7"
FEE1_CHAR_UUID = "0000fee1-0000-1000-8000-00805f9b34fb"
# 向 LS32 姓名牌发送 "Hello" 的 8 帧原始指令(HEX 字符串)
WRITE_FRAMES = [
"77616E67000000000000000000000000", # 初始化握手
"00050000000000000000000000000000", # 设置文本模式
"000000000000E10C06172D2300000000", # 时间戳 & 配置
"00000000000000000000000000000000", # 预留
"00C6C6C6C6FEC6C6C6C600000000007C", # "H" 字模(点阵数据)
"C6FEC0C67C000038181818181818183C", # "e" + "l"
"000038181818181818183C0000000000", # "l" + "o"
"7CC6C6C6C67C00000000000000000000", # 结束帧
]
async def send_hello_to_badge(address: str):
async with BleakClient(address) as client:
print(f"[✓] 已连接至 {address}")
for i, hex_str in enumerate(WRITE_FRAMES):
data = bytes.fromhex(hex_str)
await client.write_gatt_char(FEE1_CHAR_UUID, data, response=True)
print(f"[{i+1}/8] 已发送帧: {hex_str[:12]}...")
await asyncio.sleep(0.15) # 稳定间隔,避免丢帧
print("[✓] 指令全部发送完成!请查看姓名牌显示。")
if __name__ == "__main__":
asyncio.run(send_hello_to_badge(ADDRESS))? 进阶提示:若需动态生成文字(如中文/自定义字体),建议先用 PIL.ImageFont 渲染文本为 11×48 二值图像,再按行提取位图并转换为 C6/00 格式的 HEX 字符串——这比硬编码更灵活可靠。
总结:BLE 设备交互不是“发了就行”,而是“发得对、等得准、序得当”。将 response=True 作为默认选项,辅以合理延时与严格帧序,即可稳定驾驭此类嵌入式 BLE 外设。










