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.
This commit is contained in:
ITotalJustice
2025-04-29 18:11:07 +01:00
parent 81469d0ac9
commit 8f39acbaa2
7 changed files with 366 additions and 275 deletions

View File

@@ -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

View File

@@ -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<std::string> m_names{};
bool m_usb_has_connection{};
u32 m_usb_speed{};
u32 m_usb_count{};
};
} // namespace sphaira::ui::menu::usb

View File

@@ -2,7 +2,9 @@
#include "base.hpp"
#include "fs.hpp"
#include <vector>
#include <string>
#include <new>
#include <switch.h>
@@ -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<std::string>& 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

View File

@@ -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<Menu*>(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<std::string> 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<ui::ProgressBox>(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<ui::ProgressBox>(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:

View File

@@ -14,23 +14,51 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// 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 <ranges>
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<std::string>& 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<char> 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();
}

171
tools/usb_install_pc.py Normal file
View File

@@ -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 <path/to/nsp_folder>
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('<B', CMD_TYPE_RESPONSE))
out_ep.write(b'\x00' * 3)
out_ep.write(struct.pack('<I', cmd_id))
out_ep.write(struct.pack('<Q', data_size))
out_ep.write(b'\x00' * 0xC)
def file_range_cmd(nsp_dir, in_ep, out_ep, data_size):
file_range_header = in_ep.read(0x20)
range_size = struct.unpack('<Q', file_range_header[:8])[0]
range_offset = struct.unpack('<Q', file_range_header[8:16])[0]
nsp_name_len = struct.unpack('<Q', file_range_header[16:24])[0]
#in_ep.read(0x8) # Reserved
nsp_name = bytes(in_ep.read(nsp_name_len)).decode('utf-8')
print('Range Size: {}, Range Offset: {}, Name len: {}, Name: {}'.format(range_size, range_offset, nsp_name_len, nsp_name))
send_response_header(out_ep, CMD_ID_FILE_RANGE, range_size)
with open(nsp_name, 'rb') as f:
f.seek(range_offset)
curr_off = 0x0
end_off = range_size
read_size = 0x800000
while curr_off < end_off:
if curr_off + read_size >= 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('<B', cmd_header[4:5])[0]
cmd_id = struct.unpack('<I', cmd_header[8:12])[0]
data_size = struct.unpack('<Q', cmd_header[12:20])[0]
print('Cmd Type: {}, Command id: {}, Data size: {}'.format(cmd_type, cmd_id, data_size), flush=True)
if cmd_id == CMD_ID_EXIT:
print('Exiting...')
break
elif cmd_id == CMD_ID_FILE_RANGE:
file_range_cmd(nsp_dir, in_ep, out_ep, data_size)
def send_nsp_list(nsp_dir, out_ep):
nsp_path_list = list()
nsp_path_list_len = 0
# Add all files with the extension .nsp in the provided dir
for nsp_path in [f for f in nsp_dir.iterdir() if f.is_file() and (f.suffix in EXTS)]:
nsp_path_list.append(nsp_path.__str__() + '\n')
nsp_path_list_len += len(nsp_path.__str__()) + 1
print('Sending header...')
out_ep.write(b'TUL0') # Tinfoil USB List 0
out_ep.write(struct.pack('<I', nsp_path_list_len))
out_ep.write(b'\x00' * 0x8) # Padding
print('Sending NSP list: {}'.format(nsp_path_list))
for nsp_path in nsp_path_list:
out_ep.write(nsp_path)
def print_usage():
print("""\
usb_install_pc.py
Used for the installation of NSPs over USB.
Usage: usb_install_pc.py <nsp folder>""")
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)

View File

@@ -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('<I', header[0:4])[0]
switch_version = struct.unpack('<I', header[4:8])[0]
if switch_magic != MAGIC:
raise Exception("Unexpected magic {}".format(switch_magic))
if switch_version != VERSION:
raise Exception("Unexpected version {}".format(switch_version))
send_data = struct.pack('<IIII', MAGIC, VERSION, bcdUSB, count)
out_ep.write(data=send_data, timeout=0)
def send_file_info(path, in_ep, out_ep):
file_name = Path(path).name
file_size = Path(path).stat().st_size
file_name_len = len(file_name)
send_data = struct.pack('<QQ', file_size, file_name_len)
out_ep.write(data=send_data, timeout=0)
out_ep.write(data=file_name, timeout=0)
def wait_for_input(path, in_ep, out_ep):
buf = None
predicted_off = 0
print("now waiting for intput\n")
with open(path, "rb") as file:
while True:
header = in_ep.read(24, timeout=0)
range_offset = struct.unpack('<Q', header[8:16])[0]
range_size = struct.unpack('<Q', header[16:24])[0]
if (range_offset == 0 and range_size == 0):
break
if (buf != None and range_offset == predicted_off and range_size == len(buf)):
# print("predicted the read off {} size {}".format(predicted_off, len(buf)))
pass
else:
file.seek(range_offset)
buf = file.read(range_size)
if (len(buf) != range_size):
# print("off: {} size: {}".format(range_offset, range_size))
raise ValueError('bad buf size!!!!!')
result = out_ep.write(data=buf, timeout=0)
if (len(buf) != result):
print("off: {} size: {}".format(range_offset, range_size))
raise ValueError('bad result!!!!!')
predicted_off = range_offset + range_size
buf = file.read(range_size)
if __name__ == '__main__':
print("hello world")
# check which mode the user has selected.
args = len(sys.argv)
if (args != 2):
print("either run python usb_total.py game.nsp OR drag and drop the game onto the python file (if python is in your path)")
sys.exit(1)
path = sys.argv[1]
files = []
if os.path.isfile(path) and path.endswith(EXTS):
files.append(path)
elif os.path.isdir(path):
for f in glob.glob(path + "/**/*.*", recursive=True):
if os.path.isfile(f) and f.endswith(EXTS):
files.append(f)
else:
raise ValueError('must be a file!')
# for file in files:
# print("found file: {}".format(file))
# Find the switch
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.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))
try:
verify_switch(dev.bcdUSB, len(files), in_ep, out_ep)
for file in files:
print("installing file: {}".format(file))
send_file_info(file, in_ep, out_ep)
wait_for_input(file, in_ep, out_ep)
dev.reset()
except Exception as inst:
print("An exception occurred " + str(inst))