Serial comm update (#1820)
* Broke apart DreamConn and DreamcastControllerUsb implementations; added serial timeouts; process serial in thread; removed crc from DreamcastControllerUsb sdl definition * Added missing include statements * Changed INFO_LOG to NOTICE_LOG on a couple of lines * Clear the read queue once string is pulled off of it
This commit is contained in:
parent
d84108de32
commit
a35e8214df
|
@ -26,6 +26,11 @@
|
|||
#include <asio.hpp>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
|
||||
#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))
|
||||
#include <dirent.h>
|
||||
|
@ -38,256 +43,568 @@
|
|||
|
||||
void createDreamConnDevices(std::shared_ptr<DreamConn> dreamconn, bool gameStart);
|
||||
|
||||
static asio::error_code sendMsg(const MapleMsg& msg, asio::ip::tcp::iostream& stream, asio::io_context& io_context, asio::serial_port& serial_handler, int dreamcastControllerType)
|
||||
class DreamcastControllerConnection
|
||||
{
|
||||
std::ostringstream s;
|
||||
s.fill('0');
|
||||
if (dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB)
|
||||
{
|
||||
// Messages to Dreamcast Controller USB need to be prefixed to trigger the correct parser
|
||||
s << "X ";
|
||||
}
|
||||
|
||||
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::error_code ec;
|
||||
|
||||
if (dreamcastControllerType == TYPE_DREAMCONN)
|
||||
{
|
||||
if (!stream)
|
||||
return asio::error::not_connected;
|
||||
asio::ip::tcp::socket& sock = static_cast<asio::ip::tcp::socket&>(stream.socket());
|
||||
asio::write(sock, asio::buffer(s.str()), ec);
|
||||
}
|
||||
else if (dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB)
|
||||
{
|
||||
io_context.run();
|
||||
io_context.reset();
|
||||
|
||||
if (!serial_handler.is_open())
|
||||
return asio::error::not_connected;
|
||||
asio::async_write(serial_handler, asio::buffer(s.str()), asio::transfer_exactly(s.str().size()), [ &serial_handler](const asio::error_code& error, size_t bytes_transferred)
|
||||
{
|
||||
if (error) {
|
||||
serial_handler.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return ec;
|
||||
}
|
||||
protected:
|
||||
//! The maple bus index [0,3]
|
||||
const int bus;
|
||||
|
||||
static bool receiveMsg(MapleMsg& msg, std::istream& stream, asio::serial_port& serial_handler, int dreamcastControllerType)
|
||||
{
|
||||
std::string response;
|
||||
|
||||
if (dreamcastControllerType == TYPE_DREAMCONN)
|
||||
public:
|
||||
DreamcastControllerConnection(const DreamcastControllerConnection&) = delete;
|
||||
DreamcastControllerConnection() = delete;
|
||||
|
||||
explicit DreamcastControllerConnection(int bus) : bus(bus)
|
||||
{}
|
||||
|
||||
std::optional<MapleMsg> connect(){
|
||||
if (!establishConnection()) {
|
||||
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);
|
||||
|
||||
asio::error_code ec = sendMsg(msg);
|
||||
if (ec)
|
||||
{
|
||||
WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str());
|
||||
disconnect();
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!receiveMsg(msg)) {
|
||||
WARN_LOG(INPUT, "DreamcastController[%d] read timeout", bus);
|
||||
disconnect();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
onConnectComplete();
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
virtual void disconnect() = 0;
|
||||
virtual asio::error_code sendMsg(const MapleMsg& msg) = 0;
|
||||
virtual bool receiveMsg(MapleMsg& msg) = 0;
|
||||
|
||||
protected:
|
||||
virtual bool establishConnection() = 0;
|
||||
virtual void onConnectComplete() = 0;
|
||||
|
||||
std::string msgToString(const MapleMsg& msg, const std::string& delim = " ")
|
||||
{
|
||||
if (!std::getline(stream, response))
|
||||
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
|
||||
{
|
||||
//! Base port of communication to DreamConn
|
||||
static constexpr u16 BASE_PORT = 37393;
|
||||
//! Stream to a DreamConn device
|
||||
asio::ip::tcp::iostream iostream;
|
||||
|
||||
public:
|
||||
//! DreamConn VID:4457 PID:4443
|
||||
static constexpr const char* VID_PID_GUID = "5744000043440000";
|
||||
|
||||
public:
|
||||
DreamConnConnection(const DreamConnConnection&) = delete;
|
||||
DreamConnConnection() = delete;
|
||||
|
||||
explicit DreamConnConnection(int bus) : DreamcastControllerConnection(bus)
|
||||
{}
|
||||
|
||||
~DreamConnConnection() {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
bool establishConnection() override {
|
||||
#if !defined(_WIN32)
|
||||
WARN_LOG(INPUT, "DreamcastController[%d] connection failed: DreamConn+ / DreamConn S Controller supported on Windows only", bus);
|
||||
return false;
|
||||
#else
|
||||
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 false;
|
||||
}
|
||||
iostream.expires_from_now(std::chrono::seconds(1));
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void onConnectComplete() override {
|
||||
iostream.expires_from_now(std::chrono::duration<u32>::max()); // don't use a 64-bit based duration to avoid overflow
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
return ec;
|
||||
}
|
||||
|
||||
bool receiveMsg(MapleMsg& msg) override
|
||||
{
|
||||
std::string response;
|
||||
|
||||
if (!std::getline(iostream, 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();
|
||||
return !iostream.fail();
|
||||
|
||||
return false;
|
||||
}
|
||||
else if (dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB)
|
||||
};
|
||||
|
||||
//! See: https://github.com/OrangeFox86/DreamcastControllerUsbPico
|
||||
class DreamcastControllerUsbPicoConnection : public DreamcastControllerConnection
|
||||
{
|
||||
//! 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
|
||||
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;
|
||||
//! Signaled when serial_write_in_progress transitions to false
|
||||
std::condition_variable write_cv;
|
||||
//! Mutex for write_cv and serializes access to serial_write_in_progress
|
||||
std::mutex write_cv_mutex;
|
||||
//! Input stream buffer from serial_handler
|
||||
asio::streambuf serial_read_buffer;
|
||||
//! Thread which runs the io_context
|
||||
std::unique_ptr<std::thread> io_context_thread;
|
||||
//! Contains queue of incoming lines from serial
|
||||
std::list<std::string> read_queue;
|
||||
//! Signaled when data is in read_queue
|
||||
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);
|
||||
|
||||
// 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
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
|
||||
serial_handler = asio::serial_port(io_context);
|
||||
io_context.reset();
|
||||
|
||||
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_handler.open(serial_device, ec);
|
||||
|
||||
if (ec || !serial_handler.is_open()) {
|
||||
WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str());
|
||||
disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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();});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void onConnectComplete() override {
|
||||
// Timeout is extended to 5 seconds for all other communication after connection
|
||||
timeout_ms = std::chrono::seconds(5);
|
||||
}
|
||||
|
||||
void disconnect() override
|
||||
{
|
||||
io_context.stop();
|
||||
|
||||
if (serial_handler.is_open()) {
|
||||
try
|
||||
{
|
||||
serial_handler.cancel();
|
||||
}
|
||||
catch(const asio::system_error&)
|
||||
{
|
||||
// Ignore cancel errors
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
serial_handler.close();
|
||||
}
|
||||
catch(const asio::system_error&)
|
||||
{
|
||||
// Ignore closing errors
|
||||
}
|
||||
}
|
||||
|
||||
void contextThreadEnty()
|
||||
{
|
||||
// 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;
|
||||
|
||||
char c;
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
// discard the first message as we are interested in the second only which returns the controller configuration
|
||||
response = "";
|
||||
while (serial_handler.read_some(asio::buffer(&c, 1), ec) > 0)
|
||||
{
|
||||
if (!serial_handler.is_open())
|
||||
return false;
|
||||
if (c == '\n')
|
||||
break;
|
||||
response += c;
|
||||
}
|
||||
response.pop_back();
|
||||
|
||||
if (!serial_handler.is_open()) {
|
||||
return asio::error::not_connected;
|
||||
}
|
||||
|
||||
sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size);
|
||||
|
||||
if (!ec && serial_handler.is_open())
|
||||
return true;
|
||||
else
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static std::string getFirstSerialDevice() {
|
||||
|
||||
// On Windows, we get the first serial device matching our VID/PID
|
||||
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
|
||||
#if defined(_WIN32)
|
||||
HDEVINFO deviceInfoSet = SetupDiGetClassDevs(NULL, "USB", NULL, DIGCF_PRESENT | DIGCF_ALLCLASSES);
|
||||
if (deviceInfoSet == INVALID_HANDLE_VALUE) {
|
||||
return "";
|
||||
}
|
||||
HDEVINFO deviceInfoSet = SetupDiGetClassDevs(NULL, "USB", NULL, DIGCF_PRESENT | DIGCF_ALLCLASSES);
|
||||
if (deviceInfoSet == INVALID_HANDLE_VALUE) {
|
||||
return "";
|
||||
}
|
||||
|
||||
SP_DEVINFO_DATA deviceInfoData;
|
||||
deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
|
||||
SP_DEVINFO_DATA deviceInfoData;
|
||||
deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
|
||||
|
||||
for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &deviceInfoData); ++i) {
|
||||
DWORD dataType, bufferSize = 0;
|
||||
SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, NULL, 0, &bufferSize);
|
||||
for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &deviceInfoData); ++i) {
|
||||
DWORD dataType, bufferSize = 0;
|
||||
SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, NULL, 0, &bufferSize);
|
||||
|
||||
if (bufferSize > 0) {
|
||||
std::vector<char> buffer(bufferSize);
|
||||
if (SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, (PBYTE)buffer.data(), bufferSize, NULL)) {
|
||||
std::string hardwareId(buffer.begin(), buffer.end());
|
||||
if (hardwareId.find("VID_1209") != std::string::npos && hardwareId.find("PID_2F07") != std::string::npos) {
|
||||
HKEY deviceKey = SetupDiOpenDevRegKey(deviceInfoSet, &deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ);
|
||||
if (deviceKey != INVALID_HANDLE_VALUE) {
|
||||
char portName[256];
|
||||
DWORD portNameSize = sizeof(portName);
|
||||
if (RegQueryValueEx(deviceKey, "PortName", NULL, NULL, (LPBYTE)portName, &portNameSize) == ERROR_SUCCESS) {
|
||||
if (bufferSize > 0) {
|
||||
std::vector<char> buffer(bufferSize);
|
||||
if (SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, (PBYTE)buffer.data(), bufferSize, NULL)) {
|
||||
std::string hardwareId(buffer.begin(), buffer.end());
|
||||
if (hardwareId.find("VID_1209") != std::string::npos && hardwareId.find("PID_2F07") != std::string::npos) {
|
||||
HKEY deviceKey = SetupDiOpenDevRegKey(deviceInfoSet, &deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ);
|
||||
if (deviceKey != INVALID_HANDLE_VALUE) {
|
||||
char portName[256];
|
||||
DWORD portNameSize = sizeof(portName);
|
||||
if (RegQueryValueEx(deviceKey, "PortName", NULL, NULL, (LPBYTE)portName, &portNameSize) == ERROR_SUCCESS) {
|
||||
RegCloseKey(deviceKey);
|
||||
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
||||
return std::string(portName);
|
||||
}
|
||||
RegCloseKey(deviceKey);
|
||||
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
||||
return std::string(portName);
|
||||
}
|
||||
RegCloseKey(deviceKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
||||
return "";
|
||||
|
||||
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
||||
return "";
|
||||
#endif
|
||||
|
||||
|
||||
#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))
|
||||
// On MacOS/Linux, we get the first serial device matching the device prefix
|
||||
std::string device_prefix = "";
|
||||
#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))
|
||||
|
||||
|
||||
#if defined(__linux__)
|
||||
device_prefix = "ttyACM";
|
||||
device_prefix = "ttyACM";
|
||||
#elif (defined(__APPLE__) && defined(TARGET_OS_MAC))
|
||||
device_prefix = "tty.usbmodem";
|
||||
device_prefix = "tty.usbmodem";
|
||||
#endif
|
||||
|
||||
std::string path = "/dev/";
|
||||
DIR *dir;
|
||||
struct dirent *ent;
|
||||
if ((dir = opendir(path.c_str())) != NULL) {
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
std::string device = ent->d_name;
|
||||
if (device.find(device_prefix) != std::string::npos) {
|
||||
closedir(dir);
|
||||
return path + device;
|
||||
|
||||
std::string path = "/dev/";
|
||||
DIR *dir;
|
||||
struct dirent *ent;
|
||||
if ((dir = opendir(path.c_str())) != NULL) {
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
std::string device = ent->d_name;
|
||||
if (device.find(device_prefix) != std::string::npos) {
|
||||
closedir(dir);
|
||||
return path + device;
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
return "";
|
||||
#endif
|
||||
}
|
||||
|
||||
void startSerialRead()
|
||||
{
|
||||
serialReadHandler(asio::error_code(), 0);
|
||||
// Just to make sure initial data is cleared off of incoming buffer
|
||||
io_context.poll_one();
|
||||
read_queue.clear();
|
||||
}
|
||||
|
||||
void serialReadHandler(const asio::error_code& error, std::size_t size)
|
||||
{
|
||||
if (error) {
|
||||
std::lock_guard<std::mutex> lock(read_cv_mutex);
|
||||
try
|
||||
{
|
||||
serial_handler.cancel();
|
||||
}
|
||||
catch(const asio::system_error&)
|
||||
{
|
||||
// Ignore cancel errors
|
||||
}
|
||||
read_cv.notify_all();
|
||||
}
|
||||
else {
|
||||
// Rearm the read
|
||||
asio::async_read_until(
|
||||
serial_handler,
|
||||
serial_read_buffer,
|
||||
'\n',
|
||||
[this](const asio::error_code& error, std::size_t size) -> void {
|
||||
if (size > 0)
|
||||
{
|
||||
// Lock access to read_queue
|
||||
std::lock_guard<std::mutex> lock(read_cv_mutex);
|
||||
// Consume the received data
|
||||
if (consumeReadBuffer() > 0)
|
||||
{
|
||||
// New lines available
|
||||
read_cv.notify_all();
|
||||
}
|
||||
}
|
||||
// Auto reload read - io_context will always have work to do
|
||||
serialReadHandler(error, size);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int consumeReadBuffer() {
|
||||
if (serial_read_buffer.size() <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int numberOfLines = 0;
|
||||
while (true)
|
||||
{
|
||||
char c = '\0';
|
||||
std::string line;
|
||||
|
||||
// Consume characters until buffers are empty or \n found
|
||||
asio::const_buffers_1 data = serial_read_buffer.data();
|
||||
std::size_t consumed = 0;
|
||||
for (const asio::const_buffer& buff : data)
|
||||
{
|
||||
const char* buffDat = static_cast<const char*>(buff.data());
|
||||
for (std::size_t i = 0; i < buff.size(); ++i)
|
||||
{
|
||||
c = *buffDat++;
|
||||
++consumed;
|
||||
|
||||
if (c == '\n') {
|
||||
// Stop reading now
|
||||
break;
|
||||
}
|
||||
|
||||
line += c;
|
||||
}
|
||||
|
||||
if (c == '\n') {
|
||||
// Stop reading now
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (c == '\n') {
|
||||
serial_read_buffer.consume(consumed);
|
||||
|
||||
// Remove carriage return if found and add this line to queue
|
||||
if (line.size() > 0 && line[line.size() - 1] == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
read_queue.push_back(std::move(line));
|
||||
|
||||
++numberOfLines;
|
||||
}
|
||||
else {
|
||||
// Ran out of data to consume
|
||||
return numberOfLines;
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
return "";
|
||||
#endif
|
||||
};
|
||||
|
||||
DreamConn::DreamConn(int bus, int dreamcastControllerType) : bus(bus), dreamcastControllerType(dreamcastControllerType) {
|
||||
switch (dreamcastControllerType)
|
||||
{
|
||||
case TYPE_DREAMCONN:
|
||||
dcConnection = std::make_unique<DreamConnConnection>(bus);
|
||||
break;
|
||||
|
||||
case TYPE_DREAMCASTCONTROLLERUSB:
|
||||
dcConnection = std::make_unique<DreamcastControllerUsbPicoConnection>(bus);
|
||||
break;
|
||||
}
|
||||
|
||||
connect();
|
||||
}
|
||||
|
||||
DreamConn::~DreamConn() {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
void DreamConn::connect()
|
||||
{
|
||||
maple_io_connected = false;
|
||||
|
||||
asio::error_code ec;
|
||||
|
||||
switch (dreamcastControllerType) {
|
||||
case TYPE_DREAMCONN:
|
||||
{
|
||||
#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));
|
||||
break;
|
||||
}
|
||||
case TYPE_DREAMCASTCONTROLLERUSB:
|
||||
{
|
||||
// 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
|
||||
#if defined(_WIN32)
|
||||
Sleep(500);
|
||||
#elif defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))
|
||||
usleep(500000);
|
||||
#endif
|
||||
expansionDevs = 0;
|
||||
|
||||
serial_handler = asio::serial_port(io_context);
|
||||
|
||||
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");
|
||||
INFO_LOG(INPUT, "DreamcastController[%d] connecting to user-configured serial device: %s", bus, serial_device.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
serial_device = getFirstSerialDevice();
|
||||
INFO_LOG(INPUT, "DreamcastController[%d] connecting to autoselected serial device: %s", bus, serial_device.c_str());
|
||||
}
|
||||
|
||||
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());
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!dcConnection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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, io_context, serial_handler, dreamcastControllerType);
|
||||
if (ec)
|
||||
|
||||
std::optional<MapleMsg> msg = dcConnection->connect();
|
||||
if (!msg)
|
||||
{
|
||||
WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str());
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
if (!receiveMsg(msg, iostream, serial_handler, dreamcastControllerType)) {
|
||||
WARN_LOG(INPUT, "DreamcastController[%d] read timeout", bus);
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
if (dreamcastControllerType == TYPE_DREAMCONN)
|
||||
iostream.expires_from_now(std::chrono::duration<u32>::max()); // don't use a 64-bit based duration to avoid overflow
|
||||
|
||||
expansionDevs = msg.originAP & 0x1f;
|
||||
|
||||
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, dreamcastControllerType == 1 ? "DreamConn+ / DreamcConn S Controller" : "Dreamcast Controller USB", hasVmu(), hasRumble());
|
||||
|
@ -303,30 +620,27 @@ void DreamConn::connect()
|
|||
|
||||
void DreamConn::disconnect()
|
||||
{
|
||||
if (dreamcastControllerType == TYPE_DREAMCONN)
|
||||
{
|
||||
if (iostream)
|
||||
iostream.close();
|
||||
if (!dcConnection) {
|
||||
return;
|
||||
}
|
||||
else if (dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB)
|
||||
{
|
||||
if (serial_handler.is_open())
|
||||
serial_handler.cancel();
|
||||
serial_handler.close();
|
||||
io_context.stop();
|
||||
}
|
||||
|
||||
|
||||
dcConnection->disconnect();
|
||||
|
||||
maple_io_connected = false;
|
||||
|
||||
|
||||
NOTICE_LOG(INPUT, "Disconnected from DreamcastController[%d]", bus);
|
||||
}
|
||||
|
||||
bool DreamConn::send(const MapleMsg& msg)
|
||||
{
|
||||
if (!dcConnection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
asio::error_code ec;
|
||||
|
||||
if (maple_io_connected)
|
||||
ec = sendMsg(msg, iostream, io_context, serial_handler, dreamcastControllerType);
|
||||
ec = dcConnection->sendMsg(msg);
|
||||
else
|
||||
return false;
|
||||
if (ec) {
|
||||
|
@ -342,14 +656,16 @@ bool DreamConnGamepad::isDreamcastController(int deviceIndex)
|
|||
{
|
||||
char guid_str[33] {};
|
||||
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(deviceIndex), guid_str, sizeof(guid_str));
|
||||
INFO_LOG(INPUT, "GUID: %s VID:%c%c%c%c PID:%c%c%c%c", guid_str,
|
||||
NOTICE_LOG(INPUT, "GUID: %s VID:%c%c%c%c PID:%c%c%c%c", guid_str,
|
||||
guid_str[10], guid_str[11], guid_str[8], guid_str[9],
|
||||
guid_str[18], guid_str[19], guid_str[16], guid_str[17]);
|
||||
|
||||
|
||||
// DreamConn VID:4457 PID:4443
|
||||
// Dreamcast Controller USB VID:1209 PID:2f07
|
||||
if (memcmp("5744000043440000", guid_str + 8, 16) == 0 || memcmp("09120000072f0000", guid_str + 8, 16) == 0)
|
||||
if (memcmp(DreamConnConnection::VID_PID_GUID, guid_str + 8, 16) == 0 ||
|
||||
memcmp(DreamcastControllerUsbPicoConnection::VID_PID_GUID, guid_str + 8, 16) == 0)
|
||||
{
|
||||
NOTICE_LOG(INPUT, "Dreamcast controller found!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -359,22 +675,22 @@ DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystic
|
|||
: SDLGamepad(maple_port, joystick_idx, sdl_joystick)
|
||||
{
|
||||
char guid_str[33] {};
|
||||
|
||||
|
||||
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(joystick_idx), guid_str, sizeof(guid_str));
|
||||
|
||||
|
||||
// DreamConn VID:4457 PID:4443
|
||||
// Dreamcast Controller USB VID:1209 PID:2f07
|
||||
if (memcmp("5744000043440000", guid_str + 8, 16) == 0)
|
||||
if (memcmp(DreamConnConnection::VID_PID_GUID, guid_str + 8, 16) == 0)
|
||||
{
|
||||
dreamcastControllerType = TYPE_DREAMCONN;
|
||||
_name = "DreamConn+ / DreamConn S Controller";
|
||||
}
|
||||
else if (memcmp("09120000072f0000", guid_str + 8, 16) == 0)
|
||||
else if (memcmp(DreamcastControllerUsbPicoConnection::VID_PID_GUID, guid_str + 8, 16) == 0)
|
||||
{
|
||||
dreamcastControllerType = TYPE_DREAMCASTCONTROLLERUSB;
|
||||
_name = "Dreamcast Controller USB";
|
||||
}
|
||||
|
||||
|
||||
EventManager::listen(Event::Start, handleEvent, this);
|
||||
EventManager::listen(Event::LoadState, handleEvent, this);
|
||||
}
|
||||
|
@ -444,7 +760,7 @@ void DreamConnGamepad::checkKeyCombo() {
|
|||
gui_open_settings();
|
||||
}
|
||||
|
||||
#else
|
||||
#else // USE_DREAMCASTCONTROLLER
|
||||
|
||||
void DreamConn::connect() {
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#define TYPE_DREAMCASTCONTROLLERUSB 2
|
||||
#include <asio.hpp>
|
||||
#endif
|
||||
#include <memory>
|
||||
|
||||
struct MapleMsg
|
||||
{
|
||||
|
@ -52,21 +53,15 @@ class DreamConn
|
|||
const int bus;
|
||||
const int dreamcastControllerType;
|
||||
#ifdef USE_DREAMCASTCONTROLLER
|
||||
asio::ip::tcp::iostream iostream;
|
||||
asio::io_context io_context;
|
||||
asio::serial_port serial_handler{io_context};
|
||||
std::unique_ptr<class DreamcastControllerConnection> dcConnection;
|
||||
#endif
|
||||
bool maple_io_connected;
|
||||
u8 expansionDevs = 0;
|
||||
static constexpr u16 BASE_PORT = 37393;
|
||||
|
||||
public:
|
||||
DreamConn(int bus, int dreamcastControllerType) : bus(bus), dreamcastControllerType(dreamcastControllerType) {
|
||||
connect();
|
||||
}
|
||||
~DreamConn() {
|
||||
disconnect();
|
||||
}
|
||||
DreamConn(int bus, int dreamcastControllerType);
|
||||
|
||||
~DreamConn();
|
||||
|
||||
bool send(const MapleMsg& msg);
|
||||
|
||||
|
|
|
@ -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,crc:3cef,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 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");
|
||||
#elif defined(_WIN32)
|
||||
SDL_GameControllerAddMapping("0300000009120000072f000000000000,OrangeFox86 Dreamcast Controller USB,crc:baa5,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 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");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -547,7 +547,7 @@ void input_sdl_handle()
|
|||
case SDL_JOYDEVICEREMOVED:
|
||||
sdl_close_joystick((SDL_JoystickID)event.jdevice.which);
|
||||
break;
|
||||
|
||||
|
||||
case SDL_DROPFILE:
|
||||
gui_start_game(event.drop.file);
|
||||
break;
|
||||
|
@ -602,7 +602,7 @@ static inline void get_window_state()
|
|||
windowPos.h /= hdpiScaling;
|
||||
SDL_GetWindowPosition(window, &windowPos.x, &windowPos.y);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if defined(_WIN32) && !defined(TARGET_UWP)
|
||||
|
@ -629,14 +629,14 @@ bool sdl_recreate_window(u32 flags)
|
|||
PROCESS_SYSTEM_DPI_AWARE = 1,
|
||||
PROCESS_PER_MONITOR_DPI_AWARE = 2
|
||||
} PROCESS_DPI_AWARENESS;
|
||||
|
||||
|
||||
HRESULT(WINAPI *SetProcessDpiAwareness)(PROCESS_DPI_AWARENESS dpiAwareness); // Windows 8.1 and later
|
||||
void* shcoreDLL = SDL_LoadObject("SHCORE.DLL");
|
||||
if (shcoreDLL) {
|
||||
SetProcessDpiAwareness = (HRESULT(WINAPI *)(PROCESS_DPI_AWARENESS)) SDL_LoadFunction(shcoreDLL, "SetProcessDpiAwareness");
|
||||
if (SetProcessDpiAwareness) {
|
||||
SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
|
||||
|
||||
|
||||
if (SDL_GetDisplayDPI(0, &settings.display.dpi, NULL, NULL) != -1){ //SDL_WINDOWPOS_UNDEFINED is Display 0
|
||||
//When using HiDPI mode, set correct DPI scaling
|
||||
hdpiScaling = settings.display.dpi / 96.f;
|
||||
|
@ -645,7 +645,7 @@ bool sdl_recreate_window(u32 flags)
|
|||
SDL_UnloadObject(shcoreDLL);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef __SWITCH__
|
||||
AppletOperationMode om = appletGetOperationMode();
|
||||
if (om == AppletOperationMode_Handheld)
|
||||
|
|
Loading…
Reference in New Issue