Files
TeslaRel410/emulator/vpx-device/serialrio.h
T
CydandClaude Opus 4.8 9dd0aa8702 emulator: RIO panel click-to-press + lamp flash blink
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>
2026-07-24 11:50:53 -05:00

195 lines
9.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 <mutex>
#include <set>
#include <string>
#include <utility>
#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]);
// A panel button was clicked (down=true) or released in a VDB head window.
// Called from the VPX render thread (rt_wndproc), so it is thread-safe: the
// event is queued and applied on the emulator's next tick. No-op if no rio port.
void RIO_HostButton(int addr, bool down);
// ---- 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]);
void hostButton(int addr, bool down); // panel click (VPX thread -> queue)
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::mutex panelMx; // guards panelQ (VPX thread <-> emu)
std::vector<std::pair<int,bool>> panelQ; // pending panel clicks (addr, down)
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