From fea0a62d064eec326e6f5b828721a2d4fd322951 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Tue, 16 Jul 2024 22:53:13 +0200 Subject: [PATCH] Add Artic Controller support --- Makefile | 2 +- README.md | 2 +- app/Makefile | 2 +- plugin/Makefile | 2 +- plugin/includes/ArticBaseFunctions.hpp | 9 + plugin/includes/ArticBaseServer.hpp | 11 +- plugin/includes/hidExtension.hpp | 12 ++ plugin/sources/ArticBaseFunctions.cpp | 258 ++++++++++++++++++++++++- plugin/sources/ArticBaseServer.cpp | 31 +++ plugin/sources/Logger.cpp | 3 + plugin/sources/hidExtension.cpp | 26 +++ plugin/sources/main.cpp | 79 ++++++-- 12 files changed, 414 insertions(+), 23 deletions(-) create mode 100644 plugin/includes/hidExtension.hpp create mode 100644 plugin/sources/hidExtension.cpp diff --git a/Makefile b/Makefile index 8181621..bfca83b 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ VERSION_MAJOR := 1 -VERSION_MINOR := 1 +VERSION_MINOR := 2 VERSION_REVISION := 0 all: diff --git a/README.md b/README.md index 9012ad9..95b458d 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Artic Base Server is a Luma3DS 3GX plugin that allows using the Artic Base proto ### Features - Play games from your console without having to dump them, with purchased updates and DLC. - Sync the savedata/extdata of the broadcasted game during the play session. +- Use the console as the input device by enabling the Artic Controller feature on the emulator. - Load shared ext data and NCCH archives from your console. - Remove the need to dump AES keys, as the decryption is done by the console's OS. @@ -39,7 +40,6 @@ NOTE: A recent version of Luma3DS (v13.1.1 or newer) is requires to use Artic Ba This section lists features that Artic Base Server cannot currently provide. Some of these features may be added in the future. ### Things that might be implemented -- Use the console as a controller itself. - Broadcasting homebrew applications ### Things that will never be implemented diff --git a/app/Makefile b/app/Makefile index 6b5351a..8e27257 100644 --- a/app/Makefile +++ b/app/Makefile @@ -25,7 +25,7 @@ LIBRARY_DIRS := $(PORTLIBS) $(CTRULIB) $(DEVKITPRO)/libcwav $(DEVKITPRO)/libncsn LIBRARIES := ctru VERSION_MAJOR := 1 -VERSION_MINOR := 1 +VERSION_MINOR := 2 VERSION_MICRO := 0 BUILD_FLAGS := -march=armv6k -mtune=mpcore -mfloat-abi=hard diff --git a/plugin/Makefile b/plugin/Makefile index fc7103c..ca6781f 100644 --- a/plugin/Makefile +++ b/plugin/Makefile @@ -15,7 +15,7 @@ INCLUDES := includes SOURCES := sources sources/CTRPluginFramework VERSION_MAJOR := 1 -VERSION_MINOR := 1 +VERSION_MINOR := 2 VERSION_REVISION := 0 SERVER_PORT := 5543 diff --git a/plugin/includes/ArticBaseFunctions.hpp b/plugin/includes/ArticBaseFunctions.hpp index f19fcec..91911e8 100644 --- a/plugin/includes/ArticBaseFunctions.hpp +++ b/plugin/includes/ArticBaseFunctions.hpp @@ -16,4 +16,13 @@ namespace ArticBaseFunctions { DIR, ARCHIVE }; + + // Controller_Start + namespace ArticController { + extern Thread thread; + extern bool thread_run; + extern int socket_fd; + extern volatile bool socket_ready; + void Handler(void* arg); + }; }; \ No newline at end of file diff --git a/plugin/includes/ArticBaseServer.hpp b/plugin/includes/ArticBaseServer.hpp index 0267ae9..f9cd136 100644 --- a/plugin/includes/ArticBaseServer.hpp +++ b/plugin/includes/ArticBaseServer.hpp @@ -13,15 +13,16 @@ public: void Serve(); void QueryStop(); - static bool SetNonBlock(int sockFD, bool blocking); + static bool SetNonBlock(int sockFD, bool nonBlocking); + static bool Read(int& sockFD, void* buffer, size_t size); + static bool Write(int& sockFD, void* buffer, size_t size); + static size_t RecvFrom(int& sockFD, void* buffer, size_t size, void* addr, void* addr_size); + static size_t SendTo(int& sockFD, void* buffer, size_t size, void* addr, void* addr_size); private: void Stop(); static constexpr size_t MAX_WORK_BUF_SIZE = 4 * 1024 * 1024 + 512 * 1024; // 4.5MB static constexpr size_t MAX_PARAM_AMOUNT = 10; - static bool Read(int& sockFD, void* buffer, size_t size); - static bool Write(int& sockFD, void* buffer, size_t size); - class RequestHandler { public: RequestHandler(ArticBaseServer* serv, int id); @@ -45,7 +46,7 @@ private: std::vector reqParameters; }; - static constexpr const char* VERSION = "1"; + static constexpr const char* VERSION = "2"; int socketFd; bool run = true; diff --git a/plugin/includes/hidExtension.hpp b/plugin/includes/hidExtension.hpp new file mode 100644 index 0000000..c473a44 --- /dev/null +++ b/plugin/includes/hidExtension.hpp @@ -0,0 +1,12 @@ +#pragma once +#include "3ds.h" + +struct GyroscopeCalibrateParam { + struct { + s16 zero_point; + s16 positive_unit_point; + s16 negative_unit_point; + } x, y, z; +}; + +Result HIDUSER_GetGyroscopeCalibrateParam(GyroscopeCalibrateParam* calibrateParam); \ No newline at end of file diff --git a/plugin/sources/ArticBaseFunctions.cpp b/plugin/sources/ArticBaseFunctions.cpp index c588594..ef92b41 100644 --- a/plugin/sources/ArticBaseFunctions.cpp +++ b/plugin/sources/ArticBaseFunctions.cpp @@ -1,8 +1,18 @@ +#include +#include +#include +#include +#include + #include "ArticBaseFunctions.hpp" #include "Main.hpp" #include "amExtension.hpp" #include "fsExtension.hpp" +#include "hidExtension.hpp" #include "CTRPluginFramework/CTRPluginFramework.hpp" +#include "CTRPluginFramework/Clock.hpp" + +extern bool isControllerMode; namespace ArticBaseFunctions { @@ -1547,9 +1557,224 @@ namespace ArticBaseFunctions { mi.FinishGood(res); } + void ArticController::Handler(void* arg) { + using namespace ArticController; + + struct ControllerPacket { + u32 id; + u32 pad; + circlePosition c_pad; + touchPosition touch; + circlePosition c_stick; + accelVector accel; + angularRate gyro; + } packet; + u32 current_id = 0; + static_assert(sizeof(packet) == 0x20, "ControllerPacket invalid size"); + + constexpr int port = SERVER_PORT + 10; + struct sockaddr_in addr = {0}; + socklen_t addr_size = static_cast(sizeof(addr)); + int res, failedCount = 0; + + socket_fd = socket(AF_INET, SOCK_DGRAM, 0); + if (socket_fd < 0) { + logger.Error("ArticController: Cannot create socket"); + socket_ready = true; + threadExit(1); + return; + } + + if (!ArticBaseServer::SetNonBlock(socket_fd, true)) { + logger.Error("ArticController: Cannot set non-block"); + close(socket_fd); + socket_fd = -1; + socket_ready = true; + threadExit(1); + return; + } + + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(port); + res = bind(socket_fd, (struct sockaddr *)&addr, addr_size); + if (res < 0) { + logger.Error("ArticController: Failed to bind() to port %d", port); + close(socket_fd); + socket_fd = -1; + socket_ready = true; + threadExit(1); + return; + } + + socket_ready = true; + + u8 a; + size_t transfered = ArticBaseServer::RecvFrom(socket_fd, &a, sizeof(a), &addr, &addr_size); + if (transfered > 0) { + logger.Debug("ArticController: Started"); + } else { + logger.Error("ArticController: Error reading from socket"); + close(socket_fd); + socket_fd = -1; + threadExit(1); + return; + } + + constexpr CTRPluginFramework::Time INTERVAL = CTRPluginFramework::Milliseconds(2); + // hid scan input is done on the main thread, no need to do it here. + while (thread_run) { + CTRPluginFramework::Clock clock; + + if (isControllerMode) { + packet.id = current_id++; + packet.pad = hidKeysHeld(); + hidCircleRead(&packet.c_pad); + hidTouchRead(&packet.touch); + irrstCstickRead(&packet.c_stick); + hidAccelRead(&packet.accel); + hidGyroRead(&packet.gyro); + + if (ArticBaseServer::SendTo(socket_fd, &packet, sizeof(packet), &addr, &addr_size) <= 0) { + if (failedCount++ >= 1000) { + logger.Error("ArticController: Error writing to socket"); + break; + } + } else { + failedCount = 0; + } + } + + CTRPluginFramework::Time elapsed = clock.GetElapsedTime(); + if (elapsed < INTERVAL) { + svcSleepThread((INTERVAL - elapsed).AsMicroseconds() * 1000); + } + } + + close(socket_fd); + socket_fd = -1; + threadExit(1); + } + + static bool stopController(void); + void Controller_Start(ArticBaseServer::MethodInterface& mi) { + bool good = true; + + if (good) good = mi.FinishInputParameters(); + + if (!good) return; + + constexpr int port = SERVER_PORT + 10; + ArticBaseCommon::Buffer* conf_buf = mi.ReserveResultBuffer(0, sizeof(int)); + if (!conf_buf) { + return; + } + *(int*)conf_buf->data = port; + + stopController(); + + s32 prio = 0; + svcGetThreadPriority(&prio, CUR_THREAD_HANDLE); + ArticController::socket_ready = false; + ArticController::thread_run = true; + ArticController::thread = threadCreate(ArticController::Handler, nullptr, 0x800, prio + 1, -2, false); + logger.Debug("ArticController: Starting..."); + isControllerMode = true; + + while (!ArticController::socket_ready) { + svcSleepThread(1000000); + } + ArticController::socket_ready = false; + + mi.FinishGood(0); + } + + void HIDUSER_EnableAccelerometer_(ArticBaseServer::MethodInterface& mi) { + bool good = true; + + if (good) mi.FinishInputParameters(); + + Result res = HIDUSER_EnableAccelerometer(); + + mi.FinishGood(res); + } + + void HIDUSER_DisableAccelerometer_(ArticBaseServer::MethodInterface& mi) { + bool good = true; + + if (good) mi.FinishInputParameters(); + + Result res = HIDUSER_DisableAccelerometer(); + + mi.FinishGood(res); + } + + void HIDUSER_EnableGyroscope_(ArticBaseServer::MethodInterface& mi) { + bool good = true; + + if (good) mi.FinishInputParameters(); + + Result res = HIDUSER_EnableGyroscope(); + + mi.FinishGood(res); + } + + void HIDUSER_DisableGyroscope_(ArticBaseServer::MethodInterface& mi) { + bool good = true; + + if (good) mi.FinishInputParameters(); + + Result res = HIDUSER_DisableGyroscope(); + + mi.FinishGood(res); + } + + void HIDUSER_GetGyroscopeRawToDpsCoefficient_(ArticBaseServer::MethodInterface& mi) { + bool good = true; + + if (good) mi.FinishInputParameters(); + + float coef; + Result res = HIDUSER_GetGyroscopeRawToDpsCoefficient(&coef); + if (R_FAILED(res)) { + mi.FinishGood(res); + return; + } + + ArticBaseCommon::Buffer* coef_buf = mi.ReserveResultBuffer(0, sizeof(float)); + if (!coef_buf) { + return; + } + *(float*)coef_buf->data = coef; + + mi.FinishGood(res); + } + + + void HIDUSER_GetGyroscopeCalibrateParam_(ArticBaseServer::MethodInterface& mi) { + bool good = true; + + if (good) mi.FinishInputParameters(); + + GyroscopeCalibrateParam param = { 0 }; + Result res = HIDUSER_GetGyroscopeCalibrateParam(¶m); + if (R_FAILED(res)) { + mi.FinishGood(res); + return; + } + + ArticBaseCommon::Buffer* coef_buf = mi.ReserveResultBuffer(0, sizeof(param)); + if (!coef_buf) { + return; + } + *(GyroscopeCalibrateParam*)coef_buf->data = param; + + mi.FinishGood(res); + } + template constexpr auto& METHOD_NAME(char const (&s)[N]) { - static_assert(N < sizeof(ArticBaseCommon::RequestPacket::method), "String exceeds 10 bytes!"); + static_assert(N < sizeof(ArticBaseCommon::RequestPacket::method), "String exceeds 32 bytes!"); return s; } @@ -1607,6 +1832,15 @@ namespace ArticBaseFunctions { {METHOD_NAME("AMAPP_ListDataTitleTicketInfos"), AMAPP_ListDataTitleTicketInfos_}, {METHOD_NAME("AMAPP_GetPatchTitleInfos"), AMAPP_GetPatchTitleInfos_}, {METHOD_NAME("CFGU_GetConfigInfoBlk2"), CFGU_GetConfigInfoBlk2_}, + {METHOD_NAME("HIDUSER_EnableAccelerometer"), HIDUSER_EnableAccelerometer_}, + {METHOD_NAME("HIDUSER_DisableAccelerometer"), HIDUSER_DisableAccelerometer_}, + {METHOD_NAME("HIDUSER_EnableGyroscope"), HIDUSER_EnableGyroscope_}, + {METHOD_NAME("HIDUSER_DisableGyroscope"), HIDUSER_DisableGyroscope_}, + {METHOD_NAME("HIDUSER_GetGyroRawToDpsCoef"), HIDUSER_GetGyroscopeRawToDpsCoefficient_}, + {METHOD_NAME("HIDUSER_GetGyroCalibrateParam"), HIDUSER_GetGyroscopeCalibrateParam_}, + + // UDP Streams + {METHOD_NAME("#ArticController"), Controller_Start}, }; bool obtainExheader() { @@ -1654,11 +1888,33 @@ namespace ArticBaseFunctions { return true; } + static bool stopController(void) { + if (ArticController::thread_run) { + logger.Debug("ArticController: Stopping..."); + ArticController::thread_run = false; + if (ArticController::socket_fd != -1) { + close(ArticController::socket_fd); + ArticController::socket_fd = -1; + } + threadJoin(ArticController::thread, U64_MAX); + threadFree(ArticController::thread); + ArticController::thread = nullptr; + logger.Debug("ArticController: Stopped..."); + } + return true; + } + std::vector setupFunctions { obtainExheader, }; std::vector destructFunctions { closeHandles, + stopController, }; + + Thread ArticController::thread = nullptr; + bool ArticController::thread_run = false; + int ArticController::socket_fd = -1; + volatile bool ArticController::socket_ready = false; } diff --git a/plugin/sources/ArticBaseServer.cpp b/plugin/sources/ArticBaseServer.cpp index 50871ea..a9a89d2 100644 --- a/plugin/sources/ArticBaseServer.cpp +++ b/plugin/sources/ArticBaseServer.cpp @@ -221,6 +221,37 @@ bool ArticBaseServer::Write(int& sockFD, void* buffer, size_t size) { return write_bytes == size; } +size_t ArticBaseServer::RecvFrom(int& sockFD, void* buffer, size_t size, void* addr, void* addr_size) { + while (true) { + int new_read = recvfrom(sockFD, buffer, size, 0, (sockaddr*)addr, (socklen_t*)addr_size); + if (new_read < 0) { + if (errno == EWOULDBLOCK) { + svcSleepThread(1000000); + continue; + } + return 0; + } + transferedBytes += new_read; + return new_read; + } +} + +size_t ArticBaseServer::SendTo(int& sockFD, void* buffer, size_t size, void* addr, void* addr_size) { + socklen_t addr_len = *(socklen_t*)addr_size; + while (true) { + int new_written = sendto(sockFD, buffer, size, 0, (sockaddr*)addr, addr_len); + if (new_written < 0) { + if (errno == EWOULDBLOCK) { + svcSleepThread(1000000); + continue; + } + return 0; + } + transferedBytes += new_written; + return new_written; + } +} + ArticBaseServer::RequestHandler::RequestHandler(ArticBaseServer* serv, int id) { server = serv; this->id = id; diff --git a/plugin/sources/Logger.cpp b/plugin/sources/Logger.cpp index 5daaf07..b515ac0 100644 --- a/plugin/sources/Logger.cpp +++ b/plugin/sources/Logger.cpp @@ -141,7 +141,10 @@ void Logger::Handler() { switch (log.type) { case PendingLog::Type::RAW: + back = bottomScreenConsole.cursorY; + bottomScreenConsole.cursorY = 0; printf("%s\n", log.string.c_str()); + bottomScreenConsole.cursorY = back; break; case PendingLog::Type::DEBUG: printf("[D] %s\n", log.string.c_str()); diff --git a/plugin/sources/hidExtension.cpp b/plugin/sources/hidExtension.cpp new file mode 100644 index 0000000..2233332 --- /dev/null +++ b/plugin/sources/hidExtension.cpp @@ -0,0 +1,26 @@ +#include "hidExtension.hpp" +#include +#include <3ds/types.h> +#include <3ds/result.h> +#include <3ds/svc.h> +#include <3ds/srv.h> +#include <3ds/synchronization.h> +#include <3ds/services/fs.h> +#include <3ds/ipc.h> +#include <3ds/env.h> + +extern Handle hidHandle; + +Result HIDUSER_GetGyroscopeCalibrateParam(GyroscopeCalibrateParam* calibrateParam) { + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x16,0,0); + + Result ret = 0; + if(R_FAILED(ret = svcSendSyncRequest(hidHandle))) return ret; + + if (R_SUCCEEDED(cmdbuf[1])) { + memcpy(calibrateParam, &cmdbuf[2], sizeof(GyroscopeCalibrateParam)); + } + return cmdbuf[1]; +} \ No newline at end of file diff --git a/plugin/sources/main.cpp b/plugin/sources/main.cpp index 6e8d718..6964cf7 100644 --- a/plugin/sources/main.cpp +++ b/plugin/sources/main.cpp @@ -30,6 +30,12 @@ static bool should_run = true; static int listen_fd = -1; static int accept_fd = -1; static ArticBaseServer* articBase = nullptr; +static bool wasControllerMode = false; +static bool everControllerMode = false; +static bool reloadBottomText = false; + +int transferedBytes = 0; +bool isControllerMode = false; extern "C" { #include "csvc.h" @@ -159,13 +165,15 @@ void Start(void* arg) { for (auto it = ArticBaseFunctions::destructFunctions.begin(); it != ArticBaseFunctions::destructFunctions.end(); it++) { (*it)(); } + isControllerMode = false; + everControllerMode = false; } socExit(); free(SOC_buffer); } PrintConsole topScreenConsole, bottomScreenConsole; -int transferedBytes = 0; + void Main() { logger.Start(); @@ -195,13 +203,39 @@ void Main() { { CTRPluginFramework::BCLIM((void*)__data_logo_bin, __data_logo_bin_size).Render(CTRPluginFramework::Rect((320 - 128) / 2, (240 - 128) / 2, 128, 128)); } - logger.Raw(false, "\n ArticBase v%d.%d.%d\n", VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION); - logger.Raw(false, " Press A to enter sleep."); - logger.Raw(false, " Press X to show debug log."); - logger.Raw(false, " Press Y to restart server."); - logger.Raw(false, " Press START to exit."); + auto print_bottom_info = [](bool controller_mode) { + if (controller_mode) { + logger.Raw(false, "\n ArticBase v%d.%d.%d\n\n" + " Artic Controller enabled \n" + " \n" + " - X + START + SELECT: Exit \n" + " Artic Controller mode \n" + " " + , VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION); + } else { + if (everControllerMode) { + logger.Raw(false, "\n ArticBase v%d.%d.%d\n\n" + " - A: Enter sleep \n" + " - X: Show debug log \n" + " - Y: Restart server \n" + " - START: Exit \n" + " - SELECT: Enter Artic controller mode " + , VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION); + } else { + logger.Raw(false, "\n ArticBase v%d.%d.%d\n\n" + " - A: Enter sleep \n" + " - X: Show debug log \n" + " - Y: Restart server \n" + " - START: Exit \n" + " " + , VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION); + } + + } + }; + print_bottom_info(false); logger.Raw(true, ""); bool setupCorrect = true; @@ -225,29 +259,44 @@ void Main() { while (aptMainLoop()) { - //Scan all the inputs. This should be done once for each frame + if (isControllerMode != wasControllerMode || reloadBottomText) { + reloadBottomText = false; + everControllerMode |= isControllerMode; + if (isControllerMode) { + logger.Info("Server: Controller mode enabled"); + } else { + logger.Info("Server: Controller mode disabled"); + } + print_bottom_info(isControllerMode); + wasControllerMode = isControllerMode; + } + hidScanInput(); - //hidKeysDown returns information about which buttons have been just pressed (and they weren't in the previous frame) u32 kDown = hidKeysDown(); + u32 kHeld = hidKeysHeld(); - if ((kDown != 0) && sleeping) { + if (((kDown & ~KEY_SELECT) != 0) && sleeping && !isControllerMode) { sleeping = false; GSPLCD_PowerOnBacklight(3); continue; } - if ((kDown & KEY_A) && !sleeping) { + if ((kDown & KEY_A) && !sleeping && !isControllerMode) { sleeping = true; GSPLCD_PowerOffBacklight(3); } - if (kDown & KEY_START) { + if ((kDown & KEY_START) && !isControllerMode) { logger.Info("Server: Exiting"); break; } - if (kDown & KEY_Y) { + if ((kDown & KEY_SELECT) && !isControllerMode && everControllerMode) { + isControllerMode = true; + } + + if ((kDown & KEY_Y) && !isControllerMode) { if (articBase) { logger.Info("Server: Restarting"); articBase->QueryStop(); @@ -256,7 +305,7 @@ void Main() { } } - if (kDown & KEY_X) { + if ((kDown & KEY_X) && !isControllerMode) { if (logger.debug_enable) { logger.Info("Server: Debug log disabled"); logger.debug_enable = false; @@ -266,6 +315,10 @@ void Main() { } } + if (((kHeld & (KEY_X | KEY_START | KEY_SELECT)) == (KEY_X | KEY_START | KEY_SELECT)) && isControllerMode) { + isControllerMode = false; + } + if (clock.HasTimePassed(CTRPluginFramework::Seconds(1))) { if (articBase) {