forked from mirror/joycontrol
request device info
This commit is contained in:
@@ -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()
|
||||||
@@ -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
@@ -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)))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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}"')
|
||||||
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user