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:
34
README.md
34
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
171
tools/usb_install_pc.py
Normal 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)
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user