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 <mike@kosek.de>
This commit is contained in:
James 2025-02-16 09:36:57 -07:00 committed by GitHub
parent 0414a90500
commit 4e74619d9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 664 additions and 264 deletions

View File

@ -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<maple_device> 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<const maple_device> 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<maple_device> 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<maple_naomi_jamma> 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<maple_naomi_jamma>(MapleDevices[0][5]);
}

View File

@ -1,6 +1,7 @@
#pragma once
#include "types.h"
#include <cstring>
#include <memory>
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<maple_naomi_jamma> getMieDevice();

View File

@ -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<maple_device> mapleDev = MapleDevices[1 + playerNum][5];
if (mapleDev != nullptr && mapleDev->get_device_type() == MDT_RFIDReaderWriter)
((RFIDReaderWriter *)mapleDev)->insertCard();
std::static_pointer_cast<RFIDReaderWriter>(mapleDev)->insertCard();
}
void setRfidCardData(int playerNum, u8 *data)
{
maple_device *mapleDev = MapleDevices[1 + playerNum][5];
std::shared_ptr<maple_device> mapleDev = MapleDevices[1 + playerNum][5];
if (mapleDev != nullptr && mapleDev->get_device_type() == MDT_RFIDReaderWriter)
((RFIDReaderWriter *)mapleDev)->setCardData(data);
std::static_pointer_cast<RFIDReaderWriter>(mapleDev)->setCardData(data);
}
const u8 *getRfidCardData(int playerNum)
{
maple_device *mapleDev = MapleDevices[1 + playerNum][5];
std::shared_ptr<maple_device> mapleDev = MapleDevices[1 + playerNum][5];
if (mapleDev != nullptr && mapleDev->get_device_type() == MDT_RFIDReaderWriter)
return ((RFIDReaderWriter *)mapleDev)->getCardData();
return std::static_pointer_cast<RFIDReaderWriter>(mapleDev)->getCardData();
else
return nullptr;
}
maple_device* maple_Create(MapleDeviceType type)
std::shared_ptr<maple_device> maple_Create(MapleDeviceType type)
{
switch(type)
{
case MDT_SegaController:
if (!settings.platform.isAtomiswave())
return new maple_sega_controller();
return std::make_shared<maple_sega_controller>();
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<maple_atomiswave_controller>();
case MDT_Microphone: return std::make_shared<maple_microphone>();
case MDT_SegaVMU: return std::make_shared<maple_sega_vmu>();
case MDT_PurupuruPack: return std::make_shared<maple_sega_purupuru>();
case MDT_Keyboard: return std::make_shared<maple_keyboard>();
case MDT_Mouse: return std::make_shared<maple_mouse>();
case MDT_LightGun:
if (!settings.platform.isAtomiswave())
return new maple_lightgun();
return std::make_shared<maple_lightgun>();
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<atomiswave_lightgun>();
case MDT_NaomiJamma: return std::make_shared<maple_naomi_jamma>();
case MDT_TwinStick: return std::make_shared<maple_sega_twinstick>();
case MDT_AsciiStick: return std::make_shared<maple_ascii_stick>();
case MDT_MaracasController: return std::make_shared<maple_maracas_controller>();
case MDT_FishingController: return std::make_shared<maple_fishing_controller>();
case MDT_PopnMusicController: return std::make_shared<maple_popnmusic_controller>();
case MDT_RacingController: return std::make_shared<maple_racing_controller>();
case MDT_DenshaDeGoController: return std::make_shared<maple_densha_controller>();
case MDT_SegaControllerXL: return std::make_shared<FullController>();
case MDT_RFIDReaderWriter: return std::make_shared<RFIDReaderWriter>();
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 <list>
#include <memory>
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<maple_sega_vmu> 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<maple_sega_vmu> 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<std::shared_ptr<DreamConnVmu>> dreamConnVmus;
static std::list<std::shared_ptr<DreamConnPurupuru>> dreamConnPurupurus;
void createDreamConnDevices(std::shared_ptr<DreamConn> 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<DreamConnVmu> vmu;
for (const std::shared_ptr<DreamConnVmu>& vmuIter : dreamConnVmus)
{
if (vmuIter->dreamconn.get() == dreamconn.get())
{
vmuFound = true;
vmu = vmuIter;
break;
}
}
std::shared_ptr<maple_device> 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<maple_sega_vmu*>(dev));
vmu->updateScreen();
bool vmuCreated = false;
if (!vmu)
{
vmu = std::make_shared<DreamConnVmu>(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<maple_sega_vmu>(dev));
if (!gameStart) {
vmu->updateScreen();
}
}
if (!vmuFound) dreamConnVmus.push_back(vmu);
}
}
if (dreamconn->hasRumble())
{
maple_device *dev = MapleDevices[bus][1];
std::shared_ptr<DreamConnPurupuru> rumble;
for (const std::shared_ptr<DreamConnPurupuru>& purupuru : dreamConnPurupurus)
{
if (purupuru->dreamconn.get() == dreamconn.get())
{
rumbleFound = true;
rumble = purupuru;
break;
}
}
std::shared_ptr<maple_device> 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<DreamConnPurupuru>(dreamconn);
}
rumble->Setup(bus, 1);
if (!rumbleFound) dreamConnPurupurus.push_back(rumble);
}
}
}
void tearDownDreamConnDevices(std::shared_ptr<DreamConn> dreamconn)
{
const int bus = dreamconn->getBus();
for (std::list<std::shared_ptr<DreamConnVmu>>::const_iterator iter = dreamConnVmus.begin();
iter != dreamConnVmus.end();)
{
if ((*iter)->dreamconn.get() == dreamconn.get())
{
std::shared_ptr<maple_device> dev = maple_Create(MDT_SegaVMU);
dev->Setup(bus, 0);
(*iter)->copyOut(std::static_pointer_cast<maple_sega_vmu>(dev));
iter = dreamConnVmus.erase(iter);
break;
}
else
{
++iter;
}
}
for (std::list<std::shared_ptr<DreamConnPurupuru>>::const_iterator iter = dreamConnPurupurus.begin();
iter != dreamConnPurupurus.end();)
{
if ((*iter)->dreamconn.get() == dreamconn.get())
{
std::shared_ptr<maple_device> dev = maple_Create(MDT_PurupuruPack);
dev->Setup(bus, 1);
iter = dreamConnPurupurus.erase(iter);
break;
}
else
{
++iter;
}
}
}

