"""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