Browse Source
Input: UDP Client to provide motion and touch controls
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. Co-Authored-By: jroweboy <jroweboy@gmail.com>pull/15/merge
committed by
FearlessTobi
14 changed files with 904 additions and 4 deletions
-
7CMakeLists.txt
-
9src/common/thread.h
-
3src/core/settings.h
-
8src/input_common/CMakeLists.txt
-
12src/input_common/main.cpp
-
283src/input_common/udp/client.cpp
-
96src/input_common/udp/client.h
-
79src/input_common/udp/protocol.cpp
-
249src/input_common/udp/protocol.h
-
96src/input_common/udp/udp.cpp
-
27src/input_common/udp/udp.h
-
17src/yuzu/configuration/config.cpp
-
5src/yuzu_cmd/config.cpp
-
17src/yuzu_cmd/default_ini.h
@ -0,0 +1,283 @@ |
|||||
|
// 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 "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, u8 pad_index, 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)), pad_index(pad_index), |
||||
|
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) { |
||||
|
// Send a request for getting port info for the pad
|
||||
|
Request::PortInfo port_info{1, {pad_index, 0, 0, 0}}; |
||||
|
auto port_message = Request::Create(port_info, client_id); |
||||
|
std::memcpy(&send_buffer1, &port_message, PORT_INFO_SIZE); |
||||
|
std::size_t len = socket.send_to(boost::asio::buffer(send_buffer1), send_endpoint); |
||||
|
|
||||
|
// Send a request for getting pad data for the pad
|
||||
|
Request::PadData pad_data{Request::PadData::Flags::Id, pad_index, EMPTY_MAC_ADDRESS}; |
||||
|
auto pad_message = Request::Create(pad_data, client_id); |
||||
|
std::memcpy(send_buffer2.data(), &pad_message, PAD_DATA_SIZE); |
||||
|
std::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; |
||||
|
u8 pad_index; |
||||
|
|
||||
|
static constexpr std::size_t PORT_INFO_SIZE = sizeof(Message<Request::PortInfo>); |
||||
|
static constexpr std::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, |
||||
|
u8 pad_index, u32 client_id) |
||||
|
: status(status) { |
||||
|
StartCommunication(host, port, pad_index, client_id); |
||||
|
} |
||||
|
|
||||
|
Client::~Client() { |
||||
|
socket->Stop(); |
||||
|
thread.join(); |
||||
|
} |
||||
|
|
||||
|
void Client::ReloadSocket(const std::string& host, u16 port, u8 pad_index, u32 client_id) { |
||||
|
socket->Stop(); |
||||
|
thread.join(); |
||||
|
StartCommunication(host, port, pad_index, client_id); |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
// TODO: Check how the Switch handles motions and how the CemuhookUDP motion
|
||||
|
// directions correspond to the ones of the Switch
|
||||
|
Common::Vec3f accel = Common::MakeVec<float>(data.accel.x, data.accel.y, data.accel.z); |
||||
|
Common::Vec3f gyro = Common::MakeVec<float>(data.gyro.pitch, data.gyro.yaw, data.gyro.roll); |
||||
|
{ |
||||
|
std::lock_guard 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}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void Client::StartCommunication(const std::string& host, u16 port, u8 pad_index, u32 client_id) { |
||||
|
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, pad_index, client_id, callback); |
||||
|
thread = std::thread{SocketLoop, this->socket.get()}; |
||||
|
} |
||||
|
|
||||
|
void TestCommunication(const std::string& host, u16 port, u8 pad_index, u32 client_id, |
||||
|
std::function<void()> success_callback, |
||||
|
std::function<void()> failure_callback) { |
||||
|
std::thread([=] { |
||||
|
Common::Event success_event; |
||||
|
SocketCallback callback{[](Response::Version version) {}, [](Response::PortInfo info) {}, |
||||
|
[&](Response::PadData data) { success_event.Set(); }}; |
||||
|
Socket socket{host, port, pad_index, client_id, callback}; |
||||
|
std::thread worker_thread{SocketLoop, &socket}; |
||||
|
bool result = success_event.WaitFor(std::chrono::seconds(8)); |
||||
|
socket.Stop(); |
||||
|
worker_thread.join(); |
||||
|
if (result) |
||||
|
success_callback(); |
||||
|
else |
||||
|
failure_callback(); |
||||
|
}) |
||||
|
.detach(); |
||||
|
} |
||||
|
|
||||
|
CalibrationConfigurationJob::CalibrationConfigurationJob( |
||||
|
const std::string& host, u16 port, u8 pad_index, u32 client_id, |
||||
|
std::function<void(Status)> status_callback, |
||||
|
std::function<void(u16, u16, u16, u16)> data_callback) { |
||||
|
|
||||
|
std::thread([=] { |
||||
|
constexpr u16 CALIBRATION_THRESHOLD = 100; |
||||
|
|
||||
|
u16 min_x{UINT16_MAX}, min_y{UINT16_MAX}; |
||||
|
u16 max_x, max_y; |
||||
|
|
||||
|
Status current_status{Status::Initialized}; |
||||
|
SocketCallback callback{[](Response::Version version) {}, [](Response::PortInfo info) {}, |
||||
|
[&](Response::PadData data) { |
||||
|
if (current_status == Status::Initialized) { |
||||
|
// Receiving data means the communication is ready now
|
||||
|
current_status = Status::Ready; |
||||
|
status_callback(current_status); |
||||
|
} |
||||
|
if (!data.touch_1.is_active) |
||||
|
return; |
||||
|
LOG_DEBUG(Input, "Current touch: {} {}", data.touch_1.x, |
||||
|
data.touch_1.y); |
||||
|
min_x = std::min(min_x, static_cast<u16>(data.touch_1.x)); |
||||
|
min_y = std::min(min_y, static_cast<u16>(data.touch_1.y)); |
||||
|
if (current_status == Status::Ready) { |
||||
|
// First touch - min data (min_x/min_y)
|
||||
|
current_status = Status::Stage1Completed; |
||||
|
status_callback(current_status); |
||||
|
} |
||||
|
if (data.touch_1.x - min_x > CALIBRATION_THRESHOLD && |
||||
|
data.touch_1.y - min_y > CALIBRATION_THRESHOLD) { |
||||
|
// Set the current position as max value and finishes
|
||||
|
// configuration
|
||||
|
max_x = data.touch_1.x; |
||||
|
max_y = data.touch_1.y; |
||||
|
current_status = Status::Completed; |
||||
|
data_callback(min_x, min_y, max_x, max_y); |
||||
|
status_callback(current_status); |
||||
|
|
||||
|
complete_event.Set(); |
||||
|
} |
||||
|
}}; |
||||
|
Socket socket{host, port, pad_index, client_id, callback}; |
||||
|
std::thread worker_thread{SocketLoop, &socket}; |
||||
|
complete_event.Wait(); |
||||
|
socket.Stop(); |
||||
|
worker_thread.join(); |
||||
|
}) |
||||
|
.detach(); |
||||
|
} |
||||
|
|
||||
|
CalibrationConfigurationJob::~CalibrationConfigurationJob() { |
||||
|
Stop(); |
||||
|
} |
||||
|
|
||||
|
void CalibrationConfigurationJob::Stop() { |
||||
|
complete_event.Set(); |
||||
|
} |
||||
|
|
||||
|
} // namespace InputCommon::CemuhookUDP
|
||||
@ -0,0 +1,96 @@ |
|||||
|
// Copyright 2018 Citra Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <functional> |
||||
|
#include <memory> |
||||
|
#include <mutex> |
||||
|
#include <optional> |
||||
|
#include <string> |
||||
|
#include <thread> |
||||
|
#include <tuple> |
||||
|
#include <vector> |
||||
|
#include "common/common_types.h" |
||||
|
#include "common/thread.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<Common::Vec3<float>, Common::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; |
||||
|
}; |
||||
|
std::optional<CalibrationData> touch_calibration; |
||||
|
}; |
||||
|
|
||||
|
class Client { |
||||
|
public: |
||||
|
explicit Client(std::shared_ptr<DeviceStatus> status, const std::string& host = DEFAULT_ADDR, |
||||
|
u16 port = DEFAULT_PORT, u8 pad_index = 0, u32 client_id = 24872); |
||||
|
~Client(); |
||||
|
void ReloadSocket(const std::string& host = "127.0.0.1", u16 port = 26760, u8 pad_index = 0, |
||||
|
u32 client_id = 24872); |
||||
|
|
||||
|
private: |
||||
|
void OnVersion(Response::Version); |
||||
|
void OnPortInfo(Response::PortInfo); |
||||
|
void OnPadData(Response::PadData); |
||||
|
void StartCommunication(const std::string& host, u16 port, u8 pad_index, u32 client_id); |
||||
|
|
||||
|
std::unique_ptr<Socket> socket; |
||||
|
std::shared_ptr<DeviceStatus> status; |
||||
|
std::thread thread; |
||||
|
u64 packet_sequence = 0; |
||||
|
}; |
||||
|
|
||||
|
/// An async job allowing configuration of the touchpad calibration. |
||||
|
class CalibrationConfigurationJob { |
||||
|
public: |
||||
|
enum class Status { |
||||
|
Initialized, |
||||
|
Ready, |
||||
|
Stage1Completed, |
||||
|
Completed, |
||||
|
}; |
||||
|
/** |
||||
|
* Constructs and starts the job with the specified parameter. |
||||
|
* |
||||
|
* @param status_callback Callback for job status updates |
||||
|
* @param data_callback Called when calibration data is ready |
||||
|
*/ |
||||
|
explicit CalibrationConfigurationJob(const std::string& host, u16 port, u8 pad_index, |
||||
|
u32 client_id, std::function<void(Status)> status_callback, |
||||
|
std::function<void(u16, u16, u16, u16)> data_callback); |
||||
|
~CalibrationConfigurationJob(); |
||||
|
void Stop(); |
||||
|
|
||||
|
private: |
||||
|
Common::Event complete_event; |
||||
|
}; |
||||
|
|
||||
|
void TestCommunication(const std::string& host, u16 port, u8 pad_index, u32 client_id, |
||||
|
std::function<void()> success_callback, |
||||
|
std::function<void()> failure_callback); |
||||
|
|
||||
|
} // namespace InputCommon::CemuhookUDP |
||||
@ -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 std::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) |
||||
|
*/ |
||||
|
std::optional<Type> Validate(u8* data, std::size_t size) { |
||||
|
if (size < sizeof(Header)) { |
||||
|
LOG_DEBUG(Input, "Invalid UDP packet received"); |
||||
|
return {}; |
||||
|
} |
||||
|
Header header; |
||||
|
std::memcpy(&header, data, sizeof(Header)); |
||||
|
if (header.magic != SERVER_MAGIC) { |
||||
|
LOG_ERROR(Input, "UDP Packet has an unexpected magic value"); |
||||
|
return {}; |
||||
|
} |
||||
|
if (header.protocol_version != PROTOCOL_VERSION) { |
||||
|
LOG_ERROR(Input, "UDP Packet protocol mismatch"); |
||||
|
return {}; |
||||
|
} |
||||
|
if (header.type < Type::Version || header.type > Type::PadData) { |
||||
|
LOG_ERROR(Input, "UDP Packet is an unknown type"); |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
// 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 std::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 {}; |
||||
|
} |
||||
|
|
||||
|
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 {}; |
||||
|
} |
||||
|
return header.type; |
||||
|
} |
||||
|
} // namespace Response
|
||||
|
|
||||
|
} // namespace InputCommon::CemuhookUDP
|
||||
@ -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 <optional> |
||||
|
#include <type_traits> |
||||
|
#include <vector> |
||||
|
#include <boost/crc.hpp> |
||||
|
#include "common/bit_field.h" |
||||
|
#include "common/swap.h" |
||||
|
|
||||
|
namespace InputCommon::CemuhookUDP { |
||||
|
|
||||
|
constexpr std::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> button_1; // Share |
||||
|
// BitField<1, 1, u16> button_2; // L3 |
||||
|
// BitField<2, 1, u16> button_3; // R3 |
||||
|
// BitField<3, 1, u16> button_4; // Options |
||||
|
// BitField<4, 1, u16> button_5; // Up |
||||
|
// BitField<5, 1, u16> button_6; // Right |
||||
|
// BitField<6, 1, u16> button_7; // Down |
||||
|
// BitField<7, 1, u16> button_8; // Left |
||||
|
// BitField<8, 1, u16> button_9; // L2 |
||||
|
// BitField<9, 1, u16> button_10; // R2 |
||||
|
// BitField<10, 1, u16> button_11; // L1 |
||||
|
// BitField<11, 1, u16> button_12; // R1 |
||||
|
// BitField<12, 1, u16> button_13; // Triangle |
||||
|
// BitField<13, 1, u16> button_14; // Circle |
||||
|
// BitField<14, 1, u16> button_15; // Cross |
||||
|
// BitField<15, 1, u16> 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 |
||||
|
*/ |
||||
|
std::optional<Type> Validate(u8* data, std::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 |
||||
@ -0,0 +1,96 @@ |
|||||
|
// 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 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<Common::Vec3<float>, Common::Vec3<float>> GetStatus() const { |
||||
|
std::lock_guard 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 guard(status->update_mutex); |
||||
|
status->touch_calibration.emplace(); |
||||
|
// 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, Settings::values.udp_pad_index); |
||||
|
|
||||
|
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"); |
||||
|
} |
||||
|
|
||||
|
void State::ReloadUDPClient() { |
||||
|
client->ReloadSocket(Settings::values.udp_input_address, Settings::values.udp_input_port, |
||||
|
Settings::values.udp_pad_index); |
||||
|
} |
||||
|
|
||||
|
std::unique_ptr<State> Init() { |
||||
|
return std::make_unique<State>(); |
||||
|
} |
||||
|
} // namespace InputCommon::CemuhookUDP
|
||||
@ -0,0 +1,27 @@ |
|||||
|
// 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(); |
||||
|
void ReloadUDPClient(); |
||||
|
|
||||
|
private: |
||||
|
std::unique_ptr<Client> client; |
||||
|
}; |
||||
|
|
||||
|
std::unique_ptr<State> Init(); |
||||
|
|
||||
|
} // namespace InputCommon::CemuhookUDP |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue