Skip to content

Commit

Permalink
Merge pull request #267 from google/gbg/rfcomm-with-uuid
Browse files Browse the repository at this point in the history
rfcomm with UUID
  • Loading branch information
barbibulle authored Sep 7, 2023
2 parents acdbc4d + 8be9f4c commit 9b11142
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 62 deletions.
64 changes: 62 additions & 2 deletions bumble/rfcomm.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,29 @@
import logging
import asyncio
import enum
from typing import Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING

from pyee import EventEmitter
from typing import Optional, Tuple, Callable, Dict, Union, TYPE_CHECKING

from . import core, l2cap
from .colors import color
from .core import BT_BR_EDR_TRANSPORT, InvalidStateError, ProtocolError
from .core import (
UUID,
BT_RFCOMM_PROTOCOL_ID,
BT_BR_EDR_TRANSPORT,
BT_L2CAP_PROTOCOL_ID,
InvalidStateError,
ProtocolError,
)
from .sdp import (
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_PUBLIC_BROWSE_ROOT,
DataElement,
ServiceAttribute,
)

if TYPE_CHECKING:
from bumble.device import Device, Connection
Expand Down Expand Up @@ -111,6 +127,50 @@
# fmt: on


# -----------------------------------------------------------------------------
def make_service_sdp_records(
service_record_handle: int, channel: int, uuid: Optional[UUID] = None
) -> List[ServiceAttribute]:
"""
Create SDP records for an RFComm service given a channel number and an
optional UUID. A Service Class Attribute is included only if the UUID is not None.
"""
records = [
ServiceAttribute(
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
DataElement.unsigned_integer_32(service_record_handle),
),
ServiceAttribute(
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
),
ServiceAttribute(
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
DataElement.sequence(
[
DataElement.sequence([DataElement.uuid(BT_L2CAP_PROTOCOL_ID)]),
DataElement.sequence(
[
DataElement.uuid(BT_RFCOMM_PROTOCOL_ID),
DataElement.unsigned_integer_8(channel),
]
),
]
),
),
]

if uuid:
records.append(
ServiceAttribute(
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
DataElement.sequence([DataElement.uuid(uuid)]),
)
)

return records


# -----------------------------------------------------------------------------
def compute_fcs(buffer: bytes) -> int:
result = 0xFF
Expand Down
151 changes: 91 additions & 60 deletions examples/run_rfcomm_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,83 +20,109 @@
import os
import logging

from bumble.core import UUID
from bumble.device import Device
from bumble.transport import open_transport_or_link
from bumble.core import BT_L2CAP_PROTOCOL_ID, BT_RFCOMM_PROTOCOL_ID, UUID
from bumble.rfcomm import Server
from bumble.sdp import (
DataElement,
ServiceAttribute,
SDP_PUBLIC_BROWSE_ROOT,
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
)
from bumble.utils import AsyncRunner
from bumble.rfcomm import make_service_sdp_records


# -----------------------------------------------------------------------------
def sdp_records(channel):
def sdp_records(channel, uuid):
service_record_handle = 0x00010001
return {
0x00010001: [
ServiceAttribute(
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
DataElement.unsigned_integer_32(0x00010001),
),
ServiceAttribute(
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
),
ServiceAttribute(
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
DataElement.sequence(
[DataElement.uuid(UUID('E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'))]
),
),
ServiceAttribute(
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
DataElement.sequence(
[
DataElement.sequence([DataElement.uuid(BT_L2CAP_PROTOCOL_ID)]),
DataElement.sequence(
[
DataElement.uuid(BT_RFCOMM_PROTOCOL_ID),
DataElement.unsigned_integer_8(channel),
]
),
]
),
),
]
service_record_handle: make_service_sdp_records(
service_record_handle, channel, UUID(uuid)
)
}


# -----------------------------------------------------------------------------
def on_dlc(dlc):
print('*** DLC connected', dlc)
dlc.sink = lambda data: on_rfcomm_data_received(dlc, data)
def on_rfcomm_session(rfcomm_session, tcp_server):
print('*** RFComm session connected', rfcomm_session)
tcp_server.attach_session(rfcomm_session)


# -----------------------------------------------------------------------------
def on_rfcomm_data_received(dlc, data):
print(f'<<< Data received: {data.hex()}')
try:
message = data.decode('utf-8')
print(f'<<< Message = {message}')
except Exception:
pass
class TcpServerProtocol(asyncio.Protocol):
def __init__(self, server):
self.server = server

# Echo everything back
dlc.write(data)
def connection_made(self, transport):
peer_name = transport.get_extra_info('peer_name')
print(f'<<< TCP Server: connection from {peer_name}')
if self.server:
self.server.tcp_transport = transport
else:
transport.close()

def connection_lost(self, exc):
print('<<< TCP Server: connection lost')
if self.server:
self.server.tcp_transport = None

def data_received(self, data):
print(f'<<< TCP Server: data received: {len(data)} bytes - {data.hex()}')
if self.server:
self.server.tcp_data_received(data)


# -----------------------------------------------------------------------------
class TcpServer:
def __init__(self, port):
self.rfcomm_session = None
self.tcp_transport = None
AsyncRunner.spawn(self.run(port))

def attach_session(self, rfcomm_session):
if self.rfcomm_session:
self.rfcomm_session.sink = None

self.rfcomm_session = rfcomm_session
rfcomm_session.sink = self.rfcomm_data_received

def rfcomm_data_received(self, data):
print(f'<<< RFCOMM Data: {data.hex()}')
if self.tcp_transport:
self.tcp_transport.write(data)
else:
print('!!! no TCP connection, dropping data')

def tcp_data_received(self, data):
if self.rfcomm_session:
self.rfcomm_session.write(data)
else:
print('!!! no RFComm session, dropping data')

async def run(self, port):
print(f'$$$ Starting TCP server on port {port}')

server = await asyncio.get_running_loop().create_server(
lambda: TcpServerProtocol(self), '127.0.0.1', port
)

async with server:
await server.serve_forever()


# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 3:
print('Usage: run_rfcomm_server.py <device-config> <transport-spec>')
print('example: run_rfcomm_server.py classic2.json usb:04b4:f901')
if len(sys.argv) < 4:
print(
'Usage: run_rfcomm_server.py <device-config> <transport-spec> '
'<tcp-port> [<uuid>]'
)
print('example: run_rfcomm_server.py classic2.json usb:0 8888')
return

tcp_port = int(sys.argv[3])

if len(sys.argv) >= 5:
uuid = sys.argv[4]
else:
uuid = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'

print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
Expand All @@ -105,15 +131,20 @@ async def main():
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
device.classic_enabled = True

# Create and register a server
# Create a TCP server
tcp_server = TcpServer(tcp_port)

# Create and register an RFComm server
rfcomm_server = Server(device)

# Listen for incoming DLC connections
channel_number = rfcomm_server.listen(on_dlc)
print(f'### Listening for connection on channel {channel_number}')
channel_number = rfcomm_server.listen(
lambda session: on_rfcomm_session(session, tcp_server)
)
print(f'### Listening for RFComm connections on channel {channel_number}')

# Setup the SDP to advertise this channel
device.sdp_service_records = sdp_records(channel_number)
device.sdp_service_records = sdp_records(channel_number, uuid)

# Start the controller
await device.power_on()
Expand Down

0 comments on commit 9b11142

Please sign in to comment.