diff --git a/controller_hotkey_config.h b/controller_hotkey_config.h index ebd2cc0..883d657 100644 --- a/controller_hotkey_config.h +++ b/controller_hotkey_config.h @@ -19,8 +19,8 @@ // Local confirmation pulse sent only to the controller that toggled. #define SWITCH_ABXY_FEEDBACK_DURATION_MS 120 -#define SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE 0xFF -#define SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE 0xFF +#define SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE 0x80 +#define SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE 0x80 // A longer pulse confirms disabled; a shorter pulse confirms enabled. #define SWITCH_MOTION_DISABLED_FEEDBACK_DURATION_MS 180 diff --git a/firmware/switch-pico-aio.elf b/firmware/switch-pico-aio.elf index f0fa733..aba0438 100755 Binary files a/firmware/switch-pico-aio.elf and b/firmware/switch-pico-aio.elf differ diff --git a/firmware/switch-pico-aio.uf2 b/firmware/switch-pico-aio.uf2 index 6f93f85..cb0802c 100644 Binary files a/firmware/switch-pico-aio.uf2 and b/firmware/switch-pico-aio.uf2 differ diff --git a/patches/bluepad32-sdl3-imu.patch b/patches/bluepad32-sdl3-imu.patch index 5ba7ef7..90e27e8 100644 --- a/patches/bluepad32-sdl3-imu.patch +++ b/patches/bluepad32-sdl3-imu.patch @@ -259,10 +259,10 @@ index 0265f93..5c0f2bb 100644 return (psmove_instance_t*)&d->parser_data[0]; } diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/components/bluepad32/parser/uni_hid_parser_switch.c -index 599fc35..9f073b4 100644 +index 599fc35..c72f056 100644 --- a/src/components/bluepad32/parser/uni_hid_parser_switch.c +++ b/src/components/bluepad32/parser/uni_hid_parser_switch.c -@@ -51,13 +51,15 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0; +@@ -51,7 +51,8 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0; static const int16_t DEFAULT_ACCEL_SCALE = 16384; static const int16_t DEFAULT_GYRO_OFFSET = 0; static const int16_t DEFAULT_GYRO_SCALE = 13371; @@ -272,97 +272,7 @@ index 599fc35..9f073b4 100644 #define SWITCH_FACTORY_IMU_CAL_DATA_SIZE 24 static const uint16_t SWITCH_FACTORY_IMU_CAL_DATA_ADDR = 0x6020; - - #define SWITCH_DUMP_ROM_DATA_SIZE 24 // Max size is 24 - #define SWITCH_SETUP_TIMEOUT_MS 800 -+#define SWITCH_RUMBLE_REFRESH_MS 40 - #if ENABLE_SPI_FLASH_DUMP - static const uint32_t SWITCH_DUMP_ROM_DATA_ADDR_START = 0x20000; - static const uint32_t SWITCH_DUMP_ROM_DATA_ADDR_END = 0x30000; -@@ -72,6 +74,7 @@ enum switch_state { - STATE_READ_FACTORY_IMU_CALIBRATION, // Factory IMU calibration info - STATE_SET_FULL_REPORT, // Request report 0x30 - STATE_ENABLE_IMU, // Enable/Disable gyro/accel -+ STATE_ENABLE_RUMBLE, // Enable controller vibration - STATE_DUMP_FLASH, // Dump SPI Flash memory - STATE_UPDATE_LED, // Update LEDs - STATE_READY, // Gamepad setup ready! -@@ -111,6 +114,7 @@ enum switch_subcmd { - SUBCMD_SPI_FLASH_READ = 0x10, - SUBCMD_SET_PLAYER_LEDS = 0x30, - SUBCMD_ENABLE_IMU = 0x40, -+ SUBCMD_ENABLE_RUMBLE = 0x48, - }; - - typedef enum { -@@ -137,6 +141,7 @@ typedef struct switch_instance_s { - // Although technically, we can use one timer for delay and duration, easier to debug/maintain if we have two. - btstack_timer_source_t rumble_timer_duration; - btstack_timer_source_t rumble_timer_delayed_start; -+ btstack_timer_source_t rumble_timer_refresh; - switch_state_rumble_t rumble_state; - - btstack_timer_source_t setup_timer; -@@ -322,6 +327,7 @@ static void fsm_read_user_stick_calibration(struct uni_hid_device_s* d); - static void fsm_read_factory_imu_calibration(struct uni_hid_device_s* d); - static void fsm_set_full_report(struct uni_hid_device_s* d); - static void fsm_enable_imu(struct uni_hid_device_s* d); -+static void fsm_enable_rumble(struct uni_hid_device_s* d); - static void fsm_update_led(struct uni_hid_device_s* d); - static void fsm_ready(struct uni_hid_device_s* d); - static void process_reply_read_spi_dump(struct uni_hid_device_s* d, const uint8_t* data, int len); -@@ -333,11 +339,16 @@ static void process_reply_set_report_mode(struct uni_hid_device_s* d, const stru - static void process_reply_spi_flash_read(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len); - static void process_reply_set_player_leds(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len); - static void process_reply_enable_imu(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len); -+static void process_reply_enable_rumble(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len); - static int32_t calibrate_axis(int32_t v, switch_cal_stick_t cal); - static void set_led(uni_hid_device_t* d, uint8_t leds); - static void on_switch_set_rumble_on(btstack_timer_source_t* ts); - static void on_switch_set_rumble_off(btstack_timer_source_t* ts); -+static void on_switch_refresh_rumble(btstack_timer_source_t* ts); - static void switch_stop_rumble_now(uni_hid_device_t* d); -+static void switch_send_dual_rumble_now(uni_hid_device_t* d, -+ uint8_t weak_magnitude, -+ uint8_t strong_magnitude); - static void switch_play_dual_rumble_now(uni_hid_device_t* d, - uint16_t duration_ms, - uint8_t weak_magnitude, -@@ -451,6 +462,10 @@ static void process_fsm(struct uni_hid_device_s* d) { - break; - case STATE_ENABLE_IMU: - logd("STATE_ENABLE_IMU\n"); -+ fsm_enable_rumble(d); -+ break; -+ case STATE_ENABLE_RUMBLE: -+ logd("STATE_ENABLE_RUMBLE\n"); - fsm_dump_rom(d); - break; - case STATE_DUMP_FLASH: -@@ -725,6 +740,12 @@ static void process_reply_enable_imu(struct uni_hid_device_s* d, const struct sw - ARG_UNUSED(r); - ARG_UNUSED(len); - } -+static void process_reply_enable_rumble(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len) { -+ ARG_UNUSED(d); -+ ARG_UNUSED(r); -+ ARG_UNUSED(len); -+} -+ - - // Process 0x21 input report: SWITCH_INPUT_SUBCMD_REPLY - static void process_input_subcmd_reply(struct uni_hid_device_s* d, const uint8_t* report, int len) { -@@ -752,6 +773,9 @@ static void process_input_subcmd_reply(struct uni_hid_device_s* d, const uint8_t - case SUBCMD_ENABLE_IMU: - process_reply_enable_imu(d, r, len); - break; -+ case SUBCMD_ENABLE_RUMBLE: -+ process_reply_enable_rumble(d, r, len); -+ break; - default: - loge("Switch: Error, unexpected subcmd_id=0x%02x in report 0x21\n", r->subcmd_id); - break; -@@ -823,19 +847,26 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { +@@ -823,19 +824,26 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { switch_instance_t* ins = get_switch_instance(d); uni_controller_t* ctl = &d->controller; @@ -398,7 +308,7 @@ index 599fc35..9f073b4 100644 if (ins->controller_type == SWITCH_CONTROLLER_TYPE_JCR) { accel[1] = -accel[1]; accel[2] = -accel[2]; -@@ -843,10 +874,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { +@@ -843,10 +851,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { gyro[2] = -gyro[2]; } @@ -416,121 +326,6 @@ index 599fc35..9f073b4 100644 } // Process 0x30 input report: SWITCH_INPUT_IMU_DATA -@@ -1172,6 +1206,18 @@ static void fsm_enable_imu(struct uni_hid_device_s* d) { - req->data[0] = (ins->mode == SWITCH_MODE_IMU); - send_subcmd(d, req, sizeof(out)); - } -+static void fsm_enable_rumble(struct uni_hid_device_s* d) { -+ switch_instance_t* ins = get_switch_instance(d); -+ ins->state = STATE_ENABLE_RUMBLE; -+ -+ uint8_t out[sizeof(struct switch_subcmd_request) + 1] = {0}; -+ struct switch_subcmd_request* req = (struct switch_subcmd_request*)&out[0]; -+ req->report_id = OUTPUT_RUMBLE_AND_SUBCMD; -+ req->subcmd_id = SUBCMD_ENABLE_RUMBLE; -+ req->data[0] = 0x01; -+ send_subcmd(d, req, sizeof(out)); -+} -+ - - static void fsm_update_led(struct uni_hid_device_s* d) { - switch_instance_t* ins = get_switch_instance(d); -@@ -1203,6 +1249,10 @@ static struct switch_rumble_freq_data find_rumble_freq(uint16_t freq) { - return rumble_freqs[i]; - } - -+static uint16_t switch_magnitude_to_amp(uint8_t magnitude) { -+ return (uint16_t)(((uint32_t)magnitude * 1003 + 127) / 255); -+} -+ - static struct switch_rumble_amp_data find_rumble_amp(uint16_t amp) { - unsigned int i = 0; - if (amp > rumble_amps[0].amp) { -@@ -1259,6 +1309,7 @@ void uni_hid_parser_switch_play_dual_rumble(struct uni_hid_device_s* d, - break; - case SWITCH_STATE_RUMBLE_IN_PROGRESS: - btstack_run_loop_remove_timer(&ins->rumble_timer_duration); -+ btstack_run_loop_remove_timer(&ins->rumble_timer_refresh); - break; - default: - // Do nothing -@@ -1366,6 +1417,7 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) { - - // No need to protect it with a mutex since it runs in the same main thread - assert(ins->rumble_state == SWITCH_STATE_RUMBLE_IN_PROGRESS); -+ btstack_run_loop_remove_timer(&ins->rumble_timer_refresh); - ins->rumble_state = SWITCH_STATE_RUMBLE_DISABLED; - - struct switch_subcmd_request req = {0}; -@@ -1379,6 +1431,22 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) { - send_subcmd(d, (struct switch_subcmd_request*)&req, sizeof(req) - 1); - } - -+static void switch_send_dual_rumble_now(uni_hid_device_t* d, -+ uint8_t weak_magnitude, -+ uint8_t strong_magnitude) { -+ struct switch_subcmd_request req = { -+ .report_id = OUTPUT_RUMBLE_ONLY, -+ }; -+ // Fixed frequencies match the standard Switch LRA envelope and the -+ // 8BitDo Switch-mode implementation. Magnitudes control amplitude only. -+ switch_encode_rumble(req.rumble_left, 453, 135, -+ switch_magnitude_to_amp(weak_magnitude)); -+ switch_encode_rumble(req.rumble_right, 453, 99, -+ switch_magnitude_to_amp(strong_magnitude)); -+ // Rumble request don't include the last byte of "switch_subcmd_request": subcmd_id -+ send_subcmd(d, &req, sizeof(req) - 1); -+} -+ - static void switch_play_dual_rumble_now(uni_hid_device_t* d, - uint16_t duration_ms, - uint8_t weak_magnitude, -@@ -1391,14 +1459,17 @@ static void switch_play_dual_rumble_now(uni_hid_device_t* d, - return; - } - -- struct switch_subcmd_request req = { -- .report_id = OUTPUT_RUMBLE_ONLY, -- }; -- switch_encode_rumble(req.rumble_left, weak_magnitude << 2, weak_magnitude, 500); -- switch_encode_rumble(req.rumble_right, strong_magnitude << 2, strong_magnitude, 500); -+ ins->rumble_weak_magnitude = weak_magnitude; -+ ins->rumble_strong_magnitude = strong_magnitude; -+ switch_send_dual_rumble_now(d, weak_magnitude, strong_magnitude); - -- // Rumble request don't include the last byte of "switch_subcmd_request": subcmd_id -- send_subcmd(d, &req, sizeof(req) - 1); -+ // Refresh active rumble for Switch-compatible controllers that do not -+ // retain a single output packet, including 8BitDo Switch mode. -+ ins->rumble_timer_refresh.process = &on_switch_refresh_rumble; -+ ins->rumble_timer_refresh.context = d; -+ btstack_run_loop_set_timer(&ins->rumble_timer_refresh, -+ SWITCH_RUMBLE_REFRESH_MS); -+ btstack_run_loop_add_timer(&ins->rumble_timer_refresh); - - // Set timer to turn off rumble - ins->rumble_timer_duration.process = &on_switch_set_rumble_off; -@@ -1414,6 +1485,20 @@ static void on_switch_set_rumble_on(btstack_timer_source_t* ts) { - - switch_play_dual_rumble_now(d, ins->rumble_duration_ms, ins->rumble_weak_magnitude, ins->rumble_strong_magnitude); - } -+static void on_switch_refresh_rumble(btstack_timer_source_t* ts) { -+ uni_hid_device_t* d = btstack_run_loop_get_timer_context(ts); -+ switch_instance_t* ins = get_switch_instance(d); -+ if (ins->rumble_state != SWITCH_STATE_RUMBLE_IN_PROGRESS) { -+ return; -+ } -+ switch_send_dual_rumble_now( -+ d, (uint8_t)ins->rumble_weak_magnitude, -+ (uint8_t)ins->rumble_strong_magnitude); -+ btstack_run_loop_set_timer(&ins->rumble_timer_refresh, -+ SWITCH_RUMBLE_REFRESH_MS); -+ btstack_run_loop_add_timer(&ins->rumble_timer_refresh); -+} -+ - - static void on_switch_set_rumble_off(btstack_timer_source_t* ts) { - uni_hid_device_t* d = btstack_run_loop_get_timer_context(ts); diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/components/bluepad32/parser/uni_hid_parser_wii.c index be2103e..4819639 100644 --- a/src/components/bluepad32/parser/uni_hid_parser_wii.c diff --git a/tests/bluepad32_backend_lifecycle_test.cpp b/tests/bluepad32_backend_lifecycle_test.cpp index cca33a1..957bd37 100644 --- a/tests/bluepad32_backend_lifecycle_test.cpp +++ b/tests/bluepad32_backend_lifecycle_test.cpp @@ -889,10 +889,8 @@ void test_abxy_hotkey() { require(slot_zero.rumble_calls == 1 && slot_zero.last_high == kAbxyFeedbackWeakMagnitude && slot_zero.last_low == kAbxyFeedbackStrongMagnitude && - slot_zero.last_high == UINT8_MAX && - slot_zero.last_low == UINT8_MAX && g_slots[0].rumble_pending, - "ABXY confirmation was not full-strength or did not take priority"); + "ABXY confirmation did not take priority over host rumble"); platform_on_controller_data(&slot_zero, &input); process_rumble_timer(&g_rumble_timer); diff --git a/tools/Test-AdapterFeasibility.ps1 b/tools/Test-AdapterFeasibility.ps1 deleted file mode 100644 index 0b071de..0000000 --- a/tools/Test-AdapterFeasibility.ps1 +++ /dev/null @@ -1,358 +0,0 @@ -[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