Reverse thrust was dead on the desktop (user report): wire the 0x3F throttle-handle button
LALT -> button 0x3F was bound in both binding layers and PadRIO pushed the RIO ButtonPressed event correctly, but nothing ever set the mapper's ReverseThrust: - the authentic route is BTL4.RES 'L4' streamed record [2] (Button Throttle1 0x3F -> attr 6 ReverseThrust@0x124), but our streamed BUTTON mappings are not consumed at all -- MechControlsMapper::AddOrErase is still the unreconstructed Fail stub, so the event never reaches the attribute; - the ONLY writer of reverseThrust was the keyboard bridge's 'key_throttle < 0' test, which is bypassed whenever a RIO is operational (the pad RIO always is, so the bridge is OFF on the desktop) AND is unreachable regardless, because L4PADRIO clamps the Throttle channel to [0,1]. Net: Alt did nothing and the flag was re-zeroed every frame. FIX: publish the 0x3F hold state from PadRIO::EmitButton -- the one chokepoint every desktop source funnels through (keyboard, gamepad, DirectInput joystick, glass-panel clicks via SetScreenButton) -- and apply it in InterpretControls gated on BTPadRIOActive() so real pod hardware is untouched (there the streamed mapping owns the flag). Marked [T3]; delete once streamed button mappings land. Reverse is a HOLD (manual: hold the red throttle button, release = forward). Verified with scratchpad/revtest.py (keybd_event injection, BT_KEY_NOFOCUS): forward = rev=0 dmd +61.501; LALT held = rev=1 dmd -61.501; release edge logged. KB: pod-hardware.md now records the streamed-button-mapping gap + this seam. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
This commit is contained in:
co-authored by
Claude Opus 5
parent
6c3fca2f67
commit
23229e573e
@@ -118,6 +118,20 @@ The authentic pod throttle path — traced 2026-07 (task #50 throttle-fidelity q
|
||||
binary's 3) — every streamed record landed one member late (a real-pod landmine: the RIO
|
||||
throttle scalar drove `pedalsPosition`). Ids are now PINNED to the binary numbering with an
|
||||
id-2 pad + `static_assert` locks (`mechmppr.hpp/.cpp`); audit tool `BT_CTRLMAP_LOG=1`.
|
||||
**⚠ The streamed BUTTON mappings (like record [2]) are NOT CONSUMED yet** — the SCALAR records
|
||||
drive their attributes, but `MechControlsMapper::AddOrErase` (both overloads, @004b02b0/
|
||||
@004b02d4) is still the unreconstructed `Fail("Unhandled mapping!")` stub, so a streamed
|
||||
button never reaches its attribute. Consequence found the hard way (user report 2026-07-24,
|
||||
"i cant seem to reverse"): **REVERSE THRUST was dead on the desktop.** LALT→0x3F was bound and
|
||||
PadRIO pushed the RIO ButtonPressed event correctly, but nothing applied it to attr 6, and the
|
||||
only writer of `reverseThrust` was the keyboard bridge's `key_throttle < 0` test — which is
|
||||
(a) bypassed whenever a RIO is operational (the pad RIO always is, so the bridge is OFF on the
|
||||
desktop) and (b) unreachable anyway, since `L4PADRIO` clamps the Throttle channel to **[0,1]**
|
||||
(`low = 0.0f`). Fixed by publishing the 0x3F hold state from `PadRIO::EmitButton` (the single
|
||||
chokepoint keyboard/pad/joystick/glass clicks all share) and applying it in
|
||||
`InterpretControls`, **gated on `BTPadRIOActive()`** so pod hardware is untouched — marked
|
||||
[T3], delete when streamed button mappings land. Verified: hold LALT ⇒ `rev=1`,
|
||||
`speedDemand −61.5`; release ⇒ forward. Reverse is a **HOLD**, per the manual. [T2]
|
||||
Details: `docs/GLASS_COCKPIT.md` §2a; see [[glass-cockpit]].
|
||||
4. **Interpretation:** `L4MechControlsMapper::InterpretControls` @004d196c applies the ONLY software
|
||||
detent — snap to 1.0 when |t−1.0| ≤ 0.05 — then `MechControlsMapper::InterpretControls` @004afd10
|
||||
|
||||
@@ -28,6 +28,14 @@ PadRIO *PadRIO::activeInstance = NULL;
|
||||
//
|
||||
int gBTPadViewToggleEdges = 0;
|
||||
|
||||
//
|
||||
// REVERSE THRUST hold state (the pod's throttle-handle button, unit 0x3F --
|
||||
// bound to LALT on the keyboard). Set in EmitButton, the single chokepoint all
|
||||
// input sources share; consumed by MechControlsMapper::InterpretControls's
|
||||
// desktop bridge, which owns `reverseThrust` (mapper attr 6 @0x124) every frame.
|
||||
//
|
||||
int gBTReverseHeld = 0;
|
||||
|
||||
//
|
||||
// The desktop per-MFD preset-page cycle edges (J/K/L -> Mfd1/2/3), consumed
|
||||
// by L4MechControlsMapper::InterpretControls (btl4mppr.cpp step 3b -- the
|
||||
@@ -332,6 +340,24 @@ void
|
||||
void
|
||||
PadRIO::EmitButton(int address, int pressed)
|
||||
{
|
||||
//
|
||||
// REVERSE THRUST (the red button on the pod's throttle HANDLE, unit 0x3F).
|
||||
// BTL4.RES's "L4" streamed record [2] maps `Button Throttle1 (0x3F)` to the
|
||||
// mapper's attr 6 `ReverseThrust@0x124`, and the 1995 manual is explicit that
|
||||
// you HOLD it (release = forward) -- see context/pod-hardware.md. Publish the
|
||||
// hold state here, at the ONE chokepoint every source funnels through
|
||||
// (keyboard bindings, gamepad, DirectInput joystick, and glass-panel clicks
|
||||
// via SetScreenButton), so the desktop bridge can honour the button exactly
|
||||
// like the pod's RIO board did.
|
||||
//
|
||||
if (address == 0x3F)
|
||||
{
|
||||
gBTReverseHeld = pressed ? 1 : 0;
|
||||
if (getenv("BT_PAD_LOG"))
|
||||
DEBUG_STREAM << "[padwrite] reverse button 0x3F "
|
||||
<< (pressed ? "HELD" : "released") << "\n" << std::flush;
|
||||
}
|
||||
|
||||
RIOEvent event;
|
||||
event.Type = pressed ? ButtonPressedEvent : ButtonReleasedEvent;
|
||||
event.Data.Unit = address;
|
||||
@@ -1053,6 +1079,16 @@ void
|
||||
activeInstance->EmitButton(unit, pressed);
|
||||
}
|
||||
|
||||
//
|
||||
// Is the desktop PAD RIO the live input device? (On real pod hardware the RIO
|
||||
// is the serial Ranger board and this is 0, so pod behaviour is never altered by
|
||||
// the desktop-only seams that consult it.)
|
||||
//
|
||||
int BTPadRIOActive(void)
|
||||
{
|
||||
return PadRIO::HasActiveInstance();
|
||||
}
|
||||
|
||||
int
|
||||
PadRIO::GetLampState(int unit)
|
||||
{
|
||||
|
||||
@@ -158,4 +158,12 @@ protected:
|
||||
|
||||
static PadRIO
|
||||
*activeInstance;
|
||||
|
||||
public:
|
||||
//
|
||||
// Is the desktop pad RIO the live input device? (0 on real pod hardware,
|
||||
// whose RIO is the serial Ranger board -- see BTPadRIOActive.)
|
||||
//
|
||||
static int
|
||||
HasActiveInstance() { return activeInstance != 0; }
|
||||
};
|
||||
|
||||
@@ -763,7 +763,29 @@ void
|
||||
}
|
||||
}
|
||||
throttlePosition = (key_throttle >= 0.0f) ? key_throttle : -key_throttle;
|
||||
reverseThrust = (key_throttle < 0.0f) ? 1 : 0;
|
||||
//
|
||||
// REVERSE THRUST -- the pod's red throttle-HANDLE button (RIO unit
|
||||
// 0x3F, LALT on the keyboard), NOT a lever sweep through zero. The
|
||||
// authentic wiring is BTL4.RES's "L4" streamed record [2]:
|
||||
// `Button Throttle1 (0x3F) -> attr 6 ReverseThrust@0x124`, and the
|
||||
// 1995 manual says HOLD it (release = forward) -- see
|
||||
// context/pod-hardware.md.
|
||||
//
|
||||
// This line used to read ONLY the negative-throttle case, which is
|
||||
// unreachable on the desktop: L4PADRIO clamps the Throttle channel to
|
||||
// [0,1] (see its "low = 0.0f" clamp), so key_throttle never goes
|
||||
// negative -- and because the bridge owns reverseThrust EVERY frame,
|
||||
// it also zeroed the flag right back out even when the streamed
|
||||
// button mapping had set it. Net effect: Alt did nothing.
|
||||
// (User-reported 2026-07-24, "i cant seem to reverse".)
|
||||
//
|
||||
// The negative-throttle term stays for the headless harnesses, which
|
||||
// drive forcedThrottle directly and CAN go negative.
|
||||
//
|
||||
{
|
||||
extern int gBTReverseHeld; // L4PADRIO::EmitButton (any 0x3F source)
|
||||
reverseThrust = (gBTReverseHeld || key_throttle < 0.0f) ? 1 : 0;
|
||||
}
|
||||
// (task #68) look-behind: hold 'V' = the pod's rear-view button.
|
||||
// (The other look buttons stay unbound on the keyboard rig.)
|
||||
{
|
||||
@@ -867,6 +889,35 @@ void
|
||||
<< mech->forwardThrottleScale << " -- healed to 1.0" << "\n" << std::flush;
|
||||
mech->forwardThrottleScale = 1.0f;
|
||||
}
|
||||
//
|
||||
// REVERSE THRUST on the DESKTOP. The authentic wiring is BTL4.RES's "L4"
|
||||
// streamed record [2]: `Button Throttle1 (0x3F, the red button on the throttle
|
||||
// handle) -> attr 6 ReverseThrust@0x124`, held for reverse / released for
|
||||
// forward (context/pod-hardware.md, manual-verified). PadRIO publishes that
|
||||
// button's hold state from EmitButton -- the one chokepoint every desktop
|
||||
// source funnels through (keyboard LALT, gamepad, DirectInput joystick, glass
|
||||
// panel click) -- but our streamed BUTTON mappings are not consumed yet
|
||||
// (MechControlsMapper::AddOrErase is still the unreconstructed Fail stub), so
|
||||
// the event never reaches attr 6 on its own. Apply it here, gated on the pad
|
||||
// RIO being the live device so REAL POD hardware is untouched (there
|
||||
// BTPadRIOActive() is 0 and the streamed mapping owns the flag).
|
||||
//
|
||||
// This is why LALT did nothing: the only writer was the keyboard bridge's
|
||||
// `key_throttle < 0` test, which (a) is bypassed entirely whenever a RIO is
|
||||
// operational -- which the pad RIO always is -- and (b) can never fire anyway,
|
||||
// because L4PADRIO clamps the Throttle channel to [0,1].
|
||||
// [T3 accommodation, marked: delete this block once streamed button mappings
|
||||
// land, at which point record [2] drives attr 6 by itself.]
|
||||
//
|
||||
{
|
||||
extern int BTPadRIOActive(void);
|
||||
extern int gBTReverseHeld;
|
||||
if (BTPadRIOActive())
|
||||
{
|
||||
reverseThrust = gBTReverseHeld ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (reverseThrust < 1)
|
||||
{
|
||||
speedDemand =
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Reverse-thrust verification: hold W to build throttle, then hold LALT and
|
||||
confirm the mapper flips to reverse (speedDemand goes negative).
|
||||
|
||||
Injects keys with keybd_event into a btl4 launched with BT_KEY_NOFOCUS=1, so it
|
||||
does not need window focus. Kills ONLY the PID it spawned.
|
||||
"""
|
||||
import ctypes
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
VK_W = 0x57
|
||||
VK_LMENU = 0xA4 # left Alt
|
||||
KEYEVENTF_KEYUP = 0x0002
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
|
||||
def hold(vk, seconds):
|
||||
user32.keybd_event(vk, 0, 0, 0)
|
||||
time.sleep(seconds)
|
||||
|
||||
def release(vk):
|
||||
user32.keybd_event(vk, 0, KEYEVENTF_KEYUP, 0)
|
||||
|
||||
CONTENT = r"C:\git\bt411\content"
|
||||
EXE = r"C:\git\bt411\build\Release\btl4.exe"
|
||||
LOG = r"C:\git\bt411\scratchpad_rev.log"
|
||||
|
||||
if os.path.exists(LOG):
|
||||
os.remove(LOG)
|
||||
|
||||
env = dict(os.environ)
|
||||
env.update({
|
||||
"BT_START_INSIDE": "1",
|
||||
"BT_KEY_NOFOCUS": "1",
|
||||
"BT_MPPR_TRACE": "1", # [mppr-c] thr= rev= -> dmd=
|
||||
"BT_PAD_LOG": "1", # [padwrite] reverse button
|
||||
"BT_LOG": LOG,
|
||||
})
|
||||
proc = subprocess.Popen([EXE, "-egg", "LAST.EGG"], cwd=CONTENT, env=env)
|
||||
print("spawned pid", proc.pid)
|
||||
|
||||
try:
|
||||
# wait for the mission to be running (the trace only prints in-mission)
|
||||
deadline = time.time() + 180
|
||||
while time.time() < deadline:
|
||||
if os.path.exists(LOG):
|
||||
txt = open(LOG, errors="replace").read()
|
||||
if "[mppr-c]" in txt:
|
||||
break
|
||||
time.sleep(2)
|
||||
else:
|
||||
print("FAIL: never reached the mapper trace")
|
||||
sys.exit(1)
|
||||
|
||||
# 1) throttle up with W for 3s, no Alt -> expect rev=0, dmd > 0
|
||||
hold(VK_W, 6.0)
|
||||
release(VK_W)
|
||||
time.sleep(1.5)
|
||||
mark = len(open(LOG, errors="replace").read())
|
||||
|
||||
# 2) now hold LALT for 4s -> expect rev=1 and dmd < 0
|
||||
hold(VK_LMENU, 5.0)
|
||||
release(VK_LMENU)
|
||||
time.sleep(1.5)
|
||||
|
||||
txt = open(LOG, errors="replace").read()
|
||||
before = txt[:mark]
|
||||
during = txt[mark:]
|
||||
|
||||
def rows(chunk):
|
||||
return re.findall(r"\[mppr-c\] thr=([-\d.]+) rev=(\d+).*?dmd=([-\d.]+)", chunk)
|
||||
|
||||
b = rows(before)
|
||||
d = rows(during)
|
||||
print("\n--- forward phase (last 3 samples):", b[-3:])
|
||||
print("--- alt-held phase (samples): ", d[:6])
|
||||
print("--- [padwrite] lines:", re.findall(r"\[padwrite\] reverse button.*", txt)[:4])
|
||||
|
||||
fwd_ok = any(int(r[1]) == 0 and float(r[2]) > 0.5 for r in b)
|
||||
rev_ok = any(int(r[1]) == 1 and float(r[2]) < -0.5 for r in d)
|
||||
print("\nforward (rev=0, dmd>0): %s" % ("PASS" if fwd_ok else "FAIL"))
|
||||
print("reverse (rev=1, dmd<0): %s" % ("PASS" if rev_ok else "FAIL"))
|
||||
finally:
|
||||
proc.terminate()
|
||||
print("killed", proc.pid)
|
||||
Reference in New Issue
Block a user