Files
TeslaRel410/emulator/vpx-device/serialrio.h
T
CydandClaude Opus 4.8 86d6b950e5 emulator: B2 RIO lamp/button bezel on the VDB heads + explode-view polish
Draw the cockpit RIO buttons around the mono-MFD and radar heads (explode
layout), lit by the host-commanded lamp state -- the display side of the
in-fork glass cockpit. Live-validated 2026-07-24 (operator).

- vpxlog.cpp: per-head button bezel in pal_draw, reading serialrio's new
  RIO_GetPanelState seam. The 5 MFD heads get red buttons (4 top / 4 bottom,
  100px tall tucked under a grown 640x500 display so a 10px lip shows); the
  radar gets amber Secondary/Screen side columns (6x 104px each) plus a
  centered bottom indicator strip (the 4 spares). Lamp byte decoded to
  off/dim/bright (vRIO RioLampState, brighter of the two brightness fields);
  a press shows white-hot. No labels -- the MFD shows each button's function.
  Also swapped the two upper-outer MFD window NAMES to match their (already
  position-swapped) desktop locations.
- serialrio.{cpp,h}: RIO_GetPanelState(lamps, pressed) accessor -- returns
  false when no rio port, so non-rio configs and other heads render normally.
- pod-launch: in explode (dev) layout the Division bridge is a normal, freely
  movable window -- not pinned topmost (Focus.cs) and nudged to 8,40 so its
  title bar clears the top of the screen (its client was at 0,0, pushing the
  frame off-screen). Cockpit/kiosk keeps the topmost + 0,0 borderless look.

Only affects the explode layout and only when a serial=rio port is present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:43:41 -05:00

185 lines
8.6 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);
// Current panel state for the in-fork lamp/button bezel (B2): fills lamps[]
// with the host-commanded LampRequest byte and pressed[] with 1 where a button
// address is currently held. Returns false (and touches nothing) when no
// serial<n>=rio port is installed, so the VDB heads render normally.
bool RIO_GetPanelState(uint8_t lamps[72], uint8_t pressed[72]);
// ---- 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();
void getPanelState(uint8_t lampsOut[72], uint8_t pressedOut[72]);
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