diff --git a/joycontrol/button_state.py b/joycontrol/button_state.py deleted file mode 100644 index 8cf1fd5..0000000 --- a/joycontrol/button_state.py +++ /dev/null @@ -1,75 +0,0 @@ -from joycontrol import utils - - -class ButtonState: - """ - Utility class to set buttons in the input report - https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/bluetooth_hid_notes.md - Byte 0 1 2 3 4 5 6 7 - 1 Y X B A SR SL R ZR - 2 Minus Plus R Stick L Stick Home Capture - 3 Down Up Right Left SR SL L ZL - """ - def __init__(self): - # 3 bytes - self._byte_1 = 0 - self._byte_2 = 0 - self._byte_3 = 0 - - # generating methods for each button - def button_method_factory(byte, bit): - def flip(): - setattr(self, byte, utils.flip_bit(getattr(self, byte), bit)) - - def getter(): - return utils.get_bit(getattr(self, byte), bit) - return flip, getter - - # byte 1 - self.y, self.y_is_set = button_method_factory('_byte_1', 0) - self.x, self.x_is_set = button_method_factory('_byte_1', 1) - self.b, self.b_is_set = button_method_factory('_byte_1', 2) - self.a, self.a_is_set = button_method_factory('_byte_1', 3) - self.right_sr, self.right_sr_is_set = button_method_factory('_byte_1', 4) - self.right_sl, self.right_sl_is_set = button_method_factory('_byte_1', 5) - self.r, self.r_is_set = button_method_factory('_byte_1', 6) - self.zr, self.zr_is_set = button_method_factory('_byte_1', 7) - - # byte 2 - self.minus, self.minus_is_set = button_method_factory('_byte_2', 0) - self.plus, self.plus_is_set = button_method_factory('_byte_2', 1) - self.r_stick, self.r_stick_is_set = button_method_factory('_byte_2', 2) - self.l_stick, self.l_stick_is_set = button_method_factory('_byte_2', 3) - self.home, self.home_is_set = button_method_factory('_byte_2', 4) - self.capture, self.capture_is_set = button_method_factory('_byte_2', 5) - - # byte 3 - self.down, self.down_is_set = button_method_factory('_byte_3', 0) - self.up, self.up_is_set = button_method_factory('_byte_3', 1) - self.right, self.right_is_set = button_method_factory('_byte_3', 2) - self.left, self.left_is_set = button_method_factory('_byte_3', 3) - self.left_sr, self.left_sr_is_set = button_method_factory('_byte_3', 4) - self.left_sl, self.left_sl_is_set = button_method_factory('_byte_3', 5) - self.l, self.l_is_set = button_method_factory('_byte_3', 6) - self.zl, self.zl_is_set = button_method_factory('_byte_3', 7) - - """ - Example for generated methods: home button (byte_2, 4) - - def home(self): - self.byte_2 = flip_bit(self.byte_2, 4) - - def home_is_set(self): - return get_bit(self.byte_2, 4) - """ - - def __iter__(self): - """ - @returns iterator of the button bytes - """ - yield self._byte_1 - yield self._byte_2 - yield self._byte_3 - - def clear(self): - self._byte_1 = self._byte_2 = self._byte_3 = 0 diff --git a/joycontrol/controller_state.py b/joycontrol/controller_state.py index 952c012..bf41ec6 100644 --- a/joycontrol/controller_state.py +++ b/joycontrol/controller_state.py @@ -1,40 +1,100 @@ import asyncio -from joycontrol.button_state import ButtonState -from joycontrol.protocol import ControllerProtocol +from joycontrol import utils class ControllerState: - def __init__(self, transport: asyncio.Transport, protocol: ControllerProtocol): - super().__init__() - self.transport = transport - self.protocol = protocol + def __init__(self, protocol): + self._protocol = protocol - self.input_report = self.protocol.get_button_input_report() + self.button_state = None + self.stick_state = None + + self.sig_is_send = asyncio.Event() async def send(self): - await self.input_report.write(self.transport) + self.sig_is_send.clear() + await self.sig_is_send.wait() async def connect(self): """ Waits until the switch is paired with the controller and accepts button commands """ - # TODO HACK: Hard to say for now. - await self.protocol.wait_for_output_report() - # The switch sends data to our device, it shouldn't take long until the connection is fully established. - await asyncio.sleep(5) + await self._protocol.sig_wait_player_lights.wait() - def set_button_state(self, button_state: ButtonState): - """ - Sets the button status bytes in the input report - """ - self.input_report.set_button_status(button_state) - def set_stick_state(self): +class ButtonState: + """ + Utility class to set buttons in the input report + https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/bluetooth_hid_notes.md + Byte 0 1 2 3 4 5 6 7 + 1 Y X B A SR SL R ZR + 2 Minus Plus R Stick L Stick Home Capture + 3 Down Up Right Left SR SL L ZL + """ + def __init__(self): + # 3 bytes + self._byte_1 = 0 + self._byte_2 = 0 + self._byte_3 = 0 + + # generating methods for each button + def button_method_factory(byte, bit): + def flip(): + setattr(self, byte, utils.flip_bit(getattr(self, byte), bit)) + + def getter(): + return utils.get_bit(getattr(self, byte), bit) + return flip, getter + + # byte 1 + self.y, self.y_is_set = button_method_factory('_byte_1', 0) + self.x, self.x_is_set = button_method_factory('_byte_1', 1) + self.b, self.b_is_set = button_method_factory('_byte_1', 2) + self.a, self.a_is_set = button_method_factory('_byte_1', 3) + self.right_sr, self.right_sr_is_set = button_method_factory('_byte_1', 4) + self.right_sl, self.right_sl_is_set = button_method_factory('_byte_1', 5) + self.r, self.r_is_set = button_method_factory('_byte_1', 6) + self.zr, self.zr_is_set = button_method_factory('_byte_1', 7) + + # byte 2 + self.minus, self.minus_is_set = button_method_factory('_byte_2', 0) + self.plus, self.plus_is_set = button_method_factory('_byte_2', 1) + self.r_stick, self.r_stick_is_set = button_method_factory('_byte_2', 2) + self.l_stick, self.l_stick_is_set = button_method_factory('_byte_2', 3) + self.home, self.home_is_set = button_method_factory('_byte_2', 4) + self.capture, self.capture_is_set = button_method_factory('_byte_2', 5) + + # byte 3 + self.down, self.down_is_set = button_method_factory('_byte_3', 0) + self.up, self.up_is_set = button_method_factory('_byte_3', 1) + self.right, self.right_is_set = button_method_factory('_byte_3', 2) + self.left, self.left_is_set = button_method_factory('_byte_3', 3) + self.left_sr, self.left_sr_is_set = button_method_factory('_byte_3', 4) + self.left_sl, self.left_sl_is_set = button_method_factory('_byte_3', 5) + self.l, self.l_is_set = button_method_factory('_byte_3', 6) + self.zl, self.zl_is_set = button_method_factory('_byte_3', 7) + + """ + Example for generated methods: home button (byte_2, 4) + + def home(self): + self.byte_2 = flip_bit(self.byte_2, 4) + + def home_is_set(self): + return get_bit(self.byte_2, 4) + """ + + def __iter__(self): """ - TODO + @returns iterator of the button bytes """ - raise NotImplementedError() + yield self._byte_1 + yield self._byte_2 + yield self._byte_3 + + def clear(self): + self._byte_1 = self._byte_2 = self._byte_3 = 0 async def button_push(controller_state, button, sec=0.1): @@ -44,7 +104,7 @@ async def button_push(controller_state, button, sec=0.1): getattr(button_state, button)() # send report - controller_state.set_button_state(button_state) + controller_state.button_state = button_state await controller_state.send() await asyncio.sleep(sec) @@ -52,5 +112,10 @@ async def button_push(controller_state, button, sec=0.1): getattr(button_state, button)() # send report - controller_state.set_button_state(button_state) - await controller_state.send() \ No newline at end of file + controller_state.button_state = button_state + await controller_state.send() + + +class StickState: + def __init__(self): + raise NotImplementedError() diff --git a/joycontrol/protocol.py b/joycontrol/protocol.py index 9e6c2f0..259f092 100644 --- a/joycontrol/protocol.py +++ b/joycontrol/protocol.py @@ -4,6 +4,7 @@ from asyncio import BaseTransport, BaseProtocol from typing import Optional, Union, Tuple, Text from joycontrol.controller import Controller +from joycontrol.controller_state import ControllerState from joycontrol.report import OutputReport, SubCommand, InputReport, OutputReportID logger = logging.getLogger(__name__) @@ -21,15 +22,27 @@ class ControllerProtocol(BaseProtocol): self.transport = None - # This must always be an 0x21 input report to be compatible with button events - self._button_input_report = InputReport() - self._button_input_report.set_input_report_id(0x21) - self._button_input_report.set_misc() - self._data_received = asyncio.Event() - def get_button_input_report(self): - return self._button_input_report + self._controller_state = ControllerState(self) + + self._pending_write = None + self._pending_input_report = None + + self._0x30_input_report_sender = None + + self.sig_wait_player_lights = asyncio.Event() + + async def write(self, input_report: InputReport): + # set button and TODO: stick date + if self._controller_state.button_state is not None: + input_report.set_button_status(self._controller_state.button_state) + self._controller_state.sig_is_send.set() + + await self.transport.write(input_report) + + def get_controller_state(self): + return self._controller_state async def wait_for_output_report(self): self._data_received.clear() @@ -45,6 +58,27 @@ class ControllerProtocol(BaseProtocol): def error_received(self, exc: Exception) -> None: raise NotImplementedError() + async def send_0x30_input_reports(self): + input_report = InputReport() + input_report.set_input_report_id(0x30) + input_report.set_misc() + + while True: + # TODO: set sensor data + input_report.set_6axis_data() + + await self.write(input_report) + + """ + if self.controller == Controller.PRO_CONTROLLER: + # send state at 120Hz if Pro Controller + await asyncio.sleep(1 / 120) + else: + # send state at 60Hz + await asyncio.sleep(1 / 60) + """ + await asyncio.sleep(1 / 30) + async def report_received(self, data: Union[bytes, Text], addr: Tuple[str, int]) -> None: self._data_received.set() @@ -87,6 +121,19 @@ class ControllerProtocol(BaseProtocol): elif sub_command == SubCommand.TRIGGER_BUTTONS_ELAPSED_TIME: await self._command_trigger_buttons_elapsed_time(report) + + elif sub_command == SubCommand.ENABLE_6AXIS_SENSOR: + await self._command_enable_6axis_sensor(report) + + elif sub_command == SubCommand.ENABLE_VIBRATION: + await self._command_enable_vibration(report) + + elif sub_command == SubCommand.SET_NFC_IR_MCU_CONFIG: + await self._command_set_nfc_ir_mcu_config(report) + + elif sub_command == SubCommand.SET_PLAYER_LIGHTS: + await self._command_set_player_lights(report) + else: logger.warning(f'Sub command 0x{sub_command.value:02x} not implemented - ignoring') #elif output_report_id == OutputReportID.RUMBLE_ONLY: @@ -95,48 +142,108 @@ class ControllerProtocol(BaseProtocol): logger.warning(f'Output report {output_report_id} not implemented - ignoring') async def _command_request_device_info(self, output_report): + input_report = InputReport() + input_report.set_input_report_id(0x21) + input_report.set_misc() + address = self.transport.get_extra_info('sockname') assert address is not None bd_address = list(map(lambda x: int(x, 16), address[0].split(':'))) - self._button_input_report.set_misc() - self._button_input_report.set_ack(0x82) - self._button_input_report.sub_0x02_device_info(bd_address, controller=self.controller) + input_report.set_ack(0x82) + input_report.sub_0x02_device_info(bd_address, controller=self.controller) - await self._button_input_report.write(self.transport) + await self.write(input_report) async def _command_set_shipment_state(self, output_report): - self._button_input_report.set_misc() - self._button_input_report.set_ack(0x80) - self._button_input_report.sub_0x08_shipment() + input_report = InputReport() + input_report.set_input_report_id(0x21) + input_report.set_misc() - await self._button_input_report.write(self.transport) + input_report.set_ack(0x80) + input_report.reply_to_subcommand_id(0x08) + + await self.write(input_report) async def _command_spi_flash_read(self, output_report): - self._button_input_report.set_misc() - self._button_input_report.set_ack(0x90) - self._button_input_report.sub_0x10_spi_flash_read(output_report) + input_report = InputReport() + input_report.set_input_report_id(0x21) + input_report.set_misc() - await self._button_input_report.write(self.transport) + input_report.set_ack(0x90) + input_report.sub_0x10_spi_flash_read(output_report) + + await self.write(input_report) async def _command_set_input_report_mode(self, output_report): - self._button_input_report.set_misc() - self._button_input_report.set_ack(0x80) - self._button_input_report.sub_0x03_set_input_report_mode() + if output_report.data[12] == 0x30: + logger.info('Setting input report mode to 0x30...') + # start sending 0x30 input reports + assert self._0x30_input_report_sender is None + self._0x30_input_report_sender = asyncio.ensure_future(self.send_0x30_input_reports()) - await self._button_input_report.write(self.transport) + input_report = InputReport() + input_report.set_input_report_id(0x21) + input_report.set_misc() + + input_report.set_ack(0x80) + input_report.reply_to_subcommand_id(0x03) + + await self.write(input_report) + else: + logger.error(f'input report mode {output_report.data[12]} not implemented - ignoring request') async def _command_trigger_buttons_elapsed_time(self, output_report): - self._button_input_report.set_misc() - self._button_input_report.set_ack(0x83) - self._button_input_report.sub_0x04_trigger_buttons_elapsed_time() + input_report = InputReport() + input_report.set_input_report_id(0x21) + input_report.set_misc() - await self._button_input_report.write(self.transport) + input_report.set_ack(0x83) + input_report.sub_0x04_trigger_buttons_elapsed_time() - async def _enable_6axis_sensor(self, output_report): - self._button_input_report.set_misc() - self._button_input_report.set_ack(0x80) + await self.write(input_report) - self._button_input_report.reply_to_subcommand_id(0x40) + async def _command_enable_6axis_sensor(self, output_report): + input_report = InputReport() + input_report.set_input_report_id(0x21) + input_report.set_misc() - await self._button_input_report.write(self.transport) + input_report.set_ack(0x80) + input_report.reply_to_subcommand_id(0x40) + + await self.write(input_report) + + async def _command_enable_vibration(self, output_report): + input_report = InputReport() + input_report.set_input_report_id(0x21) + input_report.set_misc() + + input_report.set_ack(0x80) + input_report.reply_to_subcommand_id(SubCommand.ENABLE_VIBRATION.value) + + await self.write(input_report) + + async def _command_set_nfc_ir_mcu_config(self, output_report): + input_report = InputReport() + input_report.set_input_report_id(0x21) + input_report.set_misc() + + input_report.set_ack(0xA0) + input_report.reply_to_subcommand_id(SubCommand.SET_NFC_IR_MCU_CONFIG.value) + + for i in range(16, 51): + input_report.data[i] = 0xFF + + await self.write(input_report) + + async def _command_set_player_lights(self, output_report): + input_report = InputReport() + input_report.set_input_report_id(0x21) + input_report.set_misc() + + input_report.set_ack(0x80) + input_report.reply_to_subcommand_id(SubCommand.SET_PLAYER_LIGHTS.value) + + await self.write(input_report) + + self.sig_wait_player_lights.set() diff --git a/joycontrol/report.py b/joycontrol/report.py index e659164..121fba9 100644 --- a/joycontrol/report.py +++ b/joycontrol/report.py @@ -1,7 +1,5 @@ -import asyncio from enum import Enum -from joycontrol.button_state import ButtonState from joycontrol.controller import Controller @@ -10,27 +8,35 @@ class InputReport: Class to create Input Reports. Reference: https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/bluetooth_hid_notes.md """ - def __init__(self): - self.data = [0x00] * 51 - # all input reports are prepended with 0xA1 - self.data[0] = 0xA1 - - self.subcommand_is_set = False - - self.is_writing = None + def __init__(self, data=None): + if data is None: + # TODO: not enough space for NFC/IR data input report + self.data = [0x00] * 51 + # all input reports are prepended with 0xA1 + self.data[0] = 0xA1 + else: + if data[0] != 0xA1: + raise ValueError('Input reports must start with 0xA1') + self.data = data def clear_sub_command(self): + """ + Clear sub command reply data of 0x21 input reports + """ for i in range(14, 51): self.data[i] = 0x00 - self.subcommand_is_set = False def set_input_report_id(self, _id): """ :param _id: e.g. 0x21 Standard input reports used for sub command replies + 0x30 Input reports with IMU data instead of sub command replies etc... (TODO) """ self.data[1] = _id + def get_input_report_id(self): + return self.data[1] + def set_timer(self, timer): """ Input report timer (0x00-0xFF), usually set by the transport @@ -41,7 +47,7 @@ class InputReport: # battery level + connection info self.data[3] = 0x8E - def set_button_status(self, button_status: ButtonState): + def set_button_status(self, button_status): """ Sets the button status bytes """ @@ -72,7 +78,18 @@ class InputReport: """ self.data[14] = ack - def sub_0x02_device_info(self, mac, fm_version=(0x03, 0x48), controller=Controller.JOYCON_L): + def set_6axis_data(self): + """ + Set accelerator and gyro of 0x30 input reports + """ + # HACK: Set all 0 for now + for i in range(14, 50): + self.data[i] = 0x00 + + def reply_to_subcommand_id(self, id_): + self.data[15] = id_ + + def sub_0x02_device_info(self, mac, fm_version=(0x04, 0x00), controller=Controller.JOYCON_L): """ Sub command 0x02 request device info response. @@ -96,38 +113,22 @@ class InputReport: self.data[offset + 10] = 0x01 self.data[offset + 11] = 0x01 - def reply_to_subcommand_id(self, id_): - self.subcommand_is_set = True - self.data[15] = id_ - - def sub_0x08_shipment(self): - self.reply_to_subcommand_id(0x08) - def sub_0x10_spi_flash_read(self, output_report): self.reply_to_subcommand_id(0x10) self.data[16:18] = output_report.data[12:14] - def sub_0x03_set_input_report_mode(self): - self.reply_to_subcommand_id(0x03) - def sub_0x04_trigger_buttons_elapsed_time(self): self.reply_to_subcommand_id(0x04) - # TODO blub = [0x00, 0xCC, 0x00, 0xEE, 0x00, 0xFF] self.data[16:22] = blub - async def write(self, transport): - if self.is_writing is None: - self.is_writing = asyncio.ensure_future(transport.write(self)) - await self.is_writing - self.is_writing = None - def __bytes__(self): - if self.subcommand_is_set: - return bytes(self.data) + _id = self.get_input_report_id() + if _id == 0x21: + return bytes(self.data[:51]) else: - return bytes(self.data[:15]) + return bytes(self.data) class SubCommand(Enum): @@ -136,7 +137,10 @@ class SubCommand(Enum): TRIGGER_BUTTONS_ELAPSED_TIME = 0x04 SET_SHIPMENT_STATE = 0x08 SPI_FLASH_READ = 0x10 + SET_NFC_IR_MCU_CONFIG = 0x21 + SET_PLAYER_LIGHTS = 0x30 ENABLE_6AXIS_SENSOR = 0x40 + ENABLE_VIBRATION = 0x48 class OutputReportID(Enum): @@ -154,7 +158,7 @@ class OutputReport: try: return OutputReportID(self.data[1]) except ValueError: - raise NotImplementedError(f'Output report id {self.data[1]}') + raise NotImplementedError(f'Output report id {hex(self.data[1])} not implemented') def get_timer(self): return OutputReportID(self.data[2]) @@ -168,7 +172,7 @@ class OutputReport: try: return SubCommand(self.data[11]) except ValueError: - raise NotImplementedError(f'Sub command id {self.data[11]}') + raise NotImplementedError(f'Sub command id {hex(self.data[11])} not implemented') def __bytes__(self): return bytes(self.data) diff --git a/joycontrol/server.py b/joycontrol/server.py index b99dd7b..d378f07 100644 --- a/joycontrol/server.py +++ b/joycontrol/server.py @@ -21,7 +21,7 @@ async def _send_empty_input_reports(transport): await asyncio.sleep(1) -async def create_hid_server(protocol_factory, ctl_psm, itr_psm): +async def create_hid_server(protocol_factory, ctl_psm, itr_psm, capture_file=None): 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) @@ -57,7 +57,7 @@ async def create_hid_server(protocol_factory, ctl_psm, itr_psm): logger.info(f'Accepted connection at psm {itr_psm} from {itr_address}') assert ctl_address[0] == itr_address[0] - transport = L2CAP_Transport(asyncio.get_event_loop(), protocol, client_itr, 50) + transport = L2CAP_Transport(asyncio.get_event_loop(), protocol, client_itr, 50, capture_file=capture_file) protocol.connection_made(transport) # send some empty input reports until the switch decides to reply diff --git a/joycontrol/transport.py b/joycontrol/transport.py index 506b7e3..8b7eeb5 100644 --- a/joycontrol/transport.py +++ b/joycontrol/transport.py @@ -1,5 +1,7 @@ import asyncio import logging +import struct +import time from typing import Any from joycontrol.report import InputReport @@ -8,7 +10,7 @@ logger = logging.getLogger(__name__) class L2CAP_Transport(asyncio.Transport): - def __init__(self, loop, protocol, l2cap_socket, read_buffer_size) -> None: + def __init__(self, loop, protocol, l2cap_socket, read_buffer_size, capture_file=None) -> None: self._loop = loop self._protocol = protocol @@ -28,12 +30,21 @@ class L2CAP_Transport(asyncio.Transport): self._input_report_timer = 0x00 + self._capture_file = capture_file + async def _read(self): while True: await self._is_reading.wait() data = await self._loop.sock_recv(self._sock, self._read_buffer_size) - logger.debug(f'received "{list(map(hex, list(data)))}"') + + if self._capture_file is not None: + # write data to log file + _time = struct.pack('d', time.time()) + size = struct.pack('i', len(data)) + self._capture_file.write(_time + size + data) + + #logger.debug(f'received "{list(data)}"') await self._protocol.report_received(data, self._sock.getpeername()) def is_reading(self) -> bool: @@ -62,13 +73,16 @@ class L2CAP_Transport(asyncio.Transport): data.set_timer(self._input_report_timer) self._input_report_timer = (self._input_report_timer + 1) % 256 _bytes = bytes(data) - - if data.subcommand_is_set: - data.clear_sub_command() else: raise ValueError('data must be bytes or InputReport') - logger.debug(f'sending "{_bytes}"') + if self._capture_file is not None: + # write data to log file + _time = struct.pack('d', time.time()) + size = struct.pack('i', len(_bytes)) + self._capture_file.write(_time + size + _bytes) + + #logger.debug(f'sending "{_bytes}"') await self._loop.sock_sendall(self._sock, _bytes) def abort(self) -> None: diff --git a/run_test_controller_buttons.py b/run_test_controller_buttons.py index 9933db9..93edd50 100644 --- a/run_test_controller_buttons.py +++ b/run_test_controller_buttons.py @@ -69,7 +69,7 @@ async def test_controller_buttons(controller_state: ControllerState): async def main(): transport, protocol = await create_hid_server(controller_protocol_factory(Controller.PRO_CONTROLLER), 17, 19) - await test_controller_buttons(ControllerState(transport, protocol)) + await test_controller_buttons(protocol.get_controller_state()) logger.info('Stopping communication...') await transport.close() diff --git a/setup.py b/setup.py index facd416..029f682 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup(name='joycontrol', - version='0.1', + version='0.11', author='Robert Martin', author_email='martinro@informatik.hu-berlin.de', description='Emulate Nintendo Switch Controllers over Bluetooth', @@ -10,7 +10,7 @@ setup(name='joycontrol', package_data={'joycontrol': ['profile/sdp_record_hid.xml']}, zip_safe=False, install_requires=[ - # TODO + 'hid' ] )