├── .bumpversion.cfg ├── .github └── FUNDING.yml ├── README.md ├── data └── darwin │ └── com.dbibackend.usb.agent.plist ├── dbibackend ├── __init__.py └── dbibackend.py └── setup.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.1.1.dev0 3 | commit = True 4 | message = BUMP_VERSION: {new_version} 5 | tag = True 6 | tag_name = {new_version} 7 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? 8 | serialize = 9 | {major}.{minor}.{patch}.{release}{dev} 10 | {major}.{minor}.{patch} 11 | 12 | [bumpversion:part:release] 13 | optional_value = gamma 14 | values = 15 | dev 16 | gamma 17 | 18 | [bumpversion:file:dbibackend/__init__.py] 19 | search = __version__ = '{current_version}' 20 | replace = __version__ = '{new_version}' 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.tinkoff.ru/rm/kalashnikov.roman1/ZLI4G51069 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DBI backend 2 | =========== 3 | 4 | PC-side server for games installation into Nintendo Switch 5 | 6 | Requirements 7 | ------------ 8 | Host: 9 | 10 | - libusb 11 | - pyusb 12 | - python3.7+ 13 | 14 | Nintendo Switch: 15 | - DBI v202+ 16 | 17 | Usage 18 | ----- 19 | * Run server `python3 dbibackend.py tites_dir_path` 20 | * Run DBI and then Installation titles from USB 21 | * Install files 22 | -------------------------------------------------------------------------------- /data/darwin/com.dbibackend.usb.agent.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.dbibackend.usb.agent 7 | ProgramArguments 8 | 9 | /usr/local/bin/dbibackend 10 | /pat_to_titles 11 | 12 | LaunchEvents 13 | 14 | com.apple.iokit.matching 15 | 16 | com.apple.device-attach 17 | 18 | idProduct 19 | 12288 20 | idVendor 21 | 1406 22 | IOProviderClass 23 | IOUSBDevice 24 | IOMatchLaunchStream 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /dbibackend/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.1.1.dev0' 2 | -------------------------------------------------------------------------------- /dbibackend/dbibackend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import usb.core 3 | import usb.util 4 | import struct 5 | import sys 6 | import time 7 | import argparse 8 | import logging 9 | import os 10 | from enum import IntEnum 11 | from collections import OrderedDict 12 | from pathlib import Path 13 | 14 | 15 | log = logging.getLogger(__name__) 16 | log.addHandler(logging.StreamHandler(sys.stdout)) 17 | log.setLevel(logging.INFO) 18 | 19 | BUFFER_SEGMENT_DATA_SIZE = 0x100000 20 | 21 | 22 | class CommandID(IntEnum): 23 | EXIT = 0 24 | LIST_DEPRECATED = 1 25 | FILE_RANGE = 2 26 | LIST = 3 27 | 28 | 29 | class CommandType(IntEnum): 30 | REQUEST = 0 31 | RESPONSE = 1 32 | ACK = 2 33 | 34 | 35 | class UsbContext: 36 | def __init__(self, vid: hex, pid: hex): 37 | dev = usb.core.find(idVendor=vid, idProduct=pid) 38 | if dev is None: 39 | raise ConnectionError(f'Device {vid}:{pid} not found') 40 | 41 | dev.reset() 42 | dev.set_configuration() 43 | cfg = dev.get_active_configuration() 44 | 45 | self._out = usb.util.find_descriptor( 46 | cfg[(0, 0)], 47 | custom_match=lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT 48 | ) 49 | self._in = usb.util.find_descriptor( 50 | cfg[(0, 0)], 51 | custom_match=lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN 52 | ) 53 | 54 | if self._out is None: 55 | raise LookupError(f'Device {vid}:{pid} output endpoint not found') 56 | if self._in is None: 57 | raise LookupError(f'Device {vid}:{pid} input endpoint not found') 58 | 59 | def read(self, data_size, timeout=0): 60 | return self._in.read(data_size, timeout=timeout) 61 | 62 | def write(self, data, timeout=0): 63 | self._out.write(data, timeout=timeout) 64 | 65 | 66 | def process_file_range_command(data_size, context, cache=None): 67 | log.info('File range') 68 | context.write(struct.pack('<4sIII', b'DBI0', CommandType.ACK, CommandID.FILE_RANGE, data_size)) 69 | file_range_header = context.read(data_size) 70 | range_size = struct.unpack(' 0: 75 | if nsp_name in cache: 76 | nsp_name = cache[nsp_name] 77 | 78 | log.info(f'Range Size: {range_size}, Range Offset: {range_offset}, Name len: {nsp_name_len}, Name: {nsp_name}') 79 | 80 | response_bytes = struct.pack('<4sIII', b'DBI0', CommandType.RESPONSE, CommandID.FILE_RANGE, range_size) 81 | context.write(response_bytes) 82 | 83 | ack = bytes(context.read(16, timeout=0)) 84 | cmd_type = struct.unpack('= end_off: 99 | read_size = end_off - curr_off 100 | 101 | buf = f.read(read_size) 102 | context.write(data=buf, timeout=0) 103 | curr_off += read_size 104 | 105 | 106 | def process_exit_command(context): 107 | log.info('Exit') 108 | context.write(struct.pack('<4sIII', b'DBI0', CommandType.RESPONSE, CommandID.EXIT, 0)) 109 | sys.exit(0) 110 | 111 | 112 | def process_list_command(context, work_dir_path): 113 | log.info('Get list') 114 | 115 | cached_titles = OrderedDict() 116 | for dirName, subdirList, fileList in os.walk(work_dir_path): 117 | log.debug(f'Found directory: {dirName}') 118 | for filename in fileList: 119 | if filename.lower().endswith('.nsp') or filename.lower().endswith('nsz') or filename.lower().endswith('.xci'): 120 | log.debug(f'\t{filename}') 121 | cached_titles[f'{filename}'] = str(Path(dirName).joinpath(filename)) 122 | 123 | nsp_path_list = '' 124 | for title in cached_titles.keys(): 125 | nsp_path_list += f'{title}\n' 126 | nsp_path_list_bytes = nsp_path_list.encode('utf-8') 127 | nsp_path_list_len = len(nsp_path_list_bytes) 128 | 129 | context.write(struct.pack('<4sIII', b'DBI0', CommandType.RESPONSE, CommandID.LIST, nsp_path_list_len)) 130 | 131 | ack = bytes(context.read(16, timeout=0)) 132 | cmd_type = struct.unpack('