Add Windows XInput feasibility test
This commit is contained in:
parent
604450375d
commit
a7fb6c5694
1 changed files with 358 additions and 0 deletions
358
tools/Test-AdapterFeasibility.ps1
Normal file
358
tools/Test-AdapterFeasibility.ps1
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateRange(5, 120)]
|
||||
[int]$DetectionTimeoutSeconds = 30,
|
||||
|
||||
[ValidateRange(1, 4)]
|
||||
[int]$ExpectedActiveControllers = 1,
|
||||
|
||||
[ValidateRange(2, 30)]
|
||||
[int]$InputTestSeconds = 6,
|
||||
|
||||
[ValidateRange(100, 5000)]
|
||||
[int]$RumbleMilliseconds = 600,
|
||||
|
||||
[switch]$SkipInteractive,
|
||||
|
||||
[string]$OutputPath = (Join-Path ([System.IO.Path]::GetTempPath()) "switch-pico-adapter-feasibility-results.json")
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if ($PSVersionTable.PSEdition -eq "Core" -and -not $IsWindows) {
|
||||
throw "This test must run on Windows."
|
||||
}
|
||||
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class SwitchPicoXInput {
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Gamepad {
|
||||
public ushort Buttons;
|
||||
public byte LeftTrigger;
|
||||
public byte RightTrigger;
|
||||
public short LeftThumbX;
|
||||
public short LeftThumbY;
|
||||
public short RightThumbX;
|
||||
public short RightThumbY;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct State {
|
||||
public uint PacketNumber;
|
||||
public Gamepad Gamepad;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Vibration {
|
||||
public ushort LeftMotorSpeed;
|
||||
public ushort RightMotorSpeed;
|
||||
}
|
||||
|
||||
[DllImport("xinput1_4.dll", EntryPoint = "XInputGetState")]
|
||||
public static extern uint GetState(uint userIndex, out State state);
|
||||
|
||||
[DllImport("xinput1_4.dll", EntryPoint = "XInputSetState")]
|
||||
public static extern uint SetState(uint userIndex, ref Vibration vibration);
|
||||
}
|
||||
"@
|
||||
|
||||
$script:Results = [System.Collections.Generic.List[object]]::new()
|
||||
|
||||
function Add-Result {
|
||||
param(
|
||||
[string]$Name,
|
||||
[bool]$Passed,
|
||||
[string]$Detail,
|
||||
[bool]$Required = $true
|
||||
)
|
||||
|
||||
$script:Results.Add([pscustomobject]@{
|
||||
Name = $Name
|
||||
Passed = $Passed
|
||||
Required = $Required
|
||||
Detail = $Detail
|
||||
})
|
||||
|
||||
$color = if ($Passed) { "Green" } elseif ($Required) { "Red" } else { "Yellow" }
|
||||
$label = if ($Passed) { "PASS" } elseif ($Required) { "FAIL" } else { "SKIP" }
|
||||
Write-Host ("[{0}] {1}: {2}" -f $label, $Name, $Detail) -ForegroundColor $color
|
||||
}
|
||||
|
||||
function Get-UsbIdentityDevices {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Vid,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Pid
|
||||
)
|
||||
|
||||
$pattern = "VID_{0}&PID_{1}" -f $Vid.ToUpperInvariant(), $Pid.ToUpperInvariant()
|
||||
if (Get-Command Get-PnpDevice -ErrorAction SilentlyContinue) {
|
||||
return @(Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.InstanceId -like "*$pattern*" })
|
||||
}
|
||||
|
||||
return @(Get-CimInstance Win32_PnPEntity -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.PNPDeviceID -like "*$pattern*" })
|
||||
}
|
||||
|
||||
function Get-XInputState {
|
||||
param([ValidateRange(0, 3)][int]$Index)
|
||||
|
||||
$state = [SwitchPicoXInput+State]::new()
|
||||
$status = [SwitchPicoXInput]::GetState([uint32]$Index, [ref]$state)
|
||||
return [pscustomobject]@{
|
||||
Index = $Index
|
||||
Connected = ($status -eq 0)
|
||||
Status = $status
|
||||
PacketNumber = $state.PacketNumber
|
||||
Buttons = $state.Gamepad.Buttons
|
||||
LeftTrigger = $state.Gamepad.LeftTrigger
|
||||
RightTrigger = $state.Gamepad.RightTrigger
|
||||
LeftThumbX = $state.Gamepad.LeftThumbX
|
||||
LeftThumbY = $state.Gamepad.LeftThumbY
|
||||
RightThumbX = $state.Gamepad.RightThumbX
|
||||
RightThumbY = $state.Gamepad.RightThumbY
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ConnectedXInputStates {
|
||||
return @(0..3 | ForEach-Object { Get-XInputState -Index $_ } |
|
||||
Where-Object { $_.Connected })
|
||||
}
|
||||
|
||||
function Test-StateChanged {
|
||||
param($Before, $After)
|
||||
|
||||
return $Before.PacketNumber -ne $After.PacketNumber -or
|
||||
$Before.Buttons -ne $After.Buttons -or
|
||||
$Before.LeftTrigger -ne $After.LeftTrigger -or
|
||||
$Before.RightTrigger -ne $After.RightTrigger -or
|
||||
$Before.LeftThumbX -ne $After.LeftThumbX -or
|
||||
$Before.LeftThumbY -ne $After.LeftThumbY -or
|
||||
$Before.RightThumbX -ne $After.RightThumbX -or
|
||||
$Before.RightThumbY -ne $After.RightThumbY
|
||||
}
|
||||
|
||||
function Set-XInputRumble {
|
||||
param(
|
||||
[ValidateRange(0, 3)][int]$Index,
|
||||
[ValidateRange(0, 65535)][int]$Left,
|
||||
[ValidateRange(0, 65535)][int]$Right
|
||||
)
|
||||
|
||||
$vibration = [SwitchPicoXInput+Vibration]::new()
|
||||
$vibration.LeftMotorSpeed = [uint16]$Left
|
||||
$vibration.RightMotorSpeed = [uint16]$Right
|
||||
return [SwitchPicoXInput]::SetState([uint32]$Index, [ref]$vibration)
|
||||
}
|
||||
|
||||
function Read-YesNo {
|
||||
param([string]$Prompt)
|
||||
|
||||
while ($true) {
|
||||
$answer = (Read-Host "$Prompt [y/n]").Trim().ToLowerInvariant()
|
||||
if ($answer -eq "y" -or $answer -eq "yes") { return $true }
|
||||
if ($answer -eq "n" -or $answer -eq "no") { return $false }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Switch Pico Windows XInput feasibility test" -ForegroundColor Cyan
|
||||
Write-Host "Prototype identities: Switch probe 057E:2009 -> XInput CAFE:4010"
|
||||
Write-Host "Disconnect other XInput controllers before continuing."
|
||||
|
||||
$baseline = Get-ConnectedXInputStates
|
||||
Add-Result -Name "Clean XInput baseline" -Passed ($baseline.Count -eq 0) `
|
||||
-Detail ("{0} XInput controller(s) connected before the Pico" -f $baseline.Count)
|
||||
|
||||
if (-not $SkipInteractive) {
|
||||
Write-Host ""
|
||||
Write-Host "Disconnect the Pico from USB." -ForegroundColor Yellow
|
||||
[void](Read-Host "Press Enter after it is disconnected")
|
||||
|
||||
$disconnectDeadline = (Get-Date).AddSeconds(10)
|
||||
do {
|
||||
$switchPresent = (Get-UsbIdentityDevices -Vid "057E" -Pid "2009").Count -gt 0
|
||||
$xinputPresent = (Get-UsbIdentityDevices -Vid "CAFE" -Pid "4010").Count -gt 0
|
||||
if (-not $switchPresent -and -not $xinputPresent) { break }
|
||||
Start-Sleep -Milliseconds 100
|
||||
} while ((Get-Date) -lt $disconnectDeadline)
|
||||
|
||||
Add-Result -Name "Pico disconnected" `
|
||||
-Passed (-not $switchPresent -and -not $xinputPresent) `
|
||||
-Detail "Neither prototype USB identity is present"
|
||||
|
||||
Write-Host "Connect the Pico to this Windows PC now." -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Non-interactive mode: inspecting the currently connected Pico." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$transitionStart = Get-Date
|
||||
$deadline = $transitionStart.AddSeconds($DetectionTimeoutSeconds)
|
||||
$seenSwitch = $false
|
||||
$seenXInput = $false
|
||||
$switchSeenAt = $null
|
||||
$xinputSeenAt = $null
|
||||
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (-not $seenSwitch -and (Get-UsbIdentityDevices -Vid "057E" -Pid "2009").Count -gt 0) {
|
||||
$seenSwitch = $true
|
||||
$switchSeenAt = (Get-Date)
|
||||
Write-Host "Observed Switch probe identity 057E:2009" -ForegroundColor DarkCyan
|
||||
}
|
||||
|
||||
if ((Get-UsbIdentityDevices -Vid "CAFE" -Pid "4010").Count -gt 0) {
|
||||
$seenXInput = $true
|
||||
$xinputSeenAt = (Get-Date)
|
||||
Write-Host "Observed XInput identity CAFE:4010" -ForegroundColor DarkCyan
|
||||
break
|
||||
}
|
||||
Start-Sleep -Milliseconds 50
|
||||
}
|
||||
|
||||
if ($SkipInteractive -and $seenXInput) {
|
||||
Add-Result -Name "Automatic identity transition" -Passed $true -Required $false `
|
||||
-Detail "XInput identity is present; initial Switch probe was not observed in this run"
|
||||
} else {
|
||||
$transitionPassed = $seenSwitch -and $seenXInput -and $xinputSeenAt -gt $switchSeenAt
|
||||
$transitionMs = if ($transitionPassed) {
|
||||
[math]::Round(($xinputSeenAt - $switchSeenAt).TotalMilliseconds)
|
||||
} else { -1 }
|
||||
Add-Result -Name "Automatic identity transition" -Passed $transitionPassed `
|
||||
-Detail ("SwitchSeen={0}, XInputSeen={1}, transition={2} ms" -f $seenSwitch, $seenXInput, $transitionMs)
|
||||
}
|
||||
|
||||
$xinputPnp = @(Get-UsbIdentityDevices -Vid "CAFE" -Pid "4010")
|
||||
Add-Result -Name "XInput PnP identity" -Passed ($xinputPnp.Count -gt 0) `
|
||||
-Detail ("Found {0} present PnP node(s) for CAFE:4010" -f $xinputPnp.Count)
|
||||
|
||||
$problemDevices = @($xinputPnp | Where-Object {
|
||||
$_.PSObject.Properties.Name -contains "Status" -and $_.Status -notin @("OK", "Unknown")
|
||||
})
|
||||
Add-Result -Name "PnP device health" -Passed ($problemDevices.Count -eq 0) `
|
||||
-Detail ("{0} unhealthy PnP node(s)" -f $problemDevices.Count)
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
$connectedStates = Get-ConnectedXInputStates
|
||||
Add-Result -Name "Four XInput API slots" -Passed ($connectedStates.Count -eq 4) `
|
||||
-Detail ("XInputGetState reports {0}/4 connected slots" -f $connectedStates.Count)
|
||||
|
||||
$inputSummaries = @()
|
||||
if (-not $SkipInteractive) {
|
||||
for ($index = 0; $index -lt $ExpectedActiveControllers; $index++) {
|
||||
Write-Host ""
|
||||
Write-Host ("For {0} seconds, exercise physical controller slot {1}:" -f $InputTestSeconds, ($index + 1)) -ForegroundColor Yellow
|
||||
Write-Host " D-pad, face buttons, both shoulders, both triggers, and both sticks."
|
||||
[void](Read-Host "Press Enter to start capture")
|
||||
|
||||
$before = Get-XInputState -Index $index
|
||||
$changed = $false
|
||||
$samples = 0
|
||||
$observedControls = [ordered]@{
|
||||
DPad = $false
|
||||
FaceButtons = $false
|
||||
Shoulders = $false
|
||||
LeftTrigger = $false
|
||||
RightTrigger = $false
|
||||
LeftStick = $false
|
||||
RightStick = $false
|
||||
}
|
||||
$captureDeadline = (Get-Date).AddSeconds($InputTestSeconds)
|
||||
while ((Get-Date) -lt $captureDeadline) {
|
||||
$after = Get-XInputState -Index $index
|
||||
if (Test-StateChanged -Before $before -After $after) {
|
||||
$changed = $true
|
||||
}
|
||||
if (($after.Buttons -band 0x000F) -ne 0) { $observedControls.DPad = $true }
|
||||
if (($after.Buttons -band 0xF000) -ne 0) { $observedControls.FaceButtons = $true }
|
||||
if (($after.Buttons -band 0x0300) -ne 0) { $observedControls.Shoulders = $true }
|
||||
if ($after.LeftTrigger -gt 0) { $observedControls.LeftTrigger = $true }
|
||||
if ($after.RightTrigger -gt 0) { $observedControls.RightTrigger = $true }
|
||||
if ([math]::Abs([int]$after.LeftThumbX) -gt 8000 -or
|
||||
[math]::Abs([int]$after.LeftThumbY) -gt 8000) {
|
||||
$observedControls.LeftStick = $true
|
||||
}
|
||||
if ([math]::Abs([int]$after.RightThumbX) -gt 8000 -or
|
||||
[math]::Abs([int]$after.RightThumbY) -gt 8000) {
|
||||
$observedControls.RightStick = $true
|
||||
}
|
||||
$before = $after
|
||||
$samples++
|
||||
Start-Sleep -Milliseconds 20
|
||||
}
|
||||
$missingControls = @($observedControls.GetEnumerator() |
|
||||
Where-Object { -not $_.Value } |
|
||||
ForEach-Object { $_.Key })
|
||||
$inputPassed = $changed -and $missingControls.Count -eq 0
|
||||
$inputSummaries += [pscustomobject]@{
|
||||
Slot = $index
|
||||
Changed = $changed
|
||||
Samples = $samples
|
||||
LastPacket = $before.PacketNumber
|
||||
Observed = $observedControls
|
||||
Missing = $missingControls
|
||||
}
|
||||
Add-Result -Name ("XInput slot {0} complete input" -f ($index + 1)) `
|
||||
-Passed $inputPassed `
|
||||
-Detail ("samples={0}, packet={1}, missing=[{2}]" -f $samples, $before.PacketNumber, ($missingControls -join ", "))
|
||||
}
|
||||
|
||||
for ($index = 0; $index -lt $ExpectedActiveControllers; $index++) {
|
||||
Write-Host ""
|
||||
Write-Host ("Sending rumble to XInput slot {0} for {1} ms." -f ($index + 1), $RumbleMilliseconds) -ForegroundColor Yellow
|
||||
$startStatus = Set-XInputRumble -Index $index -Left 65535 -Right 49152
|
||||
Start-Sleep -Milliseconds $RumbleMilliseconds
|
||||
$stopStatus = Set-XInputRumble -Index $index -Left 0 -Right 0
|
||||
$observed = Read-YesNo -Prompt ("Did only physical controller slot {0} vibrate" -f ($index + 1))
|
||||
Add-Result -Name ("XInput slot {0} rumble/isolation" -f ($index + 1)) `
|
||||
-Passed ($startStatus -eq 0 -and $stopStatus -eq 0 -and $observed) `
|
||||
-Detail ("XInputSetState start={0}, stop={1}, observed={2}" -f $startStatus, $stopStatus, $observed)
|
||||
}
|
||||
} else {
|
||||
Add-Result -Name "Interactive input and rumble" -Passed $true -Required $false `
|
||||
-Detail "Skipped by -SkipInteractive"
|
||||
}
|
||||
|
||||
$pnpDetails = @(Get-CimInstance Win32_PnPEntity -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.PNPDeviceID -like "*VID_CAFE&PID_4010*" } |
|
||||
Select-Object Name, PNPDeviceID, Service, ConfigManagerErrorCode)
|
||||
|
||||
$report = [ordered]@{
|
||||
TimestampUtc = [DateTime]::UtcNow.ToString("o")
|
||||
ComputerName = $env:COMPUTERNAME
|
||||
PowerShellVersion = $PSVersionTable.PSVersion.ToString()
|
||||
Parameters = [ordered]@{
|
||||
DetectionTimeoutSeconds = $DetectionTimeoutSeconds
|
||||
ExpectedActiveControllers = $ExpectedActiveControllers
|
||||
InputTestSeconds = $InputTestSeconds
|
||||
RumbleMilliseconds = $RumbleMilliseconds
|
||||
SkipInteractive = [bool]$SkipInteractive
|
||||
}
|
||||
Transition = [ordered]@{
|
||||
SeenSwitch = $seenSwitch
|
||||
SeenXInput = $seenXInput
|
||||
SwitchSeenAt = if ($switchSeenAt) { $switchSeenAt.ToUniversalTime().ToString("o") } else { $null }
|
||||
XInputSeenAt = if ($xinputSeenAt) { $xinputSeenAt.ToUniversalTime().ToString("o") } else { $null }
|
||||
}
|
||||
ConnectedXInputSlots = @($connectedStates | Select-Object Index, PacketNumber)
|
||||
InputSummaries = $inputSummaries
|
||||
PnpDevices = $pnpDetails
|
||||
Results = @($script:Results)
|
||||
}
|
||||
|
||||
$report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $OutputPath -Encoding UTF8
|
||||
Write-Host ""
|
||||
Write-Host ("Wrote report: {0}" -f (Resolve-Path $OutputPath)) -ForegroundColor Cyan
|
||||
|
||||
$failed = @($script:Results | Where-Object { $_.Required -and -not $_.Passed })
|
||||
if ($failed.Count -gt 0) {
|
||||
Write-Host ("Feasibility test failed: {0} required check(s) failed." -f $failed.Count) -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "All required feasibility checks passed." -ForegroundColor Green
|
||||
exit 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue