add mtp custom mount support (image sd, image nand, install, speed test).

This commit is contained in:
ITotalJustice
2025-06-12 14:47:33 +01:00
parent c535b96b12
commit 275707fe27
12 changed files with 952 additions and 62 deletions

View File

@@ -45,6 +45,7 @@ add_executable(sphaira
source/ui/menus/ghdl.cpp
source/ui/menus/usb_menu.cpp
source/ui/menus/ftp_menu.cpp
source/ui/menus/mtp_menu.cpp
source/ui/menus/gc_menu.cpp
source/ui/menus/game_menu.cpp
source/ui/menus/grid_menu_base.cpp
@@ -79,6 +80,7 @@ add_executable(sphaira
source/hasher.cpp
source/i18n.cpp
source/ftpsrv_helper.cpp
source/haze_helper.cpp
source/threaded_file_transfer.cpp
source/minizip_helper.cpp
@@ -160,7 +162,7 @@ FetchContent_Declare(ftpsrv
FetchContent_Declare(libhaze
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
GIT_TAG 8e16df2
GIT_TAG d318432
)
FetchContent_Declare(libpulsar

View File

@@ -7,7 +7,6 @@
#include <string>
#include <switch.h>
#include <nxlink.h>
#include <haze.h>
#include "download.hpp"
namespace sphaira::evman {
@@ -24,7 +23,6 @@ struct ExitEventData {
using EventData = std::variant<
LaunchNroEventData,
ExitEventData,
HazeCallbackData,
NxlinkCallbackData,
curl::DownloadEventData
>;

View File

@@ -195,6 +195,7 @@ struct Dir {
~Dir();
Result GetEntryCount(s64* out);
Result Read(s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf);
Result ReadAll(std::vector<FsDirectoryEntry>& buf);
void Close();
@@ -398,36 +399,6 @@ struct FsNative : Fs {
return fsFsGetTotalSpace(&m_fs, path, out);
}
// Result OpenDirectory(const FsPath& path, u32 mode, FsDir *out) {
// return fsFsOpenDirectory(&m_fs, path, mode, out);
// }
// void DirClose(FsDir *d) {
// fsDirClose(d);
// }
// Result DirGetEntryCount(FsDir *d, s64* out) {
// return fsDirGetEntryCount(d, out);
// }
// Result DirGetEntryCount(const FsPath& path, u32 mode, s64* out) {
// FsDir d;
// R_TRY(OpenDirectory(path, mode, &d));
// ON_SCOPE_EXIT(DirClose(&d));
// return DirGetEntryCount(&d, out);
// }
// Result DirRead(FsDir *d, s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) {
// return fsDirRead(d, total_entries, max_entries, buf);
// }
// Result DirRead(const FsPath& path, u32 mode, s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) {
// FsDir d;
// R_TRY(OpenDirectory(path, mode, &d));
// ON_SCOPE_EXIT(DirClose(&d));
// return DirRead(&d, total_entries, max_entries, buf);
// }
virtual bool IsFsActive() {
return serviceIsActive(&m_fs.s);
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include <functional>
namespace sphaira::haze {
bool Init();
void Exit();
using OnInstallStart = std::function<bool(void* user, const char* path)>;
using OnInstallWrite = std::function<bool(void* user, const void* buf, size_t size)>;
using OnInstallClose = std::function<void(void* user)>;
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
void DisableInstallMode();
} // namespace sphaira::haze

View File

@@ -0,0 +1,57 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/source/stream.hpp"
namespace sphaira::ui::menu::mtp {
enum class State {
// not connected.
None,
// just connected, starts the transfer.
Connected,
// set whilst transfer is in progress.
Progress,
// set when the transfer is finished.
Done,
// failed to connect.
Failed,
};
struct StreamFtp final : yati::source::Stream {
StreamFtp(const fs::FsPath& path, std::stop_token token);
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
bool Push(const void* buf, s64 size);
void Disable();
// private:
fs::FsPath m_path{};
std::stop_token m_token{};
std::vector<u8> m_buffer{};
Mutex m_mutex{};
bool m_active{};
// bool m_push_exit{};
};
struct Menu final : MenuBase {
Menu(u32 flags);
~Menu();
auto GetShortTitle() const -> const char* override { return "MTP"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
// this should be private
// private:
std::shared_ptr<StreamFtp> m_source{};
Thread m_thread{};
Mutex m_mutex{};
// the below are shared across threads, lock with the above mutex!
State m_state{State::None};
bool m_was_mtp_enabled{};
};
} // namespace sphaira::ui::menu::mtp

View File

@@ -2,6 +2,11 @@
#include "base.hpp"
// TODO: remove these when libnx pr is merged.
enum { UsbDeviceSpeed_None = 0x0 };
enum { UsbDeviceSpeed_Low = 0x1 };
Result usbDsGetSpeed(UsbDeviceSpeed *out);
namespace sphaira::usb {
// Device Host

View File

@@ -19,13 +19,13 @@
#include "defines.hpp"
#include "i18n.hpp"
#include "ftpsrv_helper.hpp"
#include "haze_helper.hpp"
#include "web.hpp"
#include "swkbd.hpp"
#include <nanovg_dk.h>
#include <minIni.h>
#include <pulsar.h>
#include <haze.h>
#include <algorithm>
#include <ranges>
#include <cassert>
@@ -400,11 +400,6 @@ void LoadThemeInternal(ThemeMeta meta, ThemeData& theme_data, int inherit_level
}
}
void haze_callback(const HazeCallbackData *data) {
App::NotifyFlashLed();
evman::push(*data, false);
}
void nxlink_callback(const NxlinkCallbackData *data) {
App::NotifyFlashLed();
evman::push(*data, false);
@@ -460,9 +455,6 @@ void App::Loop() {
} else if constexpr(std::is_same_v<T, evman::ExitEventData>) {
log_write("[ExitEventData] got event\n");
m_quit = true;
} else if constexpr(std::is_same_v<T, HazeCallbackData>) {
// log_write("[ExitEventData] got event\n");
// m_quit = true;
} else if constexpr(std::is_same_v<T, NxlinkCallbackData>) {
switch (arg.type) {
case NxlinkCallbackType_Connected:
@@ -857,9 +849,9 @@ void App::SetMtpEnable(bool enable) {
if (App::GetMtpEnable() != enable) {
g_app->m_mtp_enabled.Set(enable);
if (enable) {
hazeInitialize(haze_callback, 0x2C, 2);
haze::Init();
} else {
hazeExit();
haze::Exit();
}
}
}
@@ -1379,7 +1371,7 @@ App::App(const char* argv0) {
}
if (App::GetMtpEnable()) {
hazeInitialize(haze_callback, PRIO_PREEMPTIVE, 2);
haze::Init();
}
if (App::GetFtpEnable()) {
@@ -1948,7 +1940,7 @@ App::~App() {
if (App::GetMtpEnable()) {
log_write("closing mtp\n");
hazeExit();
haze::Exit();
}
if (App::GetFtpEnable()) {

View File

@@ -702,6 +702,47 @@ Result Dir::GetEntryCount(s64* out) {
R_SUCCEED();
}
Result Dir::Read(s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) {
R_UNLESS(m_fs, Result_FsNotActive);
*total_entries = 0;
if (m_fs->IsNative()) {
R_TRY(fsDirRead(&m_native, total_entries, max_entries, buf));
} else {
while (auto d = readdir(m_stdio)) {
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
continue;
}
FsDirectoryEntry entry{};
if (d->d_type == DT_DIR) {
if (!(m_mode & FsDirOpenMode_ReadDirs)) {
continue;
}
entry.type = FsDirEntryType_Dir;
} else if (d->d_type == DT_REG) {
if (!(m_mode & FsDirOpenMode_ReadFiles)) {
continue;
}
entry.type = FsDirEntryType_File;
} else {
log_write("[FS] WARNING: unknown type when reading dir: %u\n", d->d_type);
continue;
}
std::strcpy(entry.name, d->d_name);
std::memcpy(&buf[*total_entries], &entry, sizeof(*buf));
*total_entries = *total_entries + 1;
if (*total_entries >= max_entries) {
break;
}
}
}
R_SUCCEED();
}
Result Dir::ReadAll(std::vector<FsDirectoryEntry>& buf) {
buf.clear();
R_UNLESS(m_fs, Result_FsNotActive);

View File

@@ -0,0 +1,520 @@
#include "haze_helper.hpp"
#include "app.hpp"
#include "fs.hpp"
#include "log.hpp"
#include "evman.hpp"
#include "i18n.hpp"
#include <mutex>
#include <algorithm>
#include <haze.h>
namespace sphaira::haze {
namespace {
struct InstallSharedData {
std::mutex mutex;
std::deque<std::string> queued_files;
void* user;
OnInstallStart on_start;
OnInstallWrite on_write;
OnInstallClose on_close;
bool in_progress;
bool enabled;
};
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
constexpr int THREAD_CORE = 2;
volatile bool g_should_exit = false;
bool g_is_running{false};
std::mutex g_mutex{};
InstallSharedData g_shared_data{};
const char* SUPPORTED_EXT[] = {
".nsp", ".xci", ".nsz", ".xcz",
};
// ive given up with good names.
void on_thing() {
log_write("[MTP] doing on_thing\n");
std::scoped_lock lock{g_shared_data.mutex};
log_write("[MTP] locked on_thing\n");
if (!g_shared_data.in_progress) {
if (!g_shared_data.queued_files.empty()) {
log_write("[MTP] pushing new file data\n");
if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.user, g_shared_data.queued_files[0].c_str())) {
g_shared_data.queued_files.clear();
} else {
log_write("[MTP] success on new file push\n");
g_shared_data.in_progress = true;
}
}
}
}
struct FsProxyBase : ::haze::FileSystemProxyImpl {
FsProxyBase(const char* name, const char* display_name) : m_name{name}, m_display_name{display_name} {
}
auto FixPath(const char* path) const {
fs::FsPath buf;
const auto len = std::strlen(GetName());
if (len && !strncasecmp(path + 1, GetName(), len)) {
std::snprintf(buf, sizeof(buf), "/%s", path + 1 + len);
} else {
std::strcpy(buf, path);
}
log_write("[FixPath] %s -> %s\n", path, buf.s);
return buf;
}
const char* GetName() const override {
return m_name.c_str();
}
const char* GetDisplayName() const override {
return m_display_name.c_str();
}
protected:
const std::string m_name;
const std::string m_display_name;
};
struct FsProxy final : FsProxyBase {
FsProxy(std::shared_ptr<fs::Fs> fs, const char* name, const char* display_name) : FsProxyBase{name, display_name} {
m_fs = fs;
}
~FsProxy() {
if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs.get();
fsFsCommit(&fs->m_fs);
}
}
// TODO: impl this for stdio
Result GetTotalSpace(const char *path, s64 *out) override {
if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs.get();
return fsFsGetTotalSpace(&fs->m_fs, FixPath(path), out);
}
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED();
}
Result GetFreeSpace(const char *path, s64 *out) override {
if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs.get();
return fsFsGetFreeSpace(&fs->m_fs, FixPath(path), out);
}
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED();
}
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
const auto rc = m_fs->GetEntryType(FixPath(path), out_entry_type);
log_write("[HAZE] GetEntryType(%s) 0x%X\n", path, rc);
return rc;
}
Result CreateFile(const char* path, s64 size, u32 option) override {
log_write("[HAZE] CreateFile(%s)\n", path);
return m_fs->CreateFile(FixPath(path), size, option);
}
Result DeleteFile(const char* path) override {
log_write("[HAZE] DeleteFile(%s)\n", path);
return m_fs->DeleteFile(FixPath(path));
}
Result RenameFile(const char *old_path, const char *new_path) override {
log_write("[HAZE] RenameFile(%s -> %s)\n", old_path, new_path);
return m_fs->RenameFile(FixPath(old_path), FixPath(new_path));
}
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
log_write("[HAZE] OpenFile(%s)\n", path);
auto fptr = new fs::File();
const auto rc = m_fs->OpenFile(FixPath(path), mode, fptr);
if (R_SUCCEEDED(rc)) {
std::memcpy(&out_file->s, &fptr, sizeof(fptr));
} else {
delete fptr;
}
return rc;
}
Result GetFileSize(FsFile *file, s64 *out_size) override {
log_write("[HAZE] GetFileSize()\n");
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
return f->GetSize(out_size);
}
Result SetFileSize(FsFile *file, s64 size) override {
log_write("[HAZE] SetFileSize()\n");
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
return f->SetSize(size);
}
Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) override {
log_write("[HAZE] ReadFile()\n");
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
return f->Read(off, buf, read_size, option, out_bytes_read);
}
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
log_write("[HAZE] WriteFile()\n");
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
return f->Write(off, buf, write_size, option);
}
void CloseFile(FsFile *file) override {
log_write("[HAZE] CloseFile()\n");
fs::File* f;
std::memcpy(&f, &file->s, sizeof(f));
if (f) {
delete f;
}
std::memset(file, 0, sizeof(*file));
}
Result CreateDirectory(const char* path) override {
log_write("[HAZE] DeleteFile(%s)\n", path);
return m_fs->CreateDirectory(FixPath(path));
}
Result DeleteDirectoryRecursively(const char* path) override {
log_write("[HAZE] DeleteDirectoryRecursively(%s)\n", path);
return m_fs->DeleteDirectoryRecursively(FixPath(path));
}
Result RenameDirectory(const char *old_path, const char *new_path) override {
log_write("[HAZE] RenameDirectory(%s -> %s)\n", old_path, new_path);
return m_fs->RenameDirectory(FixPath(old_path), FixPath(new_path));
}
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
auto fptr = new fs::Dir();
const auto rc = m_fs->OpenDirectory(FixPath(path), mode, fptr);
if (R_SUCCEEDED(rc)) {
std::memcpy(&out_dir->s, &fptr, sizeof(fptr));
} else {
delete fptr;
}
log_write("[HAZE] OpenDirectory(%s) 0x%X\n", path, rc);
return rc;
}
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
fs::Dir* f;
std::memcpy(&f, &d->s, sizeof(f));
const auto rc = f->Read(out_total_entries, max_entries, buf);
log_write("[HAZE] ReadDirectory(%zd) 0x%X\n", *out_total_entries, rc);
return rc;
}
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
fs::Dir* f;
std::memcpy(&f, &d->s, sizeof(f));
const auto rc = f->GetEntryCount(out_count);
log_write("[HAZE] GetDirectoryEntryCount(%zd) 0x%X\n", *out_count, rc);
return rc;
}
void CloseDirectory(FsDir *d) override {
log_write("[HAZE] CloseDirectory()\n");
fs::Dir* f;
std::memcpy(&f, &d->s, sizeof(f));
if (f) {
delete f;
}
std::memset(d, 0, sizeof(*d));
}
private:
std::shared_ptr<fs::Fs> m_fs{};
};
struct FsDevNullProxy final : FsProxyBase {
using FsProxyBase::FsProxyBase;
Result GetTotalSpace(const char *path, s64 *out) override {
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED();
}
Result GetFreeSpace(const char *path, s64 *out) override {
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
R_SUCCEED();
}
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
if (FixPath(path) == "/") {
*out_entry_type = FsDirEntryType_Dir;
R_SUCCEED();
} else {
*out_entry_type = FsDirEntryType_File;
R_SUCCEED();
}
}
Result CreateFile(const char* path, s64 size, u32 option) override {
R_SUCCEED();
}
Result DeleteFile(const char* path) override {
R_SUCCEED();
}
Result RenameFile(const char *old_path, const char *new_path) override {
R_SUCCEED();
}
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
R_SUCCEED();
}
Result GetFileSize(FsFile *file, s64 *out_size) override {
*out_size = 0;
R_SUCCEED();
}
Result SetFileSize(FsFile *file, s64 size) override {
R_SUCCEED();
}
Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) override {
*out_bytes_read = 0;
R_SUCCEED();
}
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
R_SUCCEED();
}
void CloseFile(FsFile *file) override {
std::memset(file, 0, sizeof(*file));
}
Result CreateDirectory(const char* path) override {
R_SUCCEED();
}
Result DeleteDirectoryRecursively(const char* path) override {
R_SUCCEED();
}
Result RenameDirectory(const char *old_path, const char *new_path) override {
R_SUCCEED();
}
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
R_SUCCEED();
}
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
*out_total_entries = 0;
R_SUCCEED();
}
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
*out_count = 0;
R_SUCCEED();
}
void CloseDirectory(FsDir *d) override {
std::memset(d, 0, sizeof(*d));
}
};
struct FsInstallProxy final : FsProxyBase {
using FsProxyBase::FsProxyBase;
Result FailedIfNotEnabled() {
std::scoped_lock lock{g_shared_data.mutex};
if (!g_shared_data.enabled) {
App::Notify("Please launch MTP install menu before trying to install"_i18n);
R_THROW(0x1);
}
R_SUCCEED();
}
// TODO: impl this.
Result GetTotalSpace(const char *path, s64 *out) override {
if (App::GetApp()->m_install_sd.Get()) {
return fs::FsNativeContentStorage(FsContentStorageId_SdCard).GetTotalSpace("/", out);
} else {
return fs::FsNativeContentStorage(FsContentStorageId_User).GetTotalSpace("/", out);
}
}
Result GetFreeSpace(const char *path, s64 *out) override {
if (App::GetApp()->m_install_sd.Get()) {
return fs::FsNativeContentStorage(FsContentStorageId_SdCard).GetFreeSpace("/", out);
} else {
return fs::FsNativeContentStorage(FsContentStorageId_User).GetFreeSpace("/", out);
}
}
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
if (FixPath(path) == "/") {
*out_entry_type = FsDirEntryType_Dir;
R_SUCCEED();
} else {
*out_entry_type = FsDirEntryType_File;
R_SUCCEED();
}
}
Result CreateFile(const char* path, s64 size, u32 option) override {
return FailedIfNotEnabled();
}
Result DeleteFile(const char* path) override {
R_SUCCEED();
}
Result RenameFile(const char *old_path, const char *new_path) override {
R_SUCCEED();
}
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
if (mode & FsOpenMode_Read) {
R_SUCCEED();
} else {
std::scoped_lock lock{g_shared_data.mutex};
if (!g_shared_data.enabled) {
R_THROW(0x1);
}
const char* ext = std::strrchr(path, '.');
if (!ext) {
R_THROW(0x1);
}
bool found = false;
for (size_t i = 0; i < std::size(SUPPORTED_EXT); i++) {
if (!strcasecmp(ext, SUPPORTED_EXT[i])) {
found = true;
break;
}
}
if (!found) {
R_THROW(0x1);
}
// check if we already have this file queued.
auto it = std::ranges::find(g_shared_data.queued_files, path);
if (it != g_shared_data.queued_files.cend()) {
R_THROW(0x1);
}
g_shared_data.queued_files.push_back(path);
}
on_thing();
log_write("[MTP] got file: %s\n", path);
R_SUCCEED();
}
Result GetFileSize(FsFile *file, s64 *out_size) override {
*out_size = 0;
R_SUCCEED();
}
Result SetFileSize(FsFile *file, s64 size) override {
R_SUCCEED();
}
Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) override {
*out_bytes_read = 0;
R_SUCCEED();
}
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
std::scoped_lock lock{g_shared_data.mutex};
if (!g_shared_data.enabled) {
R_THROW(0x1);
}
if (!g_shared_data.on_write || !g_shared_data.on_write(g_shared_data.user, buf, write_size)) {
R_THROW(0x1);
}
R_SUCCEED();
}
void CloseFile(FsFile *file) override {
{
log_write("[MTP] closing file\n");
std::scoped_lock lock{g_shared_data.mutex};
log_write("[MTP] closing valid file\n");
log_write("[MTP] closing current file\n");
if (g_shared_data.on_close) {
g_shared_data.on_close(g_shared_data.user);
}
g_shared_data.in_progress = false;
g_shared_data.queued_files.clear();
}
on_thing();
std::memset(file, 0, sizeof(*file));
}
Result CreateDirectory(const char* path) override {
R_SUCCEED();
}
Result DeleteDirectoryRecursively(const char* path) override {
R_SUCCEED();
}
Result RenameDirectory(const char *old_path, const char *new_path) override {
R_SUCCEED();
}
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
R_SUCCEED();
}
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
*out_total_entries = 0;
R_SUCCEED();
}
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
*out_count = 0;
R_SUCCEED();
}
void CloseDirectory(FsDir *d) override {
std::memset(d, 0, sizeof(*d));
}
};
::haze::FsEntries g_fs_entries{};
void haze_callback(const ::haze::CallbackData *data) {
App::NotifyFlashLed();
}
} // namespace
bool Init() {
std::scoped_lock lock{g_mutex};
if (g_is_running) {
log_write("[MTP] already enabled, cannot open\n");
return false;
}
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_shared<fs::FsNativeSd>(), "", "microSD card"));
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_shared<fs::FsNativeImage>(FsImageDirectoryId_Nand), "image_nand", "Image nand"));
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_shared<fs::FsNativeImage>(FsImageDirectoryId_Sd), "image_sd", "Image sd"));
g_fs_entries.emplace_back(std::make_shared<FsDevNullProxy>("DevNull", "DevNull (Speed Test)"));
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));
g_should_exit = false;
if (!::haze::Initialize(haze_callback, PRIO_PREEMPTIVE, 2, g_fs_entries)) {
return false;
}
log_write("[MTP] started\n");
return g_is_running = true;
}
void Exit() {
std::scoped_lock lock{g_mutex};
if (!g_is_running) {
return;
}
::haze::Exit();
g_is_running = false;
g_should_exit = true;
g_fs_entries.clear();
log_write("[MTP] exitied\n");
}
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) {
std::scoped_lock lock{g_shared_data.mutex};
g_shared_data.user = user;
g_shared_data.on_start = on_start;
g_shared_data.on_write = on_write;
g_shared_data.on_close = on_close;
g_shared_data.enabled = true;
}
void DisableInstallMode() {
std::scoped_lock lock{g_shared_data.mutex};
g_shared_data.enabled = false;
}
} // namespace sphaira::haze

