From 3abcdb0a6748078d196d95027730d7c09513162d Mon Sep 17 00:00:00 2001 From: Poohl Date: Fri, 30 Oct 2020 12:28:30 +0100 Subject: [PATCH 01/43] fixed packet-flooding --- joycontrol/protocol.py | 157 ++++++++++++---------------------------- joycontrol/transport.py | 24 ------ 2 files changed, 46 insertions(+), 135 deletions(-) diff --git a/joycontrol/protocol.py b/joycontrol/protocol.py index f83a9d9a..522f95f1 100644 --- a/joycontrol/protocol.py +++ b/joycontrol/protocol.py @@ -39,8 +39,11 @@ def __init__(self, controller: Controller, spi_flash: FlashMemory = None): self._controller_state = ControllerState(self, controller, spi_flash=spi_flash) self._controller_state_sender = None + self._writer = None + # daley between two input reports + self.send_delay = 1/15 - # None = Just answer to sub commands + # None = Send empty input reports & answer to sub commands self._input_report_mode = None # This event gets triggered once the Switch assigns a player number to the controller and accepts user inputs @@ -90,6 +93,11 @@ async def write(self, input_report: InputReport): input_report.set_timer(self._input_report_timer) self._input_report_timer = (self._input_report_timer + 1) % 0x100 + # this would close the grip menu, so go into fullspeed + if self._controller_state.button_state.a_is_set() or self._controller_state.button_state.b_is_set(): + print("Exiting grip-menu") + self.send_delay = 1 / 60 + await self.transport.write(input_report) self._controller_state.sig_is_send.set() @@ -107,6 +115,7 @@ async def wait_for_output_report(self): def connection_made(self, transport: BaseTransport) -> None: logger.debug('Connection established.') self.transport = transport + self._writer = asyncio.ensure_future(self.writer()) def connection_lost(self, exc: Optional[Exception] = None) -> None: if self.transport is not None: @@ -121,92 +130,37 @@ def error_received(self, exc: Exception) -> None: # TODO? raise NotImplementedError() - async def input_report_mode_full(self): + async def writer(self): """ - Continuously sends: - 0x30 input reports containing the controller state OR - 0x31 input reports containing the controller state and nfc data + This continuously sends input reports to the switch. + This relys on the asyncio scheduler to sneak the additional + subcommand-replys in """ - if self.transport.is_reading(): - raise ValueError('Transport must be paused in full input report mode') - - # send state at 66Hz - send_delay = 0.015 - await asyncio.sleep(send_delay) - last_send_time = time.time() - - input_report = InputReport() - input_report.set_vibrator_input() - input_report.set_misc() - if self._input_report_mode is None: - raise ValueError('Input report mode is not set.') - input_report.set_input_report_id(self._input_report_mode) - - reader = asyncio.ensure_future(self.transport.read()) - - try: - while True: - reply_send = False - if reader.done(): - data = await reader - - reader = asyncio.ensure_future(self.transport.read()) - - try: - report = OutputReport(list(data)) - output_report_id = report.get_output_report_id() - - if output_report_id == OutputReportID.RUMBLE_ONLY: - # TODO - pass - elif output_report_id == OutputReportID.SUB_COMMAND: - reply_send = await self._reply_to_sub_command(report) - elif output_report_id == OutputReportID.REQUEST_IR_NFC_MCU: - # TODO NFC - raise NotImplementedError('NFC communictation is not implemented.') - else: - logger.warning(f'Report unknown output report "{output_report_id}" - IGNORE') - except ValueError as v_err: - logger.warning(f'Report parsing error "{v_err}" - IGNORE') - except NotImplementedError as err: - logger.warning(err) - - if reply_send: - # Hack: Adding a delay here to avoid flooding during pairing - await asyncio.sleep(0.3) - else: - # write 0x30 input report. - # TODO: set some sensor data - input_report.set_6axis_data() - - # TODO NFC - set nfc data - if input_report.get_input_report_id() == 0x31: - pass - - await self.write(input_report) - - # calculate delay - current_time = time.time() - time_delta = time.time() - last_send_time - sleep_time = send_delay - time_delta - last_send_time = current_time - - if sleep_time < 0: - # logger.warning(f'Code is running {abs(sleep_time)} s too slow!') - sleep_time = 0 - - await asyncio.sleep(sleep_time) - - except NotConnectedError as err: - # Stop 0x30 input report mode if disconnected. - logger.error(err) - finally: - # cleanup - self._input_report_mode = None - # cancel the reader - with suppress(asyncio.CancelledError, NotConnectedError): - if reader.cancel(): - await reader + while self.transport: + last_send_time = time.time() + input_report = InputReport() + if self._input_report_mode is None: + input_report.set_input_report_id(0x30) + elif self._input_report_mode == 0x30 or self._input_report_mode == 0x31: + input_report.set_vibrator_input() + input_report.set_misc() + input_report.set_input_report_id(self._input_report_mode) + input_report.set_6axis_data() + if self._input_report_mode == 0x31: + input_report.set_ir_nfc_data(self.mcu.sending_31()) + await self.write(input_report) + + # calculate delay + current_time = time.time() + active_time = current_time - last_send_time + sleep_time = self.send_delay - active_time + last_send_time = current_time + if sleep_time < 0: + # logger.warning(f'Code is running {abs(sleep_time)} s too slow!') + sleep_time = 0 + + await asyncio.sleep(sleep_time) + return None async def report_received(self, data: Union[bytes, Text], addr: Tuple[str, int]) -> None: self._data_received.set() @@ -225,8 +179,12 @@ async def report_received(self, data: Union[bytes, Text], addr: Tuple[str, int]) if output_report_id == OutputReportID.SUB_COMMAND: await self._reply_to_sub_command(report) - # elif output_report_id == OutputReportID.RUMBLE_ONLY: - # pass + elif output_report_id == OutputReportID.RUMBLE_ONLY: + #TODO Rumble + pass + elif output_report_id == OutputReportID.REQUEST_IR_NFC_MCU: + #TODO NFC + pass else: logger.warning(f'Output report {output_report_id} not implemented - ignoring') @@ -342,30 +300,7 @@ async def _command_set_input_report_mode(self, sub_command_data): if self._input_report_mode == sub_command_data[0]: logger.warning(f'Already in input report mode {sub_command_data[0]} - ignoring request') - # Start input report reader - if sub_command_data[0] in (0x30, 0x31): - new_reader = asyncio.ensure_future(self.input_report_mode_full()) - else: - logger.error(f'input report mode {sub_command_data[0]} not implemented - ignoring request') - return - - # Replace the currently running reader with the input report mode sender, - # which will also handle incoming requests in the future - - self.transport.pause_reading() - - # We need to replace the reader in the future because this function was probably called by it - async def set_reader(): - await self.transport.set_reader(new_reader) - - logger.info(f'Setting input report mode to {hex(sub_command_data[0])}...') - self._input_report_mode = sub_command_data[0] - - self.transport.resume_reading() - - asyncio.ensure_future(set_reader()).add_done_callback( - utils.create_error_check_callback() - ) + self._input_report_mode = sub_command_data[0] # Send acknowledgement input_report = InputReport() diff --git a/joycontrol/transport.py b/joycontrol/transport.py index f2ff62df..8301709d 100644 --- a/joycontrol/transport.py +++ b/joycontrol/transport.py @@ -64,30 +64,6 @@ def start_reader(self): callback = utils.create_error_check_callback(ignore=asyncio.CancelledError) self._read_thread.add_done_callback(callback) - async def set_reader(self, reader: asyncio.Future): - """ - Cancel the currently running reader and register the new one. - A reader is a coroutine that calls this transports 'read' function. - The 'read' function calls can be paused by calling pause_reading of this transport. - :param reader: future reader - """ - if self._read_thread is not None: - # cancel currently running reader - if self._read_thread.cancel(): - try: - await self._read_thread - except asyncio.CancelledError: - pass - - # Create callback for debugging in case the reader is failing - err_callback = utils.create_error_check_callback(ignore=asyncio.CancelledError) - reader.add_done_callback(err_callback) - - self._read_thread = reader - - def get_reader(self): - return self._read_thread - async def read(self): """ Read data from the underlying socket. This function waits, From 6f4c41dae042edafc8636454ccda19eeb73d2b9b Mon Sep 17 00:00:00 2001 From: Poohl Date: Fri, 6 Nov 2020 00:07:36 +0100 Subject: [PATCH 02/43] Amiibo read support --- joycontrol/mcu.py | 226 +++++++++++++++++++++++++++++++++++++++++ joycontrol/protocol.py | 38 ++++--- joycontrol/report.py | 13 ++- run_controller_cli.py | 8 +- 4 files changed, 262 insertions(+), 23 deletions(-) create mode 100644 joycontrol/mcu.py diff --git a/joycontrol/mcu.py b/joycontrol/mcu.py new file mode 100644 index 00000000..f1551d1b --- /dev/null +++ b/joycontrol/mcu.py @@ -0,0 +1,226 @@ +import enum +import logging +import crc8 + +from joycontrol.controller_state import ControllerState + +logger = logging.getLogger(__name__) + +############################################################### +## This simulates the MCU in the right joycon/Pro-Controller ## +############################################################### +# This is sufficient to read one amiibo +# multiple can mess up the internal state +# anything but amiboo is not supported +# TODO: +# - figure out the NFC-content transfer, currently everything is hardcoded to work wich amiibo +# see https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp l2456ff for sugesstions +# - IR-camera +# - writing to Amiibo + +class MCUPowerState(enum.Enum): + SUSPENDED = 0x00 + READY = 0x01 + READY_UPDATE = 0x02 + CONFIGURED_NFC = 0x10 + CONFIGURED_IR = 0x11 # TODO: support this + +def MCU_crc(data): + if not isinstance(data, bytes): + data = bytes(data) + my_hash = crc8.crc8() + my_hash.update(data) + # At this point I'm not even mad this works... + return my_hash.digest()[0] + + +class MarvelCinematicUniverse: + def __init__(self, controller: ControllerState): + + self.power_state = MCUPowerState.SUSPENDED + + # NOT USED + # Just a store for the remaining configuration data + self.configuration = None + + # a cache to store the tag's data during reading + self.nfc_tag_data = None + # reading is done in multiple packets + # https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp line 2537 + # there seem to be some flow-controls and order-controls in this process + # which is all hardcoded here + self.reading_cursor = None + + # NOT IMPLEMENTED + # remove the tag from the controller after a successfull read + self.remove_nfc_after_read = False + + # controllerstate to look for nfc-data + self._controller = controller + + # We are getting 0x11 commands to do something, but cannot answer directly + # answers are passed inside the input-reports that are sent regularly + # they also seem to just be repeated until a new request comes in + self.response = [0] * 313 + + def set_remove_nfc_after_read(self, value): + self.remove_nfc_after_read = value + + def set_nfc_tag_data(self, data): + if not data: + self.nfc_tag_data = None + if not data is bytes: + data = bytes(data) + if len(data) != 540: + logger.warning("not implemented length") + return + self.nfc_tag_data = data + + def _get_status_data(self): + """ + create a status packet to be used when responding to 1101 commands + """ + out = [0] * 313 + if self.power_state == MCUPowerState.SUSPENDED: + return out + else: + out[0:7] = bytes.fromhex("01000000030005") + if self.power_state == MCUPowerState.CONFIGURED_NFC: + out[7] = 0x04 + else: + out[7] = 0x01 + out[-1] = MCU_crc(out[0:-1]) + return out + + # I don't actually know if we are supposed to change the MCU-data based on + # regular subcommands, but the switch is spamming the status-requests anyway, + # so sending responses seems to not hurt + + def set_power_state_cmd(self, power_state): + # 1 == (READY = 1) evaluates to false. WHY? + if power_state in (MCUPowerState.SUSPENDED.value, MCUPowerState.READY.value): + self.power_state = MCUPowerState(power_state) + self.response = [0] * 313 + if power_state == MCUPowerState.READY_UPDATE: + logger.error("NFC Update not implemented") + print(f"MCU: went into power_state {power_state}") + if self.power_state == MCUPowerState.SUSPENDED: + # the response probably doesnt matter. + self.response[0] = 0xFF + self.response[-1] = 0x6F + elif self.power_state == MCUPowerState.READY: + # this one does however + self.response[1] = 1 + self.response[-1] = 0xc1 + + def set_config_cmd(self, config): + if self.power_state == MCUPowerState.SUSPENDED: + if config[3] == 0: + # the switch does this during initial setup, presumably to disable + # any MCU weirdness + pass + else: + logger.warning("Set MCU Config not in READY mode") + elif self.power_state == MCUPowerState.READY: + self.configuration = config + logger.info(f"MCU Set configuration{self.configuration}") + self.response[0:7] = bytes.fromhex("01000000030005") + # see https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp l 2359 for values + if config[2] == 4: + # configure into NFC + self.response[7] = 0x04 + self.power_state = MCUPowerState.CONFIGURED_NFC + elif config[2] == 1: + # deconfigure / disable + self.response[7] = 0x01 + self.power_state = MCUPowerState.READY + #elif config[2] == 5: IR-Camera + #elif config[2] == 6: FW-Update Maybe + else: + logger.error("Not implemented configuration written") + self.response[7] = 0x01 + self.response[-1] = MCU_crc(self.response[0:-1]) + + def received_11(self, subcommand, subcommanddata): + if self.reading_cursor is not None: + return + self.response = [0] * 313 + if subcommand == 0x01: + # status request, respond with string and Powerstate + self.response[0:7] = bytes.fromhex("01000000030005") + self.response[7] = 0x04 if self.power_state == MCUPowerState.CONFIGURED_NFC else 0x01 + elif subcommand == 0x02: + # NFC command + if self.power_state != MCUPowerState.CONFIGURED_NFC: + logger.warning("NFC command outside NFC mode, ignoring") + elif subcommanddata[0] == 0x04: + # Start discovery + self.response[0:7] = bytes.fromhex("2a000500000931") + elif subcommanddata[0] == 0x01: + # Start polling + self.set_nfc_tag_data(self._controller.get_nfc()) + if self.nfc_tag_data: + # send the tag we found + self.response[0:16] = bytes.fromhex("2a000500000931090000000101020007") + self.response[16:19] = self.nfc_tag_data[0:3] + self.response[19:23] = self.nfc_tag_data[4:8] + else: + # send found nothing + self.response[0:8] = bytes.fromhex("2a00050000093101") + # we could report the tag immediately, but the switch doesn't like too much success + # TODO: better way to delay tag detection + logger.info("MCU: Looking for tag") + elif subcommanddata[0] == 0x06: + # start reading + if not self.reading_cursor: + self.reading_cursor = 0 + #elif subcommanddata[0] == 0x00: # cancel eveyhting -> exit mode? + elif subcommanddata[0] == 0x02: + # stop Polling + # AKA discovery again + self.response[0:7] = bytes.fromhex("2a000500000931") + self.response[-1] = MCU_crc(self.response[0:-1]) + + def get_data(self): + if self.reading_cursor is not None: + # reading seems to be just packets back to back, so we have to rewrite + # each when sending them + # TODO: Use a packet queue for this + self.response = [0] * 313 + if self.reading_cursor == 0: + # Data is sent in 2 packages plus a trailer + # the first one contains a lot of fixed header and the UUID + # the length and packetnumber is in there somewhere, see + # https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp line 2523ff + self.response[0:15] = bytes.fromhex("3a0007010001310200000001020007") + self.response[15:18] = self.nfc_tag_data[0:3] + self.response[18:22] = self.nfc_tag_data[4:8] + self.response[22:67] = bytes.fromhex( + "000000007DFDF0793651ABD7466E39C191BABEB856CEEDF1CE44CC75EAFB27094D087AE803003B3C7778860000") + self.response[67:-1] = self.nfc_tag_data[0:245] + elif self.reading_cursor == 1: + # the second one is mostely data, followed by presumably zeroes (zeroes work) + self.response[0:7] = bytes.fromhex("3a000702000927") + self.response[7:302] = self.nfc_tag_data[245:540] + elif self.reading_cursor == 2: + # the trailer includes the UUID again + self.response[0:16] = bytes.fromhex("2a000500000931040000000101020007") + self.response[16:19] = self.nfc_tag_data[0:3] + self.response[19:23] = self.nfc_tag_data[4:8] + self.nfc_tag_data = None + if self.remove_nfc_after_read: + self._controller.set_nfc(None) + elif self.reading_cursor == 3: + # we are done but still need a graceful shutdown + # HACK: sending the SUSPENDED response seems to not crash it + # The next thing the switch requests sometimes is start discovery + self.reading_cursor = None + # self.nfc_tag_data = None + self.response[0] = 0xff + # if self.remove_nfc_after_read: + # self._controller.set_nfc(None) + if self.reading_cursor is not None: + self.reading_cursor += 1 + self.response[-1] = MCU_crc(self.response[0:-1]) + return self.response + diff --git a/joycontrol/protocol.py b/joycontrol/protocol.py index 522f95f1..16ea7899 100644 --- a/joycontrol/protocol.py +++ b/joycontrol/protocol.py @@ -2,7 +2,6 @@ import logging import time from asyncio import BaseTransport, BaseProtocol -from contextlib import suppress from typing import Optional, Union, Tuple, Text from joycontrol import utils @@ -11,6 +10,7 @@ from joycontrol.memory import FlashMemory from joycontrol.report import OutputReport, SubCommand, InputReport, OutputReportID from joycontrol.transport import NotConnectedError +from joycontrol.mcu import MarvelCinematicUniverse logger = logging.getLogger(__name__) @@ -24,7 +24,6 @@ def create_controller_protocol(): return create_controller_protocol - class ControllerProtocol(BaseProtocol): def __init__(self, controller: Controller, spi_flash: FlashMemory = None): self.controller = controller @@ -43,6 +42,8 @@ def __init__(self, controller: Controller, spi_flash: FlashMemory = None): # daley between two input reports self.send_delay = 1/15 + self._mcu = MarvelCinematicUniverse(self._controller_state) + # None = Send empty input reports & answer to sub commands self._input_report_mode = None @@ -94,8 +95,10 @@ async def write(self, input_report: InputReport): self._input_report_timer = (self._input_report_timer + 1) % 0x100 # this would close the grip menu, so go into fullspeed - if self._controller_state.button_state.a_is_set() or self._controller_state.button_state.b_is_set(): - print("Exiting grip-menu") + if self.send_delay >= 1/15 and (self._controller_state.button_state.a_is_set() + or self._controller_state.button_state.b_is_set() + or self._controller_state.button_state.home_is_set()): + logger.info("Leaving grip menu") self.send_delay = 1 / 60 await self.transport.write(input_report) @@ -116,6 +119,9 @@ def connection_made(self, transport: BaseTransport) -> None: logger.debug('Connection established.') self.transport = transport self._writer = asyncio.ensure_future(self.writer()) + self._writer.add_done_callback( + utils.create_error_check_callback() + ) def connection_lost(self, exc: Optional[Exception] = None) -> None: if self.transport is not None: @@ -133,9 +139,10 @@ def error_received(self, exc: Exception) -> None: async def writer(self): """ This continuously sends input reports to the switch. - This relys on the asyncio scheduler to sneak the additional - subcommand-replys in + This relies on the asyncio scheduler to sneak the additional + subcommand-replies in """ + logger.info("writer started") while self.transport: last_send_time = time.time() input_report = InputReport() @@ -147,19 +154,21 @@ async def writer(self): input_report.set_input_report_id(self._input_report_mode) input_report.set_6axis_data() if self._input_report_mode == 0x31: - input_report.set_ir_nfc_data(self.mcu.sending_31()) + input_report.set_ir_nfc_data(self._mcu.get_data()) + await self.write(input_report) # calculate delay current_time = time.time() active_time = current_time - last_send_time sleep_time = self.send_delay - active_time - last_send_time = current_time + # last_send_time = current_time if sleep_time < 0: # logger.warning(f'Code is running {abs(sleep_time)} s too slow!') sleep_time = 0 await asyncio.sleep(sleep_time) + logger.warning("Writer exited...") return None async def report_received(self, data: Union[bytes, Text], addr: Tuple[str, int]) -> None: @@ -180,10 +189,11 @@ async def report_received(self, data: Union[bytes, Text], addr: Tuple[str, int]) if output_report_id == OutputReportID.SUB_COMMAND: await self._reply_to_sub_command(report) elif output_report_id == OutputReportID.RUMBLE_ONLY: - #TODO Rumble + # TODO Rumble pass elif output_report_id == OutputReportID.REQUEST_IR_NFC_MCU: - #TODO NFC + # TODO: support 0x11 outputs in OutputReport + self._mcu.received_11(report.data[11], report.get_sub_command_data()) pass else: logger.warning(f'Output report {output_report_id} not implemented - ignoring') @@ -199,7 +209,7 @@ async def _reply_to_sub_command(self, report): if sub_command is None: raise ValueError('Received output report does not contain a sub command') - logging.info(f'received output report - Sub command {sub_command}') + logging.info(f'received Sub command {sub_command}') sub_command_data = report.get_sub_command_data() assert sub_command_data is not None @@ -351,11 +361,12 @@ async def _command_enable_vibration(self, sub_command_data): await self.write(input_report) async def _command_set_nfc_ir_mcu_config(self, sub_command_data): - # TODO NFC input_report = InputReport() input_report.set_input_report_id(0x21) input_report.set_misc() + self._mcu.set_config_cmd(sub_command_data) + input_report.set_ack(0xA0) input_report.reply_to_subcommand_id(SubCommand.SET_NFC_IR_MCU_CONFIG.value) @@ -371,6 +382,8 @@ async def _command_set_nfc_ir_mcu_state(self, sub_command_data): input_report.set_input_report_id(0x21) input_report.set_misc() + self._mcu.set_power_state_cmd(sub_command_data[0]) + if sub_command_data[0] == 0x01: # 0x01 = Resume input_report.set_ack(0x80) @@ -382,7 +395,6 @@ async def _command_set_nfc_ir_mcu_state(self, sub_command_data): else: raise NotImplementedError(f'Argument {sub_command_data[0]} of {SubCommand.SET_NFC_IR_MCU_STATE} ' f'not implemented.') - await self.write(input_report) async def _command_set_player_lights(self, sub_command_data): diff --git a/joycontrol/report.py b/joycontrol/report.py index 32182459..a6574b1e 100644 --- a/joycontrol/report.py +++ b/joycontrol/report.py @@ -10,7 +10,7 @@ class InputReport: """ def __init__(self, data=None): if not data: - self.data = [0x00] * 364 + self.data = [0x00] * 363 # all input reports are prepended with 0xA1 self.data[0] = 0xA1 else: @@ -113,12 +113,11 @@ def set_6axis_data(self): self.data[i] = 0x00 def set_ir_nfc_data(self, data): - if 50 + len(data) > len(self.data): - raise ValueError('Too much data.') - - # write to data - for i in range(len(data)): - self.data[50 + i] = data[i] + if len(data) > 313: + raise ValueError(f'Too much data {len(data)} > 313.') + elif len(data) != 313: + print("warning : too short mcu data") + self.data[50:50+len(data)] = data def reply_to_subcommand_id(self, _id): if isinstance(_id, SubCommand): diff --git a/run_controller_cli.py b/run_controller_cli.py index c84597b6..3b8447ae 100755 --- a/run_controller_cli.py +++ b/run_controller_cli.py @@ -248,7 +248,7 @@ async def nfc(*args): nfc Set controller state NFC content to file nfc remove Remove NFC content from controller state """ - logger.error('NFC Support was removed from joycontrol - see https://github.com/mart1nro/joycontrol/issues/80') + #logger.error('NFC Support was removed from joycontrol - see https://github.com/mart1nro/joycontrol/issues/80') if controller_state.get_controller() == Controller.JOYCON_L: raise ValueError('NFC content cannot be set for JOYCON_L') elif not args: @@ -260,12 +260,16 @@ async def nfc(*args): _loop = asyncio.get_event_loop() with open(args[0], 'rb') as nfc_file: content = await _loop.run_in_executor(None, nfc_file.read) + logger.info("CLI: Set nfc content") controller_state.set_nfc(content) cli.add_command(nfc.__name__, nfc) async def _main(args): + # Get controller name to emulate from arguments + controller = Controller.from_arg(args.controller) + # parse the spi flash if args.spi_flash: with open(args.spi_flash, 'rb') as spi_flash_file: @@ -274,8 +278,6 @@ async def _main(args): # Create memory containing default controller stick calibration spi_flash = FlashMemory() - # Get controller name to emulate from arguments - controller = Controller.from_arg(args.controller) with utils.get_output(path=args.log, default=None) as capture_file: # prepare the the emulated controller From abdc1459ee7fb638efe2241e7aa6b11a25f5b0aa Mon Sep 17 00:00:00 2001 From: Poohl Date: Wed, 9 Dec 2020 14:12:40 +0100 Subject: [PATCH 03/43] working properly w reconnect --- README.md | 4 +- joycontrol/mcu.py | 346 ++++++++++++++++++++++++++--------------- joycontrol/protocol.py | 10 +- mcu.md | 103 ++++++++++++ run_controller_cli.py | 15 +- 5 files changed, 341 insertions(+), 137 deletions(-) create mode 100644 mcu.md diff --git a/README.md b/README.md index 86d8a95a..e7a19c5f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Tested on Ubuntu 19.10, and with Raspberry Pi 3B+ and 4B Raspbian GNU/Linux 10 ( Emulation of JOYCON_R, JOYCON_L and PRO_CONTROLLER. Able to send: - button commands - stick state -- ~~nfc data~~ (removed, see [#80](https://github.com/mart1nro/joycontrol/issues/80)) +- nfc data read ## Installation - Install dependencies @@ -51,7 +51,7 @@ Call "help" to see a list of available commands. - Some bluetooth adapters seem to cause disconnects for reasons unknown, try to use an usb adapter instead - Incompatibility with Bluetooth "input" plugin requires a bluetooth restart, see [#8](https://github.com/mart1nro/joycontrol/issues/8) - It seems like the Switch is slower processing incoming messages while in the "Change Grip/Order" menu. - This causes flooding of packets and makes pairing somewhat inconsistent. + This causes flooding of packets and makes input after initial pairing somewhat inconsistent. Not sure yet what exactly a real controller does to prevent that. A workaround is to use the reconnect option after a controller was paired once, so that opening of the "Change Grip/Order" menu is not required. diff --git a/joycontrol/mcu.py b/joycontrol/mcu.py index f1551d1b..0b81258e 100644 --- a/joycontrol/mcu.py +++ b/joycontrol/mcu.py @@ -1,11 +1,16 @@ import enum import logging import crc8 +import traceback from joycontrol.controller_state import ControllerState logger = logging.getLogger(__name__) +def debug(args): + print(args) + return args + ############################################################### ## This simulates the MCU in the right joycon/Pro-Controller ## ############################################################### @@ -18,12 +23,36 @@ # - IR-camera # - writing to Amiibo +# These Values are used in set_power, set_config and get_status packets +# But not all of them can appear in every one +# see https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp l 2359 for set_config class MCUPowerState(enum.Enum): - SUSPENDED = 0x00 - READY = 0x01 + SUSPENDED = 0x00 # set_power + READY = 0x01 # set_power, set_config, get_status READY_UPDATE = 0x02 - CONFIGURED_NFC = 0x10 - CONFIGURED_IR = 0x11 # TODO: support this + CONFIGURED_NFC = 0x04 # set_config, get_status + # CONFIGURED_IR = 0x05 # TODO: support this + # CONFIGUERED_UPDATE = 0x06 + +SET_POWER_VALUES = ( + MCUPowerState.SUSPENDED.value, + MCUPowerState.READY.value, +# MCUPowerState.READY_UPDATE.value, +) + +SET_CONFIG_VALUES = ( + MCUPowerState.READY.value, + MCUPowerState.CONFIGURED_NFC.value, +# MCUPowerState.CONFIGURED_IR.value, +) + +GET_STATUS_VALUES = ( + MCUPowerState.READY.value, +# MCUPowerState.READY_UPDATE.value, + MCUPowerState.CONFIGURED_NFC.value, +# MCUPowerState.CONFIGURED_IR.value +) + def MCU_crc(data): if not isinstance(data, bytes): @@ -34,6 +63,29 @@ def MCU_crc(data): return my_hash.digest()[0] +class NFC_state(enum.Enum): + NONE = 0x00 + POLL = 0x01 + PENDING_READ = 0x02 + POLL_AGAIN = 0x09 + + +class MCU_Message: + def __init__(self, *args, background=0, checksum=MCU_crc): + self.data = bytearray([background] * 313) + c = 0 + for i in args: + if isinstance(i, str): + b = bytes.fromhex(i) + else: + b = bytes(i) + self.data[c:c+len(b)] = b + if checksum: + self.data[-1] = checksum(self.data[0:-1]) + + def __bytes__(self): + return self.data + class MarvelCinematicUniverse: def __init__(self, controller: ControllerState): @@ -43,13 +95,9 @@ def __init__(self, controller: ControllerState): # Just a store for the remaining configuration data self.configuration = None - # a cache to store the tag's data during reading + # a cache to store the tag's data between Poll and Read self.nfc_tag_data = None - # reading is done in multiple packets - # https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp line 2537 - # there seem to be some flow-controls and order-controls in this process - # which is all hardcoded here - self.reading_cursor = None + self.nfc_state = NFC_state.NONE # NOT IMPLEMENTED # remove the tag from the controller after a successfull read @@ -59,14 +107,35 @@ def __init__(self, controller: ControllerState): self._controller = controller # We are getting 0x11 commands to do something, but cannot answer directly - # answers are passed inside the input-reports that are sent regularly - # they also seem to just be repeated until a new request comes in - self.response = [0] * 313 + # responses have to be passed in regular input reports + # If there was no Command, this is the default report + self.no_response = [0] * 313 + self.no_response[0] = 0xff + self.no_response[-1] = MCU_crc(self.no_response[:-1]) + self.response_queue = [] + self.max_response_queue_len = 3 + + #debug + self.reading = 0 + + def _flush_response_queue(self): + self.response_queue = [] + + def _queue_response(self, resp): + if resp == None: # the if "missing return statement" because python + traceback.print_stack() + exit(1) + if len(self.response_queue) <= self.max_response_queue_len: + self.response_queue.append(resp) + + def _force_queue_response(self, resp): + self.response_queue.append(resp) def set_remove_nfc_after_read(self, value): self.remove_nfc_after_read = value def set_nfc_tag_data(self, data): + logger.info("MCU-NFC: set NFC tag data") if not data: self.nfc_tag_data = None if not data is bytes: @@ -76,20 +145,48 @@ def set_nfc_tag_data(self, data): return self.nfc_tag_data = data - def _get_status_data(self): + def entered_31_input_mode(self): + resp = [0] * 313 + resp[0:8] = bytes.fromhex("0100000008001b01") + resp[-1] = MCU_crc(resp[:-1]) + self._queue_response(resp) + + def _get_status_data(self, args=None): """ create a status packet to be used when responding to 1101 commands """ - out = [0] * 313 if self.power_state == MCUPowerState.SUSPENDED: - return out - else: - out[0:7] = bytes.fromhex("01000000030005") - if self.power_state == MCUPowerState.CONFIGURED_NFC: - out[7] = 0x04 - else: - out[7] = 0x01 + logger.warning("MCU: status request when disabled") + return self.no_response + elif self.power_state.value in GET_STATUS_VALUES: + resp = [0] * 313 + resp[0:7] = bytes.fromhex("0100000008001b") + resp[7] = self.power_state.value + resp[-1] = MCU_crc(resp[:-1]) + return resp + #self._queue_response(resp) + #return self._get_status_data() + #else: + #out = [0] * 313 + #out[0:7] = bytes.fromhex("01000000030005") + #out[7] = MCUPowerState.READY.value + #out[-1] = MCU_crc(out[0:-1]) + #return out + + def _get_nfc_status_data(self, args): + out = [0] * 313 + out[0:7] = bytes.fromhex("2a000500000931") + out[7] = self.nfc_state.value + if self.nfc_state == NFC_state.POLL and self._controller.get_nfc(): + self.set_nfc_tag_data(self._controller.get_nfc()) + if self.nfc_tag_data and self.nfc_state in (NFC_state.POLL, NFC_state.POLL_AGAIN): + out[8:16] = bytes.fromhex("0000000101020007") + out[16:19] = self.nfc_tag_data[0:3] + out[19:23] = self.nfc_tag_data[4:8] + self.nfc_state = NFC_state.POLL_AGAIN out[-1] = MCU_crc(out[0:-1]) + self._queue_response(out) + self._queue_response(out) return out # I don't actually know if we are supposed to change the MCU-data based on @@ -97,130 +194,121 @@ def _get_status_data(self): # so sending responses seems to not hurt def set_power_state_cmd(self, power_state): - # 1 == (READY = 1) evaluates to false. WHY? - if power_state in (MCUPowerState.SUSPENDED.value, MCUPowerState.READY.value): + logger.info(f"MCU: Set power state cmd {power_state}") + if power_state in SET_POWER_VALUES: self.power_state = MCUPowerState(power_state) - self.response = [0] * 313 - if power_state == MCUPowerState.READY_UPDATE: - logger.error("NFC Update not implemented") - print(f"MCU: went into power_state {power_state}") - if self.power_state == MCUPowerState.SUSPENDED: - # the response probably doesnt matter. - self.response[0] = 0xFF - self.response[-1] = 0x6F - elif self.power_state == MCUPowerState.READY: - # this one does however - self.response[1] = 1 - self.response[-1] = 0xc1 + else: + logger.error(f"not implemented power state {power_state}") + self.power_state = MCUPowerState.READY + self._queue_response(self._get_status_data()) def set_config_cmd(self, config): if self.power_state == MCUPowerState.SUSPENDED: - if config[3] == 0: + if config[2] == 0: # the switch does this during initial setup, presumably to disable # any MCU weirdness pass else: logger.warning("Set MCU Config not in READY mode") elif self.power_state == MCUPowerState.READY: - self.configuration = config - logger.info(f"MCU Set configuration{self.configuration}") - self.response[0:7] = bytes.fromhex("01000000030005") - # see https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp l 2359 for values - if config[2] == 4: - # configure into NFC - self.response[7] = 0x04 - self.power_state = MCUPowerState.CONFIGURED_NFC - elif config[2] == 1: - # deconfigure / disable - self.response[7] = 0x01 - self.power_state = MCUPowerState.READY - #elif config[2] == 5: IR-Camera - #elif config[2] == 6: FW-Update Maybe + if config[2] in SET_CONFIG_VALUES: + self.power_state = MCUPowerState(config[2]) + self.configuration = config + logger.info(f"MCU Set configuration {self.power_state} {self.configuration}") else: - logger.error("Not implemented configuration written") - self.response[7] = 0x01 - self.response[-1] = MCU_crc(self.response[0:-1]) - - def received_11(self, subcommand, subcommanddata): - if self.reading_cursor is not None: - return - self.response = [0] * 313 - if subcommand == 0x01: - # status request, respond with string and Powerstate - self.response[0:7] = bytes.fromhex("01000000030005") - self.response[7] = 0x04 if self.power_state == MCUPowerState.CONFIGURED_NFC else 0x01 - elif subcommand == 0x02: - # NFC command - if self.power_state != MCUPowerState.CONFIGURED_NFC: - logger.warning("NFC command outside NFC mode, ignoring") - elif subcommanddata[0] == 0x04: - # Start discovery - self.response[0:7] = bytes.fromhex("2a000500000931") - elif subcommanddata[0] == 0x01: - # Start polling - self.set_nfc_tag_data(self._controller.get_nfc()) - if self.nfc_tag_data: - # send the tag we found - self.response[0:16] = bytes.fromhex("2a000500000931090000000101020007") - self.response[16:19] = self.nfc_tag_data[0:3] - self.response[19:23] = self.nfc_tag_data[4:8] - else: - # send found nothing - self.response[0:8] = bytes.fromhex("2a00050000093101") - # we could report the tag immediately, but the switch doesn't like too much success - # TODO: better way to delay tag detection - logger.info("MCU: Looking for tag") - elif subcommanddata[0] == 0x06: - # start reading - if not self.reading_cursor: - self.reading_cursor = 0 - #elif subcommanddata[0] == 0x00: # cancel eveyhting -> exit mode? - elif subcommanddata[0] == 0x02: - # stop Polling - # AKA discovery again - self.response[0:7] = bytes.fromhex("2a000500000931") - self.response[-1] = MCU_crc(self.response[0:-1]) + self.power_state = MCUPowerState.READY + logger.error(f"Not implemented configuration written {config[2]} {config}") + if self.power_state == MCUPowerState.CONFIGURED_NFC: + self.nfc_state = NFC_state.NONE + self._queue_response(self._get_status_data()) - def get_data(self): - if self.reading_cursor is not None: - # reading seems to be just packets back to back, so we have to rewrite - # each when sending them - # TODO: Use a packet queue for this - self.response = [0] * 313 - if self.reading_cursor == 0: + def handle_nfc_subcommand(self, com, data): + """ + This generates responses for NFC commands and thus implements the entire + NFC-behaviour + @param com: the NFC-command (not the 0x02, the byte after that) + @param data: the remaining data + """ + if com == 0x04: # status / response request + self._queue_response(self._get_nfc_status_data(data)) + elif com == 0x01: # start polling, should we queue a nfc_status? + logger.info("MCU-NFC: start polling") + self.nfc_state = NFC_state.POLL + elif com == 0x06: # read, we probably should not respond to this at all, + # since each packet is queried individually by the switch, but parsing these + # 04 packets is just annoying + logger.info("MCU-NFC: reading...") + if self.nfc_tag_data: + self._flush_response_queue() # Data is sent in 2 packages plus a trailer # the first one contains a lot of fixed header and the UUID # the length and packetnumber is in there somewhere, see # https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp line 2523ff - self.response[0:15] = bytes.fromhex("3a0007010001310200000001020007") - self.response[15:18] = self.nfc_tag_data[0:3] - self.response[18:22] = self.nfc_tag_data[4:8] - self.response[22:67] = bytes.fromhex( + out = [0] * 313 + out[0:15] = bytes.fromhex("3a0007010001310200000001020007") + out[15:18] = self.nfc_tag_data[0:3] + out[18:22] = self.nfc_tag_data[4:8] + out[22:67] = bytes.fromhex( "000000007DFDF0793651ABD7466E39C191BABEB856CEEDF1CE44CC75EAFB27094D087AE803003B3C7778860000") - self.response[67:-1] = self.nfc_tag_data[0:245] - elif self.reading_cursor == 1: - # the second one is mostely data, followed by presumably zeroes (zeroes work) - self.response[0:7] = bytes.fromhex("3a000702000927") - self.response[7:302] = self.nfc_tag_data[245:540] - elif self.reading_cursor == 2: + out[67:-1] = self.nfc_tag_data[0:245] + out[-1] = MCU_crc(out[0:-1]) + self._force_queue_response(out) + # the second one is mostely data, followed by presumably anything (zeroes work) + out = [0] * 313 + out[0:7] = bytes.fromhex("3a000702000927") + out[7:302] = self.nfc_tag_data[245:540] + out[-1] = MCU_crc(out[0:-1]) + self._force_queue_response(out) # the trailer includes the UUID again - self.response[0:16] = bytes.fromhex("2a000500000931040000000101020007") - self.response[16:19] = self.nfc_tag_data[0:3] - self.response[19:23] = self.nfc_tag_data[4:8] - self.nfc_tag_data = None - if self.remove_nfc_after_read: - self._controller.set_nfc(None) - elif self.reading_cursor == 3: - # we are done but still need a graceful shutdown - # HACK: sending the SUSPENDED response seems to not crash it - # The next thing the switch requests sometimes is start discovery - self.reading_cursor = None - # self.nfc_tag_data = None - self.response[0] = 0xff - # if self.remove_nfc_after_read: + out = [0] * 313 + out[0:16] = bytes.fromhex("2a000500000931040000000101020007") + out[16:19] = self.nfc_tag_data[0:3] + out[19:23] = self.nfc_tag_data[4:8] + out[-1] = MCU_crc(out[0:-1]) + self._force_queue_response(out) + self.reading = 3 + for msg in self.response_queue: + print("MCU-NFC: reading, queued", msg) + #self.nfc_tag_data = None + #if self.remove_nfc_after_read: # self._controller.set_nfc(None) - if self.reading_cursor is not None: - self.reading_cursor += 1 - self.response[-1] = MCU_crc(self.response[0:-1]) - return self.response + # elif com == 0x00: # cancel eveyhting -> exit mode? + elif com == 0x02: # stop polling, respond? + logger.info("MCU-NFC: stop polling...") + self.nfc_state = NFC_state.NONE + else: + logger.error("unhandled NFC subcommand", com, data) + + def received_11(self, subcommand, subcommanddata): + """ + This function handles all 0x11 output-reports. + @param subcommand: the subcommand as integer + @param subcommanddata: the remaining data + @return: None + """ + if subcommand == 0x01: + # status request + self._queue_response(self._get_status_data(subcommanddata)) + elif subcommand == 0x02: + # NFC command + if self.power_state != MCUPowerState.CONFIGURED_NFC: + logger.warning("NFC command outside NFC mode, ignoring", subcommand, subcommanddata) + else: + self.handle_nfc_subcommand(subcommanddata[0], subcommanddata[1:]) + else: + logger.error("unknown 11 subcommand", subcommand, subcommanddata) + + def get_data(self): + """ + The function returning what is to write into mcu data of tha outgoing 0x31 packet + usually it is some queued response + @return: the data + """ + if self.reading > 0: + print("sending", self.response_queue[0]) + self.reading -= 1 + if len(self.response_queue) > 0: + return self.response_queue.pop(0) + else: + return self.no_response diff --git a/joycontrol/protocol.py b/joycontrol/protocol.py index 16ea7899..ff8c7eb3 100644 --- a/joycontrol/protocol.py +++ b/joycontrol/protocol.py @@ -15,17 +15,17 @@ logger = logging.getLogger(__name__) -def controller_protocol_factory(controller: Controller, spi_flash=None): +def controller_protocol_factory(controller: Controller, spi_flash=None, reconnect = False): if isinstance(spi_flash, bytes): spi_flash = FlashMemory(spi_flash_memory_data=spi_flash) def create_controller_protocol(): - return ControllerProtocol(controller, spi_flash=spi_flash) + return ControllerProtocol(controller, spi_flash=spi_flash, grip_menu = not reconnect) return create_controller_protocol class ControllerProtocol(BaseProtocol): - def __init__(self, controller: Controller, spi_flash: FlashMemory = None): + def __init__(self, controller: Controller, spi_flash: FlashMemory = None, grip_menu = False): self.controller = controller self.spi_flash = spi_flash @@ -40,8 +40,8 @@ def __init__(self, controller: Controller, spi_flash: FlashMemory = None): self._controller_state_sender = None self._writer = None # daley between two input reports - self.send_delay = 1/15 - + self.send_delay = 1/60 if not grip_menu else 1/15 + self._mcu = MarvelCinematicUniverse(self._controller_state) # None = Send empty input reports & answer to sub commands diff --git a/mcu.md b/mcu.md new file mode 100644 index 00000000..3b384999 --- /dev/null +++ b/mcu.md @@ -0,0 +1,103 @@ + +# MCU +MarvelCinematicUniverse? + +This is a chip on the switch's controllers handling all kinds of high data throughput stuff. + +There seem to be 5 modes (OFF, starting, ON, NFC, IR, UPDATE) and a 'busy' flag. + +Requests are being sent by the switch using 11 output reports, responses are piggibacked to regular input using the MCU-datafield in 31 input reports (and everything is sent as a response to a request). All responses feature a checksum in the last byte that is computed as follows: + +checksum: `[crc8(mcu_data[0:-1])]` + +Request notation: The values given will always be the byte after `a2` and the one ten bytes after that (maybe followed by subsequent bytes) + +All numbers are in continuous hexadecimal + +# Generic MCU requests + +* If there is no response to be sent, the MCU-datafield is + + `no_request`: `ff00....00[checksum]` + +* At first 31 mode is enabled. (`01 0331`) + + `response`: `0100...00[checksum]` + +* then the MCU is enabled (`01 2201`) + + the MCU goes through starting -> firmware-update -> on, not sure if we have to respond to this, the next command is always a status request so sending this dosn't hurt. + + The firware-update phase can (and should) be skipped. + + response: `[status | no_request]` + +* A status request (`11 01`), response: + + `status`: `0100[busy]0008001b[power_state]` + + where busy is `ff` after initial enable for a while, then goes to `00` +and power_state is `01` for off, `00` for starting, `01` for on, `04` for NFC mode, `06` for firmware-update (this is not sure) + +## NFC & Amiibo + +Here I describe what I found the nfc-reading process of amiibos looks like: + +### generic stuff: + +* the Tag data `nfc_data` is 540 bytes in length + +* the UID is 7 of those bytes a follows: + + `tag_uid`: `[nfc_data(0;3(][nfc_data(4;8(]` + +### NFC Requests + +* command: configure NFC (`01 2121`) + + response: nothing (but the command-ack is weird) + +* get status / start discovery: `11 0204` + + This is spammed like hell and seems to be some other kind of nfc-status-request + + `nfc_status`: `2a000500000931[nfc_state]` + + where nfc_state is + - `00` for nothing/polling startup or something like this + - `01` for polling without success + - `01` for polling with success, followed by `poll_success`: `0000000101020007[tag_uid]` + - `02` for pending read, followed by `[poll_success]` + - `09` for polling with success, same tag as last time, followed by `[poll_success]` + + Note: the joycon thrice reported 09 followed by just 00. in response to 0204 commands after stop-polling + +* poll (`11 0201`) + + look for a new nfc tag now + + response: noting, maybe `[nfc_status]` + +* read (`11 0206`) + + respond with the 3 read packets read1 read2 read3 followed by no_request + + Note: it seems every packet is requested individually using a 0204, as on of the bytes in the request increments from 0 to 3 shortly before/after the data is sent. + + the Packets: + + read1: `3a0007010001310200000001020007[TAG_UID]000000007DFDF0793651ABD7466E39C191BABEB856CEEDF1CE44CC75EAFB27094D087AE803003B3C7778860000[nfc_data(0;245(][checksum]` + + read2: `3a000702000927[nfc data (245;540(][checksum]` + + read3: `2a000500000931040000000101020007[TAG_UID]00...00[checksum]` + +* stop polling (`11 0202`) + + after a poll, presumably stop looking for a tag discovered during poll command + + no response + +* cancel (`11 0200`) + + No idea diff --git a/run_controller_cli.py b/run_controller_cli.py index 3b8447ae..44bc4c9c 100755 --- a/run_controller_cli.py +++ b/run_controller_cli.py @@ -164,6 +164,7 @@ async def mash_button(controller_state, button, interval): await user_input + def _register_commands_with_controller_state(controller_state, cli): """ Commands registered here can use the given controller state. @@ -195,6 +196,18 @@ async def mash(*args): cli.add_command(mash.__name__, mash) + async def click(*args): + + if not args: + raise ValueError('"click" command requires a button!') + + await controller_state.connect() + ensure_valid_button(controller_state, *args) + + await button_push(controller_state, *args) + + cli.add_command(click.__name__, click) + # Hold a button command async def hold(*args): """ @@ -281,7 +294,7 @@ async def _main(args): with utils.get_output(path=args.log, default=None) as capture_file: # prepare the the emulated controller - factory = controller_protocol_factory(controller, spi_flash=spi_flash) + factory = controller_protocol_factory(controller, spi_flash=spi_flash, reconnect = args.reconnect_bt_addr) ctl_psm, itr_psm = 17, 19 transport, protocol = await create_hid_server(factory, reconnect_bt_addr=args.reconnect_bt_addr, ctl_psm=ctl_psm, From 53a99a539848b4b67bb08d2fa9e128aab03537ac Mon Sep 17 00:00:00 2001 From: Poohl Date: Wed, 9 Dec 2020 15:22:19 +0100 Subject: [PATCH 04/43] NFC Amiibo cleanup --- joycontrol/mcu.py | 204 +++++++++++++++++++---------------------- joycontrol/protocol.py | 6 +- 2 files changed, 98 insertions(+), 112 deletions(-) diff --git a/joycontrol/mcu.py b/joycontrol/mcu.py index 0b81258e..027e8d91 100644 --- a/joycontrol/mcu.py +++ b/joycontrol/mcu.py @@ -14,7 +14,7 @@ def debug(args): ############################################################### ## This simulates the MCU in the right joycon/Pro-Controller ## ############################################################### -# This is sufficient to read one amiibo +# This is sufficient to read one amiibo when simulation Pro-Controller # multiple can mess up the internal state # anything but amiboo is not supported # TODO: @@ -22,6 +22,7 @@ def debug(args): # see https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp l2456ff for sugesstions # - IR-camera # - writing to Amiibo +# - verify right joycon # These Values are used in set_power, set_config and get_status packets # But not all of them can appear in every one @@ -70,21 +71,36 @@ class NFC_state(enum.Enum): POLL_AGAIN = 0x09 -class MCU_Message: - def __init__(self, *args, background=0, checksum=MCU_crc): - self.data = bytearray([background] * 313) - c = 0 - for i in args: - if isinstance(i, str): - b = bytes.fromhex(i) - else: - b = bytes(i) - self.data[c:c+len(b)] = b - if checksum: - self.data[-1] = checksum(self.data[0:-1]) +def pack_message(*args, background=0, checksum=MCU_crc): + """ + convinience function that packes + * hex strings + * byte lists + * integer lists + * Enums + * integers + into a 313 bytes long MCU response + """ + data = bytearray([background] * 313) + cur = 0 + for arg in args: + if isinstance(arg, str): + arg = bytes.fromhex(arg) + elif isinstance(arg, int): + arg = bytes([arg]) + elif isinstance(arg, enum.Enum): + arg = bytes([arg.value]) + else: + arg = bytes(arg) + arg_len = len(arg) + if arg_len + cur > 313: + logger.warn("MCU: too long message packed") + data[cur:cur+arg_len] = arg + cur += arg_len + if checksum: + data[-1] = checksum(data[0:-1]) + return data - def __bytes__(self): - return self.data class MarvelCinematicUniverse: def __init__(self, controller: ControllerState): @@ -108,16 +124,11 @@ def __init__(self, controller: ControllerState): # We are getting 0x11 commands to do something, but cannot answer directly # responses have to be passed in regular input reports - # If there was no Command, this is the default report - self.no_response = [0] * 313 - self.no_response[0] = 0xff - self.no_response[-1] = MCU_crc(self.no_response[:-1]) + # If there was no command, this is the default report + self.no_response = pack_message(0xff) self.response_queue = [] self.max_response_queue_len = 3 - #debug - self.reading = 0 - def _flush_response_queue(self): self.response_queue = [] @@ -134,6 +145,7 @@ def _force_queue_response(self, resp): def set_remove_nfc_after_read(self, value): self.remove_nfc_after_read = value + # called somwhere in get_nfc_status with _controller.get_nfc() def set_nfc_tag_data(self, data): logger.info("MCU-NFC: set NFC tag data") if not data: @@ -145,12 +157,6 @@ def set_nfc_tag_data(self, data): return self.nfc_tag_data = data - def entered_31_input_mode(self): - resp = [0] * 313 - resp[0:8] = bytes.fromhex("0100000008001b01") - resp[-1] = MCU_crc(resp[:-1]) - self._queue_response(resp) - def _get_status_data(self, args=None): """ create a status packet to be used when responding to 1101 commands @@ -159,69 +165,20 @@ def _get_status_data(self, args=None): logger.warning("MCU: status request when disabled") return self.no_response elif self.power_state.value in GET_STATUS_VALUES: - resp = [0] * 313 - resp[0:7] = bytes.fromhex("0100000008001b") - resp[7] = self.power_state.value - resp[-1] = MCU_crc(resp[:-1]) - return resp - #self._queue_response(resp) - #return self._get_status_data() - #else: - #out = [0] * 313 - #out[0:7] = bytes.fromhex("01000000030005") - #out[7] = MCUPowerState.READY.value - #out[-1] = MCU_crc(out[0:-1]) - #return out + return pack_message("0100000008001b", self.power_state) def _get_nfc_status_data(self, args): - out = [0] * 313 - out[0:7] = bytes.fromhex("2a000500000931") - out[7] = self.nfc_state.value if self.nfc_state == NFC_state.POLL and self._controller.get_nfc(): self.set_nfc_tag_data(self._controller.get_nfc()) if self.nfc_tag_data and self.nfc_state in (NFC_state.POLL, NFC_state.POLL_AGAIN): - out[8:16] = bytes.fromhex("0000000101020007") - out[16:19] = self.nfc_tag_data[0:3] - out[19:23] = self.nfc_tag_data[4:8] + out = pack_message("2a000500000931", self.nfc_state, "0000000101020007", self.nfc_tag_data[0:3], self.nfc_tag_data[4:8]) self.nfc_state = NFC_state.POLL_AGAIN - out[-1] = MCU_crc(out[0:-1]) + else: + out = pack_message("2a000500000931", self.nfc_state) self._queue_response(out) self._queue_response(out) return out - # I don't actually know if we are supposed to change the MCU-data based on - # regular subcommands, but the switch is spamming the status-requests anyway, - # so sending responses seems to not hurt - - def set_power_state_cmd(self, power_state): - logger.info(f"MCU: Set power state cmd {power_state}") - if power_state in SET_POWER_VALUES: - self.power_state = MCUPowerState(power_state) - else: - logger.error(f"not implemented power state {power_state}") - self.power_state = MCUPowerState.READY - self._queue_response(self._get_status_data()) - - def set_config_cmd(self, config): - if self.power_state == MCUPowerState.SUSPENDED: - if config[2] == 0: - # the switch does this during initial setup, presumably to disable - # any MCU weirdness - pass - else: - logger.warning("Set MCU Config not in READY mode") - elif self.power_state == MCUPowerState.READY: - if config[2] in SET_CONFIG_VALUES: - self.power_state = MCUPowerState(config[2]) - self.configuration = config - logger.info(f"MCU Set configuration {self.power_state} {self.configuration}") - else: - self.power_state = MCUPowerState.READY - logger.error(f"Not implemented configuration written {config[2]} {config}") - if self.power_state == MCUPowerState.CONFIGURED_NFC: - self.nfc_state = NFC_state.NONE - self._queue_response(self._get_status_data()) - def handle_nfc_subcommand(self, com, data): """ This generates responses for NFC commands and thus implements the entire @@ -237,38 +194,31 @@ def handle_nfc_subcommand(self, com, data): elif com == 0x06: # read, we probably should not respond to this at all, # since each packet is queried individually by the switch, but parsing these # 04 packets is just annoying - logger.info("MCU-NFC: reading...") + logger.info("MCU-NFC: reading Tag...") if self.nfc_tag_data: self._flush_response_queue() # Data is sent in 2 packages plus a trailer # the first one contains a lot of fixed header and the UUID # the length and packetnumber is in there somewhere, see # https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp line 2523ff - out = [0] * 313 - out[0:15] = bytes.fromhex("3a0007010001310200000001020007") - out[15:18] = self.nfc_tag_data[0:3] - out[18:22] = self.nfc_tag_data[4:8] - out[22:67] = bytes.fromhex( - "000000007DFDF0793651ABD7466E39C191BABEB856CEEDF1CE44CC75EAFB27094D087AE803003B3C7778860000") - out[67:-1] = self.nfc_tag_data[0:245] - out[-1] = MCU_crc(out[0:-1]) - self._force_queue_response(out) + self._force_queue_response(pack_message( + "3a0007010001310200000001020007", + self.nfc_tag_data[0:3], + self.nfc_tag_data[4:8], + "000000007DFDF0793651ABD7466E39C191BABEB856CEEDF1CE44CC75EAFB27094D087AE803003B3C7778860000", + self.nfc_tag_data[0:245] + )) # the second one is mostely data, followed by presumably anything (zeroes work) - out = [0] * 313 - out[0:7] = bytes.fromhex("3a000702000927") - out[7:302] = self.nfc_tag_data[245:540] - out[-1] = MCU_crc(out[0:-1]) - self._force_queue_response(out) + self._force_queue_response(pack_message( + "3a000702000927", + self.nfc_tag_data[245:540] + )) # the trailer includes the UUID again - out = [0] * 313 - out[0:16] = bytes.fromhex("2a000500000931040000000101020007") - out[16:19] = self.nfc_tag_data[0:3] - out[19:23] = self.nfc_tag_data[4:8] - out[-1] = MCU_crc(out[0:-1]) - self._force_queue_response(out) - self.reading = 3 - for msg in self.response_queue: - print("MCU-NFC: reading, queued", msg) + self._force_queue_response(pack_message( + "2a000500000931040000000101020007", + self.nfc_tag_data[0:3], + self.nfc_tag_data[4:8] + )) #self.nfc_tag_data = None #if self.remove_nfc_after_read: # self._controller.set_nfc(None) @@ -279,6 +229,46 @@ def handle_nfc_subcommand(self, com, data): else: logger.error("unhandled NFC subcommand", com, data) + # I don't actually know if we are supposed to change the MCU-data based on + # regular subcommands, but the switch is spamming the status-requests anyway, + # so sending responses seems to not hurt + + # protocoll-callback + def entered_31_input_mode(self): + self._queue_response(pack_message("0100000008001b01")) + + # protocoll-callback + def set_power_state_cmd(self, power_state): + logger.info(f"MCU: Set power state cmd {power_state}") + if power_state in SET_POWER_VALUES: + self.power_state = MCUPowerState(power_state) + else: + logger.error(f"not implemented power state {power_state}") + self.power_state = MCUPowerState.READY + self._queue_response(self._get_status_data()) + + # protocoll-callback + def set_config_cmd(self, config): + if self.power_state == MCUPowerState.SUSPENDED: + if config[2] == 0: + # the switch does this during initial setup, presumably to disable + # any MCU weirdness + pass + else: + logger.warning("Set MCU Config not in READY mode") + elif self.power_state == MCUPowerState.READY: + if config[2] in SET_CONFIG_VALUES: + self.power_state = MCUPowerState(config[2]) + self.configuration = config + logger.info(f"MCU Set configuration {self.power_state} {self.configuration}") + else: + self.power_state = MCUPowerState.READY + logger.error(f"Not implemented configuration written {config[2]} {config}") + if self.power_state == MCUPowerState.CONFIGURED_NFC: + self.nfc_state = NFC_state.NONE + self._queue_response(self._get_status_data()) + + # protocoll-callback def received_11(self, subcommand, subcommanddata): """ This function handles all 0x11 output-reports. @@ -298,6 +288,7 @@ def received_11(self, subcommand, subcommanddata): else: logger.error("unknown 11 subcommand", subcommand, subcommanddata) + # protocoll hook def get_data(self): """ The function returning what is to write into mcu data of tha outgoing 0x31 packet @@ -305,9 +296,6 @@ def get_data(self): usually it is some queued response @return: the data """ - if self.reading > 0: - print("sending", self.response_queue[0]) - self.reading -= 1 if len(self.response_queue) > 0: return self.response_queue.pop(0) else: diff --git a/joycontrol/protocol.py b/joycontrol/protocol.py index ff8c7eb3..2e74b14a 100644 --- a/joycontrol/protocol.py +++ b/joycontrol/protocol.py @@ -39,9 +39,9 @@ def __init__(self, controller: Controller, spi_flash: FlashMemory = None, grip_m self._controller_state = ControllerState(self, controller, spi_flash=spi_flash) self._controller_state_sender = None self._writer = None - # daley between two input reports + # delay between two input reports self.send_delay = 1/60 if not grip_menu else 1/15 - + self._mcu = MarvelCinematicUniverse(self._controller_state) # None = Send empty input reports & answer to sub commands @@ -192,7 +192,6 @@ async def report_received(self, data: Union[bytes, Text], addr: Tuple[str, int]) # TODO Rumble pass elif output_report_id == OutputReportID.REQUEST_IR_NFC_MCU: - # TODO: support 0x11 outputs in OutputReport self._mcu.received_11(report.data[11], report.get_sub_command_data()) pass else: @@ -377,7 +376,6 @@ async def _command_set_nfc_ir_mcu_config(self, sub_command_data): await self.write(input_report) async def _command_set_nfc_ir_mcu_state(self, sub_command_data): - # TODO NFC input_report = InputReport() input_report.set_input_report_id(0x21) input_report.set_misc() From 8df9d0c028e65c56efdfa842edb2ca09c022ce50 Mon Sep 17 00:00:00 2001 From: Poohl Date: Wed, 9 Dec 2020 15:33:51 +0100 Subject: [PATCH 05/43] typos --- mcu.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mcu.md b/mcu.md index 3b384999..9deafdab 100644 --- a/mcu.md +++ b/mcu.md @@ -61,7 +61,7 @@ Here I describe what I found the nfc-reading process of amiibos looks like: This is spammed like hell and seems to be some other kind of nfc-status-request - `nfc_status`: `2a000500000931[nfc_state]` + `nfc_status`: `2a000500000931[nfc_state]00..00[checksum]` where nfc_state is - `00` for nothing/polling startup or something like this @@ -88,7 +88,7 @@ Here I describe what I found the nfc-reading process of amiibos looks like: read1: `3a0007010001310200000001020007[TAG_UID]000000007DFDF0793651ABD7466E39C191BABEB856CEEDF1CE44CC75EAFB27094D087AE803003B3C7778860000[nfc_data(0;245(][checksum]` - read2: `3a000702000927[nfc data (245;540(][checksum]` + read2: `3a000702000927[nfc_data(245;540(][checksum]` read3: `2a000500000931040000000101020007[TAG_UID]00...00[checksum]` @@ -101,3 +101,9 @@ Here I describe what I found the nfc-reading process of amiibos looks like: * cancel (`11 0200`) No idea + +# Resources + +* [jctool.cpp](https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp) c.a. line 2523 + +* [bluetooth_hid_subcommands.md](https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/bluetooth_hid_subcommands_notes.md) From 5d7bfe6124f921912aee78c552ced17256af4e79 Mon Sep 17 00:00:00 2001 From: Poohl Date: Sun, 31 Jan 2021 23:36:33 +0100 Subject: [PATCH 06/43] rename MCU and additional flooding mitigation --- joycontrol/mcu.py | 2 +- joycontrol/protocol.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/joycontrol/mcu.py b/joycontrol/mcu.py index 027e8d91..470fd5a9 100644 --- a/joycontrol/mcu.py +++ b/joycontrol/mcu.py @@ -102,7 +102,7 @@ def pack_message(*args, background=0, checksum=MCU_crc): return data -class MarvelCinematicUniverse: +class MicroControllerUnit: def __init__(self, controller: ControllerState): self.power_state = MCUPowerState.SUSPENDED diff --git a/joycontrol/protocol.py b/joycontrol/protocol.py index 2e74b14a..9ee2b05c 100644 --- a/joycontrol/protocol.py +++ b/joycontrol/protocol.py @@ -10,7 +10,7 @@ from joycontrol.memory import FlashMemory from joycontrol.report import OutputReport, SubCommand, InputReport, OutputReportID from joycontrol.transport import NotConnectedError -from joycontrol.mcu import MarvelCinematicUniverse +from joycontrol.mcu import MicroControllerUnit logger = logging.getLogger(__name__) @@ -42,7 +42,7 @@ def __init__(self, controller: Controller, spi_flash: FlashMemory = None, grip_m # delay between two input reports self.send_delay = 1/60 if not grip_menu else 1/15 - self._mcu = MarvelCinematicUniverse(self._controller_state) + self._mcu = MicroControllerUnit(self._controller_state) # None = Send empty input reports & answer to sub commands self._input_report_mode = None @@ -118,10 +118,6 @@ async def wait_for_output_report(self): def connection_made(self, transport: BaseTransport) -> None: logger.debug('Connection established.') self.transport = transport - self._writer = asyncio.ensure_future(self.writer()) - self._writer.add_done_callback( - utils.create_error_check_callback() - ) def connection_lost(self, exc: Optional[Exception] = None) -> None: if self.transport is not None: @@ -404,5 +400,8 @@ async def _command_set_player_lights(self, sub_command_data): input_report.reply_to_subcommand_id(SubCommand.SET_PLAYER_LIGHTS.value) await self.write(input_report) - + self._writer = asyncio.ensure_future(self.writer()) + self._writer.add_done_callback( + utils.create_error_check_callback() + ) self.sig_set_player_lights.set() From c150d2ac5a74ba83ca6286c2ca7793793035cd35 Mon Sep 17 00:00:00 2001 From: Poohl Date: Mon, 8 Feb 2021 22:24:08 +0100 Subject: [PATCH 07/43] initial writeup --- joycontrol/mcu.py | 97 +++++++++++++++++++++++++++++++------------ joycontrol/nfc_tag.py | 18 ++++++++ 2 files changed, 88 insertions(+), 27 deletions(-) create mode 100644 joycontrol/nfc_tag.py diff --git a/joycontrol/mcu.py b/joycontrol/mcu.py index 470fd5a9..1c92eea8 100644 --- a/joycontrol/mcu.py +++ b/joycontrol/mcu.py @@ -2,15 +2,19 @@ import logging import crc8 import traceback +import asyncio from joycontrol.controller_state import ControllerState +from joycontrol.nfc_tag import NFCTag logger = logging.getLogger(__name__) + def debug(args): print(args) return args + ############################################################### ## This simulates the MCU in the right joycon/Pro-Controller ## ############################################################### @@ -28,30 +32,31 @@ def debug(args): # But not all of them can appear in every one # see https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp l 2359 for set_config class MCUPowerState(enum.Enum): - SUSPENDED = 0x00 # set_power - READY = 0x01 # set_power, set_config, get_status + SUSPENDED = 0x00 # set_power + READY = 0x01 # set_power, set_config, get_status READY_UPDATE = 0x02 - CONFIGURED_NFC = 0x04 # set_config, get_status + CONFIGURED_NFC = 0x04 # set_config, get_status # CONFIGURED_IR = 0x05 # TODO: support this # CONFIGUERED_UPDATE = 0x06 + SET_POWER_VALUES = ( MCUPowerState.SUSPENDED.value, MCUPowerState.READY.value, -# MCUPowerState.READY_UPDATE.value, + # MCUPowerState.READY_UPDATE.value, ) SET_CONFIG_VALUES = ( MCUPowerState.READY.value, MCUPowerState.CONFIGURED_NFC.value, -# MCUPowerState.CONFIGURED_IR.value, + # MCUPowerState.CONFIGURED_IR.value, ) GET_STATUS_VALUES = ( MCUPowerState.READY.value, -# MCUPowerState.READY_UPDATE.value, + # MCUPowerState.READY_UPDATE.value, MCUPowerState.CONFIGURED_NFC.value, -# MCUPowerState.CONFIGURED_IR.value + # MCUPowerState.CONFIGURED_IR.value ) @@ -68,6 +73,7 @@ class NFC_state(enum.Enum): NONE = 0x00 POLL = 0x01 PENDING_READ = 0x02 + WRITING = 0x03 POLL_AGAIN = 0x09 @@ -95,7 +101,7 @@ def pack_message(*args, background=0, checksum=MCU_crc): arg_len = len(arg) if arg_len + cur > 313: logger.warn("MCU: too long message packed") - data[cur:cur+arg_len] = arg + data[cur:cur + arg_len] = arg cur += arg_len if checksum: data[-1] = checksum(data[0:-1]) @@ -112,7 +118,7 @@ def __init__(self, controller: ControllerState): self.configuration = None # a cache to store the tag's data between Poll and Read - self.nfc_tag_data = None + self.nfc_tag: NFCTag = None self.nfc_state = NFC_state.NONE # NOT IMPLEMENTED @@ -122,6 +128,11 @@ def __init__(self, controller: ControllerState): # controllerstate to look for nfc-data self._controller = controller + self.seq_no = 0 + self.ack_seq_no = 0 + # self.expect_data = False + self.received_data = [] + # We are getting 0x11 commands to do something, but cannot answer directly # responses have to be passed in regular input reports # If there was no command, this is the default report @@ -133,7 +144,7 @@ def _flush_response_queue(self): self.response_queue = [] def _queue_response(self, resp): - if resp == None: # the if "missing return statement" because python + if resp == None: # the if "missing return statement" because python traceback.print_stack() exit(1) if len(self.response_queue) <= self.max_response_queue_len: @@ -149,13 +160,13 @@ def set_remove_nfc_after_read(self, value): def set_nfc_tag_data(self, data): logger.info("MCU-NFC: set NFC tag data") if not data: - self.nfc_tag_data = None + self.nfc_tag = None if not data is bytes: data = bytes(data) if len(data) != 540: logger.warning("not implemented length") return - self.nfc_tag_data = data + self.nfc_tag = NFCTag(data) def _get_status_data(self, args=None): """ @@ -170,8 +181,9 @@ def _get_status_data(self, args=None): def _get_nfc_status_data(self, args): if self.nfc_state == NFC_state.POLL and self._controller.get_nfc(): self.set_nfc_tag_data(self._controller.get_nfc()) - if self.nfc_tag_data and self.nfc_state in (NFC_state.POLL, NFC_state.POLL_AGAIN): - out = pack_message("2a000500000931", self.nfc_state, "0000000101020007", self.nfc_tag_data[0:3], self.nfc_tag_data[4:8]) + if self.nfc_tag and self.nfc_state in (NFC_state.POLL, NFC_state.POLL_AGAIN, NFC_state.WRITING): + out = pack_message("2a0005", self.seq_no, self.ack_seq_no, "0931", self.nfc_state, "0000000101020007", + self.nfc_tag.getUID()) self.nfc_state = NFC_state.POLL_AGAIN else: out = pack_message("2a000500000931", self.nfc_state) @@ -179,6 +191,23 @@ def _get_nfc_status_data(self, args): self._queue_response(out) return out + async def process_nfc_write(self, command): + if not self.nfc_tag: + return + if command[1] != 0x07: # panic wrong UUID length + return + if command[2:9] != self.nfc_tag.getUID(): + return + self.nfc_tag.write(command[12] * 4, command[13:13 + 4]) + i = 22 + while i + 1 < len(command): + addr = command[i] * 4 + len = command[i + 1] + data = command[i + 2:i + 2 + len] + self.nfc_tag.write(addr, len, data) + i += 2 + len + return + def handle_nfc_subcommand(self, com, data): """ This generates responses for NFC commands and thus implements the entire @@ -195,37 +224,51 @@ def handle_nfc_subcommand(self, com, data): # since each packet is queried individually by the switch, but parsing these # 04 packets is just annoying logger.info("MCU-NFC: reading Tag...") - if self.nfc_tag_data: + if self.nfc_tag: self._flush_response_queue() # Data is sent in 2 packages plus a trailer # the first one contains a lot of fixed header and the UUID # the length and packetnumber is in there somewhere, see # https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp line 2523ff self._force_queue_response(pack_message( - "3a0007010001310200000001020007", - self.nfc_tag_data[0:3], - self.nfc_tag_data[4:8], - "000000007DFDF0793651ABD7466E39C191BABEB856CEEDF1CE44CC75EAFB27094D087AE803003B3C7778860000", - self.nfc_tag_data[0:245] + "3a0007010001310200000001020007", + self.nfc_tag.getUID(), + "000000007DFDF0793651ABD7466E39C191BABEB856CEEDF1CE44CC75EAFB27094D087AE803003B3C7778860000", + self.nfc_tag.data[0:245] )) # the second one is mostely data, followed by presumably anything (zeroes work) self._force_queue_response(pack_message( - "3a000702000927", - self.nfc_tag_data[245:540] + "3a000702000927", + self.nfc_tag.data[245:540] )) # the trailer includes the UUID again self._force_queue_response(pack_message( - "2a000500000931040000000101020007", - self.nfc_tag_data[0:3], - self.nfc_tag_data[4:8] + "2a000500000931040000000101020007", + self.nfc_tag.getUID() )) - #self.nfc_tag_data = None - #if self.remove_nfc_after_read: + # self.nfc_tag = None + # if self.remove_nfc_after_read: # self._controller.set_nfc(None) # elif com == 0x00: # cancel eveyhting -> exit mode? elif com == 0x02: # stop polling, respond? logger.info("MCU-NFC: stop polling...") self.nfc_state = NFC_state.NONE + elif com == 0x08: # write NTAG + if data[0] == 0 and data[2] == 0x08: # never seen, single packet as entire payload + asyncio.ensure_future(self.process_nfc_write(data[4: 4 + data[3]])) + return + if data[0] == self.ack_seq_no: # we already saw this one + pass + elif data[0] == 1 + self.ack_seq_no: # next packet in sequence + self.received_data += data[4: 4 + data[3]] + self.ack_seq_no += 1 + else: # panic we missed/skipped something + self.ack_seq_no = 0 + self.nfc_state = NFC_state.WRITING + self._force_queue_response(self._get_status_data(data)) + if data[2] == 0x08: # end of sequence + self.ack_seq_no = 0 + asyncio.ensure_future(self.process_nfc_write(self.received_data)) else: logger.error("unhandled NFC subcommand", com, data) diff --git a/joycontrol/nfc_tag.py b/joycontrol/nfc_tag.py new file mode 100644 index 00000000..628a9688 --- /dev/null +++ b/joycontrol/nfc_tag.py @@ -0,0 +1,18 @@ + +import enum + +class NFCTagType(enum.Enum): + AMIIBO = enum.auto + +class NFCTag: + def __init__(self, length=540, data=None, type=NFCTagType.AMIIBO): + self.data: bytes = data if data else bytearray(length) + self.type = type + if self.type == NFCTagType.AMIIBO and len(self.data) != 540: + self.data = bytearray(540) + + def getUID(self): + return self.data[0:3], self.data[4:8] + + def write(self, idx, data): + self.data[idx:idx+len(data)] = data From 4d45f894028d50961d0889fb8118b8884a1a52cb Mon Sep 17 00:00:00 2001 From: Poohl Date: Mon, 8 Feb 2021 22:27:14 +0100 Subject: [PATCH 08/43] no bins --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 69281e79..51d6496e 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,6 @@ dmypy.json # Pyre type checker .pyre/ + +# binarys or dumps +*.bin From 18eba52917180b335b412f06af0e33f0b8a4b8ce Mon Sep 17 00:00:00 2001 From: Poohl Date: Mon, 8 Feb 2021 23:08:33 +0100 Subject: [PATCH 09/43] adapted NFCTag everywhere --- joycontrol/mcu.py | 13 ++++--------- joycontrol/nfc_tag.py | 45 ++++++++++++++++++++++++++++++++++++++++++- run_controller_cli.py | 8 +++----- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/joycontrol/mcu.py b/joycontrol/mcu.py index 1c92eea8..c76242df 100644 --- a/joycontrol/mcu.py +++ b/joycontrol/mcu.py @@ -157,16 +157,11 @@ def set_remove_nfc_after_read(self, value): self.remove_nfc_after_read = value # called somwhere in get_nfc_status with _controller.get_nfc() - def set_nfc_tag_data(self, data): + def set_nfc_tag(self, tag: NFCTag): logger.info("MCU-NFC: set NFC tag data") - if not data: + if not tag: self.nfc_tag = None - if not data is bytes: - data = bytes(data) - if len(data) != 540: - logger.warning("not implemented length") - return - self.nfc_tag = NFCTag(data) + self.nfc_tag = tag def _get_status_data(self, args=None): """ @@ -180,7 +175,7 @@ def _get_status_data(self, args=None): def _get_nfc_status_data(self, args): if self.nfc_state == NFC_state.POLL and self._controller.get_nfc(): - self.set_nfc_tag_data(self._controller.get_nfc()) + self.set_nfc_tag(self._controller.get_nfc()) if self.nfc_tag and self.nfc_state in (NFC_state.POLL, NFC_state.POLL_AGAIN, NFC_state.WRITING): out = pack_message("2a0005", self.seq_no, self.ack_seq_no, "0931", self.nfc_state, "0000000101020007", self.nfc_tag.getUID()) diff --git a/joycontrol/nfc_tag.py b/joycontrol/nfc_tag.py index 628a9688..eb187fe7 100644 --- a/joycontrol/nfc_tag.py +++ b/joycontrol/nfc_tag.py @@ -1,18 +1,61 @@ import enum +import copy +import asyncio + +import logging + +logger = logging.getLogger(__name__) + +unnamed_saves = 0 +default_path = "/tmp/{}.bin" class NFCTagType(enum.Enum): AMIIBO = enum.auto class NFCTag: - def __init__(self, length=540, data=None, type=NFCTagType.AMIIBO): + def __init__(self, length=540, data=None, type=NFCTagType.AMIIBO, source=None, mutable=False, isClone=False): self.data: bytes = data if data else bytearray(length) self.type = type + self.mutable = mutable + self.source = source + self.isClone = isClone if self.type == NFCTagType.AMIIBO and len(self.data) != 540: + logger.warning("Illegal Amiibo tag size, using zeros") self.data = bytearray(540) + @classmethod + async def load_amiibo(cls, path): + # if someone want to make this async have fun + with open(path, "rb") as reader: + return NFCTag(data=bytearray(reader.read(540)), type=NFCTagType.AMIIBO, source=path) + + def save(self): + if not self.source: + global unnamed_saves + unnamed_saves += 1 + self.source = default_path.format(unnamed_saves) + logger.info("Saved amiibo witout source as " + self.source) + with open(self.source, "wb") as writer: + writer.write(self.data) + def getUID(self): return self.data[0:3], self.data[4:8] + def clone(self): + clone = copy.deepcopy(self) + if self.isClone: + clone.source[-1] += 1 + else: + clone.isClone = True + clone.source += ".1" + clone.mutable = True + return clone + def write(self, idx, data): + if not self.mutable: + logger.warning("Ignored amiibo write to non-mutable amiibo") self.data[idx:idx+len(data)] = data + + def __deepcopy__(self, memo): + return NFCTag(copy.deepcopy(self.data, self.source), memo) diff --git a/run_controller_cli.py b/run_controller_cli.py index 44bc4c9c..1ad05670 100755 --- a/run_controller_cli.py +++ b/run_controller_cli.py @@ -14,6 +14,7 @@ from joycontrol.memory import FlashMemory from joycontrol.protocol import controller_protocol_factory from joycontrol.server import create_hid_server +from joycontrol.nfc_tag import NFCTag logger = logging.getLogger(__name__) @@ -270,11 +271,8 @@ async def nfc(*args): controller_state.set_nfc(None) print('Removed nfc content.') else: - _loop = asyncio.get_event_loop() - with open(args[0], 'rb') as nfc_file: - content = await _loop.run_in_executor(None, nfc_file.read) - logger.info("CLI: Set nfc content") - controller_state.set_nfc(content) + controller_state.set_nfc(NFCTag.load_amiibo(args[0])) + print("added nfc content") cli.add_command(nfc.__name__, nfc) From 34889d08c8c6f89508199704dd973989af334b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Wei=C3=9F?= Date: Sun, 21 Feb 2021 00:17:17 +0100 Subject: [PATCH 10/43] Writing until all data is transfered until EOF Don't know how to respond to that yet... --- joycontrol/mcu.py | 97 +++++++++++++++++++++++++++---------------- joycontrol/nfc_tag.py | 66 ++++++++++++++--------------- 2 files changed, 94 insertions(+), 69 deletions(-) diff --git a/joycontrol/mcu.py b/joycontrol/mcu.py index c76242df..0bb64c4e 100644 --- a/joycontrol/mcu.py +++ b/joycontrol/mcu.py @@ -74,6 +74,7 @@ class NFC_state(enum.Enum): POLL = 0x01 PENDING_READ = 0x02 WRITING = 0x03 + AWAITING_WRITE = 0x04 POLL_AGAIN = 0x09 @@ -138,7 +139,10 @@ def __init__(self, controller: ControllerState): # If there was no command, this is the default report self.no_response = pack_message(0xff) self.response_queue = [] - self.max_response_queue_len = 3 + # to prevent overfill of the queue drops packets, but some are integral. + self.response_queue_importance = [] + # the length after which we start dropping packets + self.max_response_queue_len = 4 def _flush_response_queue(self): self.response_queue = [] @@ -147,11 +151,15 @@ def _queue_response(self, resp): if resp == None: # the if "missing return statement" because python traceback.print_stack() exit(1) - if len(self.response_queue) <= self.max_response_queue_len: + if len(self.response_queue) < self.max_response_queue_len: self.response_queue.append(resp) + else: + logger.warning("Full queue, dropped packet") def _force_queue_response(self, resp): self.response_queue.append(resp) + if len(self.response_queue) > self.max_response_queue_len: + logger.warning("Forced response queue") def set_remove_nfc_after_read(self, value): self.remove_nfc_after_read = value @@ -159,6 +167,10 @@ def set_remove_nfc_after_read(self, value): # called somwhere in get_nfc_status with _controller.get_nfc() def set_nfc_tag(self, tag: NFCTag): logger.info("MCU-NFC: set NFC tag data") + if not isinstance(tag, NFCTag): + # I hope someone burns in hell for this bullshit... + print("NOT A NFC TAG DUMBO") + exit(-1) if not tag: self.nfc_tag = None self.nfc_tag = tag @@ -174,16 +186,17 @@ def _get_status_data(self, args=None): return pack_message("0100000008001b", self.power_state) def _get_nfc_status_data(self, args): + next_state = self.nfc_state if self.nfc_state == NFC_state.POLL and self._controller.get_nfc(): self.set_nfc_tag(self._controller.get_nfc()) - if self.nfc_tag and self.nfc_state in (NFC_state.POLL, NFC_state.POLL_AGAIN, NFC_state.WRITING): + next_state = NFC_state.POLL_AGAIN + logger.info("polled and found tag") + if self.nfc_tag and self.nfc_state in (NFC_state.POLL, NFC_state.POLL_AGAIN, NFC_state.AWAITING_WRITE, NFC_state.WRITING): out = pack_message("2a0005", self.seq_no, self.ack_seq_no, "0931", self.nfc_state, "0000000101020007", self.nfc_tag.getUID()) - self.nfc_state = NFC_state.POLL_AGAIN else: out = pack_message("2a000500000931", self.nfc_state) - self._queue_response(out) - self._queue_response(out) + self.nfc_state = next_state return out async def process_nfc_write(self, command): @@ -192,7 +205,8 @@ async def process_nfc_write(self, command): if command[1] != 0x07: # panic wrong UUID length return if command[2:9] != self.nfc_tag.getUID(): - return + return # wrong UUID, won't write to wrong UUID + self.nfc_tag = self.nfc_tag.get_mutable() self.nfc_tag.write(command[12] * 4, command[13:13 + 4]) i = 22 while i + 1 < len(command): @@ -211,39 +225,48 @@ def handle_nfc_subcommand(self, com, data): @param data: the remaining data """ if com == 0x04: # status / response request - self._queue_response(self._get_nfc_status_data(data)) + self._force_queue_response(self._get_nfc_status_data(data)) elif com == 0x01: # start polling, should we queue a nfc_status? logger.info("MCU-NFC: start polling") self.nfc_state = NFC_state.POLL - elif com == 0x06: # read, we probably should not respond to this at all, - # since each packet is queried individually by the switch, but parsing these - # 04 packets is just annoying - logger.info("MCU-NFC: reading Tag...") - if self.nfc_tag: - self._flush_response_queue() - # Data is sent in 2 packages plus a trailer - # the first one contains a lot of fixed header and the UUID - # the length and packetnumber is in there somewhere, see - # https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp line 2523ff - self._force_queue_response(pack_message( - "3a0007010001310200000001020007", - self.nfc_tag.getUID(), - "000000007DFDF0793651ABD7466E39C191BABEB856CEEDF1CE44CC75EAFB27094D087AE803003B3C7778860000", - self.nfc_tag.data[0:245] - )) - # the second one is mostely data, followed by presumably anything (zeroes work) - self._force_queue_response(pack_message( - "3a000702000927", - self.nfc_tag.data[245:540] - )) - # the trailer includes the UUID again + elif com == 0x06: # read + # IT FUCKING KNOWS THIS IS A ARGUMENT, JUST CRASHES FOR GOOD MEASURE + logger.info("MCU-NFCRead %s", data[6:13]) + # this language gives no f*** about types, but a byte is no integer.... + if all(map(lambda x, y: x == y, data[6:13], bytearray(7))): # This is the UID, 0 seems to mean read anything + logger.info("MCU-NFC: reading Tag...") + if self.nfc_tag: + self._flush_response_queue() + # Data is sent in 2 packages plus a trailer + # the first one contains a lot of fixed header and the UUID + # the length and packetnumber is in there somewhere, see + # https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp line 2523ff + self._force_queue_response(pack_message( + "3a0007010001310200000001020007", + self.nfc_tag.getUID(), + "000000007DFDF0793651ABD7466E39C191BABEB856CEEDF1CE44CC75EAFB27094D087AE803003B3C7778860000", + self.nfc_tag.data[0:245] + )) + # the second one is mostely data, followed by presumably anything (zeroes work) + self._force_queue_response(pack_message( + "3a000702000927", + self.nfc_tag.data[245:540] + )) + # the trailer includes the UUID again + # this is not actually the trailer, joycons send both packets again but with the first bytes in the + # first one 0x2a then this again. But it seems to work without? + self._force_queue_response(pack_message( + "2a000500000931040000000101020007", + self.nfc_tag.getUID() + )) + else: # the UID is nonzero, so I assume a read follows + print("writing", data[6:13]) self._force_queue_response(pack_message( - "2a000500000931040000000101020007", - self.nfc_tag.getUID() + "3a0007010008400200000001020007", self.nfc_tag.getUID(), # standard header for write, some bytes differ + "00000000fdb0c0a434c9bf31690030aaef56444b0f602627366d5a281adc697fde0d6cbc010303000000000000f110ffee" + # any guesses are welcome. The end seems like something generic, a magic number? )) - # self.nfc_tag = None - # if self.remove_nfc_after_read: - # self._controller.set_nfc(None) + self.nfc_state = NFC_state.AWAITING_WRITE # elif com == 0x00: # cancel eveyhting -> exit mode? elif com == 0x02: # stop polling, respond? logger.info("MCU-NFC: stop polling...") @@ -260,7 +283,7 @@ def handle_nfc_subcommand(self, com, data): else: # panic we missed/skipped something self.ack_seq_no = 0 self.nfc_state = NFC_state.WRITING - self._force_queue_response(self._get_status_data(data)) + self._force_queue_response(self._get_nfc_status_data(data)) if data[2] == 0x08: # end of sequence self.ack_seq_no = 0 asyncio.ensure_future(self.process_nfc_write(self.received_data)) @@ -273,11 +296,13 @@ def handle_nfc_subcommand(self, com, data): # protocoll-callback def entered_31_input_mode(self): + self._flush_response_queue() self._queue_response(pack_message("0100000008001b01")) # protocoll-callback def set_power_state_cmd(self, power_state): logger.info(f"MCU: Set power state cmd {power_state}") + self._flush_response_queue() if power_state in SET_POWER_VALUES: self.power_state = MCUPowerState(power_state) else: diff --git a/joycontrol/nfc_tag.py b/joycontrol/nfc_tag.py index eb187fe7..fdceca90 100644 --- a/joycontrol/nfc_tag.py +++ b/joycontrol/nfc_tag.py @@ -1,61 +1,61 @@ - import enum -import copy -import asyncio +from os import path as ph import logging logger = logging.getLogger(__name__) unnamed_saves = 0 +hint_path = "/tmp/{}_{}.bin" default_path = "/tmp/{}.bin" + +def get_savepath(hint=None): + global unnamed_saves, default_path + unnamed_saves += 1 + return hint_path.format(hint, unnamed_saves) if hint else default_path.format(unnamed_saves) + + class NFCTagType(enum.Enum): AMIIBO = enum.auto + class NFCTag: - def __init__(self, length=540, data=None, type=NFCTagType.AMIIBO, source=None, mutable=False, isClone=False): - self.data: bytes = data if data else bytearray(length) - self.type = type - self.mutable = mutable - self.source = source - self.isClone = isClone - if self.type == NFCTagType.AMIIBO and len(self.data) != 540: - logger.warning("Illegal Amiibo tag size, using zeros") - self.data = bytearray(540) + def __init__(self, data, tag_type: NFCTagType = NFCTagType.AMIIBO, mutable=False, source=None): + self.data: bytearray = bytearray(data) + self.tag_type: NFCTagType = tag_type + self.mutable: bool = mutable + self.source: str = source + if self.tag_type == NFCTagType.AMIIBO and len(self.data) != 540: + logger.warning("Illegal Amiibo tag size") @classmethod - async def load_amiibo(cls, path): + def load_amiibo(cls, path): # if someone want to make this async have fun with open(path, "rb") as reader: - return NFCTag(data=bytearray(reader.read(540)), type=NFCTagType.AMIIBO, source=path) + return NFCTag(data=bytearray(reader.read(540)), tag_type=NFCTagType.AMIIBO, source=path) def save(self): - if not self.source: - global unnamed_saves - unnamed_saves += 1 - self.source = default_path.format(unnamed_saves) - logger.info("Saved amiibo witout source as " + self.source) - with open(self.source, "wb") as writer: - writer.write(self.data) + if self.mutable: + if not self.source: + self.source = get_savepath() + logger.info("Saved amiibo without source as " + self.source) + with open(self.source, "wb") as writer: + writer.write(self.data) def getUID(self): - return self.data[0:3], self.data[4:8] + return self.data[0:3] + self.data[4:8] - def clone(self): - clone = copy.deepcopy(self) - if self.isClone: - clone.source[-1] += 1 + def get_mutable(self): + if self.mutable: + return self else: - clone.isClone = True - clone.source += ".1" - clone.mutable = True - return clone + return NFCTag(self.data.copy(), self.tag_type, True, get_savepath(ph.splitext(ph.basename(self.source))[0])) def write(self, idx, data): if not self.mutable: logger.warning("Ignored amiibo write to non-mutable amiibo") - self.data[idx:idx+len(data)] = data + self.data[idx:idx + len(data)] = data - def __deepcopy__(self, memo): - return NFCTag(copy.deepcopy(self.data, self.source), memo) + def __del__(self): + self.save() From 234de289c62fa9ce6bb79ae48f86db02d4305df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Wei=C3=9F?= Date: Mon, 15 Mar 2021 19:06:09 +0100 Subject: [PATCH 11/43] fix bluetooth scripts --- scripts/reset_bluetooth.sh | 20 ++++++++++++++++++++ scripts/restart_bluetooth.sh | 5 +++++ 2 files changed, 25 insertions(+) create mode 100755 scripts/reset_bluetooth.sh create mode 100755 scripts/restart_bluetooth.sh diff --git a/scripts/reset_bluetooth.sh b/scripts/reset_bluetooth.sh new file mode 100755 index 00000000..85ea0851 --- /dev/null +++ b/scripts/reset_bluetooth.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# To modify bluetooth settings: /lib/systemd/system/bluetooth.service + + +# remove any device specific configurations, logs, etc... +rm -r /var/lib/bluetooth + +# just to be sure redownload bluez +#apt --reinstall install bluez + +# reset all configurations and rewrite device specific setup +dpkg-reconfigure bluez + +./restart_bluetooth.sh + +# do that again, because sometime it doesn't work +sleep 3 + +./restart_bluetooth.sh diff --git a/scripts/restart_bluetooth.sh b/scripts/restart_bluetooth.sh new file mode 100755 index 00000000..b5daaf54 --- /dev/null +++ b/scripts/restart_bluetooth.sh @@ -0,0 +1,5 @@ +# realod systemd config changed by reconfigure +systemctl daemon-reload + +# restart bluez with the new settings +systemctl restart bluetooth.service From f06f4476e52d9ee021b7d1978e47825693634c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Wei=C3=9F?= Date: Sun, 21 Mar 2021 00:36:35 +0100 Subject: [PATCH 12/43] dirty hacks doing gods work --- joycontrol/mcu.py | 35 ++++++++++++++++++------ joycontrol/nfc_tag.py | 63 +++++++++++++++++++++++++++++++------------ 2 files changed, 73 insertions(+), 25 deletions(-) diff --git a/joycontrol/mcu.py b/joycontrol/mcu.py index 0bb64c4e..0630dbec 100644 --- a/joycontrol/mcu.py +++ b/joycontrol/mcu.py @@ -18,6 +18,9 @@ def debug(args): ############################################################### ## This simulates the MCU in the right joycon/Pro-Controller ## ############################################################### +# WARNING: THIS IS ONE GIANT RACE CONDITION, DON'T DO THINGS FAST +# No I won't fix this, I have had enough of this asyncio s*** +# DIY or STFU # This is sufficient to read one amiibo when simulation Pro-Controller # multiple can mess up the internal state # anything but amiboo is not supported @@ -200,21 +203,32 @@ def _get_nfc_status_data(self, args): return out async def process_nfc_write(self, command): + logger.info("processing write") if not self.nfc_tag: + logger.warning("FAAK self.nfc_tag is none, couldn't write") return if command[1] != 0x07: # panic wrong UUID length return - if command[2:9] != self.nfc_tag.getUID(): - return # wrong UUID, won't write to wrong UUID - self.nfc_tag = self.nfc_tag.get_mutable() - self.nfc_tag.write(command[12] * 4, command[13:13 + 4]) + if command[1:8] != self.nfc_tag.getUID(): + logger.warning("FAAK self.nfc_tag.uid isnt equal" + bytes(self.nfc_tag.getUID()).hex() + bytes(command[1:8]).hex()) + # return # wrong UUID, won't write to wrong UUID + self.nfc_tag.set_mutable(True) + + # write write-lock + # self.nfc_tag.write(command[12] * 4, command[13:13 + 4]) + # HACK: remove the write-lock + self.nfc_tag.data[16] = 0xa5 + self.nfc_tag.data[17:19] = (int.from_bytes(self.nfc_tag.data[17:19], "big") + 1).to_bytes(2, 'big') + self.nfc_tag.data[19] = 0x00 i = 22 while i + 1 < len(command): addr = command[i] * 4 - len = command[i + 1] - data = command[i + 2:i + 2 + len] - self.nfc_tag.write(addr, len, data) - i += 2 + len + leng = command[i + 1] + data = command[i + 2:i + 2 + leng] + self.nfc_tag.write(addr, data) + i += 2 + leng + logger.info("saving...") + self.nfc_tag.save() return def handle_nfc_subcommand(self, com, data): @@ -274,17 +288,22 @@ def handle_nfc_subcommand(self, com, data): elif com == 0x08: # write NTAG if data[0] == 0 and data[2] == 0x08: # never seen, single packet as entire payload asyncio.ensure_future(self.process_nfc_write(data[4: 4 + data[3]])) + logger.info("NFC write valid but WTF") return if data[0] == self.ack_seq_no: # we already saw this one + logger.info("NFC write packet repeat " + str(data[0])) pass elif data[0] == 1 + self.ack_seq_no: # next packet in sequence self.received_data += data[4: 4 + data[3]] self.ack_seq_no += 1 + logger.info("NFC write packet " + str(self.ack_seq_no)) else: # panic we missed/skipped something + logger.warn("NFC write unexpected packet, expected " + str(self.ack_seq_no) + " got " + str(data[0]) + " " + str(data[2])) self.ack_seq_no = 0 self.nfc_state = NFC_state.WRITING self._force_queue_response(self._get_nfc_status_data(data)) if data[2] == 0x08: # end of sequence + logger.info("End of write, putting through") self.ack_seq_no = 0 asyncio.ensure_future(self.process_nfc_write(self.received_data)) else: diff --git a/joycontrol/nfc_tag.py b/joycontrol/nfc_tag.py index fdceca90..8d4605f6 100644 --- a/joycontrol/nfc_tag.py +++ b/joycontrol/nfc_tag.py @@ -3,17 +3,35 @@ import logging + logger = logging.getLogger(__name__) unnamed_saves = 0 -hint_path = "/tmp/{}_{}.bin" -default_path = "/tmp/{}.bin" - -def get_savepath(hint=None): - global unnamed_saves, default_path +def get_savepath(hint='/tmp/amiibo'): + global unnamed_saves unnamed_saves += 1 - return hint_path.format(hint, unnamed_saves) if hint else default_path.format(unnamed_saves) + if hint.endswith('.bin'): + hint = hint[:-4] + while True: + path = hint + '_' + str(unnamed_saves) + '.bin' + if not ph.exists(path): + break + unnamed_saves += 1 + return path + + +unnamed_backups = 0 + +def get_backuppath(hint='/tmp/amiibo.bin'): + global unnamed_backups + unnamed_backups += 1 + while True: + path = hint + '.bak' + str(unnamed_backups) + if not ph.exists(path): + break + unnamed_backups += 1 + return path class NFCTagType(enum.Enum): @@ -30,18 +48,28 @@ def __init__(self, data, tag_type: NFCTagType = NFCTagType.AMIIBO, mutable=False logger.warning("Illegal Amiibo tag size") @classmethod - def load_amiibo(cls, path): + def load_amiibo(cls, source): # if someone want to make this async have fun - with open(path, "rb") as reader: - return NFCTag(data=bytearray(reader.read(540)), tag_type=NFCTagType.AMIIBO, source=path) + with open(source, "rb") as reader: + return NFCTag(data=bytearray(reader.read(540)), tag_type=NFCTagType.AMIIBO, source=source) + + def create_backup(self): + path = get_backuppath(self.source) + logger.info("creating amiibo backup at " + path) + with open(path, "wb") as writer: + writer.write(self.data) + + def set_mutable(self, mutable=True): + if mutable > self.mutable: + self.create_backup() + self.mutable = mutable def save(self): - if self.mutable: - if not self.source: - self.source = get_savepath() - logger.info("Saved amiibo without source as " + self.source) - with open(self.source, "wb") as writer: - writer.write(self.data) + if not self.source: + self.source = get_savepath() + with open(self.source, "wb") as writer: + writer.write(self.data) + logger.info("Saved altered amiibo as " + self.source) def getUID(self): return self.data[0:3] + self.data[4:8] @@ -50,12 +78,13 @@ def get_mutable(self): if self.mutable: return self else: - return NFCTag(self.data.copy(), self.tag_type, True, get_savepath(ph.splitext(ph.basename(self.source))[0])) + return NFCTag(self.data.copy(), self.tag_type, True, get_savepath(self.source)) def write(self, idx, data): if not self.mutable: logger.warning("Ignored amiibo write to non-mutable amiibo") - self.data[idx:idx + len(data)] = data + else: + self.data[idx:idx + len(data)] = data def __del__(self): self.save() From 7d204529c112626420a465d79f17fc2141915b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Wei=C3=9F?= Date: Sun, 21 Mar 2021 01:05:31 +0100 Subject: [PATCH 13/43] cleand up hacky write support --- joycontrol/mcu.py | 44 ++++++++++++++++++++++++++----------------- joycontrol/nfc_tag.py | 11 +++++++++++ 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/joycontrol/mcu.py b/joycontrol/mcu.py index 0630dbec..5e07972a 100644 --- a/joycontrol/mcu.py +++ b/joycontrol/mcu.py @@ -19,17 +19,19 @@ def debug(args): ## This simulates the MCU in the right joycon/Pro-Controller ## ############################################################### # WARNING: THIS IS ONE GIANT RACE CONDITION, DON'T DO THINGS FAST -# No I won't fix this, I have had enough of this asyncio s*** +# No I won't fix this, I have had enough of this asyncio b***s*** # DIY or STFU -# This is sufficient to read one amiibo when simulation Pro-Controller +# This is sufficient to read or write one amiibo when simulating +# a Pro-Controller # multiple can mess up the internal state # anything but amiboo is not supported # TODO: # - figure out the NFC-content transfer, currently everything is hardcoded to work wich amiibo # see https://github.com/CTCaer/jc_toolkit/blob/5.2.0/jctool/jctool.cpp l2456ff for sugesstions # - IR-camera -# - writing to Amiibo +# - writing to Amiibo the proper way # - verify right joycon +# - Figure out the UID index in write commands, currently the check is just uncommented... # These Values are used in set_power, set_config and get_status packets # But not all of them can appear in every one @@ -104,7 +106,7 @@ def pack_message(*args, background=0, checksum=MCU_crc): arg = bytes(arg) arg_len = len(arg) if arg_len + cur > 313: - logger.warn("MCU: too long message packed") + logger.warning("MCU: too long message packed") data[cur:cur + arg_len] = arg cur += arg_len if checksum: @@ -132,6 +134,7 @@ def __init__(self, controller: ControllerState): # controllerstate to look for nfc-data self._controller = controller + # long messages are split, this keeps track of in and outgoing transfers self.seq_no = 0 self.ack_seq_no = 0 # self.expect_data = False @@ -142,8 +145,7 @@ def __init__(self, controller: ControllerState): # If there was no command, this is the default report self.no_response = pack_message(0xff) self.response_queue = [] - # to prevent overfill of the queue drops packets, but some are integral. - self.response_queue_importance = [] + # to prevent the queue from becoming too laggy, limit it's size # the length after which we start dropping packets self.max_response_queue_len = 4 @@ -151,14 +153,12 @@ def _flush_response_queue(self): self.response_queue = [] def _queue_response(self, resp): - if resp == None: # the if "missing return statement" because python - traceback.print_stack() - exit(1) if len(self.response_queue) < self.max_response_queue_len: self.response_queue.append(resp) else: logger.warning("Full queue, dropped packet") + # used to queue messages that cannot be dropped, as we don't have any kind of resend-mechanism def _force_queue_response(self, resp): self.response_queue.append(resp) if len(self.response_queue) > self.max_response_queue_len: @@ -172,7 +172,7 @@ def set_nfc_tag(self, tag: NFCTag): logger.info("MCU-NFC: set NFC tag data") if not isinstance(tag, NFCTag): # I hope someone burns in hell for this bullshit... - print("NOT A NFC TAG DUMBO") + logger.error("NOT A NFC TAG DUMBO") exit(-1) if not tag: self.nfc_tag = None @@ -180,7 +180,7 @@ def set_nfc_tag(self, tag: NFCTag): def _get_status_data(self, args=None): """ - create a status packet to be used when responding to 1101 commands + create a status packet to be used when responding to 1101 commands (outside NFC-mode) """ if self.power_state == MCUPowerState.SUSPENDED: logger.warning("MCU: status request when disabled") @@ -189,6 +189,13 @@ def _get_status_data(self, args=None): return pack_message("0100000008001b", self.power_state) def _get_nfc_status_data(self, args): + """ + Generate a NFC-Status report to be sent back to switch + This is 40% of all logic in this file, as all we do in NFC-mode is send these responses + but some (the read-tag ones) are hardcoded somewhere else + @param args: not used + @return: the status-message + """ next_state = self.nfc_state if self.nfc_state == NFC_state.POLL and self._controller.get_nfc(): self.set_nfc_tag(self._controller.get_nfc()) @@ -203,14 +210,19 @@ def _get_nfc_status_data(self, args): return out async def process_nfc_write(self, command): + """ + After all data regarding the write to the tag has been received, this funcion acutally applies the changesd + @param command: the entire write request + @return: None + """ logger.info("processing write") if not self.nfc_tag: - logger.warning("FAAK self.nfc_tag is none, couldn't write") + logger.error("self.nfc_tag is none, couldn't write") return if command[1] != 0x07: # panic wrong UUID length return - if command[1:8] != self.nfc_tag.getUID(): - logger.warning("FAAK self.nfc_tag.uid isnt equal" + bytes(self.nfc_tag.getUID()).hex() + bytes(command[1:8]).hex()) + if command[2:9] != self.nfc_tag.getUID(): + logger.error("self.nfc_tag.uid and target uid isnt equal, are " + bytes(self.nfc_tag.getUID()).hex() +' and '+ bytes(command[1:8]).hex()) # return # wrong UUID, won't write to wrong UUID self.nfc_tag.set_mutable(True) @@ -227,7 +239,6 @@ async def process_nfc_write(self, command): data = command[i + 2:i + 2 + leng] self.nfc_tag.write(addr, data) i += 2 + leng - logger.info("saving...") self.nfc_tag.save() return @@ -298,12 +309,11 @@ def handle_nfc_subcommand(self, com, data): self.ack_seq_no += 1 logger.info("NFC write packet " + str(self.ack_seq_no)) else: # panic we missed/skipped something - logger.warn("NFC write unexpected packet, expected " + str(self.ack_seq_no) + " got " + str(data[0]) + " " + str(data[2])) + logger.warning("NFC write unexpected packet, expected " + str(self.ack_seq_no) + " got " + str(data[0]) + " " + str(data[2])) self.ack_seq_no = 0 self.nfc_state = NFC_state.WRITING self._force_queue_response(self._get_nfc_status_data(data)) if data[2] == 0x08: # end of sequence - logger.info("End of write, putting through") self.ack_seq_no = 0 asyncio.ensure_future(self.process_nfc_write(self.received_data)) else: diff --git a/joycontrol/nfc_tag.py b/joycontrol/nfc_tag.py index 8d4605f6..7f6fe6c7 100644 --- a/joycontrol/nfc_tag.py +++ b/joycontrol/nfc_tag.py @@ -39,6 +39,9 @@ class NFCTagType(enum.Enum): class NFCTag: + """ + Class that represents a (Amiibo) NFC-Tag usually backed by a file. If needed files are generated. + """ def __init__(self, data, tag_type: NFCTagType = NFCTagType.AMIIBO, mutable=False, source=None): self.data: bytearray = bytearray(data) self.tag_type: NFCTagType = tag_type @@ -54,12 +57,20 @@ def load_amiibo(cls, source): return NFCTag(data=bytearray(reader.read(540)), tag_type=NFCTagType.AMIIBO, source=source) def create_backup(self): + """ + copy the file backing this Tag + """ path = get_backuppath(self.source) logger.info("creating amiibo backup at " + path) with open(path, "wb") as writer: writer.write(self.data) def set_mutable(self, mutable=True): + """ + By default tags are marked immutable to prevent corruption. To make them mutable create a backup first. + @param mutable: + @return: + """ if mutable > self.mutable: self.create_backup() self.mutable = mutable From ee63a00d0dffcf9edfa9afd4109a479abdce5396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Wei=C3=9F?= Date: Sun, 21 Mar 2021 01:20:33 +0100 Subject: [PATCH 14/43] another proxy script --- scripts/.gitignore | 1 + scripts/joycon_ip_proxy.py | 157 +++++++++++++++++++++++++++++++++++++ scripts/logparser.txt | 10 +++ 3 files changed, 168 insertions(+) create mode 100644 scripts/.gitignore create mode 100755 scripts/joycon_ip_proxy.py create mode 100644 scripts/logparser.txt diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 00000000..a8a0dcec --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1 @@ +*.bin diff --git a/scripts/joycon_ip_proxy.py b/scripts/joycon_ip_proxy.py new file mode 100755 index 00000000..fc9d5b28 --- /dev/null +++ b/scripts/joycon_ip_proxy.py @@ -0,0 +1,157 @@ +import argparse +import asyncio +import logging +import os +import socket +import re + +# from yamakai + +import hid + +from joycontrol import logging_default as log, utils +from joycontrol.device import HidDevice +from joycontrol.server import PROFILE_PATH +from joycontrol.utils import AsyncHID + +logger = logging.getLogger(__name__) + +async def myPipe(src, dest): + while data := await src(): + await dest(data) + +def read_from_sock(sock): + async def internal(): + return await asyncio.get_event_loop().sock_recv(sock, 500) + return internal + +def write_to_sock(sock): + async def internal(data): + return await asyncio.get_event_loop().sock_sendall(sock, data) + return internal + +async def connect_bt(bt_addr): + loop = asyncio.get_event_loop() + ctl = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) + itr = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) + + # See https://bugs.python.org/issue27929?@ok_message=issue%2027929%20versions%20edited%20ok&@template=item + # bug here: https://github.com/python/cpython/blob/5e29021a5eb10baa9147fd977cab82fa3f652bf0/Lib/asyncio/selector_events.py#L495 + # should be + # if hasattr(socket, 'AF_INET') or hasattr(socket, 'AF_INET6') sock.family in (socket.AF_INET, socket.AF_INET6): + # or something similar + # ctl.setblocking(0) + # itr.setblocking(0) + # await loop.sock_connect(ctl, (bt_addr, 17)) + # await loop.sock_connect(itr, (bt_addr, 19)) + ctl.connect((bt_addr, 17)) + itr.connect((bt_addr, 19)) + ctl.setblocking(0) + itr.setblocking(0) + return ctl, itr + +def bt_to_callbacks(ctl, itr): + def internal(): + itr.close() + ctl.close() + return read_from_sock(itr), write_to_sock(itr), internal + +async def connectEth(eth, server=False): + loop = asyncio.get_event_loop() + ip, port = eth.split(':') + port = int(port) + s = socket.socket() + s.setblocking(0) + if server: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('0.0.0.0', port)) + s.listen(1) + while 1: + c, caddr = await loop.sock_accept(s) + if caddr[0] == ip: + s.close() + c.setblocking(0) + s = c + break + else: + print("unexpecetd host", caddr) + c.close() + else: + await loop.sock_connect(s, (ip, port)) + # make the data f****** go + s.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + return s + +def eth_to_callbacks(sock): + return read_from_sock(sock), write_to_sock(sock), lambda: sock.close() + +async def _main(sw_addr, jc_addr): + # loop = asyncio.get_event_loop() + + jc_eth = not re.match("([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}", jc_addr) + sw_eth = not re.match("([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}", sw_addr) + + print("jc_eth", jc_eth, "sw_eth", sw_eth) + + send_to_jc = None + recv_from_jc = None + cleanup_jc = None + + send_to_switch = None + recv_from_switch = None + cleanup_switch = None + try: + # DONT do if-else here, because order should be easily adjustable + if not jc_eth: + print("waiting for joycon") + recv_from_jc, send_to_jc, cleanup_jc = bt_to_callbacks(*await connect_bt(jc_addr)) + + if jc_eth: + print("opening joycon eth") + recv_from_jc, send_to_jc, cleanup_jc = eth_to_callbacks(await connectEth(jc_addr, True)) + #print("waiting for initial packet") + #print(await recv_from_jc()) + #print("got initial") + + if sw_eth: + print("opening switch eth") + recv_from_switch, send_to_switch, cleanup_switch = eth_to_callbacks(await connectEth(sw_addr, False)) + #print("waiting for initial packet") + #print (await recv_from_switch()) + #print("got initial") + + if not sw_eth: + print("waiting for switch") + recv_from_switch, send_to_switch, cleanup_switch = bt_to_callbacks(*await connect_bt(sw_addr)) + + + print("stared forwarding") + await asyncio.gather( + asyncio.ensure_future(myPipe(recv_from_switch, send_to_jc)), + asyncio.ensure_future(myPipe(recv_from_jc, send_to_switch)), + ) + finally: + if cleanup_switch: + cleanup_switch() + if cleanup_jc: + cleanup_jc() + + + +if __name__ == '__main__': + # check if root + if not os.geteuid() == 0: + raise PermissionError('Script must be run as root!') + + parser = argparse.ArgumentParser(description="Acts as proxy for Switch-joycon communtcation between the two given addresses.\n Start the instance forwarding to the Switch directly first") + parser.add_argument('-S', '--switch', type=str, default=None, + help='talk to switch at the given address. Either a BT-MAC or a tcp ip:port combo.') + parser.add_argument('-J', '--joycon', type=str, default=None, + help='talk to switch at the given address. Either a BT-MAC or a tcp ip:port combo.') + + args = parser.parse_args() + if not args.switch or not args.joycon: + print("missing args") + exit(1) + + asyncio.run(_main(args.switch, args.joycon)) diff --git a/scripts/logparser.txt b/scripts/logparser.txt new file mode 100644 index 00000000..ec5d2a5e --- /dev/null +++ b/scripts/logparser.txt @@ -0,0 +1,10 @@ + +Settings to be used with this fork of wireshark: +https://gitlab.com/paulniklasweiss/wireshark/-/tree/regex_text_import +to import the message log format into wireshark + + +regex: $(?[<>]\s*(?