Make the in-fork glass cockpit fully interactive and finish the lamp model. - Click-to-press: clicking a button's region in a VDB head window presses that RIO address into the game (lights white-hot while held, releases on mouse-up). vpxlog rt_wndproc hit-tests the click against the bezel geometry (MFD 4 top / 4 bottom, radar 6 left / 6 right; the radar bottom indicators are display-only, not clickable); head windows are tagged via GWLP_USERDATA. Clicks arrive on the VPX render thread, so the seam serialrio RIO_HostButton() queues them under a mutex and pollInput() drains them on the emu tick through the same incHold/ decHold path as the keyboard/pad -- so a click also lights the bezel. - Flash blink: decode the lamp byte's flash bits (RioLampState 1 slow / 2 med / 3 fast) -- an unpressed flashing lamp now toggles between its brightness level and off (half-periods 500/250/125ms); solid lamps unchanged, a press still wins. - Focus: head windows are WS_EX_NOACTIVATE so a button click never pulls foreground off DOSBox (which would demote the emu thread and stop SDL polling the pad); a click uses MA_NOACTIVATE too. Do NOT enable SDL background joystick events -- that raced the emu-thread controller poll against SDL's main-thread pump and crashed the process. Explode layout only, and only when a serial=rio port is present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1073 lines
40 KiB
C++
1073 lines
40 KiB
C++
/*
|
|
* VWE fork: in-fork virtual RIO cockpit board (serial1=rio). See serialrio.h
|
|
* for the rationale. The protocol state machine + input mapping are transcribed
|
|
* from the validated vRIO app (C:\VWE\vrio VRio.Core); the transport, wire pacer,
|
|
* panel UI and thread locks are dropped -- everything here runs on the single
|
|
* emulator thread (CSerial callbacks + the SDL reads + the host keyboard hook),
|
|
* so no synchronization is needed. RX byte pacing reuses directserial's state
|
|
* machine (via the same rxpollus/rxburst knobs), except doReceive() drains a
|
|
* queue the board itself fills rather than a pipe.
|
|
*/
|
|
|
|
#include "dosbox.h"
|
|
|
|
#include "logging.h"
|
|
#include "pic.h"
|
|
#include "setup.h"
|
|
#include "serialport.h"
|
|
#include "serialrio.h"
|
|
|
|
#include <SDL.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdarg>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <cctype>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
bool getBituSubstring(const char* name, Bitu* data, CommandLine* cmd);
|
|
|
|
enum { P_RX_IDLE = 0, P_RX_WAIT, P_RX_BLOCKED, P_RX_FASTWAIT };
|
|
|
|
// ---- RIO wire constants (mirror vRIO VRio.Core/Protocol) --------------------
|
|
enum {
|
|
CMD_CHECK_REQ = 0x80, CMD_VER_REQ = 0x81, CMD_ANALOG_REQ = 0x82,
|
|
CMD_RESET_REQ = 0x83, CMD_LAMP_REQ = 0x84, CMD_CHECK_REPLY = 0x85,
|
|
CMD_VER_REPLY = 0x86, CMD_ANALOG_REPLY = 0x87, CMD_BTN_PRESS = 0x88,
|
|
CMD_BTN_RELEASE = 0x89, CMD_KEY_PRESS = 0x8A, CMD_KEY_RELEASE = 0x8B,
|
|
CMD_TESTMODE = 0x8C
|
|
};
|
|
enum { CTL_ACK = 0xFC, CTL_NAK = 0xFD, CTL_RESTART = 0xFE, CTL_IDLE = 0xFF };
|
|
|
|
// The I/O boards a healthy cockpit reports (RioAddressSpace.Boards); the game's
|
|
// CheckRequest expects one BoardOk CheckReply per entry, framed by test mode.
|
|
static const uint8_t kBoards[] = {
|
|
0x00, 0x08, 0x10, 0x11, 0x18, 0x19, 0x1A, 0x20, 0x28, 0x30, 0x38
|
|
};
|
|
|
|
static int rio_payload_len(uint8_t cmd) {
|
|
static const int t[] = { 0, 0, 0, 1, 2, 2, 2, 10, 1, 1, 2, 2, 1 };
|
|
if (cmd < 0x80 || cmd > 0x8C) return -1;
|
|
return t[cmd - 0x80];
|
|
}
|
|
|
|
static uint8_t rio_cs(const uint8_t* b, int n) {
|
|
int s = 0;
|
|
for (int i = 0; i < n; i++) s += (b[i] & 0x7F);
|
|
return (uint8_t)(s & 0x7F);
|
|
}
|
|
|
|
// 14-bit signed axis value -> two 7-bit wire bytes (AnalogCodec.Split).
|
|
static void rio_split(int16_t v, uint8_t& lo, uint8_t& hi) {
|
|
int raw = v & 0x3FFF;
|
|
lo = (uint8_t)(raw & 0x7F);
|
|
hi = (uint8_t)((raw >> 7) & 0x7F);
|
|
}
|
|
|
|
// ---- input mapping helpers (transcribed from vRIO InputRouter) ---------------
|
|
// axis indices match RioAxis: Throttle 0, LeftPedal 1, RightPedal 2, JoyY 3, JoyX 4
|
|
enum { PS_LX = 0, PS_LY, PS_RX, PS_RY, PS_LT, PS_RT };
|
|
|
|
static float rio_clampf(float x) { return x < -1.0f ? -1.0f : (x > 1.0f ? 1.0f : x); }
|
|
|
|
// deadzone (rescaled so travel stays continuous) + inversion (InputRouter.Shape)
|
|
static float rio_shape(float v, float dz, bool invert) {
|
|
if (dz > 0.0f) {
|
|
float m = std::fabs(v);
|
|
v = m <= dz ? 0.0f : (v < 0 ? -1.0f : 1.0f) * (m - dz) / (1.0f - dz);
|
|
}
|
|
return invert ? -v : v;
|
|
}
|
|
|
|
// stick axes are bipolar (-1..1), throttle/pedals unipolar (0..1) (ClampNorm)
|
|
static float rio_clampNorm(int axis, float v) {
|
|
float lo = (axis == 3 || axis == 4) ? -1.0f : 0.0f;
|
|
return v < lo ? lo : (v > 1.0f ? 1.0f : v);
|
|
}
|
|
|
|
// raw value at full travel from rest; sign is the wire direction (RioAxisRange)
|
|
static int rio_fullTravel(int axis) {
|
|
if (axis == 0) return -800; // throttle (forward = negative)
|
|
if (axis == 1 || axis == 2) return 500; // pedals (spring)
|
|
return 80; // joystick extent
|
|
}
|
|
|
|
static std::string rio_lower(const std::string& s) {
|
|
std::string r = s;
|
|
for (size_t i = 0; i < r.size(); i++) r[i] = (char)tolower((unsigned char)r[i]);
|
|
return r;
|
|
}
|
|
|
|
// key name -> SDL scancode. vRIO bindings files use .NET Keys names (D1,
|
|
// NumPad0, OemMinus, ...) -- alias those first so a vRIO bindings.txt ports
|
|
// over unchanged; everything else goes through SDL_GetScancodeFromName
|
|
// ("A", "1", "F1", "Up", "Space", "Keypad 5", ... -- case-insensitive).
|
|
static int rio_key_scancode(const std::string& name) {
|
|
static const struct { const char* net; const char* sdl; } kAliases[] = {
|
|
{ "d0", "0" }, { "d1", "1" }, { "d2", "2" }, { "d3", "3" }, { "d4", "4" },
|
|
{ "d5", "5" }, { "d6", "6" }, { "d7", "7" }, { "d8", "8" }, { "d9", "9" },
|
|
{ "numpad0", "Keypad 0" }, { "numpad1", "Keypad 1" }, { "numpad2", "Keypad 2" },
|
|
{ "numpad3", "Keypad 3" }, { "numpad4", "Keypad 4" }, { "numpad5", "Keypad 5" },
|
|
{ "numpad6", "Keypad 6" }, { "numpad7", "Keypad 7" }, { "numpad8", "Keypad 8" },
|
|
{ "numpad9", "Keypad 9" },
|
|
{ "oemminus", "-" }, { "oemplus", "=" }, { "oemcomma", "," }, { "oemperiod", "." },
|
|
{ "oemopenbrackets", "[" }, { "oemclosebrackets", "]" },
|
|
{ "divide", "Keypad /" }, { "multiply", "Keypad *" }, { "subtract", "Keypad -" },
|
|
{ "add", "Keypad +" }, { "decimal", "Keypad ." }, { "enter", "Return" },
|
|
{ "lshift", "Left Shift" }, { "rshift", "Right Shift" },
|
|
{ "lctrl", "Left Ctrl" }, { "rctrl", "Right Ctrl" },
|
|
{ "lalt", "Left Alt" }, { "ralt", "Right Alt" },
|
|
{ "scrolllock", "ScrollLock" }, { "pause", "Pause" },
|
|
};
|
|
std::string low = rio_lower(name);
|
|
for (size_t i = 0; i < sizeof(kAliases) / sizeof(kAliases[0]); i++)
|
|
if (low == kAliases[i].net)
|
|
return (int)SDL_GetScancodeFromName(kAliases[i].sdl);
|
|
return (int)SDL_GetScancodeFromName(name.c_str());
|
|
}
|
|
|
|
static bool rio_pad_button_name(const std::string& s, uint16_t& bit) {
|
|
static const struct { const char* n; uint16_t b; } t[] = {
|
|
{ "dpadup", 0x0001 }, { "dpaddown", 0x0002 }, { "dpadleft", 0x0004 },
|
|
{ "dpadright", 0x0008 }, { "start", 0x0010 }, { "back", 0x0020 },
|
|
{ "leftthumb", 0x0040 }, { "rightthumb", 0x0080 },
|
|
{ "leftshoulder", 0x0100 }, { "rightshoulder", 0x0200 },
|
|
{ "a", 0x1000 }, { "b", 0x2000 }, { "x", 0x4000 }, { "y", 0x8000 },
|
|
};
|
|
std::string low = rio_lower(s);
|
|
for (size_t i = 0; i < sizeof(t) / sizeof(t[0]); i++)
|
|
if (low == t[i].n) { bit = t[i].b; return true; }
|
|
return false;
|
|
}
|
|
|
|
static bool rio_pad_axis_name(const std::string& s, int& src) {
|
|
static const struct { const char* n; int v; } t[] = {
|
|
{ "leftstickx", PS_LX }, { "leftsticky", PS_LY },
|
|
{ "rightstickx", PS_RX }, { "rightsticky", PS_RY },
|
|
{ "lefttrigger", PS_LT }, { "righttrigger", PS_RT },
|
|
};
|
|
std::string low = rio_lower(s);
|
|
for (size_t i = 0; i < sizeof(t) / sizeof(t[0]); i++)
|
|
if (low == t[i].n) { src = t[i].v; return true; }
|
|
return false;
|
|
}
|
|
|
|
static bool rio_axis_name(const std::string& s, int& axis) {
|
|
static const struct { const char* n; int v; } t[] = {
|
|
{ "throttle", 0 }, { "leftpedal", 1 }, { "rightpedal", 2 },
|
|
{ "joysticky", 3 }, { "joystickx", 4 },
|
|
};
|
|
std::string low = rio_lower(s);
|
|
for (size_t i = 0; i < sizeof(t) / sizeof(t[0]); i++)
|
|
if (low == t[i].n) { axis = t[i].v; return true; }
|
|
return false;
|
|
}
|
|
|
|
static bool rio_parse_addr(const std::string& s, int& addr) {
|
|
char* end = nullptr;
|
|
long v = strtol(s.c_str(), &end, 0); // 0x-prefixed hex or decimal
|
|
if (end == s.c_str() || *end != '\0') return false;
|
|
addr = (int)v;
|
|
return (addr >= 0 && addr < 72) ||
|
|
(addr >= 0x50 && addr <= 0x5F) || (addr >= 0x60 && addr <= 0x6F);
|
|
}
|
|
|
|
static bool rio_parse_float(const std::string& s, float& f) {
|
|
char* end = nullptr;
|
|
f = strtof(s.c_str(), &end);
|
|
return end != s.c_str() && *end == '\0';
|
|
}
|
|
|
|
// the KMOD bit a modifier key itself contributes (0 for ordinary keys) --
|
|
// used so a BOUND modifier (e.g. LShift = throttle slew) can be routed and
|
|
// doesn't chord-block itself or other bound keys while held.
|
|
static unsigned int rio_own_mod_bit(int sc) {
|
|
switch (sc) {
|
|
case SDL_SCANCODE_LCTRL: return KMOD_LCTRL;
|
|
case SDL_SCANCODE_RCTRL: return KMOD_RCTRL;
|
|
case SDL_SCANCODE_LSHIFT: return KMOD_LSHIFT;
|
|
case SDL_SCANCODE_RSHIFT: return KMOD_RSHIFT;
|
|
case SDL_SCANCODE_LALT: return KMOD_LALT;
|
|
case SDL_SCANCODE_RALT: return KMOD_RALT;
|
|
case SDL_SCANCODE_LGUI: return KMOD_LGUI;
|
|
case SDL_SCANCODE_RGUI: return KMOD_RGUI;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// ---- host keyboard hook entry points ----------------------------------------
|
|
// One RIO port owns the host keyboard (first constructed wins).
|
|
static CSerialRio* rio_instance = nullptr;
|
|
|
|
bool RIO_HostKeyEvent(int sdl_scancode, bool pressed, bool repeat, unsigned int sdl_mods) {
|
|
return rio_instance ? rio_instance->hostKeyEvent(sdl_scancode, pressed, repeat, sdl_mods) : false;
|
|
}
|
|
|
|
void RIO_HostFocusLost(void) {
|
|
if (rio_instance) rio_instance->hostFocusLost();
|
|
}
|
|
|
|
bool RIO_GetPanelState(uint8_t lamps[72], uint8_t pressed[72]) {
|
|
if (!rio_instance) return false;
|
|
rio_instance->getPanelState(lamps, pressed);
|
|
return true;
|
|
}
|
|
|
|
void RIO_HostButton(int addr, bool down) {
|
|
if (rio_instance) rio_instance->hostButton(addr, down);
|
|
}
|
|
|
|
// ============================================================================
|
|
|
|
CSerialRio::CSerialRio(Bitu id, CommandLine* cmd) : CSerial(id, cmd) {
|
|
InstallationSuccessful = false;
|
|
|
|
Bitu tmp = 0;
|
|
if (getBituSubstring("pad:", &tmp, cmd)) pad_index = (int)tmp;
|
|
invertY = cmd->FindExist("inverty", false);
|
|
|
|
std::string bindPath, tmpstr;
|
|
if (cmd->FindStringBegin("bindings:", tmpstr, false)) bindPath = tmpstr;
|
|
else if (const char* e = getenv("VWE_RIO_BINDINGS")) bindPath = e;
|
|
|
|
Bitu rx_poll_us = 0;
|
|
if (getBituSubstring("rxpollus:", &rx_poll_us, cmd)) {
|
|
if (rx_poll_us < 50) rx_poll_us = 50;
|
|
if (rx_poll_us > 1000) rx_poll_us = 1000;
|
|
rx_poll_ms = (float)rx_poll_us / 1000.0f;
|
|
}
|
|
Bitu rx_burst = 0;
|
|
if (getBituSubstring("rxburst:", &rx_burst, cmd)) {
|
|
if (rx_burst < 1) rx_burst = 1;
|
|
if (rx_burst > 64) rx_burst = 64;
|
|
rx_burst_div = (float)rx_burst;
|
|
}
|
|
if (getBituSubstring("rxdelay:", &rx_retry_max, cmd)) {
|
|
if (!(rx_retry_max <= 10000)) rx_retry_max = 0;
|
|
}
|
|
|
|
logv = (getenv("VWE_RIO_LOG") != nullptr);
|
|
|
|
// capture toggle keys: Pause and ScrollLock by default (not every keyboard
|
|
// has both), plus an optional custom one via togglekey:<sdl-key-name>.
|
|
toggleKeys[0] = SDL_SCANCODE_PAUSE;
|
|
toggleKeys[1] = SDL_SCANCODE_SCROLLLOCK;
|
|
toggleKeys[2] = 0;
|
|
if (cmd->FindStringBegin("togglekey:", tmpstr, false)) {
|
|
int sc = rio_key_scancode(tmpstr);
|
|
if (sc != SDL_SCANCODE_UNKNOWN) {
|
|
toggleKeys[2] = sc;
|
|
LOG_MSG("Serial%d: RIO capture toggle also on '%s'", (int)COMNUMBER, tmpstr.c_str());
|
|
} else {
|
|
LOG_MSG("Serial%d: togglekey:'%s' not a recognized key name -- ignored",
|
|
(int)COMNUMBER, tmpstr.c_str());
|
|
}
|
|
}
|
|
|
|
loadDefaultProfile();
|
|
if (!bindPath.empty()) {
|
|
if (loadBindingsFile(bindPath))
|
|
LOG_MSG("Serial%d: RIO bindings file %s REPLACES the default profile",
|
|
(int)COMNUMBER, bindPath.c_str());
|
|
else
|
|
LOG_MSG("Serial%d: RIO bindings file %s unreadable -- keeping the default profile",
|
|
(int)COMNUMBER, bindPath.c_str());
|
|
}
|
|
computeBoundModMask();
|
|
|
|
LOG_MSG("Serial%d: virtual RIO board (pad %d, inverty %d, %u key + %u pad bindings, "
|
|
"keyboard CAPTURED for the panel -- PAUSE toggles panel/DOS)",
|
|
(int)COMNUMBER, pad_index, invertY ? 1 : 0,
|
|
(unsigned)(keyButtons.size() + keyAxes.size()),
|
|
(unsigned)(padButtons.size() + padAxes.size()));
|
|
|
|
CSerial::Init_Registers();
|
|
// the board is always present -- assert the modem-in lines the game reads
|
|
setRI(false);
|
|
setCD(true);
|
|
setDSR(true);
|
|
setCTS(true);
|
|
|
|
if (rio_instance == nullptr) rio_instance = this;
|
|
else LOG_MSG("Serial%d: another RIO port already owns the host keyboard hook", (int)COMNUMBER);
|
|
|
|
InstallationSuccessful = true;
|
|
rx_state = P_RX_IDLE;
|
|
setEvent(SERIAL_POLLING_EVENT, rx_poll_ms);
|
|
}
|
|
|
|
CSerialRio::~CSerialRio() {
|
|
if (rio_instance == this) rio_instance = nullptr;
|
|
if (pad) SDL_GameControllerClose((SDL_GameController*)pad);
|
|
pad = nullptr;
|
|
// events are cleared by the framework on port destruction
|
|
}
|
|
|
|
void CSerialRio::logf(const char* fmt, ...) {
|
|
if (!logv) return;
|
|
va_list ap; va_start(ap, fmt);
|
|
fprintf(stderr, "[rio] ");
|
|
vfprintf(stderr, fmt, ap);
|
|
fprintf(stderr, "\n");
|
|
va_end(ap);
|
|
}
|
|
|
|
// ---- binding profile ---------------------------------------------------------
|
|
|
|
// The vRIO default profile (BindingProfileFormat.DefaultText), transcribed 1:1
|
|
// with SDL scancodes standing in for the .NET Keys names.
|
|
void CSerialRio::loadDefaultProfile() {
|
|
keyButtons.clear(); keyAxes.clear(); padButtons.clear(); padAxes.clear();
|
|
|
|
static const RioKeyButtonBind kKeys[] = {
|
|
// upper MFD bank: number row = top MFD row, QWERTY row under it
|
|
{ SDL_SCANCODE_1, 0x2F, false }, { SDL_SCANCODE_2, 0x2E, false },
|
|
{ SDL_SCANCODE_3, 0x2D, false }, { SDL_SCANCODE_4, 0x2C, false },
|
|
{ SDL_SCANCODE_5, 0x27, false }, { SDL_SCANCODE_6, 0x26, false },
|
|
{ SDL_SCANCODE_7, 0x25, false }, { SDL_SCANCODE_8, 0x24, false },
|
|
{ SDL_SCANCODE_9, 0x37, false }, { SDL_SCANCODE_0, 0x36, false },
|
|
{ SDL_SCANCODE_MINUS, 0x35, false }, { SDL_SCANCODE_EQUALS, 0x34, false },
|
|
{ SDL_SCANCODE_Q, 0x2B, false }, { SDL_SCANCODE_W, 0x2A, false },
|
|
{ SDL_SCANCODE_E, 0x29, false }, { SDL_SCANCODE_R, 0x28, false },
|
|
{ SDL_SCANCODE_T, 0x23, false }, { SDL_SCANCODE_Y, 0x22, false },
|
|
{ SDL_SCANCODE_U, 0x21, false }, { SDL_SCANCODE_I, 0x20, false },
|
|
{ SDL_SCANCODE_O, 0x33, false }, { SDL_SCANCODE_P, 0x32, false },
|
|
{ SDL_SCANCODE_LEFTBRACKET, 0x31, false }, { SDL_SCANCODE_RIGHTBRACKET, 0x30, false },
|
|
// lower MFD bank: home row + the row below, 4-key blocks split by a gap
|
|
{ SDL_SCANCODE_A, 0x0F, false }, { SDL_SCANCODE_S, 0x0E, false },
|
|
{ SDL_SCANCODE_D, 0x0D, false }, { SDL_SCANCODE_F, 0x0C, false },
|
|
{ SDL_SCANCODE_H, 0x07, false }, { SDL_SCANCODE_J, 0x06, false },
|
|
{ SDL_SCANCODE_K, 0x05, false }, { SDL_SCANCODE_L, 0x04, false },
|
|
{ SDL_SCANCODE_Z, 0x0B, false }, { SDL_SCANCODE_X, 0x0A, false },
|
|
{ SDL_SCANCODE_C, 0x09, false }, { SDL_SCANCODE_V, 0x08, false },
|
|
{ SDL_SCANCODE_N, 0x03, false }, { SDL_SCANCODE_M, 0x02, false },
|
|
{ SDL_SCANCODE_COMMA, 0x01, false }, { SDL_SCANCODE_PERIOD, 0x00, false },
|
|
// Secondary / Screen columns on the function keys, top to bottom
|
|
{ SDL_SCANCODE_F1, 0x10, false }, { SDL_SCANCODE_F2, 0x11, false },
|
|
{ SDL_SCANCODE_F3, 0x12, false }, { SDL_SCANCODE_F4, 0x13, false },
|
|
{ SDL_SCANCODE_F5, 0x14, false }, { SDL_SCANCODE_F6, 0x15, false },
|
|
{ SDL_SCANCODE_F7, 0x18, false }, { SDL_SCANCODE_F8, 0x19, false },
|
|
{ SDL_SCANCODE_F9, 0x1A, false }, { SDL_SCANCODE_F10, 0x1B, false },
|
|
{ SDL_SCANCODE_F11, 0x1C, false }, { SDL_SCANCODE_F12, 0x1D, false },
|
|
// joystick column: hat on the arrows, Main on Space
|
|
{ SDL_SCANCODE_SPACE, 0x40, false }, // Main (fire)
|
|
{ SDL_SCANCODE_UP, 0x42, false }, // Hat Up
|
|
{ SDL_SCANCODE_DOWN, 0x41, false }, // Hat Back
|
|
{ SDL_SCANCODE_LEFT, 0x44, false }, // Hat Left
|
|
{ SDL_SCANCODE_RIGHT, 0x43, false }, // Hat Right
|
|
};
|
|
for (size_t i = 0; i < sizeof(kKeys) / sizeof(kKeys[0]); i++)
|
|
keyButtons.push_back(kKeys[i]);
|
|
|
|
// keyboard flight controls (operator 2026-07-22): numpad = stick/pedals,
|
|
// LShift/LCtrl = throttle slew, numpad-5 = throttle zero. The internal
|
|
// keypad is deliberately UNBOUND (dead plumbing in normal missions --
|
|
// mission review only; re-add via a bindings file if ever needed).
|
|
static const RioKeyAxisBind kKeyAxes[] = {
|
|
{ SDL_SCANCODE_KP_8, 3, RIO_KAX_DEFLECT, 2.5f }, // JoystickY full up
|
|
{ SDL_SCANCODE_KP_2, 3, RIO_KAX_DEFLECT, -2.5f }, // JoystickY full down
|
|
// wire X runs NEGATIVE to the right (same reason pad LeftStickX has
|
|
// invert), so left = +, right = -
|
|
{ SDL_SCANCODE_KP_4, 4, RIO_KAX_DEFLECT, 2.5f }, // JoystickX full left
|
|
{ SDL_SCANCODE_KP_6, 4, RIO_KAX_DEFLECT, -2.5f }, // JoystickX full right
|
|
{ SDL_SCANCODE_KP_7, 1, RIO_KAX_DEFLECT, 2.5f }, // LeftPedal full press
|
|
{ SDL_SCANCODE_KP_9, 2, RIO_KAX_DEFLECT, 2.5f }, // RightPedal full press
|
|
{ SDL_SCANCODE_LSHIFT, 0, RIO_KAX_RATE, 0.7f }, // Throttle slew +
|
|
{ SDL_SCANCODE_LCTRL, 0, RIO_KAX_RATE, -0.7f }, // Throttle slew -
|
|
{ SDL_SCANCODE_KP_5, 0, RIO_KAX_SET, 0.0f }, // Throttle zero
|
|
};
|
|
for (size_t i = 0; i < sizeof(kKeyAxes) / sizeof(kKeyAxes[0]); i++)
|
|
keyAxes.push_back(kKeyAxes[i]);
|
|
|
|
static const RioPadAxisBind kPadAxes[] = {
|
|
{ PS_LX, 4, true, 0.20f, 0.00f }, // LeftStickX -> JoystickX (invert)
|
|
{ PS_LY, 3, false, 0.20f, 0.00f }, // LeftStickY -> JoystickY
|
|
{ PS_RY, 0, false, 0.20f, 0.75f }, // RightStickY -> Throttle (rate)
|
|
{ PS_LT, 1, false, 0.12f, 0.00f }, // LeftTrigger -> LeftPedal
|
|
{ PS_RT, 2, false, 0.12f, 0.00f }, // RightTrigger-> RightPedal
|
|
};
|
|
for (size_t i = 0; i < sizeof(kPadAxes) / sizeof(kPadAxes[0]); i++)
|
|
padAxes.push_back(kPadAxes[i]);
|
|
|
|
static const RioPadButtonBind kPadBtns[] = {
|
|
{ 0x1000, 0x40, false }, // A -> Main / trigger (fire)
|
|
{ 0x2000, 0x46, false }, // B -> Thumb-low (torso center)
|
|
{ 0x4000, 0x45, false }, // X -> Pinky (look down)
|
|
{ 0x8000, 0x47, false }, // Y -> Thumb-high (look up)
|
|
{ 0x0001, 0x42, false }, // DPad Up -> Hat Up
|
|
{ 0x0002, 0x41, false }, // DPad Down -> Hat Back
|
|
{ 0x0004, 0x44, false }, // DPad Left -> Hat Left (torso twist L)
|
|
{ 0x0008, 0x43, false }, // DPad Right -> Hat Right (torso twist R)
|
|
{ 0x0100, 0x3D, false }, // Left Shoulder-> Panic
|
|
{ 0x0200, 0x3F, false }, // Right Should.-> Throttle button
|
|
};
|
|
for (size_t i = 0; i < sizeof(kPadBtns) / sizeof(kPadBtns[0]); i++)
|
|
padButtons.push_back(kPadBtns[i]);
|
|
}
|
|
|
|
// Bindings file, vRIO grammar (BindingProfileFormat.Parse): one binding per
|
|
// line, '#' comments, case-insensitive keywords; bad lines are reported with
|
|
// their line number and skipped. A readable file REPLACES the whole profile.
|
|
bool CSerialRio::loadBindingsFile(const std::string& path) {
|
|
std::ifstream f(path.c_str());
|
|
if (!f) return false;
|
|
|
|
std::vector<RioKeyButtonBind> kb;
|
|
std::vector<RioKeyAxisBind> ka;
|
|
std::vector<RioPadButtonBind> pb;
|
|
std::vector<RioPadAxisBind> pa;
|
|
|
|
std::string line;
|
|
int lineno = 0, bad = 0;
|
|
while (std::getline(f, line)) {
|
|
lineno++;
|
|
size_t hash = line.find('#');
|
|
if (hash != std::string::npos) line.erase(hash);
|
|
std::istringstream ss(line);
|
|
std::vector<std::string> tok;
|
|
std::string t;
|
|
while (ss >> t) tok.push_back(t);
|
|
if (tok.empty()) continue;
|
|
|
|
const char* err = nullptr;
|
|
if (tok.size() < 4) {
|
|
err = "expected '<source> <name> <target> <value> ...'";
|
|
} else {
|
|
std::string source = rio_lower(tok[0]);
|
|
std::string target = rio_lower(tok[2]);
|
|
bool toggle = false;
|
|
if (tok.size() >= 5 && rio_lower(tok[4]) == "toggle") toggle = true;
|
|
|
|
if (source == "key" && target == "button") {
|
|
int sc = rio_key_scancode(tok[1]);
|
|
int addr = 0;
|
|
if (sc == SDL_SCANCODE_UNKNOWN) err = "unknown key name";
|
|
else if (!rio_parse_addr(tok[3], addr)) err = "not a RIO input address (0x00-0x47, 0x50-0x6F)";
|
|
else if (tok.size() > 5 || (tok.size() == 5 && !toggle)) err = "only 'toggle' may follow the address";
|
|
else kb.push_back(RioKeyButtonBind{ sc, addr, toggle });
|
|
} else if (source == "key" && target == "axis") {
|
|
int sc = rio_key_scancode(tok[1]);
|
|
int axis = 0; float val = 0;
|
|
if (sc == SDL_SCANCODE_UNKNOWN) err = "unknown key name";
|
|
else if (!rio_axis_name(tok[3], axis)) err = "unknown RIO axis (Throttle/LeftPedal/RightPedal/JoystickY/JoystickX)";
|
|
else if (tok.size() < 6) err = "key axis bindings need 'deflect|slew|rate|set <n>'";
|
|
else {
|
|
std::string mode = rio_lower(tok[4]);
|
|
int kax = mode == "deflect" ? RIO_KAX_DEFLECT
|
|
: (mode == "rate" || mode == "slew") ? RIO_KAX_RATE
|
|
: mode == "set" ? RIO_KAX_SET : -1;
|
|
// value may carry its sign as a separate token: "+ 2.5"
|
|
std::string vs = tok[5];
|
|
if ((vs == "+" || vs == "-") && tok.size() > 6) vs += tok[6];
|
|
if (kax < 0) err = "unknown key-axis mode (deflect/slew/rate/set)";
|
|
else if (!rio_parse_float(vs, val)) err = "not a number";
|
|
else ka.push_back(RioKeyAxisBind{ sc, axis, kax, val });
|
|
}
|
|
} else if (source == "pad" && target == "button") {
|
|
uint16_t bit = 0; int addr = 0;
|
|
if (!rio_pad_button_name(tok[1], bit)) err = "unknown pad button";
|
|
else if (!rio_parse_addr(tok[3], addr)) err = "not a RIO input address (0x00-0x47, 0x50-0x6F)";
|
|
else if (tok.size() > 5 || (tok.size() == 5 && !toggle)) err = "only 'toggle' may follow the address";
|
|
else pb.push_back(RioPadButtonBind{ bit, addr, toggle });
|
|
} else if (source == "padaxis" && target == "axis") {
|
|
int src = 0, axis = 0;
|
|
bool invert = false; float dz = 0, rate = 0;
|
|
if (!rio_pad_axis_name(tok[1], src)) err = "unknown pad axis";
|
|
else if (!rio_axis_name(tok[3], axis)) err = "unknown RIO axis (Throttle/LeftPedal/RightPedal/JoystickY/JoystickX)";
|
|
else {
|
|
for (size_t i = 4; i < tok.size() && !err; i++) {
|
|
std::string opt = rio_lower(tok[i]);
|
|
if (opt == "invert") invert = true;
|
|
else if (opt == "deadzone") {
|
|
if (i + 1 >= tok.size() || !rio_parse_float(tok[++i], dz)) err = "'deadzone' needs a number";
|
|
} else if (opt == "rate") {
|
|
if (i + 1 >= tok.size() || !rio_parse_float(tok[++i], rate)) err = "'rate' needs a number";
|
|
} else err = "unknown padaxis option";
|
|
}
|
|
if (!err) pa.push_back(RioPadAxisBind{ src, axis, invert, dz, rate });
|
|
}
|
|
} else {
|
|
err = "unknown binding form (key/pad/padaxis ... button/axis)";
|
|
}
|
|
}
|
|
if (err) {
|
|
bad++;
|
|
LOG_MSG("Serial%d: RIO bindings line %d: %s", (int)COMNUMBER, lineno, err);
|
|
}
|
|
}
|
|
|
|
keyButtons.swap(kb);
|
|
keyAxes.swap(ka);
|
|
padButtons.swap(pb);
|
|
padAxes.swap(pa);
|
|
LOG_MSG("Serial%d: RIO bindings: %u key-button, %u key-axis, %u pad-button, %u pad-axis (%d bad line(s))",
|
|
(int)COMNUMBER, (unsigned)keyButtons.size(), (unsigned)keyAxes.size(),
|
|
(unsigned)padButtons.size(), (unsigned)padAxes.size(), bad);
|
|
return true;
|
|
}
|
|
|
|
// modifier bits bound as cockpit inputs are exempt from chord passthrough
|
|
// (holding a bound LShift/LCtrl must not push other bound keys to DOS).
|
|
void CSerialRio::computeBoundModMask() {
|
|
boundModMask = 0;
|
|
for (size_t i = 0; i < keyButtons.size(); i++)
|
|
boundModMask |= rio_own_mod_bit(keyButtons[i].scancode);
|
|
for (size_t i = 0; i < keyAxes.size(); i++)
|
|
boundModMask |= rio_own_mod_bit(keyAxes[i].scancode);
|
|
}
|
|
|
|
// ---- host keyboard routing ----------------------------------------------------
|
|
bool CSerialRio::hostKeyEvent(int sc, bool pressed, bool repeat, unsigned int mods) {
|
|
// capture toggle keys (Pause / ScrollLock / togglekey:<name>): always ours
|
|
// while the RIO is active.
|
|
for (int i = 0; i < 3; i++) {
|
|
if (toggleKeys[i] == 0 || sc != toggleKeys[i]) continue;
|
|
if (pressed && !repeat) {
|
|
captureKeys = !captureKeys;
|
|
if (!captureKeys) releaseAllKeys();
|
|
LOG_MSG("Serial%d: RIO keyboard %s", (int)COMNUMBER,
|
|
captureKeys ? "CAPTURED -> cockpit panel" : "RELEASED -> DOS");
|
|
logf("keyboard %s", captureKeys ? "captured" : "released");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (pressed) {
|
|
if (heldKeys.count(sc)) return true; // repeats / dup downs of routed keys
|
|
if (repeat) return false; // repeat of a key we didn't route
|
|
if (!captureKeys) return false;
|
|
// host chords (Alt+Enter etc.) pass through -- but modifier bits that
|
|
// are BOUND as inputs (LShift/LCtrl throttle slew) don't count, and a
|
|
// modifier key never blocks on its own bit.
|
|
unsigned int chord = (mods & ~rio_own_mod_bit(sc)) &
|
|
((KMOD_CTRL | KMOD_ALT | KMOD_GUI) & ~boundModMask);
|
|
if (chord) return false;
|
|
|
|
bool bound = false;
|
|
for (size_t i = 0; i < keyButtons.size(); i++) {
|
|
if (keyButtons[i].scancode != sc) continue;
|
|
bound = true;
|
|
if (keyButtons[i].toggle) toggleAddress(keyButtons[i].addr);
|
|
else incHold(keyButtons[i].addr);
|
|
}
|
|
for (size_t i = 0; i < keyAxes.size(); i++) {
|
|
if (keyAxes[i].scancode != sc) continue;
|
|
bound = true;
|
|
if (keyAxes[i].mode == RIO_KAX_SET) // snap the position now;
|
|
rate_i[keyAxes[i].axis] = // deflect/rate act in pollInput
|
|
rio_clampNorm(keyAxes[i].axis, keyAxes[i].value);
|
|
}
|
|
if (!bound) return false;
|
|
heldKeys.insert(sc);
|
|
return true;
|
|
}
|
|
|
|
// release: ours iff we routed the press, regardless of capture/mods now
|
|
if (!heldKeys.erase(sc)) return false;
|
|
for (size_t i = 0; i < keyButtons.size(); i++)
|
|
if (keyButtons[i].scancode == sc && !keyButtons[i].toggle)
|
|
decHold(keyButtons[i].addr);
|
|
return true;
|
|
}
|
|
|
|
// focus loss releases every held key so no panel button sticks; toggle latches
|
|
// survive (vRIO ReleaseAllKeys semantics).
|
|
void CSerialRio::hostFocusLost() {
|
|
releaseAllKeys();
|
|
}
|
|
|
|
// panel snapshot for the in-fork lamp bezel (vpxlog reads this each frame)
|
|
void CSerialRio::getPanelState(uint8_t lampsOut[72], uint8_t pressedOut[72]) {
|
|
for (int i = 0; i < 72; i++) {
|
|
lampsOut[i] = lamps[i];
|
|
pressedOut[i] = holdCounts.count(i) ? 1 : 0; // addr held right now
|
|
}
|
|
}
|
|
|
|
// panel click from the VPX render thread -- queue it; pollInput() (emu thread)
|
|
// drains and applies it, so it composes with the keyboard/pad hold counts.
|
|
void CSerialRio::hostButton(int addr, bool down) {
|
|
std::lock_guard<std::mutex> lk(panelMx);
|
|
panelQ.push_back(std::make_pair(addr, down));
|
|
}
|
|
|
|
void CSerialRio::releaseAllKeys() {
|
|
std::set<int> keys = heldKeys;
|
|
heldKeys.clear();
|
|
for (std::set<int>::const_iterator it = keys.begin(); it != keys.end(); ++it)
|
|
for (size_t i = 0; i < keyButtons.size(); i++)
|
|
if (keyButtons[i].scancode == *it && !keyButtons[i].toggle)
|
|
decHold(keyButtons[i].addr);
|
|
}
|
|
|
|
void CSerialRio::toggleAddress(int addr) {
|
|
if (toggled.erase(addr)) decHold(addr);
|
|
else { toggled.insert(addr); incHold(addr); }
|
|
}
|
|
|
|
// ---- board -> game queue helpers -------------------------------------------
|
|
void CSerialRio::emit(const uint8_t* p, int n) {
|
|
for (int i = 0; i < n; i++) rxq.push_back(p[i]);
|
|
}
|
|
void CSerialRio::emitControl(uint8_t c) { rxq.push_back(c); }
|
|
|
|
void CSerialRio::sendEvent(const uint8_t* p, int n) {
|
|
lastEventPacket.assign(p, p + n); // remembered so a NAK can re-send it
|
|
resends = 0;
|
|
emit(p, n);
|
|
}
|
|
|
|
void CSerialRio::pressAddress(int addr) {
|
|
if (addr >= 0 && addr < 72) { // lamp-capable button
|
|
uint8_t p[3] = { CMD_BTN_PRESS, (uint8_t)addr, 0 };
|
|
p[2] = rio_cs(p, 2); sendEvent(p, 3);
|
|
} else if ((addr >= 0x50 && addr <= 0x5F) || (addr >= 0x60 && addr <= 0x6F)) {
|
|
uint8_t padb = addr >= 0x60 ? 1 : 0;
|
|
uint8_t idx = (uint8_t)(addr - (padb ? 0x60 : 0x50));
|
|
uint8_t p[4] = { CMD_KEY_PRESS, padb, idx, 0 };
|
|
p[3] = rio_cs(p, 3); sendEvent(p, 4);
|
|
}
|
|
logf("press 0x%02X", addr);
|
|
}
|
|
|
|
void CSerialRio::releaseAddress(int addr) {
|
|
if (addr >= 0 && addr < 72) {
|
|
uint8_t p[3] = { CMD_BTN_RELEASE, (uint8_t)addr, 0 };
|
|
p[2] = rio_cs(p, 2); sendEvent(p, 3);
|
|
} else if ((addr >= 0x50 && addr <= 0x5F) || (addr >= 0x60 && addr <= 0x6F)) {
|
|
uint8_t padb = addr >= 0x60 ? 1 : 0;
|
|
uint8_t idx = (uint8_t)(addr - (padb ? 0x60 : 0x50));
|
|
uint8_t p[4] = { CMD_KEY_RELEASE, padb, idx, 0 };
|
|
p[3] = rio_cs(p, 3); sendEvent(p, 4);
|
|
}
|
|
logf("release 0x%02X", addr);
|
|
}
|
|
|
|
void CSerialRio::setAxis(int axis, int value) {
|
|
if (value < -8192) value = -8192;
|
|
if (value > 8191) value = 8191;
|
|
axes[axis] = (int16_t)value;
|
|
}
|
|
|
|
// ---- game -> board protocol -------------------------------------------------
|
|
void CSerialRio::feedByte(uint8_t ch) {
|
|
if (parse_remaining != 0) {
|
|
if (ch & 0x80) { // high bit mid-packet = framing error
|
|
parse_remaining = 0; parse_count = 0;
|
|
logf("framing error 0x%02X mid-packet", ch);
|
|
return;
|
|
}
|
|
parse_buf[parse_count++] = ch;
|
|
parse_remaining--;
|
|
if (parse_remaining == 0) {
|
|
int bodyLen = parse_count - 1; // command + payload (exclude checksum)
|
|
uint8_t got = parse_buf[bodyLen];
|
|
bool valid = rio_cs(parse_buf, bodyLen) == got;
|
|
onPacket(parse_buf, bodyLen, valid);
|
|
parse_remaining = 0; parse_count = 0;
|
|
}
|
|
return;
|
|
}
|
|
if (ch >= 0x80 && ch <= 0x8C) { // a command starts a packet
|
|
parse_count = 0;
|
|
parse_buf[parse_count++] = ch;
|
|
parse_remaining = rio_payload_len(ch) + 1; // + checksum byte
|
|
return;
|
|
}
|
|
onControl(ch); // ACK/NAK/RESTART/IDLE or garbage
|
|
}
|
|
|
|
void CSerialRio::onPacket(const uint8_t* body, int bodyLen, bool checksumValid) {
|
|
if (!checksumValid) {
|
|
emitControl(CTL_NAK);
|
|
logf("RX cmd 0x%02X bad checksum -> NAK", body[0]);
|
|
return;
|
|
}
|
|
emitControl(CTL_ACK);
|
|
|
|
uint8_t cmd = body[0];
|
|
const uint8_t* pl = body + 1;
|
|
int plen = bodyLen - 1;
|
|
|
|
switch (cmd) {
|
|
case CMD_CHECK_REQ: {
|
|
// init handshake: TestMode ENTER, a BoardOk per board, TestMode EXIT
|
|
uint8_t tm1[3] = { CMD_TESTMODE, 1, 0 }; tm1[2] = rio_cs(tm1, 2); emit(tm1, 3);
|
|
for (size_t i = 0; i < sizeof(kBoards); i++) {
|
|
uint8_t cr[4] = { CMD_CHECK_REPLY, 0 /*BoardOk*/, kBoards[i], 0 };
|
|
cr[3] = rio_cs(cr, 3); emit(cr, 4);
|
|
}
|
|
uint8_t tm0[3] = { CMD_TESTMODE, 0, 0 }; tm0[2] = rio_cs(tm0, 2); emit(tm0, 3);
|
|
logf("RX CheckRequest -> test mode, %d boards OK, exit", (int)sizeof(kBoards));
|
|
break;
|
|
}
|
|
case CMD_VER_REQ: {
|
|
uint8_t vr[4] = { CMD_VER_REPLY, verMajor, verMinor, 0 };
|
|
vr[3] = rio_cs(vr, 3); emit(vr, 4);
|
|
logf("RX VersionRequest -> %u.%u", verMajor, verMinor);
|
|
break;
|
|
}
|
|
case CMD_ANALOG_REQ: {
|
|
if (analogMuted) { logf("RX AnalogRequest dropped (wedged)"); break; }
|
|
analogRequests++;
|
|
int16_t wy = invertY ? axes[3]
|
|
: (int16_t)std::min(8191, -(int)axes[3]);
|
|
uint8_t p[12]; p[0] = CMD_ANALOG_REPLY;
|
|
rio_split(axes[0], p[1], p[2]); // throttle
|
|
rio_split(axes[1], p[3], p[4]); // left pedal
|
|
rio_split(axes[2], p[5], p[6]); // right pedal
|
|
rio_split(wy, p[7], p[8]); // joystick Y (wire direction)
|
|
rio_split(axes[4], p[9], p[10]); // joystick X
|
|
p[11] = rio_cs(p, 11); emit(p, 12);
|
|
if ((analogRequests % 200) == 0) logf("analog polls served: %ld", analogRequests);
|
|
break;
|
|
}
|
|
case CMD_RESET_REQ: {
|
|
uint8_t target = plen >= 1 ? pl[0] : 0;
|
|
applyReset(target);
|
|
analogMuted = false;
|
|
logf("RX ResetRequest %u", target);
|
|
break;
|
|
}
|
|
case CMD_LAMP_REQ: {
|
|
if (plen >= 2) {
|
|
int lamp = pl[0]; uint8_t st = pl[1];
|
|
if (lamp >= 0 && lamp < 72) {
|
|
lamps[lamp] = st; // stored for a future VDB lamp head
|
|
logf("RX Lamp 0x%02X = 0x%02X", lamp, st);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
logf("RX unexpected cmd 0x%02X", cmd);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CSerialRio::onControl(uint8_t b) {
|
|
switch (b) {
|
|
case CTL_ACK:
|
|
lastEventPacket.clear();
|
|
resends = 0;
|
|
break;
|
|
case CTL_NAK:
|
|
if (lastEventPacket.empty()) {
|
|
logf("RX NAK (nothing pending)");
|
|
} else if (resends < 4) { // v4.2 retry budget
|
|
resends++;
|
|
emit(lastEventPacket.data(), (int)lastEventPacket.size());
|
|
logf("RX NAK -> resend (%d)", resends);
|
|
} else {
|
|
lastEventPacket.clear();
|
|
resends = 0;
|
|
emitControl(CTL_RESTART); // give up (EmulateReplyWedge OFF)
|
|
logf("RX NAK -> retries exhausted, RESTART");
|
|
}
|
|
break;
|
|
case CTL_RESTART:
|
|
logf("RX RESTART");
|
|
break;
|
|
case CTL_IDLE:
|
|
break; // keep-alive noise
|
|
default:
|
|
logf("RX stray 0x%02X", b);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CSerialRio::applyReset(uint8_t target) {
|
|
int idx = target == 1 ? 0 : target == 2 ? 1 : target == 3 ? 2
|
|
: target == 4 ? 3 : target == 5 ? 4 : -1;
|
|
if (idx < 0) {
|
|
for (int i = 0; i < 5; i++) { axes[i] = 0; rate_i[i] = 0; last_sent_valid[i] = false; }
|
|
} else {
|
|
axes[idx] = 0; rate_i[idx] = 0; last_sent_valid[idx] = false;
|
|
}
|
|
}
|
|
|
|
// ---- gamepad + axis input ----------------------------------------------------
|
|
void CSerialRio::openPad() {
|
|
// NOTE: do NOT enable SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS here -- it
|
|
// makes SDL's main-thread event pump update the joystick concurrently with
|
|
// our emu-thread SDL_GameControllerUpdate (not thread-safe -> process crash,
|
|
// 2026-07-24). Keeping DOSBox focused (the head windows are WS_EX_NOACTIVATE
|
|
// so a click can't steal foreground) is what keeps the pad polling.
|
|
SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER);
|
|
int n = SDL_NumJoysticks();
|
|
for (int i = 0; i < n; i++) {
|
|
if (!SDL_IsGameController(i)) continue;
|
|
if (pad_index >= 0 && i != pad_index) continue;
|
|
SDL_GameController* gc = SDL_GameControllerOpen(i);
|
|
if (gc) {
|
|
pad = gc;
|
|
logf("opened gamepad %d: %s", i, SDL_GameControllerName(gc));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CSerialRio::incHold(int addr) {
|
|
int c = holdCounts[addr];
|
|
holdCounts[addr] = c + 1;
|
|
if (c == 0) pressAddress(addr);
|
|
}
|
|
|
|
void CSerialRio::decHold(int addr) {
|
|
std::map<int,int>::iterator it = holdCounts.find(addr);
|
|
if (it == holdCounts.end()) return;
|
|
if (it->second <= 1) { holdCounts.erase(it); releaseAddress(addr); }
|
|
else it->second--;
|
|
}
|
|
|
|
void CSerialRio::pollInput() {
|
|
double now = PIC_FullIndex();
|
|
double dt = last_tick_ms > 0.0 ? (now - last_tick_ms) / 1000.0 : 0.0;
|
|
last_tick_ms = now;
|
|
if (dt < 0.0) dt = 0.0;
|
|
if (dt > 0.1) dt = 0.1;
|
|
|
|
// apply panel clicks queued by the VPX window thread (RIO_HostButton) --
|
|
// same hold-count path as the keyboard/pad, so a held click lights the bezel
|
|
std::vector<std::pair<int,bool>> clicks;
|
|
{ std::lock_guard<std::mutex> lk(panelMx); clicks.swap(panelQ); }
|
|
for (size_t i = 0; i < clicks.size(); i++) {
|
|
if (clicks[i].second) incHold(clicks[i].first);
|
|
else decHold(clicks[i].first);
|
|
logf("panel %s 0x%02X", clicks[i].second ? "click" : "release", clicks[i].first);
|
|
}
|
|
|
|
// gamepad snapshot; everything rests at zero when absent/detached, so a
|
|
// detach releases whatever the pad held (button edges below see btns=0)
|
|
float src[6] = { 0, 0, 0, 0, 0, 0 };
|
|
uint16_t btns = 0;
|
|
if (!pad && now >= next_pad_open_ms) { next_pad_open_ms = now + 1000.0; openPad(); }
|
|
if (pad) {
|
|
SDL_GameController* gc = (SDL_GameController*)pad;
|
|
if (!SDL_GameControllerGetAttached(gc)) {
|
|
SDL_GameControllerClose(gc); pad = nullptr;
|
|
logf("gamepad detached");
|
|
} else {
|
|
SDL_GameControllerUpdate();
|
|
src[PS_LX] = rio_clampf( SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX) / 32767.0f);
|
|
src[PS_LY] = rio_clampf(-SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY) / 32767.0f);
|
|
src[PS_RX] = rio_clampf( SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX) / 32767.0f);
|
|
src[PS_RY] = rio_clampf(-SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY) / 32767.0f);
|
|
src[PS_LT] = rio_clampf( SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERLEFT) / 32767.0f);
|
|
src[PS_RT] = rio_clampf( SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) / 32767.0f);
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_A)) btns |= 0x1000;
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_B)) btns |= 0x2000;
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_X)) btns |= 0x4000;
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_Y)) btns |= 0x8000;
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_DPAD_UP)) btns |= 0x0001;
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) btns |= 0x0002;
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) btns |= 0x0004;
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) btns |= 0x0008;
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_START)) btns |= 0x0010;
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_BACK)) btns |= 0x0020;
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_LEFTSTICK)) btns |= 0x0040;
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_RIGHTSTICK)) btns |= 0x0080;
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_LEFTSHOULDER)) btns |= 0x0100;
|
|
if (SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)) btns |= 0x0200;
|
|
}
|
|
}
|
|
|
|
// pass 1: advance rate integrators (throttle-style axes; pad + held rate keys)
|
|
for (size_t i = 0; i < padAxes.size(); i++) {
|
|
const RioPadAxisBind& b = padAxes[i];
|
|
if (b.rate > 0.0f)
|
|
rate_i[b.axis] = rio_clampNorm(b.axis,
|
|
rate_i[b.axis] + rio_shape(src[b.src], b.dz, b.invert) * b.rate * (float)dt);
|
|
}
|
|
for (size_t i = 0; i < keyAxes.size(); i++) {
|
|
const RioKeyAxisBind& b = keyAxes[i];
|
|
if (b.mode == RIO_KAX_RATE && heldKeys.count(b.scancode))
|
|
rate_i[b.axis] = rio_clampNorm(b.axis, rate_i[b.axis] + b.value * (float)dt);
|
|
}
|
|
// pass 2: compose each axis (rate + held deflect keys + direct pad) and
|
|
// push it if it moved
|
|
for (int a = 0; a < 5; a++) {
|
|
float total = rate_i[a];
|
|
for (size_t i = 0; i < keyAxes.size(); i++) {
|
|
const RioKeyAxisBind& b = keyAxes[i];
|
|
if (b.mode == RIO_KAX_DEFLECT && b.axis == a && heldKeys.count(b.scancode))
|
|
total += b.value;
|
|
}
|
|
for (size_t i = 0; i < padAxes.size(); i++) {
|
|
const RioPadAxisBind& b = padAxes[i];
|
|
if (b.axis == a && b.rate <= 0.0f)
|
|
total += rio_shape(src[b.src], b.dz, b.invert);
|
|
}
|
|
total = rio_clampNorm(a, total);
|
|
int raw = (int)lround(total * rio_fullTravel(a));
|
|
if (!last_sent_valid[a] || raw != last_sent[a]) {
|
|
setAxis(a, raw);
|
|
last_sent[a] = raw;
|
|
last_sent_valid[a] = true;
|
|
}
|
|
}
|
|
// pad buttons: edge-detect and route through hold counts
|
|
uint16_t changed = btns ^ prevPadButtons;
|
|
if (changed) {
|
|
for (size_t i = 0; i < padButtons.size(); i++) {
|
|
const RioPadButtonBind& b = padButtons[i];
|
|
if (!(changed & b.bit)) continue;
|
|
bool down = (btns & b.bit) != 0;
|
|
if (b.toggle) { if (down) toggleAddress(b.addr); }
|
|
else if (down) incHold(b.addr);
|
|
else decHold(b.addr);
|
|
}
|
|
prevPadButtons = btns;
|
|
}
|
|
}
|
|
|
|
// ---- UART plumbing (mirrors serialnamedpipe / directserial) -----------------
|
|
bool CSerialRio::doReceive() {
|
|
if (rxq.empty()) return false;
|
|
uint8_t b = rxq.front();
|
|
rxq.pop_front();
|
|
receiveByteEx(b, 0);
|
|
return true;
|
|
}
|
|
|
|
void CSerialRio::handleUpperEvent(uint16_t type) {
|
|
switch (type) {
|
|
case SERIAL_POLLING_EVENT: {
|
|
setEvent(SERIAL_POLLING_EVENT, rx_poll_ms);
|
|
pollInput();
|
|
// directserial's RX state machine, delivering queued board->game bytes
|
|
switch (rx_state) {
|
|
case P_RX_IDLE:
|
|
if (CanReceiveByte()) {
|
|
if (doReceive()) {
|
|
rx_state = P_RX_WAIT;
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
|
|
}
|
|
} else {
|
|
rx_state = P_RX_BLOCKED;
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
|
|
}
|
|
break;
|
|
case P_RX_BLOCKED:
|
|
if (!CanReceiveByte()) {
|
|
rx_retry++;
|
|
if (rx_retry >= rx_retry_max) {
|
|
rx_retry = 0;
|
|
removeEvent(SERIAL_RX_EVENT);
|
|
if (doReceive()) {
|
|
while (doReceive());
|
|
rx_state = P_RX_WAIT;
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
|
|
} else {
|
|
rx_state = P_RX_IDLE;
|
|
}
|
|
}
|
|
} else {
|
|
removeEvent(SERIAL_RX_EVENT);
|
|
rx_retry = 0;
|
|
if (doReceive()) {
|
|
rx_state = P_RX_FASTWAIT;
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.65f / rx_burst_div);
|
|
} else {
|
|
rx_state = P_RX_IDLE;
|
|
}
|
|
}
|
|
break;
|
|
case P_RX_WAIT:
|
|
case P_RX_FASTWAIT:
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case SERIAL_RX_EVENT: {
|
|
switch (rx_state) {
|
|
case P_RX_IDLE:
|
|
LOG_MSG("internal error in serialrio");
|
|
break;
|
|
case P_RX_BLOCKED:
|
|
case P_RX_WAIT:
|
|
case P_RX_FASTWAIT:
|
|
if (CanReceiveByte()) {
|
|
rx_retry = 0;
|
|
if (doReceive()) {
|
|
if (rx_state == P_RX_WAIT)
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
|
|
else {
|
|
rx_state = P_RX_FASTWAIT;
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.65f / rx_burst_div);
|
|
}
|
|
} else {
|
|
rx_state = P_RX_IDLE;
|
|
}
|
|
} else {
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.65f / rx_burst_div);
|
|
rx_state = P_RX_BLOCKED;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case SERIAL_TX_EVENT: {
|
|
if (rx_state == P_RX_IDLE && CanReceiveByte()) {
|
|
if (doReceive()) {
|
|
rx_state = P_RX_WAIT;
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
|
|
}
|
|
}
|
|
ByteTransmitted();
|
|
break;
|
|
}
|
|
case SERIAL_THR_EVENT: {
|
|
ByteTransmitting();
|
|
setEvent(SERIAL_TX_EVENT, bytetime * 1.1f);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CSerialRio::updatePortConfig(uint16_t divider, uint8_t lcr) {
|
|
(void)divider; (void)lcr; // the board is fixed 9600 8N1; base tracks bytetime
|
|
}
|
|
|
|
void CSerialRio::updateMSR() {
|
|
// modem-in lines are static (board present); nothing to poll
|
|
}
|
|
|
|
void CSerialRio::transmitByte(uint8_t val, bool first) {
|
|
if (first) setEvent(SERIAL_THR_EVENT, bytetime / 10);
|
|
else setEvent(SERIAL_TX_EVENT, bytetime);
|
|
feedByte(val); // hand the game's byte to the board protocol
|
|
}
|
|
|
|
void CSerialRio::setBreak(bool value) {
|
|
(void)value; // RIO doesn't use break
|
|
}
|
|
|
|
void CSerialRio::setRTSDTR(bool rts, bool dtr) {
|
|
(void)rts;
|
|
setDTR(dtr);
|
|
}
|
|
|
|
void CSerialRio::setRTS(bool val) {
|
|
(void)val; // board ignores host RTS
|
|
}
|
|
|
|
void CSerialRio::setDTR(bool val) {
|
|
if (val == dtr_state) return;
|
|
bool rising = val && !dtr_state;
|
|
dtr_state = val;
|
|
if (rising) {
|
|
// the game pulses DTR to hard-reset the board; clear comms state so
|
|
// the following CheckRequest handshake starts clean.
|
|
parse_remaining = 0; parse_count = 0;
|
|
lastEventPacket.clear(); resends = 0;
|
|
analogMuted = false;
|
|
rxq.clear();
|
|
logf("DTR reset");
|
|
}
|
|
}
|