forked from auracaster/bumble_mirror
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69c6643bb8 | ||
|
|
b8214bf948 | ||
|
|
a9c62c44b3 | ||
|
|
7d0b4ef4e0 | ||
|
|
313340f1c6 | ||
|
|
e8ed69fb09 | ||
|
|
16d5cf6770 | ||
|
|
a2caf1deb2 | ||
|
|
01bfdd2c98 |
@@ -29,6 +29,17 @@ from bumble.hci import (
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Constant
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INTEL_USB_PRODUCTS = {
|
||||||
|
# Intel AX210
|
||||||
|
(0x8087, 0x0032),
|
||||||
|
# Intel BE200
|
||||||
|
(0x8087, 0x0036),
|
||||||
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# HCI Commands
|
# HCI Commands
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -52,13 +63,34 @@ class Driver(common.Driver):
|
|||||||
def __init__(self, host):
|
def __init__(self, host):
|
||||||
self.host = host
|
self.host = host
|
||||||
|
|
||||||
@classmethod
|
@staticmethod
|
||||||
async def for_host(cls, host): # type: ignore
|
def check(host):
|
||||||
# Only instantiate this driver if explicitly selected
|
driver = host.hci_metadata.get("driver")
|
||||||
if host.hci_metadata.get("driver") == "intel":
|
if driver == "intel":
|
||||||
return cls(host)
|
return True
|
||||||
|
|
||||||
return None
|
vendor_id = host.hci_metadata.get("vendor_id")
|
||||||
|
product_id = host.hci_metadata.get("product_id")
|
||||||
|
|
||||||
|
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 INTEL_USB_PRODUCTS:
|
||||||
|
logger.debug(
|
||||||
|
f"USB device ({vendor_id:04X}, {product_id:04X}) " "not in known list"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def for_host(cls, host, force=False): # type: ignore
|
||||||
|
# Only instantiate this driver if explicitly selected
|
||||||
|
if not force and not cls.check(host):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return cls(host)
|
||||||
|
|
||||||
async def init_controller(self):
|
async def init_controller(self):
|
||||||
self.host.ready = True
|
self.host.ready = True
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ from pandora import host_pb2
|
|||||||
from pandora.host_pb2 import (
|
from pandora.host_pb2 import (
|
||||||
NOT_CONNECTABLE,
|
NOT_CONNECTABLE,
|
||||||
NOT_DISCOVERABLE,
|
NOT_DISCOVERABLE,
|
||||||
|
DISCOVERABLE_LIMITED,
|
||||||
|
DISCOVERABLE_GENERAL,
|
||||||
PRIMARY_1M,
|
PRIMARY_1M,
|
||||||
PRIMARY_CODED,
|
PRIMARY_CODED,
|
||||||
SECONDARY_1M,
|
SECONDARY_1M,
|
||||||
@@ -69,6 +71,7 @@ from pandora.host_pb2 import (
|
|||||||
ConnectResponse,
|
ConnectResponse,
|
||||||
DataTypes,
|
DataTypes,
|
||||||
DisconnectRequest,
|
DisconnectRequest,
|
||||||
|
DiscoverabilityMode,
|
||||||
InquiryResponse,
|
InquiryResponse,
|
||||||
PrimaryPhy,
|
PrimaryPhy,
|
||||||
ReadLocalAddressResponse,
|
ReadLocalAddressResponse,
|
||||||
@@ -483,14 +486,10 @@ class HostService(HostServicer):
|
|||||||
target_bytes = bytes(reversed(request.target))
|
target_bytes = bytes(reversed(request.target))
|
||||||
if request.target_variant() == "public":
|
if request.target_variant() == "public":
|
||||||
target = Address(target_bytes, Address.PUBLIC_DEVICE_ADDRESS)
|
target = Address(target_bytes, Address.PUBLIC_DEVICE_ADDRESS)
|
||||||
advertising_type = (
|
advertising_type = AdvertisingType.DIRECTED_CONNECTABLE_LOW_DUTY
|
||||||
AdvertisingType.DIRECTED_CONNECTABLE_HIGH_DUTY
|
|
||||||
) # FIXME: HIGH_DUTY ?
|
|
||||||
else:
|
else:
|
||||||
target = Address(target_bytes, Address.RANDOM_DEVICE_ADDRESS)
|
target = Address(target_bytes, Address.RANDOM_DEVICE_ADDRESS)
|
||||||
advertising_type = (
|
advertising_type = AdvertisingType.DIRECTED_CONNECTABLE_LOW_DUTY
|
||||||
AdvertisingType.DIRECTED_CONNECTABLE_HIGH_DUTY
|
|
||||||
) # FIXME: HIGH_DUTY ?
|
|
||||||
|
|
||||||
if request.connectable:
|
if request.connectable:
|
||||||
|
|
||||||
@@ -867,6 +866,16 @@ class HostService(HostServicer):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
flag_map = {
|
||||||
|
NOT_DISCOVERABLE: 0x00,
|
||||||
|
DISCOVERABLE_LIMITED: AdvertisingData.LE_LIMITED_DISCOVERABLE_MODE_FLAG,
|
||||||
|
DISCOVERABLE_GENERAL: AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG,
|
||||||
|
}
|
||||||
|
|
||||||
|
if dt.le_discoverability_mode:
|
||||||
|
flags = flag_map[dt.le_discoverability_mode]
|
||||||
|
ad_structures.append((AdvertisingData.FLAGS, flags.to_bytes(1, 'big')))
|
||||||
|
|
||||||
return AdvertisingData(ad_structures)
|
return AdvertisingData(ad_structures)
|
||||||
|
|
||||||
def pack_data_types(self, ad: AdvertisingData) -> DataTypes:
|
def pack_data_types(self, ad: AdvertisingData) -> DataTypes:
|
||||||
|
|||||||
@@ -113,9 +113,10 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
|||||||
self.loop.call_soon_threadsafe(self.stop_event.set)
|
self.loop.call_soon_threadsafe(self.stop_event.set)
|
||||||
|
|
||||||
class UsbPacketSource(asyncio.Protocol, ParserSource):
|
class UsbPacketSource(asyncio.Protocol, ParserSource):
|
||||||
def __init__(self, device, sco_enabled):
|
def __init__(self, device, metadata, sco_enabled):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.device = device
|
self.device = device
|
||||||
|
self.metadata = metadata
|
||||||
self.loop = asyncio.get_running_loop()
|
self.loop = asyncio.get_running_loop()
|
||||||
self.queue = asyncio.Queue()
|
self.queue = asyncio.Queue()
|
||||||
self.dequeue_task = None
|
self.dequeue_task = None
|
||||||
@@ -216,6 +217,15 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
|||||||
if ':' in spec:
|
if ':' in spec:
|
||||||
vendor_id, product_id = spec.split(':')
|
vendor_id, product_id = spec.split(':')
|
||||||
device = usb_find(idVendor=int(vendor_id, 16), idProduct=int(product_id, 16))
|
device = usb_find(idVendor=int(vendor_id, 16), idProduct=int(product_id, 16))
|
||||||
|
elif '-' in spec:
|
||||||
|
|
||||||
|
def device_path(device):
|
||||||
|
if device.port_numbers:
|
||||||
|
return f'{device.bus}-{".".join(map(str, device.port_numbers))}'
|
||||||
|
else:
|
||||||
|
return str(device.bus)
|
||||||
|
|
||||||
|
device = usb_find(custom_match=lambda device: device_path(device) == spec)
|
||||||
else:
|
else:
|
||||||
device_index = int(spec)
|
device_index = int(spec)
|
||||||
devices = list(
|
devices = list(
|
||||||
@@ -235,6 +245,9 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
|||||||
raise ValueError('device not found')
|
raise ValueError('device not found')
|
||||||
logger.debug(f'USB Device: {device}')
|
logger.debug(f'USB Device: {device}')
|
||||||
|
|
||||||
|
# Collect the metadata
|
||||||
|
device_metadata = {'vendor_id': device.idVendor, 'product_id': device.idProduct}
|
||||||
|
|
||||||
# Detach the kernel driver if needed
|
# Detach the kernel driver if needed
|
||||||
if device.is_kernel_driver_active(0):
|
if device.is_kernel_driver_active(0):
|
||||||
logger.debug("detaching kernel driver")
|
logger.debug("detaching kernel driver")
|
||||||
@@ -289,7 +302,7 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
|||||||
# except usb.USBError:
|
# except usb.USBError:
|
||||||
# logger.warning('failed to set alternate setting')
|
# logger.warning('failed to set alternate setting')
|
||||||
|
|
||||||
packet_source = UsbPacketSource(device, sco_enabled)
|
packet_source = UsbPacketSource(device, device_metadata, sco_enabled)
|
||||||
packet_sink = UsbPacketSink(device)
|
packet_sink = UsbPacketSink(device)
|
||||||
packet_source.start()
|
packet_source.start()
|
||||||
packet_sink.start()
|
packet_sink.start()
|
||||||
|
|||||||
@@ -396,6 +396,16 @@ async def open_usb_transport(spec: str) -> Transport:
|
|||||||
break
|
break
|
||||||
device_index -= 1
|
device_index -= 1
|
||||||
device.close()
|
device.close()
|
||||||
|
elif '-' in spec:
|
||||||
|
|
||||||
|
def device_path(device):
|
||||||
|
return f'{device.getBusNumber()}-{".".join(map(str, device.getPortNumberList()))}'
|
||||||
|
|
||||||
|
for device in context.getDeviceIterator(skip_on_error=True):
|
||||||
|
if device_path(device) == spec:
|
||||||
|
found = device
|
||||||
|
break
|
||||||
|
device.close()
|
||||||
else:
|
else:
|
||||||
# Look for a compatible device by index
|
# Look for a compatible device by index
|
||||||
def device_is_bluetooth_hci(device):
|
def device_is_bluetooth_hci(device):
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ The moniker for a USB transport is either:
|
|||||||
* `usb:<vendor>:<product>`
|
* `usb:<vendor>:<product>`
|
||||||
* `usb:<vendor>:<product>/<serial-number>`
|
* `usb:<vendor>:<product>/<serial-number>`
|
||||||
* `usb:<vendor>:<product>#<index>`
|
* `usb:<vendor>:<product>#<index>`
|
||||||
|
* `usb:<bus>-<port_numbers>`
|
||||||
|
|
||||||
with `<index>` as a 0-based index (0 being the first one) to select amongst all the matching devices when there are more than one.
|
with `<index>` as a 0-based index (0 being the first one) to select amongst all the matching devices when there are more than one.
|
||||||
In the `usb:<index>` form, matching devices are the ones supporting Bluetooth HCI, as declared by their Class, Subclass and Protocol.
|
In the `usb:<index>` form, matching devices are the ones supporting Bluetooth HCI, as declared by their Class, Subclass and Protocol.
|
||||||
@@ -17,6 +18,8 @@ In the `usb:<vendor>:<product>#<index>` form, matching devices are the ones with
|
|||||||
|
|
||||||
`<vendor>` and `<product>` are a vendor ID and product ID in hexadecimal.
|
`<vendor>` and `<product>` are a vendor ID and product ID in hexadecimal.
|
||||||
|
|
||||||
|
with `<port_numbers>` as a list of all port numbers from root separated with dots `.`
|
||||||
|
|
||||||
In addition, if the moniker ends with the symbol "!", the device will be used in "forced" mode:
|
In addition, if the moniker ends with the symbol "!", the device will be used in "forced" mode:
|
||||||
the first USB interface of the device will be used, regardless of the interface class/subclass.
|
the first USB interface of the device will be used, regardless of the interface class/subclass.
|
||||||
This may be useful for some devices that use a custom class/subclass but may nonetheless work as-is.
|
This may be useful for some devices that use a custom class/subclass but may nonetheless work as-is.
|
||||||
@@ -37,6 +40,9 @@ This may be useful for some devices that use a custom class/subclass but may non
|
|||||||
`usb:0B05:17CB!`
|
`usb:0B05:17CB!`
|
||||||
The BT USB dongle vendor=0B05 and product=17CB, in "forced" mode.
|
The BT USB dongle vendor=0B05 and product=17CB, in "forced" mode.
|
||||||
|
|
||||||
|
`usb:3-3.4.1`
|
||||||
|
The BT USB dongle on bus 3 on port path 3, 4, 1.
|
||||||
|
|
||||||
|
|
||||||
## Alternative
|
## Alternative
|
||||||
The library includes two different implementations of the USB transport, implemented using different python bindings for `libusb`.
|
The library includes two different implementations of the USB transport, implemented using different python bindings for `libusb`.
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
# Next
|
# 0.2.0
|
||||||
|
|
||||||
- Code-gen company ID table
|
- Code-gen company ID table
|
||||||
|
- Unstable support for extended advertisements
|
||||||
|
- CLI tools for downloading Realtek firmware
|
||||||
|
- PDL-generated types for HCI commands
|
||||||
|
|
||||||
# 0.1.0
|
# 0.1.0
|
||||||
|
|
||||||
- Initial release
|
- Initial release
|
||||||
|
|||||||
2
rust/Cargo.lock
generated
2
rust/Cargo.lock
generated
@@ -182,7 +182,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumble"
|
name = "bumble"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bumble"
|
name = "bumble"
|
||||||
description = "Rust API for the Bumble Bluetooth stack"
|
description = "Rust API for the Bumble Bluetooth stack"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
homepage = "https://google.github.io/bumble/index.html"
|
homepage = "https://google.github.io/bumble/index.html"
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ PYTHONPATH=..:[virtualenv site-packages] \
|
|||||||
cargo run --features bumble-tools --bin bumble -- --help
|
cargo run --features bumble-tools --bin bumble -- --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Notable subcommands:
|
||||||
|
|
||||||
|
- `firmware realtek download`: download Realtek firmware for various chipsets so that it can be automatically loaded when needed
|
||||||
|
- `usb probe`: show USB devices, highlighting the ones usable for Bluetooth
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
Run the tests:
|
Run the tests:
|
||||||
@@ -63,4 +68,4 @@ To regenerate the assigned number tables based on the Python codebase:
|
|||||||
|
|
||||||
```
|
```
|
||||||
PYTHONPATH=.. cargo run --bin gen-assigned-numbers --features dev-tools
|
PYTHONPATH=.. cargo run --bin gen-assigned-numbers --features dev-tools
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ impl Controller {
|
|||||||
/// module specifies the defaults. Must be called from a thread with a Python event loop, which
|
/// module specifies the defaults. Must be called from a thread with a Python event loop, which
|
||||||
/// should be true on `tokio::main` and `async_std::main`.
|
/// should be true on `tokio::main` and `async_std::main`.
|
||||||
///
|
///
|
||||||
/// For more info, see https://awestlake87.github.io/pyo3-asyncio/master/doc/pyo3_asyncio/#event-loop-references-and-contextvars.
|
/// For more info, see <https://awestlake87.github.io/pyo3-asyncio/master/doc/pyo3_asyncio/#event-loop-references-and-contextvars>.
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
name: &str,
|
name: &str,
|
||||||
host_source: Option<TransportSource>,
|
host_source: Option<TransportSource>,
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ impl ToPyObject for Address {
|
|||||||
|
|
||||||
/// An error meaning that the u64 value did not represent a valid BT address.
|
/// An error meaning that the u64 value did not represent a valid BT address.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InvalidAddress(u64);
|
pub struct InvalidAddress(#[allow(unused)] u64);
|
||||||
|
|
||||||
impl TryInto<packets::Address> for Address {
|
impl TryInto<packets::Address> for Address {
|
||||||
type Error = ConversionError<InvalidAddress>;
|
type Error = ConversionError<InvalidAddress>;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ impl LeConnectionOrientedChannel {
|
|||||||
/// Must be called from a thread with a Python event loop, which should be true on
|
/// Must be called from a thread with a Python event loop, which should be true on
|
||||||
/// `tokio::main` and `async_std::main`.
|
/// `tokio::main` and `async_std::main`.
|
||||||
///
|
///
|
||||||
/// For more info, see https://awestlake87.github.io/pyo3-asyncio/master/doc/pyo3_asyncio/#event-loop-references-and-contextvars.
|
/// For more info, see <https://awestlake87.github.io/pyo3-asyncio/master/doc/pyo3_asyncio/#event-loop-references-and-contextvars>.
|
||||||
pub async fn disconnect(&mut self) -> PyResult<()> {
|
pub async fn disconnect(&mut self) -> PyResult<()> {
|
||||||
Python::with_gil(|py| {
|
Python::with_gil(|py| {
|
||||||
self.0
|
self.0
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ development =
|
|||||||
types-invoke >= 1.7.3
|
types-invoke >= 1.7.3
|
||||||
types-protobuf >= 4.21.0
|
types-protobuf >= 4.21.0
|
||||||
avatar =
|
avatar =
|
||||||
pandora-avatar == 0.0.5
|
pandora-avatar == 0.0.8
|
||||||
rootcanal == 1.9.0 ; python_version>='3.10'
|
rootcanal == 1.9.0 ; python_version>='3.10'
|
||||||
documentation =
|
documentation =
|
||||||
mkdocs >= 1.4.0
|
mkdocs >= 1.4.0
|
||||||
|
|||||||
Reference in New Issue
Block a user