Add support for Raw Thrills I/O (DDR X2 US cabinets) on Windows (#1894)
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
Reference in New Issue
Block a user