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.
|
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
|
## Credits
|
||||||
|
|
||||||
- borealis
|
- borealis
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ namespace sphaira::ui::menu::usb {
|
|||||||
enum class State {
|
enum class State {
|
||||||
// not connected.
|
// not connected.
|
||||||
None,
|
None,
|
||||||
|
// just connected, waiting for file list.
|
||||||
|
Connected_WaitForFileList,
|
||||||
// just connected, starts the transfer.
|
// just connected, starts the transfer.
|
||||||
Connected,
|
Connected_StartingTransfer,
|
||||||
// set whilst transfer is in progress.
|
// set whilst transfer is in progress.
|
||||||
Progress,
|
Progress,
|
||||||
// set when the transfer is finished.
|
// set when the transfer is finished.
|
||||||
@@ -35,9 +37,8 @@ struct Menu final : MenuBase {
|
|||||||
Mutex m_mutex{};
|
Mutex m_mutex{};
|
||||||
// the below are shared across threads, lock with the above mutex!
|
// the below are shared across threads, lock with the above mutex!
|
||||||
State m_state{State::None};
|
State m_state{State::None};
|
||||||
|
std::vector<std::string> m_names{};
|
||||||
bool m_usb_has_connection{};
|
bool m_usb_has_connection{};
|
||||||
u32 m_usb_speed{};
|
|
||||||
u32 m_usb_count{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::ui::menu::usb
|
} // namespace sphaira::ui::menu::usb
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
#include "base.hpp"
|
#include "base.hpp"
|
||||||
#include "fs.hpp"
|
#include "fs.hpp"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
#include <new>
|
#include <new>
|
||||||
#include <switch.h>
|
#include <switch.h>
|
||||||
|
|
||||||
@@ -26,8 +28,9 @@ struct Usb final : Base {
|
|||||||
Result Finished();
|
Result Finished();
|
||||||
|
|
||||||
Result Init();
|
Result Init();
|
||||||
Result WaitForConnection(u64 timeout, u32& speed, u32& count);
|
Result IsUsbConnected(u64 timeout) const;
|
||||||
Result GetFileInfo(std::string& name_out, u64& size_out);
|
Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names);
|
||||||
|
void SetFileNameForTranfser(const std::string& name);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// custom allocator for std::vector that respects alignment.
|
// custom allocator for std::vector that respects alignment.
|
||||||
@@ -62,10 +65,9 @@ private:
|
|||||||
UsbSessionEndpoint_Out = 1,
|
UsbSessionEndpoint_Out = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
Result SendCommand(s64 off, s64 size);
|
Result SendCmdHeader(u32 cmdId, size_t dataSize);
|
||||||
Result InternalRead(void* buf, s64 off, s64 size);
|
Result SendFileRangeCmd(u64 offset, u64 size);
|
||||||
|
|
||||||
bool GetConfigured() const;
|
|
||||||
Event *GetCompletionEvent(UsbSessionEndpoint ep) const;
|
Event *GetCompletionEvent(UsbSessionEndpoint ep) const;
|
||||||
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const;
|
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const;
|
||||||
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) 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.
|
// aligned buffer that transfer data is copied to and from.
|
||||||
// a vector is used to avoid multiple alloc within the transfer loop.
|
// a vector is used to avoid multiple alloc within the transfer loop.
|
||||||
PageAlignedVector m_aligned{};
|
PageAlignedVector m_aligned{};
|
||||||
|
std::string m_transfer_file_name{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::yati::source
|
} // namespace sphaira::yati::source
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
namespace sphaira::ui::menu::usb {
|
namespace sphaira::ui::menu::usb {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 3;
|
constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 1; // 1 second
|
||||||
constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5;
|
constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5; // 5 seconds
|
||||||
|
|
||||||
void thread_func(void* user) {
|
void thread_func(void* user) {
|
||||||
auto app = static_cast<Menu*>(user);
|
auto app = static_cast<Menu*>(user);
|
||||||
@@ -21,17 +21,26 @@ void thread_func(void* user) {
|
|||||||
break;
|
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);
|
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)) {
|
if (R_SUCCEEDED(rc)) {
|
||||||
app->m_state = State::Connected;
|
std::vector<std::string> names;
|
||||||
break;
|
if (R_SUCCEEDED(app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, names))) {
|
||||||
} else if (R_FAILED(rc) && R_VALUE(rc) != 0xEA01) {
|
mutexLock(&app->m_mutex);
|
||||||
log_write("got: 0x%X value: 0x%X\n", rc, R_VALUE(rc));
|
ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex));
|
||||||
app->m_state = State::Failed;
|
app->m_state = State::Connected_StartingTransfer;
|
||||||
break;
|
app->m_names = names;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,55 +104,36 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
|||||||
mutexLock(&m_mutex);
|
mutexLock(&m_mutex);
|
||||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
|
||||||
switch (m_state) {
|
if (m_state == State::Connected_StartingTransfer) {
|
||||||
case State::None:
|
log_write("set to progress\n");
|
||||||
break;
|
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("inside progress box\n");
|
||||||
log_write("set to progress\n");
|
for (const auto& file_name : m_names) {
|
||||||
m_state = State::Progress;
|
m_usb_source->SetFileNameForTranfser(file_name);
|
||||||
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("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)) {
|
||||||
const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name);
|
return false;
|
||||||
if (R_FAILED(rc)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
App::Notify("Installed via usb"_i18n);
|
|
||||||
m_usb_source->Finished();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
App::Notify("Installed via usb"_i18n);
|
||||||
}, [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;
|
|
||||||
|
|
||||||
case State::Progress:
|
return true;
|
||||||
break;
|
}, [this](bool result){
|
||||||
|
if (result) {
|
||||||
case State::Done:
|
App::Notify("Usb install success!"_i18n);
|
||||||
break;
|
m_state = State::Done;
|
||||||
|
SetPop();
|
||||||
case State::Failed:
|
} else {
|
||||||
break;
|
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());
|
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;
|
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;
|
break;
|
||||||
|
|
||||||
case State::Progress:
|
case State::Progress:
|
||||||
|
|||||||
@@ -14,23 +14,51 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* 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 "yati/source/usb.hpp"
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
namespace sphaira::yati::source {
|
namespace sphaira::yati::source {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr u32 MAGIC = 0x53504841;
|
enum USBCmdType : u8 {
|
||||||
constexpr u32 VERSION = 2;
|
REQUEST = 0,
|
||||||
|
RESPONSE = 1
|
||||||
struct RecvHeader {
|
|
||||||
u32 magic;
|
|
||||||
u32 version;
|
|
||||||
u32 bcdUSB;
|
|
||||||
u32 count;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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
|
} // namespace
|
||||||
|
|
||||||
Usb::Usb(u64 transfer_timeout) {
|
Usb::Usb(u64 transfer_timeout) {
|
||||||
@@ -50,6 +78,11 @@ Result Usb::Init() {
|
|||||||
log_write("doing USB init\n");
|
log_write("doing USB init\n");
|
||||||
R_TRY(m_open_result);
|
R_TRY(m_open_result);
|
||||||
|
|
||||||
|
SetSysSerialNumber serial_number;
|
||||||
|
R_TRY(setsysInitialize());
|
||||||
|
ON_SCOPE_EXIT(setsysExit());
|
||||||
|
R_TRY(setsysGetSerialNumber(&serial_number));
|
||||||
|
|
||||||
u8 iManufacturer, iProduct, iSerialNumber;
|
u8 iManufacturer, iProduct, iSerialNumber;
|
||||||
static const u16 supported_langs[1] = {0x0409};
|
static const u16 supported_langs[1] = {0x0409};
|
||||||
// Send language descriptor
|
// Send language descriptor
|
||||||
@@ -59,7 +92,7 @@ Result Usb::Init() {
|
|||||||
// Send product
|
// Send product
|
||||||
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
|
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
|
||||||
// Send serial number
|
// Send serial number
|
||||||
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, "SerialNumber"));
|
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, serial_number.number));
|
||||||
|
|
||||||
// Send device descriptors
|
// Send device descriptors
|
||||||
struct usb_device_descriptor device_descriptor = {
|
struct usb_device_descriptor device_descriptor = {
|
||||||
@@ -128,13 +161,11 @@ Result Usb::Init() {
|
|||||||
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
|
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
struct usb_endpoint_descriptor endpoint_descriptor_in = {
|
struct usb_endpoint_descriptor endpoint_descriptor_in = {
|
||||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||||
.bDescriptorType = USB_DT_ENDPOINT,
|
.bDescriptorType = USB_DT_ENDPOINT,
|
||||||
.bEndpointAddress = USB_ENDPOINT_IN,
|
.bEndpointAddress = USB_ENDPOINT_IN,
|
||||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||||
.wMaxPacketSize = 0x40,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct usb_endpoint_descriptor endpoint_descriptor_out = {
|
struct usb_endpoint_descriptor endpoint_descriptor_out = {
|
||||||
@@ -142,7 +173,6 @@ Result Usb::Init() {
|
|||||||
.bDescriptorType = USB_DT_ENDPOINT,
|
.bDescriptorType = USB_DT_ENDPOINT,
|
||||||
.bEndpointAddress = USB_ENDPOINT_OUT,
|
.bEndpointAddress = USB_ENDPOINT_OUT,
|
||||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||||
.wMaxPacketSize = 0x40,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
|
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
|
||||||
@@ -160,6 +190,8 @@ Result Usb::Init() {
|
|||||||
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||||
|
|
||||||
// Full Speed Config
|
// 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, &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_in, USB_DT_ENDPOINT_SIZE));
|
||||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, 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();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result Usb::WaitForConnection(u64 timeout, u32& speed, u32& count) {
|
Result Usb::IsUsbConnected(u64 timeout) const {
|
||||||
struct {
|
return usbDsWaitReady(timeout);
|
||||||
u32 magic;
|
}
|
||||||
u32 version;
|
|
||||||
} send_header{MAGIC, VERSION};
|
|
||||||
|
|
||||||
// send header.
|
Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names) {
|
||||||
R_TRY(TransferAll(false, &send_header, sizeof(send_header), timeout));
|
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.
|
std::vector<char> names(header.nspListSize);
|
||||||
struct RecvHeader recv_header;
|
R_TRY(TransferAll(true, names.data(), names.size(), timeout));
|
||||||
R_TRY(TransferAll(true, &recv_header, sizeof(recv_header), timeout));
|
|
||||||
|
|
||||||
// validate received header.
|
out_names.clear();
|
||||||
R_UNLESS(recv_header.magic == MAGIC, Result_BadMagic);
|
for (const auto& name : std::views::split(names, '\n')) {
|
||||||
R_UNLESS(recv_header.version == VERSION, Result_BadVersion);
|
if (!name.empty()) {
|
||||||
R_UNLESS(recv_header.count > 0, Result_BadCount);
|
out_names.emplace_back(name.data(), name.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
count = recv_header.count;
|
for (auto& name : out_names) {
|
||||||
speed = recv_header.bcdUSB;
|
log_write("got name: %s\n", name.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
R_UNLESS(!out_names.empty(), Result_BadCount);
|
||||||
|
log_write("USB SUCCESS\n");
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result Usb::GetFileInfo(std::string& name_out, u64& size_out) {
|
void Usb::SetFileNameForTranfser(const std::string& name) {
|
||||||
struct {
|
m_transfer_file_name = name;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const {
|
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;
|
u32 urb_id;
|
||||||
|
|
||||||
/* If we're not configured yet, wait to become configured first. */
|
/* If we're not configured yet, wait to become configured first. */
|
||||||
// R_TRY(usbDsWaitReady(timeout));
|
R_TRY(IsUsbConnected(timeout));
|
||||||
if (!GetConfigured()) {
|
|
||||||
R_TRY(eventWait(usbDsGetStateChangeEvent(), timeout));
|
|
||||||
R_TRY(eventClear(usbDsGetStateChangeEvent()));
|
|
||||||
R_THROW(0xEA01);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Select the appropriate endpoint and begin a transfer. */
|
/* Select the appropriate endpoint and begin a transfer. */
|
||||||
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
|
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();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result Usb::SendCommand(s64 off, s64 size) {
|
Result Usb::SendCmdHeader(u32 cmdId, size_t dataSize) {
|
||||||
struct {
|
USBCmdHeader header{
|
||||||
u32 hash;
|
.magic = 0x30435554, // TUC0 (Tinfoil USB Command 0)
|
||||||
u32 magic;
|
.type = USBCmdType::REQUEST,
|
||||||
s64 off;
|
.cmdId = cmdId,
|
||||||
s64 size;
|
.dataSize = dataSize,
|
||||||
} meta{0, 0, off, size};
|
};
|
||||||
|
|
||||||
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() {
|
Result Usb::Finished() {
|
||||||
return SendCommand(0, 0);
|
return SendCmdHeader(USBCmdId::EXIT, 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||||
R_TRY(GetOpenResult());
|
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;
|
*bytes_read = size;
|
||||||
R_SUCCEED();
|
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