/* * 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 #include #include #include #include #include #include #include #include #include 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; } // ============================================================================ 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:. 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 kb; std::vector ka; std::vector pb; std::vector 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 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 ' ...'"; } 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 '"; 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:): 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 } } void CSerialRio::releaseAllKeys() { std::set keys = heldKeys; heldKeys.clear(); for (std::set::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() { 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::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; // 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"); } }