Verify HID feeder end-to-end; add smoke-test tool; update docs

The RioGamepad driver now installs and enumerates, so the user-mode
feeder path is verifiable. Add tools/RioJoySmokeTest, a standalone
on-cabinet utility that drives the real HidFeederJoystickSink (open the
device, submit reports via IOCTL_RIO_SUBMIT_REPORT) and reads the gamepad
back through winmm joyGetPosEx, asserting axes (min/mid/max), buttons,
and the POV hat all surface to the OS. Verified: all checks pass against
the installed driver.

Update docs to match reality (PLAN.md predated the HidFeederJoystickSink
commit): Phase 1 is now test-signed/installed/verified with the VHF
LowerFilters requirement noted; the stale "NullJoystickSink placeholder"
remainders in Phases 3 and 5 are corrected to reflect the wired,
verified feeder. driver/README.md notes the end-to-end verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-06-26 23:56:16 -05:00
co-authored by Claude Opus 4.8
parent 38ffb27458
commit 2deeb374ae
5 changed files with 197 additions and 15 deletions
+133
View File
@@ -0,0 +1,133 @@
using System.Runtime.InteropServices;
using RioJoy.Core.Calibration;
using RioJoy.Core.Mapping;
using RioJoy.Core.Output;
// On-cabinet smoke test: drive the real HidFeederJoystickSink (which opens the
// RioGamepad driver and submits reports via IOCTL_RIO_SUBMIT_REPORT) and read
// the virtual gamepad back through winmm joyGetPosEx, confirming fed values
// actually surface to the OS. Run after installing the driver:
// dotnet run --project tools/RioJoySmokeTest -c Release
// Exit code: 0 = all passed, 1 = a check failed, 2 = driver/joystick not found.
const uint JOY_RETURNALL = 0x000000FF;
int failures = 0;
string lastName = "";
void Check(string what, bool ok)
{
Console.WriteLine($" [{(ok ? "PASS" : "FAIL")}] {what}");
if (!ok) failures++;
}
Console.WriteLine("== RioJoy feeder -> driver smoke test ==");
if (!HidFeederJoystickSink.TryCreate(out var sink) || sink is null)
{
Console.WriteLine(" [FAIL] HidFeederJoystickSink.TryCreate returned false (driver not present?).");
return 2;
}
using (sink)
{
Console.WriteLine(" [PASS] Opened RioGamepad device interface.");
int joyId = FindJoyId();
if (joyId < 0)
{
Console.WriteLine(" [FAIL] No winmm joystick found to read back from.");
return 2;
}
Console.WriteLine($" Reading back via winmm joystick id {joyId} (\"{lastName}\").");
// 1. X axis: min / mid / max (RioHidReport logical max = 32767 -> joyGetPosEx ~0..65535)
sink.SetAxis(JoyAxis.X, 0); var lo = Read(joyId);
sink.SetAxis(JoyAxis.X, 16384); var mid = Read(joyId);
sink.SetAxis(JoyAxis.X, 32767); var hi = Read(joyId);
Console.WriteLine($" X(min)={lo.X} X(mid)={mid.X} X(max)={hi.X}");
Check("X axis tracks min<mid<max", lo.X < mid.X && mid.X < hi.X);
Check("X(min) near 0", lo.X < 4000);
Check("X(max) near 65535", hi.X > 61000);
// reset X to centre so it doesn't confuse later reads
sink.SetAxis(JoyAxis.X, 16384);
// 2. Z axis independent move
sink.SetAxis(JoyAxis.Z, 32767); var z = Read(joyId);
Console.WriteLine($" Z(max)={z.Z}");
Check("Z axis reaches max", z.Z > 61000);
sink.SetAxis(JoyAxis.Z, 16384);
// 3. Buttons: 1 and 5
sink.SetButton(1, true); var b1 = Read(joyId);
Check("button 1 down -> bit0 set", (b1.Buttons & 0x1) != 0);
sink.SetButton(5, true); var b5 = Read(joyId);
Check("button 5 down -> bit4 set", (b5.Buttons & 0x10) != 0);
sink.SetButton(1, false);
sink.SetButton(5, false); var b0 = Read(joyId);
Check("buttons released -> none set (low 8)", (b0.Buttons & 0xFF) == 0);
// 4. Hat / POV
sink.SetHat(RioHat.Up); var hUp = Read(joyId);
Check("hat Up -> POV 0", hUp.Pov == 0);
sink.SetHat(RioHat.Right); var hR = Read(joyId);
Check("hat Right -> POV 9000", hR.Pov == 9000);
sink.SetHat(RioHat.Centered); var hC = Read(joyId);
Check("hat Centered -> POV centered (0xFFFF)", hC.Pov == 0xFFFF);
}
Console.WriteLine(failures == 0
? "== ALL CHECKS PASSED =="
: $"== {failures} CHECK(S) FAILED ==");
return failures == 0 ? 0 : 1;
// ---- winmm read-back helpers ------------------------------------------------
static (uint X, uint Y, uint Z, uint Buttons, uint Pov) Read(int id)
{
Thread.Sleep(120); // let the report propagate through the HID stack
var info = new JOYINFOEX { dwSize = (uint)Marshal.SizeOf<JOYINFOEX>(), dwFlags = JOY_RETURNALL };
uint rc = joyGetPosEx(id, ref info);
if (rc != 0) throw new InvalidOperationException($"joyGetPosEx failed: {rc}");
return (info.dwXpos, info.dwYpos, info.dwZpos, info.dwButtons, info.dwPOV);
}
int FindJoyId()
{
int n = (int)joyGetNumDevs();
int firstPresent = -1;
for (int id = 0; id < n; id++)
{
var caps = new JOYCAPS();
if (joyGetDevCapsW(id, ref caps, (uint)Marshal.SizeOf<JOYCAPS>()) != 0) continue;
var info = new JOYINFOEX { dwSize = (uint)Marshal.SizeOf<JOYINFOEX>(), dwFlags = JOY_RETURNALL };
if (joyGetPosEx(id, ref info) != 0) continue; // not present
string name = caps.szPname ?? "";
if (firstPresent < 0) { firstPresent = id; lastName = name; }
// RioGamepad identity (Public.h: RIO_VENDOR_ID 0x1209 / RIO_PRODUCT_ID 0x5249).
if (caps.wMid == 0x1209 && caps.wPid == 0x5249) { lastName = name; return id; }
if (name.Contains("RIOJoy", StringComparison.OrdinalIgnoreCase)) { lastName = name; return id; }
}
return firstPresent;
}
[DllImport("winmm.dll")] static extern uint joyGetNumDevs();
[DllImport("winmm.dll")] static extern uint joyGetPosEx(int uJoyID, ref JOYINFOEX pji);
[DllImport("winmm.dll", CharSet = CharSet.Unicode)] static extern uint joyGetDevCapsW(int uJoyID, ref JOYCAPS pjc, uint cbjc);
[StructLayout(LayoutKind.Sequential)]
struct JOYINFOEX
{
public uint dwSize, dwFlags, dwXpos, dwYpos, dwZpos, dwRpos, dwUpos, dwVpos,
dwButtons, dwButtonNumber, dwPOV, dwReserved1, dwReserved2;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct JOYCAPS
{
public ushort wMid, wPid;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string szPname;
public uint wXmin, wXmax, wYmin, wYmax, wZmin, wZmax, wNumButtons, wPeriodMin, wPeriodMax,
wRmin, wRmax, wUmin, wUmax, wVmin, wVmax, wCaps, wMaxAxes, wNumAxes, wMaxButtons;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string szRegKey;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szOEMVxD;
}
+20
View File
@@ -0,0 +1,20 @@
# RioJoySmokeTest
On-cabinet verification that the **virtual gamepad path works end to end**: it
drives the real [`HidFeederJoystickSink`](../../src/RioJoy.Core/Output/HidFeederJoystickSink.cs)
(open the RioGamepad device → submit reports via `IOCTL_RIO_SUBMIT_REPORT`) and
reads the gamepad back through `winmm joyGetPosEx`, the same data `joy.cpl` and a
game would see. It checks the six axes (min/mid/max), buttons, and the POV hat.
This is **not** part of `RioJoy.sln` and not a unit test — it needs the signed
driver actually installed (see [`driver/README.md`](../../driver/README.md)), so
it only passes on a machine where the RioGamepad device is present.
## Run
```cmd
dotnet run --project tools/RioJoySmokeTest -c Release
```
Exit codes: `0` all checks passed · `1` a check failed · `2` driver or joystick
not found (driver not installed, or device not started — check Device Manager).
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Platforms>x64</Platforms>
<PlatformTarget>x64</PlatformTarget>
<AssemblyName>RioJoySmokeTest</AssemblyName>
<!-- Standalone on-cabinet verification tool; not part of RioJoy.sln. -->
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\RioJoy.Core\RioJoy.Core.csproj" />
</ItemGroup>
</Project>