forked from auracaster/bumble_mirror
Compare commits
11 Commits
gbg/multi-
...
gbg/event-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b50a48fed3 | ||
|
|
393ea6a7bb | ||
|
|
0d36d99a73 | ||
|
|
d8a9f5a724 | ||
|
|
2c66e1a042 | ||
|
|
d5eccdb00f | ||
|
|
32626573a6 | ||
|
|
caa82b8f7e | ||
|
|
5af347b499 | ||
|
|
4ed5bb5a9e | ||
|
|
f39f5f531c |
@@ -3,7 +3,7 @@ import click
|
||||
import logging
|
||||
import json
|
||||
|
||||
from bumble.pandora import PandoraDevice, serve
|
||||
from bumble.pandora import PandoraDevice, Config, serve
|
||||
from typing import Dict, Any
|
||||
|
||||
BUMBLE_SERVER_GRPC_PORT = 7999
|
||||
@@ -29,12 +29,14 @@ def main(grpc_port: int, rootcanal_port: int, transport: str, config: str) -> No
|
||||
transport = transport.replace('<rootcanal-port>', str(rootcanal_port))
|
||||
|
||||
bumble_config = retrieve_config(config)
|
||||
if 'transport' not in bumble_config.keys():
|
||||
bumble_config.update({'transport': transport})
|
||||
bumble_config.setdefault('transport', transport)
|
||||
device = PandoraDevice(bumble_config)
|
||||
|
||||
server_config = Config()
|
||||
server_config.load_from_dict(bumble_config.get('server', {}))
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
asyncio.run(serve(device, port=grpc_port))
|
||||
asyncio.run(serve(device, config=server_config, port=grpc_port))
|
||||
|
||||
|
||||
def retrieve_config(config: str) -> Dict[str, Any]:
|
||||
|
||||
@@ -80,7 +80,7 @@ class BaseError(Exception):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
error_code: int | None,
|
||||
error_code: Optional[int],
|
||||
error_namespace: str = '',
|
||||
error_name: str = '',
|
||||
details: str = '',
|
||||
|
||||
@@ -4397,7 +4397,7 @@ class HCI_Event(HCI_Packet):
|
||||
if len(parameters) != length:
|
||||
raise ValueError('invalid packet length')
|
||||
|
||||
cls: Type[HCI_Event | HCI_LE_Meta_Event] | None
|
||||
cls: Any
|
||||
if event_code == HCI_LE_META_EVENT:
|
||||
# We do this dispatch here and not in the subclass in order to avoid call
|
||||
# loops
|
||||
|
||||
@@ -757,7 +757,7 @@ class Channel(EventEmitter):
|
||||
)
|
||||
self.state = new_state
|
||||
|
||||
def send_pdu(self, pdu: SupportsBytes | bytes) -> None:
|
||||
def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
|
||||
self.manager.send_pdu(self.connection, self.destination_cid, pdu)
|
||||
|
||||
def send_control_frame(self, frame: L2CAP_Control_Frame) -> None:
|
||||
@@ -1098,7 +1098,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
||||
elif new_state == self.DISCONNECTED:
|
||||
self.emit('close')
|
||||
|
||||
def send_pdu(self, pdu: SupportsBytes | bytes) -> None:
|
||||
def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
|
||||
self.manager.send_pdu(self.connection, self.destination_cid, pdu)
|
||||
|
||||
def send_control_frame(self, frame: L2CAP_Control_Frame) -> None:
|
||||
@@ -1571,7 +1571,7 @@ class ChannelManager:
|
||||
if connection_handle in self.identifiers:
|
||||
del self.identifiers[connection_handle]
|
||||
|
||||
def send_pdu(self, connection, cid: int, pdu: SupportsBytes | bytes) -> None:
|
||||
def send_pdu(self, connection, cid: int, pdu: Union[SupportsBytes, bytes]) -> None:
|
||||
pdu_str = pdu.hex() if isinstance(pdu, bytes) else str(pdu)
|
||||
logger.debug(
|
||||
f'{color(">>> Sending L2CAP PDU", "blue")} '
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
import logging
|
||||
import grpc.aio
|
||||
|
||||
from typing import Optional, Union
|
||||
|
||||
from .common import PumpedTransport, PumpedPacketSource, PumpedPacketSink, Transport
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
@@ -33,7 +35,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_android_emulator_transport(spec: str | None) -> Transport:
|
||||
async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
|
||||
'''
|
||||
Open a transport connection to an Android emulator via its gRPC interface.
|
||||
The parameter string has this syntax:
|
||||
@@ -82,7 +84,7 @@ async def open_android_emulator_transport(spec: str | None) -> Transport:
|
||||
logger.debug(f'connecting to gRPC server at {server_address}')
|
||||
channel = grpc.aio.insecure_channel(server_address)
|
||||
|
||||
service: EmulatedBluetoothServiceStub | VhciForwardingServiceStub
|
||||
service: Union[EmulatedBluetoothServiceStub, VhciForwardingServiceStub]
|
||||
if mode == 'host':
|
||||
# Connect as a host
|
||||
service = EmulatedBluetoothServiceStub(channel)
|
||||
|
||||
@@ -122,7 +122,7 @@ def publish_grpc_port(grpc_port) -> bool:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_android_netsim_controller_transport(
|
||||
server_host: str | None, server_port: int
|
||||
server_host: Optional[str], server_port: int
|
||||
) -> Transport:
|
||||
if not server_port:
|
||||
raise ValueError('invalid port')
|
||||
|
||||
@@ -23,6 +23,8 @@ import socket
|
||||
import ctypes
|
||||
import collections
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .common import Transport, ParserSource
|
||||
|
||||
|
||||
@@ -33,7 +35,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_hci_socket_transport(spec: str | None) -> Transport:
|
||||
async def open_hci_socket_transport(spec: Optional[str]) -> Transport:
|
||||
'''
|
||||
Open an HCI Socket (only available on some platforms).
|
||||
The parameter string is either empty (to use the first/default Bluetooth adapter)
|
||||
@@ -45,9 +47,9 @@ async def open_hci_socket_transport(spec: str | None) -> Transport:
|
||||
# Create a raw HCI socket
|
||||
try:
|
||||
hci_socket = socket.socket(
|
||||
socket.AF_BLUETOOTH,
|
||||
socket.SOCK_RAW | socket.SOCK_NONBLOCK,
|
||||
socket.BTPROTO_HCI, # type: ignore
|
||||
socket.AF_BLUETOOTH, # type: ignore[attr-defined]
|
||||
socket.SOCK_RAW | socket.SOCK_NONBLOCK, # type: ignore[attr-defined]
|
||||
socket.BTPROTO_HCI, # type: ignore[attr-defined]
|
||||
)
|
||||
except AttributeError as error:
|
||||
# Not supported on this platform
|
||||
@@ -78,7 +80,7 @@ async def open_hci_socket_transport(spec: str | None) -> Transport:
|
||||
bind_address = struct.pack(
|
||||
# pylint: disable=no-member
|
||||
'<HHH',
|
||||
socket.AF_BLUETOOTH,
|
||||
socket.AF_BLUETOOTH, # type: ignore[attr-defined]
|
||||
adapter_index,
|
||||
HCI_CHANNEL_USER,
|
||||
)
|
||||
|
||||
@@ -23,6 +23,8 @@ import atexit
|
||||
import os
|
||||
import logging
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -32,7 +34,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_pty_transport(spec: str | None) -> Transport:
|
||||
async def open_pty_transport(spec: Optional[str]) -> Transport:
|
||||
'''
|
||||
Open a PTY transport.
|
||||
The parameter string may be empty, or a path name where a symbolic link
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .common import Transport
|
||||
from .file import open_file_transport
|
||||
|
||||
@@ -27,7 +29,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_vhci_transport(spec: str | None) -> Transport:
|
||||
async def open_vhci_transport(spec: Optional[str]) -> Transport:
|
||||
'''
|
||||
Open a VHCI transport (only available on some platforms).
|
||||
The parameter string is either empty (to use the default VHCI device
|
||||
|
||||
@@ -19,9 +19,19 @@ import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
import collections
|
||||
from contextlib import ExitStack
|
||||
import sys
|
||||
from typing import Awaitable, Set, TypeVar
|
||||
from functools import wraps
|
||||
from typing import (
|
||||
Awaitable,
|
||||
Callable,
|
||||
ContextManager,
|
||||
Mapping,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
)
|
||||
import functools
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .colors import color
|
||||
@@ -167,7 +177,7 @@ class AsyncRunner:
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
coroutine = func(*args, **kwargs)
|
||||
if queue is None:
|
||||
@@ -302,3 +312,77 @@ class FlowControlAsyncPipe:
|
||||
self.resume_source()
|
||||
|
||||
self.check_pump()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def event_emitter_once_for_group(
|
||||
emitter: EventEmitter,
|
||||
handlers: Mapping[str, Callable],
|
||||
context: Optional[ContextManager] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Register event listeners as a group, with optional context manager.
|
||||
|
||||
For each entry in the map, an event listener is registered with the emitter.
|
||||
When any of the event names in the handlers map is emitted by the emitter,
|
||||
the corresponding handler is invoked, then all of the listeners are removed from
|
||||
the emitter.
|
||||
If a context manager is specified, it will be entered prior to registering the
|
||||
listeners, and exited when any of the events is emitted.
|
||||
|
||||
Args:
|
||||
emitter:
|
||||
The event emitter with which to register the event listeners.
|
||||
handlers:
|
||||
A map that associates an event name with an event handler.
|
||||
context:
|
||||
A context manager to manager resources, or None if not needed.
|
||||
"""
|
||||
event_emitters_once_for_group(
|
||||
{(emitter, event_name): handler for event_name, handler in handlers.items()},
|
||||
context,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def event_emitters_once_for_group(
|
||||
handlers: Mapping[Tuple[EventEmitter, str], Callable],
|
||||
context: Optional[ContextManager] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Register event listeners as a group, with optional context manager.
|
||||
|
||||
Similar to event_emitter_once_for_group, but instead of registering the listeners
|
||||
with a single emitter, each event may by associate with a different emitter.
|
||||
|
||||
Args:
|
||||
handlers:
|
||||
A map that associates an (emitter, event_name) pair with an event handler.
|
||||
context:
|
||||
A context manager to manager resources, or None if not needed.
|
||||
"""
|
||||
# Setup an exit stack to enter and exit the context, if any.
|
||||
if context is not None:
|
||||
exit_stack = ExitStack()
|
||||
exit_stack.enter_context(context)
|
||||
else:
|
||||
exit_stack = None
|
||||
|
||||
def on_event(handler, *args, **kwargs) -> None:
|
||||
# Invoke the handler.
|
||||
handler(*args, **kwargs)
|
||||
|
||||
# Release the context, if any.
|
||||
if exit_stack is not None:
|
||||
exit_stack.close()
|
||||
|
||||
# Remove all listeners.
|
||||
for (emitter, event_name), listener in listeners.items():
|
||||
emitter.remove_listener(event_name, listener)
|
||||
|
||||
listeners = {
|
||||
(emitter, event_name): emitter.on(
|
||||
event_name, functools.partial(on_event, handler)
|
||||
)
|
||||
for (emitter, event_name), handler in handlers.items()
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ development =
|
||||
black == 22.10
|
||||
grpcio-tools >= 1.57.0
|
||||
invoke >= 1.7.3
|
||||
mypy == 1.2.0
|
||||
mypy == 1.5.0
|
||||
nox >= 2022
|
||||
pylint == 2.15.8
|
||||
types-appdirs >= 1.4.3
|
||||
|
||||
122
tests/utils_test.py
Normal file
122
tests/utils_test.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# 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 contextlib
|
||||
from pyee import EventEmitter
|
||||
|
||||
from bumble.utils import event_emitter_once_for_group, event_emitters_once_for_group
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_event_emitter_once_for_group():
|
||||
results = {'event1': None, 'event2': None, 'released': 0}
|
||||
|
||||
def handler1(x):
|
||||
results['event1'] = x
|
||||
|
||||
def handler2(y):
|
||||
results['event2'] = y
|
||||
|
||||
emitter = EventEmitter()
|
||||
|
||||
event_emitter_once_for_group(
|
||||
emitter,
|
||||
{
|
||||
'event1': handler1,
|
||||
'event2': handler2,
|
||||
},
|
||||
)
|
||||
|
||||
emitter.emit('event1', 'hello')
|
||||
|
||||
assert results['event1'] == 'hello'
|
||||
assert results['event2'] is None
|
||||
|
||||
results['event1'] = None
|
||||
|
||||
emitter.emit('event1', 'hello')
|
||||
emitter.emit('event2', 1234)
|
||||
|
||||
assert results['event1'] is None
|
||||
assert results['event2'] is None
|
||||
|
||||
@contextlib.contextmanager
|
||||
def managed():
|
||||
try:
|
||||
yield 1234
|
||||
finally:
|
||||
results['released'] += 1
|
||||
|
||||
event_emitter_once_for_group(
|
||||
emitter,
|
||||
{
|
||||
'event1': handler1,
|
||||
'event2': handler2,
|
||||
},
|
||||
managed(),
|
||||
)
|
||||
|
||||
assert results['released'] == 0
|
||||
|
||||
emitter.emit('event2', 7756)
|
||||
|
||||
assert results['event1'] is None
|
||||
assert results['event2'] == 7756
|
||||
assert results['released'] == 1
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_event_emitters_once_for_group():
|
||||
results = {'event1': None, 'event2': None, 'released': 0}
|
||||
|
||||
def handler1(x):
|
||||
results['event1'] = x
|
||||
|
||||
def handler2(y):
|
||||
results['event2'] = y
|
||||
|
||||
emitter1 = EventEmitter()
|
||||
emitter2 = EventEmitter()
|
||||
|
||||
event_emitters_once_for_group(
|
||||
{
|
||||
(emitter1, 'event1'): handler1,
|
||||
(emitter2, 'event2'): handler2,
|
||||
},
|
||||
)
|
||||
|
||||
emitter1.emit('event1', 'hello')
|
||||
emitter2.emit('event1', 'foobar')
|
||||
|
||||
assert results['event1'] == 'hello'
|
||||
assert results['event2'] is None
|
||||
|
||||
results['event1'] = None
|
||||
|
||||
emitter1.emit('event1', 'hello')
|
||||
emitter1.emit('event2', 1234)
|
||||
emitter2.emit('event1', 'hello')
|
||||
emitter2.emit('event2', 1234)
|
||||
|
||||
assert results['event1'] is None
|
||||
assert results['event2'] is None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
test_event_emitter_once_for_group()
|
||||
test_event_emitters_once_for_group()
|
||||
Reference in New Issue
Block a user