diff --git a/scripts/switch_emu.py b/scripts/switch_emu.py new file mode 100644 index 0000000..e8fea39 --- /dev/null +++ b/scripts/switch_emu.py @@ -0,0 +1,206 @@ +""" +Quick script to emulate a Switch connecting to a Joy-Con/Pro Controller. + +Note: If you get an Invalid Exchange error when running this script, this means +that the Switch has paired to the controller, invalidating the original pairing +key we created. You'll need to remove the controller before continuing. +""" + +import socket +import sys +import os +import time + +from nxbt import toggle_input_plugin +from nxbt import BlueZ + +REQUEST_INFO = b'\xA2\x01\x02\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +SET_SHIPMENT = b'\xA2\x01\x07\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +SERIAL_NUMBER = b'\xA2\x01\x08\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x60\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +COLOURS = b'\xA2\x01\x09\x00\x00\x00\x00\x00\x00\x00\x00\x10\x50\x60\x00\x00\x0D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +INPUT_MODE = b'\xA2\x01\x0A\x00\x01\x40\x40\x00\x01\x40\x40\x03\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +TRIGGER_BUTTONS = b'\xA2\x01\x0D\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +FACTORY_PARAMS = b'\xA2\x01\x0F\x00\x00\x00\x00\x00\x00\x00\x00\x10\x80\x60\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +FACTORY_PARAMS_2 = b'\xA2\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x10\x98\x60\x00\x00\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +USER_CAL = b'\xA2\x01\x02\x00\x00\x00\x00\x00\x00\x00\x00\x10\x10\x80\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +FACTORY_CAL = b'\xA2\x01\x04\x00\x00\x00\x00\x00\x00\x00\x00\x10\x3D\x60\x00\x00\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +SIX_AXIS_CAL = b'\xA2\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x10\x20\x60\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +ENABLE_IMU = b'\xA2\x01\x07\x00\x01\x40\x40\x00\x01\x40\x40\x40\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +ENABLE_VIBRATION = b'\xA2\x01\x09\x00\x00\x00\x00\x00\x00\x00\x00\x48\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +SET_NFC_IR = b'\xA2\x01\x0C\x00\x01\x40\x40\x00\x01\x40\x40\x21\x21\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +SET_PLAYER_LIGHTS = b'\xA2\x01\x0D\x00\x00\x00\x00\x00\x00\x00\x00\x30\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + +FLASH_PLAYER_LIGHTS = b'\xA2\x01\x0D\x00\x00\x00\x00\x00\x00\x00\x00\x30\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + +COMMANDS = [ + REQUEST_INFO, + SET_SHIPMENT, + SERIAL_NUMBER, + COLOURS, + INPUT_MODE, + TRIGGER_BUTTONS, + FACTORY_PARAMS, + FACTORY_PARAMS_2, + USER_CAL, + FACTORY_CAL, + SIX_AXIS_CAL, + ENABLE_IMU, + ENABLE_VIBRATION, + SET_NFC_IR, +] + + +def format_message(data, split, name): + """Formats a given byte message in hex format split + into payload and subcommand sections. + + :param data: A series of bytes + :type data: bytes + :param split: The location of the payload/subcommand split + :type split: integer + :param name: The name featured in the start/end messages + :type name: string + :return: The formatted data + :rtype: string + """ + + payload = "" + subcommand = "" + for i in range(0, len(data)): + data_byte = str(hex(data[i]))[2:].upper() + if len(data_byte) < 2: + data_byte = "0" + data_byte + if i <= split: + payload += "0x" + data_byte + " " + else: + subcommand += "0x" + data_byte + " " + + formatted = ( + f"--- {name} Msg ---\n" + + f"Payload: {payload}\n" + + f"Subcommand: {subcommand}") + + return formatted + + +def print_msg_controller(data): + """Prints a formatted message from a controller + + :param data: The bytes from the controller message + :type data: bytes + """ + + print(format_message(data, 13, "Controller")) + + +def print_msg_switch(data): + """Prints a formatted message from a Switch + + :param data: The bytes from the Switch message + :type data: bytes + """ + + print(format_message(data, 10, "Switch")) + + +def wait_for_reply(itr): + + while True: + data = itr.recv(350) + print_msg_controller(data) + if data[1] == 0x21: + break + + +if __name__ == "__main__": + + # Switch Controller Bluetooth MAC Address goes here + jc_MAC = "7C:BB:8A:FA:41:3D" + port_ctrl = 17 + port_itr = 19 + + # Joy-Con Sockets + jc_ctrl = socket.socket(family=socket.AF_BLUETOOTH, + type=socket.SOCK_SEQPACKET, + proto=socket.BTPROTO_L2CAP) + jc_itr = socket.socket(family=socket.AF_BLUETOOTH, + type=socket.SOCK_SEQPACKET, + proto=socket.BTPROTO_L2CAP) + toggle_input_plugin(False) + bt = BlueZ(adapter_path="/org/bluez/hci0") + + try: + # Remove the device before we try to re-pair + device_path = bt.find_device_by_address(jc_MAC) + if not device_path: + print("Device not paired. Pairing...") + + # Ensure we are paired/connected to the JC + print("Attempting to re-pair with device") + devices = bt.discover_devices(alias="Pro Controller", timeout=8) + jc_device_path = None + for key in devices.keys(): + print(devices[key]["Address"]) + if devices[key]["Address"] == jc_MAC: + jc_device_path = key + break + + if not jc_device_path: + print("The specified Joy-Con could not be found") + else: + bt.pair_device(jc_device_path) + print("Paired Joy-Con") + + bt.set_alias("Nintendo Switch") + print("Connecting to Joy-Con: ", jc_MAC) + jc_ctrl.connect((jc_MAC, port_ctrl)) + jc_itr.connect((jc_MAC, port_itr)) + print("Got connection.") + + # Initial Input report from Joy-Con + jc_data = jc_itr.recv(350) + print("Got initial Joy-Con Empty Report") + print_msg_controller(jc_data) + + for command in COMMANDS: + print_msg_switch(command) + jc_itr.sendall(command) + wait_for_reply(jc_itr) + + print("Here") + jc_itr.sendall(FLASH_PLAYER_LIGHTS) + count = 0 + while count < 360: + data = jc_itr.recv(350) + print_msg_controller(data) + time.sleep(1/120) + count += 1 + + while True: + data = jc_itr.recv(350) + print_msg_controller(data) + jc_itr.sendall(SET_PLAYER_LIGHTS) + print_msg_switch(SET_PLAYER_LIGHTS) + time.sleep(1/120) + + except KeyboardInterrupt: + print("Closing sockets") + + jc_ctrl.close() + jc_itr.close() + + try: + sys.exit(1) + except SystemExit: + os._exit(1) + + except OSError as e: + print("Closing sockets") + + jc_ctrl.close() + jc_itr.close() + + raise e + + finally: + toggle_input_plugin(True)