Add per-controller motion toggle
This commit is contained in:
parent
94ccfc7220
commit
81f6ac1236
8 changed files with 247 additions and 45 deletions
12
README.md
12
README.md
|
|
@ -91,6 +91,18 @@ Each connected AIO controller can toggle its own ABXY layout by pressing **L + R
|
||||||
|
|
||||||
Edit `controller_hotkey_config.h` to change the chord, default layout, or confirmation pulse.
|
Edit `controller_hotkey_config.h` to change the chord, default layout, or confirmation pulse.
|
||||||
|
|
||||||
|
### Per-controller motion toggle
|
||||||
|
|
||||||
|
Press **D-pad Up + R + Start** together to disable or re-enable motion for one controller. On DualSense, use **D-pad Up + R1 + Options**.
|
||||||
|
|
||||||
|
- A longer rumble confirms motion disabled.
|
||||||
|
- A shorter rumble confirms motion enabled.
|
||||||
|
- The chord is consumed locally and is not forwarded to the Switch.
|
||||||
|
- Other controller slots are unaffected.
|
||||||
|
- Motion returns to enabled after disconnect or reboot.
|
||||||
|
|
||||||
|
Edit `controller_hotkey_config.h` to change the chord, default state, or feedback patterns.
|
||||||
|
|
||||||
### Per-slot controller colors
|
### Per-slot controller colors
|
||||||
|
|
||||||
Each AIO slot has one color shared by its emulated Switch Pro grips and its physical Bluetooth controller:
|
Each AIO slot has one color shared by its emulated Switch Pro grips and its physical Bluetooth controller:
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,35 @@ constexpr uint8_t kAbxyFeedbackWeakMagnitude =
|
||||||
SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE;
|
SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE;
|
||||||
constexpr uint8_t kAbxyFeedbackStrongMagnitude =
|
constexpr uint8_t kAbxyFeedbackStrongMagnitude =
|
||||||
SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE;
|
SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE;
|
||||||
|
constexpr uint32_t kMotionHotkeyDpadMask =
|
||||||
|
SWITCH_MOTION_HOTKEY_DPAD_MASK;
|
||||||
|
constexpr uint32_t kMotionHotkeyButtonMask =
|
||||||
|
SWITCH_MOTION_HOTKEY_BUTTON_MASK;
|
||||||
|
constexpr uint32_t kMotionHotkeyMiscMask =
|
||||||
|
SWITCH_MOTION_HOTKEY_MISC_MASK;
|
||||||
|
constexpr bool kDefaultMotionEnabled =
|
||||||
|
SWITCH_MOTION_DEFAULT_ENABLED != 0;
|
||||||
|
constexpr uint16_t kMotionDisabledFeedbackDurationMs =
|
||||||
|
SWITCH_MOTION_DISABLED_FEEDBACK_DURATION_MS;
|
||||||
|
constexpr uint8_t kMotionDisabledFeedbackWeakMagnitude =
|
||||||
|
SWITCH_MOTION_DISABLED_FEEDBACK_WEAK_MAGNITUDE;
|
||||||
|
constexpr uint8_t kMotionDisabledFeedbackStrongMagnitude =
|
||||||
|
SWITCH_MOTION_DISABLED_FEEDBACK_STRONG_MAGNITUDE;
|
||||||
|
constexpr uint16_t kMotionEnabledFeedbackDurationMs =
|
||||||
|
SWITCH_MOTION_ENABLED_FEEDBACK_DURATION_MS;
|
||||||
|
constexpr uint8_t kMotionEnabledFeedbackWeakMagnitude =
|
||||||
|
SWITCH_MOTION_ENABLED_FEEDBACK_WEAK_MAGNITUDE;
|
||||||
|
constexpr uint8_t kMotionEnabledFeedbackStrongMagnitude =
|
||||||
|
SWITCH_MOTION_ENABLED_FEEDBACK_STRONG_MAGNITUDE;
|
||||||
|
|
||||||
static_assert(kAbxyHotkeyButtonMask != 0);
|
static_assert(kAbxyHotkeyButtonMask != 0);
|
||||||
static_assert(kAbxyHotkeyMiscMask != 0);
|
static_assert(kAbxyHotkeyMiscMask != 0);
|
||||||
static_assert(kAbxyFeedbackDurationMs > 0);
|
static_assert(kAbxyFeedbackDurationMs > 0);
|
||||||
|
static_assert(kMotionHotkeyDpadMask != 0);
|
||||||
|
static_assert(kMotionHotkeyButtonMask != 0);
|
||||||
|
static_assert(kMotionHotkeyMiscMask != 0);
|
||||||
|
static_assert(kMotionDisabledFeedbackDurationMs > 0);
|
||||||
|
static_assert(kMotionEnabledFeedbackDurationMs > 0);
|
||||||
|
|
||||||
static_assert(kSlotCount == 4);
|
static_assert(kSlotCount == 4);
|
||||||
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == kSlotCount);
|
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == kSlotCount);
|
||||||
|
|
@ -62,6 +87,13 @@ struct RumbleEnvelope {
|
||||||
uint32_t connection_generation;
|
uint32_t connection_generation;
|
||||||
SwitchRumbleOutput rumble;
|
SwitchRumbleOutput rumble;
|
||||||
};
|
};
|
||||||
|
struct FeedbackEnvelope {
|
||||||
|
uint32_t connection_generation;
|
||||||
|
uint16_t duration_ms;
|
||||||
|
uint8_t weak_magnitude;
|
||||||
|
uint8_t strong_magnitude;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
struct BackendSlot {
|
struct BackendSlot {
|
||||||
SwitchInputState state;
|
SwitchInputState state;
|
||||||
|
|
@ -72,11 +104,13 @@ struct BackendSlot {
|
||||||
bool active;
|
bool active;
|
||||||
bool rumble_pending;
|
bool rumble_pending;
|
||||||
bool swap_abxy;
|
bool swap_abxy;
|
||||||
bool hotkey_latched;
|
bool abxy_hotkey_latched;
|
||||||
|
bool motion_enabled;
|
||||||
|
bool motion_hotkey_latched;
|
||||||
bool feedback_pending;
|
bool feedback_pending;
|
||||||
uint32_t feedback_generation;
|
|
||||||
uint32_t feedback_until_ms;
|
uint32_t feedback_until_ms;
|
||||||
RumbleEnvelope pending_rumble;
|
RumbleEnvelope pending_rumble;
|
||||||
|
FeedbackEnvelope pending_feedback;
|
||||||
};
|
};
|
||||||
|
|
||||||
critical_section_t g_state_lock;
|
critical_section_t g_state_lock;
|
||||||
|
|
@ -229,7 +263,8 @@ constexpr int16_t clamp_int16(int64_t value) {
|
||||||
return static_cast<int16_t>(value);
|
return static_cast<int16_t>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr int64_t divide_round_nearest(int64_t numerator, int64_t denominator) {
|
constexpr int64_t divide_round_nearest(int64_t numerator,
|
||||||
|
int64_t denominator) {
|
||||||
if (numerator >= 0) {
|
if (numerator >= 0) {
|
||||||
return (numerator + denominator / 2) / denominator;
|
return (numerator + denominator / 2) / denominator;
|
||||||
}
|
}
|
||||||
|
|
@ -264,7 +299,8 @@ bool has_motion(const uni_gamepad_t& gamepad) {
|
||||||
}
|
}
|
||||||
|
|
||||||
SwitchInputState map_gamepad(const uni_gamepad_t& gamepad,
|
SwitchInputState map_gamepad(const uni_gamepad_t& gamepad,
|
||||||
bool swap_abxy) {
|
bool swap_abxy,
|
||||||
|
bool motion_enabled) {
|
||||||
SwitchInputState state = make_neutral_state();
|
SwitchInputState state = make_neutral_state();
|
||||||
|
|
||||||
state.dpad_up = (gamepad.dpad & DPAD_UP) != 0;
|
state.dpad_up = (gamepad.dpad & DPAD_UP) != 0;
|
||||||
|
|
@ -302,7 +338,7 @@ SwitchInputState map_gamepad(const uni_gamepad_t& gamepad,
|
||||||
state.rx = scale_stick(gamepad.axis_rx);
|
state.rx = scale_stick(gamepad.axis_rx);
|
||||||
state.ry = scale_stick(gamepad.axis_ry);
|
state.ry = scale_stick(gamepad.axis_ry);
|
||||||
|
|
||||||
if (has_motion(gamepad)) {
|
if (motion_enabled && has_motion(gamepad)) {
|
||||||
// Dependency patches normalize both arrays to SDL3 PlayStation axes.
|
// Dependency patches normalize both arrays to SDL3 PlayStation axes.
|
||||||
SwitchImuSample sample{};
|
SwitchImuSample sample{};
|
||||||
sample.accel_x = convert_accel(-static_cast<int64_t>(gamepad.accel[2]));
|
sample.accel_x = convert_accel(-static_cast<int64_t>(gamepad.accel[2]));
|
||||||
|
|
@ -319,31 +355,87 @@ SwitchInputState map_gamepad(const uni_gamepad_t& gamepad,
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
struct LayoutDecision {
|
struct HotkeyDecision {
|
||||||
bool swap_abxy;
|
bool swap_abxy;
|
||||||
bool suppress_hotkey;
|
bool motion_enabled;
|
||||||
|
uint32_t suppress_dpad;
|
||||||
|
uint32_t suppress_buttons;
|
||||||
|
uint32_t suppress_misc_buttons;
|
||||||
};
|
};
|
||||||
|
|
||||||
LayoutDecision update_layout_hotkey(uint8_t slot_index,
|
void queue_local_feedback(BackendSlot& slot, uint16_t duration_ms,
|
||||||
uni_hid_device_t* device,
|
uint8_t weak_magnitude,
|
||||||
|
uint8_t strong_magnitude) {
|
||||||
|
slot.feedback_pending = true;
|
||||||
|
slot.pending_feedback = {
|
||||||
|
slot.connection_generation, duration_ms, weak_magnitude,
|
||||||
|
strong_magnitude};
|
||||||
|
}
|
||||||
|
void reset_slot_hotkeys(BackendSlot& slot) {
|
||||||
|
slot.swap_abxy = kDefaultSwapAbxy;
|
||||||
|
slot.abxy_hotkey_latched = false;
|
||||||
|
slot.motion_enabled = kDefaultMotionEnabled;
|
||||||
|
slot.motion_hotkey_latched = false;
|
||||||
|
slot.feedback_pending = false;
|
||||||
|
slot.feedback_until_ms = 0;
|
||||||
|
slot.pending_feedback = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
HotkeyDecision update_controller_hotkeys(
|
||||||
|
uint8_t slot_index, uni_hid_device_t* device,
|
||||||
const uni_gamepad_t& gamepad) {
|
const uni_gamepad_t& gamepad) {
|
||||||
const bool chord_pressed =
|
const bool abxy_pressed =
|
||||||
(gamepad.buttons & kAbxyHotkeyButtonMask) ==
|
(gamepad.buttons & kAbxyHotkeyButtonMask) ==
|
||||||
kAbxyHotkeyButtonMask &&
|
kAbxyHotkeyButtonMask &&
|
||||||
(gamepad.misc_buttons & kAbxyHotkeyMiscMask) ==
|
(gamepad.misc_buttons & kAbxyHotkeyMiscMask) ==
|
||||||
kAbxyHotkeyMiscMask;
|
kAbxyHotkeyMiscMask;
|
||||||
LayoutDecision decision{kDefaultSwapAbxy, false};
|
const bool motion_pressed =
|
||||||
|
(gamepad.dpad & kMotionHotkeyDpadMask) ==
|
||||||
|
kMotionHotkeyDpadMask &&
|
||||||
|
(gamepad.buttons & kMotionHotkeyButtonMask) ==
|
||||||
|
kMotionHotkeyButtonMask &&
|
||||||
|
(gamepad.misc_buttons & kMotionHotkeyMiscMask) ==
|
||||||
|
kMotionHotkeyMiscMask;
|
||||||
|
HotkeyDecision decision{
|
||||||
|
kDefaultSwapAbxy, kDefaultMotionEnabled, 0, 0, 0};
|
||||||
|
|
||||||
critical_section_enter_blocking(&g_state_lock);
|
critical_section_enter_blocking(&g_state_lock);
|
||||||
BackendSlot& slot = g_slots[slot_index];
|
BackendSlot& slot = g_slots[slot_index];
|
||||||
if (slot.active && slot.device == device) {
|
if (slot.active && slot.device == device) {
|
||||||
if (chord_pressed && !slot.hotkey_latched) {
|
if (abxy_pressed && !slot.abxy_hotkey_latched) {
|
||||||
slot.swap_abxy = !slot.swap_abxy;
|
slot.swap_abxy = !slot.swap_abxy;
|
||||||
slot.feedback_pending = true;
|
queue_local_feedback(
|
||||||
slot.feedback_generation = slot.connection_generation;
|
slot, static_cast<uint16_t>(kAbxyFeedbackDurationMs),
|
||||||
|
kAbxyFeedbackWeakMagnitude,
|
||||||
|
kAbxyFeedbackStrongMagnitude);
|
||||||
|
} else if (motion_pressed && !slot.motion_hotkey_latched) {
|
||||||
|
slot.motion_enabled = !slot.motion_enabled;
|
||||||
|
if (slot.motion_enabled) {
|
||||||
|
queue_local_feedback(
|
||||||
|
slot, kMotionEnabledFeedbackDurationMs,
|
||||||
|
kMotionEnabledFeedbackWeakMagnitude,
|
||||||
|
kMotionEnabledFeedbackStrongMagnitude);
|
||||||
|
} else {
|
||||||
|
queue_local_feedback(
|
||||||
|
slot, kMotionDisabledFeedbackDurationMs,
|
||||||
|
kMotionDisabledFeedbackWeakMagnitude,
|
||||||
|
kMotionDisabledFeedbackStrongMagnitude);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slot.abxy_hotkey_latched = abxy_pressed;
|
||||||
|
slot.motion_hotkey_latched = motion_pressed;
|
||||||
|
decision.swap_abxy = slot.swap_abxy;
|
||||||
|
decision.motion_enabled = slot.motion_enabled;
|
||||||
|
if (abxy_pressed) {
|
||||||
|
decision.suppress_buttons |= kAbxyHotkeyButtonMask;
|
||||||
|
decision.suppress_misc_buttons |= kAbxyHotkeyMiscMask;
|
||||||
|
}
|
||||||
|
if (motion_pressed) {
|
||||||
|
decision.suppress_dpad |= kMotionHotkeyDpadMask;
|
||||||
|
decision.suppress_buttons |= kMotionHotkeyButtonMask;
|
||||||
|
decision.suppress_misc_buttons |= kMotionHotkeyMiscMask;
|
||||||
}
|
}
|
||||||
slot.hotkey_latched = chord_pressed;
|
|
||||||
decision = {slot.swap_abxy, chord_pressed};
|
|
||||||
}
|
}
|
||||||
critical_section_exit(&g_state_lock);
|
critical_section_exit(&g_state_lock);
|
||||||
return decision;
|
return decision;
|
||||||
|
|
@ -437,6 +529,7 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
||||||
|
|
||||||
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
|
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
|
||||||
RumbleEnvelope envelope{};
|
RumbleEnvelope envelope{};
|
||||||
|
FeedbackEnvelope feedback{};
|
||||||
uni_hid_device_t* device = nullptr;
|
uni_hid_device_t* device = nullptr;
|
||||||
bool feedback_dispatch = false;
|
bool feedback_dispatch = false;
|
||||||
bool host_dispatch = false;
|
bool host_dispatch = false;
|
||||||
|
|
@ -444,15 +537,17 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
||||||
critical_section_enter_blocking(&g_state_lock);
|
critical_section_enter_blocking(&g_state_lock);
|
||||||
BackendSlot& slot = g_slots[slot_index];
|
BackendSlot& slot = g_slots[slot_index];
|
||||||
if (slot.feedback_pending) {
|
if (slot.feedback_pending) {
|
||||||
|
feedback = slot.pending_feedback;
|
||||||
feedback_dispatch =
|
feedback_dispatch =
|
||||||
slot.active && slot.device != nullptr &&
|
slot.active && slot.device != nullptr &&
|
||||||
slot.feedback_generation == slot.connection_generation &&
|
feedback.connection_generation ==
|
||||||
|
slot.connection_generation &&
|
||||||
slot.device->report_parser.play_dual_rumble != nullptr;
|
slot.device->report_parser.play_dual_rumble != nullptr;
|
||||||
slot.feedback_pending = false;
|
slot.feedback_pending = false;
|
||||||
if (feedback_dispatch) {
|
if (feedback_dispatch) {
|
||||||
device = slot.device;
|
device = slot.device;
|
||||||
slot.feedback_until_ms =
|
slot.feedback_until_ms =
|
||||||
now_ms + kAbxyFeedbackDurationMs;
|
now_ms + feedback.duration_ms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -475,9 +570,8 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
||||||
|
|
||||||
if (feedback_dispatch) {
|
if (feedback_dispatch) {
|
||||||
device->report_parser.play_dual_rumble(
|
device->report_parser.play_dual_rumble(
|
||||||
device, 0, kAbxyFeedbackDurationMs,
|
device, 0, feedback.duration_ms,
|
||||||
kAbxyFeedbackWeakMagnitude,
|
feedback.weak_magnitude, feedback.strong_magnitude);
|
||||||
kAbxyFeedbackStrongMagnitude);
|
|
||||||
} else if (host_dispatch &&
|
} else if (host_dispatch &&
|
||||||
device->report_parser.play_dual_rumble != nullptr) {
|
device->report_parser.play_dual_rumble != nullptr) {
|
||||||
device->report_parser.play_dual_rumble(
|
device->report_parser.play_dual_rumble(
|
||||||
|
|
@ -546,10 +640,7 @@ void platform_on_device_connected(uni_hid_device_t* device) {
|
||||||
if (!slot.active && slot.device == nullptr) {
|
if (!slot.active && slot.device == nullptr) {
|
||||||
slot.device = device;
|
slot.device = device;
|
||||||
slot.rumble_pending = false;
|
slot.rumble_pending = false;
|
||||||
slot.swap_abxy = kDefaultSwapAbxy;
|
reset_slot_hotkeys(slot);
|
||||||
slot.hotkey_latched = false;
|
|
||||||
slot.feedback_pending = false;
|
|
||||||
slot.feedback_until_ms = 0;
|
|
||||||
tracked_connection = true;
|
tracked_connection = true;
|
||||||
} else {
|
} else {
|
||||||
tracked_connection = slot.device == device;
|
tracked_connection = slot.device == device;
|
||||||
|
|
@ -578,10 +669,7 @@ void platform_on_device_disconnected(uni_hid_device_t* device) {
|
||||||
slot.device = nullptr;
|
slot.device = nullptr;
|
||||||
slot.active = false;
|
slot.active = false;
|
||||||
slot.rumble_pending = false;
|
slot.rumble_pending = false;
|
||||||
slot.swap_abxy = kDefaultSwapAbxy;
|
reset_slot_hotkeys(slot);
|
||||||
slot.hotkey_latched = false;
|
|
||||||
slot.feedback_pending = false;
|
|
||||||
slot.feedback_until_ms = 0;
|
|
||||||
++slot.connection_generation;
|
++slot.connection_generation;
|
||||||
disconnected_tracked_device = true;
|
disconnected_tracked_device = true;
|
||||||
}
|
}
|
||||||
|
|
@ -613,10 +701,7 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
|
||||||
slot.state = make_neutral_state();
|
slot.state = make_neutral_state();
|
||||||
slot.active = true;
|
slot.active = true;
|
||||||
slot.rumble_pending = false;
|
slot.rumble_pending = false;
|
||||||
slot.swap_abxy = kDefaultSwapAbxy;
|
reset_slot_hotkeys(slot);
|
||||||
slot.hotkey_latched = false;
|
|
||||||
slot.feedback_pending = false;
|
|
||||||
slot.feedback_until_ms = 0;
|
|
||||||
++slot.state_generation;
|
++slot.state_generation;
|
||||||
became_active = true;
|
became_active = true;
|
||||||
}
|
}
|
||||||
|
|
@ -644,14 +729,15 @@ void platform_on_controller_data(uni_hid_device_t* device,
|
||||||
}
|
}
|
||||||
|
|
||||||
uni_gamepad_t gamepad = controller->gamepad;
|
uni_gamepad_t gamepad = controller->gamepad;
|
||||||
const LayoutDecision layout = update_layout_hotkey(
|
const HotkeyDecision hotkeys = update_controller_hotkeys(
|
||||||
static_cast<uint8_t>(slot_index), device, gamepad);
|
static_cast<uint8_t>(slot_index), device, gamepad);
|
||||||
if (layout.suppress_hotkey) {
|
gamepad.dpad &= ~hotkeys.suppress_dpad;
|
||||||
gamepad.buttons &= ~kAbxyHotkeyButtonMask;
|
gamepad.buttons &= ~hotkeys.suppress_buttons;
|
||||||
gamepad.misc_buttons &= ~kAbxyHotkeyMiscMask;
|
gamepad.misc_buttons &= ~hotkeys.suppress_misc_buttons;
|
||||||
}
|
publish_device_state(
|
||||||
publish_device_state(static_cast<uint8_t>(slot_index), device,
|
static_cast<uint8_t>(slot_index), device,
|
||||||
map_gamepad(gamepad, layout.swap_abxy));
|
map_gamepad(gamepad, hotkeys.swap_abxy,
|
||||||
|
hotkeys.motion_enabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uni_property_t* platform_get_property(uni_property_idx_t index) {
|
const uni_property_t* platform_get_property(uni_property_idx_t index) {
|
||||||
|
|
@ -724,7 +810,7 @@ void bluepad32_input_backend_init() {
|
||||||
slot = {};
|
slot = {};
|
||||||
slot.state = make_neutral_state();
|
slot.state = make_neutral_state();
|
||||||
slot.pending_rumble.slot = slot_index;
|
slot.pending_rumble.slot = slot_index;
|
||||||
slot.swap_abxy = kDefaultSwapAbxy;
|
reset_slot_hotkeys(slot);
|
||||||
g_consumed_generation[slot_index] = 0;
|
g_consumed_generation[slot_index] = 0;
|
||||||
g_last_snapshot_generation[slot_index] = 0;
|
g_last_snapshot_generation[slot_index] = 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,12 @@
|
||||||
#define SWITCH_ABXY_HOTKEY_MISC_MASK \
|
#define SWITCH_ABXY_HOTKEY_MISC_MASK \
|
||||||
(MISC_BUTTON_SELECT | MISC_BUTTON_START)
|
(MISC_BUTTON_SELECT | MISC_BUTTON_START)
|
||||||
|
|
||||||
|
// Motion toggle chord: D-pad Up + R + START / Options.
|
||||||
|
#define SWITCH_MOTION_HOTKEY_DPAD_MASK DPAD_UP
|
||||||
|
#define SWITCH_MOTION_HOTKEY_BUTTON_MASK BUTTON_SHOULDER_R
|
||||||
|
#define SWITCH_MOTION_HOTKEY_MISC_MASK MISC_BUTTON_START
|
||||||
|
#define SWITCH_MOTION_DEFAULT_ENABLED 1
|
||||||
|
|
||||||
// 0 starts each new connection in Nintendo positional layout; 1 starts swapped.
|
// 0 starts each new connection in Nintendo positional layout; 1 starts swapped.
|
||||||
#define SWITCH_ABXY_DEFAULT_SWAPPED 0
|
#define SWITCH_ABXY_DEFAULT_SWAPPED 0
|
||||||
|
|
||||||
|
|
@ -15,3 +21,11 @@
|
||||||
#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 0x80
|
||||||
#define SWITCH_ABXY_FEEDBACK_STRONG_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
|
||||||
|
#define SWITCH_MOTION_DISABLED_FEEDBACK_WEAK_MAGNITUDE 0xA0
|
||||||
|
#define SWITCH_MOTION_DISABLED_FEEDBACK_STRONG_MAGNITUDE 0xA0
|
||||||
|
#define SWITCH_MOTION_ENABLED_FEEDBACK_DURATION_MS 80
|
||||||
|
#define SWITCH_MOTION_ENABLED_FEEDBACK_WEAK_MAGNITUDE 0x60
|
||||||
|
#define SWITCH_MOTION_ENABLED_FEEDBACK_STRONG_MAGNITUDE 0x60
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -39,11 +39,12 @@ void require(bool condition, const char* message) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void play_rumble(uni_hid_device_t* device, uint16_t, uint16_t,
|
void play_rumble(uni_hid_device_t* device, uint16_t,
|
||||||
uint8_t high, uint8_t low) {
|
uint16_t duration_ms, uint8_t high, uint8_t low) {
|
||||||
++device->rumble_calls;
|
++device->rumble_calls;
|
||||||
device->last_high = high;
|
device->last_high = high;
|
||||||
device->last_low = low;
|
device->last_low = low;
|
||||||
|
device->last_rumble_duration_ms = duration_ms;
|
||||||
}
|
}
|
||||||
void set_lightbar(uni_hid_device_t* device, uint8_t red, uint8_t green,
|
void set_lightbar(uni_hid_device_t* device, uint8_t red, uint8_t green,
|
||||||
uint8_t blue) {
|
uint8_t blue) {
|
||||||
|
|
@ -793,11 +794,96 @@ void test_abxy_hotkey() {
|
||||||
uni_hid_device_t replacement = device(0);
|
uni_hid_device_t replacement = device(0);
|
||||||
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS &&
|
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS &&
|
||||||
g_slots[0].swap_abxy == kDefaultSwapAbxy &&
|
g_slots[0].swap_abxy == kDefaultSwapAbxy &&
|
||||||
!g_slots[0].hotkey_latched &&
|
!g_slots[0].abxy_hotkey_latched &&
|
||||||
!g_slots[0].feedback_pending,
|
!g_slots[0].feedback_pending,
|
||||||
"disconnect did not reset slot 0 hotkey state");
|
"disconnect did not reset slot 0 hotkey state");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_motion_hotkey() {
|
||||||
|
start_pairing_backend();
|
||||||
|
uni_hid_device_t slot_zero = device(0);
|
||||||
|
uni_hid_device_t slot_one = device(1);
|
||||||
|
require(platform_on_device_ready(&slot_zero) == UNI_ERROR_SUCCESS &&
|
||||||
|
platform_on_device_ready(&slot_one) == UNI_ERROR_SUCCESS,
|
||||||
|
"motion hotkey test controllers did not become ready");
|
||||||
|
|
||||||
|
uni_controller_t input{};
|
||||||
|
input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||||
|
input.gamepad.accel[0] = 8192;
|
||||||
|
platform_on_controller_data(&slot_zero, &input);
|
||||||
|
SwitchInputState snapshot{};
|
||||||
|
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
|
||||||
|
snapshot.imu_sample_count ==
|
||||||
|
(kDefaultMotionEnabled ? 3 : 0),
|
||||||
|
"slot 0 did not start with configured motion state");
|
||||||
|
|
||||||
|
input.gamepad.dpad = kMotionHotkeyDpadMask;
|
||||||
|
input.gamepad.buttons = kMotionHotkeyButtonMask;
|
||||||
|
input.gamepad.misc_buttons = kMotionHotkeyMiscMask;
|
||||||
|
platform_on_controller_data(&slot_zero, &input);
|
||||||
|
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
|
||||||
|
snapshot.imu_sample_count ==
|
||||||
|
(kDefaultMotionEnabled ? 0 : 3) &&
|
||||||
|
!snapshot.dpad_up && !snapshot.button_r &&
|
||||||
|
!snapshot.button_plus,
|
||||||
|
"motion chord did not toggle motion or suppress its inputs");
|
||||||
|
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(slot_zero.rumble_calls == 1 &&
|
||||||
|
slot_zero.last_rumble_duration_ms ==
|
||||||
|
(kDefaultMotionEnabled
|
||||||
|
? kMotionDisabledFeedbackDurationMs
|
||||||
|
: kMotionEnabledFeedbackDurationMs) &&
|
||||||
|
slot_zero.last_high ==
|
||||||
|
(kDefaultMotionEnabled
|
||||||
|
? kMotionDisabledFeedbackWeakMagnitude
|
||||||
|
: kMotionEnabledFeedbackWeakMagnitude) &&
|
||||||
|
slot_zero.last_low ==
|
||||||
|
(kDefaultMotionEnabled
|
||||||
|
? kMotionDisabledFeedbackStrongMagnitude
|
||||||
|
: kMotionEnabledFeedbackStrongMagnitude),
|
||||||
|
"motion toggle did not send distinct state feedback");
|
||||||
|
|
||||||
|
platform_on_controller_data(&slot_zero, &input);
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(g_slots[0].motion_enabled == !kDefaultMotionEnabled &&
|
||||||
|
slot_zero.rumble_calls == 1,
|
||||||
|
"held motion chord toggled or rumbled more than once");
|
||||||
|
|
||||||
|
uni_controller_t peer_input{};
|
||||||
|
peer_input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||||
|
peer_input.gamepad.accel[0] = 8192;
|
||||||
|
platform_on_controller_data(&slot_one, &peer_input);
|
||||||
|
require(bluepad32_input_backend_snapshot(1, &snapshot) &&
|
||||||
|
snapshot.imu_sample_count ==
|
||||||
|
(kDefaultMotionEnabled ? 3 : 0),
|
||||||
|
"slot 0 motion chord changed slot 1 motion state");
|
||||||
|
|
||||||
|
input.gamepad = {};
|
||||||
|
platform_on_controller_data(&slot_zero, &input);
|
||||||
|
input.gamepad.accel[0] = 8192;
|
||||||
|
input.gamepad.dpad = kMotionHotkeyDpadMask;
|
||||||
|
input.gamepad.buttons = kMotionHotkeyButtonMask;
|
||||||
|
input.gamepad.misc_buttons = kMotionHotkeyMiscMask;
|
||||||
|
platform_on_controller_data(&slot_zero, &input);
|
||||||
|
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
|
||||||
|
snapshot.imu_sample_count ==
|
||||||
|
(kDefaultMotionEnabled ? 3 : 0),
|
||||||
|
"released motion chord did not re-arm or restore motion");
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(g_slots[0].motion_enabled == kDefaultMotionEnabled &&
|
||||||
|
slot_zero.rumble_calls == 2,
|
||||||
|
"second motion chord did not restore configured state");
|
||||||
|
|
||||||
|
platform_on_device_disconnected(&slot_zero);
|
||||||
|
uni_hid_device_t replacement = device(0);
|
||||||
|
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS &&
|
||||||
|
g_slots[0].motion_enabled == kDefaultMotionEnabled &&
|
||||||
|
!g_slots[0].motion_hotkey_latched &&
|
||||||
|
!g_slots[0].feedback_pending,
|
||||||
|
"disconnect did not reset slot 0 motion hotkey state");
|
||||||
|
}
|
||||||
|
|
||||||
void test_flash_core_start_contract() {
|
void test_flash_core_start_contract() {
|
||||||
bluepad32_input_backend_init();
|
bluepad32_input_backend_init();
|
||||||
flash_core_init_result = false;
|
flash_core_init_result = false;
|
||||||
|
|
@ -851,6 +937,8 @@ int main(int argc, char** argv) {
|
||||||
test_slot_lighting();
|
test_slot_lighting();
|
||||||
} else if (scenario == "abxy-hotkey") {
|
} else if (scenario == "abxy-hotkey") {
|
||||||
test_abxy_hotkey();
|
test_abxy_hotkey();
|
||||||
|
} else if (scenario == "motion-hotkey") {
|
||||||
|
test_motion_hotkey();
|
||||||
} else if (scenario == "flash-core-start") {
|
} else if (scenario == "flash-core-start") {
|
||||||
test_flash_core_start_contract();
|
test_flash_core_start_contract();
|
||||||
} else if (scenario == "flash-core-failure") {
|
} else if (scenario == "flash-core-failure") {
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ struct uni_hid_device_t {
|
||||||
int rumble_calls;
|
int rumble_calls;
|
||||||
uint8_t last_high;
|
uint8_t last_high;
|
||||||
uint8_t last_low;
|
uint8_t last_low;
|
||||||
|
uint16_t last_rumble_duration_ms;
|
||||||
int lightbar_calls;
|
int lightbar_calls;
|
||||||
uint8_t lightbar_red;
|
uint8_t lightbar_red;
|
||||||
uint8_t lightbar_green;
|
uint8_t lightbar_green;
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
||||||
"pairing-policy",
|
"pairing-policy",
|
||||||
"slot-lighting",
|
"slot-lighting",
|
||||||
"abxy-hotkey",
|
"abxy-hotkey",
|
||||||
|
"motion-hotkey",
|
||||||
"flash-core-start",
|
"flash-core-start",
|
||||||
"flash-core-failure",
|
"flash-core-failure",
|
||||||
):
|
):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue