impress_sig_mesh_hacs/custom_components/sigmesh_gateway/hci_gateway.py
impressionyang b61d99c2e0 feat: 实现 HCI 网关协议支持 E104-BT12USP
1. 新建 hci_gateway.py - HCI 协议实现
   - HCI 命令包构建和解析
   - 支持配网扫描、配置密钥等操作
   - 支持 Mesh 消息发送

2. 更新 serial_reader.py
   - 集成 HciGateway
   - 使用 HCI 协议解析数据(而非 AT 命令)

3. 更新 provisioning.py
   - 使用 HCI 协议发送扫描命令
   - 移除 AT+PROV=SCAN 命令

原因:E104-BT12USP 网关使用 HCI 固件,不是 AT 命令固件
2026-04-16 17:24:28 +08:00

348 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""E104-BT12USP HCI 网关协议模块.
根据亿佰特 E104-BT12USP 网关的 HCI 协议实现通信。
该网关使用 HCI 数据包格式,不是 AT 命令。
"""
from __future__ import annotations
import asyncio
import logging
from dataclasses import dataclass
from enum import IntEnum
_LOGGER = logging.getLogger(__name__)
class HciGatewayOp(IntEnum):
"""HCI 网关操作码."""
# 网关控制
GATEWAY_RESET = 0x0001 # 网关复位
GATEWAY_VERSION = 0x0002 # 获取版本
# 配网相关
PROV_GET_STS = 0x000C # 获取配网状态
PROV_START = 0x0010 # 开始配网
PROV_STOP = 0x0011 # 停止配网
PROV_SCAN = 0x0012 # 扫描设备
# 密钥配置
CFG_NETKEY = 0x0020 # 配置网络密钥
CFG_APPKEY = 0x0021 # 配置应用密钥
# 数据发送
MESH_SEND = 0x0030 # 发送 Mesh 数据
MESH_RECV = 0x0031 # 接收 Mesh 数据
# 事件
EVENT_PROV_DEVICE = 0x0040 # 配网设备事件
EVENT_MESH_DATA = 0x0041 # Mesh 数据事件
# HCI 数据包格式:
# [0xE9][0xFF][Opcode(2)][Length(2)][Payload(N)][Checksum(1)]
# 或
# [0xE8][0xFF][Opcode(2)][Length(2)][Payload(N)]
HCI_CMD_PREFIX = b"\xe9\xff"
HCI_RSP_PREFIX = b"\x91"
HCI_EVT_PREFIX = b"\xe8\xff"
@dataclass
class HciPacket:
"""HCI 数据包."""
opcode: int
payload: bytes
is_response: bool = False
def build_hci_command(opcode: int, payload: bytes = b"") -> bytes:
"""构建 HCI 命令包。
格式E9 FF [OPCODE(2)] [LEN(2)] [PAYLOAD] [CHECKSUM]
"""
length = len(payload)
cmd = HCI_CMD_PREFIX + opcode.to_bytes(2, "little") + length.to_bytes(2, "little") + payload
# 计算校验和(简单累加)
checksum = sum(cmd) & 0xFF
return cmd + bytes([checksum])
def build_hci_mesh_send(opcode: int, payload: bytes, dst_addr: int = 0xFFFF) -> bytes:
"""构建 Mesh 发送命令。
格式E9 FF 00 30 [LEN] [DST_ADDR(2)] [OPCODE(2)] [PAYLOAD] [CHECKSUM]
"""
inner_payload = dst_addr.to_bytes(2, "little") + opcode.to_bytes(2, "little") + payload
return build_hci_command(HciGatewayOp.MESH_SEND, inner_payload)
def parse_hci_response(data: bytes) -> tuple[int, bytes] | None:
"""解析 HCI 响应数据。
返回:(opcode, payload) 或 None
"""
if len(data) < 7:
return None
# 检查响应头
if data[0] != 0x91:
return None
opcode = int.from_bytes(data[1:3], "little")
payload = data[3:]
return (opcode, payload)
def parse_hci_event(data: bytes) -> tuple[int, bytes] | None:
"""解析 HCI 事件数据。
返回:(opcode, payload) 或 None
"""
if len(data) < 6:
return None
# 检查事件头 E8 FF
if data[0:2] != b"\xe8\xff":
return None
opcode = int.from_bytes(data[2:4], "little")
length = int.from_bytes(data[4:6], "little")
if len(data) < 6 + length:
return None
payload = data[6 : 6 + length]
return (opcode, payload)
class HciGateway:
"""HCI 网关通信类."""
def __init__(self, serial_reader) -> None:
"""初始化 HCI 网关。
Args:
serial_reader: SerialReader 实例
"""
self.serial_reader = serial_reader
self._buffer = bytearray()
self._response_futures: dict[int, asyncio.Future] = {}
async def send_command(self, opcode: int, payload: bytes = b"", timeout: float = 5.0) -> bytes | None:
"""发送 HCI 命令并等待响应。
Args:
opcode: 操作码
payload: 命令数据
timeout: 超时时间(秒)
Returns:
响应数据或 None
"""
cmd = build_hci_command(opcode, payload)
_LOGGER.debug("发送 HCI 命令0x%04X, 数据:%s", opcode, cmd.hex().upper())
# 创建响应 future
loop = asyncio.get_event_loop()
future = loop.create_future()
self._response_futures[opcode] = future
try:
# 发送命令
await self.serial_reader.write(cmd)
# 等待响应
response = await asyncio.wait_for(future, timeout)
_LOGGER.debug("收到 HCI 响应0x%04X, 数据:%s", opcode, response.hex().upper())
return response
except asyncio.TimeoutError:
_LOGGER.warning("HCI 命令超时0x%04X", opcode)
return None
except Exception as e:
_LOGGER.error("HCI 命令失败0x%04X, 错误:%s", opcode, e)
return None
finally:
self._response_futures.pop(opcode, None)
def handle_data(self, data: bytes) -> None:
"""处理接收到的数据。
Args:
data: 原始串口数据
"""
self._buffer.extend(data)
_LOGGER.debug("HCI 接收原始数据:%s", data.hex().upper())
# 尝试解析数据包
while len(self._buffer) >= 7:
# 检查响应头 0x91
if self._buffer[0] == 0x91:
if len(self._buffer) < 4:
break
# 解析响应
opcode = int.from_bytes(self._buffer[1:3], "little")
# 响应数据长度需要根据具体opcode确定
# 简化处理:取剩余所有数据
payload = bytes(self._buffer[3:])
# 唤醒等待的 future
if opcode in self._response_futures:
self._response_futures[opcode].set_result(payload)
self._buffer.clear()
return
# 检查事件头 E8 FF
elif self._buffer[0:2] == b"\xe8\xff":
if len(self._buffer) < 6:
break
length = int.from_bytes(self._buffer[4:6], "little")
if len(self._buffer) < 6 + length:
break
event_data = bytes(self._buffer[: 6 + length])
self._buffer = self._buffer[6 + length :]
_LOGGER.debug("HCI 事件:%s", event_data.hex().upper())
# 事件处理由上层负责
self._handle_event(event_data)
else:
# 未知数据,清空
_LOGGER.warning("未知 HCI 数据头:%s", self._buffer[0:2].hex().upper())
self._buffer.clear()
return
def _handle_event(self, event_data: bytes) -> None:
"""处理 HCI 事件。
Args:
event_data: 事件数据(包含头)
"""
# 由上层回调处理
pass
async def get_version(self) -> str | None:
"""获取网关版本。
Returns:
版本字符串或 None
"""
response = await self.send_command(HciGatewayOp.GATEWAY_VERSION)
if response:
# 解析版本响应
try:
return response.decode("utf-8", errors="ignore").strip()
except Exception:
return response.hex().upper()
return None
async def get_prov_state(self) -> dict | None:
"""获取配网状态。
Returns:
配网状态字典或 None
"""
response = await self.send_command(HciGatewayOp.PROV_GET_STS)
if response:
# 解析配网状态
# 根据 danglo 日志91 8b 00 [state][...]
state_byte = response[0] if len(response) > 0 else 0
return {
"provisioned": state_byte == 0x01,
"raw": response.hex().upper(),
}
return None
async def start_scanning(self) -> bool:
"""开始扫描设备。
Returns:
True 表示成功False 表示失败
"""
response = await self.send_command(HciGatewayOp.PROV_SCAN)
return response is not None
async def stop_scanning(self) -> bool:
"""停止扫描。
Returns:
True 表示成功False 表示失败
"""
response = await self.send_command(HciGatewayOp.PROV_STOP)
return response is not None
async def start_provisioning(self, device_mac: bytes) -> bool:
"""开始配网设备。
Args:
device_mac: 设备 MAC 地址6 字节)
Returns:
True 表示成功False 表示失败
"""
payload = device_mac
response = await self.send_command(HciGatewayOp.PROV_START, payload)
return response is not None
async def configure_netkey(self, netkey: bytes) -> bool:
"""配置网络密钥。
Args:
netkey: 16 字节网络密钥
Returns:
True 表示成功False 表示失败
"""
if len(netkey) != 16:
_LOGGER.error("网络密钥长度错误:%d", len(netkey))
return False
response = await self.send_command(HciGatewayOp.CFG_NETKEY, netkey)
return response is not None
async def configure_appkey(self, appkey: bytes) -> bool:
"""配置应用密钥。
Args:
appkey: 16 字节应用密钥
Returns:
True 表示成功False 表示失败
"""
if len(appkey) != 16:
_LOGGER.error("应用密钥长度错误:%d", len(appkey))
return False
response = await self.send_command(HciGatewayOp.CFG_APPKEY, appkey)
return response is not None
async def mesh_send(self, dst_addr: int, opcode: int, payload: bytes) -> bool:
"""发送 Mesh 消息。
Args:
dst_addr: 目标地址
opcode: 操作码
payload: 数据
Returns:
True 表示成功False 表示失败
"""
cmd = build_hci_mesh_send(opcode, payload, dst_addr)
_LOGGER.debug("发送 Mesh 消息DST=0x%04X, OP=0x%04X", dst_addr, opcode)
try:
await self.serial_reader.write(cmd)
return True
except Exception as e:
_LOGGER.error("发送 Mesh 消息失败:%s", e)
return False