Input: UDP Client to provide motion and touch controls

An implementation of the cemuhook motion/touch protocol, this adds the
ability for users to connect several different devices to citra to send
direct motion and touch data to citra.
This commit is contained in:
James Rowe 2018-01-18 08:43:17 -07:00
parent 1e724b046b
commit 6bcbda5ab2
16 changed files with 758 additions and 14 deletions

View file

@ -363,6 +363,13 @@ function(get_timestamp _var)
set(${_var} "${timestamp}" PARENT_SCOPE) set(${_var} "${timestamp}" PARENT_SCOPE)
endfunction() endfunction()
# Prevent boost from linking against libs when building
add_definitions(-DBOOST_ERROR_CODE_HEADER_ONLY
-DBOOST_SYSTEM_NO_LIB
-DBOOST_DATE_TIME_NO_LIB
-DBOOST_REGEX_NO_LIB
)
# generate git/build information # generate git/build information
include(GetGitRevisionDescription) include(GetGitRevisionDescription)
get_git_head_revision(GIT_REF_SPEC GIT_REV) get_git_head_revision(GIT_REF_SPEC GIT_REV)

2
externals/boost vendored

@ -1 +1 @@
Subproject commit 7310c95e2320ed262b0600315fa2f762c4b6cc54 Subproject commit 502437b2ae3f1da821aa7d5d5174ec356fa89269

View file

