From 8f39acbaa23b9c3cc723a033a4792fd576ba14b1 Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:11:07 +0100 Subject: [PATCH] replace usb protocol with tinfoils protocol, in order to support applications supporting said protocol. - replace the python script with the one included with tinfoil, minor changes such as changing the supported extension, removing unused imports. - tested with the included script, fluffy and ns-usbloader on linux. a user was unable to get it working on mac however... - added build instructions to the readme, i think they're correct. - added install instructions to the readme. --- README.md | 34 +++++ sphaira/include/ui/menus/usb_menu.hpp | 7 +- sphaira/include/yati/source/usb.hpp | 13 +- sphaira/source/ui/menus/usb_menu.cpp | 105 ++++++++-------- sphaira/source/yati/source/usb.cpp | 170 ++++++++++++++----------- tools/usb_install_pc.py | 171 ++++++++++++++++++++++++++ tools/usb_total.py | 141 --------------------- 7 files changed, 366 insertions(+), 275 deletions(-) create mode 100644 tools/usb_install_pc.py delete mode 100644 tools/usb_total.py diff --git a/README.md b/README.md index e59fd04..18a62ca 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,40 @@ The `path` field is optional. If left out, it will use the name of the ini to fi See `assets/romfs/assoc/` for more examples of file assoc entries. +## Installing (applications) + +Sphaira can install applications (nsp, xci, nsz, xcz) from various sources (sd card, gamecard, ftp, usb). + +For informantion about the install options, [see the wiki](https://github.com/ITotalJustice/sphaira/wiki/Install). + +### Usb (install) + +The USB protocol is the same as tinfoil, so tools such as [ns-usbloader](https://github.com/developersu/ns-usbloader) and [fluffy](https://github.com/fourminute/Fluffy) should work with sphaira. You may also use the provided python script found [here](tools/usb_install_pc.py). + +### Ftp (install) + +Once you have connected your ftp client to your switch, you can upload files to install into the `install` folder. + +## Building from source + +You will first need to install [devkitPro](https://devkitpro.org/wiki/Getting_Started). + +Next you will need to install the dependencies: +```sh +sudo pacman -S switch-dev deko3d switch-cmake switch-curl switch-glm switch-zlib +``` + +Once devkitPro and all dependencies are installed, you can now build sphaira. + +```sh +git clone https://github.com/ITotalJustice/sphaira.git +cd sphaira +cmake --preset MinSizeRel +cmake --build --preset MinSizeRel +``` + +The output will be found in `build/MinSizeRel/sphaira.nro` + ## Credits - borealis diff --git a/sphaira/include/ui/menus/usb_menu.hpp b/sphaira/include/ui/menus/usb_menu.hpp index 14f2c42..b9b2c5f 100644 --- a/sphaira/include/ui/menus/usb_menu.hpp +++ b/sphaira/include/ui/menus/usb_menu.hpp @@ -8,8 +8,10 @@ namespace sphaira::ui::menu::usb { enum class State { // not connected. None, + // just connected, waiting for file list. + Connected_WaitForFileList, // just connected, starts the transfer. - Connected, + Connected_StartingTransfer, // set whilst transfer is in progress. Progress, // set when the transfer is finished. @@ -35,9 +37,8 @@ struct Menu final : MenuBase { Mutex m_mutex{}; // the below are shared across threads, lock with the above mutex! State m_state{State::None}; + std::vector m_names{}; bool m_usb_has_connection{}; - u32 m_usb_speed{}; - u32 m_usb_count{}; }; } // namespace sphaira::ui::menu::usb diff --git a/sphaira/include/yati/source/usb.hpp b/sphaira/include/yati/source/usb.hpp index 9ba64cc..200b0d1 100644 --- a/sphaira/include/yati/source/usb.hpp +++ b/sphaira/include/yati/source/usb.hpp @@ -2,7 +2,9 @@ #include "base.hpp" #include "fs.hpp" + #include +#include #include #include @@ -26,8 +28,9 @@ struct Usb final : Base { Result Finished(); Result Init(); - Result WaitForConnection(u64 timeout, u32& speed, u32& count); - Result GetFileInfo(std::string& name_out, u64& size_out); + Result IsUsbConnected(u64 timeout) const; + Result WaitForConnection(u64 timeout, std::vector& out_names); + void SetFileNameForTranfser(const std::string& name); public: // custom allocator for std::vector that respects alignment. @@ -62,10 +65,9 @@ private: UsbSessionEndpoint_Out = 1, }; - Result SendCommand(s64 off, s64 size); - Result InternalRead(void* buf, s64 off, s64 size); + Result SendCmdHeader(u32 cmdId, size_t dataSize); + Result SendFileRangeCmd(u64 offset, u64 size); - bool GetConfigured() const; Event *GetCompletionEvent(UsbSessionEndpoint ep) const; Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const; Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const; @@ -80,6 +82,7 @@ private: // aligned buffer that transfer data is copied to and from. // a vector is used to avoid multiple alloc within the transfer loop. PageAlignedVector m_aligned{}; + std::string m_transfer_file_name{}; }; } // namespace sphaira::yati::source diff --git a/sphaira/source/ui/menus/usb_menu.cpp b/sphaira/source/ui/menus/usb_menu.cpp index bc0eae5..f1732d7 100644 --- a/sphaira/source/ui/menus/usb_menu.cpp +++ b/sphaira/source/ui/menus/usb_menu.cpp @@ -10,8 +10,8 @@ namespace sphaira::ui::menu::usb { namespace { -constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 3; -constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5; +constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 1; // 1 second +constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5; // 5 seconds void thread_func(void* user) { auto app = static_cast(user); @@ -21,17 +21,26 @@ void thread_func(void* user) { break; } - const auto rc = app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, app->m_usb_speed, app->m_usb_count); + const auto rc = app->m_usb_source->IsUsbConnected(CONNECTION_TIMEOUT); + + // set connected status mutexLock(&app->m_mutex); - ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex)); + if (R_SUCCEEDED(rc)) { + app->m_state = State::Connected_WaitForFileList; + } else { + app->m_state = State::None; + } + mutexUnlock(&app->m_mutex); if (R_SUCCEEDED(rc)) { - app->m_state = State::Connected; - break; - } else if (R_FAILED(rc) && R_VALUE(rc) != 0xEA01) { - log_write("got: 0x%X value: 0x%X\n", rc, R_VALUE(rc)); - app->m_state = State::Failed; - break; + std::vector names; + if (R_SUCCEEDED(app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, names))) { + mutexLock(&app->m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex)); + app->m_state = State::Connected_StartingTransfer; + app->m_names = names; + break; + } } } } @@ -95,55 +104,36 @@ void Menu::Update(Controller* controller, TouchInfo* touch) { mutexLock(&m_mutex); ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); - switch (m_state) { - case State::None: - break; + if (m_state == State::Connected_StartingTransfer) { + log_write("set to progress\n"); + m_state = State::Progress; + log_write("got connection\n"); + App::Push(std::make_shared(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool { + ON_SCOPE_EXIT(m_usb_source->Finished()); - case State::Connected: - log_write("set to progress\n"); - m_state = State::Progress; - log_write("got connection\n"); - App::Push(std::make_shared(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool { - log_write("inside progress box\n"); - for (u32 i = 0; i < m_usb_count; i++) { - std::string file_name; - u64 file_size; - if (R_FAILED(m_usb_source->GetFileInfo(file_name, file_size))) { - return false; - } + log_write("inside progress box\n"); + for (const auto& file_name : m_names) { + m_usb_source->SetFileNameForTranfser(file_name); - log_write("got file name: %s size: %lX\n", file_name.c_str(), file_size); - - const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name); - if (R_FAILED(rc)) { - return false; - } - - App::Notify("Installed via usb"_i18n); - m_usb_source->Finished(); + const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name); + if (R_FAILED(rc)) { + return false; } - return true; - }, [this](bool result){ - if (result) { - App::Notify("Usb install success!"_i18n); - m_state = State::Done; - SetPop(); - } else { - App::Notify("Usb install failed!"_i18n); - m_state = State::Failed; - } - })); - break; + App::Notify("Installed via usb"_i18n); + } - case State::Progress: - break; - - case State::Done: - break; - - case State::Failed: - break; + return true; + }, [this](bool result){ + if (result) { + App::Notify("Usb install success!"_i18n); + m_state = State::Done; + SetPop(); + } else { + App::Notify("Usb install failed!"_i18n); + m_state = State::Failed; + } + })); } } @@ -158,7 +148,12 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str()); break; - case State::Connected: + case State::Connected_WaitForFileList: + gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Connected, waiting for file list..."_i18n.c_str()); + break; + + case State::Connected_StartingTransfer: + gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Connected, starting transfer..."_i18n.c_str()); break; case State::Progress: diff --git a/sphaira/source/yati/source/usb.cpp b/sphaira/source/yati/source/usb.cpp index 908a7c9..ccfcd0f 100644 --- a/sphaira/source/yati/source/usb.cpp +++ b/sphaira/source/yati/source/usb.cpp @@ -14,23 +14,51 @@ * along with this program. If not, see . */ -// Most of the usb transfer code was taken from Haze. +// The USB transfer code was taken from Haze (part of Atmosphere). +// The USB protocol was taken from Tinfoil, by Adubbz. + #include "yati/source/usb.hpp" #include "log.hpp" +#include namespace sphaira::yati::source { namespace { -constexpr u32 MAGIC = 0x53504841; -constexpr u32 VERSION = 2; - -struct RecvHeader { - u32 magic; - u32 version; - u32 bcdUSB; - u32 count; +enum USBCmdType : u8 { + REQUEST = 0, + RESPONSE = 1 }; +enum USBCmdId : u32 { + EXIT = 0, + FILE_RANGE = 1 +}; + +struct NX_PACKED USBCmdHeader { + u32 magic; + USBCmdType type; + u8 padding[0x3] = {0}; + u32 cmdId; + u64 dataSize; + u8 reserved[0xC] = {0}; +}; + +struct FileRangeCmdHeader { + u64 size; + u64 offset; + u64 nspNameLen; + u64 padding; +}; + +struct TUSHeader { + u32 magic; // TUL0 (Tinfoil Usb List 0) + u32 nspListSize; + u64 padding; +}; + +static_assert(sizeof(TUSHeader) == 0x10, "TUSHeader must be 0x10!"); +static_assert(sizeof(USBCmdHeader) == 0x20, "USBCmdHeader must be 0x20!"); + } // namespace Usb::Usb(u64 transfer_timeout) { @@ -50,6 +78,11 @@ Result Usb::Init() { log_write("doing USB init\n"); R_TRY(m_open_result); + SetSysSerialNumber serial_number; + R_TRY(setsysInitialize()); + ON_SCOPE_EXIT(setsysExit()); + R_TRY(setsysGetSerialNumber(&serial_number)); + u8 iManufacturer, iProduct, iSerialNumber; static const u16 supported_langs[1] = {0x0409}; // Send language descriptor @@ -59,7 +92,7 @@ Result Usb::Init() { // Send product R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch")); // Send serial number - R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, "SerialNumber")); + R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, serial_number.number)); // Send device descriptors struct usb_device_descriptor device_descriptor = { @@ -128,13 +161,11 @@ Result Usb::Init() { .bInterfaceProtocol = USB_CLASS_VENDOR_SPEC, }; - struct usb_endpoint_descriptor endpoint_descriptor_in = { .bLength = USB_DT_ENDPOINT_SIZE, .bDescriptorType = USB_DT_ENDPOINT, .bEndpointAddress = USB_ENDPOINT_IN, .bmAttributes = USB_TRANSFER_TYPE_BULK, - .wMaxPacketSize = 0x40, }; struct usb_endpoint_descriptor endpoint_descriptor_out = { @@ -142,7 +173,6 @@ Result Usb::Init() { .bDescriptorType = USB_DT_ENDPOINT, .bEndpointAddress = USB_ENDPOINT_OUT, .bmAttributes = USB_TRANSFER_TYPE_BULK, - .wMaxPacketSize = 0x40, }; const struct usb_ss_endpoint_companion_descriptor endpoint_companion = { @@ -160,6 +190,8 @@ Result Usb::Init() { endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1; // Full Speed Config + endpoint_descriptor_in.wMaxPacketSize = 0x40; + endpoint_descriptor_out.wMaxPacketSize = 0x40; R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE)); R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE)); R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE)); @@ -191,49 +223,38 @@ Result Usb::Init() { R_SUCCEED(); } -Result Usb::WaitForConnection(u64 timeout, u32& speed, u32& count) { - struct { - u32 magic; - u32 version; - } send_header{MAGIC, VERSION}; +Result Usb::IsUsbConnected(u64 timeout) const { + return usbDsWaitReady(timeout); +} - // send header. - R_TRY(TransferAll(false, &send_header, sizeof(send_header), timeout)); +Result Usb::WaitForConnection(u64 timeout, std::vector& out_names) { + TUSHeader header; + R_TRY(TransferAll(true, &header, sizeof(header), timeout)); + R_UNLESS(header.magic == 0x304C5554, Result_BadMagic); + R_UNLESS(header.nspListSize > 0, Result_BadCount); + log_write("USB got header\n"); - // receive header. - struct RecvHeader recv_header; - R_TRY(TransferAll(true, &recv_header, sizeof(recv_header), timeout)); + std::vector names(header.nspListSize); + R_TRY(TransferAll(true, names.data(), names.size(), timeout)); - // validate received header. - R_UNLESS(recv_header.magic == MAGIC, Result_BadMagic); - R_UNLESS(recv_header.version == VERSION, Result_BadVersion); - R_UNLESS(recv_header.count > 0, Result_BadCount); + out_names.clear(); + for (const auto& name : std::views::split(names, '\n')) { + if (!name.empty()) { + out_names.emplace_back(name.data(), name.size()); + } + } - count = recv_header.count; - speed = recv_header.bcdUSB; + for (auto& name : out_names) { + log_write("got name: %s\n", name.c_str()); + } + + R_UNLESS(!out_names.empty(), Result_BadCount); + log_write("USB SUCCESS\n"); R_SUCCEED(); } -Result Usb::GetFileInfo(std::string& name_out, u64& size_out) { - struct { - u64 size; - u64 name_length; - } file_info_meta; - - // receive meta. - R_TRY(TransferAll(true, &file_info_meta, sizeof(file_info_meta), m_transfer_timeout)); - - name_out.resize(file_info_meta.name_length); - R_TRY(TransferAll(true, name_out.data(), file_info_meta.name_length, m_transfer_timeout)); - - size_out = file_info_meta.size; - R_SUCCEED(); -} - -bool Usb::GetConfigured() const { - UsbState usb_state; - usbDsGetState(std::addressof(usb_state)); - return usb_state == UsbState_Configured; +void Usb::SetFileNameForTranfser(const std::string& name) { + m_transfer_file_name = name; } Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const { @@ -270,12 +291,7 @@ Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_tr u32 urb_id; /* If we're not configured yet, wait to become configured first. */ - // R_TRY(usbDsWaitReady(timeout)); - if (!GetConfigured()) { - R_TRY(eventWait(usbDsGetStateChangeEvent(), timeout)); - R_TRY(eventClear(usbDsGetStateChangeEvent())); - R_THROW(0xEA01); - } + R_TRY(IsUsbConnected(timeout)); /* Select the appropriate endpoint and begin a transfer. */ const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In; @@ -319,30 +335,42 @@ Result Usb::TransferAll(bool read, void *data, u32 size, u64 timeout) { R_SUCCEED(); } -Result Usb::SendCommand(s64 off, s64 size) { - struct { - u32 hash; - u32 magic; - s64 off; - s64 size; - } meta{0, 0, off, size}; +Result Usb::SendCmdHeader(u32 cmdId, size_t dataSize) { + USBCmdHeader header{ + .magic = 0x30435554, // TUC0 (Tinfoil USB Command 0) + .type = USBCmdType::REQUEST, + .cmdId = cmdId, + .dataSize = dataSize, + }; - return TransferAll(false, &meta, sizeof(meta), m_transfer_timeout); + return TransferAll(false, &header, sizeof(header), m_transfer_timeout); +} + +Result Usb::SendFileRangeCmd(u64 off, u64 size) { + FileRangeCmdHeader fRangeHeader; + fRangeHeader.size = size; + fRangeHeader.offset = off; + fRangeHeader.nspNameLen = m_transfer_file_name.size(); + fRangeHeader.padding = 0; + + R_TRY(SendCmdHeader(USBCmdId::FILE_RANGE, sizeof(fRangeHeader) + fRangeHeader.nspNameLen)); + R_TRY(TransferAll(false, &fRangeHeader, sizeof(fRangeHeader), m_transfer_timeout)); + R_TRY(TransferAll(false, m_transfer_file_name.data(), m_transfer_file_name.size(), m_transfer_timeout)); + + USBCmdHeader responseHeader; + R_TRY(TransferAll(true, &responseHeader, sizeof(responseHeader), m_transfer_timeout)); + + R_SUCCEED(); } Result Usb::Finished() { - return SendCommand(0, 0); -} - -Result Usb::InternalRead(void* buf, s64 off, s64 size) { - R_TRY(SendCommand(off, size)); - R_TRY(TransferAll(true, buf, size, m_transfer_timeout)); - R_SUCCEED(); + return SendCmdHeader(USBCmdId::EXIT, 0); } Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) { R_TRY(GetOpenResult()); - R_TRY(InternalRead(buf, off, size)); + R_TRY(SendFileRangeCmd(off, size)); + R_TRY(TransferAll(true, buf, size, m_transfer_timeout)); *bytes_read = size; R_SUCCEED(); } diff --git a/tools/usb_install_pc.py b/tools/usb_install_pc.py new file mode 100644 index 0000000..86ae800 --- /dev/null +++ b/tools/usb_install_pc.py @@ -0,0 +1,171 @@ +# This script depends on PyUSB. You can get it with pip install pyusb. +# You will also need libusb installed + +# My sincere apologies for this process being overly complicated. Apparently Python and Windows +# aren't very friendly :( +# Windows Instructions: +# 1. Download Zadig from https://zadig.akeo.ie/. +# 2. With your switch plugged in and on the Tinfoil USB install menu, +# choose "List All Devices" under the options menu in Zadig, and select libnx USB comms. +# 3. Choose libusbK from the driver list and click the "Replace Driver" button. +# 4. Run this script + +# macOS Instructions: +# 1. Install Homebrew https://brew.sh +# 2. Install Python 3 +# sudo mkdir /usr/local/Frameworks +# sudo chown $(whoami) /usr/local/Frameworks +# brew install python +# 3. Install PyUSB +# pip3 install pyusb +# 4. Install libusb +# brew install libusb +# 5. Plug in your Switch and go to Tinfoil > Title Management > USB Install NSP +# 6. Run this script +# python3 usb_install_pc.py + +import usb.core +import usb.util +import struct +import sys +from pathlib import Path +import time + +CMD_ID_EXIT = 0 +CMD_ID_FILE_RANGE = 1 + +CMD_TYPE_RESPONSE = 1 + +# list of supported extensions. +EXTS = (".nsp", ".xci", ".nsz", ".xcz") + +def send_response_header(out_ep, cmd_id, data_size): + out_ep.write(b'TUC0') # Tinfoil USB Command 0 + out_ep.write(struct.pack('= end_off: + read_size = end_off - curr_off + + buf = f.read(read_size) + out_ep.write(data=buf, timeout=0) + curr_off += read_size + +def poll_commands(nsp_dir, in_ep, out_ep): + while True: + cmd_header = bytes(in_ep.read(0x20, timeout=0)) + magic = cmd_header[:4] + print('Magic: {}'.format(magic), flush=True) + + if magic != b'TUC0': # Tinfoil USB Command 0 + continue + + cmd_type = struct.unpack('""") + +if __name__ == '__main__': + if len(sys.argv) != 2: + print_usage() + sys.exit(1) + + nsp_dir = Path(sys.argv[1]) + + if not nsp_dir.is_dir(): + raise ValueError('1st argument must be a directory') + + print("waiting for switch...\n") + dev = None + + while (dev is None): + dev = usb.core.find(idVendor=0x057E, idProduct=0x3000) + time.sleep(0.5) + + print("found the switch!\n") + + cfg = None + + try: + cfg = dev.get_active_configuration() + print("found active config") + except usb.core.USBError: + print("no currently active config") + cfg = None + + if cfg is None: + dev.reset() + dev.set_configuration() + cfg = dev.get_active_configuration() + + is_out_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT + is_in_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN + out_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_out_ep) + in_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_in_ep) + + assert out_ep is not None + assert in_ep is not None + + print("iManufacturer: {} iProduct: {} iSerialNumber: {}".format(dev.manufacturer, dev.product, dev.serial_number)) + print("bcdUSB: {} bMaxPacketSize0: {}".format(hex(dev.bcdUSB), dev.bMaxPacketSize0)) + + send_nsp_list(nsp_dir, out_ep) + poll_commands(nsp_dir, in_ep, out_ep) diff --git a/tools/usb_total.py b/tools/usb_total.py deleted file mode 100644 index 178f976..0000000 --- a/tools/usb_total.py +++ /dev/null @@ -1,141 +0,0 @@ -# based on usb.py from Tinfoil, by Adubbz. -import struct -import sys -import os -import usb.core -import usb.util -import time -import glob -from pathlib import Path - -# magic number (SPHA) for the script and switch. -MAGIC = 0x53504841 -# version of the usb script. -VERSION = 2 -# list of supported extensions. -EXTS = (".nsp", ".xci", ".nsz", ".xcz") - -def verify_switch(bcdUSB, count, in_ep, out_ep): - header = in_ep.read(8, timeout=0) - switch_magic = struct.unpack('