View File

@ -121,7 +121,7 @@ enum AWAVE_KEYS
AWAVE_TRIGGER_KEY = 1 << 17,
};
struct maple_device
struct maple_device : public std::enable_shared_from_this<maple_device>
{
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_device> maple_Create(MapleDeviceType type);
#define MAPLE_PORTS 4

View File

@ -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)<<i;
rv|=(MapleDevices[bus][i]!=nullptr?1:0)<<i;
return rv;
}

View File

@ -8,6 +8,8 @@
#include "network/ggpo.h"
#include "hw/naomi/card_reader.h"
#include <memory>
enum MaplePattern
{
MP_Start,
@ -17,7 +19,7 @@ enum MaplePattern
MP_NOP = 7
};
maple_device* MapleDevices[MAPLE_PORTS][6];
std::shared_ptr<maple_device> 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);

View File

@ -1,7 +1,8 @@
#pragma once
#include "maple_devs.h"
#include <memory>
extern maple_device* MapleDevices[MAPLE_PORTS][6];
extern std::shared_ptr<maple_device> MapleDevices[MAPLE_PORTS][6];
void maple_Init();
void maple_Reset(bool Manual);

View File

@ -31,6 +31,7 @@
#include <list>
#include <mutex>
#include <condition_variable>
#include <atomic>
#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))
#include <dirent.h>
@ -42,40 +43,40 @@
#endif
void createDreamConnDevices(std::shared_ptr<DreamConn> dreamconn, bool gameStart);
void tearDownDreamConnDevices(std::shared_ptr<DreamConn> 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<MapleMsg> connect(){
if (!establishConnection()) {
std::optional<MapleMsg> 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<u32>::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<asio::ip::tcp::socket&>(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<std::thread>([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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<DreamPortSerialHandler> serial;
//! Number of devices using the above serial
static std::atomic<std::uint32_t> 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<DreamPortSerialHandler>();
}
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<DreamPortSerialHandler> DreamPortConnection::serial;
std::atomic<std::uint32_t> 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<DreamConnConnection>();
break;
case TYPE_DREAMPORT:
dcConnection = std::make_unique<DreamPortConnection>(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<DreamConnConnection>(bus);
break;
case TYPE_DREAMCASTCONTROLLERUSB:
dcConnection = std::make_unique<DreamcastControllerUsbPicoConnection>(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<MapleMsg> msg = dcConnection->connect();
std::optional<MapleMsg> 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<DreamConn>(maple_port, TYPE_DREAMCONN, _name);
dreamconn = std::make_shared<DreamConn>(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<DreamConn>(maple_port, TYPE_DREAMCASTCONTROLLERUSB, _name);
dreamconn = std::make_shared<DreamConn>(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<DreamConnGamepad*>(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;

View File

@ -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 <asio.hpp>
#endif
#include <memory>
@ -52,7 +52,6 @@ class DreamConn
{
int bus = -1;
const int dreamcastControllerType;
const std::string name;
#ifdef USE_DREAMCASTCONTROLLER
std::unique_ptr<class DreamcastControllerConnection> 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();

View File

@ -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
}