Files
BT411/engine/MUNGA_L4/L4PADRIO.cpp
T
arcattackandClaude Fable 5 6ad1e074cc Glass lever: port the pod-build GAIT DETENT to the PadRIO throttle slew
Human re-test on the fresh build: zero-stop OK, but the lever could PARK inside
the walk/run hysteresis band (walkStrideLength@0x534 .. reverseSpeedMax@0x538)
where the gait SM has no stable state -- walk<->run hunt, EngineShiftFwd/Rev
retriggering ("mixed walk/run mode + repeated acceleration sample").  The
pod-build keyboard lever already snaps out of the band to the NEARER edge at
key-rest (mech4.cpp GAIT DETENT); the PadRIO slew channel lacked it.

Faithful port, not an invention: mech4.cpp publishes the band in lever units
every driven frame (gBTGaitDetentLo/Hi seam, same guards + the same +0.002
engage margin); L4PADRIO Poll() applies the identical nearer-edge snap to the
Throttle channel when the slew rests (no slew key held, no pad slew axis
deflected).  Sweeping through the band while held stays continuous (the
authentic moving lever, one authentic shift).

Verified (DEV.EGG glass, graduated parks each ~0.10 of the sweep,
BT_GAIT_TRACE+BT_MPPR_TRACE): band [0.358026,0.50388) = demand 22.02-30.87;
in-band parks snap both directions (0.4137->lo, 0.465126->hi); no at-rest
sample in the band; exactly ONE walk->run clip (state 10) up and ONE run->walk
(state 14) down across the whole sweep; zero-stop unaffected; pod build
rebuilt + BT_AUTODRIVE smoke clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:57:34 -05:00

780 lines
21 KiB
C++

