| |
| |
| |
|
|
| #include <algorithm> |
| #include <array> |
| #include <chrono> |
| #include <cstring> |
| #include <functional> |
| #include <thread> |
| #include <boost/asio.hpp> |
| #include "common/logging/log.h" |
| #include "input_common/udp/client.h" |
| #include "input_common/udp/protocol.h" |
|
|
| 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) |
| : callback(std::move(callback)), timer(io_service), |
| socket(io_service, udp::endpoint(udp::v4(), 0)), client_id(client_id), |
| pad_index(pad_index) { |
| boost::system::error_code ec{}; |
| auto ipv4 = boost::asio::ip::make_address_v4(host, ec); |
| if (ec.value() != boost::system::errc::success) { |
| LOG_ERROR(Input, "Invalid IPv4 address \"{}\" provided to socket", host); |
| ipv4 = boost::asio::ip::address_v4{}; |
| } |
|
|
| send_endpoint = {udp::endpoint(ipv4, port)}; |
| } |
|
|
| 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) { |
| boost::system::error_code _ignored{}; |
| |
| Request::PortInfo port_info{1, {pad_index, 0, 0, 0}}; |
| const auto port_message = Request::Create(port_info, client_id); |
| std::memcpy(&send_buffer1, &port_message, PORT_INFO_SIZE); |
| socket.send_to(boost::asio::buffer(send_buffer1), send_endpoint, {}, _ignored); |
|
|
| |
| Request::PadData pad_data{Request::PadData::Flags::Id, pad_index, EMPTY_MAC_ADDRESS}; |
| const auto pad_message = Request::Create(pad_data, client_id); |
| std::memcpy(send_buffer2.data(), &pad_message, PAD_DATA_SIZE); |
| socket.send_to(boost::asio::buffer(send_buffer2), send_endpoint, {}, _ignored); |
| 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(std::move(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; |
| |
| |
| |
| 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}; |
|
|
| |
| |
| const bool is_active = data.touch_1.is_active != 0; |
|
|
| float x = 0; |
| float y = 0; |
|
|
| if (is_active && status->touch_calibration) { |
| const u16 min_x = status->touch_calibration->min_x; |
| const u16 max_x = status->touch_calibration->max_x; |
| const u16 min_y = status->touch_calibration->min_y; |
| const 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, |
| const std::function<void()>& success_callback, |
| const 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, std::move(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([=, this] { |
| u16 min_x{UINT16_MAX}; |
| u16 min_y{UINT16_MAX}; |
| u16 max_x{}; |
| u16 max_y{}; |
|
|
| Status current_status{Status::Initialized}; |
| SocketCallback callback{[](Response::Version version) {}, [](Response::PortInfo info) {}, |
| [&](Response::PadData data) { |
| constexpr u16 CALIBRATION_THRESHOLD = 100; |
|
|
| if (current_status == Status::Initialized) { |
| |
| 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) { |
| |
| 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) { |
| |
| |
| 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, std::move(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(); |
| } |
|
|
| } |
|
|