Emulate the cockpit RIO board natively inside the DOSBox-X fork, driven by
an SDL game controller + host keyboard -- no external vRIO process and no
named pipe. The game<->board round trip runs in-process at the emulated
UART's own cadence, so the serial round-trip dropouts (livelock, TXMAXIDLE,
rxburst, 15s retry) can't occur.
- serialrio.{cpp,h}: RIO 9600-8N1 device state machine + protocol codec +
input mapping, transcribed from the vRIO app (VRioDevice + Protocol +
InputRouter/BindingProfileFormat), minus transport/pacer/UI/locks.
B0 = gamepad (5 axes + joystick column). B1 = keyboard field: MFD banks on
the letter rows, F-keys = Secondary/Screen, numpad = flight controls,
LShift/LCtrl throttle slew; PAUSE/ScrollLock toggle panel<->DOS; vRIO-
grammar bindings file via bindings:/VWE_RIO_BINDINGS.
- vpx-device/README.md: device notes + apply steps (the DOSBox-X src tree is
git-ignored, so this dir is the tracked source of truth; the three sdlmain
keyboard-hook edits are documented there).
- net_{loop,rp}_rio.conf + deploy templates; render-bridge/
gauge_arena_rio_sound.conf (standalone -egg trim); pod-launch --rio
(mutually exclusive with --pipe).
Additive: real pods keep directserial realport:COM1; vRIO-over-pipe keeps
namedpipe pipe:vrio. Validated live 2026-07-22 (pad + keyboard field +
console networking) on the dist install.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
178 lines
8.1 KiB
C++
178 lines
8.1 KiB
C++
/*
|
|
* VWE fork: in-fork virtual RIO cockpit board (serial1=rio).
|
|
*
|
|
* A native DOSBox-X serial backend that *is* the cockpit RIO board: it speaks
|
|
* the device side of the RIO 9600-8N1 protocol to the game's UART, and sources
|
|
* its pilot input from an SDL game controller + the host keyboard inside the
|
|
* emulator. There is no external process and no pipe, so the game<->board
|
|
* round trip happens entirely in-process at the emulated UART's own byte
|
|
* cadence -- the same timing a real ISA UART sees on a physical pod, which
|
|
* removes the whole class of serial / pipe round-trip-latency dropouts
|
|
* (livelock, TXMAXIDLE, rxburst, 15s retry).
|
|
*
|
|
* The protocol state machine + input mapping are transcribed from the
|
|
* validated vRIO app (C:\VWE\vrio VRio.Core: VRioDevice, the Protocol codec,
|
|
* Input/InputRouter + BindingProfileFormat), minus its transport, wire pacer,
|
|
* panel UI and thread locks -- CSerial callbacks, the SDL reads and the host
|
|
* keyboard hook all run on the single emulator thread.
|
|
*
|
|
* Keyboard field (B1): sdlmain's SDL2 event loop offers every key to
|
|
* RIO_HostKeyEvent() first. While capture is ON (default; PAUSE or
|
|
* SCROLL LOCK toggles it -- plus an optional `togglekey:<name>`) a key bound
|
|
* in the profile presses its RIO address (button 0x00-0x47 / keypad
|
|
* 0x50-0x6F) or drives an axis, and is swallowed before the DOS keyboard
|
|
* sees it; unbound keys and Ctrl/Alt/Gui chords pass through (bound modifier
|
|
* keys are exempt from the chord rule), so DOSBox-X host hotkeys keep
|
|
* working. Toggling capture OFF releases everything held and gives the whole
|
|
* keyboard back to DOS (needed at the netnub loop / DOS prompts).
|
|
*
|
|
* Bindings: the built-in default profile = vRIO's pad + button field
|
|
* (number/QWERTY rows = upper MFD bank, home/bottom rows = lower bank,
|
|
* F1-F12 = Secondary/Screen columns, arrows+Space = hat+main) plus keyboard
|
|
* FLIGHT controls (operator 2026-07-22): numpad 8/2/4/6 = stick, 7/9 =
|
|
* pedals, LShift/LCtrl = throttle slew, numpad-5 = throttle zero; the
|
|
* internal keypad is unbound (mission-review-only plumbing). A bindings file
|
|
* in vRIO's grammar (`key <name> button <addr> [toggle]` / `key <name> axis
|
|
* <axis> deflect|slew|rate|set <n>` / `pad <button> button <addr> [toggle]`
|
|
* / `padaxis <src> axis <axis> [invert] [deadzone <d>] [rate <n>]`, '#'
|
|
* comments, sign may be a separate token) REPLACES the default: conf arg
|
|
* `bindings:<path>`, else env VWE_RIO_BINDINGS. vRIO's .NET key names (D1,
|
|
* NumPad0, OemMinus, ...) are aliased, so a vRIO bindings.txt ports over.
|
|
*
|
|
* This is an ADDITIVE dev / standalone backend. Real pods keep the physical
|
|
* board on `serial1=directserial realport:COM1`; vRIO-over-pipe keeps
|
|
* `serial1=namedpipe pipe:vrio`. Neither is touched by this file.
|
|
*
|
|
* Conf: serial1=rio [pad:<n>] [inverty] [bindings:<path>] [togglekey:<key>]
|
|
* [rxpollus:<us>] [rxburst:<n>]
|
|
* pad:<n> game-controller index to use (default: first present)
|
|
* inverty send the joystick Y in the physical direction (up = +)
|
|
* bindings:<p> bindings file (vRIO grammar; replaces the default)
|
|
* togglekey:<k> extra capture-toggle key (SDL name, e.g. End, F24)
|
|
* rx opts directserial semantics (board->game delivery pacing)
|
|
*/
|
|
|
|
#ifndef DOSBOX_SERIALRIO_H
|
|
#define DOSBOX_SERIALRIO_H
|
|
|
|
#include "dosbox.h"
|
|
#include "serialport.h"
|
|
|
|
#include <deque>
|
|
#include <map>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// ---- host-side hooks (called from sdlmain's SDL2 event loop) ----------------
|
|
// Both are cheap no-ops when no serial<n>=rio port is installed.
|
|
// RIO_HostKeyEvent returns true when the key was consumed by the cockpit panel
|
|
// (bound + captured, or the PAUSE capture toggle) -- the caller must then skip
|
|
// its normal keyboard handling for that event.
|
|
bool RIO_HostKeyEvent(int sdl_scancode, bool pressed, bool repeat, unsigned int sdl_mods);
|
|
void RIO_HostFocusLost(void);
|
|
|
|
// ---- binding profile (transcribed from vRIO BindingProfile) -----------------
|
|
// key-axis modes: DEFLECT holds the axis at value while the key is down and
|
|
// springs back; RATE ("slew" in bindings files) walks the axis by value/second
|
|
// and the position sticks; SET snaps the axis position to value on key press.
|
|
enum { RIO_KAX_DEFLECT = 0, RIO_KAX_RATE = 1, RIO_KAX_SET = 2 };
|
|
struct RioKeyButtonBind { int scancode; int addr; bool toggle; };
|
|
struct RioKeyAxisBind { int scancode; int axis; int mode; float value; };
|
|
struct RioPadButtonBind { uint16_t bit; int addr; bool toggle; };
|
|
struct RioPadAxisBind { int src; int axis; bool invert; float dz; float rate; };
|
|
|
|
class CSerialRio : public CSerial {
|
|
public:
|
|
CSerialRio(Bitu id, CommandLine* cmd);
|
|
virtual ~CSerialRio();
|
|
|
|
void updatePortConfig(uint16_t divider, uint8_t lcr);
|
|
void updateMSR();
|
|
void transmitByte(uint8_t val, bool first);
|
|
void setBreak(bool value);
|
|
void setRTSDTR(bool rts, bool dtr);
|
|
void setRTS(bool val);
|
|
void setDTR(bool val);
|
|
void handleUpperEvent(uint16_t type);
|
|
|
|
// host keyboard (forwarded by the RIO_Host* free functions)
|
|
bool hostKeyEvent(int sdl_scancode, bool pressed, bool repeat, unsigned int sdl_mods);
|
|
void hostFocusLost();
|
|
|
|
private:
|
|
// ---- board -> game byte delivery (UART RX pacing; mirrors directserial) --
|
|
bool doReceive();
|
|
std::deque<uint8_t> rxq; // bytes the board wants to send to the game
|
|
float rx_poll_ms = 1.0f;
|
|
float rx_burst_div = 1.0f;
|
|
Bitu rx_retry = 0;
|
|
Bitu rx_retry_max = 0;
|
|
Bitu rx_state = 0;
|
|
|
|
// ---- game -> board protocol RX parser --------------------------------
|
|
void feedByte(uint8_t ch);
|
|
uint8_t parse_buf[16] = {};
|
|
int parse_remaining = 0; // bytes still expected (0 = not in a packet)
|
|
int parse_count = 0; // bytes stored for the current packet
|
|
|
|
// ---- device (board) state --------------------------------------------
|
|
void onPacket(const uint8_t* body, int bodyLen, bool checksumValid);
|
|
void onControl(uint8_t b);
|
|
void applyReset(uint8_t target);
|
|
void emit(const uint8_t* p, int n); // queue board->game bytes
|
|
void emitControl(uint8_t c);
|
|
void sendEvent(const uint8_t* p, int n); // remember (for NAK resend) + emit
|
|
void pressAddress(int addr);
|
|
void releaseAddress(int addr);
|
|
void setAxis(int axis, int value);
|
|
|
|
int16_t axes[5] = {};
|
|
uint8_t lamps[72] = {};
|
|
std::vector<uint8_t> lastEventPacket; // last button/key/test event
|
|
int resends = 0;
|
|
bool analogMuted = false; // v4.2 wedge bug -- default OFF
|
|
uint8_t verMajor = 4, verMinor = 2;
|
|
bool invertY = false;
|
|
long analogRequests = 0;
|
|
bool dtr_state = false;
|
|
|
|
// ---- input profile + routing state ------------------------------------
|
|
void loadDefaultProfile();
|
|
bool loadBindingsFile(const std::string& path);
|
|
void computeBoundModMask();
|
|
void openPad();
|
|
void pollInput();
|
|
void incHold(int addr);
|
|
void decHold(int addr);
|
|
void toggleAddress(int addr);
|
|
void releaseAllKeys();
|
|
|
|
std::vector<RioKeyButtonBind> keyButtons;
|
|
std::vector<RioKeyAxisBind> keyAxes;
|
|
std::vector<RioPadButtonBind> padButtons;
|
|
std::vector<RioPadAxisBind> padAxes;
|
|
|
|
void* pad = nullptr; // SDL_GameController* (opaque here)
|
|
int pad_index = -1; // requested index; -1 = first
|
|
double next_pad_open_ms = 0.0;
|
|
double last_tick_ms = 0.0;
|
|
float rate_i[5] = {}; // rate-integrator per axis
|
|
int last_sent[5] = {};
|
|
bool last_sent_valid[5] = {};
|
|
std::map<int,int> holdCounts; // held count per RIO address
|
|
std::set<int> heldKeys; // scancodes routed to the panel
|
|
std::set<int> toggled; // latched (toggle) addresses
|
|
uint16_t prevPadButtons = 0;
|
|
bool captureKeys = true; // toggle keys flip panel <-> DOS
|
|
int toggleKeys[3] = { 0, 0, 0 }; // scancodes; defaults Pause+ScrollLock,
|
|
// slot 2 = togglekey:<name> conf arg
|
|
unsigned int boundModMask = 0; // modifier bits bound as inputs
|
|
// (exempt from chord passthrough)
|
|
|
|
bool logv = false; // VWE_RIO_LOG stderr trace
|
|
void logf(const char* fmt, ...);
|
|
};
|
|
|
|
#endif // DOSBOX_SERIALRIO_H
|