# Copyright 2021-2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Support for Realtek USB dongles. Based on various online bits of information, including the Linux kernel. (see `drivers/bluetooth/btrtl.c`) """ # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- from dataclasses import dataclass, field import asyncio import enum import logging import math import os import pathlib import platform import struct import weakref from bumble import core from bumble import hci from bumble.drivers import common # ----------------------------------------------------------------------------- # Logging # ----------------------------------------------------------------------------- logger = logging.getLogger(__name__) class RtkFirmwareError(core.BaseBumbleError): """Error raised when RTK firmware initialization fails.""" # ----------------------------------------------------------------------------- # Constants # ----------------------------------------------------------------------------- RTK_ROM_LMP_8723A = 0x1200 RTK_ROM_LMP_8723B = 0x8723 RTK_ROM_LMP_8821A = 0x8821 RTK_ROM_LMP_8761A = 0x8761 RTK_ROM_LMP_8822B = 0x8822 RTK_ROM_LMP_8852A = 0x8852 RTK_CONFIG_MAGIC = 0x8723AB55 RTK_EPATCH_SIGNATURE = b"Realtech" RTK_FRAGMENT_LENGTH = 252 RTK_FIRMWARE_DIR_ENV = "BUMBLE_RTK_FIRMWARE_DIR" RTK_LINUX_FIRMWARE_DIR = "/lib/firmware/rtl_bt" class RtlProjectId(enum.IntEnum): PROJECT_ID_8723A = 0 PROJECT_ID_8723B = 1 PROJECT_ID_8821A = 2 PROJECT_ID_8761A = 3 PROJECT_ID_8822B = 8 PROJECT_ID_8723D = 9 PROJECT_ID_8821C = 10 PROJECT_ID_8822C = 13 PROJECT_ID_8761B = 14 PROJECT_ID_8852A = 18 PROJECT_ID_8852B = 20 PROJECT_ID_8852C = 25 RTK_PROJECT_ID_TO_ROM = { 0: RTK_ROM_LMP_8723A, 1: RTK_ROM_LMP_8723B, 2: RTK_ROM_LMP_8821A, 3: RTK_ROM_LMP_8761A, 8: RTK_ROM_LMP_8822B, 9: RTK_ROM_LMP_8723B, 10: RTK_ROM_LMP_8821A, 13: RTK_ROM_LMP_8822B, 14: RTK_ROM_LMP_8761A, 18: RTK_ROM_LMP_8852A, 20: RTK_ROM_LMP_8852A, 25: RTK_ROM_LMP_8852A, } # List of USB (VendorID, ProductID) for Realtek-based devices. RTK_USB_PRODUCTS = { # Realtek 8723AE (0x0930, 0x021D), (0x13D3, 0x3394), # Realtek 8723BE (0x0489, 0xE085), (0x0489, 0xE08B), (0x04F2, 0xB49F), (0x13D3, 0x3410), (0x13D3, 0x3416), (0x13D3, 0x3459), (0x13D3, 0x3494), # Realtek 8723BU (0x7392, 0xA611), # Realtek 8723DE (0x0BDA, 0xB009), (0x2FF8, 0xB011), # Realtek 8761BUV (0x0B05, 0x190E), (0x0BDA, 0x8771), (0x2230, 0x0016), (0x2357, 0x0604), (0x2550, 0x8761), (0x2B89, 0x8761), (0x7392, 0xC611), (0x0BDA, 0x877B), # Realtek 8821AE (0x0B05, 0x17DC), (0x13D3, 0x3414), (0x13D3, 0x3458), (0x13D3, 0x3461), (0x13D3, 0x3462), # Realtek 8821CE (0x0BDA, 0xB00C), (0x0BDA, 0xC822), (0x13D3, 0x3529), # Realtek 8822BE (0x0B05, 0x185C), (0x13D3, 0x3526), # Realtek 8822CE (0x04C5, 0x161F), (0x04CA, 0x4005), (0x0B05, 0x18EF), (0x0BDA, 0xB00C), (0x0BDA, 0xC123), (0x0BDA, 0xC822), (0x0CB5, 0xC547), (0x1358, 0xC123), (0x13D3, 0x3548), (0x13D3, 0x3549), (0x13D3, 0x3553), (0x13D3, 0x3555), (0x2FF8, 0x3051), # Realtek 8822CU (0x13D3, 0x3549), # Realtek 8852AE (0x04C5, 0x165C), (0x04CA, 0x4006), (0x0BDA, 0x2852), (0x0BDA, 0x385A), (0x0BDA, 0x4852), (0x0BDA, 0xC852), (0x0CB8, 0xC549), # Realtek 8852BE (0x0BDA, 0x887B), (0x0CB8, 0xC559), (0x13D3, 0x3571), # Realtek 8852CE (0x04C5, 0x1675), (0x04CA, 0x4007), (0x0CB8, 0xC558), (0x13D3, 0x3586), (0x13D3, 0x3587), (0x13D3, 0x3592), } # ----------------------------------------------------------------------------- # HCI Commands # ----------------------------------------------------------------------------- HCI_RTK_READ_ROM_VERSION_COMMAND = hci.hci_vendor_command_op_code(0x6D) HCI_RTK_DOWNLOAD_COMMAND = hci.hci_vendor_command_op_code(0x20) HCI_RTK_DROP_FIRMWARE_COMMAND = hci.hci_vendor_command_op_code(0x66) hci.HCI_Command.register_commands(globals()) @hci.HCI_Command.command @dataclass class HCI_RTK_Read_ROM_Version_Command(hci.HCI_Command): return_parameters_fields = [("status", hci.STATUS_SPEC), ("version", 1)] @hci.HCI_Command.command @dataclass class HCI_RTK_Download_Command(hci.HCI_Command): index: int = field(metadata=hci.metadata(1)) payload: bytes = field(metadata=hci.metadata(RTK_FRAGMENT_LENGTH)) return_parameters_fields = [("status", hci.STATUS_SPEC), ("index", 1)] @hci.HCI_Command.command @dataclass class HCI_RTK_Drop_Firmware_Command(hci.HCI_Command): pass # ----------------------------------------------------------------------------- class Firmware: def __init__(self, firmware): extension_sig = bytes([0x51, 0x04, 0xFD, 0x77]) if not firmware.startswith(RTK_EPATCH_SIGNATURE): raise RtkFirmwareError("Firmware does not start with epatch signature") if not firmware.endswith(extension_sig): raise RtkFirmwareError("Firmware does not end with extension sig") # The firmware should start with a 14 byte header. epatch_header_size = 14 if len(firmware) < epatch_header_size: raise RtkFirmwareError("Firmware too short") # Look for the "project ID", starting from the end. offset = len(firmware) - len(extension_sig) project_id = -1 while offset >= epatch_header_size: length, opcode = firmware[offset - 2 : offset] offset -= 2 if opcode == 0xFF: # End break if length == 0: raise RtkFirmwareError("Invalid 0-length instruction") if opcode == 0 and length == 1: project_id = firmware[offset - 1] break offset -= length if project_id < 0: raise RtkFirmwareError("Project ID not found") self.project_id = project_id # Read the patch tables info. self.version, num_patches = struct.unpack("... (16 bits each) # ... (16 bits each) # ... (32 bits each) if epatch_header_size + 8 * num_patches > len(firmware): raise RtkFirmwareError("Firmware too short") chip_id_table_offset = epatch_header_size patch_length_table_offset = chip_id_table_offset + 2 * num_patches patch_offset_table_offset = chip_id_table_offset + 4 * num_patches for patch_index in range(num_patches): chip_id_offset = chip_id_table_offset + 2 * patch_index (chip_id,) = struct.unpack_from(" len(firmware): raise RtkFirmwareError("Firmware too short") # Get the SVN version for the patch (svn_version,) = struct.unpack_from( "= 0x80: download_index += 1 if fragment_index == fragment_count - 1: download_index |= 0x80 # End marker. fragment_offset = fragment_index * RTK_FRAGMENT_LENGTH fragment = payload[fragment_offset : fragment_offset + RTK_FRAGMENT_LENGTH] logger.debug(f"downloading fragment {fragment_index}") await self.host.send_command( HCI_RTK_Download_Command(index=download_index, payload=fragment), check_result=True, ) logger.debug("download complete!") # Read the version again response = await self.host.send_command( HCI_RTK_Read_ROM_Version_Command(), check_result=True ) if response.return_parameters.status != hci.HCI_SUCCESS: logger.warning("can't get ROM version") else: rom_version = response.return_parameters.version logger.debug(f"ROM version after download: {rom_version:04X}") async def download_firmware(self): if self.driver_info.rom == RTK_ROM_LMP_8723A: return await self.download_for_rtl8723a() if self.driver_info.rom in ( RTK_ROM_LMP_8723B, RTK_ROM_LMP_8821A, RTK_ROM_LMP_8761A, RTK_ROM_LMP_8822B, RTK_ROM_LMP_8852A, ): return await self.download_for_rtl8723b() raise RtkFirmwareError("ROM not supported") async def init_controller(self): await self.download_firmware() await self.host.send_command(hci.HCI_Reset_Command(), check_result=True) logger.info(f"loaded FW image {self.driver_info.fw_name}") def rtk_firmware_dir() -> pathlib.Path: """ Returns: A path to a subdir of the project data dir for Realtek firmware. The directory is created if it doesn't exist. """ from bumble.drivers import project_data_dir p = project_data_dir() / "firmware" / "realtek" p.mkdir(parents=True, exist_ok=True) return p