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_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())
+39 -22
View File
@@ -1,31 +1,14 @@
import asyncio
import enum
import logging
from asyncio import BaseTransport, BaseProtocol
from typing import Optional, Union, Tuple, Text
from controller import Controller
from report import OutputReport, SubCommand, InputReport
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 create_controller_protocol():
return ControllerProtocol(controller)
@@ -34,6 +17,8 @@ def controller_protocol_factory(controller: Controller):
class ControllerProtocol(BaseProtocol):
def __init__(self, controller: Controller):
self.controller = controller
self.transport = None
self._data_received = asyncio.Event()
@@ -49,8 +34,40 @@ class ControllerProtocol(BaseProtocol):
def connection_lost(self, exc: Optional[Exception]) -> None:
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:
self._data_received.set()
def error_received(self, exc: Exception) -> None:
raise NotImplementedError()
try:
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
#[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:
def __init__(self):
@@ -7,21 +15,50 @@ class InputReport:
# all input reports are prepended with 0xA1
self.data[0] = 0xA1
def set(self, input_report_id, timer=0x00):
self.data[1] = input_report_id
def set_input_report_id(self, _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
def set_misc(self):
# battery level + connection info
self.data[3] = 0x8E
# Todo: Button status, analog stick data, vibrator input
# ACK byte for subcmd reply
self.data[14] = 0x82
# Reply-to subcommand ID
self.data[14] = 0x02
def set_button_status(self):
"""
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.
@@ -35,14 +72,14 @@ class InputReport:
raise ValueError('Bluetooth mac address must consist of 6 bytes!')
# reply to sub command ID
self.data[14] = 0x02
self.data[15] = 0x02
# sub command reply data
offset = 15
self.data[offset: offset + 1] = fm_version
self.data[offset + 2] = controller
offset = 16
self.data[offset: offset + 2] = fm_version
self.data[offset + 2] = controller.value
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 + 11] = 0x01
@@ -61,7 +98,8 @@ class OutputReport:
raise ValueError('Output reports must start with 0xA2')
self.data = data
def sub_command(self):
def get_sub_command(self):
print('subcommand:', self.data[11])
if self.data[11] == 0x02:
return SubCommand.REQUEST_DEVICE_INFO
else:
+9 -6
View File
@@ -32,7 +32,7 @@ async def create_hid_server(protocol_factory, ctl_psm, itr_psm):
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_name(Controller.JOYCON_L.device_name())
await hid.set_class()
logger.info('Advertising the Bluetooth SDP record...')
@@ -40,13 +40,14 @@ async def create_hid_server(protocol_factory, ctl_psm, itr_psm):
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}')
client_ctl, ctl_address = await loop.sock_accept(ctl_sock)
logger.info(f'Accepted connection at psm {ctl_psm} from {ctl_address}')
client_itr, itr_address = await loop.sock_accept(itr_sock)
logger.info(f'Accepted connection at psm {itr_psm} from {itr_address}')
assert ctl_address[0] == itr_address[0]
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)
return transport, protocol
@@ -73,6 +74,8 @@ async def main():
except asyncio.CancelledError:
pass
await asyncio.sleep(60)
await transport.close()
+10 -4
View File
@@ -6,14 +6,20 @@ logger = logging.getLogger(__name__)
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._protocol = protocol
self._sock = l2cap_socket
self._client_addr = client_addr
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._is_closing = False
@@ -28,7 +34,7 @@ class L2CAP_Transport(asyncio.Transport):
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)
await self._protocol.report_received(data, self._sock.getpeername())
except asyncio.CancelledError:
# reading has been stopped
pass
@@ -59,7 +65,7 @@ class L2CAP_Transport(asyncio.Transport):
super().abort()
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:
return self._is_closing
+14 -1
View File
@@ -1,5 +1,6 @@
import asyncio
import logging
import re
logger = logging.getLogger(__name__)
@@ -18,4 +19,16 @@ async def run_system_command(cmd):
if stderr:
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}"')
"""