Compare commits
3 commits
7e3be8675f
...
a7fb6c5694
| Author | SHA1 | Date | |
|---|---|---|---|
| a7fb6c5694 | |||
| 604450375d | |||
| 8351cb1d15 |
6 changed files with 572 additions and 7 deletions
|
|
@ -19,8 +19,8 @@
|
||||||
|
|
||||||
// Local confirmation pulse sent only to the controller that toggled.
|
// Local confirmation pulse sent only to the controller that toggled.
|
||||||
#define SWITCH_ABXY_FEEDBACK_DURATION_MS 120
|
#define SWITCH_ABXY_FEEDBACK_DURATION_MS 120
|
||||||
#define SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE 0x80
|
#define SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE 0xFF
|
||||||
#define SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE 0x80
|
#define SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE 0xFF
|
||||||
|
|
||||||
// A longer pulse confirms disabled; a shorter pulse confirms enabled.
|
// A longer pulse confirms disabled; a shorter pulse confirms enabled.
|
||||||
#define SWITCH_MOTION_DISABLED_FEEDBACK_DURATION_MS 180
|
#define SWITCH_MOTION_DISABLED_FEEDBACK_DURATION_MS 180
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -259,10 +259,10 @@ index 0265f93..5c0f2bb 100644
|
||||||
return (psmove_instance_t*)&d->parser_data[0];
|
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
|
diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/components/bluepad32/parser/uni_hid_parser_switch.c
|
||||||
index 599fc35..c72f056 100644
|
index 599fc35..9f073b4 100644
|
||||||
--- a/src/components/bluepad32/parser/uni_hid_parser_switch.c
|
--- a/src/components/bluepad32/parser/uni_hid_parser_switch.c
|
||||||
+++ b/src/components/bluepad32/parser/uni_hid_parser_switch.c
|
+++ b/src/components/bluepad32/parser/uni_hid_parser_switch.c
|
||||||
@@ -51,7 +51,8 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0;
|
@@ -51,13 +51,15 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0;
|
||||||
static const int16_t DEFAULT_ACCEL_SCALE = 16384;
|
static const int16_t DEFAULT_ACCEL_SCALE = 16384;
|
||||||
static const int16_t DEFAULT_GYRO_OFFSET = 0;
|
static const int16_t DEFAULT_GYRO_OFFSET = 0;
|
||||||
static const int16_t DEFAULT_GYRO_SCALE = 13371;
|
static const int16_t DEFAULT_GYRO_SCALE = 13371;
|
||||||
|
|
@ -272,7 +272,97 @@ index 599fc35..c72f056 100644
|
||||||
|
|
||||||
#define SWITCH_FACTORY_IMU_CAL_DATA_SIZE 24
|
#define SWITCH_FACTORY_IMU_CAL_DATA_SIZE 24
|
||||||
static const uint16_t SWITCH_FACTORY_IMU_CAL_DATA_ADDR = 0x6020;
|
static const uint16_t SWITCH_FACTORY_IMU_CAL_DATA_ADDR = 0x6020;
|
||||||
@@ -823,19 +824,26 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
|
|
||||||
|
#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) {
|
||||||
switch_instance_t* ins = get_switch_instance(d);
|
switch_instance_t* ins = get_switch_instance(d);
|
||||||
uni_controller_t* ctl = &d->controller;
|
uni_controller_t* ctl = &d->controller;
|
||||||
|
|
||||||
|
|
@ -308,7 +398,7 @@ index 599fc35..c72f056 100644
|
||||||
if (ins->controller_type == SWITCH_CONTROLLER_TYPE_JCR) {
|
if (ins->controller_type == SWITCH_CONTROLLER_TYPE_JCR) {
|
||||||
accel[1] = -accel[1];
|
accel[1] = -accel[1];
|
||||||
accel[2] = -accel[2];
|
accel[2] = -accel[2];
|
||||||
@@ -843,10 +851,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
|
@@ -843,10 +874,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
|
||||||
gyro[2] = -gyro[2];
|
gyro[2] = -gyro[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -326,6 +416,121 @@ index 599fc35..c72f056 100644
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process 0x30 input report: SWITCH_INPUT_IMU_DATA
|
// 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
|
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
|
index be2103e..4819639 100644
|
||||||
--- a/src/components/bluepad32/parser/uni_hid_parser_wii.c
|
--- a/src/components/bluepad32/parser/uni_hid_parser_wii.c
|
||||||
|
|
|
||||||
|
|
@ -889,8 +889,10 @@ void test_abxy_hotkey() {
|
||||||
require(slot_zero.rumble_calls == 1 &&
|
require(slot_zero.rumble_calls == 1 &&
|
||||||
slot_zero.last_high == kAbxyFeedbackWeakMagnitude &&
|
slot_zero.last_high == kAbxyFeedbackWeakMagnitude &&
|
||||||
slot_zero.last_low == kAbxyFeedbackStrongMagnitude &&
|
slot_zero.last_low == kAbxyFeedbackStrongMagnitude &&
|
||||||
|
slot_zero.last_high == UINT8_MAX &&
|
||||||
|
slot_zero.last_low == UINT8_MAX &&
|
||||||
g_slots[0].rumble_pending,
|
g_slots[0].rumble_pending,
|
||||||
"ABXY confirmation did not take priority over host rumble");
|
"ABXY confirmation was not full-strength or did not take priority");
|
||||||
|
|
||||||
platform_on_controller_data(&slot_zero, &input);
|
platform_on_controller_data(&slot_zero, &input);
|
||||||
process_rumble_timer(&g_rumble_timer);
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
|
|
||||||
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