From 4e74619d9eab93a8d5d26a31738cfe30e772eaf0 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 16 Feb 2025 09:36:57 -0700 Subject: [PATCH] Harden serial comm interface (#1847) * Harden serial interface * Fixed build errors * Minor tweaks * Automatically attached A->A, B->B, etc * Interfaces go in decending order * Do a modulo in case I decide to move interface numbers * Do specifically mod 4 * Call SDL_JoystickGetDeviceInstanceID which seems to help with a Windows bug * Made MapleDevices smart pointers so they can freely be swapped out without leaking * Fixed issues some issues reported by kosekmi * Added missing bracket * Fixed another compile issue * Fixed a missing else * If device had no serial number, fall down to checking name * Use new DreamPort firmware dynamics * Added missing parens * Fixed screen blanking issue * Added const * Reset VMU Screen on game/emulator exit * Added gameTermination() hook to hardware gamepad * Send SW port on game termination * Added checks so port is only sent if data is valid * Fixed bug: wait for write to complete in sendCmd * Fixed bug leading to multiple pointers to VMU and Rumble pack --------- Co-authored-by: Mike Kosek --- core/hw/maple/maple_cfg.cpp | 15 +- core/hw/maple/maple_cfg.h | 3 +- core/hw/maple/maple_devs.cpp | 171 ++++++-- core/hw/maple/maple_devs.h | 4 +- core/hw/maple/maple_helper.cpp | 6 +- core/hw/maple/maple_if.cpp | 12 +- core/hw/maple/maple_if.h | 3 +- core/sdl/dreamconn.cpp | 696 +++++++++++++++++++++++---------- core/sdl/dreamconn.h | 14 +- core/sdl/sdl.cpp | 4 +- 10 files changed, 664 insertions(+), 264 deletions(-) diff --git a/core/hw/maple/maple_cfg.cpp b/core/hw/maple/maple_cfg.cpp index 72245b9ec..601454054 100644 --- a/core/hw/maple/maple_cfg.cpp +++ b/core/hw/maple/maple_cfg.cpp @@ -213,8 +213,8 @@ bool maple_atomiswave_coin_chute(int slot) static void mcfg_Create(MapleDeviceType type, u32 bus, u32 port, s32 player_num = -1) { - delete MapleDevices[bus][port]; - maple_device* dev = maple_Create(type); + MapleDevices[bus][port].reset(); + std::shared_ptr dev = maple_Create(type); dev->Setup(bus, port, player_num); } @@ -364,7 +364,7 @@ static void vmuDigest() for (int i = 0; i < MAPLE_PORTS; i++) for (int j = 0; j < 6; j++) { - const maple_device* device = MapleDevices[i][j]; + std::shared_ptr device = MapleDevices[i][j]; if (device != nullptr) { size_t size; @@ -414,8 +414,7 @@ void mcfg_DestroyDevices(bool full) if (MapleDevices[i][j] != nullptr && (full || MapleDevices[i][j]->get_device_type() != MDT_NaomiJamma)) { - delete MapleDevices[i][j]; - MapleDevices[i][j] = nullptr; + MapleDevices[i][j].reset(); } } } @@ -435,7 +434,7 @@ void mcfg_SerializeDevices(Serializer& ser) for (int j = 0; j < 6; j++) { u8 deviceType = MDT_None; - maple_device* device = MapleDevices[i][j]; + std::shared_ptr device = MapleDevices[i][j]; if (device != nullptr) deviceType = device->get_device_type(); ser << deviceType; @@ -490,9 +489,9 @@ void mcfg_DeserializeDevices(Deserializer& deser) memcpy(EEPROM, eeprom, sizeof(eeprom)); } -maple_naomi_jamma *getMieDevice() +std::shared_ptr getMieDevice() { if (MapleDevices[0][5] == nullptr || MapleDevices[0][5]->get_device_type() != MDT_NaomiJamma) return nullptr; - return (maple_naomi_jamma *)MapleDevices[0][5]; + return std::static_pointer_cast(MapleDevices[0][5]); } diff --git a/core/hw/maple/maple_cfg.h b/core/hw/maple/maple_cfg.h index 404d7e86f..cb46b3ae3 100644 --- a/core/hw/maple/maple_cfg.h +++ b/core/hw/maple/maple_cfg.h @@ -1,6 +1,7 @@ #pragma once #include "types.h" #include +#include enum MapleDeviceType { @@ -123,4 +124,4 @@ const u8 *getRfidCardData(int playerNum); void setRfidCardData(int playerNum, u8 *data); struct maple_naomi_jamma; -maple_naomi_jamma *getMieDevice(); +std::shared_ptr getMieDevice(); diff --git a/core/hw/maple/maple_devs.cpp b/core/hw/maple/maple_devs.cpp index 1602f746f..988cb6adb 100755 --- a/core/hw/maple/maple_devs.cpp +++ b/core/hw/maple/maple_devs.cpp @@ -44,7 +44,7 @@ void maple_device::Setup(u32 bus, u32 port, int playerNum) config = new MapleConfigMap(this); OnSetup(); - MapleDevices[bus][port] = this; + MapleDevices[bus][port] = shared_from_this(); } maple_device::~maple_device() { @@ -368,7 +368,7 @@ struct maple_sega_vmu: maple_base } fullSaveNeeded = true; } - + bool fullSave() { if (file == nullptr) @@ -402,7 +402,7 @@ struct maple_sega_vmu: maple_base { memset(flash_data, 0, sizeof(flash_data)); memset(lcd_data, 0, sizeof(lcd_data)); - + // Load existing vmu file if found std::string rpath = hostfs::getVmuPath(logical_port, false); // this might be a storage url @@ -2047,56 +2047,56 @@ struct RFIDReaderWriter : maple_base void insertRfidCard(int playerNum) { - maple_device *mapleDev = MapleDevices[1 + playerNum][5]; + std::shared_ptr mapleDev = MapleDevices[1 + playerNum][5]; if (mapleDev != nullptr && mapleDev->get_device_type() == MDT_RFIDReaderWriter) - ((RFIDReaderWriter *)mapleDev)->insertCard(); + std::static_pointer_cast(mapleDev)->insertCard(); } void setRfidCardData(int playerNum, u8 *data) { - maple_device *mapleDev = MapleDevices[1 + playerNum][5]; + std::shared_ptr mapleDev = MapleDevices[1 + playerNum][5]; if (mapleDev != nullptr && mapleDev->get_device_type() == MDT_RFIDReaderWriter) - ((RFIDReaderWriter *)mapleDev)->setCardData(data); + std::static_pointer_cast(mapleDev)->setCardData(data); } const u8 *getRfidCardData(int playerNum) { - maple_device *mapleDev = MapleDevices[1 + playerNum][5]; + std::shared_ptr mapleDev = MapleDevices[1 + playerNum][5]; if (mapleDev != nullptr && mapleDev->get_device_type() == MDT_RFIDReaderWriter) - return ((RFIDReaderWriter *)mapleDev)->getCardData(); + return std::static_pointer_cast(mapleDev)->getCardData(); else return nullptr; } -maple_device* maple_Create(MapleDeviceType type) +std::shared_ptr maple_Create(MapleDeviceType type) { switch(type) { case MDT_SegaController: if (!settings.platform.isAtomiswave()) - return new maple_sega_controller(); + return std::make_shared(); else - return new maple_atomiswave_controller(); - case MDT_Microphone: return new maple_microphone(); - case MDT_SegaVMU: return new maple_sega_vmu(); - case MDT_PurupuruPack: return new maple_sega_purupuru(); - case MDT_Keyboard: return new maple_keyboard(); - case MDT_Mouse: return new maple_mouse(); + return std::make_shared(); + case MDT_Microphone: return std::make_shared(); + case MDT_SegaVMU: return std::make_shared(); + case MDT_PurupuruPack: return std::make_shared(); + case MDT_Keyboard: return std::make_shared(); + case MDT_Mouse: return std::make_shared(); case MDT_LightGun: if (!settings.platform.isAtomiswave()) - return new maple_lightgun(); + return std::make_shared(); else - return new atomiswave_lightgun(); - case MDT_NaomiJamma: return new maple_naomi_jamma(); - case MDT_TwinStick: return new maple_sega_twinstick(); - case MDT_AsciiStick: return new maple_ascii_stick(); - case MDT_MaracasController: return new maple_maracas_controller(); - case MDT_FishingController: return new maple_fishing_controller(); - case MDT_PopnMusicController: return new maple_popnmusic_controller(); - case MDT_RacingController: return new maple_racing_controller(); - case MDT_DenshaDeGoController: return new maple_densha_controller(); - case MDT_SegaControllerXL: return new FullController(); - case MDT_RFIDReaderWriter: return new RFIDReaderWriter(); + return std::make_shared(); + case MDT_NaomiJamma: return std::make_shared(); + case MDT_TwinStick: return std::make_shared(); + case MDT_AsciiStick: return std::make_shared(); + case MDT_MaracasController: return std::make_shared(); + case MDT_FishingController: return std::make_shared(); + case MDT_PopnMusicController: return std::make_shared(); + case MDT_RacingController: return std::make_shared(); + case MDT_DenshaDeGoController: return std::make_shared(); + case MDT_SegaControllerXL: return std::make_shared(); + case MDT_RFIDReaderWriter: return std::make_shared(); default: ERROR_LOG(MAPLE, "Invalid device type %d", type); @@ -2108,6 +2108,8 @@ maple_device* maple_Create(MapleDeviceType type) #if (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))) && !defined(TARGET_UWP) && defined(USE_SDL) && !defined(LIBRETRO) #include "sdl/dreamconn.h" +#include +#include struct DreamConnVmu : public maple_sega_vmu { @@ -2131,7 +2133,7 @@ struct DreamConnVmu : public maple_sega_vmu return maple_sega_vmu::dma(cmd); } - void copy(maple_sega_vmu *other) + void copyIn(std::shared_ptr other) { memcpy(flash_data, other->flash_data, sizeof(flash_data)); memcpy(lcd_data, other->lcd_data, sizeof(lcd_data)); @@ -2139,6 +2141,14 @@ struct DreamConnVmu : public maple_sega_vmu fullSaveNeeded = other->fullSaveNeeded; } + void copyOut(std::shared_ptr other) + { + memcpy(other->flash_data, flash_data, sizeof(other->flash_data)); + memcpy(other->lcd_data, lcd_data, sizeof(other->lcd_data)); + memcpy(other->lcd_data_decoded, lcd_data_decoded, sizeof(other->lcd_data_decoded)); + other->fullSaveNeeded = fullSaveNeeded; + } + void updateScreen() { MapleMsg msg; @@ -2170,32 +2180,111 @@ struct DreamConnPurupuru : public maple_sega_purupuru } }; +static std::list> dreamConnVmus; +static std::list> dreamConnPurupurus; + void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart) { const int bus = dreamconn->getBus(); + + bool vmuFound = false; + bool rumbleFound = false; + if (dreamconn->hasVmu()) { - maple_device *dev = MapleDevices[bus][0]; + std::shared_ptr vmu; + for (const std::shared_ptr& vmuIter : dreamConnVmus) + { + if (vmuIter->dreamconn.get() == dreamconn.get()) + { + vmuFound = true; + vmu = vmuIter; + break; + } + } + + std::shared_ptr dev = MapleDevices[bus][0]; if (gameStart || (dev != nullptr && dev->get_device_type() == MDT_SegaVMU)) { - DreamConnVmu *vmu = new DreamConnVmu(dreamconn); - vmu->Setup(bus, 0); - if (!gameStart) { - // if loading a state, copy data from the regular vmu and send a screen update - vmu->copy(static_cast(dev)); - vmu->updateScreen(); + bool vmuCreated = false; + if (!vmu) + { + vmu = std::make_shared(dreamconn); + vmuCreated = true; } - delete dev; + + vmu->Setup(bus, 0); + + if ((!gameStart || !vmuCreated) && dev) { + // if loading a state or DreamConnVmu existed, copy data from the regular vmu and send a screen update + vmu->copyIn(std::static_pointer_cast(dev)); + if (!gameStart) { + vmu->updateScreen(); + } + } + + if (!vmuFound) dreamConnVmus.push_back(vmu); } } if (dreamconn->hasRumble()) { - maple_device *dev = MapleDevices[bus][1]; + std::shared_ptr rumble; + for (const std::shared_ptr& purupuru : dreamConnPurupurus) + { + if (purupuru->dreamconn.get() == dreamconn.get()) + { + rumbleFound = true; + rumble = purupuru; + break; + } + } + + std::shared_ptr dev = MapleDevices[bus][1]; if (gameStart || (dev != nullptr && dev->get_device_type() == MDT_PurupuruPack)) { - delete dev; - DreamConnPurupuru *rumble = new DreamConnPurupuru(dreamconn); + if (!rumble) + { + rumble = std::make_shared(dreamconn); + } rumble->Setup(bus, 1); + + if (!rumbleFound) dreamConnPurupurus.push_back(rumble); + } + } +} + +void tearDownDreamConnDevices(std::shared_ptr dreamconn) +{ + const int bus = dreamconn->getBus(); + for (std::list>::const_iterator iter = dreamConnVmus.begin(); + iter != dreamConnVmus.end();) + { + if ((*iter)->dreamconn.get() == dreamconn.get()) + { + std::shared_ptr dev = maple_Create(MDT_SegaVMU); + dev->Setup(bus, 0); + (*iter)->copyOut(std::static_pointer_cast(dev)); + iter = dreamConnVmus.erase(iter); + break; + } + else + { + ++iter; + } + } + for (std::list>::const_iterator iter = dreamConnPurupurus.begin(); + iter != dreamConnPurupurus.end();) + { + if ((*iter)->dreamconn.get() == dreamconn.get()) + { + std::shared_ptr dev = maple_Create(MDT_PurupuruPack); + dev->Setup(bus, 1); + iter = dreamConnPurupurus.erase(iter); + break; + } + else + { + ++iter; } } } diff --git a/core/hw/maple/maple_devs.h b/core/hw/maple/maple_devs.h index 89f1e5836..5d69a2a3c 100755 --- a/core/hw/maple/maple_devs.h +++ b/core/hw/maple/maple_devs.h @@ -121,7 +121,7 @@ enum AWAVE_KEYS AWAVE_TRIGGER_KEY = 1 << 17, }; -struct maple_device +struct maple_device : public std::enable_shared_from_this { u8 maple_port; //raw maple port u8 bus_port; //0 .. 5 @@ -150,7 +150,7 @@ struct maple_device virtual const void *getData(size_t& size) const { size = 0; return nullptr; } }; -maple_device* maple_Create(MapleDeviceType type); +std::shared_ptr maple_Create(MapleDeviceType type); #define MAPLE_PORTS 4 diff --git a/core/hw/maple/maple_helper.cpp b/core/hw/maple/maple_helper.cpp index 6e683d972..639b5d66b 100644 --- a/core/hw/maple/maple_helper.cpp +++ b/core/hw/maple/maple_helper.cpp @@ -3,12 +3,12 @@ u32 maple_GetAttachedDevices(u32 bus) { - verify(MapleDevices[bus][5]!=0); + verify(MapleDevices[bus][5]!=nullptr); u32 rv=0; - + for (int i=0;i<5;i++) - rv|=(MapleDevices[bus][i]!=0?1:0)< + enum MaplePattern { MP_Start, @@ -17,7 +19,7 @@ enum MaplePattern MP_NOP = 7 }; -maple_device* MapleDevices[MAPLE_PORTS][6]; +std::shared_ptr MapleDevices[MAPLE_PORTS][6]; int maple_schid; @@ -202,13 +204,13 @@ static void maple_DoDma() } const u32 frame_header = swap_msb ? SWAP32(p_data[0]) : p_data[0]; - //Command code + //Command code u32 command = frame_header & 0xFF; - //Recipient address + //Recipient address u32 reci = (frame_header >> 8) & 0xFF;//0-5; - //Sender address + //Sender address //u32 send = (frame_header >> 16) & 0xFF; - //Number of additional words in frame + //Number of additional words in frame u32 inlen = (frame_header >> 24) & 0xFF; u32 port = getPort(reci); diff --git a/core/hw/maple/maple_if.h b/core/hw/maple/maple_if.h index a3feb59a9..04ee4510e 100644 --- a/core/hw/maple/maple_if.h +++ b/core/hw/maple/maple_if.h @@ -1,7 +1,8 @@ #pragma once #include "maple_devs.h" +#include -extern maple_device* MapleDevices[MAPLE_PORTS][6]; +extern std::shared_ptr MapleDevices[MAPLE_PORTS][6]; void maple_Init(); void maple_Reset(bool Manual); diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index e2d76bc8c..8b22c7904 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) #include @@ -42,40 +43,40 @@ #endif void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart); +void tearDownDreamConnDevices(std::shared_ptr dreamconn); class DreamcastControllerConnection { -protected: - //! The maple bus index [0,3] - const int bus; +private: + MapleMsg connection_msg; public: DreamcastControllerConnection(const DreamcastControllerConnection&) = delete; - DreamcastControllerConnection() = delete; - explicit DreamcastControllerConnection(int bus) : bus(bus) - {} + DreamcastControllerConnection() = default; + ~DreamcastControllerConnection() = default; - std::optional connect(){ - if (!establishConnection()) { + std::optional connect(int bus){ + bool result = establishConnection(bus); + + if (!result) { return std::nullopt; } // 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); + connection_msg.command = MDCF_GetCondition; + connection_msg.destAP = (bus << 6) | 0x20; + connection_msg.originAP = bus << 6; + connection_msg.setData(MFID_0_Input); - asio::error_code ec = sendMsg(msg); + asio::error_code ec = sendMsg(connection_msg); if (ec) { WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); disconnect(); return std::nullopt; } - if (!receiveMsg(msg)) { + if (!receiveMsg(connection_msg)) { WARN_LOG(INPUT, "DreamcastController[%d] read timeout", bus); disconnect(); return std::nullopt; @@ -83,34 +84,24 @@ public: onConnectComplete(); - return msg; + return connection_msg; } virtual void disconnect() = 0; virtual asio::error_code sendMsg(const MapleMsg& msg) = 0; virtual bool receiveMsg(MapleMsg& msg) = 0; + virtual std::string getName() = 0; + virtual int getDefaultBus() { + // Value of -1 means to use enumeration order + return -1; + } + virtual void gameTermination() { + // Do nothing by default + } protected: - virtual bool establishConnection() = 0; + virtual bool establishConnection(int bus) = 0; virtual void onConnectComplete() = 0; - - std::string msgToString(const MapleMsg& msg, const std::string& delim = " ") - { - 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"; - - return s.str(); - } }; class DreamConnConnection : public DreamcastControllerConnection @@ -126,16 +117,14 @@ public: public: DreamConnConnection(const DreamConnConnection&) = delete; - DreamConnConnection() = delete; - explicit DreamConnConnection(int bus) : DreamcastControllerConnection(bus) - {} + DreamConnConnection() = default; ~DreamConnConnection() { disconnect(); } - bool establishConnection() override { + bool establishConnection(int bus) override { #if !defined(_WIN32) WARN_LOG(INPUT, "DreamcastController[%d] connection failed: DreamConn+ / DreamConn S Controller supported on Windows only", bus); return false; @@ -155,29 +144,32 @@ public: iostream.expires_from_now(std::chrono::duration::max()); // don't use a 64-bit based duration to avoid overflow } - void disconnect() override - { + void disconnect() override { if (iostream) { iostream.close(); } } - asio::error_code sendMsg(const MapleMsg& msg) override - { - const std::string msgStr = msgToString(msg); - asio::error_code ec; + asio::error_code sendMsg(const MapleMsg& msg) override { + 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"; - if (!iostream) { - return asio::error::not_connected; - } asio::ip::tcp::socket& sock = static_cast(iostream.socket()); - asio::write(sock, asio::buffer(msgStr), ec); - + asio::error_code ec; + asio::write(sock, asio::buffer(s.str()), ec); return ec; } - bool receiveMsg(MapleMsg& msg) override - { + bool receiveMsg(MapleMsg& msg) override { std::string response; if (!std::getline(iostream, response)) @@ -191,16 +183,19 @@ public: return false; } + + std::string getName() override { + return "DreamConn+ / DreamConn S Controller"; + } }; -//! See: https://github.com/OrangeFox86/DreamcastControllerUsbPico -class DreamcastControllerUsbPicoConnection : public DreamcastControllerConnection +class DreamPortSerialHandler { //! Asynchronous context for serial_handler asio::io_context io_context; //! Output buffer data for serial_handler std::string serial_out_data; - //! Handles communication to DreamcastControllerUsbPico + //! Handles communication to DreamPort asio::serial_port serial_handler{io_context}; //! Set to true while an async write is in progress with serial_handler bool serial_write_in_progress = false; @@ -218,29 +213,9 @@ class DreamcastControllerUsbPicoConnection : public DreamcastControllerConnectio std::condition_variable read_cv; //! Mutex for read_cv and serializes access to read_queue std::mutex read_cv_mutex; - //! Current timeout in milliseconds - std::chrono::milliseconds timeout_ms; public: - //! Dreamcast Controller USB VID:1209 PID:2f07 - static constexpr const char* VID_PID_GUID = "09120000072f0000"; - -public: - DreamcastControllerUsbPicoConnection(const DreamcastControllerUsbPicoConnection&) = delete; - DreamcastControllerUsbPicoConnection() = delete; - - explicit DreamcastControllerUsbPicoConnection(int bus) : DreamcastControllerConnection(bus) - {} - - ~DreamcastControllerUsbPicoConnection(){ - disconnect(); - } - - bool establishConnection() override { - asio::error_code ec; - - // Timeout is 1 second while establishing connection - timeout_ms = std::chrono::seconds(1); + DreamPortSerialHandler() { // the serial port isn't ready at this point, so we need to sleep briefly // we probably should have a better way to handle this @@ -252,38 +227,163 @@ public: std::string serial_device = ""; // use user-configured serial device if available, fallback to first available - if (cfgLoadStr("input", "DreamcastControllerUsbSerialDevice", "default") != "default") { - serial_device = cfgLoadStr("input", "DreamcastControllerUsbSerialDevice", "default"); - NOTICE_LOG(INPUT, "DreamcastController[%d] connecting to user-configured serial device: %s", bus, serial_device.c_str()); - } - else - { - serial_device = getFirstSerialDevice(); - NOTICE_LOG(INPUT, "DreamcastController[%d] connecting to autoselected serial device: %s", bus, serial_device.c_str()); + serial_device = cfgLoadStr("input", "DreamPortSerialDevice", ""); + if (serial_device.empty()) { + serial_device = cfgLoadStr("input", "DreamcastControllerUsbSerialDevice", ""); + if (!serial_device.empty()) { + WARN_LOG(INPUT, "DreamcastControllerUsbSerialDevice config is deprecated; use DreamPortSerialDevice instead"); + } } + if (!serial_device.empty()) + { + NOTICE_LOG(INPUT, "DreamPort connecting to user-configured serial device: %s", serial_device.c_str()); + } else { + serial_device = getFirstSerialDevice(); + NOTICE_LOG(INPUT, "DreamPort connecting to autoselected serial device: %s", serial_device.c_str()); + } + + asio::error_code ec; serial_handler.open(serial_device, ec); if (ec || !serial_handler.is_open()) { - WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); + WARN_LOG(INPUT, "DreamPort serial connection failed: %s", ec.message().c_str()); disconnect(); - return false; + } else { + NOTICE_LOG(INPUT, "DreamPort serial connection successful!"); } // This must be done before the io_context is run because it will keep io_context from returning immediately startSerialRead(); io_context_thread = std::make_unique([this](){contextThreadEnty();}); + } + ~DreamPortSerialHandler() { + disconnect(); + io_context_thread->join(); + } + + bool is_open() const { + return serial_handler.is_open(); + } + + asio::error_code sendCmd(const std::string& cmd, std::chrono::milliseconds timeout_ms) { + asio::error_code ec; + + if (!serial_handler.is_open()) { + return asio::error::not_connected; + } + + // Wait for last write to complete + std::unique_lock lock(write_cv_mutex); + const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; + if (!write_cv.wait_until(lock, expiration, [this](){return (!serial_write_in_progress || !serial_handler.is_open());})) + { + return asio::error::timed_out; + } + + // Check again before continuing + if (!serial_handler.is_open()) { + return asio::error::not_connected; + } + + serial_out_data = cmd; + + // Clear out the read buffer before writing next command + read_queue.clear(); + serial_write_in_progress = true; + asio::async_write( + serial_handler, + asio::buffer(serial_out_data), + asio::transfer_exactly(serial_out_data.size()), + [this](const asio::error_code& error, size_t bytes_transferred) + { + std::unique_lock lock(write_cv_mutex); + if (error) { + try + { + serial_handler.cancel(); + } + catch(const asio::system_error&) + { + // Ignore cancel errors + } + } + serial_write_in_progress = false; + write_cv.notify_all(); + } + ); + + return ec; + } + + asio::error_code sendMsg(const MapleMsg& msg, int hardware_bus, std::chrono::milliseconds timeout_ms) { + // Build serial_out_data string + // Need to message the hardware bus instead of the software bus + u8 hwDestAP = (hardware_bus << 6) | (msg.destAP & 0x3F); + u8 hwOriginAP = (hardware_bus << 6) | (msg.originAP & 0x3F); + + std::ostringstream s; + s << "X "; // 'X' prefix triggers flycast command parser + s.fill('0'); + s << std::hex << std::uppercase + << std::setw(2) << (u32)msg.command + << std::setw(2) << (u32)hwDestAP // override dest + << std::setw(2) << (u32)hwOriginAP // override origin + << 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 << "\n"; + + return sendCmd(s.str(), timeout_ms); + } + + bool receiveCmd(std::string& cmd, std::chrono::milliseconds timeout_ms) + { + // Wait for at least 2 lines to be received (first line is echo back) + std::unique_lock lock(read_cv_mutex); + const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; + if (!read_cv.wait_until(lock, expiration, [this](){return ((read_queue.size() >= 2) || !serial_handler.is_open());})) + { + // Timeout + return false; + } + + if (read_queue.size() < 2) { + // Connection was closed before data could be received + return false; + } + + // discard the first message as we are interested in the second only which returns the controller configuration + cmd = std::move(read_queue.back()); + read_queue.clear(); return true; } - void onConnectComplete() override { - // Timeout is extended to 5 seconds for all other communication after connection - timeout_ms = std::chrono::seconds(5); + bool receiveMsg(MapleMsg& msg, std::chrono::milliseconds timeout_ms) + { + std::string response; + if (!receiveCmd(response, timeout_ms)) { + return false; + } + + sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); + + if (serial_handler.is_open()) { + return true; + } + else { + return false; + } + + return false; } - void disconnect() override +private: + void disconnect() { io_context.stop(); @@ -313,89 +413,6 @@ public: // This context should never exit until disconnect due to read handler automatically rearming io_context.run(); } - - asio::error_code sendMsg(const MapleMsg& msg) override - { - asio::error_code ec; - - if (!serial_handler.is_open()) { - return asio::error::not_connected; - } - - // Wait for last write to complete - std::unique_lock lock(write_cv_mutex); - const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; - if (!write_cv.wait_until(lock, expiration, [this](){return (!serial_write_in_progress || !serial_handler.is_open());})) - { - return asio::error::timed_out; - } - - // Check again before continuing - if (!serial_handler.is_open()) { - return asio::error::not_connected; - } - - // Clear out the read buffer before writing next command - read_queue.clear(); - - serial_write_in_progress = true; - // Messages to Dreamcast Controller USB need to be prefixed to trigger the correct parser - serial_out_data = std::string("X ") + msgToString(msg); - asio::async_write(serial_handler, asio::buffer(serial_out_data), asio::transfer_exactly(serial_out_data.size()), [this](const asio::error_code& error, size_t bytes_transferred) - { - std::unique_lock lock(write_cv_mutex); - if (error) { - try - { - serial_handler.cancel(); - } - catch(const asio::system_error&) - { - // Ignore cancel errors - } - } - serial_write_in_progress = false; - write_cv.notify_all(); - }); - - return ec; - } - - bool receiveMsg(MapleMsg& msg) override - { - std::string response; - - // Wait for at least 2 lines to be received (first line is echo back) - std::unique_lock lock(read_cv_mutex); - const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; - if (!read_cv.wait_until(lock, expiration, [this](){return ((read_queue.size() >= 2) || !serial_handler.is_open());})) - { - // Timeout - return false; - } - - if (read_queue.size() < 2) { - // Connection was closed before data could be received - return false; - } - - // discard the first message as we are interested in the second only which returns the controller configuration - response = std::move(read_queue.back()); - read_queue.clear(); - - sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); - - if (serial_handler.is_open()) { - return true; - } - else { - return false; - } - - return false; - } - -private: static std::string getFirstSerialDevice() { // On Windows, we get the first serial device matching our VID/PID @@ -566,29 +583,288 @@ private: } }; -DreamConn::DreamConn(int bus, int dreamcastControllerType, const std::string& name) : - bus(bus), dreamcastControllerType(dreamcastControllerType), name(name) +//! See: https://github.com/OrangeFox86/DreamPort +class DreamPortConnection : public DreamcastControllerConnection { - change_bus(bus); + //! The one and only serial port + static std::unique_ptr serial; + //! Number of devices using the above serial + static std::atomic connected_dev_count; + //! Current timeout in milliseconds + std::chrono::milliseconds timeout_ms; + //! The bus ID dictated by flycast + int software_bus = -1; + //! The bus index of the hardware connection which will differ from the software bus + int hardware_bus = -1; + //! true iff only a single devices was found when enumerating devices + bool is_single_device = true; + //! True when initial enumeration failed + bool is_hardware_bus_implied = true; + //! True once connection is established + bool connection_established = false; + +public: + //! Dreamcast Controller USB VID:1209 PID:2f07 + static constexpr const std::uint16_t VID = 0x1209; + static constexpr const std::uint16_t PID = 0x2f07; + static constexpr const char* VID_PID_GUID = "09120000072f0000"; + +public: + DreamPortConnection(const DreamPortConnection&) = delete; + DreamPortConnection() = delete; + + DreamPortConnection(int joystick_idx, SDL_Joystick* sdl_joystick) : + DreamcastControllerConnection() + { +#if defined(_WIN32) + // Workaround: Getting the instance ID here fixes some sort of L/R trigger bug in Windows dinput for some reason + (void)SDL_JoystickGetDeviceInstanceID(joystick_idx); +#endif + determineHardwareBus(joystick_idx, sdl_joystick); + } + + ~DreamPortConnection(){ + disconnect(); + } + + int hardwareBus() const { + return hardware_bus; + } + + bool isHardwareBusImplied() const { + return is_hardware_bus_implied; + } + + bool isSingleDevice() const { + return is_single_device; + } + + bool establishConnection(int bus) override { + // Timeout is 1 second while establishing connection + timeout_ms = std::chrono::seconds(1); + + software_bus = bus; + + if (connection_established && serial) { + if (serial->is_open()) { + // This equipment is fixed to the hardware bus - the software bus isn't relevant + sendPort(); + return true; + } else { + disconnect(); + return false; + } + } + + ++connected_dev_count; + connection_established = true; + if (!serial) { + serial = std::make_unique(); + } + + if (serial && serial->is_open()) { + sendPort(); + return true; + } else { + disconnect(); + return false; + } + } + + void sendPort() { + if (connection_established && software_bus >= 0 && software_bus <= 3 && hardware_bus >=0 && hardware_bus <= 3) { + // This will update the displayed port letter on the screen + std::ostringstream s; + s << "XP "; // XP is flycast "set port" command + s << hardware_bus << " " << software_bus << "\n"; + serial->sendCmd(s.str(), timeout_ms); + // Don't really care about the response, just want to ensure it gets fully processed before continuing + std::string buffer; + serial->receiveCmd(buffer, timeout_ms); + } + } + + void onConnectComplete() override { + // Timeout is extended to 5 seconds for all other communication after connection + timeout_ms = std::chrono::seconds(5); + } + + void disconnect() override { + if (connection_established) { + connection_established = false; + if (--connected_dev_count == 0) { + // serial is no longer needed + serial.reset(); + } + } + } + + asio::error_code sendMsg(const MapleMsg& msg) override { + if (serial) { + return serial->sendMsg(msg, hardware_bus, timeout_ms); + } + + return asio::error::not_connected; + } + + bool receiveMsg(MapleMsg& msg) override { + if (serial) { + return serial->receiveMsg(msg, timeout_ms); + } + + return false; + } + + std::string getName() override { + std::string name = "DreamPort"; + if (!is_hardware_bus_implied && !is_single_device) { + const char portChar = ('A' + hardware_bus); + name += " " + std::string(1, portChar); + } + return name; + } + + int getDefaultBus() override { + if (!is_hardware_bus_implied && !is_single_device) { + return hardware_bus; + } else { + // Value of -1 means to use enumeration order + return -1; + } + } + + void gameTermination() override { + // Reset screen to selected port + sendPort(); + } + +private: + void determineHardwareBus(int joystick_idx, SDL_Joystick* sdl_joystick) { + // This function determines what bus index to use when communicating with the hardware. +#if defined(_WIN32) + // This only works in Windows because the joystick_path is not given in other OSes + const char* joystick_name = SDL_JoystickName(sdl_joystick); + const char* joystick_path = SDL_JoystickPath(sdl_joystick); + + struct SDL_hid_device_info* devs = SDL_hid_enumerate(VID, PID); + if (devs) { + if (!devs->next) { + // Only single device found, so this is simple (host-1p firmware used) + hardware_bus = 0; + is_hardware_bus_implied = false; + is_single_device = true; + } else { + struct SDL_hid_device_info* it = devs; + struct SDL_hid_device_info* my_dev = nullptr; + + if (joystick_path) + { + while (it) + { + // Note: hex characters will be differing case, so case-insensitive cmp is needed + if (it->path && 0 == SDL_strcasecmp(it->path, joystick_path)) { + my_dev = it; + break; + } + it = it->next; + } + } + + if (my_dev) { + it = devs; + int count = 0; + if (my_dev->serial_number) { + while (it) { + if (it->serial_number && + 0 == wcscmp(it->serial_number, my_dev->serial_number)) + { + ++count; + } + it = it->next; + } + + if (count == 1) { + // Single device of this serial found + is_single_device = true; + hardware_bus = 0; + is_hardware_bus_implied = false; + } else { + is_single_device = false; + if (my_dev->release_number < 0x0102) { + // Interfaces go in decending order + hardware_bus = (count - (my_dev->interface_number % 4) - 1); + is_hardware_bus_implied = false; + } else { + // Version 1.02 of interface will make interfaces in ascending order + hardware_bus = (my_dev->interface_number % 4); + is_hardware_bus_implied = false; + } + } + } + } + } + SDL_hid_free_enumeration(devs); + } +#endif + + if (hardware_bus < 0) { + // The number of buttons gives a clue as to what index the controller is + int nbuttons = SDL_JoystickNumButtons(sdl_joystick); + + if (nbuttons >= 32 || nbuttons <= 27) { + // Older version of firmware or single player + hardware_bus = 0; + is_hardware_bus_implied = true; + is_single_device = true; + } + else { + hardware_bus = 31 - nbuttons; + is_hardware_bus_implied = false; + is_single_device = false; + } + } + } +}; + +// Define the static instances here +std::unique_ptr DreamPortConnection::serial; +std::atomic DreamPortConnection::connected_dev_count = 0; + +DreamConn::DreamConn(int bus, int dreamcastControllerType, int joystick_idx, SDL_Joystick* sdl_joystick) : + bus(bus), dreamcastControllerType(dreamcastControllerType) +{ + switch (dreamcastControllerType) + { + case TYPE_DREAMCONN: + dcConnection = std::make_unique(); + break; + + case TYPE_DREAMPORT: + dcConnection = std::make_unique(joystick_idx, sdl_joystick); + break; + } } DreamConn::~DreamConn() { disconnect(); } -void DreamConn::change_bus(int bus) { - disconnect(); - dcConnection.reset(); - switch (dreamcastControllerType) - { - case TYPE_DREAMCONN: - dcConnection = std::make_unique(bus); - break; - - case TYPE_DREAMCASTCONTROLLERUSB: - dcConnection = std::make_unique(bus); - break; +int DreamConn::getDefaultBus() { + if (dcConnection) { + return dcConnection->getDefaultBus(); } + return -1; +} + +void DreamConn::changeBus(int newBus) { + bus = newBus; +} + +std::string DreamConn::getName() { + if (dcConnection) { + return dcConnection->getName(); + } + return "Unknown DreamConn"; } void DreamConn::connect() @@ -604,7 +880,7 @@ void DreamConn::connect() return; } - std::optional msg = dcConnection->connect(); + std::optional msg = dcConnection->connect(bus); if (!msg) { return; @@ -617,7 +893,7 @@ void DreamConn::connect() if (hasVmu() || hasRumble()) { - NOTICE_LOG(INPUT, "Connected to DreamcastController[%d]: Type:%s, VMU:%d, Rumble Pack:%d", bus, name.c_str(), 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 @@ -662,6 +938,13 @@ bool DreamConn::send(const MapleMsg& msg) return true; } +void DreamConn::gameTermination() +{ + if (dcConnection) { + dcConnection->gameTermination(); + } +} + bool DreamConnGamepad::isDreamcastController(int deviceIndex) { char guid_str[33] {}; @@ -673,7 +956,7 @@ bool DreamConnGamepad::isDreamcastController(int deviceIndex) // DreamConn VID:4457 PID:4443 // Dreamcast Controller USB VID:1209 PID:2f07 if (memcmp(DreamConnConnection::VID_PID_GUID, guid_str + 8, 16) == 0 || - memcmp(DreamcastControllerUsbPicoConnection::VID_PID_GUID, guid_str + 8, 16) == 0) + memcmp(DreamPortConnection::VID_PID_GUID, guid_str + 8, 16) == 0) { NOTICE_LOG(INPUT, "Dreamcast controller found!"); return true; @@ -692,22 +975,34 @@ DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystic // Dreamcast Controller USB VID:1209 PID:2f07 if (memcmp(DreamConnConnection::VID_PID_GUID, guid_str + 8, 16) == 0) { - _name = "DreamConn+ / DreamConn S Controller"; - dreamconn = std::make_shared(maple_port, TYPE_DREAMCONN, _name); + dreamconn = std::make_shared(maple_port, TYPE_DREAMCONN, joystick_idx, sdl_joystick); } - else if (memcmp(DreamcastControllerUsbPicoConnection::VID_PID_GUID, guid_str + 8, 16) == 0) + else if (memcmp(DreamPortConnection::VID_PID_GUID, guid_str + 8, 16) == 0) { - _name = "Dreamcast Controller USB"; - dreamconn = std::make_shared(maple_port, TYPE_DREAMCASTCONTROLLERUSB, _name); + dreamconn = std::make_shared(maple_port, TYPE_DREAMPORT, joystick_idx, sdl_joystick); + } + + if (dreamconn) { + _name = dreamconn->getName(); + int defaultBus = dreamconn->getDefaultBus(); + if (defaultBus >= 0 && defaultBus < 4) { + set_maple_port(defaultBus); + } } EventManager::listen(Event::Start, handleEvent, this); EventManager::listen(Event::LoadState, handleEvent, this); + EventManager::listen(Event::Terminate, handleEvent, this); } DreamConnGamepad::~DreamConnGamepad() { EventManager::unlisten(Event::Start, handleEvent, this); EventManager::unlisten(Event::LoadState, handleEvent, this); + EventManager::unlisten(Event::Terminate, handleEvent, this); + if (dreamconn) { + tearDownDreamConnDevices(dreamconn); + dreamconn.reset(); + } } void DreamConnGamepad::set_maple_port(int port) @@ -717,7 +1012,7 @@ void DreamConnGamepad::set_maple_port(int port) dreamconn->disconnect(); } else if (dreamconn->getBus() != port) { - dreamconn->change_bus(port); + dreamconn->changeBus(port); if (is_registered()) { dreamconn->connect(); } @@ -739,6 +1034,11 @@ void DreamConnGamepad::handleEvent(Event event, void *arg) DreamConnGamepad *gamepad = static_cast(arg); if (gamepad->dreamconn != nullptr) createDreamConnDevices(gamepad->dreamconn, event == Event::Start); + + if (gamepad->dreamconn != nullptr && event == Event::Terminate) + { + gamepad->dreamconn->gameTermination(); + } } bool DreamConnGamepad::gamepad_btn_input(u32 code, bool pressed) @@ -788,6 +1088,8 @@ void DreamConn::connect() { } void DreamConn::disconnect() { } +void DreamConn::gameTermination() { +} bool DreamConnGamepad::isDreamcastController(int deviceIndex) { return false; diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h index 8a0f98513..8d3f9c74a 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -23,7 +23,7 @@ #if (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))) && !defined(TARGET_UWP) #define USE_DREAMCASTCONTROLLER 1 #define TYPE_DREAMCONN 1 -#define TYPE_DREAMCASTCONTROLLERUSB 2 +#define TYPE_DREAMPORT 2 #include #endif #include @@ -52,7 +52,6 @@ class DreamConn { int bus = -1; const int dreamcastControllerType; - const std::string name; #ifdef USE_DREAMCASTCONTROLLER std::unique_ptr dcConnection; #endif @@ -60,12 +59,15 @@ class DreamConn u8 expansionDevs = 0; public: - DreamConn(int bus, int dreamcastControllerType, const std::string& name); + DreamConn(int bus, int dreamcastControllerType, int joystick_idx, SDL_Joystick* sdl_joystick); ~DreamConn(); bool send(const MapleMsg& msg); + // When called, do teardown stuff like reset screen + void gameTermination(); + int getBus() const { return bus; } @@ -76,7 +78,11 @@ public: return expansionDevs & 2; } - void change_bus(int bus); + int getDefaultBus(); + + void changeBus(int newBus); + + std::string getName(); void connect(); void disconnect(); diff --git a/core/sdl/sdl.cpp b/core/sdl/sdl.cpp index 794c2f315..09acb494c 100644 --- a/core/sdl/sdl.cpp +++ b/core/sdl/sdl.cpp @@ -271,9 +271,9 @@ void input_sdl_init() // Linux mappings are OK by default // Can be removed once mapping is merged into SDL, see https://github.com/libsdl-org/SDL/pull/12039 #if (defined(__APPLE__) && defined(TARGET_OS_MAC)) - SDL_GameControllerAddMapping("0300000009120000072f000000010000,OrangeFox86 Dreamcast Controller USB,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:a2,righttrigger:a5,start:b11"); + SDL_GameControllerAddMapping("0300000009120000072f000000010000,OrangeFox86 DreamPort,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:a2,righttrigger:a5,start:b11"); #elif defined(_WIN32) - SDL_GameControllerAddMapping("0300000009120000072f000000000000,OrangeFox86 Dreamcast Controller USB,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:-a2,righttrigger:-a5,start:b11"); + SDL_GameControllerAddMapping("0300000009120000072f000000000000,OrangeFox86 DreamPort,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:-a2,righttrigger:-a5,start:b11"); #endif }