Files
TeslaRel410/emulator/vpx-device/serialrio.cpp
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

1047 lines
39 KiB
C++

/*
* 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 <SDL.h>
#include <algorithm>
#include <cmath>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cctype>
#include <fstream>
#include <sstream>
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:<sdl-key-name>.
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<RioKeyButtonBind> kb;
std::vector<RioKeyAxisBind> ka;
std::vector<RioPadButtonBind> pb;
std::vector<RioPadAxisBind> 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<std::string> 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 '<source> <name> <target> <value> ...'";
} 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 <n>'";
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:<name>): 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<int> keys = heldKeys;
heldKeys.clear();
for (std::set<int>::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<int,int>::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");
}
}