request device info

This commit is contained in:
Robert Martin
2020-01-28 23:27:32 +09:00
parent 60ad6298df
commit ed847601cf
7 changed files with 143 additions and 50 deletions
+20
View File
@@ -0,0 +1,20 @@
import enum
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()
-4
View File
@@ -12,10 +12,6 @@ class HidDevice:
_HID_UUID = '00001124-0000-1000-8000-00805f9b34fb' _HID_UUID = '00001124-0000-1000-8000-00805f9b34fb'
_HID_PATH = '/bluez/switch/hid' _HID_PATH = '/bluez/switch/hid'
PRO_CONTROLLER = 'Pro Controller'
JOYCON_R = 'Joy-Con (R)'
JOYCON_L = 'Joy-Con (L)'
def __init__(self): def __init__(self):
self._uuid = str(uuid.uuid4()) self._uuid = str(uuid.uuid4())
+39 -22
View File
@@ -1,31 +1,14 @@
import asyncio import asyncio
import enum
import logging import logging
from asyncio import BaseTransport, BaseProtocol from asyncio import BaseTransport, BaseProtocol
from typing import Optional, Union, Tuple, Text from typing import Optional, Union, Tuple, Text
from controller import Controller
from report import OutputReport, SubCommand, InputReport
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
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 controller_protocol_factory(controller: Controller):
def create_controller_protocol(): def create_controller_protocol():
return ControllerProtocol(controller) return ControllerProtocol(controller)
@@ -34,6 +17,8 @@ def controller_protocol_factory(controller: Controller):
class ControllerProtocol(BaseProtocol): class ControllerProtocol(BaseProtocol):
def __init__(self, controller: Controller): def __init__(self, controller: Controller):
self.controller = controller
self.transport = None self.transport = None
self._data_received = asyncio.Event() self._data_received = asyncio.Event()
@@ -49,8 +34,40 @@ class ControllerProtocol(BaseProtocol):
def connection_lost(self, exc: Optional[Exception]) -> None: def connection_lost(self, exc: Optional[Exception]) -> None:
raise NotImplementedError() raise NotImplementedError()
def error_received(self, exc: Exception) -> None:
raise NotImplementedError()
async def report_received(self, data: Union[bytes, Text], addr: Tuple[str, int]) -> None: async def report_received(self, data: Union[bytes, Text], addr: Tuple[str, int]) -> None:
self._data_received.set() self._data_received.set()
def error_received(self, exc: Exception) -> None: try:
raise NotImplementedError() report = OutputReport(list(data))
except ValueError as v_err:
logger.warning(f'Report parsing error "{v_err}" - IGNORE')
return
# classify sub command
sub_command = report.get_sub_command()
logging.info(f'received output report - {sub_command}')
if sub_command is None:
logger.error(f'No sub command found')
elif sub_command == SubCommand.REQUEST_DEVICE_INFO:
await self._command_request_device_info(report)
elif sub_command == SubCommand.NOT_IMPLEMENTED:
logger.error(f'Sub command not implemented')
async def _command_request_device_info(self, output_report):
address = self.transport.get_extra_info('sockname')
assert address is not None
bd_address = list(map(lambda x: int(x, 16), address[0].split(':')))
input_report = InputReport()
input_report.set_input_report_id(0x21)
input_report.set_misc()
input_report.set_button_status()
input_report.set_left_analog_stick()
input_report.set_right_analog_stick()
input_report.set_vibrator_input()
input_report.sub_0x2_device_info(bd_address)
asyncio.ensure_future(self.transport.write(bytes(input_report)))
+51 -13
View File
@@ -1,5 +1,13 @@
from enum import Enum, auto from enum import Enum, auto
#[0xA1 0, 0x21 1, 0x05 2, 0x8E 3,
# 0x84, 0x00, 0x12, button
# 0x01, 0x18, 0x80, left analog
# 0x01, 0x18, 0x80, right analog
# 0x80, vibrator?
# 0x82, 0x02, 0x03, 0x48, 0x01, 0x02, 0xDC, 0xA6, 0x32, 0x71, 0x58, 0xBB, 0x01, 0x01]
from controller import Controller
class InputReport: class InputReport:
def __init__(self): def __init__(self):
@@ -7,21 +15,50 @@ class InputReport:
# all input reports are prepended with 0xA1 # all input reports are prepended with 0xA1
self.data[0] = 0xA1 self.data[0] = 0xA1
def set(self, input_report_id, timer=0x00): def set_input_report_id(self, _id):
self.data[1] = input_report_id """
:param _id: e.g. 0x21 Standard input reports used for subcommand replies, etc... (TODO)
"""
self.data[1] = _id
def set_timer(self, timer):
"""
Input report timer, usually set by the transport
"""
self.data[2] = timer % 256 self.data[2] = timer % 256
def set_misc(self):
# battery level + connection info # battery level + connection info
self.data[3] = 0x8E self.data[3] = 0x8E
# Todo: Button status, analog stick data, vibrator input
# ACK byte for subcmd reply # ACK byte for subcmd reply
self.data[14] = 0x82 self.data[14] = 0x82
# Reply-to subcommand ID def set_button_status(self):
self.data[14] = 0x02 """
TODO
"""
self.data[4:7] = [0x84, 0x00, 0x12]
def sub_0x2_device_info(self, mac, fm_version=(0x03, 0x48), controller=0x01): def set_left_analog_stick(self):
"""
TODO
"""
self.data[7:10] = [0x01, 0x18, 0x80]
def set_right_analog_stick(self):
"""
TODO
"""
self.data[10:13] = [0x01, 0x18, 0x80]
def set_vibrator_input(self):
"""
TODO
"""
self.data[13] = 0x80
def sub_0x2_device_info(self, mac, fm_version=(0x03, 0x48), controller=Controller.JOYCON_L):
""" """
Sub command 0x02 request device info response. Sub command 0x02 request device info response.
@@ -35,14 +72,14 @@ class InputReport:
raise ValueError('Bluetooth mac address must consist of 6 bytes!') raise ValueError('Bluetooth mac address must consist of 6 bytes!')
# reply to sub command ID # reply to sub command ID
self.data[14] = 0x02 self.data[15] = 0x02
# sub command reply data # sub command reply data
offset = 15 offset = 16
self.data[offset: offset + 1] = fm_version self.data[offset: offset + 2] = fm_version
self.data[offset + 2] = controller self.data[offset + 2] = controller.value
self.data[offset + 3] = 0x02 self.data[offset + 3] = 0x02
self.data[offset + 4: offset + 9] = mac self.data[offset + 4: offset + 10] = mac
self.data[offset + 10] = 0x01 self.data[offset + 10] = 0x01
self.data[offset + 11] = 0x01 self.data[offset + 11] = 0x01
@@ -61,7 +98,8 @@ class OutputReport:
raise ValueError('Output reports must start with 0xA2') raise ValueError('Output reports must start with 0xA2')
self.data = data self.data = data
def sub_command(self): def get_sub_command(self):
print('subcommand:', self.data[11])
if self.data[11] == 0x02: if self.data[11] == 0x02:
return SubCommand.REQUEST_DEVICE_INFO return SubCommand.REQUEST_DEVICE_INFO
else: else:
+9 -6
View File
@@ -32,7 +32,7 @@ async def create_hid_server(protocol_factory, ctl_psm, itr_psm):
hid = HidDevice() hid = HidDevice()
# setting bluetooth adapter name and class to the device we wish to emulate # setting bluetooth adapter name and class to the device we wish to emulate
await hid.set_name(HidDevice.JOYCON_L) await hid.set_name(Controller.JOYCON_L.device_name())
await hid.set_class() await hid.set_class()
logger.info('Advertising the Bluetooth SDP record...') logger.info('Advertising the Bluetooth SDP record...')
@@ -40,13 +40,14 @@ async def create_hid_server(protocol_factory, ctl_psm, itr_psm):
hid.discoverable() hid.discoverable()
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
client_ctl, address = await loop.sock_accept(ctl_sock) client_ctl, ctl_address = await loop.sock_accept(ctl_sock)
logger.info(f'Accepted connection at psm {ctl_psm} from {address}') logger.info(f'Accepted connection at psm {ctl_psm} from {ctl_address}')
client_itr, address = await loop.sock_accept(itr_sock) client_itr, itr_address = await loop.sock_accept(itr_sock)
logger.info(f'Accepted connection at psm {itr_psm} from {address}') logger.info(f'Accepted connection at psm {itr_psm} from {itr_address}')
assert ctl_address[0] == itr_address[0]
protocol = protocol_factory() protocol = protocol_factory()
transport = L2CAP_Transport(asyncio.get_event_loop(), protocol, client_itr, address, 50) transport = L2CAP_Transport(asyncio.get_event_loop(), protocol, client_itr, 50)
protocol.connection_made(transport) protocol.connection_made(transport)
return transport, protocol return transport, protocol
@@ -73,6 +74,8 @@ async def main():
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
await asyncio.sleep(60)
await transport.close() await transport.close()
+10 -4
View File
@@ -6,14 +6,20 @@ logger = logging.getLogger(__name__)
class L2CAP_Transport(asyncio.Transport): class L2CAP_Transport(asyncio.Transport):
def __init__(self, loop, protocol, l2cap_socket, client_addr, read_buffer_size) -> None: def __init__(self, loop, protocol, l2cap_socket, read_buffer_size) -> None:
self._loop = loop self._loop = loop
self._protocol = protocol self._protocol = protocol
self._sock = l2cap_socket self._sock = l2cap_socket
self._client_addr = client_addr
self._read_buffer_size = read_buffer_size self._read_buffer_size = read_buffer_size
self._extra_info = {
'peername': self._sock.getpeername(),
'sockname': self._sock.getsockname()
}
print("peer", self._sock.getpeername())
self._read_thread = asyncio.ensure_future(self._read()) self._read_thread = asyncio.ensure_future(self._read())
self._is_closing = False self._is_closing = False
@@ -28,7 +34,7 @@ class L2CAP_Transport(asyncio.Transport):
data = await self._loop.sock_recv(self._sock, self._read_buffer_size) data = await self._loop.sock_recv(self._sock, self._read_buffer_size)
logger.debug(f'received "{data}') logger.debug(f'received "{data}')
await self._protocol.report_received(data, self._client_addr) await self._protocol.report_received(data, self._sock.getpeername())
except asyncio.CancelledError: except asyncio.CancelledError:
# reading has been stopped # reading has been stopped
pass pass
@@ -59,7 +65,7 @@ class L2CAP_Transport(asyncio.Transport):
super().abort() super().abort()
def get_extra_info(self, name: Any, default: Any = ...) -> Any: def get_extra_info(self, name: Any, default: Any = ...) -> Any:
return super().get_extra_info(name, default) return self._extra_info.get(name, default)
def is_closing(self) -> bool: def is_closing(self) -> bool:
return self._is_closing return self._is_closing
+14 -1
View File
@@ -1,5 +1,6 @@
import asyncio import asyncio
import logging import logging
import re
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -18,4 +19,16 @@ async def run_system_command(cmd):
if stderr: if stderr:
logger.debug(f'[stderr]\n{stderr.decode()}') logger.debug(f'[stderr]\n{stderr.decode()}')
return proc.returncode return proc.returncode, stdout, stderr
"""
async def get_bt_mac_address(dev=0):
ret, stdout, stderr = await run_system_command(f'hciconfig hci{dev}')
# TODO: Process error handling
match = re.search(r'BD Address: (?P<mac>\w\w:\w\w:\w\w:\w\w:\w\w:\w\w)', stdout.decode('UTF-8'))
if match:
return list(map(lambda x: int(x, 16), match.group('mac').split(':')))
else:
raise ValueError(f'BD Address not found in "{stdout}"')
"""