Add support for Raw Thrills I/O (DDR X2 US cabinets) on Windows (#1894)

This commit is contained in:
Tracy Ward
2019-10-13 03:59:35 -04:00
committed by GitHub
5 changed files with 902 additions and 3 deletions
+4 -2
View File
@@ -269,13 +269,15 @@ if(WIN32)
"arch/InputHandler/InputHandler_DirectInputHelper.cpp"
"arch/InputHandler/InputHandler_Win32_MIDI.cpp"
"arch/InputHandler/InputHandler_Win32_Para.cpp"
"arch/InputHandler/InputHandler_Win32_Pump.cpp")
"arch/InputHandler/InputHandler_Win32_Pump.cpp"
"arch/InputHandler/InputHandler_Win32_RTIO.cpp")
list(APPEND SMDATA_ARCH_INPUT_HPP
"arch/InputHandler/InputHandler_DirectInput.h"
"arch/InputHandler/InputHandler_DirectInputHelper.h"
"arch/InputHandler/InputHandler_Win32_MIDI.h"
"arch/InputHandler/InputHandler_Win32_Para.h"
"arch/InputHandler/InputHandler_Win32_Pump.h")
"arch/InputHandler/InputHandler_Win32_Pump.h"
"arch/InputHandler/InputHandler_Win32_RTIO.h")
if(NOT MSVC)
list(APPEND SMDATA_ARCH_INPUT_SRC
"arch/InputHandler/InputHandler_SextetStream.cpp")
+29
View File
@@ -455,6 +455,35 @@ static const AutoMappings g_AutoMappings[] =
AutoMappingEntry( 0, JOY_BUTTON_8, GAME_BUTTON_START, false ), // start button
AutoMappingEntry( 0, JOY_BUTTON_7, GAME_BUTTON_BACK, false ) // back button
),
AutoMappings(
"dance",
"Raw Thrills I/O",
"Raw Thrills I/O",
AutoMappingEntry( 0, JOY_BUTTON_1, DANCE_BUTTON_LEFT, false ),
AutoMappingEntry( 0, JOY_BUTTON_2, DANCE_BUTTON_DOWN, false ),
AutoMappingEntry( 0, JOY_BUTTON_3, DANCE_BUTTON_UP, false ),
AutoMappingEntry( 0, JOY_BUTTON_4, DANCE_BUTTON_RIGHT, false ),
AutoMappingEntry( 0, JOY_BUTTON_5, GAME_BUTTON_MENULEFT, false ),
AutoMappingEntry( 0, JOY_BUTTON_6, GAME_BUTTON_MENUDOWN, false ),
AutoMappingEntry( 0, JOY_BUTTON_7, GAME_BUTTON_MENUUP, false ),
AutoMappingEntry( 0, JOY_BUTTON_8, GAME_BUTTON_MENURIGHT, false ),
AutoMappingEntry( 0, JOY_BUTTON_9, GAME_BUTTON_START, false ),
AutoMappingEntry( 0, JOY_BUTTON_10, DANCE_BUTTON_LEFT, true ),
AutoMappingEntry( 0, JOY_BUTTON_11, DANCE_BUTTON_DOWN, true ),
AutoMappingEntry( 0, JOY_BUTTON_12, DANCE_BUTTON_UP, true ),
AutoMappingEntry( 0, JOY_BUTTON_13, DANCE_BUTTON_RIGHT, true ),
AutoMappingEntry( 0, JOY_BUTTON_14, GAME_BUTTON_MENULEFT, true ),
AutoMappingEntry( 0, JOY_BUTTON_15, GAME_BUTTON_MENUDOWN, true ),
AutoMappingEntry( 0, JOY_BUTTON_16, GAME_BUTTON_MENUUP, true ),
AutoMappingEntry( 0, JOY_BUTTON_17, GAME_BUTTON_MENURIGHT, true ),
AutoMappingEntry( 0, JOY_BUTTON_18, GAME_BUTTON_START, true ),
AutoMappingEntry( 0, JOY_BUTTON_19, GAME_BUTTON_OPERATOR, false ),
AutoMappingEntry( 0, JOY_BUTTON_20, GAME_BUTTON_SELECT, false ),
AutoMappingEntry( 0, JOY_BUTTON_21, GAME_BUTTON_COIN, false ),
AutoMappingEntry( 0, JOY_BUTTON_22, GAME_BUTTON_COIN, false ),
AutoMappingEntry( 0, JOY_BUTTON_23, GAME_BUTTON_EFFECT_DOWN, false ),
AutoMappingEntry( 0, JOY_BUTTON_24, GAME_BUTTON_EFFECT_UP, false )
),
AutoMappings(
"pump",
"Pump USB",
+2 -1
View File
@@ -392,7 +392,8 @@ if WINDOWS
arch/InputHandler/InputHandler_DirectInputHelper.cpp arch/InputHandler/InputHandler_DirectInputHelper.h \
arch/InputHandler/InputHandler_Win32_MIDI.cpp arch/InputHandler/InputHandler_Win32_MIDI.h \
arch/InputHandler/InputHandler_Win32_Para.cpp arch/InputHandler/InputHandler_Win32_Para.h \
arch/InputHandler/InputHandler_Win32_Pump.cpp arch/InputHandler/InputHandler_Win32_Pump.h
arch/InputHandler/InputHandler_Win32_Pump.cpp arch/InputHandler/InputHandler_Win32_Pump.h \
arch/InputHandler/InputHandler_Win32_RTIO.cpp arch/InputHandler/InputHandler_Win32_RTIO.h
Sound += arch/Sound/RageSoundDriver_DSound_Software.cpp arch/Sound/RageSoundDriver_DSound_Software.h \
arch/Sound/DSoundHelpers.cpp arch/Sound/DSoundHelpers.h \
@@ -0,0 +1,731 @@
// This input handler is meant to be used with Raw Thrills DDR cabinets, such
// as the US arcade release of DDR X2.
#include "global.h"
#include "InputHandler_Win32_RTIO.h"
#include <algorithm>
#include "RageLog.h"
#include "RageInputDevice.h"
// The coin counter won't accept an increment command immediately after acking
// an older increment command. This delay is the minimum amount of time to wait
// between receiving an ack for an increment command and sending a new
// increment command.
const static float COUNTER_MINIMUM_SEND_DELAY = 0.15;
// The longest amount of time to wait for an ack to the increment command
// before moving on with the coin counter increment sequence.
const static float COUNTER_MAXIMUM_RECV_DELAY = 3.0;
// If RTIO does not initialize within this amount of time, exit the RTIO input
// loop to free resources.
const static float RTIO_INIT_TIME_MAX = 10.0;
// If there are this many failures, something has most likely gone very wrong,
// so just exit the RTIO input loop.
const static int RTIO_MAX_READ_FAILURES = 50;
REGISTER_INPUT_HANDLER_CLASS2(Rtio, Win32_RTIO);
InputHandler_Win32_RTIO::InputHandler_Win32_RTIO()
{
LOG->Trace("RTIO: Start");
if (!Initialize()) {
return;
}
input_thread_.SetName("RTIO thread");
input_thread_.Create(InputThread_Start, this);
}
InputHandler_Win32_RTIO::~InputHandler_Win32_RTIO()
{
if (input_thread_.IsCreated())
{
shutdown_ = true;
LOG->Trace("RTIO: Shutting down RTIO thread");
input_thread_.Wait();
LOG->Info("RTIO: Thread shut down");
}
rtio_.Disconnect();
}
void InputHandler_Win32_RTIO::GetDevicesAndDescriptions(vector<InputDeviceInfo>& vDevicesOut)
{
// We use a joystick device so we can get automatic input mapping
vDevicesOut.push_back(InputDeviceInfo(InputDevice(DEVICE_JOY1), "Raw Thrills I/O"));
}
RString InputHandler_Win32_RTIO::GetDeviceSpecificInputString(const DeviceInput &di)
{
switch (di.button)
{
case JOY_BUTTON_1: return "P1 Pad Left";
case JOY_BUTTON_2: return "P1 Pad Down";
case JOY_BUTTON_3: return "P1 Pad Up";
case JOY_BUTTON_4: return "P1 Pad Right";
case JOY_BUTTON_5: return "P1 Menu Left";
case JOY_BUTTON_6: return "P1 Menu Down";
case JOY_BUTTON_7: return "P1 Menu Up";
case JOY_BUTTON_8: return "P1 Menu Right";
case JOY_BUTTON_9: return "P1 Menu Start";
case JOY_BUTTON_10: return "P2 Pad Left";
case JOY_BUTTON_11: return "P2 Pad Down";
case JOY_BUTTON_12: return "P2 Pad Up";
case JOY_BUTTON_13: return "P2 Pad Right";
case JOY_BUTTON_14: return "P2 Menu Left";
case JOY_BUTTON_15: return "P2 Menu Down";
case JOY_BUTTON_16: return "P2 Menu Up";
case JOY_BUTTON_17: return "P2 Menu Right";
case JOY_BUTTON_18: return "P2 Menu Start";
case JOY_BUTTON_19: return "Test Switch";
case JOY_BUTTON_20: return "Service Switch";
case JOY_BUTTON_21: return "P1 Coin Slot";
case JOY_BUTTON_22: return "P2 Coin Slot";
case JOY_BUTTON_23: return "Volume Down";
case JOY_BUTTON_24: return "Volume Up";
}
return InputHandler::GetDeviceSpecificInputString(di);
}
bool InputHandler_Win32_RTIO::Initialize() {
if (!rtio_.Connect()) {
LOG->Warn("RTIO: Initialize: Cannot connect to COM1");
return false;
}
// The following messages are not required to receive game/operator inputs,
// but they replicate the initialization sequence in DDR. We'll use the
// response from the version message to determine that the board is good.
if (!rtio_.WriteMsg("C0000")) {
LOG->Warn("RTIO: Initialize: Cannot write init message");
return false;
}
if (!rtio_.WriteMsg("v")) {
LOG->Warn("RTIO: Initialize: Cannot write version message");
return false;
}
if (!rtio_.WriteMsg("D0020008")) {
LOG->Warn("RTIO: Initialize: Cannot send dongle message (1/6)");
return false;
}
if (!rtio_.WriteMsg("D0010008")) {
LOG->Warn("RTIO: Initialize: Cannot send dongle message (2/6)");
return false;
}
if (!rtio_.WriteMsg("D0000020")) {
LOG->Warn("RTIO: Initialize: Cannot send dongle message (3/6)");
return false;
}
if (!rtio_.WriteMsg("D1020008")) {
LOG->Warn("RTIO: Initialize: Cannot send dongle message (4/6)");
return false;
}
if (!rtio_.WriteMsg("D1010008")) {
LOG->Warn("RTIO: Initialize: Cannot send dongle message (5/6)");
return false;
}
if (!rtio_.WriteMsg("D1000020")) {
LOG->Warn("RTIO: Initialize: Cannot send dongle message (6/6)");
return false;
}
return true;
}
int InputHandler_Win32_RTIO::InputThread_Start(void *this_ptr)
{
((InputHandler_Win32_RTIO *)this_ptr)->InputThread();
return 0;
}
void InputHandler_Win32_RTIO::InputThread()
{
RageTimer start_time;
std::vector<std::string> msgs;
int read_failures = 0;
while (!shutdown_) {
if (!rtio_.ReadMsgs(&msgs)) {
read_failures++;
LOG->Warn("RTIO: HandlerLoop: Failed to read (%d failures)", read_failures);
if (read_failures == RTIO_MAX_READ_FAILURES) {
return;
}
continue;
}
RageTimer now;
if (!initialized_ && now - start_time > RTIO_INIT_TIME_MAX) {
LOG->Warn("RTIO: Device failed to initialize; exiting input loop");
return;
}
for (auto msg : msgs) {
if (msg.length() < 1) continue;
if (msg[0] == 'c') {
LOG->Trace("RTIO: Received init ack: %s", msg.c_str());
continue;
}
if (msg[0] == 'v') {
LOG->Trace("RTIO: Received version response: %s", msg.c_str());
initialized_ = true;
continue;
}
if (msg[0] == 'd') {
LOG->Trace("RTIO: Received dongle response: %s", msg.c_str());
continue;
}
if (msg[0] == 'T' && msg.length() == 7) {
HandleGameInput(msg, now);
continue;
}
if (msg[0] == 'S' && msg.length() == 11) {
HandleOperatorInput(msg, now);
continue;
}
if (msg[0] == 'h' && msg.length() == 3) {
HandleCounterAck(msg);
continue;
}
LOG->Warn("RTIO: Unrecognized response: %s", msg.c_str());
}
if (counter_cycles_pending_ > 0) {
switch (counter_state_) {
case COUNTER_STATE_SEND_1:
if (last_counter_recv_.Ago() > COUNTER_MINIMUM_SEND_DELAY) {
if (!rtio_.WriteMsg("H10")) {
LOG->Warn("RTIO: Could not send coin counter increment (1/2)");
}
counter_state_ = COUNTER_STATE_RECV_1;
last_counter_send_.Touch();
}
break;
case COUNTER_STATE_SEND_2:
if (last_counter_recv_.Ago() > COUNTER_MINIMUM_SEND_DELAY) {
if (!rtio_.WriteMsg("H00")) {
LOG->Warn("RTIO: Could not send coin counter increment (2/2)");
}
counter_state_ = COUNTER_STATE_RECV_2;
last_counter_send_.Touch();
}
break;
}
if (last_counter_send_.Ago() > COUNTER_MAXIMUM_RECV_DELAY) {
switch (counter_state_) {
case COUNTER_STATE_RECV_1:
LOG->Warn("RTIO: Never received coin counter increment acknowledgement (1/2)");
counter_state_ = COUNTER_STATE_SEND_2;
break;
case COUNTER_STATE_RECV_2:
LOG->Warn("RTIO: Never received coin counter increment acknowledgement (2/2)");
counter_state_ = COUNTER_STATE_SEND_1;
counter_cycles_pending_--;
break;
}
}
}
}
}
int HexCharToInt(char ch)
{
if (ch >= 'A' && ch <= 'F')
return ch - 'A' + 10;
return ch - '0';
}
void InputHandler_Win32_RTIO::HandleGameInput(const std::string &msg, const RageTimer &now)
{
InputDevice id = InputDevice(DEVICE_JOY1);
int pad1 = HexCharToInt(msg[1]);
int pad2 = HexCharToInt(msg[2]);
int menu1 = HexCharToInt(msg[3]);
int menu2 = HexCharToInt(msg[4]);
int start1 = HexCharToInt(msg[5]);
int start2 = HexCharToInt(msg[6]);
GAME_INPUT input_new;
input_new.P1_PadUp = (pad1 >> 3) & 1;
input_new.P1_PadDown = (pad1 >> 2) & 1;
input_new.P1_PadLeft = (pad1 >> 1) & 1;
input_new.P1_PadRight = pad1 & 1;
input_new.P2_PadUp = (pad2 >> 3) & 1;
input_new.P2_PadDown = (pad2 >> 2) & 1;
input_new.P2_PadLeft = (pad2 >> 1) & 1;
input_new.P2_PadRight = pad2 & 1;
input_new.P1_MenuUp = (menu1 >> 3) & 1;
input_new.P1_MenuDown = (menu1 >> 2) & 1;
input_new.P1_MenuLeft = (menu1 >> 1) & 1;
input_new.P1_MenuRight = menu1 & 1;
input_new.P2_MenuUp = (menu2 >> 3) & 1;
input_new.P2_MenuDown = (menu2 >> 2) & 1;
input_new.P2_MenuLeft = (menu2 >> 1) & 1;
input_new.P2_MenuRight = menu2 & 1;
input_new.P1_MenuStart = start1 & 1;
input_new.P2_MenuStart = start2 & 1;
if (input_new.P1_PadLeft != last_game_input_.P1_PadLeft) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_1, (float)input_new.P1_PadLeft, now));
}
if (input_new.P1_PadDown != last_game_input_.P1_PadDown) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_2, (float)input_new.P1_PadDown, now));
}
if (input_new.P1_PadUp != last_game_input_.P1_PadUp) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_3, (float)input_new.P1_PadUp, now));
}
if (input_new.P1_PadRight != last_game_input_.P1_PadRight) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_4, (float)input_new.P1_PadRight, now));
}
if (input_new.P1_MenuLeft != last_game_input_.P1_MenuLeft) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_5, (float)input_new.P1_MenuLeft, now));
}
if (input_new.P1_MenuDown != last_game_input_.P1_MenuDown) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_6, (float)input_new.P1_MenuDown, now));
}
if (input_new.P1_MenuUp != last_game_input_.P1_MenuUp) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_7, (float)input_new.P1_MenuUp, now));
}
if (input_new.P1_MenuRight != last_game_input_.P1_MenuRight) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_8, (float)input_new.P1_MenuRight, now));
}
if (input_new.P1_MenuStart != last_game_input_.P1_MenuStart) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_9, (float)input_new.P1_MenuStart, now));
}
if (input_new.P2_PadLeft != last_game_input_.P2_PadLeft) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_10, (float)input_new.P2_PadLeft, now));
}
if (input_new.P2_PadDown != last_game_input_.P2_PadDown) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_11, (float)input_new.P2_PadDown, now));
}
if (input_new.P2_PadUp != last_game_input_.P2_PadUp) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_12, (float)input_new.P2_PadUp, now));
}
if (input_new.P2_PadRight != last_game_input_.P2_PadRight) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_13, (float)input_new.P2_PadRight, now));
}
if (input_new.P2_MenuLeft != last_game_input_.P2_MenuLeft) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_14, (float)input_new.P2_MenuLeft, now));
}
if (input_new.P2_MenuDown != last_game_input_.P2_MenuDown) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_15, (float)input_new.P2_MenuDown, now));
}
if (input_new.P2_MenuUp != last_game_input_.P2_MenuUp) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_16, (float)input_new.P2_MenuUp, now));
}
if (input_new.P2_MenuRight != last_game_input_.P2_MenuRight) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_17, (float)input_new.P2_MenuRight, now));
}
if (input_new.P2_MenuStart != last_game_input_.P2_MenuStart) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_18, (float)input_new.P2_MenuStart, now));
}
/*
if (memcmp(&last_game_input_, &input_new, sizeof(GAME_INPUT)) != 0) {
LOG->Trace("RTIO: P1:%d%d%d%d P2:%d%d%d%d M1:%d%d%d%d-%d M2:%d%d%d%d-%d",
input_new.P1_PadLeft, input_new.P1_PadDown, input_new.P1_PadUp, input_new.P1_PadRight,
input_new.P2_PadLeft, input_new.P2_PadDown, input_new.P2_PadUp, input_new.P2_PadRight,
input_new.P1_MenuLeft, input_new.P1_MenuDown, input_new.P1_MenuUp, input_new.P1_MenuRight, input_new.P1_MenuStart,
input_new.P2_MenuLeft, input_new.P2_MenuDown, input_new.P2_MenuUp, input_new.P2_MenuRight, input_new.P2_MenuStart);
}
*/
memcpy(&last_game_input_, &input_new, sizeof(GAME_INPUT));
}
void InputHandler_Win32_RTIO::HandleOperatorInput(const std::string &msg, const RageTimer &now)
{
InputDevice id = InputDevice(DEVICE_JOY1);
int coin1 = HexCharToInt(msg[3]);
int coin2 = HexCharToInt(msg[4]);
int vol_up = HexCharToInt(msg[5]);
int vol_dn = HexCharToInt(msg[6]);
int test = HexCharToInt(msg[7]);
int select = HexCharToInt(msg[8]);
OPERATOR_INPUT input_new;
input_new.P1_InsertCoin = coin1 & 1;
input_new.P2_InsertCoin = coin2 & 1;
input_new.VolumeUp = vol_up & 1;
input_new.VolumeDown = vol_dn & 1;
input_new.TestSwitch = test & 1;
input_new.SelectSwitch = select & 1;
if (input_new.TestSwitch != last_operator_input_.TestSwitch) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_19, (float)input_new.TestSwitch, now));
}
if (input_new.SelectSwitch != last_operator_input_.SelectSwitch) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_20, (float)input_new.SelectSwitch, now));
}
if (input_new.P1_InsertCoin != last_operator_input_.P1_InsertCoin) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_21, (float)input_new.P1_InsertCoin, now));
if (input_new.P1_InsertCoin) {
counter_cycles_pending_++;
}
}
if (input_new.P2_InsertCoin != last_operator_input_.P2_InsertCoin) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_22, (float)input_new.P2_InsertCoin, now));
if (input_new.P2_InsertCoin) {
counter_cycles_pending_++;
}
}
if (input_new.VolumeDown != last_operator_input_.VolumeDown) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_23, (float)input_new.VolumeDown, now));
}
if (input_new.VolumeUp != last_operator_input_.VolumeUp) {
ButtonPressed(DeviceInput(id, JOY_BUTTON_24, (float)input_new.VolumeUp, now));
}
/*
if (memcmp(&last_operator_input_, &input_new, sizeof(OPERATOR_INPUT)) != 0) {
LOG->Trace("RTIO: C:%d%d V:%d%d S:%d%d",
input_new.P1_InsertCoin, input_new.P2_InsertCoin, input_new.VolumeUp, input_new.VolumeDown, input_new.TestSwitch, input_new.SelectSwitch);
}
*/
memcpy(&last_operator_input_, &input_new, sizeof(OPERATOR_INPUT));
}
void InputHandler_Win32_RTIO::HandleCounterAck(const std::string &msg)
{
int ack_num = HexCharToInt(msg[1]);
last_counter_recv_.Touch();
if (ack_num == 1 && counter_state_ == COUNTER_STATE_RECV_1) {
counter_state_ = COUNTER_STATE_SEND_2;
return;
}
if (ack_num == 0 && counter_state_ == COUNTER_STATE_RECV_2) {
counter_state_ = COUNTER_STATE_SEND_1;
counter_cycles_pending_ = max(counter_cycles_pending_ - 1, 0);
return;
}
LOG->Warn("RTIO: Received stray coin counter increment acknowledgement: state=%d, msg=%s", counter_state_, msg.c_str());
}
RtioDevice::~RtioDevice()
{
Disconnect();
}
bool RtioDevice::Connect()
{
for (int i = 1; i < 16; i++) {
if (serial_.Connect(i)) {
return true;
}
}
return false;
}
void RtioDevice::Disconnect()
{
serial_.Disconnect();
}
// Read any available messages from the RTIO device and return them as strings
// with the prefixes/suffixes stripped.
bool RtioDevice::ReadMsgs(std::vector<std::string> *msgs)
{
msgs->clear();
int bytes_read = serial_.Read(&read_buffer_[read_offset_], sizeof(read_buffer_) - read_offset_);
// Exit if there was an error
if (bytes_read < 0) {
return false;
}
// Return early if we didn't read any bytes
if (bytes_read == 0) {
return true;
}
read_offset_ += bytes_read;
int pos = 0;
while (pos < read_offset_) {
int msg_size = ParseMsg(&read_buffer_[pos], read_offset_ - pos);
if (msg_size < 0) {
LOG->Warn("RTIO: RtioDevice: Bad msg start at offset %d (got %d); skipping invalid data", pos, read_buffer_[pos]);
while (pos < read_offset_) {
if (read_buffer_[pos] == '\n')
break;
pos++;
}
continue;
}
if (msg_size == 0) {
break;
}
msgs->push_back(std::string(&read_buffer_[pos + 1], msg_size - 2));
pos += msg_size;
}
memcpy(read_buffer_, &read_buffer_[pos], read_offset_ - pos);
read_offset_ -= pos;
return true;
}
// Received messages always begins with '\n' and end with '\r'. Find the length
// of the first message in buffer and return its size, including the '\n' and
// '\r' characters. Return -1 upon error.
int RtioDevice::ParseMsg(char *buffer, int buffer_size)
{
if (buffer[0] != '\n') {
return -1;
}
for (int i = 1; i < buffer_size; i++) {
if (buffer[i] == '\r') {
return i + 1;
}
}
return 0;
}
// Sends a message to the RTIO device. This function adds the necessary prefix
// and suffix ('\n' and '\r', respectively). Messages sent to RTIO also
// include a checksum, expressed in hex.
bool RtioDevice::WriteMsg(const std::string &msg)
{
std::string buf;
buf = '\n';
buf += msg;
buf += '\r';
char checksum = 0;
for (unsigned int i = 0; i < buf.length(); i++) {
checksum += buf[i];
}
char checksum_str[3];
snprintf(checksum_str, sizeof(checksum_str), "%02x", checksum);
buf += checksum_str;
int wrote = serial_.Write(buf.c_str(), buf.length());
return wrote == buf.length();
}
SerialDevice::~SerialDevice()
{
Disconnect();
}
bool SerialDevice::Connect(int com_number)
{
std::string name("COM");
name += std::to_string(com_number);
com_handle_ = CreateFile(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL, nullptr);
if (com_handle_ == INVALID_HANDLE_VALUE) {
LOG->Info("RTIO: SerialDevice: Connect failed on %s: %d", name.c_str(), GetLastError());
return false;
}
if (!Setup()) {
SetCommMask(com_handle_, 0);
return false;
}
LOG->Info("RTIO: SerialDevice: Connect succeeded on %s", name.c_str());
return true;
}
void SerialDevice::Disconnect()
{
if (com_handle_ != INVALID_HANDLE_VALUE) {
CloseHandle(read_overlapped_.hEvent);
CloseHandle(write_overlapped_.hEvent);
CloseHandle(com_handle_);
com_handle_ = INVALID_HANDLE_VALUE;
}
}
bool SerialDevice::Setup()
{
// Set the serial device to monitor for characters in the input buffer
if (!SetCommMask(com_handle_, EV_RXCHAR)) {
LOG->Warn("RTIO: SerialDevice: SetCommMask failed: %d", GetLastError());
return false;
}
// Set the sizes of the device's internal buffers
if (!SetupComm(com_handle_, read_buffer_size_, write_buffer_size_)) {
LOG->Warn("RTIO: SerialDevice: SetupComm failed: %d", GetLastError());
return false;
}
// Discard all characters from the internal buffers
if (!PurgeComm(com_handle_, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR)) {
LOG->Warn("RTIO: SerialDevice: PurgeComm failed: %d", GetLastError());
return false;
}
// Set the timeouts for read/write operations
COMMTIMEOUTS timeouts;
timeouts.ReadIntervalTimeout = 10000;
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.ReadTotalTimeoutConstant = 1000;
timeouts.WriteTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 1000;
if (!SetCommTimeouts(com_handle_, &timeouts)) {
LOG->Warn("RTIO: SerialDevice: SetCommTimeouts failed: %d", GetLastError());
return false;
}
// Configure control settings
DCB dcb;
dcb.DCBlength = sizeof(dcb);
if (!GetCommState(com_handle_, &dcb)) {
LOG->Warn("RTIO: SerialDevice: GetCommState failed: %d", GetLastError());
return false;
}
dcb.Parity = 0;
dcb.StopBits = 0;
dcb.BaudRate = CBR_115200;
dcb.ByteSize = 8;
dcb.fBinary = 1;
dcb.fParity = 0;
dcb.fOutxCtsFlow = 0;
dcb.fOutxDsrFlow = 0;
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fOutX = 0;
dcb.fInX = 0;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
if (!SetCommState(com_handle_, &dcb)) {
LOG->Warn("RTIO: SerialDevice: SetCommState failed: %d", GetLastError());
return false;
}
// Create a manually resettable events for the OVERLAPPED structures
read_overlapped_.hEvent = CreateEvent(nullptr, true, false, nullptr);
if (read_overlapped_.hEvent == nullptr) {
LOG->Warn("RTIO: SerialDevice: CreateEvent failed: %d", GetLastError());
return false;
}
write_overlapped_.hEvent = CreateEvent(nullptr, true, false, nullptr);
if (write_overlapped_.hEvent == nullptr) {
LOG->Warn("RTIO: SerialDevice: CreateEvent failed: %d", GetLastError());
return false;
}
return true;
}
void ResetOverlapped(OVERLAPPED *overlapped)
{
overlapped->Internal = 0;
overlapped->InternalHigh = 0;
overlapped->Offset = 0;
overlapped->OffsetHigh = 0;
ResetEvent(overlapped->hEvent);
}
// Reads up to buffer_size bytes from the serial device. Returns as fast as
// possible by only reading bytes that are already available in the queue.
int SerialDevice::Read(char *buffer, int buffer_size)
{
DWORD errors;
COMSTAT stat;
// Call ClearCommError to get the number of bytes waiting to be read
if (!ClearCommError(com_handle_, &errors, &stat)) {
LOG->Warn("SerialDevice: ClearCommError failed: %d", GetLastError());
return -1;
}
// If there are no bytes to read, exit here
if (stat.cbInQue == 0) {
return 0;
}
ResetOverlapped(&read_overlapped_);
DWORD read_size = min(stat.cbInQue, (DWORD)read_buffer_size_);
read_size = min(read_size, (DWORD)buffer_size);
DWORD bytes_transferred;
if (!ReadFile(com_handle_, buffer, read_size, &bytes_transferred, &read_overlapped_)) {
DWORD err = GetLastError();
if (err != ERROR_IO_PENDING) {
LOG->Warn("RTIO: SerialDevice: ReadFile failed: %d", err);
return -1;
}
// Wait for the read operation to finish
if (!GetOverlappedResult(com_handle_, &read_overlapped_, &bytes_transferred, true)) {
LOG->Warn("RTIO: SerialDevice: GetOverlappedResult failed on read: %d\n", GetLastError());
return -1;
}
}
return bytes_transferred;
}
int SerialDevice::Write(const char *buffer, int buffer_size)
{
DWORD bytes_transferred;
DWORD write_size = min((DWORD)buffer_size, (DWORD)write_buffer_size_);
ResetOverlapped(&write_overlapped_);
if (!WriteFile(com_handle_, buffer, write_size, &bytes_transferred, &write_overlapped_)) {
DWORD err = GetLastError();
if (err != ERROR_IO_PENDING) {
LOG->Warn("RTIO: SerialDevice: WriteFile failed: %d", err);
return -1;
}
// Wait for the write operation to finish
if (!GetOverlappedResult(com_handle_, &write_overlapped_, &bytes_transferred, true)) {
LOG->Warn("RTIO: SerialDevice: GetOverlappedResult failed on write: %d\n", GetLastError());
return -1;
}
}
return bytes_transferred;
}
/*
* Contributed by x0rbl (2019). Stepmania copyright/license:
*
* (c) 2003-2004 Glenn Maynard
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, and/or sell copies of the Software, and to permit persons to
* whom the Software is furnished to do so, provided that the above
* copyright notice(s) and this permission notice appear in all copies of
* the Software and that both the above copyright notice(s) and this
* permission notice appear in supporting documentation.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
* THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
@@ -0,0 +1,136 @@
#ifndef INPUT_HANDLER_WIN32_RTIO_H
#define INPUT_HANDLER_WIN32_RTIO_H
#include "InputHandler.h"
#include "RageThreads.h"
#include <windows.h>
#include <string>
#include <vector>
typedef struct {
bool P1_PadUp;
bool P1_PadDown;
bool P1_PadLeft;
bool P1_PadRight;
bool P2_PadUp;
bool P2_PadDown;
bool P2_PadLeft;
bool P2_PadRight;
bool P1_MenuUp;
bool P1_MenuDown;
bool P1_MenuLeft;
bool P1_MenuRight;
bool P2_MenuUp;
bool P2_MenuDown;
bool P2_MenuLeft;
bool P2_MenuRight;
bool P1_MenuStart;
bool P2_MenuStart;
} GAME_INPUT;
typedef struct {
int P1_InsertCoin;
int P2_InsertCoin;
int VolumeUp;
int VolumeDown;
int TestSwitch;
int SelectSwitch;
} OPERATOR_INPUT;
enum COUNTER_STATE {
COUNTER_STATE_SEND_1 = 0, // Ready to send "H10" (first coin counter increment command)
COUNTER_STATE_RECV_1, // Ready to recv "h10" (ack for first coin counter increment command)
COUNTER_STATE_SEND_2, // Ready to send "H00" (second coin counter increment command)
COUNTER_STATE_RECV_2, // Ready to recv "h00" (ack for second coin counter increment command)
};
class SerialDevice {
public:
SerialDevice(int read_buffer_size = 0x1000, int write_buffer_size = 0x1000) : read_buffer_size_(read_buffer_size), write_buffer_size_(write_buffer_size) {}
~SerialDevice();
bool Connect(int com_number);
void Disconnect();
int Read(char *buffer, int buffer_size);
int Write(const char *buffer, int buffer_size);
private:
bool Setup();
HANDLE com_handle_ = INVALID_HANDLE_VALUE;
int read_buffer_size_;
int write_buffer_size_;
OVERLAPPED read_overlapped_;
OVERLAPPED write_overlapped_;
};
class RtioDevice {
public:
~RtioDevice();
bool Connect();
void Disconnect();
bool ReadMsgs(std::vector<std::string> *msgs);
bool WriteMsg(const std::string &msg);
private:
int ParseMsg(char *buffer, int buffer_size);
SerialDevice serial_;
char read_buffer_[0x2000];
int read_offset_ = 0;
};
class InputHandler_Win32_RTIO : public InputHandler
{
public:
InputHandler_Win32_RTIO();
~InputHandler_Win32_RTIO();
void GetDevicesAndDescriptions(vector<InputDeviceInfo>& vDevicesOut);
RString GetDeviceSpecificInputString(const DeviceInput &di);
static int InputThread_Start(void *this_ptr);
private:
bool Initialize();
void InputThread();
void HandleGameInput(const std::string &msg, const RageTimer &now);
void HandleOperatorInput(const std::string &msg, const RageTimer &now);
void HandleCounterAck(const std::string &msg);
RtioDevice rtio_;
RageThread input_thread_;
bool initialized_ = false;
bool shutdown_ = false;
GAME_INPUT last_game_input_ = {};
OPERATOR_INPUT last_operator_input_ = {};
int counter_cycles_pending_ = 0;
COUNTER_STATE counter_state_ = COUNTER_STATE_SEND_1;
RageTimer last_counter_send_;
RageTimer last_counter_recv_;
};
#endif
/*
* Contributed by x0rbl (2019). Stepmania copyright/license:
*
* (c) 2003-2004 Glenn Maynard
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, and/or sell copies of the Software, and to permit persons to
* whom the Software is furnished to do so, provided that the above
* copyright notice(s) and this permission notice appear in all copies of
* the Software and that both the above copyright notice(s) and this
* permission notice appear in supporting documentation.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
* THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/