#include "mungal4.h"
#pragma hdrstop
//###########################################################################
// L4PADRIO -- the hardware-less cockpit device (BT_GLASS only; this TU is
// only in the build when the gate is on -- see CMakeLists.txt).
// Design + input model: L4PADRIO.h.
//###########################################################################
#include "l4padrio.h"
#include "l4padpanel.h"
#include "l4ctrl.h"
#include <windows.h>
#include <xinput.h>
#include <stdlib.h>
#include <string.h>
#pragma comment(lib, "xinput9_1_0.lib")
PadRIO *PadRIO::activeInstance = NULL;
//
// Pending backtick/V view-toggle edges (set in Poll, consumed by the game's
// view-toggle block through BTPadViewToggleEdge).
//
int gBTPadViewToggleEdges = 0;
//
// The desktop per-MFD preset-page cycle edges (J/K/L -> Mfd1/2/3), consumed
// by L4MechControlsMapper::InterpretControls (btl4mppr.cpp step 3b -- the
// same seam the dev-build mech4 poll feeds). Defined in mech4.cpp (always
// compiled), so the glass TU externs it -- keyboard reconciliation
// 2026-07-20: J/K/L are the CONTROLS.MAP muscle-memory keys and there is no
// single pod button that "cycles" an MFD (the pod's bank buttons are
// mode-mask-gated direct selects), so the cycle stays a port-side sender
// with the authentic SetPresetMode body.
//
extern int gBTPresetCycle[3];
int
BTPadViewToggleEdge(void)
{
if (gBTPadViewToggleEdges > 0)
{
--gBTPadViewToggleEdges;
return 1;
}
return 0;
}
//
// XInput normalization: thumbs to -1..1 past the stock deadzone, triggers
// to 0..1 past the stock threshold.
//
static float
NormalizeThumb(int value, int dead_zone)
{
float sign = (value < 0) ? -1.0f : 1.0f;
float magnitude = (float)(value < 0 ? -value : value);
if (magnitude <= (float)dead_zone)
{
return 0.0f;
}
if (magnitude > 32767.0f)
{
magnitude = 32767.0f;
}
return sign * (magnitude - dead_zone) / (32767.0f - dead_zone);
}
static float
NormalizeTrigger(int value)
{
if (value <= XINPUT_GAMEPAD_TRIGGER_THRESHOLD)
{
return 0.0f;
}
return (float)(value - XINPUT_GAMEPAD_TRIGGER_THRESHOLD)
/ (float)(255 - XINPUT_GAMEPAD_TRIGGER_THRESHOLD);
}
//
// The keyboard is live only while a window of THIS process is foreground
// (the mech4.cpp focus-guard idiom) -- alt-tabbed developers must not
// drive the mech.
//
static int
ProcessHasFocus()
{
//
// BT_KEY_NOFOCUS=1: automation harnesses read keys without focus
// (the same override the btinput binding engine honors).
//
static int s_noFocus = -1;
if (s_noFocus < 0)
{
const char *value = getenv("BT_KEY_NOFOCUS");
s_noFocus = (value != 0 && *value == '1') ? 1 : 0;
}
if (s_noFocus)
{
return 1;
}
HWND foreground = GetForegroundWindow();
if (foreground == NULL)
{
return 0;
}
DWORD process_id = 0;
GetWindowThreadProcessId(foreground, &process_id);
return process_id == GetCurrentProcessId();
}
//###########################################################################
// Construction
//###########################################################################
PadRIO::PadRIO():
RIOBase(),
eventHead(0),
eventTail(0),
lastPollMilliseconds(0),
lastPadProbeMilliseconds(0),
padIndex(-1),
previousPadButtons(0)
{
memset(previousKeyHeld, 0, sizeof(previousKeyHeld));
memset(channelValue, 0, sizeof(channelValue));
memset(lampState, 0, sizeof(lampState));
bindings.Load();
BuildKeySuppression();
//
// Per-channel spring return rate = the fastest deflect rate bound to
// the channel (a channel with no deflect bindings never auto-centers).
//
for (int c = 0; c < PadBindingProfile::ChannelCount; ++c)
{
channelReturnRate[c] = 0.0f;
}
for (int k = 0; k < bindings.keyBindingCount; ++k)
{
const PadBindingProfile::Action &action = bindings.keyBindings[k].action;
if (action.kind == PadBindingProfile::ActionAxisDeflect)
{
float rate = action.rate < 0.0f ? -action.rate : action.rate;
if (rate > channelReturnRate[action.channel])
{
channelReturnRate[action.channel] = rate;
}
}
}
flipStickAxes =
(getenv("L4PADFLIP") != NULL && *getenv("L4PADFLIP") != '0');
//
// Never revision 0.0 -- some diagnostics print it; give the synthetic
// board a recognizable version.
//
MajorRevision = 9;
MinorRevision = 9;
activeInstance = this;
DEBUG_STREAM << "[padrio] PadRIO up (XInput probe pending; keyboard "
<< "live on focus; L4PADFLIP=" << flipStickAxes << ")\n" << std::flush;
//
// The on-screen cockpit button panel (step 2d) rides the device: the
// glass preset sets BT_PAD_PANEL=1.
//
if (getenv("BT_PAD_PANEL") != NULL && *getenv("BT_PAD_PANEL") != '0')
{
BTPadPanel_Create();
}
}
PadRIO::~PadRIO()
{
BTPadPanel_Destroy();
if (activeInstance == this)
{
activeInstance = NULL;
}
}
//###########################################################################
// Typed-channel suppression (the btinput sSuppressChar/sSuppressKeyUp
// pattern -- btinput.cpp AddSuppression -- rebuilt here because btinput
// stands down whenever a cockpit device owns the input path). A bound key
// must NOT also reach the 1995 in-cockpit keyboard dispatcher: 'w' selects
// pilot 0, 'a'/'s'/'d'/'f'/'g' flip MFD2 preset pages, letter/numpad
// KEY-UP VK values alias onto lowercase hotkeys (VK_F5==0x74=='t' = pilot
// select 3, VK_NUMPAD2==0x62=='b' = MFD3 Quad, ...). Unbound keys keep
// their authentic 1995 typed meaning.
//###########################################################################
void
PadRIO::AddKeySuppression(int virtual_key)
{
//
// WM_KEYUP delivers the raw VK; every consumer downstream compares
// typed CHARACTERS, so the VK value itself is the alias to swallow.
//
if (virtual_key >= 0 && virtual_key < 256)
{
suppressKeyUp[virtual_key] = 1;
}
//
// WM_CHAR delivers typed characters: both cases of a letter, the digit
// itself (main row AND numpad -- VK_NUMPAD0..9 type '0'..'9'), space,
// and the base punctuation of the Oem keys.
//
if (virtual_key >= 'A' && virtual_key <= 'Z')
{
suppressChar[virtual_key + ('a' - 'A')] = 1;
suppressChar[virtual_key] = 1;
}
else if (virtual_key >= '0' && virtual_key <= '9')
{
suppressChar[virtual_key] = 1;
}
else if (virtual_key >= VK_NUMPAD0 && virtual_key <= VK_NUMPAD9)
{
suppressChar['0' + (virtual_key - VK_NUMPAD0)] = 1;
}
else if (virtual_key == VK_SPACE)
{
suppressChar[' '] = 1;
suppressKeyUp[VK_SPACE] = 1;
}
else
{
static const struct { int vk; char ch; } oem[] =
{
{ VK_OEM_MINUS, '-' }, { VK_OEM_PLUS, '=' },
{ VK_OEM_COMMA, ',' }, { VK_OEM_PERIOD, '.' },
{ VK_OEM_2, '/' }, { VK_OEM_3, '`' },
{ VK_OEM_4, '[' }, { VK_OEM_5, '\\' },
{ VK_OEM_6, ']' }, { VK_OEM_1, ';' },
{ VK_OEM_7, '\'' }, { VK_RETURN, '\r' },
{ VK_TAB, '\t' }, { VK_BACK, '\b' },
};
for (int i = 0; i < (int)(sizeof(oem) / sizeof(oem[0])); ++i)
{
if (oem[i].vk == virtual_key)
{
suppressChar[(unsigned char)oem[i].ch] = 1;
}
}
}
//
// The generic modifier VK is what WM_KEYUP reports for L/R variants.
//
if (virtual_key == VK_LSHIFT || virtual_key == VK_RSHIFT)
{
suppressKeyUp[VK_SHIFT] = 1;
}
if (virtual_key == VK_LCONTROL || virtual_key == VK_RCONTROL)
{
suppressKeyUp[VK_CONTROL] = 1;
}
}
void
PadRIO::BuildKeySuppression()
{
memset(suppressChar, 0, sizeof(suppressChar));
memset(suppressKeyUp, 0, sizeof(suppressKeyUp));
for (int k = 0; k < bindings.keyBindingCount; ++k)
{
AddKeySuppression(bindings.keyBindings[k].virtualKey);
}
//
// The hardcoded keys (Poll): backtick + V = view toggle, J/K/L = the
// per-MFD preset-page cycle.
//
AddKeySuppression(VK_OEM_3);
AddKeySuppression('V');
AddKeySuppression('J');
AddKeySuppression('K');
AddKeySuppression('L');
}
int
PadRIO::SuppressKey(unsigned int key_value, int is_char)
{
if (activeInstance == NULL || key_value > 255)
{
return 0; // no glass device / ALT_BIT-tagged value
}
return is_char
? activeInstance->suppressChar[key_value]
: activeInstance->suppressKeyUp[key_value];
}
//###########################################################################
// Event queue
//###########################################################################
void
PadRIO::PushEvent(const RIOEvent &event)
{
int next = (eventHead + 1) % EventQueueSize;
if (next == eventTail)
{
DEBUG_STREAM << "[padrio] event queue overflow -- event dropped\n"
<< std::flush;
return;
}
eventQueue[eventHead] = event;
eventHead = next;
}
void
PadRIO::EmitButton(int address, int pressed)
{
RIOEvent event;
event.Type = pressed ? ButtonPressedEvent : ButtonReleasedEvent;
event.Data.Unit = address;
PushEvent(event);
}
void
PadRIO::EmitKeypad(int unit, int key)
{
RIOEvent event;
event.Type = KeyEvent;
event.Data.Keyboard.Unit = unit;
event.Data.Keyboard.Key = key;
PushEvent(event);
}
//###########################################################################
// The poll -- one pass per frame (time-gated so the manager's drain loop
// terminates; an AnalogEvent is emitted every pass to keep the manager's
// five-scalar push running, matching the serial board's analog cadence).
//###########################################################################
void
PadRIO::Poll()
{
unsigned long now = timeGetTime();
float dt = (lastPollMilliseconds == 0)
? 0.0f
: (float)(now - lastPollMilliseconds) * 0.001f;
if (dt > 0.1f)
{
dt = 0.1f; // resumed from a stall -- don't slam the integrators
}
lastPollMilliseconds = now;
//
//-----------------------------------------------------------------
// XInput: hot-plug probe every ~3 s, then read the connected pad.
//-----------------------------------------------------------------
//
XINPUT_STATE pad_state;
int pad_connected = 0;
if (padIndex >= 0)
{
if (XInputGetState(padIndex, &pad_state) == ERROR_SUCCESS)
{
pad_connected = 1;
}
else
{
DEBUG_STREAM << "[padrio] XInput pad " << padIndex
<< " disconnected\n" << std::flush;
padIndex = -1;
previousPadButtons = 0;
}
}
if (padIndex < 0 && (lastPadProbeMilliseconds == 0 ||
now - lastPadProbeMilliseconds >= 3000))
{
lastPadProbeMilliseconds = now;
for (int slot = 0; slot < 4; ++slot)
{
if (XInputGetState(slot, &pad_state) == ERROR_SUCCESS)
{
padIndex = slot;
pad_connected = 1;
DEBUG_STREAM << "[padrio] XInput pad found in slot "
<< slot << "\n" << std::flush;
break;
}
}
}
//
//-----------------------------------------------------------------
// Keyboard bindings: edges fire button/keypad events; held keys
// accumulate axis motion. All keys read as RELEASED without focus
// so held buttons let go when the developer alt-tabs.
//-----------------------------------------------------------------
//
int focused = ProcessHasFocus();
//
// The backtick view toggle (per Cyd: ` = 1st/3rd person in the glass
// cockpit) + V (the CONTROLS.MAP ViewToggle key -- keyboard
// reconciliation 2026-07-20). Edge-detected here (async poll,
// message-path-free) and consumed by the game's view-toggle block via
// BTPadViewToggleEdge.
//
{
static int s_backtickWas = 0;
int backtick_held = focused &&
(((GetAsyncKeyState(VK_OEM_3) & 0x8000) != 0) ||
((GetAsyncKeyState('V') & 0x8000) != 0));
if (backtick_held && !s_backtickWas)
{
extern int gBTPadViewToggleEdges;
++gBTPadViewToggleEdges;
}
s_backtickWas = backtick_held;
}
//
// J/K/L: cycle the Mfd1/Mfd2/Mfd3 preset page (the CONTROLS.MAP keys;
// the L4 mapper consumes gBTPresetCycle and runs the authentic
// SetPresetMode body -- btl4mppr.cpp CyclePresetModeNow).
//
{
static int s_presetWas[3] = { 0, 0, 0 };
static const int s_presetKey[3] = { 'J', 'K', 'L' };
for (int g = 0; g < 3; ++g)
{
int held = focused &&
(GetAsyncKeyState(s_presetKey[g]) & 0x8000) != 0;
if (held && !s_presetWas[g])
{
gBTPresetCycle[g] = 1;
}
s_presetWas[g] = held;
}
}
float slewDelta[PadBindingProfile::ChannelCount];
int deflectHeld[PadBindingProfile::ChannelCount];
int slewHeld[PadBindingProfile::ChannelCount];
memset(slewDelta, 0, sizeof(slewDelta));
memset(deflectHeld, 0, sizeof(deflectHeld));
memset(slewHeld, 0, sizeof(slewHeld));
for (int k = 0; k < bindings.keyBindingCount; ++k)
{
const PadBindingProfile::KeyBinding &binding = bindings.keyBindings[k];
int held = focused &&
(GetAsyncKeyState(binding.virtualKey) & 0x8000) != 0;
int was_held = previousKeyHeld[k];
previousKeyHeld[k] = (unsigned char)held;
switch (binding.action.kind)
{
case PadBindingProfile::ActionButton:
if (held != was_held)
{
EmitButton(binding.action.address, held);
}
break;
case PadBindingProfile::ActionKeypad:
if (held && !was_held)
{
EmitKeypad(binding.action.address, binding.action.key);
}
break;
case PadBindingProfile::ActionAxisDeflect:
if (held)
{
deflectHeld[binding.action.channel] = 1;
channelValue[binding.action.channel] +=
binding.action.rate * dt;
}
break;
case PadBindingProfile::ActionAxisSlew:
if (held)
{
slewHeld[binding.action.channel] = 1;
slewDelta[binding.action.channel] +=
binding.action.rate * dt;
}
break;
case PadBindingProfile::ActionAxisSet:
if (held && !was_held)
{
channelValue[binding.action.channel] = binding.action.rate;
}
break;
}
}
//
// Spring return: a deflect-managed channel with no deflect key held
// re-centers at its fastest bound rate.
//
for (int c = 0; c < PadBindingProfile::ChannelCount; ++c)
{
channelValue[c] += slewDelta[c];
if (!deflectHeld[c] && channelReturnRate[c] > 0.0f)
{
float step = channelReturnRate[c] * dt;
if (channelValue[c] > step)
{
channelValue[c] -= step;
}
else if (channelValue[c] < -step)
{
channelValue[c] += step;
}
else
{
channelValue[c] = 0.0f;
}
}
}
//
//-----------------------------------------------------------------
// Pad: button edges + axis writes (direct absolute past the
// deadzone; slew axes integrate).
//-----------------------------------------------------------------
//
if (pad_connected)
{
unsigned buttons = pad_state.Gamepad.wButtons;
for (int b = 0; b < bindings.padButtonBindingCount; ++b)
{
const PadBindingProfile::PadButtonBinding &binding =
bindings.padButtonBindings[b];
int held = (buttons & binding.buttonMask) != 0;
int was_held = (previousPadButtons & binding.buttonMask) != 0;
if (held == was_held)
{
continue;
}
if (binding.action.kind == PadBindingProfile::ActionButton)
{
EmitButton(binding.action.address, held);
}
else if (binding.action.kind == PadBindingProfile::ActionKeypad
&& held)
{
EmitKeypad(binding.action.address, binding.action.key);
}
}
previousPadButtons = buttons;
for (int a = 0; a < bindings.padAxisBindingCount; ++a)
{
const PadBindingProfile::PadAxisBinding &binding =
bindings.padAxisBindings[a];
float raw = 0.0f;
switch (binding.axis)
{
case PadBindingProfile::PadAxisLX:
raw = NormalizeThumb(pad_state.Gamepad.sThumbLX,
XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
break;
case PadBindingProfile::PadAxisLY:
raw = NormalizeThumb(pad_state.Gamepad.sThumbLY,
XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
break;
case PadBindingProfile::PadAxisRX:
raw = NormalizeThumb(pad_state.Gamepad.sThumbRX,
XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE);
break;
case PadBindingProfile::PadAxisRY:
raw = NormalizeThumb(pad_state.Gamepad.sThumbRY,
XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE);
break;
case PadBindingProfile::PadAxisLT:
raw = NormalizeTrigger(pad_state.Gamepad.bLeftTrigger);
break;
case PadBindingProfile::PadAxisRT:
raw = NormalizeTrigger(pad_state.Gamepad.bRightTrigger);
break;
}
if (binding.invert)
{
raw = -raw;
}
if (binding.slew)
{
if (raw != 0.0f)
{
slewHeld[binding.channel] = 1;
}
channelValue[binding.channel] += raw * binding.slewRate * dt;
}
else if (raw != 0.0f)
{
//
// Direct absolute: a deflected pad axis owns the channel;
// centered (inside the deadzone) it leaves the keyboard
// integration alone.
//
channelValue[binding.channel] = raw;
}
}
}
//
//-----------------------------------------------------------------
// GAIT DETENT (keyboard/pad accommodation, ported from the pod-build
// lever -- mech4.cpp "GAIT DETENT", 2026-07-20 glass regression): the
// gait SM has NO stable state for a demand between the walk cycle's
// cap (walkStrideLength @0x534) and the run engage speed
// (reverseSpeedMax @0x538) -- a lever PARKED in that band hunts
// walk <-> run forever, retriggering the authored EngineShiftFwd/Rev
// samples each swing. The pod's PHYSICAL throttle plausibly rested
// only at mechanical notches [T4]; reproduce the pod-build bridge's
// accommodation EXACTLY: when the throttle slew is AT REST (no slew
// key held, no pad slew axis deflected), snap the lever out of the
// dead band to the NEARER edge. Sweeping THROUGH the band while
// held stays continuous -- an authentic moving lever, firing the one
// authentic shift. The band arrives in lever units from the player
// mech via the mech4.cpp seam (hi <= lo = no band known yet).
//-----------------------------------------------------------------
//
{
extern float gBTGaitDetentLo, gBTGaitDetentHi; // mech4.cpp seam
const float band_lo = gBTGaitDetentLo;
const float band_hi = gBTGaitDetentHi;
float &lever = channelValue[PadBindingProfile::ChannelThrottle];
if (!slewHeld[PadBindingProfile::ChannelThrottle]
&& band_hi > band_lo
&& lever > band_lo && lever < band_hi)
{
const float snapped =
(lever - band_lo < (band_hi - band_lo) * 0.5f) ? band_lo : band_hi;
{ static int s_dLog = 0; if (getenv("BT_GAIT_TRACE") && s_dLog++ < 40)
DEBUG_STREAM << "[gaitdetent] pad lever " << lever
<< " in dead band [" << band_lo << "," << band_hi
<< ") -> " << snapped << "\n" << std::flush; }
lever = snapped;
}
}
//
//-----------------------------------------------------------------
// Clamp and publish the control surface. Throttle is the 0..1
// lever the mapper detents at 1.0; the rest are -1..1.
//-----------------------------------------------------------------
//
for (int c = 0; c < PadBindingProfile::ChannelCount; ++c)
{
float low = (c == PadBindingProfile::ChannelThrottle) ? 0.0f : -1.0f;
if (channelValue[c] < low) channelValue[c] = low;
if (channelValue[c] > 1.0f) channelValue[c] = 1.0f;
}
//
// PORT SIGN (user-reported inversion, closed with live sign algebra
// 2026-07-18): the mapper interprets the WIRE convention -- stick
// right = NEGATIVE JoystickX (the real RIO hardware / vRIO calibration
// convention; the keyboard bridge compensates by negating once,
// cb82d8c). Measured: wire stickX=-1 -> turnDemand=-1 = the same
// demand the user-verified D-key-RIGHT produces -- so screen-sign
// publish was inverted. X therefore publishes NEGATED; Y stays
// screen-sign. L4PADFLIP still flips both on top.
//
float stick_sign = flipStickAxes ? -1.0f : 1.0f;
Throttle = (Scalar)channelValue[PadBindingProfile::ChannelThrottle];
JoystickX = (Scalar)(stick_sign *
-channelValue[PadBindingProfile::ChannelJoystickX]);
JoystickY = (Scalar)(stick_sign *
channelValue[PadBindingProfile::ChannelJoystickY]);
LeftPedal = (Scalar)channelValue[PadBindingProfile::ChannelLeftPedal];
RightPedal = (Scalar)channelValue[PadBindingProfile::ChannelRightPedal];
//
// The analog heartbeat: tells the manager to run the five-scalar
// push this frame (LBE4ControlsManager::Execute gates the push on
// new_RIO_values).
//
RIOEvent analog;
analog.Type = AnalogEvent;
analog.Data.Unit = 0;
PushEvent(analog);
}
//###########################################################################
// RIOBase surface
//###########################################################################
Logical
PadRIO::GetNextEvent(RIOEvent *destinationPointer)
{
Check_Pointer(destinationPointer);
if (eventTail == eventHead)
{
//
// Queue drained: poll at most once per millisecond tick so the
// manager's per-frame drain loop terminates (the poll always
// enqueues the analog heartbeat).
//
unsigned long now = timeGetTime();
if (now == lastPollMilliseconds)
{
return False;
}
Poll();
}
if (eventTail == eventHead)
{
return False;
}
*destinationPointer = eventQueue[eventTail];
eventTail = (eventTail + 1) % EventQueueSize;
return True;
}
void
PadRIO::SetLamp(int lampNumber, int state)
{
if (lampNumber >= 0 && lampNumber < LampCount)
{
lampState[lampNumber] = state;
}
}
//###########################################################################
// The on-screen panel entries
//###########################################################################
Logical
PadRIO::IsActive()
{
return activeInstance != NULL;
}
void
PadRIO::SetScreenButton(int unit, int pressed)
{
if (activeInstance == NULL)
{
return;
}
//
// The keypad address space (0x50-0x6F, the vRIO panel's two 4x4 hex
// keypads): press emits a keypad KeyEvent -- internal (0x50) on the
// pilot unit, external (0x60) on the operator unit; keys have no
// release event.
//
if (unit >= 0x50 && unit <= 0x6F)
{
if (pressed)
{
activeInstance->EmitKeypad((unit >= 0x60) ? 1 : 0, unit & 0x0F);
}
return;
}
if (unit < 0 || unit >= LBE4ControlsManager::ButtonCount)
{
return;
}
activeInstance->EmitButton(unit, pressed);
}
int
PadRIO::GetLampState(int unit)
{
if (activeInstance == NULL || unit < 0 || unit >= LampCount)
{
return 0;
}
return activeInstance->lampState[unit];
}