View File

@@ -13,6 +13,7 @@
#include "ui/menus/ghdl.hpp"
#include "ui/menus/usb_menu.hpp"
#include "ui/menus/ftp_menu.hpp"
#include "ui/menus/mtp_menu.hpp"
#include "ui/menus/gc_menu.hpp"
#include "ui/menus/game_menu.hpp"
#include "ui/menus/save_menu.hpp"
@@ -54,6 +55,7 @@ const MiscMenuEntry MISC_MENU_ENTRIES[] = {
{ .name = "Themezer", .title = "Themezer", .func = MiscMenuFuncGenerator<ui::menu::themezer::Menu>, .flag = MiscMenuFlag_Shortcut },
{ .name = "GitHub", .title = "GitHub", .func = MiscMenuFuncGenerator<ui::menu::gh::Menu>, .flag = MiscMenuFlag_Shortcut },
{ .name = "FTP", .title = "FTP Install", .func = MiscMenuFuncGenerator<ui::menu::ftp::Menu>, .flag = MiscMenuFlag_Install },
{ .name = "MTP", .title = "MTP Install", .func = MiscMenuFuncGenerator<ui::menu::mtp::Menu>, .flag = MiscMenuFlag_Install },
{ .name = "USB", .title = "USB Install", .func = MiscMenuFuncGenerator<ui::menu::usb::Menu>, .flag = MiscMenuFlag_Install },
{ .name = "GameCard", .title = "GameCard", .func = MiscMenuFuncGenerator<ui::menu::gc::Menu>, .flag = MiscMenuFlag_Shortcut },
{ .name = "IRS", .title = "IRS (Infrared Joycon Camera)", .func = MiscMenuFuncGenerator<ui::menu::irs::Menu>, .flag = MiscMenuFlag_Shortcut },

View File

@@ -0,0 +1,290 @@
#include "ui/menus/mtp_menu.hpp"
#include "yati/yati.hpp"
#include "usb/usbds.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include "haze_helper.hpp"
#include <cstring>
#include <algorithm>
namespace sphaira::ui::menu::mtp {
namespace {
constexpr u64 MAX_BUFFER_SIZE = 1024*1024*32;
constexpr u64 SLEEPNS = 1000;
volatile bool IN_PUSH_THREAD{};
auto GetUsbStateStr(UsbState state) -> const char* {
switch (state) {
case UsbState_Detached: return "Detached";
case UsbState_Attached: return "Attached";
case UsbState_Powered: return "Powered";
case UsbState_Default: return "Default";
case UsbState_Address: return "Address";
case UsbState_Configured: return "Configured";
case UsbState_Suspended: return "Suspended";
}
return "Unknown";
}
auto GetUsbSpeedStr(UsbDeviceSpeed speed) -> const char* {
// todo: remove this cast when libnx pr is merged.
switch ((u32)speed) {
case UsbDeviceSpeed_None: return "None";
case UsbDeviceSpeed_Low: return "USB 1.0 Low Speed";
case UsbDeviceSpeed_Full: return "USB 1.1 Full Speed";
case UsbDeviceSpeed_High: return "USB 2.0 High Speed";
case UsbDeviceSpeed_Super: return "USB 3.0 Super Speed";
}
return "Unknown";
}
bool OnInstallStart(void* user, const char* path) {
auto menu = (Menu*)user;
log_write("[INSTALL] inside OnInstallStart()\n");
for (;;) {
mutexLock(&menu->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
if (menu->m_state != State::Progress) {
break;
}
if (menu->GetToken().stop_requested()) {
return false;
}
svcSleepThread(1e+6);
}
log_write("[INSTALL] OnInstallStart() got state: %u\n", (u8)menu->m_state);
if (menu->m_source) {
log_write("[INSTALL] OnInstallStart() we have source\n");
for (;;) {
mutexLock(&menu->m_source->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_source->m_mutex));
if (!IN_PUSH_THREAD) {
break;
}
if (menu->GetToken().stop_requested()) {
return false;
}
svcSleepThread(1e+6);
}
log_write("[INSTALL] OnInstallStart() stopped polling source\n");
}
log_write("[INSTALL] OnInstallStart() doing make_shared\n");
menu->m_source = std::make_shared<StreamFtp>(path, menu->GetToken());
mutexLock(&menu->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
menu->m_state = State::Connected;
log_write("[INSTALL] OnInstallStart() done make shared\n");
return true;
}
bool OnInstallWrite(void* user, const void* buf, size_t size) {
auto menu = (Menu*)user;
return menu->m_source->Push(buf, size);
}
void OnInstallClose(void* user) {
auto menu = (Menu*)user;
menu->m_source->Disable();
}
} // namespace
StreamFtp::StreamFtp(const fs::FsPath& path, std::stop_token token) {
m_path = path;
m_token = token;
m_buffer.reserve(MAX_BUFFER_SIZE);
m_active = true;
}
Result StreamFtp::ReadChunk(void* buf, s64 size, u64* bytes_read) {
while (!m_token.stop_requested()) {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (m_buffer.empty()) {
if (!m_active) {
break;
}
svcSleepThread(SLEEPNS);
} else {
size = std::min<s64>(size, m_buffer.size());
std::memcpy(buf, m_buffer.data(), size);
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size);
*bytes_read = size;
R_SUCCEED();
}
}
return 0x1;
}
bool StreamFtp::Push(const void* buf, s64 size) {
IN_PUSH_THREAD = true;
ON_SCOPE_EXIT(IN_PUSH_THREAD = false);
while (!m_token.stop_requested()) {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (!m_active) {
break;
}
if (m_buffer.size() + size >= MAX_BUFFER_SIZE) {
svcSleepThread(SLEEPNS);
} else {
const auto offset = m_buffer.size();
m_buffer.resize(offset + size);
std::memcpy(m_buffer.data() + offset, buf, size);
return true;
}
}
return false;
}
void StreamFtp::Disable() {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
m_active = false;
}
Menu::Menu(u32 flags) : MenuBase{"MTP Install"_i18n, flags} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
SetAction(Button::X, Action{"Options"_i18n, [this](){
App::DisplayInstallOptions(false);
}});
App::SetAutoSleepDisabled(true);
mutexInit(&m_mutex);
m_was_mtp_enabled = App::GetMtpEnable();
if (!m_was_mtp_enabled) {
log_write("[MTP] wasn't enabled, forcefully enabling\n");
App::SetMtpEnable(true);
}
haze::InitInstallMode(this, OnInstallStart, OnInstallWrite, OnInstallClose);
}
Menu::~Menu() {
// signal for thread to exit and wait.
haze::DisableInstallMode();
m_stop_source.request_stop();
if (m_source) {
m_source->Disable();
}
if (!m_was_mtp_enabled) {
log_write("[MTP] disabling on exit\n");
App::SetMtpEnable(false);
}
App::SetAutoSleepDisabled(false);
log_write("closing data!!!!\n");
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
static TimeStamp poll_ts;
if (poll_ts.GetSeconds() >= 1) {
poll_ts.Update();
UsbState state{UsbState_Detached};
usbDsGetState(&state);
UsbDeviceSpeed speed{(UsbDeviceSpeed)UsbDeviceSpeed_None};
usbDsGetSpeed(&speed);
char buf[128];
std::snprintf(buf, sizeof(buf), "State: %s | Speed: %s", i18n::get(GetUsbStateStr(state)).c_str(), i18n::get(GetUsbSpeedStr(speed)).c_str());
SetSubHeading(buf);
}
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (m_state == 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) -> Result {
log_write("inside progress box\n");
const auto rc = yati::InstallFromSource(pbox, m_source, m_source->m_path);
if (R_FAILED(rc)) {
m_source->Disable();
R_THROW(rc);
}
R_SUCCEED();
}, [this](Result rc){
App::PushErrorBox(rc, "MTP install failed!"_i18n);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (R_SUCCEEDED(rc)) {
App::Notify("MTP install success!"_i18n);
m_state = State::Done;
} else {
m_state = State::Failed;
haze::DisableInstallMode();
}
}));
}
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
switch (m_state) {
case State::None:
case State::Done:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Drag'n'Drop (NSP, XCI, NSZ, XCZ) to the install folder on PC"_i18n.c_str());
break;
case State::Connected:
case State::Progress:
break;
case State::Failed:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to install via MTP, press B to exit..."_i18n.c_str());
break;
}
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
}
} // namespace sphaira::ui::menu::mtp

