mirror of
https://github.com/google/bumble.git
synced 2026-04-16 00:25:31 +00:00
68
bumble/drivers/__init__.py
Normal file
68
bumble/drivers/__init__.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# Copyright 2021-2022 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.
|
||||
"""
|
||||
Drivers that can be used to customize the interaction between a host and a controller,
|
||||
like loading firmware after a cold start.
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import abc
|
||||
import logging
|
||||
from . import rtk
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
class Driver(abc.ABC):
|
||||
"""Base class for drivers."""
|
||||
|
||||
@staticmethod
|
||||
async def for_host(_host):
|
||||
"""Return a driver instance for a host.
|
||||
|
||||
Args:
|
||||
host: Host object for which a driver should be created.
|
||||
|
||||
Returns:
|
||||
A Driver instance if a driver should be instantiated for this host, or
|
||||
None if no driver instance of this class is needed.
|
||||
"""
|
||||
return None
|
||||
|
||||
@abc.abstractmethod
|
||||
async def init_controller(self):
|
||||
"""Initialize the controller."""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_driver_for_host(host):
|
||||
"""Probe all known diver classes until one returns a valid instance for a host,
|
||||
or none is found.
|
||||
"""
|
||||
if driver := await rtk.Driver.for_host(host):
|
||||
logger.debug("Instantiated RTK driver")
|
||||
return driver
|
||||
|
||||
return None
|
||||
647
bumble/drivers/rtk.py
Normal file
647
bumble/drivers/rtk.py
Normal file
@@ -0,0 +1,647 @@
|
||||
# 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
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import struct
|
||||
from typing import Tuple
|
||||
import weakref
|
||||
|
||||
|
||||
from bumble.hci import (
|
||||
hci_command_op_code,
|
||||
STATUS_SPEC,
|
||||
HCI_SUCCESS,
|
||||
HCI_COMMAND_NAMES,
|
||||
HCI_Command,
|
||||
HCI_Reset_Command,
|
||||
HCI_Read_Local_Version_Information_Command,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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),
|
||||
# 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_command_op_code(0x3F, 0x6D)
|
||||
HCI_COMMAND_NAMES[HCI_RTK_READ_ROM_VERSION_COMMAND] = "HCI_RTK_READ_ROM_VERSION_COMMAND"
|
||||
|
||||
|
||||
@HCI_Command.command(return_parameters_fields=[("status", STATUS_SPEC), ("version", 1)])
|
||||
class HCI_RTK_Read_ROM_Version_Command(HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
HCI_RTK_DOWNLOAD_COMMAND = hci_command_op_code(0x3F, 0x20)
|
||||
HCI_COMMAND_NAMES[HCI_RTK_DOWNLOAD_COMMAND] = "HCI_RTK_DOWNLOAD_COMMAND"
|
||||
|
||||
|
||||
@HCI_Command.command(
|
||||
fields=[("index", 1), ("payload", RTK_FRAGMENT_LENGTH)],
|
||||
return_parameters_fields=[("status", STATUS_SPEC), ("index", 1)],
|
||||
)
|
||||
class HCI_RTK_Download_Command(HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
HCI_RTK_DROP_FIRMWARE_COMMAND = hci_command_op_code(0x3F, 0x66)
|
||||
HCI_COMMAND_NAMES[HCI_RTK_DROP_FIRMWARE_COMMAND] = "HCI_RTK_DROP_FIRMWARE_COMMAND"
|
||||
|
||||
|
||||
@HCI_Command.command()
|
||||
class HCI_RTK_Drop_Firmware_Command(HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Firmware:
|
||||
def __init__(self, firmware):
|
||||
extension_sig = bytes([0x51, 0x04, 0xFD, 0x77])
|
||||
|
||||
if not firmware.startswith(RTK_EPATCH_SIGNATURE):
|
||||
raise ValueError("Firmware does not start with epatch signature")
|
||||
|
||||
if not firmware.endswith(extension_sig):
|
||||
raise ValueError("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 ValueError("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 ValueError("Invalid 0-length instruction")
|
||||
|
||||
if opcode == 0 and length == 1:
|
||||
project_id = firmware[offset - 1]
|
||||
break
|
||||
|
||||
offset -= length
|
||||
|
||||
if project_id < 0:
|
||||
raise ValueError("Project ID not found")
|
||||
|
||||
self.project_id = project_id
|
||||
|
||||
# Read the patch tables info.
|
||||
self.version, num_patches = struct.unpack("<IH", firmware[8:14])
|
||||
self.patches = []
|
||||
|
||||
# The patches tables are laid out as:
|
||||
# <ChipID_1><ChipID_2>...<ChipID_N> (16 bits each)
|
||||
# <PatchLength_1><PatchLength_2>...<PatchLength_N> (16 bits each)
|
||||
# <PatchOffset_1><PatchOffset_2>...<PatchOffset_N> (32 bits each)
|
||||
if epatch_header_size + 8 * num_patches > len(firmware):
|
||||
raise ValueError("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("<H", firmware, chip_id_offset)
|
||||
(patch_length,) = struct.unpack_from(
|
||||
"<H", firmware, patch_length_table_offset + 2 * patch_index
|
||||
)
|
||||
(patch_offset,) = struct.unpack_from(
|
||||
"<I", firmware, patch_offset_table_offset + 4 * patch_index
|
||||
)
|
||||
if patch_offset + patch_length > len(firmware):
|
||||
raise ValueError("Firmware too short")
|
||||
|
||||
# Get the SVN version for the patch
|
||||
(svn_version,) = struct.unpack_from(
|
||||
"<I", firmware, patch_offset + patch_length - 8
|
||||
)
|
||||
|
||||
# Create a payload with the patch, replacing the last 4 bytes with
|
||||
# the firmware version.
|
||||
self.patches.append(
|
||||
(
|
||||
chip_id,
|
||||
firmware[patch_offset : patch_offset + patch_length - 4]
|
||||
+ struct.pack("<I", self.version),
|
||||
svn_version,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Driver:
|
||||
@dataclass
|
||||
class DriverInfo:
|
||||
rom: int
|
||||
hci: Tuple[int, int]
|
||||
config_needed: bool
|
||||
has_rom_version: bool
|
||||
has_msft_ext: bool = False
|
||||
fw_name: str = ""
|
||||
config_name: str = ""
|
||||
|
||||
DRIVER_INFOS = [
|
||||
# 8723A
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8723A,
|
||||
hci=(0x0B, 0x06),
|
||||
config_needed=False,
|
||||
has_rom_version=False,
|
||||
fw_name="rtl8723a_fw.bin",
|
||||
config_name="",
|
||||
),
|
||||
# 8723B
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8723B,
|
||||
hci=(0x0B, 0x06),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
fw_name="rtl8723b_fw.bin",
|
||||
config_name="rtl8723b_config.bin",
|
||||
),
|
||||
# 8723D
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8723B,
|
||||
hci=(0x0D, 0x08),
|
||||
config_needed=True,
|
||||
has_rom_version=True,
|
||||
fw_name="rtl8723d_fw.bin",
|
||||
config_name="rtl8723d_config.bin",
|
||||
),
|
||||
# 8821A
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8821A,
|
||||
hci=(0x0A, 0x06),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
fw_name="rtl8821a_fw.bin",
|
||||
config_name="rtl8821a_config.bin",
|
||||
),
|
||||
# 8821C
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8821A,
|
||||
hci=(0x0C, 0x08),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
has_msft_ext=True,
|
||||
fw_name="rtl8821c_fw.bin",
|
||||
config_name="rtl8821c_config.bin",
|
||||
),
|
||||
# 8761A
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8761A,
|
||||
hci=(0x0A, 0x06),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
fw_name="rtl8761a_fw.bin",
|
||||
config_name="rtl8761a_config.bin",
|
||||
),
|
||||
# 8761BU
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8761A,
|
||||
hci=(0x0B, 0x0A),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
fw_name="rtl8761bu_fw.bin",
|
||||
config_name="rtl8761bu_config.bin",
|
||||
),
|
||||
# 8822C
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8822B,
|
||||
hci=(0x0C, 0x0A),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
has_msft_ext=True,
|
||||
fw_name="rtl8822cu_fw.bin",
|
||||
config_name="rtl8822cu_config.bin",
|
||||
),
|
||||
# 8822B
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8822B,
|
||||
hci=(0x0B, 0x07),
|
||||
config_needed=True,
|
||||
has_rom_version=True,
|
||||
has_msft_ext=True,
|
||||
fw_name="rtl8822b_fw.bin",
|
||||
config_name="rtl8822b_config.bin",
|
||||
),
|
||||
# 8852A
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8852A,
|
||||
hci=(0x0A, 0x0B),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
has_msft_ext=True,
|
||||
fw_name="rtl8852au_fw.bin",
|
||||
config_name="rtl8852au_config.bin",
|
||||
),
|
||||
# 8852B
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8852A,
|
||||
hci=(0xB, 0xB),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
has_msft_ext=True,
|
||||
fw_name="rtl8852bu_fw.bin",
|
||||
config_name="rtl8852bu_config.bin",
|
||||
),
|
||||
# 8852C
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8852A,
|
||||
hci=(0x0C, 0x0C),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
has_msft_ext=True,
|
||||
fw_name="rtl8852cu_fw.bin",
|
||||
config_name="rtl8852cu_config.bin",
|
||||
),
|
||||
]
|
||||
|
||||
POST_DROP_DELAY = 0.2
|
||||
|
||||
@staticmethod
|
||||
def find_driver_info(hci_version, hci_subversion, lmp_subversion):
|
||||
for driver_info in Driver.DRIVER_INFOS:
|
||||
if driver_info.rom == lmp_subversion and driver_info.hci == (
|
||||
hci_subversion,
|
||||
hci_version,
|
||||
):
|
||||
return driver_info
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def find_binary_path(file_name):
|
||||
# First check if an environment variable is set
|
||||
if RTK_FIRMWARE_DIR_ENV in os.environ:
|
||||
if (
|
||||
path := pathlib.Path(os.environ[RTK_FIRMWARE_DIR_ENV]) / file_name
|
||||
).is_file():
|
||||
logger.debug(f"{file_name} found in env dir")
|
||||
return path
|
||||
|
||||
# When the environment variable is set, don't look elsewhere
|
||||
return None
|
||||
|
||||
# Then, look in the package's driver directory
|
||||
if (path := pathlib.Path(__file__).parent / "rtk_fw" / file_name).is_file():
|
||||
logger.debug(f"{file_name} found in package dir")
|
||||
return path
|
||||
|
||||
# On Linux, check the system's FW directory
|
||||
if (
|
||||
platform.system() == "Linux"
|
||||
and (path := pathlib.Path(RTK_LINUX_FIRMWARE_DIR) / file_name).is_file()
|
||||
):
|
||||
logger.debug(f"{file_name} found in Linux system FW dir")
|
||||
return path
|
||||
|
||||
# Finally look in the current directory
|
||||
if (path := pathlib.Path.cwd() / file_name).is_file():
|
||||
logger.debug(f"{file_name} found in CWD")
|
||||
return path
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def check(host):
|
||||
if not host.hci_metadata:
|
||||
logger.debug("USB metadata not found")
|
||||
return False
|
||||
|
||||
vendor_id = host.hci_metadata.get("vendor_id", None)
|
||||
product_id = host.hci_metadata.get("product_id", None)
|
||||
if vendor_id is None or product_id is None:
|
||||
logger.debug("USB metadata not sufficient")
|
||||
return False
|
||||
|
||||
if (vendor_id, product_id) not in RTK_USB_PRODUCTS:
|
||||
logger.debug(
|
||||
f"USB device ({vendor_id:04X}, {product_id:04X}) " "not in known list"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def driver_info_for_host(cls, host):
|
||||
response = await host.send_command(
|
||||
HCI_Read_Local_Version_Information_Command(), check_result=True
|
||||
)
|
||||
local_version = response.return_parameters
|
||||
|
||||
logger.debug(
|
||||
f"looking for a driver: 0x{local_version.lmp_subversion:04X} "
|
||||
f"(0x{local_version.hci_version:02X}, "
|
||||
f"0x{local_version.hci_subversion:04X})"
|
||||
)
|
||||
|
||||
driver_info = cls.find_driver_info(
|
||||
local_version.hci_version,
|
||||
local_version.hci_subversion,
|
||||
local_version.lmp_subversion,
|
||||
)
|
||||
if driver_info is None:
|
||||
# TODO: it seems that the Linux driver will send command (0x3f, 0x66)
|
||||
# in this case and then re-read the local version, then re-match.
|
||||
logger.debug("firmware already loaded or no known driver for this device")
|
||||
|
||||
return driver_info
|
||||
|
||||
@classmethod
|
||||
async def for_host(cls, host, force=False):
|
||||
# Check that a driver is needed for this host
|
||||
if not force and not cls.check(host):
|
||||
return None
|
||||
|
||||
# Get the driver info
|
||||
driver_info = await cls.driver_info_for_host(host)
|
||||
if driver_info is None:
|
||||
return None
|
||||
|
||||
# Load the firmware
|
||||
firmware_path = cls.find_binary_path(driver_info.fw_name)
|
||||
if not firmware_path:
|
||||
logger.warning(f"Firmware file {driver_info.fw_name} not found")
|
||||
logger.warning("See https://google.github.io/bumble/drivers/realtek.html")
|
||||
return None
|
||||
with open(firmware_path, "rb") as firmware_file:
|
||||
firmware = firmware_file.read()
|
||||
|
||||
# Load the config
|
||||
config = None
|
||||
if driver_info.config_name:
|
||||
config_path = cls.find_binary_path(driver_info.config_name)
|
||||
if config_path:
|
||||
with open(config_path, "rb") as config_file:
|
||||
config = config_file.read()
|
||||
if driver_info.config_needed and not config:
|
||||
logger.warning("Config needed, but no config file available")
|
||||
return None
|
||||
|
||||
return cls(host, driver_info, firmware, config)
|
||||
|
||||
def __init__(self, host, driver_info, firmware, config):
|
||||
self.host = weakref.proxy(host)
|
||||
self.driver_info = driver_info
|
||||
self.firmware = firmware
|
||||
self.config = config
|
||||
|
||||
@staticmethod
|
||||
async def drop_firmware(host):
|
||||
host.send_hci_packet(HCI_RTK_Drop_Firmware_Command())
|
||||
|
||||
# Wait for the command to be effective (no response is sent)
|
||||
await asyncio.sleep(Driver.POST_DROP_DELAY)
|
||||
|
||||
async def download_for_rtl8723a(self):
|
||||
# Check that the firmware image does not include an epatch signature.
|
||||
if RTK_EPATCH_SIGNATURE in self.firmware:
|
||||
logger.warning(
|
||||
"epatch signature found in firmware, it is probably the wrong firmware"
|
||||
)
|
||||
return
|
||||
|
||||
# TODO: load the firmware
|
||||
|
||||
async def download_for_rtl8723b(self):
|
||||
if self.driver_info.has_rom_version:
|
||||
response = await self.host.send_command(
|
||||
HCI_RTK_Read_ROM_Version_Command(), check_result=True
|
||||
)
|
||||
if response.return_parameters.status != HCI_SUCCESS:
|
||||
logger.warning("can't get ROM version")
|
||||
return
|
||||
rom_version = response.return_parameters.version
|
||||
logger.debug(f"ROM version before download: {rom_version:04X}")
|
||||
else:
|
||||
rom_version = 0
|
||||
|
||||
firmware = Firmware(self.firmware)
|
||||
logger.debug(f"firmware: project_id=0x{firmware.project_id:04X}")
|
||||
for patch in firmware.patches:
|
||||
if patch[0] == rom_version + 1:
|
||||
logger.debug(f"using patch {patch[0]}")
|
||||
break
|
||||
else:
|
||||
logger.warning("no valid patch found for rom version {rom_version}")
|
||||
return
|
||||
|
||||
# Append the config if there is one.
|
||||
if self.config:
|
||||
payload = patch[1] + self.config
|
||||
else:
|
||||
payload = patch[1]
|
||||
|
||||
# Download the payload, one fragment at a time.
|
||||
fragment_count = math.ceil(len(payload) / RTK_FRAGMENT_LENGTH)
|
||||
for fragment_index in range(fragment_count):
|
||||
# NOTE: the Linux driver somehow adds 1 to the index after it wraps around.
|
||||
# That's odd, but we"ll do the same here.
|
||||
download_index = fragment_index & 0x7F
|
||||
if download_index >= 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_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 ValueError("ROM not supported")
|
||||
|
||||
async def init_controller(self):
|
||||
await self.download_firmware()
|
||||
await self.host.send_command(HCI_Reset_Command(), check_result=True)
|
||||
logger.info(f"loaded FW image {self.driver_info.fw_name}")
|
||||
@@ -1641,9 +1641,11 @@ class HCI_Object:
|
||||
# Get the value for the field
|
||||
value = hci_object[key]
|
||||
|
||||
# Map the value if needed
|
||||
# Check if there's a matching mapper passed
|
||||
if value_mappers:
|
||||
value_mapper = value_mappers.get(key, value_mapper)
|
||||
|
||||
# Map the value if we have a mapper
|
||||
if value_mapper is not None:
|
||||
value = value_mapper(value)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import struct
|
||||
from bumble.colors import color
|
||||
from bumble.l2cap import L2CAP_PDU
|
||||
from bumble.snoop import Snooper
|
||||
from bumble import drivers
|
||||
|
||||
from typing import Optional
|
||||
|
||||
@@ -116,6 +117,7 @@ class Host(AbortableEventEmitter):
|
||||
super().__init__()
|
||||
|
||||
self.hci_sink = None
|
||||
self.hci_metadata = None
|
||||
self.ready = False # True when we can accept incoming packets
|
||||
self.reset_done = False
|
||||
self.connections = {} # Connections, by connection handle
|
||||
@@ -141,6 +143,9 @@ class Host(AbortableEventEmitter):
|
||||
# Connect to the source and sink if specified
|
||||
if controller_source:
|
||||
controller_source.set_packet_sink(self)
|
||||
self.hci_metadata = getattr(
|
||||
controller_source, 'metadata', self.hci_metadata
|
||||
)
|
||||
if controller_sink:
|
||||
self.set_packet_sink(controller_sink)
|
||||
|
||||
@@ -170,7 +175,7 @@ class Host(AbortableEventEmitter):
|
||||
self.emit('flush')
|
||||
self.command_semaphore.release()
|
||||
|
||||
async def reset(self):
|
||||
async def reset(self, driver_factory=drivers.get_driver_for_host):
|
||||
if self.ready:
|
||||
self.ready = False
|
||||
await self.flush()
|
||||
@@ -178,6 +183,15 @@ class Host(AbortableEventEmitter):
|
||||
await self.send_command(HCI_Reset_Command(), check_result=True)
|
||||
self.ready = True
|
||||
|
||||
# Instantiate and init a driver for the host if needed.
|
||||
# NOTE: we don't keep a reference to the driver here, because we don't
|
||||
# currently have a need for the driver later on. But if the driver interface
|
||||
# evolves, it may be required, then, to store a reference to the driver in
|
||||
# an object property.
|
||||
if driver_factory is not None:
|
||||
if driver := await driver_factory(self):
|
||||
await driver.init_controller()
|
||||
|
||||
response = await self.send_command(
|
||||
HCI_Read_Local_Supported_Commands_Command(), check_result=True
|
||||
)
|
||||
@@ -298,7 +312,7 @@ class Host(AbortableEventEmitter):
|
||||
if self.snooper:
|
||||
self.snooper.snoop(bytes(packet), Snooper.Direction.HOST_TO_CONTROLLER)
|
||||
|
||||
self.hci_sink.on_packet(packet.to_bytes())
|
||||
self.hci_sink.on_packet(bytes(packet))
|
||||
|
||||
async def send_command(self, command, check_result=False):
|
||||
logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: {command}')
|
||||
@@ -350,7 +364,7 @@ class Host(AbortableEventEmitter):
|
||||
asyncio.create_task(send_command(command))
|
||||
|
||||
def send_l2cap_pdu(self, connection_handle, cid, pdu):
|
||||
l2cap_pdu = L2CAP_PDU(cid, pdu).to_bytes()
|
||||
l2cap_pdu = bytes(L2CAP_PDU(cid, pdu))
|
||||
|
||||
# Send the data to the controller via ACL packets
|
||||
bytes_remaining = len(l2cap_pdu)
|
||||
|
||||
@@ -206,10 +206,11 @@ async def open_usb_transport(spec):
|
||||
logger.debug('OUT transfer likely already completed')
|
||||
|
||||
class UsbPacketSource(asyncio.Protocol, ParserSource):
|
||||
def __init__(self, context, device, acl_in, events_in):
|
||||
def __init__(self, context, device, metadata, acl_in, events_in):
|
||||
super().__init__()
|
||||
self.context = context
|
||||
self.device = device
|
||||
self.metadata = metadata
|
||||
self.acl_in = acl_in
|
||||
self.events_in = events_in
|
||||
self.loop = asyncio.get_running_loop()
|
||||
@@ -510,6 +511,10 @@ async def open_usb_transport(spec):
|
||||
f'events_in=0x{events_in:02X}, '
|
||||
)
|
||||
|
||||
device_metadata = {
|
||||
'vendor_id': found.getVendorID(),
|
||||
'product_id': found.getProductID(),
|
||||
}
|
||||
device = found.open()
|
||||
|
||||
# Auto-detach the kernel driver if supported
|
||||
@@ -535,7 +540,7 @@ async def open_usb_transport(spec):
|
||||
except usb1.USBError:
|
||||
logger.warning('failed to set configuration')
|
||||
|
||||
source = UsbPacketSource(context, device, acl_in, events_in)
|
||||
source = UsbPacketSource(context, device, device_metadata, acl_in, events_in)
|
||||
sink = UsbPacketSink(device, acl_out)
|
||||
return UsbTransport(context, device, interface, setting, source, sink)
|
||||
except usb1.USBError as error:
|
||||
|
||||
@@ -36,6 +36,9 @@ nav:
|
||||
- HCI Socket: transports/hci_socket.md
|
||||
- Android Emulator: transports/android_emulator.md
|
||||
- File: transports/file.md
|
||||
- Drivers:
|
||||
- Overview: drivers/index.md
|
||||
- Realtek: drivers/realtek.md
|
||||
- API:
|
||||
- Guide: api/guide.md
|
||||
- Examples: api/examples.md
|
||||
|
||||
10
docs/mkdocs/src/drivers/index.md
Normal file
10
docs/mkdocs/src/drivers/index.md
Normal file
@@ -0,0 +1,10 @@
|
||||
DRIVERS
|
||||
=======
|
||||
|
||||
Some Bluetooth controllers require a driver to function properly.
|
||||
This may include, for instance, loading a Firmware image or patch,
|
||||
loading a configuration.
|
||||
|
||||
Drivers included in the module are:
|
||||
|
||||
* [Realtek](realtek.md): Loading of Firmware and Config for Realtek USB dongles.
|
||||
62
docs/mkdocs/src/drivers/realtek.md
Normal file
62
docs/mkdocs/src/drivers/realtek.md
Normal file
@@ -0,0 +1,62 @@
|
||||
REALTEK DRIVER
|
||||
==============
|
||||
|
||||
This driver supports loading firmware images and optional config data to
|
||||
USB dongles with a Realtek chipset.
|
||||
A number of USB dongles are supported, but likely not all.
|
||||
When using a USB dongle, the USB product ID and manufacturer ID are used
|
||||
to find whether a matching set of firmware image and config data
|
||||
is needed for that specific model. If a match exists, the driver will try
|
||||
load the firmware image and, if needed, config data.
|
||||
The driver will look for those files by name, in order, in:
|
||||
|
||||
* The directory specified by the environment variable `BUMBLE_RTK_FIRMWARE_DIR`
|
||||
if set.
|
||||
* The directory `<package-dir>/drivers/rtk_fw` where `<package-dir>` is the directory
|
||||
where the `bumble` package is installed.
|
||||
* The current directory.
|
||||
|
||||
|
||||
Obtaining Firmware Images and Config Data
|
||||
-----------------------------------------
|
||||
|
||||
Firmware images and config data may be obtained from a variety of online
|
||||
sources.
|
||||
To facilitate finding a downloading the, the utility program `bumble-rtk-fw-download`
|
||||
may be used.
|
||||
|
||||
```
|
||||
Usage: bumble-rtk-fw-download [OPTIONS]
|
||||
|
||||
Download RTK firmware images and configs.
|
||||
|
||||
Options:
|
||||
--output-dir TEXT Output directory where the files will be
|
||||
saved [default: .]
|
||||
--source [linux-kernel|realtek-opensource|linux-from-scratch]
|
||||
[default: linux-kernel]
|
||||
--single TEXT Only download a single image set, by its
|
||||
base name
|
||||
--force Overwrite files if they already exist
|
||||
--parse Parse the FW image after saving
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
Utility
|
||||
-------
|
||||
|
||||
The `bumble-rtk-util` utility may be used to interact with a Realtek USB dongle
|
||||
and/or firmware images.
|
||||
|
||||
```
|
||||
Usage: bumble-rtk-util [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Options:
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
drop Drop a firmware image from the USB dongle.
|
||||
info Get the firmware info from a USB dongle.
|
||||
load Load a firmware image into the USB dongle.
|
||||
parse Parse a firmware image.
|
||||
```
|
||||
@@ -24,11 +24,12 @@ url = https://github.com/google/bumble
|
||||
|
||||
[options]
|
||||
python_requires = >=3.8
|
||||
packages = bumble, bumble.transport, bumble.profiles, bumble.apps, bumble.apps.link_relay, bumble.pandora
|
||||
packages = bumble, bumble.transport, bumble.drivers, bumble.profiles, bumble.apps, bumble.apps.link_relay, bumble.pandora, bumble.tools
|
||||
package_dir =
|
||||
bumble = bumble
|
||||
bumble.apps = apps
|
||||
include-package-data = True
|
||||
bumble.tools = tools
|
||||
include_package_data = True
|
||||
install_requires =
|
||||
aiohttp ~= 3.8; platform_system!='Emscripten'
|
||||
appdirs >= 1.4
|
||||
@@ -64,6 +65,8 @@ console_scripts =
|
||||
bumble-bench = bumble.apps.bench:main
|
||||
bumble-speaker = bumble.apps.speaker.speaker:main
|
||||
bumble-pandora-server = bumble.apps.pandora_server:main
|
||||
bumble-rtk-util = bumble.tools.rtk_util:main
|
||||
bumble-rtk-fw-download = bumble.tools.rtk_fw_download:main
|
||||
|
||||
[options.package_data]
|
||||
* = py.typed, *.pyi
|
||||
|
||||
0
tools/__init__.py
Normal file
0
tools/__init__.py
Normal file
149
tools/rtk_fw_download.py
Normal file
149
tools/rtk_fw_download.py
Normal file
@@ -0,0 +1,149 @@
|
||||
# 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.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import pathlib
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
import click
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.drivers import rtk
|
||||
from bumble.tools import rtk_util
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
LINUX_KERNEL_GIT_SOURCE = (
|
||||
"https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/plain/rtl_bt",
|
||||
False,
|
||||
)
|
||||
REALTEK_OPENSOURCE_SOURCE = (
|
||||
"https://github.com/Realtek-OpenSource/android_hardware_realtek/raw/rtk1395/bt/rtkbt/Firmware/BT",
|
||||
True,
|
||||
)
|
||||
LINUX_FROM_SCRATCH_SOURCE = (
|
||||
"https://anduin.linuxfromscratch.org/sources/linux-firmware/rtl_bt",
|
||||
False,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
def download_file(base_url, name, remove_suffix):
|
||||
if remove_suffix:
|
||||
name = name.replace(".bin", "")
|
||||
|
||||
url = f"{base_url}/{name}"
|
||||
with urllib.request.urlopen(url) as file:
|
||||
data = file.read()
|
||||
print(f"Downloaded {name}: {len(data)} bytes")
|
||||
return data
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@click.command
|
||||
@click.option(
|
||||
"--output-dir",
|
||||
default=".",
|
||||
help="Output directory where the files will be saved",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--source",
|
||||
type=click.Choice(["linux-kernel", "realtek-opensource", "linux-from-scratch"]),
|
||||
default="linux-kernel",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option("--single", help="Only download a single image set, by its base name")
|
||||
@click.option("--force", is_flag=True, help="Overwrite files if they already exist")
|
||||
@click.option("--parse", is_flag=True, help="Parse the FW image after saving")
|
||||
def main(output_dir, source, single, force, parse):
|
||||
"""Download RTK firmware images and configs."""
|
||||
|
||||
# Check that the output dir exists
|
||||
output_dir = pathlib.Path(output_dir)
|
||||
if not output_dir.is_dir():
|
||||
print("Output dir does not exist or is not a directory")
|
||||
return
|
||||
|
||||
base_url, remove_suffix = {
|
||||
"linux-kernel": LINUX_KERNEL_GIT_SOURCE,
|
||||
"realtek-opensource": REALTEK_OPENSOURCE_SOURCE,
|
||||
"linux-from-scratch": LINUX_FROM_SCRATCH_SOURCE,
|
||||
}[source]
|
||||
|
||||
print("Downloading")
|
||||
print(color("FROM:", "green"), base_url)
|
||||
print(color("TO:", "green"), output_dir)
|
||||
|
||||
if single:
|
||||
images = [(f"{single}_fw.bin", f"{single}_config.bin", True)]
|
||||
else:
|
||||
images = [
|
||||
(driver_info.fw_name, driver_info.config_name, driver_info.config_needed)
|
||||
for driver_info in rtk.Driver.DRIVER_INFOS
|
||||
]
|
||||
|
||||
for (fw_name, config_name, config_needed) in images:
|
||||
print(color("---", "yellow"))
|
||||
fw_image_out = output_dir / fw_name
|
||||
if not force and fw_image_out.exists():
|
||||
print(color(f"{fw_image_out} already exists, skipping", "red"))
|
||||
continue
|
||||
if config_name:
|
||||
config_image_out = output_dir / config_name
|
||||
if not force and config_image_out.exists():
|
||||
print(color("f{config_out} already exists, skipping", "red"))
|
||||
continue
|
||||
|
||||
try:
|
||||
fw_image = download_file(base_url, fw_name, remove_suffix)
|
||||
except urllib.error.HTTPError as error:
|
||||
print(f"Failed to download {fw_name}: {error}")
|
||||
continue
|
||||
|
||||
config_image = None
|
||||
if config_name:
|
||||
try:
|
||||
config_image = download_file(base_url, config_name, remove_suffix)
|
||||
except urllib.error.HTTPError as error:
|
||||
if config_needed:
|
||||
print(f"Failed to download {config_name}: {error}")
|
||||
continue
|
||||
else:
|
||||
print(f"No config available as {config_name}")
|
||||
|
||||
fw_image_out.write_bytes(fw_image)
|
||||
if parse and config_name:
|
||||
print(color("Parsing:", "cyan"), fw_name)
|
||||
rtk_util.do_parse(fw_image_out)
|
||||
if config_image:
|
||||
config_image_out.write_bytes(config_image)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
161
tools/rtk_util.py
Normal file
161
tools/rtk_util.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# 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.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import click
|
||||
|
||||
from bumble import transport
|
||||
from bumble.host import Host
|
||||
from bumble.drivers import rtk
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def do_parse(firmware_path):
|
||||
with open(firmware_path, 'rb') as firmware_file:
|
||||
firmware_data = firmware_file.read()
|
||||
firmware = rtk.Firmware(firmware_data)
|
||||
print(
|
||||
f"Firmware: version=0x{firmware.version:08X} "
|
||||
f"project_id=0x{firmware.project_id:04X}"
|
||||
)
|
||||
for patch in firmware.patches:
|
||||
print(
|
||||
f" Patch: chip_id=0x{patch[0]:04X}, "
|
||||
f"{len(patch[1])} bytes, "
|
||||
f"SVN Version={patch[2]:08X}"
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def do_load(usb_transport, force):
|
||||
async with await transport.open_transport_or_link(usb_transport) as (
|
||||
hci_source,
|
||||
hci_sink,
|
||||
):
|
||||
# Create a host to communicate with the device
|
||||
host = Host(hci_source, hci_sink)
|
||||
await host.reset(driver_factory=None)
|
||||
|
||||
# Get the driver.
|
||||
driver = await rtk.Driver.for_host(host, force)
|
||||
if driver is None:
|
||||
if not force:
|
||||
print("Firmware already loaded or no supported driver for this device.")
|
||||
return
|
||||
|
||||
await driver.download_firmware()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def do_drop(usb_transport):
|
||||
async with await transport.open_transport_or_link(usb_transport) as (
|
||||
hci_source,
|
||||
hci_sink,
|
||||
):
|
||||
# Create a host to communicate with the device
|
||||
host = Host(hci_source, hci_sink)
|
||||
await host.reset(driver_factory=None)
|
||||
|
||||
# Tell the device to reset/drop any loaded patch
|
||||
await rtk.Driver.drop_firmware(host)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def do_info(usb_transport, force):
|
||||
async with await transport.open_transport(usb_transport) as (
|
||||
hci_source,
|
||||
hci_sink,
|
||||
):
|
||||
# Create a host to communicate with the device
|
||||
host = Host(hci_source, hci_sink)
|
||||
await host.reset(driver_factory=None)
|
||||
|
||||
# Check if this is a supported device.
|
||||
if not force and not rtk.Driver.check(host):
|
||||
print("USB device not supported by this RTK driver")
|
||||
return
|
||||
|
||||
# Get the driver info.
|
||||
driver_info = await rtk.Driver.driver_info_for_host(host)
|
||||
if driver_info:
|
||||
print(
|
||||
"Driver:\n"
|
||||
f" ROM: {driver_info.rom:04X}\n"
|
||||
f" Firmware: {driver_info.fw_name}\n"
|
||||
f" Config: {driver_info.config_name}\n"
|
||||
)
|
||||
else:
|
||||
print("Firmware already loaded or no supported driver for this device.")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@click.group()
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
|
||||
|
||||
@main.command
|
||||
@click.argument("firmware_path")
|
||||
def parse(firmware_path):
|
||||
"""Parse a firmware image."""
|
||||
do_parse(firmware_path)
|
||||
|
||||
|
||||
@main.command
|
||||
@click.argument("usb_transport")
|
||||
@click.option(
|
||||
"--force",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Load even if the USB info doesn't match",
|
||||
)
|
||||
def load(usb_transport, force):
|
||||
"""Load a firmware image into the USB dongle."""
|
||||
asyncio.run(do_load(usb_transport, force))
|
||||
|
||||
|
||||
@main.command
|
||||
@click.argument("usb_transport")
|
||||
def drop(usb_transport):
|
||||
"""Drop a firmware image from the USB dongle."""
|
||||
asyncio.run(do_drop(usb_transport))
|
||||
|
||||
|
||||
@main.command
|
||||
@click.argument("usb_transport")
|
||||
@click.option(
|
||||
"--force",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Try to get the device info even if the USB info doesn't match",
|
||||
)
|
||||
def info(usb_transport, force):
|
||||
"""Get the firmware info from a USB dongle."""
|
||||
asyncio.run(do_info(usb_transport, force))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user