/* Copyright 2024 flyinghead This file is part of Flycast. Flycast is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. Flycast is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ #include "dreamconn.h" #ifdef USE_DREAMCASTCONTROLLER #include "hw/maple/maple_devs.h" #include "ui/gui.h" #include #include #include #include #include #if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) #include #endif #if defined(_WIN32) #include #include #endif static asio::error_code sendMsg(const MapleMsg& msg, asio::ip::tcp::iostream& stream) { std::ostringstream s; s.fill('0'); s << std::hex << std::uppercase << std::setw(2) << (u32)msg.command << " " << std::setw(2) << (u32)msg.destAP << " " << std::setw(2) << (u32)msg.originAP << " " << std::setw(2) << (u32)msg.size; const u32 sz = msg.getDataSize(); for (u32 i = 0; i < sz; i++) s << " " << std::setw(2) << (u32)msg.data[i]; s << "\r\n"; asio::ip::tcp::socket& sock = static_cast(stream.socket()); asio::error_code ec; asio::write(sock, asio::buffer(s.str()), ec); return ec; } static bool receiveMsg(MapleMsg& msg, std::istream& stream) { std::string response; if (!std::getline(stream, response)) return false; sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); if ((msg.getDataSize() - 1) * 3 + 13 >= response.length()) return false; for (unsigned i = 0; i < msg.getDataSize(); i++) sscanf(&response[i * 3 + 12], "%hhx", &msg.data[i]); return !stream.fail(); } DreamConn::DreamConn(int bus) : bus(bus) { } DreamConn::~DreamConn() { disconnect(); } bool DreamConn::send(const MapleMsg& msg) { std::lock_guard lock(send_mutex); // Ensure thread safety for send operations return send_no_lock(msg); } bool DreamConn::send_no_lock(const MapleMsg& msg) { asio::error_code ec; if (maple_io_connected) ec = sendMsg(msg, iostream); else return false; if (ec) { maple_io_connected = false; WARN_LOG(INPUT, "DreamcastController[%d] send failed: %s", bus, ec.message().c_str()); disconnect(); return false; } return true; } bool DreamConn::send(const MapleMsg& txMsg, MapleMsg& rxMsg) { std::lock_guard lock(send_mutex); // Ensure thread safety for send operations if (!send_no_lock(txMsg)) { return false; } return receiveMsg(rxMsg, iostream); } void DreamConn::changeBus(int newBus) { bus = newBus; } void DreamConn::connect() { maple_io_connected = false; asio::error_code ec; #if !defined(_WIN32) WARN_LOG(INPUT, "DreamcastController[%d] connection failed: DreamConn+ / DreamConn S Controller supported on Windows only", bus); return; #endif iostream = asio::ip::tcp::iostream("localhost", std::to_string(BASE_PORT + bus)); if (!iostream) { WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, iostream.error().message().c_str()); disconnect(); return; } iostream.expires_from_now(std::chrono::seconds(1)); // Now get the controller configuration MapleMsg msg; msg.command = MDCF_GetCondition; msg.destAP = (bus << 6) | 0x20; msg.originAP = bus << 6; msg.setData(MFID_0_Input); ec = sendMsg(msg, iostream); if (ec) { WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); disconnect(); return; } if (!receiveMsg(msg, iostream)) { WARN_LOG(INPUT, "DreamcastController[%d] read timeout", bus); disconnect(); return; } iostream.expires_from_now(std::chrono::duration::max()); // don't use a 64-bit based duration to avoid overflow expansionDevs = msg.originAP & 0x1f; config::MapleExpansionDevices[bus][0] = hasVmu() ? MDT_SegaVMU : MDT_None; config::MapleExpansionDevices[bus][1] = hasRumble() ? MDT_PurupuruPack : MDT_None; if (hasVmu() || hasRumble()) { NOTICE_LOG(INPUT, "Connected to DreamcastController[%d]: Type:%s, VMU:%d, Rumble Pack:%d", bus, getName().c_str(), hasVmu(), hasRumble()); maple_io_connected = true; } else { WARN_LOG(INPUT, "DreamcastController[%d] connection: no VMU or Rumble Pack connected", bus); disconnect(); return; } } void DreamConn::disconnect() { if (iostream) iostream.close(); maple_io_connected = false; NOTICE_LOG(INPUT, "Disconnected from DreamcastController[%d]", bus); } #endif