Files
TeslaRel410/emulator/vpx-device/serialrio.h
T
CydandClaude Opus 4.8 73b597df35 emulator: in-fork virtual RIO cockpit board (serial1=rio)
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>
2026-07-24 10:43:41 -05:00

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