@ -12,6 +12,7 @@
#include "common/param_package.h" #include "common/param_package.h"
#include "core/settings.h" #include "core/settings.h"
#include "input_common/main.h" #include "input_common/main.h"
#include "input_common/udp/client.h"
Config::Config() { Config::Config() {
// TODO: Don't hardcode the path; let the frontend decide where to put the config files. // TODO: Don't hardcode the path; let the frontend decide where to put the config files.
@ -89,6 +90,10 @@ void Config::ReadValues() {
"engine:motion_emu,update_period:100,sensitivity:0.01,tilt_clamp:90.0"); "engine:motion_emu,update_period:100,sensitivity:0.01,tilt_clamp:90.0");
Settings::values.touch_device = Settings::values.touch_device =
sdl2_config->Get("Controls", "touch_device", "engine:emu_window"); sdl2_config->Get("Controls", "touch_device", "engine:emu_window");
Settings::values.udp_input_address =
sdl2_config->Get("Controls", "udp_input_address", InputCommon::CemuhookUDP::DEFAULT_ADDR);
Settings::values.udp_input_port = static_cast<u16>(sdl2_config->GetInteger(
"Controls", "udp_input_port", InputCommon::CemuhookUDP::DEFAULT_PORT));
// Core // Core
Settings::values.use_cpu_jit = sdl2_config->GetBoolean("Core", "use_cpu_jit", true); Settings::values.use_cpu_jit = sdl2_config->GetBoolean("Core", "use_cpu_jit", true);

View file

@ -61,12 +61,26 @@ c_stick=
# - "update_period": update period in milliseconds (default to 100) # - "update_period": update period in milliseconds (default to 100)
# - "sensitivity": the coefficient converting mouse movement to tilting angle (default to 0.01) # - "sensitivity": the coefficient converting mouse movement to tilting angle (default to 0.01)
# - "tilt_clamp": the max value of the tilt angle in degrees (default to 90) # - "tilt_clamp": the max value of the tilt angle in degrees (default to 90)
# - "cemuhookudp" reads motion input from a udp server that uses cemuhook's udp protocol
motion_device= motion_device=
# for touch input, the following devices are available: # for touch input, the following devices are available:
# - "emu_window" (default) for emulating touch input from mouse input to the emulation window. No parameters required # - "emu_window" (default) for emulating touch input from mouse input to the emulation window. No parameters required
# - "cemuhookudp" reads touch input from a udp server that uses cemuhook's udp protocol
# - "min_x", "min_y", "max_x", "max_y": defines the udp device's touch screen coordinate system
touch_device= touch_device=
# Most desktop operating systems do not expose a way to poll the motion state of the controllers
# so as a way around it, cemuhook created a udp client/server protocol to broadcast the data directly
# from a controller device to the client program. Citra has a client that can connect and read
# from any cemuhook compatible motion program.
# IPv4 address of the udp input server (Default "127.0.0.1")
udp_input_address=
# Port of the udp input server. (Default 26760)
udp_input_port=
[Core] [Core]
# Whether to use the Just-In-Time (JIT) compiler for CPU emulation # Whether to use the Just-In-Time (JIT) compiler for CPU emulation
# 0: Interpreter (slow), 1 (default): JIT (fast) # 0: Interpreter (slow), 1 (default): JIT (fast)

View file

@ -7,6 +7,7 @@
#include "citra_qt/ui_settings.h" #include "citra_qt/ui_settings.h"
#include "common/file_util.h" #include "common/file_util.h"
#include "input_common/main.h" #include "input_common/main.h"
#include "input_common/udp/client.h"
#include "network/network.h" #include "network/network.h"
Config::Config() { Config::Config() {
@ -72,6 +73,13 @@ void Config::ReadValues() {
Settings::values.touch_device = Settings::values.touch_device =
ReadSetting("touch_device", "engine:emu_window").toString().toStdString(); ReadSetting("touch_device", "engine:emu_window").toString().toStdString();
Settings::values.udp_input_address =
ReadSetting("udp_input_address", InputCommon::CemuhookUDP::DEFAULT_ADDR)
.toString()
.toStdString();
Settings::values.udp_input_port = static_cast<u16>(
ReadSetting("udp_input_port", InputCommon::CemuhookUDP::DEFAULT_PORT).toInt());
qt_config->endGroup(); qt_config->endGroup();
qt_config->beginGroup("Core"); qt_config->beginGroup("Core");
@ -310,6 +318,10 @@ void Config::SaveValues() {
"engine:motion_emu,update_period:100,sensitivity:0.01,tilt_clamp:90.0"); "engine:motion_emu,update_period:100,sensitivity:0.01,tilt_clamp:90.0");
WriteSetting("touch_device", QString::fromStdString(Settings::values.touch_device), WriteSetting("touch_device", QString::fromStdString(Settings::values.touch_device),
"engine:emu_window"); "engine:emu_window");
WriteSetting("udp_input_address", QString::fromStdString(Settings::values.udp_input_address),
InputCommon::CemuhookUDP::DEFAULT_ADDR);
WriteSetting("udp_input_port", Settings::values.udp_input_port,
InputCommon::CemuhookUDP::DEFAULT_PORT);
qt_config->endGroup(); qt_config->endGroup();
qt_config->beginGroup("Core"); qt_config->beginGroup("Core");

View file

@ -95,6 +95,8 @@ struct Values {
std::array<std::string, NativeAnalog::NumAnalogs> analogs; std::array<std::string, NativeAnalog::NumAnalogs> analogs;
std::string motion_device; std::string motion_device;
std::string touch_device; std::string touch_device;
std::string udp_input_address;
u16 udp_input_port;
// Core // Core
bool use_cpu_jit; bool use_cpu_jit;

View file

@ -7,13 +7,18 @@ add_library(input_common STATIC
main.h main.h
motion_emu.cpp motion_emu.cpp
motion_emu.h motion_emu.h
udp/client.cpp
udp/client.h
udp/protocol.cpp
udp/protocol.h
udp/udp.cpp
udp/udp.h
$<$<BOOL:${SDL2_FOUND}>:sdl/sdl.cpp sdl/sdl.h> $<$<BOOL:${SDL2_FOUND}>:sdl/sdl.cpp sdl/sdl.h>
) )
create_target_directory_groups(input_common) create_target_directory_groups(input_common)
target_link_libraries(input_common PUBLIC core PRIVATE common ${Boost_LIBRARIES})
target_link_libraries(input_common PUBLIC core PRIVATE common)
if(SDL2_FOUND) if(SDL2_FOUND)
target_link_libraries(input_common PRIVATE SDL2) target_link_libraries(input_common PRIVATE SDL2)

View file

@ -8,6 +8,7 @@
#include "input_common/keyboard.h" #include "input_common/keyboard.h"
#include "input_common/main.h" #include "input_common/main.h"
#include "input_common/motion_emu.h" #include "input_common/motion_emu.h"
#include "input_common/udp/udp.h"
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
#include "input_common/sdl/sdl.h" #include "input_common/sdl/sdl.h"
#endif #endif
@ -16,6 +17,7 @@ namespace InputCommon {
static std::shared_ptr<Keyboard> keyboard; static std::shared_ptr<Keyboard> keyboard;
static std::shared_ptr<MotionEmu> motion_emu; static std::shared_ptr<MotionEmu> motion_emu;
static std::unique_ptr<CemuhookUDP::State> udp;
void Init() { void Init() {
keyboard = std::make_shared<Keyboard>(); keyboard = std::make_shared<Keyboard>();
@ -28,6 +30,8 @@ void Init() {
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
SDL::Init(); SDL::Init();
#endif #endif
udp = CemuhookUDP::Init();
} }
void Shutdown() { void Shutdown() {
@ -75,11 +79,13 @@ std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left,
namespace Polling { namespace Polling {
std::vector<std::unique_ptr<DevicePoller>> GetPollers(DeviceType type) { std::vector<std::unique_ptr<DevicePoller>> GetPollers(DeviceType type) {
std::vector<std::unique_ptr<DevicePoller>> pollers;
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
return SDL::Polling::GetPollers(type); SDL::Polling::GetPollers(type, pollers);
#else
return {};
#endif #endif
return pollers;
} }
} // namespace Polling } // namespace Polling

View file

@ -414,18 +414,16 @@ private:
SDL_JoystickID analog_axes_joystick = -1; SDL_JoystickID analog_axes_joystick = -1;
}; };
std::vector<std::unique_ptr<InputCommon::Polling::DevicePoller>> GetPollers( void GetPollers(InputCommon::Polling::DeviceType type,
InputCommon::Polling::DeviceType type) { std::vector<std::unique_ptr<InputCommon::Polling::DevicePoller>>& pollers) {
std::vector<std::unique_ptr<InputCommon::Polling::DevicePoller>> pollers;
switch (type) { switch (type) {
case InputCommon::Polling::DeviceType::Analog: case InputCommon::Polling::DeviceType::Analog:
pollers.push_back(std::make_unique<SDLAnalogPoller>()); pollers.emplace_back(std::make_unique<SDLAnalogPoller>());
break; break;
case InputCommon::Polling::DeviceType::Button: case InputCommon::Polling::DeviceType::Button:
pollers.push_back(std::make_unique<SDLButtonPoller>()); pollers.emplace_back(std::make_unique<SDLButtonPoller>());
break; break;
} }
return pollers;
} }
} // namespace Polling } // namespace Polling
} // namespace SDL } // namespace SDL

View file

@ -34,8 +34,8 @@ Common::ParamPackage SDLEventToButtonParamPackage(const SDL_Event& event);
namespace Polling { namespace Polling {
/// Get all DevicePoller that use the SDL backend for a specific device type /// Get all DevicePoller that use the SDL backend for a specific device type
std::vector<std::unique_ptr<InputCommon::Polling::DevicePoller>> GetPollers( void GetPollers(InputCommon::Polling::DeviceType type,
InputCommon::Polling::DeviceType type); std::vector<std::unique_ptr<InputCommon::Polling::DevicePoller>>& pollers);
} // namespace Polling } // namespace Polling
} // namespace SDL } // namespace SDL

View file

@ -0,0 +1,190 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <array>
#include <chrono>
#include <cstring>
#include <functional>
#include <thread>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include "common/logging/log.h"
#include "common/vector_math.h"
#include "input_common/udp/client.h"
#include "input_common/udp/protocol.h"
using boost::asio::ip::address_v4;
using boost::asio::ip::udp;
namespace InputCommon::CemuhookUDP {
struct SocketCallback {
std::function<void(Response::Version)> version;
std::function<void(Response::PortInfo)> port_info;
std::function<void(Response::PadData)> pad_data;
};
class Socket {
public:
using clock = std::chrono::system_clock;
explicit Socket(const std::string& host, u16 port, u32 client_id, SocketCallback callback)
: client_id(client_id), timer(io_service),
send_endpoint(udp::endpoint(address_v4::from_string(host), port)),
socket(io_service, udp::endpoint(udp::v4(), 0)), callback(std::move(callback)) {}
void Stop() {
io_service.stop();
}
void Loop() {
io_service.run();
}
void StartSend(const clock::time_point& from) {
timer.expires_at(from + std::chrono::seconds(3));
timer.async_wait([this](const boost::system::error_code& error) { HandleSend(error); });
}
void StartReceive() {
socket.async_receive_from(
boost::asio::buffer(receive_buffer), receive_endpoint,
[this](const boost::system::error_code& error, std::size_t bytes_transferred) {
HandleReceive(error, bytes_transferred);
});
}
private:
void HandleReceive(const boost::system::error_code& error, std::size_t bytes_transferred) {
if (auto type = Response::Validate(receive_buffer.data(), bytes_transferred)) {
switch (*type) {
case Type::Version: {
Response::Version version;
std::memcpy(&version, &receive_buffer[sizeof(Header)], sizeof(Response::Version));
callback.version(std::move(version));
break;
}
case Type::PortInfo: {
Response::PortInfo port_info;
std::memcpy(&port_info, &receive_buffer[sizeof(Header)],
sizeof(Response::PortInfo));
callback.port_info(std::move(port_info));
break;
}
case Type::PadData: {
Response::PadData pad_data;
std::memcpy(&pad_data, &receive_buffer[sizeof(Header)], sizeof(Response::PadData));
callback.pad_data(std::move(pad_data));
break;
}
}
}
StartReceive();
}
void HandleSend(const boost::system::error_code& error) {
// TODO: add something to the UI to let people choose what ports to listen on
// Send a request for getting port info for pad 1
Request::PortInfo port_info{1, {0, 0, 0, 0}};
auto port_message = Request::Create(port_info, client_id);
std::memcpy(&send_buffer1, &port_message, PORT_INFO_SIZE);
size_t len = socket.send_to(boost::asio::buffer(send_buffer1), send_endpoint);
// Send a request for getting pad data for pad 1
Request::PadData pad_data{Request::PadData::Flags::Id, 0, EMPTY_MAC_ADDRESS};
auto pad_message = Request::Create(pad_data, client_id);
std::memcpy(send_buffer2.data(), &pad_message, PAD_DATA_SIZE);
size_t len2 = socket.send_to(boost::asio::buffer(send_buffer2), send_endpoint);
StartSend(timer.expiry());
}
SocketCallback callback;
boost::asio::io_service io_service;
boost::asio::basic_waitable_timer<clock> timer;
udp::socket socket;
u32 client_id;
static constexpr size_t PORT_INFO_SIZE = sizeof(Message<Request::PortInfo>);
static constexpr size_t PAD_DATA_SIZE = sizeof(Message<Request::PadData>);
std::array<u8, PORT_INFO_SIZE> send_buffer1;
std::array<u8, PAD_DATA_SIZE> send_buffer2;
udp::endpoint send_endpoint;
std::array<u8, MAX_PACKET_SIZE> receive_buffer;
udp::endpoint receive_endpoint;
};
static void SocketLoop(Socket* socket) {
socket->StartReceive();
socket->StartSend(Socket::clock::now());
socket->Loop();
}
Client::Client(std::shared_ptr<DeviceStatus> status, const std::string& host, u16 port,
u32 client_id)
: status(status) {
SocketCallback callback{[this](Response::Version version) { OnVersion(version); },
[this](Response::PortInfo info) { OnPortInfo(info); },
[this](Response::PadData data) { OnPadData(data); }};
LOG_INFO(Input, "Starting communication with UDP input server on {}:{}", host, port);
socket = std::make_unique<Socket>(host, port, client_id, callback);
thread = std::thread{SocketLoop, this->socket.get()};
}
Client::~Client() {
socket->Stop();
thread.join();
}
void Client::OnVersion(Response::Version data) {
LOG_TRACE(Input, "Version packet received: {}", data.version);
}
void Client::OnPortInfo(Response::PortInfo data) {
LOG_TRACE(Input, "PortInfo packet received: {}", data.model);
}
void Client::OnPadData(Response::PadData data) {
LOG_TRACE(Input, "PadData packet received");
if (data.packet_counter <= packet_sequence) {
LOG_WARNING(
Input,
"PadData packet dropped because its stale info. Current count: {} Packet count: {}",
packet_sequence, data.packet_counter);
return;
}
packet_sequence = data.packet_counter;
Math::Vec3f accel = Math::MakeVec<float>(data.accel.x, data.accel.y, data.accel.z);
Math::Vec3f gyro = Math::MakeVec<float>(data.gyro.pitch, data.gyro.yaw, data.gyro.roll);
{
std::lock_guard<std::mutex> guard(status->update_mutex);
status->motion_status = {accel, gyro};
// TODO: add a setting for "click" touch. Click touch refers to a device that differentiates
// between a simple "tap" and a hard press that causes the touch screen to click.
bool is_active = data.touch_1.is_active != 0;
float x = 0;
float y = 0;
if (is_active && status->touch_calibration) {
u16 min_x = status->touch_calibration->min_x;
u16 max_x = status->touch_calibration->max_x;
u16 min_y = status->touch_calibration->min_y;
u16 max_y = status->touch_calibration->max_y;
x = (std::clamp(static_cast<u16>(data.touch_1.x), min_x, max_x) - min_x) /
static_cast<float>(max_x - min_x);
y = (std::clamp(static_cast<u16>(data.touch_1.y), min_y, max_y) - min_y) /
static_cast<float>(max_y - min_y);
}
status->touch_status = {x, y, is_active};
}
}
} // namespace InputCommon::CemuhookUDP

View file

@ -0,0 +1,61 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <tuple>
#include <vector>
#include <boost/optional.hpp>
#include "common/common_types.h"
#include "common/vector_math.h"
namespace InputCommon::CemuhookUDP {
static constexpr u16 DEFAULT_PORT = 26760;
static constexpr const char* DEFAULT_ADDR = "127.0.0.1";
class Socket;
namespace Response {
struct PadData;
struct PortInfo;
struct Version;
} // namespace Response
struct DeviceStatus {
std::mutex update_mutex;
std::tuple<Math::Vec3<float>, Math::Vec3<float>> motion_status;
std::tuple<float, float, bool> touch_status;
// calibration data for scaling the device's touch area to 3ds
struct CalibrationData {
u16 min_x;
u16 min_y;
u16 max_x;
u16 max_y;
};
boost::optional<CalibrationData> touch_calibration;
};
class Client {
public:
explicit Client(std::shared_ptr<DeviceStatus> status, const std::string& host = DEFAULT_ADDR,
u16 port = DEFAULT_PORT, u32 client_id = 24872);
~Client();
private:
void OnVersion(Response::Version);
void OnPortInfo(Response::PortInfo);
void OnPadData(Response::PadData);
std::unique_ptr<Socket> socket;
std::shared_ptr<DeviceStatus> status;
std::thread thread;
u64 packet_sequence = 0;
};
} // namespace InputCommon::CemuhookUDP

View file

@ -0,0 +1,79 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <cstddef>
#include <cstring>
#include "common/logging/log.h"
#include "input_common/udp/protocol.h"
namespace InputCommon::CemuhookUDP {
static const size_t GetSizeOfResponseType(Type t) {
switch (t) {
case Type::Version:
return sizeof(Response::Version);
case Type::PortInfo:
return sizeof(Response::PortInfo);
case Type::PadData:
return sizeof(Response::PadData);
}
return 0;
}
namespace Response {
/**
* Returns Type if the packet is valid, else none
*
* Note: Modifies the buffer to zero out the crc (since thats the easiest way to check without
* copying the buffer)
*/
boost::optional<Type> Validate(u8* data, size_t size) {
if (size < sizeof(Header)) {
LOG_ERROR(Input, "Invalid UDP packet received");
return boost::none;
}
Header header;
std::memcpy(&header, data, sizeof(Header));
if (header.magic != SERVER_MAGIC) {
LOG_ERROR(Input, "UDP Packet has an unexpected magic value");
return boost::none;
}
if (header.protocol_version != PROTOCOL_VERSION) {
LOG_ERROR(Input, "UDP Packet protocol mismatch");
return boost::none;
}
if (header.type < Type::Version || header.type > Type::PadData) {
LOG_ERROR(Input, "UDP Packet is an unknown type");
return boost::none;
}
// Packet size must equal sizeof(Header) + sizeof(Data)
// and also verify that the packet info mentions the correct size. Since the spec includes the
// type of the packet as part of the data, we need to include it in size calculations here
// ie: payload_length == sizeof(T) + sizeof(Type)
const size_t data_len = GetSizeOfResponseType(header.type);
if (header.payload_length != data_len + sizeof(Type) || size < data_len + sizeof(Header)) {
LOG_ERROR(
Input,
"UDP Packet payload length doesn't match. Received: {} PayloadLength: {} Expected: {}",
size, header.payload_length, data_len + sizeof(Type));
return boost::none;
}
const u32 crc32 = header.crc;
boost::crc_32_type result;
// zero out the crc in the buffer and then run the crc against it
std::memset(&data[offsetof(Header, crc)], 0, sizeof(u32_le));
result.process_bytes(data, data_len + sizeof(Header));
if (crc32 != result.checksum()) {
LOG_ERROR(Input, "UDP Packet CRC check failed. Offset: {}", offsetof(Header, crc));
return boost::none;
}
return header.type;
}
} // namespace Response
} // namespace InputCommon::CemuhookUDP

View file

@ -0,0 +1,249 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <type_traits>
#include <vector>
#include <boost/crc.hpp>
#include <boost/optional.hpp>
#include "common/bit_field.h"
#include "common/swap.h"
namespace InputCommon::CemuhookUDP {
constexpr size_t MAX_PACKET_SIZE = 100;
constexpr u16 PROTOCOL_VERSION = 1001;
constexpr u32 CLIENT_MAGIC = 0x43555344; // DSUC (but flipped for LE)
constexpr u32 SERVER_MAGIC = 0x53555344; // DSUS (but flipped for LE)
enum class Type : u32 {
Version = 0x00100000,
PortInfo = 0x00100001,
PadData = 0x00100002,
};
struct Header {
u32_le magic;
u16_le protocol_version;
u16_le payload_length;
u32_le crc;
u32_le id;
///> In the protocol, the type of the packet is not part of the header, but its convenient to
///> include in the header so the callee doesn't have to duplicate the type twice when building
///> the data
Type type;
};
static_assert(sizeof(Header) == 20, "UDP Message Header struct has wrong size");
static_assert(std::is_trivially_copyable_v<Header>, "UDP Message Header is not trivially copyable");
using MacAddress = std::array<u8, 6>;
constexpr MacAddress EMPTY_MAC_ADDRESS = {0, 0, 0, 0, 0, 0};
#pragma pack(push, 1)
template <typename T>
struct Message {
Header header;
T data;
};
#pragma pack(pop)
template <typename T>
constexpr Type GetMessageType();
namespace Request {
struct Version {};
/**
* Requests the server to send information about what controllers are plugged into the ports
* In citra's case, we only have one controller, so for simplicity's sake, we can just send a
* request explicitly for the first controller port and leave it at that. In the future it would be
* nice to make this configurable
*/
constexpr u32 MAX_PORTS = 4;
struct PortInfo {
u32_le pad_count; ///> Number of ports to request data for
std::array<u8, MAX_PORTS> port;
};
static_assert(std::is_trivially_copyable_v<PortInfo>,
"UDP Request PortInfo is not trivially copyable");
/**
* Request the latest pad information from the server. If the server hasn't received this message
* from the client in a reasonable time frame, the server will stop sending updates. The default
* timeout seems to be 5 seconds.
*/
struct PadData {
enum class Flags : u8 {
AllPorts,
Id,
Mac,
};
/// Determines which method will be used as a look up for the controller
Flags flags;
/// Index of the port of the controller to retrieve data about
u8 port_id;
/// Mac address of the controller to retrieve data about
MacAddress mac;
};
static_assert(sizeof(PadData) == 8, "UDP Request PadData struct has wrong size");
static_assert(std::is_trivially_copyable_v<PadData>,
"UDP Request PadData is not trivially copyable");
/**
* Creates a message with the proper header data that can be sent to the server.
* @param T data Request body to send
* @param client_id ID of the udp client (usually not checked on the server)
*/
template <typename T>
Message<T> Create(const T data, const u32 client_id = 0) {
boost::crc_32_type crc;
Header header{
CLIENT_MAGIC, PROTOCOL_VERSION, sizeof(T) + sizeof(Type), 0, client_id, GetMessageType<T>(),
};
Message<T> message{header, data};
crc.process_bytes(&message, sizeof(Message<T>));
message.header.crc = crc.checksum();
return message;
}
} // namespace Request
namespace Response {
struct Version {
u16_le version;
};
static_assert(sizeof(Version) == 2, "UDP Response Version struct has wrong size");
static_assert(std::is_trivially_copyable_v<Version>,
"UDP Response Version is not trivially copyable");
struct PortInfo {
u8 id;
u8 state;
u8 model;
u8 connection_type;
MacAddress mac;
u8 battery;
u8 is_pad_active;
};
static_assert(sizeof(PortInfo) == 12, "UDP Response PortInfo struct has wrong size");
static_assert(std::is_trivially_copyable_v<PortInfo>,
"UDP Response PortInfo is not trivially copyable");
#pragma pack(push, 1)
struct PadData {
PortInfo info;
u32_le packet_counter;
u16_le digital_button;
// The following union isn't trivially copyable but we don't use this input anyway.
// union DigitalButton {
// u16_le button;
// BitField<0, 1, u16_le> button_1; // Share
// BitField<1, 1, u16_le> button_2; // L3
// BitField<2, 1, u16_le> button_3; // R3
// BitField<3, 1, u16_le> button_4; // Options
// BitField<4, 1, u16_le> button_5; // Up
// BitField<5, 1, u16_le> button_6; // Right
// BitField<6, 1, u16_le> button_7; // Down
// BitField<7, 1, u16_le> button_8; // Left
// BitField<8, 1, u16_le> button_9; // L2
// BitField<9, 1, u16_le> button_10; // R2
// BitField<10, 1, u16_le> button_11; // L1
// BitField<11, 1, u16_le> button_12; // R1
// BitField<12, 1, u16_le> button_13; // Triangle
// BitField<13, 1, u16_le> button_14; // Circle
// BitField<14, 1, u16_le> button_15; // Cross
// BitField<15, 1, u16_le> button_16; // Square
// } digital_button;
u8 home;
/// If the device supports a "click" on the touchpad, this will change to 1 when a click happens
u8 touch_hard_press;
u8 left_stick_x;
u8 left_stick_y;
u8 right_stick_x;
u8 right_stick_y;
struct AnalogButton {
u8 button_8;
u8 button_7;
u8 button_6;
u8 button_5;
u8 button_12;
u8 button_11;
u8 button_10;
u8 button_9;
u8 button_16;
u8 button_15;
u8 button_14;
u8 button_13;
} analog_button;
struct TouchPad {
u8 is_active;
u8 id;
u16_le x;
u16_le y;
} touch_1, touch_2;
u64_le motion_timestamp;
struct Accelerometer {
float x;
float y;
float z;
} accel;
struct Gyroscope {
float pitch;
float yaw;
float roll;
} gyro;
};
#pragma pack(pop)
static_assert(sizeof(PadData) == 80, "UDP Response PadData struct has wrong size ");
static_assert(std::is_trivially_copyable_v<PadData>,
"UDP Response PadData is not trivially copyable");
static_assert(sizeof(Message<PadData>) == MAX_PACKET_SIZE,
"UDP MAX_PACKET_SIZE is no longer larger than Message<PadData>");
/**
* Create a Response Message from the data
* @param data array of bytes sent from the server
* @return boost::none if it failed to parse or Type if it succeeded. The client can then safely
* copy the data into the appropriate struct for that Type
*/
boost::optional<Type> Validate(u8* data, size_t size);
} // namespace Response
template <>
constexpr Type GetMessageType<Request::Version>() {
return Type::Version;
}
template <>
constexpr Type GetMessageType<Request::PortInfo>() {
return Type::PortInfo;
}
template <>
constexpr Type GetMessageType<Request::PadData>() {
return Type::PadData;
}
template <>
constexpr Type GetMessageType<Response::Version>() {
return Type::Version;
}
template <>
constexpr Type GetMessageType<Response::PortInfo>() {
return Type::PortInfo;
}
template <>
constexpr Type GetMessageType<Response::PadData>() {
return Type::PadData;
}
} // namespace InputCommon::CemuhookUDP

View file

@ -0,0 +1,90 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "common/logging/log.h"
#include "common/param_package.h"
#include "core/frontend/input.h"
#include "core/settings.h"
#include "input_common/udp/client.h"
#include "input_common/udp/udp.h"
namespace InputCommon::CemuhookUDP {
class UDPTouchDevice final : public Input::TouchDevice {
public:
explicit UDPTouchDevice(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {}
std::tuple<float, float, bool> GetStatus() const {
std::lock_guard<std::mutex> guard(status->update_mutex);
return status->touch_status;
}
private:
std::shared_ptr<DeviceStatus> status;
};
class UDPMotionDevice final : public Input::MotionDevice {
public:
explicit UDPMotionDevice(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {}
std::tuple<Math::Vec3<float>, Math::Vec3<float>> GetStatus() const {
std::lock_guard<std::mutex> guard(status->update_mutex);
return status->motion_status;
}
private:
std::shared_ptr<DeviceStatus> status;
};
class UDPTouchFactory final : public Input::Factory<Input::TouchDevice> {
public:
explicit UDPTouchFactory(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {}
std::unique_ptr<Input::TouchDevice> Create(const Common::ParamPackage& params) override {
{
std::lock_guard<std::mutex> guard(status->update_mutex);
status->touch_calibration.reset({});
// These default values work well for DS4 but probably not other touch inputs
status->touch_calibration->min_x = params.Get("min_x", 100);
status->touch_calibration->min_y = params.Get("min_y", 50);
status->touch_calibration->max_x = params.Get("max_x", 1800);
status->touch_calibration->max_y = params.Get("max_y", 850);
}
return std::make_unique<UDPTouchDevice>(status);
}
private:
std::shared_ptr<DeviceStatus> status;
};
class UDPMotionFactory final : public Input::Factory<Input::MotionDevice> {
public:
explicit UDPMotionFactory(std::shared_ptr<DeviceStatus> status_) : status(std::move(status_)) {}
std::unique_ptr<Input::MotionDevice> Create(const Common::ParamPackage& params) override {
return std::make_unique<UDPMotionDevice>(status);
}
private:
std::shared_ptr<DeviceStatus> status;
};
State::State() {
auto status = std::make_shared<DeviceStatus>();
client = std::make_unique<Client>(status, Settings::values.udp_input_address,
Settings::values.udp_input_port);
Input::RegisterFactory<Input::TouchDevice>("cemuhookudp",
std::make_shared<UDPTouchFactory>(status));
Input::RegisterFactory<Input::MotionDevice>("cemuhookudp",
std::make_shared<UDPMotionFactory>(status));
}
State::~State() {
Input::UnregisterFactory<Input::TouchDevice>("cemuhookudp");
Input::UnregisterFactory<Input::MotionDevice>("cemuhookudp");
}
std::unique_ptr<State> Init() {
return std::make_unique<State>();
}
} // namespace InputCommon::CemuhookUDP

View file

@ -0,0 +1,26 @@
// Copyright 2018 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <memory>
#include <unordered_map>
#include "input_common/main.h"
#include "input_common/udp/client.h"
namespace InputCommon::CemuhookUDP {
class UDPTouchDevice;
class UDPMotionDevice;
class State {
public:
State();
~State();
private:
std::unique_ptr<Client> client;
};
std::unique_ptr<State> Init();
} // namespace InputCommon::CemuhookUDP