asyncio implementation

This commit is contained in:
Robert Martin
2020-01-27 22:41:17 +09:00
parent 913848bcf2
commit b3431b6caa
2 changed files with 276 additions and 3 deletions
+5 -3
View File
@@ -220,6 +220,10 @@ if __name__ == '__main__':
ctl_sock = blt.BluetoothSocket(blt.L2CAP)
itr_sock = blt.BluetoothSocket(blt.L2CAP)
logger.info('Restarting bluetooth service...')
os.system('systemctl restart bluetooth.service')
time.sleep(1)
logger.info(f'Binding control channel to {CTL_PSM}...')
ctl_sock.bind(("", CTL_PSM))
logger.info(f'Binding interrupt channel to {ITR_PSM}...')
@@ -229,9 +233,7 @@ if __name__ == '__main__':
ctl_sock.listen(1) # Limit of 1 connection
itr_sock.listen(1)
logger.info('Restarting bluetooth service...')
os.system('systemctl restart bluetooth.service')
time.sleep(1)
hid = HidDevice()
# setting bluetooth adapter name and class to the device we wish to emulate
+271
View File
@@ -0,0 +1,271 @@
import asyncio
import enum
import logging
import socket
import uuid
from asyncio import BaseTransport, BaseProtocol, Transport
from typing import Optional, Union, Tuple, Text, Any
import dbus
import logging_default as log
logger = logging.getLogger(__name__)
class L2CAP_Transport(Transport):
def __init__(self, loop, protocol, l2cap_socket, client_addr, read_buffer_size) -> None:
self._loop = loop
self._protocol = protocol
self._sock = l2cap_socket
self._client_addr = client_addr
self._read_buffer_size = read_buffer_size
self._read_thread = asyncio.ensure_future(self._read())
self._is_closing = False
self._is_reading = asyncio.Event()
self._is_reading.set()
async def _read(self):
try:
while True:
await self._is_reading.wait()
data = await self._loop.sock_recv(self._sock, self._read_buffer_size)
logger.debug(f'received "{data}')
await self._protocol.report_received(data, self._client_addr)
except asyncio.CancelledError:
# reading has been stopped
pass
def is_reading(self) -> bool:
return self._is_reading.is_set()
def pause_reading(self) -> None:
self._is_reading.clear()
def resume_reading(self) -> None:
self._is_reading.set()
def set_read_buffer_size(self, size):
self._read_buffer_size = size
def set_write_buffer_limits(self, high: int = ..., low: int = ...) -> None:
super().set_write_buffer_limits(high, low)
def get_write_buffer_size(self) -> int:
return super().get_write_buffer_size()
async def write(self, data: Any) -> None:
logger.debug(f'sending "{data}"')
await self._loop.sock_sendall(self._sock, data)
def abort(self) -> None:
super().abort()
def get_extra_info(self, name: Any, default: Any = ...) -> Any:
return super().get_extra_info(name, default)
def is_closing(self) -> bool:
return self._is_closing
async def close(self):
"""
Stops socket reader and closes socket
"""
self._is_closing = True
self._read_thread.cancel()
# wait for reader to cancel
try:
await self._read_thread
except asyncio.CancelledError:
pass
self._sock.close()
def set_protocol(self, protocol: BaseProtocol) -> None:
self._protocol = protocol
def get_protocol(self) -> BaseProtocol:
return self._protocol
class Controller(enum.Enum):
JOYCON_L = 0x01
JOYCON_R = 0x02
PRO_CONTROLLER = 0x03
def device_name(self):
"""
:returns corresponding bluetooth device name
"""
if self == Controller.JOYCON_L:
return 'Joy-Con (L)'
elif self == Controller.JOYCON_R:
return 'Joy-Con (R)'
elif self == Controller.PRO_CONTROLLER:
return 'Pro Controller'
else:
raise NotImplementedError()
def controller_protocol_factory(controller: Controller):
def create_controller_protocol():
return ControllerProtocol(controller)
return create_controller_protocol
class ControllerProtocol(BaseProtocol):
def __init__(self, controller: Controller):
self.transport = None
def connection_made(self, transport: BaseTransport) -> None:
logger.debug('Connection established.')
self.transport = transport
def connection_lost(self, exc: Optional[Exception]) -> None:
raise NotImplementedError()
async def report_received(self, data: Union[bytes, Text], addr: Tuple[str, int]) -> None:
raise NotImplementedError()
def error_received(self, exc: Exception) -> None:
raise NotImplementedError()
async def run_system_command(cmd):
proc = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE)
stdout, stderr = await proc.communicate()
logger.debug(f'[{cmd!r} exited with {proc.returncode}]')
if stdout:
logger.debug(f'[stdout]\n{stdout.decode()}')
if stderr:
logger.debug(f'[stderr]\n{stderr.decode()}')
return proc.returncode
class HidDevice:
_HID_UUID = '00001124-0000-1000-8000-00805f9b34fb'
_HID_PATH = '/bluez/switch/hid'
PRO_CONTROLLER = 'Pro Controller'
JOYCON_R = 'Joy-Con (R)'
JOYCON_L = 'Joy-Con (L)'
def __init__(self):
self._uuid = str(uuid.uuid4())
# Setting up dbus to advertise the service record
bus = dbus.SystemBus()
obj = bus.get_object('org.bluez', '/org/bluez/hci0')
self.adapter = dbus.Interface(obj, 'org.bluez.Adapter1')
self.properties = dbus.Interface(self.adapter, 'org.freedesktop.DBus.Properties')
def discoverable(self, boolean=True):
#self.properties.Set(self.adapter.dbus_interface, 'Powered', True)
self.properties.Set(self.adapter.dbus_interface, 'Discoverable', boolean)
async def set_class(self, cls=0x002508):
"""
:param cls: default 0x002508 (Gamepad/joystick device class)
"""
logger.info(f'setting device class to {cls}...')
await run_system_command(f'hciconfig hci0 class {cls}')
async def set_name(self, name: str):
logger.info(f'setting device name to {name}...')
await run_system_command(f'hciconfig hci0 name "{name}"')
def register_sdp_record(self, record_path):
with open(record_path) as record:
opts = {
'ServiceRecord': record.read(),
'Role': 'server',
'Service': self._HID_UUID,
'RequireAuthentication': False,
'RequireAuthorization': False
}
bus = dbus.SystemBus()
manager = dbus.Interface(bus.get_object("org.bluez", "/org/bluez"), "org.bluez.ProfileManager1")
manager.RegisterProfile(self._HID_PATH, self._uuid, opts)
async def create_hid_server(protocol_factory, ctl_psm, itr_psm):
ctl_sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP)
itr_sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP)
# for some reason we need to restart bluetooth here, the Switch does not connect to the sockets if we don't...
logger.info('Restarting bluetooth service...')
await run_system_command('systemctl restart bluetooth.service')
await asyncio.sleep(1)
ctl_sock.setblocking(False)
itr_sock.setblocking(False)
ctl_sock.bind((socket.BDADDR_ANY, ctl_psm))
itr_sock.bind((socket.BDADDR_ANY, itr_psm))
ctl_sock.listen(1)
itr_sock.listen(1)
hid = HidDevice()
# setting bluetooth adapter name and class to the device we wish to emulate
await hid.set_name(HidDevice.JOYCON_L)
await hid.set_class()
logger.info('Advertising the Bluetooth SDP record...')
hid.register_sdp_record('profile/sdp_record_hid_pro.xml')
hid.discoverable()
loop = asyncio.get_event_loop()
client_ctl, address = await loop.sock_accept(ctl_sock)
logger.info(f'Accepted connection at psm {ctl_psm} from {address}')
client_itr, address = await loop.sock_accept(itr_sock)
logger.info(f'Accepted connection at psm {itr_psm} from {address}')
protocol = protocol_factory()
transport = L2CAP_Transport(asyncio.get_event_loop(), protocol, client_itr, address, 50)
protocol.connection_made(transport)
return transport
async def send_empty_input_reports(transport):
data = [0x00] * 50
data[0] = 0xA1
while True:
await transport.write(bytes(data))
await asyncio.sleep(1)
async def main():
transport = await create_hid_server(controller_protocol_factory(Controller.JOYCON_L), 17, 19)
future = asyncio.ensure_future(send_empty_input_reports(transport))
await asyncio.sleep(10)
future.cancel()
try:
await future
except asyncio.CancelledError:
pass
await transport.close()
if __name__ == '__main__':
# setup logging
log.configure()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())