View File

@@ -4,22 +4,6 @@
#include <ranges>
#include <cstring>
namespace sphaira::usb {
namespace {
// TODO: pr missing speed fields to libnx.
enum { UsbDeviceSpeed_None = 0x0 };
enum { UsbDeviceSpeed_Low = 0x1 };
constexpr u16 DEVICE_SPEED[] = {
[UsbDeviceSpeed_None] = 0x0,
[UsbDeviceSpeed_Low] = 0x0,
[UsbDeviceSpeed_Full] = 0x40,
[UsbDeviceSpeed_High] = 0x200,
[UsbDeviceSpeed_Super] = 0x400,
};
// TODO: pr this to libnx.
Result usbDsGetSpeed(UsbDeviceSpeed *out) {
if (hosversionBefore(8,0,0)) {
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
@@ -29,6 +13,17 @@ Result usbDsGetSpeed(UsbDeviceSpeed *out) {
return serviceDispatchOut(usbDsGetServiceSession(), hosversionAtLeast(11,0,0) ? 11 : 12, *out);
}
namespace sphaira::usb {
namespace {
constexpr u16 DEVICE_SPEED[] = {
[UsbDeviceSpeed_None] = 0x0,
[UsbDeviceSpeed_Low] = 0x0,
[UsbDeviceSpeed_Full] = 0x40,
[UsbDeviceSpeed_High] = 0x200,
[UsbDeviceSpeed_Super] = 0x400,
};
} // namespace
UsbDs::~UsbDs() {