initial import

This commit is contained in:
Gilles Boccon-Gibod
2022-05-16 19:42:31 -07:00
commit 6ac91f7dec
185 changed files with 32064 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import logging
from .common import Transport, AsyncPipeSink
from ..link import RemoteLink
from ..controller import Controller
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_transport(name):
'''
Open a transport by name.
The name must be <type>:<parameters>
Where <parameters> depend on the type (and may be empty for some types).
The supported types are: serial,udp,tcp,pty,usb
'''
scheme, *spec = name.split(':', 1)
if scheme == 'serial' and spec:
from .serial import open_serial_transport
return await open_serial_transport(spec[0])
elif scheme == 'udp' and spec:
from .udp import open_udp_transport
return await open_udp_transport(spec[0])
elif scheme == 'tcp-client' and spec:
from .tcp_client import open_tcp_client_transport
return await open_tcp_client_transport(spec[0])
elif scheme == 'tcp-server' and spec:
from .tcp_server import open_tcp_server_transport
return await open_tcp_server_transport(spec[0])
elif scheme == 'ws-client' and spec:
from .ws_client import open_ws_client_transport
return await open_ws_client_transport(spec[0])
elif scheme == 'ws-server' and spec:
from .ws_server import open_ws_server_transport
return await open_ws_server_transport(spec[0])
elif scheme == 'pty':
from .pty import open_pty_transport
return await open_pty_transport(spec[0] if spec else None)
elif scheme == 'file':
from .file import open_file_transport
return await open_file_transport(spec[0] if spec else None)
elif scheme == 'vhci':
from .vhci import open_vhci_transport
return await open_vhci_transport(spec[0] if spec else None)
elif scheme == 'hci-socket':
from .hci_socket import open_hci_socket_transport
return await open_hci_socket_transport(spec[0] if spec else None)
elif scheme == 'usb':
from .usb import open_usb_transport
return await open_usb_transport(spec[0] if spec else None)
elif scheme == 'pyusb':
from .pyusb import open_pyusb_transport
return await open_pyusb_transport(spec[0] if spec else None)
elif scheme == 'android-emulator':
from .android_emulator import open_android_emulator_transport
return await open_android_emulator_transport(spec[0] if spec else None)
else:
raise ValueError('unknown transport scheme')
# -----------------------------------------------------------------------------
async def open_transport_or_link(name):
if name.startswith('link-relay:'):
link = RemoteLink(name[11:])
await link.wait_until_connected()
controller = Controller('remote', link = link)
class LinkTransport(Transport):
async def close(self):
link.close()
return LinkTransport(controller, AsyncPipeSink(controller))
else:
return await open_transport(name)
+107
View File
@@ -0,0 +1,107 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import logging
import grpc
from .common import PumpedTransport, PumpedPacketSource, PumpedPacketSink
from .emulated_bluetooth_pb2_grpc import EmulatedBluetoothServiceStub
from .emulated_bluetooth_packets_pb2 import HCIPacket
from .emulated_bluetooth_vhci_pb2_grpc import VhciForwardingServiceStub
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_android_emulator_transport(spec):
'''
Open a transport connection to an Android emulator via its gRPC interface.
The parameter string has this syntax:
[<remote-host>:<remote-port>][,mode=<host|controller>]
The <remote-host>:<remote-port> part is optional, it defaults to localhost:8554
The mode=<mode> part is optional, it defaults to mode=host
When the mode is set to 'controller', the connection is for a controller (i.e the
Android Bluetooth stack will use the connected endpoint as its controller). When
the mode is set to 'host', the connection is to the 'Root Canal' virtual controller
that runs as part of the emulator, and used by the Android Bluetooth stack.
Examples:
(empty string) --> connect as a host to the emulator on localhost:8554
localhost:8555 --> connect as a host to the emulator on localhost:8555
mode=controller --> connect as a controller to the emulator on localhost:8554
'''
# Wrapper for I/O operations
class HciDevice:
def __init__(self, hci_device):
self.hci_device = hci_device
async def read(self):
packet = await self.hci_device.read()
return bytes([packet.type]) + packet.packet
async def write(self, packet):
await self.hci_device.write(
HCIPacket(
type = packet[0],
packet = packet[1:]
)
)
# Parse the parameters
mode = 'host'
server_host = 'localhost'
server_port = 8554
if spec is not None:
params = spec.split(',')
for param in params:
if param.startswith('mode='):
mode = param.split('=')[1]
elif ':' in param:
server_host, server_port = param.split(':')
else:
raise ValueError('invalid parameter')
# Connect to the gRPC server
server_address = f'{server_host}:{server_port}'
logger.debug(f'connecting to gRPC server at {server_address}')
channel = grpc.aio.insecure_channel(server_address)
if mode == 'host':
# Connect as a host
service = EmulatedBluetoothServiceStub(channel)
hci_device = HciDevice(service.registerHCIDevice())
elif mode == 'controller':
# Connect as a controller
service = VhciForwardingServiceStub(channel)
hci_device = HciDevice(service.attachVhci())
else:
raise ValueError('invalid mode')
# Create the transport object
transport = PumpedTransport(
PumpedPacketSource(hci_device.read),
PumpedPacketSink(hci_device.write),
channel.close
)
transport.start()
return transport
+326
View File
@@ -0,0 +1,326 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import struct
import asyncio
import logging
from colors import color
from .. import hci
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
# Information needed to parse HCI packets with a generic parser:
# For each packet type, the info represents:
# (length-size, length-offset, unpack-type)
HCI_PACKET_INFO = {
hci.HCI_COMMAND_PACKET: (1, 2, 'B'),
hci.HCI_ACL_DATA_PACKET: (2, 2, 'H'),
hci.HCI_SYNCHRONOUS_DATA_PACKET: (1, 2, 'B'),
hci.HCI_EVENT_PACKET: (1, 1, 'B')
}
# -----------------------------------------------------------------------------
class PacketPump:
'''
Pump HCI packets from a reader to a sink
'''
def __init__(self, reader, sink):
self.reader = reader
self.sink = sink
async def run(self):
while True:
try:
# Get a packet from the source
packet = hci.HCI_Packet.from_bytes(await self.reader.next_packet())
# Deliver the packet to the sink
self.sink.on_packet(packet)
except Exception as error:
logger.warning(f'!!! {error}')
# -----------------------------------------------------------------------------
class PacketParser:
'''
In-line parser that accepts data and emits 'on_packet' when a full packet has been parsed
'''
NEED_TYPE = 0
NEED_LENGTH = 1
NEED_BODY = 2
def __init__(self, sink = None):
self.sink = sink
self.extended_packet_info = {}
self.reset()
def reset(self):
self.state = PacketParser.NEED_TYPE
self.bytes_needed = 1
self.packet = bytearray()
self.packet_info = None
def feed_data(self, data):
data_offset = 0
data_left = len(data)
while data_left and self.bytes_needed:
consumed = min(self.bytes_needed, data_left)
self.packet.extend(data[data_offset:data_offset + consumed])
data_offset += consumed
data_left -= consumed
self.bytes_needed -= consumed
if self.bytes_needed == 0:
if self.state == PacketParser.NEED_TYPE:
packet_type = self.packet[0]
self.packet_info = HCI_PACKET_INFO.get(packet_type) or self.extended_packet_info.get(packet_type)
if self.packet_info is None:
raise ValueError(f'invalid packet type {packet_type}')
self.state = PacketParser.NEED_LENGTH
self.bytes_needed = self.packet_info[0] + self.packet_info[1]
elif self.state == PacketParser.NEED_LENGTH:
body_length = struct.unpack_from(self.packet_info[2], self.packet, 1 + self.packet_info[1])[0]
self.bytes_needed = body_length
self.state = PacketParser.NEED_BODY
# Emit a packet if one is complete
if self.state == PacketParser.NEED_BODY and not self.bytes_needed:
if self.sink:
try:
self.sink.on_packet(bytes(self.packet))
except Exception as error:
logger.warning(color(f'!!! Exception in on_packet: {error}', 'red'))
self.reset()
def set_packet_sink(self, sink):
self.sink = sink
# -----------------------------------------------------------------------------
class PacketReader:
'''
Reader that reads HCI packets from a sync source
'''
def __init__(self, source):
self.source = source
def next_packet(self):
# Get the packet type
packet_type = self.source.read(1)
if len(packet_type) != 1:
return None
# Get the packet info based on its type
packet_info = HCI_PACKET_INFO.get(packet_type[0])
if packet_info is None:
raise ValueError(f'invalid packet type {packet_type} found')
# Read the header (that includes the length)
header_size = packet_info[0] + packet_info[1]
header = self.source.read(header_size)
if len(header) != header_size:
raise ValueError('packet too short')
# Read the body
body_length = struct.unpack_from(packet_info[2], header, packet_info[1])[0]
body = self.source.read(body_length)
if len(body) != body_length:
raise ValueError('packet too short')
return packet_type + header + body
# -----------------------------------------------------------------------------
class AsyncPacketReader:
'''
Reader that reads HCI packets from an async source
'''
def __init__(self, source):
self.source = source
async def next_packet(self):
# Get the packet type
packet_type = await self.source.readexactly(1)
# Get the packet info based on its type
packet_info = HCI_PACKET_INFO.get(packet_type[0])
if packet_info is None:
raise ValueError(f'invalid packet type {packet_type} found')
# Read the header (that includes the length)
header_size = packet_info[0] + packet_info[1]
header = await self.source.readexactly(header_size)
# Read the body
body_length = struct.unpack_from(packet_info[2], header, packet_info[1])[0]
body = await self.source.readexactly(body_length)
return packet_type + header + body
# -----------------------------------------------------------------------------
class AsyncPipeSink:
'''
Sink that forwards packets asynchronously to another sink
'''
def __init__(self, sink):
self.sink = sink
self.loop = asyncio.get_running_loop()
def on_packet(self, packet):
self.loop.call_soon(self.sink.on_packet, packet)
# -----------------------------------------------------------------------------
class ParserSource:
"""
Base class designed to be subclassed by transport-specific source classes
"""
def __init__(self):
self.parser = PacketParser()
self.terminated = asyncio.get_running_loop().create_future()
def set_packet_sink(self, sink):
self.parser.set_packet_sink(sink)
async def wait_for_termination(self):
return await self.terminated
def close(self):
pass
# -----------------------------------------------------------------------------
class StreamPacketSource(asyncio.Protocol, ParserSource):
def data_received(self, data):
self.parser.feed_data(data)
# -----------------------------------------------------------------------------
class StreamPacketSink:
def __init__(self, transport):
self.transport = transport
def on_packet(self, packet):
self.transport.write(packet)
def close(self):
self.transport.close()
# -----------------------------------------------------------------------------
class Transport:
def __init__(self, source, sink):
self.source = source
self.sink = sink
async def __aenter__(self):
return self
async def __aexit__(self, *args):
await self.close()
def __iter__(self):
return iter((self.source, self.sink))
async def close(self):
self.source.close()
self.sink.close()
# -----------------------------------------------------------------------------
class PumpedPacketSource(ParserSource):
def __init__(self, receive):
super().__init__()
self.receive_function = receive
self.pump_task = None
def start(self):
async def pump_packets():
while True:
try:
packet = await self.receive_function()
self.parser.feed_data(packet)
except asyncio.exceptions.CancelledError:
logger.debug('source pump task done')
break
except Exception as error:
logger.warn(f'exception while waiting for packet: {error}')
self.terminated.set_result(error)
break
self.pump_task = asyncio.get_running_loop().create_task(pump_packets())
def close(self):
if self.pump_task:
self.pump_task.cancel()
# -----------------------------------------------------------------------------
class PumpedPacketSink:
def __init__(self, send):
self.send_function = send
self.packet_queue = asyncio.Queue()
self.pump_task = None
def on_packet(self, packet):
self.packet_queue.put_nowait(packet)
def start(self):
async def pump_packets():
while True:
try:
packet = await self.packet_queue.get()
await self.send_function(packet)
except asyncio.exceptions.CancelledError:
logger.debug('sink pump task done')
break
except Exception as error:
logger.warn(f'exception while sending packet: {error}')
break
self.pump_task = asyncio.get_running_loop().create_task(pump_packets())
def close(self):
if self.pump_task:
self.pump_task.cancel()
# -----------------------------------------------------------------------------
class PumpedTransport(Transport):
def __init__(self, source, sink, close_function):
super().__init__(source, sink)
self.close_function = close_function
def start(self):
self.source.start()
self.sink.start()
async def close(self):
await super().close()
await self.close_function()
@@ -0,0 +1,52 @@
# 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.
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: emulated_bluetooth_packets.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n emulated_bluetooth_packets.proto\x12\x1b\x61ndroid.emulation.bluetooth\"\xfb\x01\n\tHCIPacket\x12?\n\x04type\x18\x01 \x01(\x0e\x32\x31.android.emulation.bluetooth.HCIPacket.PacketType\x12\x0e\n\x06packet\x18\x02 \x01(\x0c\"\x9c\x01\n\nPacketType\x12\x1b\n\x17PACKET_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n\x17PACKET_TYPE_HCI_COMMAND\x10\x01\x12\x13\n\x0fPACKET_TYPE_ACL\x10\x02\x12\x13\n\x0fPACKET_TYPE_SCO\x10\x03\x12\x15\n\x11PACKET_TYPE_EVENT\x10\x04\x12\x13\n\x0fPACKET_TYPE_ISO\x10\x05\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3')
_HCIPACKET = DESCRIPTOR.message_types_by_name['HCIPacket']
_HCIPACKET_PACKETTYPE = _HCIPACKET.enum_types_by_name['PacketType']
HCIPacket = _reflection.GeneratedProtocolMessageType('HCIPacket', (_message.Message,), {
'DESCRIPTOR' : _HCIPACKET,
'__module__' : 'emulated_bluetooth_packets_pb2'
# @@protoc_insertion_point(class_scope:android.emulation.bluetooth.HCIPacket)
})
_sym_db.RegisterMessage(HCIPacket)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth'
_HCIPACKET._serialized_start=66
_HCIPACKET._serialized_end=317
_HCIPACKET_PACKETTYPE._serialized_start=161
_HCIPACKET_PACKETTYPE._serialized_end=317
# @@protoc_insertion_point(module_scope)
@@ -0,0 +1,53 @@
# 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.
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: emulated_bluetooth.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x65mulated_bluetooth.proto\x12\x1b\x61ndroid.emulation.bluetooth\x1a emulated_bluetooth_packets.proto\"\x19\n\x07RawData\x12\x0e\n\x06packet\x18\x01 \x01(\x0c\x32\xcb\x02\n\x18\x45mulatedBluetoothService\x12\x64\n\x12registerClassicPhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12`\n\x0eregisterBlePhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12g\n\x11registerHCIDevice\x12&.android.emulation.bluetooth.HCIPacket\x1a&.android.emulation.bluetooth.HCIPacket(\x01\x30\x01\x42\"\n\x1e\x63om.android.emulator.bluetoothP\x01\x62\x06proto3')
_RAWDATA = DESCRIPTOR.message_types_by_name['RawData']
RawData = _reflection.GeneratedProtocolMessageType('RawData', (_message.Message,), {
'DESCRIPTOR' : _RAWDATA,
'__module__' : 'emulated_bluetooth_pb2'
# @@protoc_insertion_point(class_scope:android.emulation.bluetooth.RawData)
})
_sym_db.RegisterMessage(RawData)
_EMULATEDBLUETOOTHSERVICE = DESCRIPTOR.services_by_name['EmulatedBluetoothService']
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\036com.android.emulator.bluetoothP\001'
_RAWDATA._serialized_start=91
_RAWDATA._serialized_end=116
_EMULATEDBLUETOOTHSERVICE._serialized_start=119
_EMULATEDBLUETOOTHSERVICE._serialized_end=450
# @@protoc_insertion_point(module_scope)
@@ -0,0 +1,207 @@
# 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.
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
from . import emulated_bluetooth_pb2 as emulated__bluetooth__pb2
class EmulatedBluetoothServiceStub(object):
"""An Emulated Bluetooth Service exposes the emulated bluetooth chip from the
android emulator. It allows you to register emulated bluetooth devices and
control the packets that are exchanged between the device and the world.
This service enables you to establish a "virtual network" of emulated
bluetooth devices that can interact with each other.
Note: This is not yet finalized, it is likely that these definitions will
evolve.
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.registerClassicPhy = channel.stream_stream(
'/android.emulation.bluetooth.EmulatedBluetoothService/registerClassicPhy',
request_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
response_deserializer=emulated__bluetooth__pb2.RawData.FromString,
)
self.registerBlePhy = channel.stream_stream(
'/android.emulation.bluetooth.EmulatedBluetoothService/registerBlePhy',
request_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
response_deserializer=emulated__bluetooth__pb2.RawData.FromString,
)
self.registerHCIDevice = channel.stream_stream(
'/android.emulation.bluetooth.EmulatedBluetoothService/registerHCIDevice',
request_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
response_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
)
class EmulatedBluetoothServiceServicer(object):
"""An Emulated Bluetooth Service exposes the emulated bluetooth chip from the
android emulator. It allows you to register emulated bluetooth devices and
control the packets that are exchanged between the device and the world.
This service enables you to establish a "virtual network" of emulated
bluetooth devices that can interact with each other.
Note: This is not yet finalized, it is likely that these definitions will
evolve.
"""
def registerClassicPhy(self, request_iterator, context):
"""Connect device to link layer. This will establish a direct connection
to the emulated bluetooth chip and configure the following:
- Each connection creates a new device and attaches it to the link layer
- Link Layer packets are transmitted directly to the phy
This should be used for classic connections.
This is used to directly connect various android emulators together.
For example a wear device can connect to an android emulator through
this.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def registerBlePhy(self, request_iterator, context):
"""Connect device to link layer. This will establish a direct connection
to root canal and execute the following:
- Each connection creates a new device and attaches it to the link layer
- Link Layer packets are transmitted directly to the phy
This should be used for BLE connections.
This is used to directly connect various android emulators together.
For example a wear device can connect to an android emulator through
this.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def registerHCIDevice(self, request_iterator, context):
"""Connect the device to the emulated bluetooth chip. The device will
participate in the network. You can configure the chip to scan, advertise
and setup connections with other devices that are connected to the
network.
This is usually used when you have a need for an emulated bluetooth chip
and have a bluetooth stack that can interpret and handle the packets
correctly.
For example the apache nimble stack can use this endpoint as the
transport layer.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_EmulatedBluetoothServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'registerClassicPhy': grpc.stream_stream_rpc_method_handler(
servicer.registerClassicPhy,
request_deserializer=emulated__bluetooth__pb2.RawData.FromString,
response_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
),
'registerBlePhy': grpc.stream_stream_rpc_method_handler(
servicer.registerBlePhy,
request_deserializer=emulated__bluetooth__pb2.RawData.FromString,
response_serializer=emulated__bluetooth__pb2.RawData.SerializeToString,
),
'registerHCIDevice': grpc.stream_stream_rpc_method_handler(
servicer.registerHCIDevice,
request_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
response_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'android.emulation.bluetooth.EmulatedBluetoothService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class EmulatedBluetoothService(object):
"""An Emulated Bluetooth Service exposes the emulated bluetooth chip from the
android emulator. It allows you to register emulated bluetooth devices and
control the packets that are exchanged between the device and the world.
This service enables you to establish a "virtual network" of emulated
bluetooth devices that can interact with each other.
Note: This is not yet finalized, it is likely that these definitions will
evolve.
"""
@staticmethod
def registerClassicPhy(request_iterator,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerClassicPhy',
emulated__bluetooth__pb2.RawData.SerializeToString,
emulated__bluetooth__pb2.RawData.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def registerBlePhy(request_iterator,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerBlePhy',
emulated__bluetooth__pb2.RawData.SerializeToString,
emulated__bluetooth__pb2.RawData.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def registerHCIDevice(request_iterator,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerHCIDevice',
emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
emulated__bluetooth__packets__pb2.HCIPacket.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@@ -0,0 +1,43 @@
# 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.
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: emulated_bluetooth_vhci.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x65mulated_bluetooth_vhci.proto\x12\x1b\x61ndroid.emulation.bluetooth\x1a emulated_bluetooth_packets.proto2y\n\x15VhciForwardingService\x12`\n\nattachVhci\x12&.android.emulation.bluetooth.HCIPacket\x1a&.android.emulation.bluetooth.HCIPacket(\x01\x30\x01\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3')
_VHCIFORWARDINGSERVICE = DESCRIPTOR.services_by_name['VhciForwardingService']
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth'
_VHCIFORWARDINGSERVICE._serialized_start=96
_VHCIFORWARDINGSERVICE._serialized_end=217
# @@protoc_insertion_point(module_scope)
@@ -0,0 +1,114 @@
# 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.
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2
class VhciForwardingServiceStub(object):
"""This is a service which allows you to directly intercept the VHCI packets
that are coming and going to the device before they are delivered to
the rootcanal service described below.
This service is usually not available on the emulator, and must be explictly
requested from the commandline.
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.attachVhci = channel.stream_stream(
'/android.emulation.bluetooth.VhciForwardingService/attachVhci',
request_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
response_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
)
class VhciForwardingServiceServicer(object):
"""This is a service which allows you to directly intercept the VHCI packets
that are coming and going to the device before they are delivered to
the rootcanal service described below.
This service is usually not available on the emulator, and must be explictly
requested from the commandline.
"""
def attachVhci(self, request_iterator, context):
"""This attach directly to /dev/vhci inside the android guest if available
- This will disable root canal.
- You will have to provide your own virtual bluetooth chip.
Some things to be aware of:
- Only one client can be active.
- Registering when bluetooth is active in android can result in
undefined behavior.
- If a client disconnects, rootcanal will be activated again
Status codes:
- FAILED_PRECONDITION (code 9) If another client is controlling /dev/vhci.
This is an internal testing only interface, and is NOT publicly
supported.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_VhciForwardingServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'attachVhci': grpc.stream_stream_rpc_method_handler(
servicer.attachVhci,
request_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString,
response_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'android.emulation.bluetooth.VhciForwardingService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class VhciForwardingService(object):
"""This is a service which allows you to directly intercept the VHCI packets
that are coming and going to the device before they are delivered to
the rootcanal service described below.
This service is usually not available on the emulator, and must be explictly
requested from the commandline.
"""
@staticmethod
def attachVhci(request_iterator,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.VhciForwardingService/attachVhci',
emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString,
emulated__bluetooth__packets__pb2.HCIPacket.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+60
View File
@@ -0,0 +1,60 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import asyncio
import io
import logging
from .common import Transport, StreamPacketSource, StreamPacketSink
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_file_transport(spec):
'''
Open a File transport (typically not for a real file, but for a PTY or other unix virtual files).
The parameter string is the path of the file to open
'''
# Open the file
file = io.open(spec, 'r+b', buffering=0)
# Setup reading
read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
lambda: StreamPacketSource(),
file
)
# Setup writing
write_transport, _ = await asyncio.get_running_loop().connect_write_pipe(
lambda: asyncio.BaseProtocol(),
file
)
packet_sink = StreamPacketSink(write_transport)
class FileTransport(Transport):
async def close(self):
read_transport.close()
write_transport.close()
file.close()
return FileTransport(packet_source, packet_sink)
+146
View File
@@ -0,0 +1,146 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import asyncio
import logging
import struct
import os
import socket
import ctypes
import collections
from .common import Transport, ParserSource
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_hci_socket_transport(spec):
'''
Open an HCI Socket (only available on some platforms).
The parameter string is either empty (to use the first/default Bluetooth adapter)
or a 0-based integer to indicate the adapter number.
'''
HCI_CHANNEL_USER = 1
# Create a raw HCI socket
try:
hci_socket = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_RAW | socket.SOCK_NONBLOCK, socket.BTPROTO_HCI)
except AttributeError:
# Not supported on this platform
logger.info("HCI sockets not supported on this platform")
raise Exception('Bluetooth HCI sockets not supported on this platform')
# Compute the adapter index
if spec is None:
adapter_index = 0
else:
adapter_index = int(spec)
# Bind the socket
# NOTE: since Python doesn't support binding with the required address format (yet),
# we need to go directly to the C runtime...
try:
ctypes.cdll.LoadLibrary('libc.so.6')
libc = ctypes.CDLL('libc.so.6', use_errno=True)
except OSError:
logger.info("HCI sockets not supported on this platform")
raise Exception('Bluetooth HCI sockets not supported on this platform')
libc.bind.argtypes = (ctypes.c_int, ctypes.POINTER(ctypes.c_char), ctypes.c_int)
libc.bind.restype = ctypes.c_int
bind_address = struct.pack('<HHH', socket.AF_BLUETOOTH, adapter_index, HCI_CHANNEL_USER)
if libc.bind(hci_socket.fileno(), ctypes.create_string_buffer(bind_address), len(bind_address)) != 0:
raise IOError(ctypes.get_errno(), os.strerror(ctypes.get_errno()))
class HciSocketSource(ParserSource):
def __init__(self, socket):
super().__init__()
self.socket = socket
asyncio.get_running_loop().add_reader(socket.fileno(), self.recv_until_would_block)
def recv_until_would_block(self):
logger.debug('recv until would block +++')
while True:
try:
packet = self.socket.recv(4096)
logger.debug(f'received packet {len(packet)} bytes')
self.parser.feed_data(packet)
except BlockingIOError:
logger.debug('recv would block')
break
def close(self):
asyncio.get_running_loop().remove_reader(self.socket.fileno())
class HciSocketSink:
def __init__(self, socket):
self.socket = socket
self.packets = collections.deque()
self.writer_added = False
def send_until_would_block(self):
logger.debug('send until would block ---')
while self.packets:
packet = self.packets.pop()
logger.debug('sending packet')
try:
bytes_written = self.socket.send(packet)
except BlockingIOError:
bytes_written = 0
if bytes_written != len(packet):
# Note: we assume here that there are no partial writes
logger.debug('send would block')
break
if self.packets:
# There's still something to send, ensure that we are monitoring the socket
if not self.writer_added:
asyncio.get_running_loop().add_writer(socket.fileno(), self.send_until_would_block)
self.writer_added = True
else:
# Nothing left to send, stop monitoring the socket
if self.writer_added:
asyncio.get_running_loop().remove_writer(self.socket.fileno())
self.writer_added = False
def on_packet(self, packet):
self.packets.appendleft(packet)
self.send_until_would_block()
def close(self):
if self.writer_added:
asyncio.get_running_loop().remove_writer(self.socket.fileno())
class HciSocketTransport(Transport):
def __init__(self, socket, source, sink):
super().__init__(source, sink)
self.socket = socket
async def close(self):
logger.debug('closing HCI socket transport')
self.source.close()
self.sink.close()
self.socket.close()
packet_source = HciSocketSource(hci_socket)
packet_sink = HciSocketSink(hci_socket)
return HciSocketTransport(hci_socket, packet_source, packet_sink)
+82
View File
@@ -0,0 +1,82 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import asyncio
import pty
import tty
import io
import atexit
import os
import logging
from .common import Transport, StreamPacketSource, StreamPacketSink
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_pty_transport(spec):
'''
Open a PTY transport.
The parameter string may be empty, or a path name where a symbolic link
to the PTY will be created (the link will be removed when the transport
is closed or when the process exits)
'''
primary, replica = pty.openpty()
replica_path = os.ttyname(replica)
logger.debug(f'pty open at {replica_path}')
tty.setraw(primary)
tty.setraw(replica)
read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe(
lambda: StreamPacketSource(),
io.open(primary, 'rb', closefd=False)
)
write_transport, _ = await asyncio.get_running_loop().connect_write_pipe(
lambda: asyncio.BaseProtocol(),
io.open(primary, 'wb', closefd=False)
)
packet_sink = StreamPacketSink(write_transport)
def cleanup():
if spec:
try:
os.unlink(spec)
except FileNotFoundError:
pass
# If required, create a symbolic link to the replica
# NOTE: the link will be removed when this process exits
if spec:
os.symlink(replica_path, spec)
logger.debug(f'linked pty at {spec}')
atexit.register(cleanup)
class PtyTransport(Transport):
async def close(self):
write_transport.close()
read_transport.close()
os.close(primary)
os.close(replica)
cleanup()
return PtyTransport(packet_source, packet_sink)
+276
View File
@@ -0,0 +1,276 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import asyncio
import logging
import usb.core
import usb.util
import threading
import time
from colors import color
from .common import Transport, ParserSource
from .. import hci
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_pyusb_transport(spec):
'''
Open a USB transport. [Implementation based on PyUSB]
The parameter string has this syntax:
either <index> or <vendor>:<product>
With <index> as the 0-based index to select amongst all the devices that appear
to be supporting Bluetooth HCI (0 being the first one), or
Where <vendor> and <product> are the vendor ID and product ID in hexadecimal.
Examples:
0 --> the first BT USB dongle
04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901
'''
USB_RECIPIENT_DEVICE = 0x00
USB_REQUEST_TYPE_CLASS = 0x01 << 5
USB_ENDPOINT_EVENTS_IN = 0x81
USB_ENDPOINT_ACL_IN = 0x82
USB_ENDPOINT_SCO_IN = 0x83
USB_ENDPOINT_ACL_OUT = 0x02
# USB_ENDPOINT_SCO_OUT = 0x03
USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0
USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01
USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER = 0x01
READ_SIZE = 1024
READ_TIMEOUT = 1000
class UsbPacketSink:
def __init__(self, device):
self.device = device
self.thread = threading.Thread(target=self.run)
self.loop = asyncio.get_running_loop()
self.stop_event = None
def on_packet(self, packet):
# TODO: don't block here, just queue for the write thread
if len(packet) == 0:
logger.warning('packet too short')
return
packet_type = packet[0]
try:
if packet_type == hci.HCI_ACL_DATA_PACKET:
self.device.write(USB_ENDPOINT_ACL_OUT, packet[1:])
elif packet_type == hci.HCI_COMMAND_PACKET:
self.device.ctrl_transfer(USB_RECIPIENT_DEVICE | USB_REQUEST_TYPE_CLASS, 0, 0, 0, packet[1:])
else:
logger.warning(color(f'unsupported packet type {packet_type}', 'red'))
except usb.core.USBTimeoutError:
logger.warning('USB Write Timeout')
except usb.core.USBError as error:
logger.warning(f'USB write error: {error}')
time.sleep(1) # Sleep one second to avoid busy looping
def start(self):
self.thread.start()
async def stop(self):
# Create stop events and wait for them to be signaled
self.stop_event = asyncio.Event()
await self.stop_event.wait()
def run(self):
while self.stop_event is None:
time.sleep(1)
self.loop.call_soon_threadsafe(lambda: self.stop_event.set())
class UsbPacketSource(asyncio.Protocol, ParserSource):
def __init__(self, device, sco_enabled):
super().__init__()
self.device = device
self.loop = asyncio.get_running_loop()
self.queue = asyncio.Queue()
self.event_thread = threading.Thread(
target=self.run,
args=(USB_ENDPOINT_EVENTS_IN, hci.HCI_EVENT_PACKET)
)
self.event_thread.stop_event = None
self.acl_thread = threading.Thread(
target=self.run,
args=(USB_ENDPOINT_ACL_IN, hci.HCI_ACL_DATA_PACKET)
)
self.acl_thread.stop_event = None
# SCO support is optional
self.sco_enabled = sco_enabled
if sco_enabled:
self.sco_thread = threading.Thread(
target=self.run,
args=(USB_ENDPOINT_SCO_IN, hci.HCI_SYNCHRONOUS_DATA_PACKET)
)
self.sco_thread.stop_event = None
def data_received(self, packet):
self.parser.feed_data(packet)
def enqueue(self, packet):
self.queue.put_nowait(packet)
async def dequeue(self):
while True:
try:
data = await self.queue.get()
except asyncio.CancelledError:
return
self.data_received(data)
def start(self):
self.dequeue_task = self.loop.create_task(self.dequeue())
self.event_thread.start()
self.acl_thread.start()
if self.sco_enabled:
self.sco_thread.start()
async def stop(self):
# Stop the dequeuing task
self.dequeue_task.cancel()
# Create stop events and wait for them to be signaled
self.event_thread.stop_event = asyncio.Event()
self.acl_thread.stop_event = asyncio.Event()
await self.event_thread.stop_event.wait()
await self.acl_thread.stop_event.wait()
if self.sco_enabled:
await self.sco_thread.stop_event.wait()
def run(self, endpoint, packet_type):
# Read until asked to stop
current_thread = threading.current_thread()
while current_thread.stop_event is None:
try:
# Read, with a timeout of 1 second
data = self.device.read(endpoint, READ_SIZE, timeout=READ_TIMEOUT)
packet = bytes([packet_type]) + data.tobytes()
self.loop.call_soon_threadsafe(self.enqueue, packet)
except usb.core.USBTimeoutError:
continue
except usb.core.USBError:
# Don't log this: because pyusb doesn't really support multiple threads
# reading at the same time, we can get occasional USBError(errno=5)
# Input/Output errors reported, but they seem to be harmless.
# Until support for async or multi-thread support is added to pyusb,
# we'll just live with this as is...
# logger.warning(f'USB read error: {error}')
time.sleep(1) # Sleep one second to avoid busy looping
stop_event = current_thread.stop_event
self.loop.call_soon_threadsafe(lambda: stop_event.set())
class UsbTransport(Transport):
def __init__(self, device, source, sink):
super().__init__(source, sink)
self.device = device
async def close(self):
await self.source.stop()
await self.sink.stop()
usb.util.release_interface(self.device, 0)
# Find the device according to the spec moniker
if ':' in spec:
vendor_id, product_id = spec.split(':')
device = usb.core.find(idVendor=int(vendor_id, 16), idProduct=int(product_id, 16))
else:
device_index = int(spec)
devices = list(usb.core.find(
find_all = 1,
bDeviceClass = USB_DEVICE_CLASS_WIRELESS_CONTROLLER,
bDeviceSubClass = USB_DEVICE_SUBCLASS_RF_CONTROLLER,
bDeviceProtocol = USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER
))
if len(devices) > device_index:
device = devices[device_index]
else:
device = None
if device is None:
raise ValueError('device not found')
logger.debug(f'USB Device: {device}')
# Detach the kernel driver if needed
if device.is_kernel_driver_active(0):
logger.debug("detaching kernel driver")
device.detach_kernel_driver(0)
# Set configuration, if needed
try:
configuration = device.get_active_configuration()
except usb.core.USBError:
device.set_configuration()
configuration = device.get_active_configuration()
interface = configuration[(0, 0)]
logger.debug(f'USB Interface: {interface}')
usb.util.claim_interface(device, 0)
# Select an alternate setting for SCO, if available
sco_enabled = False
# NOTE: this is disabled for now, because SCO with alternate settings is broken,
# see: https://github.com/libusb/libusb/issues/36
#
# best_packet_size = 0
# best_interface = None
# sco_enabled = False
# for interface in configuration:
# iso_in_endpoint = None
# iso_out_endpoint = None
# for endpoint in interface:
# if (endpoint.bEndpointAddress == USB_ENDPOINT_SCO_IN and
# usb.util.endpoint_direction(endpoint.bEndpointAddress) == usb.util.ENDPOINT_IN and
# usb.util.endpoint_type(endpoint.bmAttributes) == usb.util.ENDPOINT_TYPE_ISO):
# iso_in_endpoint = endpoint
# continue
# if (endpoint.bEndpointAddress == USB_ENDPOINT_SCO_OUT and
# usb.util.endpoint_direction(endpoint.bEndpointAddress) == usb.util.ENDPOINT_OUT and
# usb.util.endpoint_type(endpoint.bmAttributes) == usb.util.ENDPOINT_TYPE_ISO):
# iso_out_endpoint = endpoint
# if iso_in_endpoint is not None and iso_out_endpoint is not None:
# if iso_out_endpoint.wMaxPacketSize > best_packet_size:
# best_packet_size = iso_out_endpoint.wMaxPacketSize
# best_interface = interface
# if best_interface is not None:
# logger.debug(f'SCO enabled, selecting alternate setting (wMaxPacketSize={best_packet_size}): {best_interface}')
# sco_enabled = True
# try:
# device.set_interface_altsetting(
# interface = best_interface.bInterfaceNumber,
# alternate_setting = best_interface.bAlternateSetting
# )
# except usb.USBError:
# logger.warning('failed to set alternate setting')
packet_source = UsbPacketSource(device, sco_enabled)
packet_sink = UsbPacketSink(device)
packet_source.start()
packet_sink.start()
return UsbTransport(device, packet_source, packet_sink)
+72
View File
@@ -0,0 +1,72 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import asyncio
import logging
import serial_asyncio
from .common import Transport, StreamPacketSource, StreamPacketSink
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_serial_transport(spec):
'''
Open a serial port transport.
The parameter string has this syntax:
<device-path>[,<speed>][,rtscts][,dsrdtr]
When <speed> is omitted, the default value of 1000000 is used
When "rtscts" is specified, RTS/CTS hardware flow control is enabled
When "dsrdtr" is specified, DSR/DTR hardware flow control is enabled
Examples:
/dev/tty.usbmodem0006839912172
/dev/tty.usbmodem0006839912172,1000000
/dev/tty.usbmodem0006839912172,rtscts
'''
speed = 1000000
rtscts = False
dsrdtr = False
if ',' in spec:
parts = spec.split(',')
device = parts[0]
for part in parts[1:]:
if part == 'rtscts':
rtscts = True
elif part == 'dsrdtr':
dsrdtr = True
elif part.isnumeric():
speed = int(part)
else:
device = spec
serial_transport, packet_source = await serial_asyncio.create_serial_connection(
asyncio.get_running_loop(),
lambda: StreamPacketSource(),
device,
baudrate=speed,
rtscts=rtscts,
dsrdtr=dsrdtr
)
packet_sink = StreamPacketSink(serial_transport)
return Transport(packet_source, packet_sink)
+52
View File
@@ -0,0 +1,52 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import asyncio
import logging
from .common import Transport, StreamPacketSource, StreamPacketSink
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_tcp_client_transport(spec):
'''
Open a TCP client transport.
The parameter string has this syntax:
<remote-host>:<remote-port>
Example: 127.0.0.1:9001
'''
class TcpPacketSource(StreamPacketSource):
def connection_lost(self, error):
logger.debug(f'connection lost: {error}')
self.terminated.set_result(error)
remote_host, remote_port = spec.split(':')
tcp_transport, packet_source = await asyncio.get_running_loop().create_connection(
lambda: TcpPacketSource(),
host=remote_host,
port=int(remote_port),
)
packet_sink = StreamPacketSink(tcp_transport)
return Transport(packet_source, packet_sink)
+88
View File
@@ -0,0 +1,88 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import asyncio
import logging
from .common import Transport, StreamPacketSource
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_tcp_server_transport(spec):
'''
Open a TCP server transport.
The parameter string has this syntax:
<local-host>:<local-port>
Where <local-host> may be the address of a local network interface, or '_'
to accept connections on all local network interfaces.
Example: _:9001
'''
class TcpServerTransport(Transport):
async def close(self):
await super().close()
class TcpServerProtocol:
def __init__(self, packet_source, packet_sink):
self.packet_source = packet_source
self.packet_sink = packet_sink
# Called when a new connection is established
def connection_made(self, transport):
peername = transport.get_extra_info('peername')
logger.debug('connection from {}'.format(peername))
self.packet_sink.transport = transport
# Called when the client is disconnected
def connection_lost(self, error):
logger.debug(f'connection lost: {error}')
self.packet_sink.transport = None
def eof_received(self):
logger.debug('connection end')
self.packet_sink.transport = None
# Called when data is received on the socket
def data_received(self, data):
self.packet_source.data_received(data)
class TcpServerPacketSink:
def __init__(self):
self.transport = None
def on_packet(self, packet):
if self.transport:
self.transport.write(packet)
else:
logger.debug('no client, dropping packet')
local_host, local_port = spec.split(':')
packet_source = StreamPacketSource()
packet_sink = TcpServerPacketSink()
await asyncio.get_running_loop().create_server(
lambda: TcpServerProtocol(packet_source, packet_sink),
host=local_host if local_host != '_' else None,
port=int(local_port),
)
return TcpServerTransport(packet_source, packet_sink)
+63
View File
@@ -0,0 +1,63 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import asyncio
import logging
from .common import Transport, ParserSource
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_udp_transport(spec):
'''
Open a UDP transport.
The parameter string has this syntax:
<local-host>:<local-port>,<remote-host>:<remote-port>
Example: 0.0.0.0:9000,127.0.0.1:9001
'''
class UdpPacketSource(asyncio.DatagramProtocol, ParserSource):
def datagram_received(self, data, addr):
self.parser.feed_data(data)
class UdpPacketSink:
def __init__(self, transport):
self.transport = transport
def on_packet(self, packet):
self.transport.sendto(packet)
def close(self):
self.transport.close()
local, remote = spec.split(',')
local_host, local_port = local.split(':')
remote_host, remote_port = remote.split(':')
udp_transport, packet_source = await asyncio.get_running_loop().create_datagram_endpoint(
lambda: UdpPacketSource(),
local_addr=(local_host, int(local_port)),
remote_addr=(remote_host, int(remote_port))
)
packet_sink = UdpPacketSink(udp_transport)
return Transport(packet_source, packet_sink)
+324
View File
@@ -0,0 +1,324 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import asyncio
import logging
import usb1
import threading
import collections
from colors import color
from .common import Transport, ParserSource
from .. import hci
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_usb_transport(spec):
'''
Open a USB transport.
The parameter string has this syntax:
either <index> or <vendor>:<product>
With <index> as the 0-based index to select amongst all the devices that appear
to be supporting Bluetooth HCI (0 being the first one), or
Where <vendor> and <product> are the vendor ID and product ID in hexadecimal.
Examples:
0 --> the first BT USB dongle
04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901
'''
USB_RECIPIENT_DEVICE = 0x00
USB_REQUEST_TYPE_CLASS = 0x01 << 5
USB_ENDPOINT_EVENTS_IN = 0x81
USB_ENDPOINT_ACL_IN = 0x82
USB_ENDPOINT_ACL_OUT = 0x02
USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0
USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01
USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER = 0x01
READ_SIZE = 1024
class UsbPacketSink:
def __init__(self, device):
self.device = device
self.transfer = device.getTransfer()
self.packets = collections.deque() # Queue of packets waiting to be sent
self.loop = asyncio.get_running_loop()
self.cancel_done = self.loop.create_future()
self.closed = False
def start(self):
pass
def on_packet(self, packet):
# Ignore packets if we're closed
if self.closed:
return
if len(packet) == 0:
logger.warning('packet too short')
return
# Queue the packet
self.packets.append(packet)
if len(self.packets) == 1:
# The queue was previously empty, re-prime the pump
self.process_queue()
def on_packet_sent(self, transfer):
status = transfer.getStatus()
# logger.debug(f'<<< USB out transfer callback: status={status}')
if status == usb1.TRANSFER_COMPLETED:
self.loop.call_soon_threadsafe(self.on_packet_sent_)
elif status == usb1.TRANSFER_CANCELLED:
self.loop.call_soon_threadsafe(self.cancel_done.set_result, None)
else:
logger.warning(color(f'!!! out transfer not completed: status={status}', 'red'))
def on_packet_sent_(self):
if self.packets:
self.packets.popleft()
self.process_queue()
def process_queue(self):
if len(self.packets) == 0:
return # Nothing to do
packet = self.packets[0]
packet_type = packet[0]
if packet_type == hci.HCI_ACL_DATA_PACKET:
self.transfer.setBulk(
USB_ENDPOINT_ACL_OUT,
packet[1:],
callback=self.on_packet_sent
)
logger.debug('submit ACL')
self.transfer.submit()
elif packet_type == hci.HCI_COMMAND_PACKET:
self.transfer.setControl(
USB_RECIPIENT_DEVICE | USB_REQUEST_TYPE_CLASS, 0, 0, 0,
packet[1:],
callback=self.on_packet_sent
)
logger.debug('submit COMMAND')
self.transfer.submit()
else:
logger.warning(color(f'unsupported packet type {packet_type}', 'red'))
async def close(self):
self.closed = True
# Empty the packet queue so that we don't send any more data
self.packets.clear()
# If we have a transfer in flight, cancel it
if self.transfer.isSubmitted():
# Try to cancel the transfer, but that may fail because it may have already completed
try:
self.transfer.cancel()
logger.debug('waiting for OUT transfer cancellation to be done...')
await self.cancel_done
logger.debug('OUT transfer cancellation done')
except usb1.USBError:
logger.debug('OUT transfer likely already completed')
class UsbPacketSource(asyncio.Protocol, ParserSource):
def __init__(self, context, device):
super().__init__()
self.context = context
self.device = device
self.loop = asyncio.get_running_loop()
self.queue = asyncio.Queue()
self.closed = False
self.event_loop_done = self.loop.create_future()
self.cancel_done = {
hci.HCI_EVENT_PACKET: self.loop.create_future(),
hci.HCI_ACL_DATA_PACKET: self.loop.create_future()
}
# Create a thread to process events
self.event_thread = threading.Thread(target=self.run)
def start(self):
# Set up transfer objects for input
self.events_in_transfer = device.getTransfer()
self.events_in_transfer.setInterrupt(
USB_ENDPOINT_EVENTS_IN,
READ_SIZE,
callback=self.on_packet_received,
user_data=hci.HCI_EVENT_PACKET
)
self.events_in_transfer.submit()
self.acl_in_transfer = device.getTransfer()
self.acl_in_transfer.setBulk(
USB_ENDPOINT_ACL_IN,
READ_SIZE,
callback=self.on_packet_received,
user_data=hci.HCI_ACL_DATA_PACKET
)
self.acl_in_transfer.submit()
self.dequeue_task = self.loop.create_task(self.dequeue())
self.event_thread.start()
def on_packet_received(self, transfer):
packet_type = transfer.getUserData()
status = transfer.getStatus()
# logger.debug(f'<<< USB IN transfer callback: status={status} packet_type={packet_type}')
if status == usb1.TRANSFER_COMPLETED:
packet = bytes([packet_type]) + transfer.getBuffer()[:transfer.getActualLength()]
self.loop.call_soon_threadsafe(self.queue.put_nowait, packet)
elif status == usb1.TRANSFER_CANCELLED:
self.loop.call_soon_threadsafe(self.cancel_done[packet_type].set_result, None)
return
else:
logger.warning(color(f'!!! transfer not completed: status={status}', 'red'))
# Re-submit the transfer so we can receive more data
transfer.submit()
async def dequeue(self):
while not self.closed:
try:
packet = await self.queue.get()
except asyncio.CancelledError:
return
self.parser.feed_data(packet)
def run(self):
logger.debug('starting USB event loop')
while self.events_in_transfer.isSubmitted() or self.acl_in_transfer.isSubmitted():
try:
self.context.handleEvents()
except usb1.USBErrorInterrupted:
pass
logger.debug('USB event loop done')
self.event_loop_done.set_result(None)
async def close(self):
self.closed = True
self.dequeue_task.cancel()
# Cancel the transfers
for transfer in (self.events_in_transfer, self.acl_in_transfer):
if transfer.isSubmitted():
# Try to cancel the transfer, but that may fail because it may have already completed
packet_type = transfer.getUserData()
try:
transfer.cancel()
logger.debug(f'waiting for IN[{packet_type}] transfer cancellation to be done...')
await self.cancel_done[packet_type]
logger.debug(f'IN[{packet_type}] transfer cancellation done')
except usb1.USBError:
logger.debug(f'IN[{packet_type}] transfer likely already completed')
# Wait for the thread to terminate
await self.event_loop_done
class UsbTransport(Transport):
def __init__(self, context, device, interface, source, sink):
super().__init__(source, sink)
self.context = context
self.device = device
self.interface = interface
# Get exclusive access
device.claimInterface(interface)
# The source and sink can now start
source.start()
sink.start()
async def close(self):
await self.source.close()
await self.sink.close()
self.device.releaseInterface(self.interface)
self.device.close()
self.context.close()
# Find the device according to the spec moniker
context = usb1.USBContext()
context.open()
try:
found = None
if ':' in spec:
vendor_id, product_id = spec.split(':')
found = context.getByVendorIDAndProductID(int(vendor_id, 16), int(product_id, 16), skip_on_error=True)
else:
device_index = int(spec)
device_iterator = context.getDeviceIterator(skip_on_error=True)
try:
for device in device_iterator:
if device.getDeviceClass() == USB_DEVICE_CLASS_WIRELESS_CONTROLLER and \
device.getDeviceSubClass() == USB_DEVICE_SUBCLASS_RF_CONTROLLER and \
device.getDeviceProtocol() == USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER:
if device_index == 0:
found = device
break
device_index -= 1
device.close()
finally:
device_iterator.close()
if found is None:
context.close()
raise ValueError('device not found')
logger.debug(f'USB Device: {found}')
device = found.open()
# Set the configuration if needed
try:
configuration = device.getConfiguration()
logger.debug(f'current configuration = {configuration}')
except usb1.USBError:
try:
logger.debug('setting configuration 1')
device.setConfiguration(1)
except usb1.USBError:
logger.debug('failed to set configuration 1')
# Use the first interface
interface = 0
# Detach the kernel driver if supported and needed
if usb1.hasCapability(usb1.CAP_SUPPORTS_DETACH_KERNEL_DRIVER):
try:
if device.kernelDriverActive(interface):
logger.debug("detaching kernel driver")
device.detachKernelDriver(interface)
except usb1.USBError:
pass
source = UsbPacketSource(context, device)
sink = UsbPacketSink(device)
return UsbTransport(context, device, interface, source, sink)
except usb1.USBError as error:
logger.warning(color(f'!!! failed to open USB device: {error}', 'red'))
context.close()
raise
+59
View File
@@ -0,0 +1,59 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import logging
from .file import open_file_transport
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_vhci_transport(spec):
'''
Open a VHCI transport (only available on some platforms).
The parameter string is either empty (to use the default VHCI device
path at /dev/vhci), or the path of a VHCI device
'''
HCI_VENDOR_PKT = 0xff
HCI_BREDR = 0x00 # Controller type
# Open the VHCI device
transport = await open_file_transport(spec or '/dev/vhci')
# Override the source's `data_received` method so that we can
# filter out the vendor packet that is received just after the
# initial open
def vhci_data_received(data):
if len(data) > 0 and data[0] == HCI_VENDOR_PKT:
if len(data) == 4:
hci_index = data[2] << 8 | data[3]
logger.info(f'HCI index {hci_index}')
else:
transport.source.parser.feed_data(data)
transport.source.data_received = vhci_data_received
# Write the initial config
transport.sink.on_packet(bytes([HCI_VENDOR_PKT, HCI_BREDR]))
return transport
+49
View File
@@ -0,0 +1,49 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import logging
import websockets
from .common import PumpedPacketSource, PumpedPacketSink, PumpedTransport
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_ws_client_transport(spec):
'''
Open a WebSocket client transport.
The parameter string has this syntax:
<remote-host>:<remote-port>
Example: 127.0.0.1:9001
'''
remote_host, remote_port = spec.split(':')
uri = f'ws://{remote_host}:{remote_port}'
websocket = await websockets.connect(uri)
transport = PumpedTransport(
PumpedPacketSource(websocket.recv),
PumpedPacketSink(websocket.send),
websocket.close
)
transport.start()
return transport
+81
View File
@@ -0,0 +1,81 @@
# 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.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import asyncio
import logging
import websockets
from .common import Transport, ParserSource, PumpedPacketSink
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
async def open_ws_server_transport(spec):
'''
Open a WebSocket server transport.
The parameter string has this syntax:
<local-host>:<local-port>
Where <local-host> may be the address of a local network interface, or '_'
to accept connections on all local network interfaces.
Example: _:9001
'''
class WsServerTransport(Transport):
def __init__(self):
source = ParserSource()
sink = PumpedPacketSink(self.send_packet)
self.connection = asyncio.get_running_loop().create_future()
super().__init__(source, sink)
async def serve(self, local_host, local_port):
self.sink.start()
self.server = await websockets.serve(
ws_handler = self.on_connection,
host = local_host if local_host != '_' else None,
port = int(local_port)
)
logger.debug(f'websocket server ready on port {local_port}')
async def on_connection(self, connection):
logger.debug(f'new connection on {connection.local_address} from {connection.remote_address}')
self.connection.set_result(connection)
try:
async for packet in connection:
if type(packet) is bytes:
self.source.parser.feed_data(packet)
else:
logger.warn('discarding packet: not a BINARY frame')
except websockets.WebSocketException as error:
logger.debug(f'exception while receiving packet: {error}')
# Wait for a new connection
self.connection = asyncio.get_running_loop().create_future()
async def send_packet(self, packet):
connection = await self.connection
return await connection.send(packet)
local_host, local_port = spec.split(':')
transport = WsServerTransport()
await transport.serve(local_host, local_port)
return transport