Add Wii MotionPlus support and calibrated Nunchuk controls
This commit is contained in:
parent
46f42a26dc
commit
fa38123873
27 changed files with 3003 additions and 57 deletions
|
|
@ -12,6 +12,7 @@
|
|||
#include "parser/uni_hid_parser_switch2.h"
|
||||
#include "parser/uni_switch2_haptics.h"
|
||||
#include "parser/uni_switch2_pairing.h"
|
||||
#include "parser/uni_hid_parser_wii.h"
|
||||
#include "platform/pico/controller_color_config.h"
|
||||
#include "input/switch2_wake.h"
|
||||
#include "pico/critical_section.h"
|
||||
|
|
@ -103,6 +104,13 @@ struct LocalRumbleEvent {
|
|||
};
|
||||
std::vector<LocalRumbleEvent> local_rumble_events;
|
||||
|
||||
struct WiiModeEvent {
|
||||
uni_hid_device_t* device;
|
||||
wii_mode_t mode;
|
||||
};
|
||||
std::vector<WiiModeEvent> wii_mode_events;
|
||||
bool wii_dispatch_active = false;
|
||||
|
||||
struct CoreStopped {};
|
||||
|
||||
|
||||
|
|
@ -683,6 +691,19 @@ void tracked_state_lock_exit(critical_section_t* lock) {
|
|||
#undef critical_section_enter_blocking
|
||||
#undef critical_section_exit
|
||||
|
||||
extern "C" void uni_hid_parser_wii_set_mode(
|
||||
uni_hid_device_t* device, wii_mode_t mode) {
|
||||
require(wii_dispatch_active && state_lock_depth == 0,
|
||||
"Wii parser mode changes must run in the Bluetooth timer without the state lock");
|
||||
wii_mode_events.push_back({device, mode});
|
||||
device->controller_subtype = mode == WII_MODE_VERTICAL
|
||||
? CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL
|
||||
: CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL;
|
||||
// The real parser's report reconfiguration can synchronously announce ready.
|
||||
require(platform_on_device_ready(device) == UNI_ERROR_SUCCESS,
|
||||
"Wii mode reconfiguration must retain the ready connection");
|
||||
}
|
||||
|
||||
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
||||
namespace {
|
||||
std::vector<btstack_timer_source_t*> native_timers;
|
||||
|
|
@ -3493,9 +3514,9 @@ void test_rejections() {
|
|||
Bluepad32ControllerLayout layout;
|
||||
};
|
||||
const LayoutCase layouts[] = {
|
||||
{CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL, Bluepad32ControllerLayout::kWiiRemote},
|
||||
{CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL, Bluepad32ControllerLayout::kWiiRemote},
|
||||
{CONTROLLER_SUBTYPE_WIIMOTE_ACCEL, Bluepad32ControllerLayout::kWiiRemote},
|
||||
{CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL, Bluepad32ControllerLayout::kWiiHorizontal},
|
||||
{CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL, Bluepad32ControllerLayout::kWiiVertical},
|
||||
{CONTROLLER_SUBTYPE_WIIMOTE_ACCEL, Bluepad32ControllerLayout::kWiiHorizontal},
|
||||
{CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK, Bluepad32ControllerLayout::kWiiNunchuk},
|
||||
{CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL, Bluepad32ControllerLayout::kWiiNunchuk},
|
||||
{CONTROLLER_SUBTYPE_WII_CLASSIC, Bluepad32ControllerLayout::kUnspecified},
|
||||
|
|
@ -5415,6 +5436,222 @@ void test_native_second_slot_selection() {
|
|||
}
|
||||
#endif
|
||||
|
||||
uni_hid_device_t wii_device(int index) {
|
||||
auto result = device(index, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
result.vendor_id = 0x057e;
|
||||
result.product_id = 0x0330;
|
||||
result.controller_type = CONTROLLER_TYPE_WiiController;
|
||||
result.controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL;
|
||||
return result;
|
||||
}
|
||||
|
||||
void dispatch_wii_requests() {
|
||||
wii_dispatch_active = true;
|
||||
now_ms += g_rumble_timer.timeout_ms;
|
||||
g_rumble_timer.handler(&g_rumble_timer);
|
||||
wii_dispatch_active = false;
|
||||
}
|
||||
|
||||
void test_wii_orientation() {
|
||||
start_pairing_backend();
|
||||
initialize_runtime_profile_storage();
|
||||
auto remote = wii_device(0);
|
||||
auto ordinary = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
ready_switch2(remote);
|
||||
ready_switch2(ordinary);
|
||||
const auto identity = slot_snapshot(0).identity;
|
||||
const auto ordinary_identity = slot_snapshot(1).identity;
|
||||
configure_gesture_profile(identity, 2, 37);
|
||||
configure_gesture_profile(ordinary_identity, 5, 63);
|
||||
controller_profile_runtime_reset();
|
||||
for (uint8_t index = 0; index < 2; ++index) {
|
||||
controller_profile_runtime_transform(
|
||||
index, slot_snapshot(index), now_ms, AdapterUsbMode::kXInput);
|
||||
}
|
||||
uni_controller_t report{};
|
||||
report.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
report.gamepad.buttons = BUTTON_A;
|
||||
++now_ms;
|
||||
platform_on_controller_data(&remote, &report);
|
||||
platform_on_controller_data(&ordinary, &report);
|
||||
for (uint8_t index = 0; index < 2; ++index) {
|
||||
require(controller_profile_runtime_transform(
|
||||
index, slot_snapshot(index), now_ms, AdapterUsbMode::kXInput)
|
||||
.state.button_system,
|
||||
"both controllers need active toggle macros before changing only Wii orientation");
|
||||
}
|
||||
report.gamepad.buttons = BUTTON_Y;
|
||||
report.gamepad.dpad = DPAD_UP;
|
||||
report.gamepad.axis_x = 511;
|
||||
report.gamepad.brake = 1023;
|
||||
platform_on_controller_data(&remote, &report);
|
||||
const auto before = slot_snapshot(0);
|
||||
const auto ordinary_before = slot_snapshot(1);
|
||||
require(bluepad32_input_backend_capture_start(
|
||||
0, before.connection_generation, CaptureOptions{}),
|
||||
"orientation transition must have an active recorder to retire");
|
||||
bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{91, 101});
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
0, before.connection_generation, 8, ControllerProfileConfirmationPolicy::kRumbleAndLed);
|
||||
const unsigned writes_before = profile_write_attempts;
|
||||
const size_t observations_before = observed_profile_identity_count;
|
||||
require(bluepad32_input_backend_set_wii_orientation(
|
||||
identity, before.connection_generation, true),
|
||||
"live standalone Wii must accept a vertical request");
|
||||
Bluepad32PlaytestSnapshot playtest{};
|
||||
bluepad32_input_backend_playtest_snapshot(0, &playtest);
|
||||
require(wii_mode_events.empty() &&
|
||||
playtest.controller_layout == Bluepad32ControllerLayout::kWiiHorizontal &&
|
||||
playtest.connection_generation == before.connection_generation &&
|
||||
playtest.state.button_north && playtest.state.dpad_up,
|
||||
"enqueue must not call the parser or claim a new mapping on the management core");
|
||||
dispatch_wii_requests();
|
||||
bluepad32_input_backend_playtest_snapshot(0, &playtest);
|
||||
require(wii_mode_events.size() == 1 && wii_mode_events[0].device == &remote &&
|
||||
wii_mode_events[0].mode == WII_MODE_VERTICAL &&
|
||||
playtest.active &&
|
||||
playtest.controller_layout == Bluepad32ControllerLayout::kWiiVertical &&
|
||||
playtest.connection_generation == before.connection_generation + 1u &&
|
||||
controller_identity_equal(playtest.identity, identity) &&
|
||||
playtest.physical_button_mask == 0 && !playtest.state.button_north &&
|
||||
!playtest.state.dpad_up && playtest.state.left_stick_x == 0 &&
|
||||
playtest.state.left_trigger == 0 && playtest.state.motion_sample_count == 0,
|
||||
"timer dispatch must atomically retire old input and publish the new live mapping epoch");
|
||||
Bluepad32CaptureSnapshot capture{};
|
||||
require(bluepad32_input_backend_capture_page(0, 0, &capture) &&
|
||||
capture.state == CaptureState::kDisconnected &&
|
||||
!controller_profile_runtime_transform(
|
||||
0, slot_snapshot(0), now_ms, AdapterUsbMode::kXInput).state.button_system &&
|
||||
controller_profile_runtime_transform(
|
||||
1, slot_snapshot(1), now_ms, AdapterUsbMode::kXInput).state.button_system &&
|
||||
slot_snapshot(1).connection_generation == ordinary_before.connection_generation &&
|
||||
remote.last_high == 0 && remote.last_low == 0 &&
|
||||
!bluepad32_input_backend_toggle_motion(0, before.connection_generation),
|
||||
"mapping changes must cancel old macro/capture/hotkey/feedback epochs without disturbing peers");
|
||||
report.gamepad = {};
|
||||
report.gamepad.buttons = BUTTON_B;
|
||||
platform_on_controller_data(&remote, &report);
|
||||
require(slot_snapshot(0).state.button_east && !slot_snapshot(0).state.button_north,
|
||||
"fresh input must use the new mapping without reviving old controls");
|
||||
const uint32_t vertical_generation = playtest.connection_generation;
|
||||
require(bluepad32_input_backend_set_wii_orientation(
|
||||
identity, vertical_generation, false),
|
||||
"current vertical mapping must be reversible without reconnecting");
|
||||
dispatch_wii_requests();
|
||||
bluepad32_input_backend_playtest_snapshot(0, &playtest);
|
||||
require(playtest.controller_layout == Bluepad32ControllerLayout::kWiiHorizontal &&
|
||||
playtest.connection_generation == vertical_generation + 1u &&
|
||||
wii_mode_events.size() == 2 && wii_mode_events[1].mode == WII_MODE_HORIZONTAL,
|
||||
"horizontal selection must return the real parser and metadata to horizontal");
|
||||
const uint32_t horizontal_generation = playtest.connection_generation;
|
||||
require(bluepad32_input_backend_set_wii_orientation(
|
||||
identity, horizontal_generation, false),
|
||||
"reselection must still reach a parser with a potentially deferred opposite choice");
|
||||
dispatch_wii_requests();
|
||||
require(wii_mode_events.size() == 3 && wii_mode_events[2].mode == WII_MODE_HORIZONTAL &&
|
||||
slot_snapshot(0).connection_generation == horizontal_generation + 1u &&
|
||||
profile_write_attempts == writes_before &&
|
||||
observed_profile_identity_count == observations_before &&
|
||||
device_disconnect_calls == 0,
|
||||
"reselection and orientation roundtrip must preserve profiles, flash and physical connections");
|
||||
require_active_profile(identity, 2, 37);
|
||||
require_active_profile(ordinary_identity, 5, 63);
|
||||
}
|
||||
|
||||
void test_wii_orientation_races() {
|
||||
auto remote = wii_device(0);
|
||||
const auto identity = identity_for_device(&remote);
|
||||
require(!bluepad32_input_backend_set_wii_orientation(identity, 0, true),
|
||||
"orientation requests must reject an uninitialized backend");
|
||||
start_pairing_backend();
|
||||
platform_on_device_connected(&remote);
|
||||
require(!bluepad32_input_backend_set_wii_orientation(
|
||||
identity, slot_snapshot(0).connection_generation, true),
|
||||
"orientation requests must reject connections that are not ready");
|
||||
require(platform_on_device_ready(&remote) == UNI_ERROR_SUCCESS,
|
||||
"Wii race fixture must become ready");
|
||||
const auto before = slot_snapshot(0);
|
||||
auto ordinary = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
ordinary.controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL;
|
||||
ready_switch2(ordinary);
|
||||
auto unstable = wii_device(2);
|
||||
unstable.conn.protocol = UNI_BT_CONN_PROTOCOL_NONE;
|
||||
gap_connection_types[unstable.conn.handle] = GAP_CONNECTION_INVALID;
|
||||
ready_switch2(unstable);
|
||||
auto wrong_identity = identity;
|
||||
wrong_identity.product_id ^= 1u;
|
||||
require(!bluepad32_input_backend_set_wii_orientation(
|
||||
identity, before.connection_generation - 1u, true) &&
|
||||
!bluepad32_input_backend_set_wii_orientation(
|
||||
wrong_identity, before.connection_generation, true) &&
|
||||
!bluepad32_input_backend_set_wii_orientation(
|
||||
slot_snapshot(1).identity, slot_snapshot(1).connection_generation, true) &&
|
||||
!bluepad32_input_backend_set_wii_orientation(
|
||||
slot_snapshot(2).identity, slot_snapshot(2).connection_generation, true),
|
||||
"stale generation, wrong owner, non-Wii and global fallback must not target a parser");
|
||||
const uni_controller_subtype_t extensions[] = {
|
||||
CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK,
|
||||
CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL,
|
||||
CONTROLLER_SUBTYPE_WII_CLASSIC,
|
||||
};
|
||||
for (auto subtype : extensions) {
|
||||
remote.controller_subtype = subtype;
|
||||
require(!bluepad32_input_backend_set_wii_orientation(
|
||||
identity, before.connection_generation, false),
|
||||
"Nunchuk and other extension mappings cannot be changed by standalone orientation");
|
||||
}
|
||||
remote.controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL;
|
||||
require(bluepad32_input_backend_set_wii_orientation(
|
||||
identity, before.connection_generation, true),
|
||||
"race fixture must enqueue while still standalone");
|
||||
remote.controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK;
|
||||
dispatch_wii_requests();
|
||||
Bluepad32PlaytestSnapshot playtest{};
|
||||
bluepad32_input_backend_playtest_snapshot(0, &playtest);
|
||||
require(wii_mode_events.empty() &&
|
||||
playtest.controller_layout == Bluepad32ControllerLayout::kWiiNunchuk &&
|
||||
playtest.connection_generation == before.connection_generation,
|
||||
"extension attachment before dispatch must reject the queued request without changing topology");
|
||||
remote.controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL;
|
||||
dispatch_wii_requests();
|
||||
require(wii_mode_events.empty(),
|
||||
"a request rejected during attachment must not revive when the Nunchuk detaches");
|
||||
require(bluepad32_input_backend_set_wii_orientation(
|
||||
identity, before.connection_generation, true),
|
||||
"standalone connection must accept another request after detach");
|
||||
const WiiOrientationRequest stale = g_slots[0].pending_wii_orientation;
|
||||
platform_on_device_disconnected(&remote);
|
||||
remote = wii_device(0);
|
||||
ready_switch2(remote);
|
||||
dispatch_wii_requests();
|
||||
require(wii_mode_events.empty() &&
|
||||
!bluepad32_input_backend_set_wii_orientation(
|
||||
identity, before.connection_generation, true),
|
||||
"physical object reuse with the same identity must discard old pending requests and reject its old epoch");
|
||||
// Exercise the dispatch guard even if an already accepted old envelope
|
||||
// survives a future mailbox refactor.
|
||||
g_slots[0].pending_wii_orientation = stale;
|
||||
g_slots[0].wii_orientation_pending = true;
|
||||
dispatch_wii_requests();
|
||||
g_slots[0].pending_wii_orientation = {
|
||||
wrong_identity, slot_snapshot(0).connection_generation, true};
|
||||
g_slots[0].wii_orientation_pending = true;
|
||||
dispatch_wii_requests();
|
||||
require(wii_mode_events.empty() &&
|
||||
slot_snapshot(0).connection_generation == before.connection_generation + 1u,
|
||||
"dispatch must independently revalidate both connection generation and identity");
|
||||
const auto replacement = slot_snapshot(0);
|
||||
require(bluepad32_input_backend_set_wii_orientation(
|
||||
replacement.identity, replacement.connection_generation, true) &&
|
||||
bluepad32_input_backend_set_wii_orientation(
|
||||
replacement.identity, replacement.connection_generation, false),
|
||||
"rapid standalone choices must coalesce before dispatch");
|
||||
dispatch_wii_requests();
|
||||
require(wii_mode_events.size() == 1 && wii_mode_events[0].mode == WII_MODE_HORIZONTAL &&
|
||||
slot_snapshot(0).connection_generation == replacement.connection_generation + 1u,
|
||||
"coalescing back to the current mapping must not apply an obsolete intermediate choice");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
|
@ -5430,6 +5667,14 @@ int main(int argc, char** argv) {
|
|||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (scenario == "wii-orientation") {
|
||||
test_wii_orientation();
|
||||
return 0;
|
||||
}
|
||||
if (scenario == "wii-orientation-races") {
|
||||
test_wii_orientation_races();
|
||||
return 0;
|
||||
}
|
||||
if (scenario == "switch2-hd-pro") {
|
||||
test_switch2_hd_pro();
|
||||
} else if (scenario == "switch2-gesture-timing") {
|
||||
|
|
|
|||
|
|
@ -75,14 +75,6 @@ void set_accel_calibration(
|
|||
}
|
||||
}
|
||||
|
||||
void test_wii_accelerometer() {
|
||||
int32_t output[3]{};
|
||||
uni_imu_normalize_wii_accel(100, -50, 25, output);
|
||||
expect(output[0] == -8192 && output[1] == 2048 &&
|
||||
output[2] == -4096,
|
||||
"Wii accelerometer scale or SDL axis mapping is wrong");
|
||||
}
|
||||
|
||||
void test_zcm1_calibration_and_normalization() {
|
||||
constexpr auto model = UNI_PSMOVE_IMU_MODEL_ZCM1;
|
||||
std::array<uint8_t, UNI_PSMOVE_ZCM1_CALIBRATION_SIZE> blob{};
|
||||
|
|
@ -200,7 +192,6 @@ void test_uncalibrated_psmove_is_suppressed() {
|
|||
} // namespace
|
||||
|
||||
int main() {
|
||||
test_wii_accelerometer();
|
||||
test_zcm1_calibration_and_normalization();
|
||||
test_zcm2_calibration_and_normalization();
|
||||
test_uncalibrated_psmove_is_suppressed();
|
||||
|
|
|
|||
19
tests/bluepad32_native_stubs/parser/uni_hid_parser_wii.h
Normal file
19
tests/bluepad32_native_stubs/parser/uni_hid_parser_wii.h
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include <uni.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum wii_flags {
|
||||
WII_MODE_HORIZONTAL = 0,
|
||||
WII_MODE_VERTICAL = 1,
|
||||
WII_MODE_ACCEL = 2,
|
||||
} wii_mode_t;
|
||||
|
||||
void uni_hid_parser_wii_set_mode(uni_hid_device_t* device, wii_mode_t mode);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -83,6 +83,11 @@ enum {
|
|||
MISC_BUTTON_CAPTURE = 1 << 3,
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
CONTROLLER_TYPE_UnknownController = 0,
|
||||
CONTROLLER_TYPE_WiiController = 35,
|
||||
} uni_controller_type_t;
|
||||
|
||||
typedef enum {
|
||||
CONTROLLER_SUBTYPE_NONE = 0,
|
||||
CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL,
|
||||
|
|
@ -163,6 +168,7 @@ inline uint8_t uni_circular_buffer_is_empty(const uni_circular_buffer_t* buffer)
|
|||
struct uni_hid_device_s {
|
||||
uint16_t vendor_id;
|
||||
uint16_t product_id;
|
||||
uni_controller_type_t controller_type;
|
||||
uni_controller_subtype_t controller_subtype;
|
||||
uni_bt_conn_t conn;
|
||||
int idx;
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
subprocess.run(command, check=True, cwd=root)
|
||||
subprocess.run([str(executable), "xbox-rumble"], check=True, cwd=root)
|
||||
for scenario in (
|
||||
"wii-orientation",
|
||||
"wii-orientation-races",
|
||||
"switch2-individual-core-start",
|
||||
"switch2-individual-forward",
|
||||
"switch2-individual-reverse",
|
||||
|
|
|
|||
|
|
@ -2396,6 +2396,9 @@ def test_playtest_layout_and_extra_inputs_preserve_legacy_firmware() -> None:
|
|||
assert current.to_json_object()["buttons"] == ["south", "dpad_up", "dpad_right"]
|
||||
assert parse(payload[:55], 3) == replace(current, layout=None)
|
||||
assert parse(payload[:54], 2) == replace(current, extra_buttons=0, layout=None)
|
||||
assert parse(payload, 5) == current
|
||||
assert parse(payload[:55] + b"\x06", 5).layout == "wii-horizontal"
|
||||
assert parse(payload[:55] + b"\x07", 5).layout == "wii-vertical"
|
||||
assert parse(payload[:55] + b"\x00", 4).layout is None
|
||||
for data, schema in (
|
||||
(payload[:54] + b"\x80", 3),
|
||||
|
|
@ -2406,7 +2409,9 @@ def test_playtest_layout_and_extra_inputs_preserve_legacy_firmware() -> None:
|
|||
(payload, 3),
|
||||
(payload[:55], 4),
|
||||
(payload + b"\x00", 4),
|
||||
(payload, 5),
|
||||
(payload[:55] + b"\x08", 5),
|
||||
(payload[:55], 5),
|
||||
(payload, 6),
|
||||
):
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
parse(data, schema)
|
||||
|
|
|
|||
|
|
@ -436,7 +436,14 @@ def test_wii_pid_does_not_claim_a_remote_or_extension_without_live_metadata(
|
|||
status, listing = request_json(f"{base_url}/api/profiles")
|
||||
assert status == 200
|
||||
assert listing["identities"][1]["controller"]["layout"] == "generic"
|
||||
for code, expected in ((0, "generic"), (4, "wii-remote"), (5, "wii-nunchuk"), (0, "generic")):
|
||||
for code, expected in (
|
||||
(0, "generic"),
|
||||
(4, "wii-remote"),
|
||||
(5, "wii-nunchuk"),
|
||||
(6, "wii-horizontal"),
|
||||
(7, "wii-vertical"),
|
||||
(0, "generic"),
|
||||
):
|
||||
device.playtest_layout = code
|
||||
status, sample = request_json(f"{base_url}/api/profiles/0/1/playtest")
|
||||
assert status == 200
|
||||
|
|
@ -445,7 +452,6 @@ def test_wii_pid_does_not_claim_a_remote_or_extension_without_live_metadata(
|
|||
assert set(sample["source_controls"]).isdisjoint(config_manager.EXTRA_BUTTONS)
|
||||
assert ("left_shoulder" in sample["source_controls"]) == (code == 5)
|
||||
|
||||
|
||||
def test_editor_reads_writes_and_activates_profiles_atomically(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
|
@ -859,4 +865,77 @@ def test_recorder_accepts_first_connection_generation_zero(
|
|||
},
|
||||
)
|
||||
assert status == 200 and stopped["state_name"] == "stopped"
|
||||
assert stopped["steps"][0]["duration_ms"] == 100
|
||||
|
||||
|
||||
|
||||
def test_wii_orientation_endpoint_validation_and_failures(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
with running_server(monkeypatch, device) as (base_url, token):
|
||||
# Missing token
|
||||
status, _ = request_json(
|
||||
f"{base_url}/api/identities/1/wii-orientation",
|
||||
method="POST",
|
||||
value={"orientation": "horizontal", "connection_generation": 1},
|
||||
)
|
||||
assert status == 403
|
||||
|
||||
# Out of bounds identity index
|
||||
status, _ = request_json(
|
||||
f"{base_url}/api/identities/99/wii-orientation",
|
||||
method="POST",
|
||||
token=token,
|
||||
value={"orientation": "horizontal", "connection_generation": 1},
|
||||
)
|
||||
assert status == 400
|
||||
|
||||
# Invalid / missing fields
|
||||
for invalid_body in (
|
||||
{},
|
||||
{"orientation": "diagonal", "connection_generation": 1},
|
||||
{"orientation": "horizontal"},
|
||||
{"connection_generation": 1},
|
||||
{"orientation": "horizontal", "connection_generation": -1},
|
||||
{"orientation": "horizontal", "connection_generation": "1"},
|
||||
{"orientation": 123, "connection_generation": 1},
|
||||
):
|
||||
status, _ = request_json(
|
||||
f"{base_url}/api/identities/1/wii-orientation",
|
||||
method="POST",
|
||||
token=token,
|
||||
value=invalid_body,
|
||||
)
|
||||
assert status == 400
|
||||
|
||||
# ConfigManagerError from config_manager
|
||||
def failing_config(*args: Any, **kwargs: Any) -> None:
|
||||
raise config_manager.ConfigManagerError("firmware rejected orientation")
|
||||
|
||||
monkeypatch.setattr(
|
||||
config_manager, "set_wii_orientation", failing_config
|
||||
)
|
||||
status, err = request_json(
|
||||
f"{base_url}/api/identities/1/wii-orientation",
|
||||
method="POST",
|
||||
token=token,
|
||||
value={"orientation": "horizontal", "connection_generation": 1},
|
||||
)
|
||||
assert status == 400
|
||||
assert "error" in err
|
||||
|
||||
# USB error from config_manager
|
||||
def usb_failing_config(*args: Any, **kwargs: Any) -> None:
|
||||
raise usb.core.USBError("USB pipe error")
|
||||
|
||||
monkeypatch.setattr(
|
||||
config_manager, "set_wii_orientation", usb_failing_config
|
||||
)
|
||||
status, err = request_json(
|
||||
f"{base_url}/api/identities/1/wii-orientation",
|
||||
method="POST",
|
||||
token=token,
|
||||
value={"orientation": "horizontal", "connection_generation": 1},
|
||||
)
|
||||
assert status == 503
|
||||
assert "error" in err
|
||||
|
|
|
|||
48
tests/test_wii_parser_native.py
Normal file
48
tests/test_wii_parser_native.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
||||
|
||||
from prepare_bluepad32 import prepare_bluepad32
|
||||
|
||||
|
||||
def test_wii_parser_native_motion_and_lifecycle(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("cc") or shutil.which("gcc")
|
||||
assert compiler is not None, "a host C compiler is required"
|
||||
prepared = prepare_bluepad32(
|
||||
root / "external" / "bluepad32",
|
||||
root / "patches" / "bluepad32-sdl3-imu.patch",
|
||||
tmp_path / "bluepad32-src",
|
||||
)
|
||||
component = prepared / "src" / "components" / "bluepad32"
|
||||
executable = tmp_path / "wii_parser_native_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=gnu11",
|
||||
"-O1",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
"-DENABLE_BLE",
|
||||
"-DENABLE_CLASSIC",
|
||||
f"-I{root / 'tests' / 'switch_parser_native_stubs'}",
|
||||
f"-I{root / 'bluepad32_config'}",
|
||||
f"-I{component / 'include'}",
|
||||
str(root / "tests" / "wii_parser_native_test.c"),
|
||||
str(component / "parser" / "uni_hid_parser_wii.c"),
|
||||
str(component / "controller" / "uni_gamepad.c"),
|
||||
"-Wl,--gc-sections",
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -567,7 +567,14 @@ void test_profile_vendor_requests() {
|
|||
control_payload[kResponseHeaderSize + 55] == 3,
|
||||
"profile playtest response lost live controller state");
|
||||
current_playtest[2].controller_layout =
|
||||
static_cast<Bluepad32ControllerLayout>(6);
|
||||
Bluepad32ControllerLayout::kWiiVertical;
|
||||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload[10] == 5 &&
|
||||
control_payload[kResponseHeaderSize + 55] == 7,
|
||||
"Wii orientation must be available through schema-5 playtest");
|
||||
current_playtest[2].controller_layout =
|
||||
static_cast<Bluepad32ControllerLayout>(8);
|
||||
uint8_t invalid_playtest[kMaximumResponseSize]{};
|
||||
require(encode_profile_playtest(
|
||||
2, current_playtest[2], invalid_playtest, sizeof(invalid_playtest)) == 0,
|
||||
|
|
@ -629,6 +636,25 @@ void test_profile_vendor_requests() {
|
|||
require(identify_requested,
|
||||
"controller Identify request was not dispatched");
|
||||
|
||||
std::vector<uint8_t> orientation = identify;
|
||||
orientation.resize(CONTROLLER_IDENTITY_ENCODED_SIZE + 5);
|
||||
write_u32(&orientation, CONTROLLER_IDENTITY_ENCODED_SIZE, 0x11223344);
|
||||
orientation.back() = 1;
|
||||
perform_out(Operation::kWiiOrientation, orientation);
|
||||
orientation.back() = 2;
|
||||
next_out_payload = make_request(Operation::kWiiOrientation, orientation);
|
||||
request = setup_request(Operation::kWiiOrientation, TUSB_DIR_OUT,
|
||||
static_cast<uint16_t>(next_out_payload.size()));
|
||||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
!usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_ACK, &request),
|
||||
"invalid Wii orientation was treated as vertical");
|
||||
request.wLength--;
|
||||
require(!usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request),
|
||||
"truncated Wii orientation request was accepted");
|
||||
|
||||
current_profile_transaction = {};
|
||||
current_profile_transaction.metadata.state =
|
||||
ProfileServiceState::kReady;
|
||||
|
|
@ -1131,6 +1157,11 @@ bool bluepad32_input_backend_identify(
|
|||
return true;
|
||||
}
|
||||
|
||||
bool bluepad32_input_backend_set_wii_orientation(
|
||||
const ControllerIdentity&, uint32_t, bool) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_reset(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t selected_profile) {
|
||||
|
|
|
|||
913
tests/wii_parser_native_test.c
Normal file
913
tests/wii_parser_native_test.c
Normal file
|
|
@ -0,0 +1,913 @@
|
|||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "parser/uni_hid_parser_wii.h"
|
||||
#include "uni_hid_device.h"
|
||||
|
||||
// The real staged parser and gamepad definitions are linked. Only transport and
|
||||
// the ready notification are substituted; no parser-private state is inspected.
|
||||
// Wire fixtures follow WiiBrew's Wiimote and Wii Motion Plus protocol pages.
|
||||
typedef struct {
|
||||
uint16_t len;
|
||||
uint8_t bytes[32];
|
||||
} transaction_t;
|
||||
|
||||
typedef enum { EXT_NONE, EXT_NUNCHUK, EXT_WII_U_PRO } extension_t;
|
||||
|
||||
typedef struct {
|
||||
uni_hid_device_t device;
|
||||
transaction_t sent[128];
|
||||
unsigned sent_count;
|
||||
unsigned cursor;
|
||||
unsigned ready_count;
|
||||
extension_t extension;
|
||||
bool motionplus;
|
||||
bool mp_initialized;
|
||||
bool mp_active;
|
||||
bool status_before_activation_ack;
|
||||
bool status_after_activation_ack;
|
||||
bool deactivation_status_before_ack;
|
||||
bool deactivation_status_after_ack;
|
||||
unsigned initializing_extension_reads;
|
||||
unsigned transient_extension_read_errors;
|
||||
uint8_t extension_identity[6];
|
||||
bool fail_accel_reads;
|
||||
uint8_t connection_buttons;
|
||||
uint8_t activation_mode;
|
||||
uint8_t report_mode;
|
||||
uint32_t fail_read_address;
|
||||
uint32_t fail_write_address;
|
||||
uint8_t accel_calibration[20];
|
||||
uint8_t mp_calibration[32];
|
||||
uint8_t nunchuk_calibration[16];
|
||||
} fixture_t;
|
||||
|
||||
static fixture_t f;
|
||||
|
||||
void uni_hid_device_send_intr_report(uni_hid_device_t* d, const uint8_t* report, uint16_t len) {
|
||||
assert(d == &f.device);
|
||||
assert(f.sent_count < 128 && len >= 2 && len <= sizeof(f.sent[0].bytes));
|
||||
assert(report[0] == 0xa2);
|
||||
transaction_t* transaction = &f.sent[f.sent_count++];
|
||||
transaction->len = len;
|
||||
memcpy(transaction->bytes, report, len);
|
||||
}
|
||||
|
||||
bool uni_hid_device_set_ready_complete(uni_hid_device_t* d) {
|
||||
assert(d == &f.device);
|
||||
++f.ready_count;
|
||||
d->conn.state = UNI_BT_CONN_STATE_DEVICE_READY;
|
||||
return true;
|
||||
}
|
||||
|
||||
void uni_log(const char* fmt, ...) { (void)fmt; }
|
||||
void printf_hexdump(const void* data, int len) { (void)data; (void)len; }
|
||||
|
||||
static void put_be16(uint8_t* output, uint16_t value) {
|
||||
output[0] = value >> 8;
|
||||
output[1] = value;
|
||||
}
|
||||
|
||||
static void put_le16(uint8_t* output, uint16_t value) {
|
||||
output[0] = value;
|
||||
output[1] = value >> 8;
|
||||
}
|
||||
|
||||
static void pack_accel_calibration(uint8_t* output, const uint16_t values[3]) {
|
||||
output[3] = 0;
|
||||
for (unsigned axis = 0; axis < 3; ++axis) {
|
||||
output[axis] = values[axis] >> 2;
|
||||
output[3] |= (values[axis] & 3) << (4 - 2 * axis);
|
||||
}
|
||||
}
|
||||
|
||||
static void update_mp_checksum(void) {
|
||||
// CRC32 covers fast[0..13] followed by slow[0..13], not the CRC slots.
|
||||
uint32_t crc = UINT32_MAX;
|
||||
for (unsigned block = 0; block < 2; ++block) {
|
||||
for (unsigned byte = 0; byte < 14; ++byte) {
|
||||
crc ^= f.mp_calibration[block * 16 + byte];
|
||||
for (unsigned bit = 0; bit < 8; ++bit)
|
||||
crc = (crc >> 1) ^ ((crc & 1) ? UINT32_C(0xedb88320) : 0);
|
||||
}
|
||||
}
|
||||
crc ^= UINT32_MAX;
|
||||
put_be16(&f.mp_calibration[14], crc >> 16);
|
||||
put_be16(&f.mp_calibration[30], crc);
|
||||
}
|
||||
|
||||
static void set_nunchuk_stick_calibration(const uint8_t values[6]) {
|
||||
memcpy(f.nunchuk_calibration + 8, values, 6);
|
||||
uint8_t checksum = 0x55;
|
||||
for (unsigned i = 0; i < 14; ++i)
|
||||
checksum += f.nunchuk_calibration[i];
|
||||
f.nunchuk_calibration[14] = checksum;
|
||||
f.nunchuk_calibration[15] = checksum + 0x55;
|
||||
}
|
||||
|
||||
static void reset_fixture(uint16_t product_id, bool motionplus, extension_t extension) {
|
||||
memset(&f, 0, sizeof(f));
|
||||
f.device.vendor_id = 0x057e;
|
||||
f.device.product_id = product_id;
|
||||
f.device.conn.connected = true;
|
||||
f.device.conn.interrupt_cid = 0x40;
|
||||
f.extension = extension;
|
||||
f.motionplus = motionplus;
|
||||
f.fail_read_address = UINT32_MAX;
|
||||
f.fail_write_address = UINT32_MAX;
|
||||
const uint8_t nunchuk_identity[6] = {0, 0, 0xa4, 0x20, 0, 0};
|
||||
memcpy(f.extension_identity, nunchuk_identity, sizeof(nunchuk_identity));
|
||||
set_nunchuk_stick_calibration((const uint8_t[]){224, 32, 128, 224, 32, 128});
|
||||
|
||||
// Non-default centers, unequal spans, and nonzero packed low bits catch
|
||||
// nominal 0x200/100-count calibration and high-byte-only decoding.
|
||||
const uint16_t zero[3] = {510, 506, 514};
|
||||
const uint16_t one_g[3] = {614, 634, 594};
|
||||
pack_accel_calibration(f.accel_calibration, zero);
|
||||
pack_accel_calibration(&f.accel_calibration[4], one_g);
|
||||
uint8_t checksum = 0x55;
|
||||
for (unsigned byte = 0; byte < 9; ++byte)
|
||||
checksum += f.accel_calibration[byte];
|
||||
f.accel_calibration[9] = checksum;
|
||||
memcpy(&f.accel_calibration[10], f.accel_calibration, 10);
|
||||
|
||||
// MP words have 16-bit precision; reports have 14 bits. Factory yaw and
|
||||
// pitch spans are negative, whereas roll is positive. Fast and slow also
|
||||
// deliberately have different centers and sensitivities on every axis.
|
||||
const uint16_t zero_fast[3] = {32000, 31600, 31200};
|
||||
const uint16_t scale_fast[3] = {22400, 36400, 28000};
|
||||
const uint16_t zero_slow[3] = {32400, 32000, 31600};
|
||||
const uint16_t scale_slow[3] = {21600, 37400, 28000};
|
||||
for (unsigned axis = 0; axis < 3; ++axis) {
|
||||
put_be16(&f.mp_calibration[2 * axis], zero_fast[axis]);
|
||||
put_be16(&f.mp_calibration[6 + 2 * axis], scale_fast[axis]);
|
||||
put_be16(&f.mp_calibration[16 + 2 * axis], zero_slow[axis]);
|
||||
put_be16(&f.mp_calibration[22 + 2 * axis], scale_slow[axis]);
|
||||
}
|
||||
f.mp_calibration[12] = 200; // Fast calibration is at 1200 degrees/s.
|
||||
f.mp_calibration[28] = 45; // Slow calibration is at 270 degrees/s.
|
||||
f.mp_calibration[13] = 0x31;
|
||||
f.mp_calibration[29] = 0x72;
|
||||
update_mp_checksum();
|
||||
}
|
||||
|
||||
static void feed(const uint8_t* report, uint16_t len) {
|
||||
uni_hid_parser_wii_init_report(&f.device);
|
||||
uni_hid_parser_wii_parse_input_report(&f.device, report, len);
|
||||
}
|
||||
|
||||
static void send_status(bool extension_present) {
|
||||
const uint8_t report[] = {0x20, f.connection_buttons, 0, extension_present ? 2 : 0, 0, 0, 0xc0};
|
||||
feed(report, sizeof(report));
|
||||
}
|
||||
|
||||
static void send_ack(uint8_t command, uint8_t error) {
|
||||
const uint8_t report[] = {0x22, 0, 0, command, error};
|
||||
feed(report, sizeof(report));
|
||||
}
|
||||
|
||||
static uint32_t transaction_address(const transaction_t* transaction) {
|
||||
assert(transaction->len >= 8);
|
||||
const uint8_t* bytes = transaction->bytes;
|
||||
return ((uint32_t)bytes[3] << 16) | ((uint32_t)bytes[4] << 8) | bytes[5];
|
||||
}
|
||||
|
||||
static void send_read_reply(uint16_t address, const uint8_t* data, uint8_t size, uint8_t error) {
|
||||
assert(size >= 1 && size <= 16);
|
||||
uint8_t report[22] = {0x21, 0, 0, (uint8_t)(((size - 1) << 4) | error)};
|
||||
put_be16(&report[4], address);
|
||||
if (!error)
|
||||
memcpy(&report[6], data, size);
|
||||
feed(report, sizeof(report));
|
||||
}
|
||||
|
||||
static bool read_wire_byte(uint8_t space, uint32_t address, uint8_t* value) {
|
||||
if (space == 0) {
|
||||
if (address >= 0x16 && address < 0x2a && !f.fail_accel_reads) {
|
||||
*value = f.accel_calibration[address - 0x16];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
assert(space == 4);
|
||||
uint32_t bank = address >> 16;
|
||||
uint16_t offset = address;
|
||||
if ((bank == 0xa6 && f.motionplus && !f.mp_active) ||
|
||||
(bank == 0xa4 && f.mp_active)) {
|
||||
if (offset >= 0x20 && offset < 0x40) {
|
||||
*value = f.mp_calibration[offset - 0x20];
|
||||
return true;
|
||||
}
|
||||
if (offset >= 0xfa && offset <= 0xff) {
|
||||
// RVL-CNT-01-TR hardware returns an A4 signature even when read
|
||||
// through inactive A600FA; the ID is not an echo of the read bank.
|
||||
const bool integrated = f.device.product_id == 0x0330;
|
||||
const uint8_t identity[] = {integrated ? 1 : 0, 0, integrated ? 0xa4 : (uint8_t)bank,
|
||||
0x20, f.mp_active ? f.activation_mode : 0, 5};
|
||||
*value = identity[offset - 0xfa];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (bank == 0xa4 && !f.mp_active && f.extension == EXT_NUNCHUK && offset >= 0x20 && offset < 0x30) {
|
||||
*value = f.nunchuk_calibration[offset - 0x20];
|
||||
return true;
|
||||
}
|
||||
if (bank == 0xa4 && !f.mp_active && f.extension != EXT_NONE && offset >= 0xfa && offset <= 0xff) {
|
||||
uint8_t identity[6];
|
||||
memcpy(identity, f.extension_identity, sizeof(identity));
|
||||
if (f.extension == EXT_WII_U_PRO) {
|
||||
identity[4] = 1;
|
||||
identity[5] = 0x20;
|
||||
}
|
||||
*value = identity[offset - 0xfa];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void answer_transaction(const transaction_t* transaction) {
|
||||
const uint8_t* bytes = transaction->bytes;
|
||||
switch (bytes[1]) {
|
||||
case 0x11: // LEDs do not acknowledge unless explicitly requested.
|
||||
if (bytes[2] & 2)
|
||||
send_ack(0x11, 0);
|
||||
break;
|
||||
case 0x12:
|
||||
assert(transaction->len == 4);
|
||||
f.report_mode = bytes[3];
|
||||
if (bytes[2] & 2)
|
||||
send_ack(0x12, 0);
|
||||
break;
|
||||
case 0x15:
|
||||
send_status(f.extension != EXT_NONE || f.mp_active);
|
||||
break;
|
||||
case 0x16: {
|
||||
uint32_t address = transaction_address(transaction);
|
||||
assert((bytes[2] & 0xfe) == 4);
|
||||
assert(bytes[6] == 1 && transaction->len >= 8);
|
||||
uint8_t error = 0;
|
||||
bool deactivated = false;
|
||||
bool activated = false;
|
||||
if (address == f.fail_write_address || ((address >> 16) == 0xa6 && !f.motionplus)) {
|
||||
error = 7;
|
||||
} else if (address == 0xa600f0) {
|
||||
assert(bytes[7] == 0x55);
|
||||
f.mp_initialized = true;
|
||||
} else if (address == 0xa600fe) {
|
||||
// Initialization before activation and the passthrough selector
|
||||
// are protocol requirements, not a pinned parser FSM sequence.
|
||||
assert(f.mp_initialized);
|
||||
assert(bytes[7] == 4 || bytes[7] == 5);
|
||||
f.mp_active = true;
|
||||
f.activation_mode = bytes[7];
|
||||
activated = true;
|
||||
if (f.status_before_activation_ack)
|
||||
send_status(true);
|
||||
} else if (address == 0xa400f0) {
|
||||
assert(bytes[7] == 0x55);
|
||||
deactivated = f.mp_active;
|
||||
f.mp_active = false;
|
||||
if (deactivated && f.deactivation_status_before_ack) {
|
||||
send_status(false);
|
||||
send_status(true);
|
||||
}
|
||||
} else {
|
||||
assert((address == 0xa400fb || address == 0xa600fb) && bytes[7] == 0);
|
||||
}
|
||||
send_ack(0x16, error);
|
||||
if (deactivated && f.deactivation_status_after_ack) {
|
||||
send_status(false);
|
||||
send_status(true);
|
||||
}
|
||||
if (activated && f.status_after_activation_ack)
|
||||
send_status(true);
|
||||
break;
|
||||
}
|
||||
case 0x17: {
|
||||
uint32_t address = transaction_address(transaction);
|
||||
uint16_t remaining = ((uint16_t)bytes[6] << 8) | bytes[7];
|
||||
assert(remaining && remaining <= 32);
|
||||
if (address == 0xa400fa && !f.mp_active && f.extension != EXT_NONE) {
|
||||
if (f.transient_extension_read_errors) {
|
||||
--f.transient_extension_read_errors;
|
||||
send_read_reply(address, NULL, 1, 7);
|
||||
break;
|
||||
}
|
||||
if (f.initializing_extension_reads) {
|
||||
--f.initializing_extension_reads;
|
||||
const uint8_t initializing[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
|
||||
send_read_reply(address, initializing, sizeof(initializing), 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (address == f.fail_read_address) {
|
||||
send_read_reply(address, NULL, 1, 7);
|
||||
break;
|
||||
}
|
||||
while (remaining) {
|
||||
uint8_t data[16];
|
||||
uint8_t size = remaining > 16 ? 16 : remaining;
|
||||
bool available = true;
|
||||
for (unsigned i = 0; i < size; ++i)
|
||||
available &= read_wire_byte(bytes[2] & 0xfe, address + i, &data[i]);
|
||||
send_read_reply(address, data, size, available ? 0 : 7);
|
||||
if (!available)
|
||||
break;
|
||||
address += size;
|
||||
remaining -= size;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assert(!"unexpected Wii output report");
|
||||
}
|
||||
}
|
||||
|
||||
static void finish_setup(void) {
|
||||
// Output may be emitted recursively by each reply. Bound the wire dialogue
|
||||
// so retry loops fail instead of hanging pytest.
|
||||
for (unsigned step = 0; step < 128 && f.cursor < f.sent_count; ++step) {
|
||||
transaction_t transaction = f.sent[f.cursor++];
|
||||
answer_transaction(&transaction);
|
||||
}
|
||||
assert(f.cursor == f.sent_count);
|
||||
assert(f.ready_count == 1);
|
||||
assert(f.report_mode != 0);
|
||||
}
|
||||
|
||||
static void connect_device(void) {
|
||||
uni_hid_parser_wii_setup(&f.device);
|
||||
finish_setup();
|
||||
}
|
||||
|
||||
static transaction_t stop_at_read(uint32_t address) {
|
||||
for (unsigned step = 0; step < 128 && f.cursor < f.sent_count; ++step) {
|
||||
transaction_t transaction = f.sent[f.cursor++];
|
||||
if (transaction.bytes[1] == 0x17 && transaction_address(&transaction) == address)
|
||||
return transaction;
|
||||
answer_transaction(&transaction);
|
||||
}
|
||||
assert(!"required Wii memory request was not sent");
|
||||
return (transaction_t){0};
|
||||
}
|
||||
|
||||
static void put_accel_report(uint8_t* report, uint16_t x, uint16_t y, uint16_t z) {
|
||||
assert(!(y & 1) && !(z & 1)); // Y/Z wire samples have no least-significant bit.
|
||||
report[1] |= (x & 3) << 5;
|
||||
report[2] |= ((y & 2) << 4) | ((z & 2) << 5);
|
||||
report[3] = x >> 2;
|
||||
report[4] = y >> 2;
|
||||
report[5] = z >> 2;
|
||||
}
|
||||
|
||||
static void put_gyro_report(uint8_t* extension, uint16_t yaw, uint16_t roll, uint16_t pitch,
|
||||
bool yaw_slow, bool roll_slow, bool pitch_slow) {
|
||||
extension[0] = yaw;
|
||||
extension[1] = roll;
|
||||
extension[2] = pitch;
|
||||
extension[3] = ((yaw >> 8) << 2) | (yaw_slow ? 2 : 0) | (pitch_slow ? 1 : 0);
|
||||
extension[4] = ((roll >> 8) << 2) | (roll_slow ? 2 : 0) | (f.extension != EXT_NONE ? 1 : 0);
|
||||
extension[5] = ((pitch >> 8) << 2) | 2;
|
||||
}
|
||||
|
||||
static void send_motion(uint16_t yaw, uint16_t roll, uint16_t pitch,
|
||||
bool yaw_slow, bool roll_slow, bool pitch_slow) {
|
||||
uint8_t report[22] = {0x35, 1, 0x0a}; // Left + A + 1, no connection-time A needed.
|
||||
put_accel_report(report, 614, 442, 534);
|
||||
put_gyro_report(&report[6], yaw, roll, pitch, yaw_slow, roll_slow, pitch_slow);
|
||||
feed(report, sizeof(report));
|
||||
}
|
||||
|
||||
static void expect_vector(const int32_t vector[3], int32_t x, int32_t y, int32_t z) {
|
||||
assert(vector[0] == x);
|
||||
assert(vector[1] == y);
|
||||
assert(vector[2] == z);
|
||||
}
|
||||
|
||||
static void expect_accel(void) {
|
||||
// Native (+1g, -0.5g, +0.25g) maps to SDL (-X, +Z, +Y).
|
||||
expect_vector(f.device.controller.gamepad.accel, -8192, 2048, -4096);
|
||||
}
|
||||
|
||||
static void send_core_and_accel(void) {
|
||||
uint8_t report[22] = {f.report_mode, 1, 0x0a};
|
||||
assert(f.report_mode == 0x30 || f.report_mode == 0x31 || f.report_mode == 0x35);
|
||||
uint16_t len = f.report_mode == 0x35 ? 22 : f.report_mode == 0x31 ? 6 : 3;
|
||||
if (len >= 6)
|
||||
put_accel_report(report, 614, 442, 534);
|
||||
feed(report, len);
|
||||
assert(f.device.controller.gamepad.dpad == DPAD_DOWN);
|
||||
assert(f.device.controller.gamepad.buttons == (BUTTON_X | BUTTON_A));
|
||||
}
|
||||
|
||||
static void send_nunchuk_controls(bool passthrough) {
|
||||
uint8_t report[22] = {0x35};
|
||||
put_accel_report(report, 614, 442, 534);
|
||||
const uint8_t extension[6] = {160, 192, 128, 128, passthrough ? 129 : 128, passthrough ? 4 : 1};
|
||||
memcpy(report + 6, extension, sizeof(extension));
|
||||
feed(report, sizeof(report));
|
||||
assert(f.device.controller.gamepad.buttons == BUTTON_X);
|
||||
assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == -341);
|
||||
assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0);
|
||||
assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL);
|
||||
expect_accel();
|
||||
}
|
||||
|
||||
static void integrated_motionplus_calibration_and_slow_bits(void) {
|
||||
reset_fixture(0x0330, true, EXT_NONE);
|
||||
connect_device();
|
||||
assert(f.mp_active && f.activation_mode == 4 && f.report_mode == 0x35);
|
||||
|
||||
// Each axis is the only slow axis in one packet. A shared slow flag or
|
||||
// swapped e[3]/e[4] bits cannot accidentally pass all three vectors.
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
expect_accel();
|
||||
assert(f.device.controller.gamepad.dpad == DPAD_DOWN);
|
||||
assert(f.device.controller.gamepad.buttons == (BUTTON_X | BUTTON_A));
|
||||
|
||||
send_motion(8300, 7660, 7560, true, false, false);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, 20 * 1024, -240 * 1024);
|
||||
send_motion(7760, 7660, 8100, false, false, true);
|
||||
expect_vector(f.device.controller.gamepad.gyro, -60 * 1024, -120 * 1024, -240 * 1024);
|
||||
}
|
||||
|
||||
static void external_motionplus_without_extension_status(void) {
|
||||
reset_fixture(0x0306, true, EXT_NONE);
|
||||
f.status_before_activation_ack = true;
|
||||
connect_device();
|
||||
assert(f.mp_active && f.activation_mode == 4 && f.report_mode == 0x35);
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
expect_accel();
|
||||
}
|
||||
|
||||
static void absent_motionplus_keeps_calibrated_remote(void) {
|
||||
reset_fixture(0x0306, false, EXT_NONE);
|
||||
connect_device();
|
||||
assert(!f.mp_active && f.report_mode == 0x31);
|
||||
send_core_and_accel();
|
||||
expect_accel();
|
||||
expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0);
|
||||
}
|
||||
|
||||
static void setup_read_and_write_errors_leave_buttons_ready(void) {
|
||||
reset_fixture(0x0330, true, EXT_NONE);
|
||||
f.fail_read_address = 0xa60030;
|
||||
connect_device();
|
||||
send_core_and_accel();
|
||||
expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0);
|
||||
|
||||
reset_fixture(0x0330, true, EXT_NONE);
|
||||
f.fail_write_address = 0xa600fe;
|
||||
connect_device();
|
||||
assert(!f.mp_active);
|
||||
send_core_and_accel();
|
||||
expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0);
|
||||
|
||||
reset_fixture(0x0306, false, EXT_NUNCHUK);
|
||||
f.fail_write_address = 0xa400f0;
|
||||
connect_device();
|
||||
// A failed reset ACK is not an extension identity. If the subsequent read
|
||||
// succeeds, preserve the real Nunchuk rather than forcing standalone mode.
|
||||
send_nunchuk_controls(false);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0);
|
||||
}
|
||||
|
||||
static void failed_calibration_never_fabricates_motion(void) {
|
||||
reset_fixture(0x0330, true, EXT_NONE);
|
||||
f.accel_calibration[9] ^= 1;
|
||||
f.accel_calibration[19] ^= 1;
|
||||
connect_device();
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
expect_vector(f.device.controller.gamepad.accel, 0, 0, 0);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
assert(f.device.controller.gamepad.buttons == (BUTTON_X | BUTTON_A));
|
||||
|
||||
reset_fixture(0x0330, true, EXT_NONE);
|
||||
f.mp_calibration[30] ^= 1;
|
||||
connect_device();
|
||||
if (f.report_mode == 0x35)
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
else
|
||||
send_core_and_accel();
|
||||
expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0);
|
||||
expect_accel();
|
||||
assert(f.device.controller.gamepad.buttons == (BUTTON_X | BUTTON_A));
|
||||
|
||||
// A correct CRC alone is insufficient: zero sensitivity must be rejected.
|
||||
reset_fixture(0x0330, true, EXT_NONE);
|
||||
memcpy(&f.mp_calibration[6], f.mp_calibration, 2);
|
||||
update_mp_checksum();
|
||||
connect_device();
|
||||
if (f.report_mode == 0x35)
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
else
|
||||
send_core_and_accel();
|
||||
expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0);
|
||||
expect_accel();
|
||||
}
|
||||
|
||||
static void accel_backup_and_memory_error_recovery(void) {
|
||||
reset_fixture(0x0306, false, EXT_NONE);
|
||||
f.accel_calibration[9] ^= 1;
|
||||
connect_device();
|
||||
send_core_and_accel();
|
||||
expect_accel();
|
||||
|
||||
reset_fixture(0x0306, false, EXT_NONE);
|
||||
f.fail_accel_reads = true;
|
||||
connect_device();
|
||||
send_core_and_accel();
|
||||
expect_vector(f.device.controller.gamepad.accel, 0, 0, 0);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0);
|
||||
}
|
||||
|
||||
static void malformed_and_mismatched_setup_replies_do_not_advance(void) {
|
||||
reset_fixture(0x0330, true, EXT_NONE);
|
||||
uni_hid_parser_wii_setup(&f.device);
|
||||
transaction_t request = stop_at_read(0xa60020);
|
||||
unsigned count = f.sent_count;
|
||||
const uint8_t empty[] = {0x21};
|
||||
const uint8_t short_header[] = {0x21, 0, 0, 0xf0, 0};
|
||||
const uint8_t short_data[] = {0x21, 0, 0, 0xf0, 0, 0x20, 0};
|
||||
const uint8_t short_ack[] = {0x22, 0, 0, 0x16};
|
||||
feed(empty, 0);
|
||||
feed(empty, sizeof(empty));
|
||||
feed(short_header, sizeof(short_header));
|
||||
feed(short_data, sizeof(short_data));
|
||||
feed(short_ack, sizeof(short_ack));
|
||||
send_ack(0x12, 0);
|
||||
send_ack(0x16, 0); // No write is outstanding.
|
||||
send_read_reply(0x30, &f.mp_calibration[16], 16, 0);
|
||||
send_read_reply(0x20, f.mp_calibration, 15, 0);
|
||||
assert(f.sent_count == count && f.ready_count == 0);
|
||||
|
||||
// Accept either two 16-byte reads or one 32-byte read; both produce the
|
||||
// same two wire replies, since a 0x21 response carries at most 16 bytes.
|
||||
unsigned requested = ((unsigned)request.bytes[6] << 8) | request.bytes[7];
|
||||
assert(requested == 16 || requested == 32);
|
||||
send_read_reply(0x20, f.mp_calibration, 16, 0);
|
||||
if (requested == 16)
|
||||
(void)stop_at_read(0xa60030);
|
||||
count = f.sent_count;
|
||||
// Duplicate first half cannot satisfy the outstanding second-half read.
|
||||
send_read_reply(0x20, f.mp_calibration, 16, 0);
|
||||
assert(f.sent_count == count && f.ready_count == 0);
|
||||
send_read_reply(0x30, &f.mp_calibration[16], 16, 0);
|
||||
finish_setup();
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
}
|
||||
|
||||
static void short_motion_reports_do_not_publish_partial_samples(void) {
|
||||
reset_fixture(0x0330, true, EXT_NONE);
|
||||
connect_device();
|
||||
uint8_t report[22];
|
||||
memset(report, 0xff, sizeof(report));
|
||||
report[0] = 0x35;
|
||||
unsigned count = f.sent_count;
|
||||
// The sixth extension byte contains pitch MSBs and the MP discriminator.
|
||||
// No prefix missing that byte constitutes a valid combined sample.
|
||||
for (uint16_t len = 1; len < 12; ++len) {
|
||||
feed(report, len);
|
||||
expect_vector(f.device.controller.gamepad.accel, 0, 0, 0);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0);
|
||||
}
|
||||
report[0] = 0x31;
|
||||
feed(report, 5);
|
||||
expect_vector(f.device.controller.gamepad.accel, 0, 0, 0);
|
||||
report[0] = 0x20;
|
||||
feed(report, 6);
|
||||
assert(f.sent_count == count && f.ready_count == 1);
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
expect_accel();
|
||||
}
|
||||
|
||||
static void nunchuk_passthrough_retains_controls_between_samples(void) {
|
||||
reset_fixture(0x0330, true, EXT_NUNCHUK);
|
||||
connect_device();
|
||||
assert(f.mp_active && f.activation_mode == 5 && f.report_mode == 0x35);
|
||||
uint8_t nunchuk[22] = {0x35, 1, 0x0a};
|
||||
put_accel_report(nunchuk, 614, 442, 534);
|
||||
const uint8_t extension[6] = {160, 96, 128, 128, 129, 4}; // C held, Z released.
|
||||
memcpy(&nunchuk[6], extension, sizeof(extension));
|
||||
feed(nunchuk, sizeof(nunchuk));
|
||||
assert(f.device.controller.gamepad.dpad == DPAD_LEFT);
|
||||
assert(f.device.controller.gamepad.buttons == (BUTTON_B | BUTTON_SHOULDER_L | BUTTON_X));
|
||||
assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == 170);
|
||||
|
||||
uint8_t gyro[22] = {0x35}; // Release core controls but keep the Nunchuk held.
|
||||
put_accel_report(gyro, 614, 442, 534);
|
||||
put_gyro_report(&gyro[6], 7760, 8200, 7560, false, true, false);
|
||||
feed(gyro, sizeof(gyro));
|
||||
assert(f.device.controller.gamepad.buttons == BUTTON_X);
|
||||
assert(f.device.controller.gamepad.dpad == 0);
|
||||
assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == 170);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
expect_accel();
|
||||
|
||||
nunchuk[2] &= 0x60; // Preserve accel low bits, release core buttons.
|
||||
nunchuk[1] &= 0x60;
|
||||
nunchuk[11] = 8; // C released, Z held: moved bits 3/2, not plain bits 1/0.
|
||||
feed(nunchuk, sizeof(nunchuk));
|
||||
assert(f.device.controller.gamepad.buttons == BUTTON_Y);
|
||||
feed(gyro, sizeof(gyro));
|
||||
assert(f.device.controller.gamepad.buttons == BUTTON_Y);
|
||||
assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == 170);
|
||||
|
||||
nunchuk[6] = nunchuk[7] = 128;
|
||||
nunchuk[11] = 12; // Both released; stale cached presses must now disappear.
|
||||
feed(nunchuk, sizeof(nunchuk));
|
||||
feed(gyro, sizeof(gyro));
|
||||
assert(f.device.controller.gamepad.buttons == 0);
|
||||
assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0);
|
||||
assert(f.device.controller.gamepad.axis_x == 0 && f.device.controller.gamepad.axis_y == 0);
|
||||
}
|
||||
|
||||
static void legacy_plain_nunchuk_and_wii_u_pro(void) {
|
||||
reset_fixture(0x0306, false, EXT_NUNCHUK);
|
||||
connect_device();
|
||||
assert(!f.mp_active && (f.report_mode == 0x32 || f.report_mode == 0x35));
|
||||
uint8_t nunchuk[22] = {f.report_mode, 1, 0x0a};
|
||||
unsigned offset = f.report_mode == 0x35 ? 6 : 3;
|
||||
if (offset == 6)
|
||||
put_accel_report(nunchuk, 614, 442, 534);
|
||||
const uint8_t extension[6] = {160, 192, 128, 128, 128, 1}; // Plain C held, Z released.
|
||||
memcpy(&nunchuk[offset], extension, sizeof(extension));
|
||||
feed(nunchuk, f.report_mode == 0x35 ? 22 : 11);
|
||||
assert(f.device.controller.gamepad.dpad == DPAD_LEFT);
|
||||
assert(f.device.controller.gamepad.buttons == (BUTTON_B | BUTTON_SHOULDER_L | BUTTON_X));
|
||||
assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == -341);
|
||||
assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0);
|
||||
|
||||
reset_fixture(0x0330, false, EXT_WII_U_PRO);
|
||||
connect_device();
|
||||
assert(!f.mp_active && f.report_mode == 0x34);
|
||||
uint8_t pro[22] = {0x34};
|
||||
put_le16(&pro[3], 0x0a80);
|
||||
put_le16(&pro[5], 0x0580);
|
||||
put_le16(&pro[7], 0x06c0);
|
||||
put_le16(&pro[9], 0x0940);
|
||||
pro[11] = 0x7b; // Right and + held.
|
||||
pro[12] = 0xbf; // Native B held.
|
||||
pro[13] = 0xff;
|
||||
feed(pro, sizeof(pro));
|
||||
assert(f.device.controller.gamepad.axis_x == 256 && f.device.controller.gamepad.axis_y == 128);
|
||||
assert(f.device.controller.gamepad.axis_rx == -256 && f.device.controller.gamepad.axis_ry == -128);
|
||||
assert(f.device.controller.gamepad.dpad == DPAD_RIGHT);
|
||||
assert(f.device.controller.gamepad.buttons == BUTTON_A);
|
||||
assert(f.device.controller.gamepad.misc_buttons == MISC_BUTTON_START);
|
||||
expect_vector(f.device.controller.gamepad.accel, 0, 0, 0);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0);
|
||||
}
|
||||
|
||||
static void plus_selects_vertical_without_disabling_motion(void) {
|
||||
reset_fixture(0x0330, true, EXT_NONE);
|
||||
f.connection_buttons = 0x10;
|
||||
connect_device();
|
||||
assert(f.mp_active && f.report_mode == 0x35);
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
assert(f.device.controller.gamepad.dpad == DPAD_LEFT);
|
||||
assert(f.device.controller.gamepad.buttons == (BUTTON_B | BUTTON_X));
|
||||
expect_accel();
|
||||
}
|
||||
|
||||
static void unsolicited_status_restores_stream_without_duplicate_ready(void) {
|
||||
reset_fixture(0x0330, true, EXT_NONE);
|
||||
connect_device();
|
||||
unsigned start = f.sent_count;
|
||||
send_status(true);
|
||||
finish_setup();
|
||||
bool restored = false;
|
||||
for (unsigned i = start; i < f.sent_count; ++i) {
|
||||
if (f.sent[i].bytes[1] == 0x12 && f.sent[i].bytes[3] == 0x35)
|
||||
restored = true;
|
||||
assert(f.sent[i].bytes[1] != 0x16); // Reinitialization deactivates MP.
|
||||
}
|
||||
assert(restored && f.ready_count == 1 && f.mp_active);
|
||||
start = f.sent_count;
|
||||
send_ack(0x16, 0);
|
||||
send_read_reply(0xfa, (const uint8_t[]){0, 0, 0xa6, 0x20, 0, 5}, 6, 0);
|
||||
assert(f.sent_count == start && f.ready_count == 1);
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
}
|
||||
|
||||
static void initializing_and_vendor_nunchuk_ids_at_connection(void) {
|
||||
reset_fixture(0x0330, true, EXT_NUNCHUK);
|
||||
// Some third-party revisions do not have the canonical 0000 prefix.
|
||||
f.extension_identity[0] = 1;
|
||||
f.initializing_extension_reads = 2;
|
||||
f.transient_extension_read_errors = 1;
|
||||
f.status_after_activation_ack = true;
|
||||
connect_device();
|
||||
assert(f.mp_active && f.activation_mode == 5);
|
||||
send_nunchuk_controls(true);
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.buttons & BUTTON_X);
|
||||
|
||||
// An all-zero suffix is not proof of absence: SDL and upstream recognize
|
||||
// the zero-filled ID documented for a replugged wireless BladeFX adapter.
|
||||
reset_fixture(0x0306, false, EXT_NUNCHUK);
|
||||
memset(f.extension_identity, 0, sizeof(f.extension_identity));
|
||||
connect_device();
|
||||
send_nunchuk_controls(false);
|
||||
}
|
||||
|
||||
static void motionplus_nunchuk_hotplug_and_detach(void) {
|
||||
reset_fixture(0x0330, true, EXT_NONE);
|
||||
connect_device();
|
||||
assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL);
|
||||
f.deactivation_status_before_ack = true;
|
||||
f.status_after_activation_ack = true;
|
||||
f.extension = EXT_NUNCHUK;
|
||||
// Downstream hotplug is carried in MP data, not the ordinary status bit.
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
finish_setup();
|
||||
assert(f.mp_active && f.activation_mode == 5 && f.ready_count == 1);
|
||||
send_nunchuk_controls(true);
|
||||
|
||||
// Unsolicited status starts a mapped-ID check. A detach during that pending
|
||||
// read must be deferred, not overwrite its address-less transaction state.
|
||||
send_status(false);
|
||||
transaction_t verify = stop_at_read(0xa400fa);
|
||||
f.extension = EXT_NONE;
|
||||
uint8_t gyro[22] = {0x35};
|
||||
put_accel_report(gyro, 614, 442, 534);
|
||||
put_gyro_report(gyro + 6, 7760, 8200, 7560, false, true, false);
|
||||
feed(gyro, sizeof(gyro));
|
||||
assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0);
|
||||
assert(f.device.controller.gamepad.axis_x == 0 && f.device.controller.gamepad.axis_y == 0);
|
||||
assert(f.device.controller.gamepad.buttons == 0);
|
||||
assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
answer_transaction(&verify);
|
||||
finish_setup();
|
||||
assert(f.mp_active && f.activation_mode == 4 && f.ready_count == 1);
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0);
|
||||
assert(f.device.controller.gamepad.axis_x == 0 && f.device.controller.gamepad.axis_y == 0);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
expect_accel();
|
||||
}
|
||||
|
||||
static void external_motionplus_hotplug_retains_selected_orientation(void) {
|
||||
reset_fixture(0x0306, true, EXT_NONE);
|
||||
f.connection_buttons = 0x10;
|
||||
connect_device();
|
||||
f.deactivation_status_after_ack = true;
|
||||
f.status_before_activation_ack = true;
|
||||
f.extension = EXT_NUNCHUK;
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
finish_setup();
|
||||
send_nunchuk_controls(true);
|
||||
f.extension = EXT_NONE;
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
finish_setup();
|
||||
assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL);
|
||||
assert(f.mp_active && f.activation_mode == 4 && f.ready_count == 1);
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
assert(f.device.controller.gamepad.dpad == DPAD_LEFT);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
|
||||
// Removing the external MP itself clears its stale angular velocity, while
|
||||
// the Remote's independently calibrated accelerometer remains available.
|
||||
f.motionplus = false;
|
||||
f.mp_active = false;
|
||||
send_status(false);
|
||||
finish_setup();
|
||||
uint8_t remote[6] = {0x31, 1, 0x0a};
|
||||
put_accel_report(remote, 614, 442, 534);
|
||||
feed(remote, sizeof(remote));
|
||||
assert(f.device.controller.gamepad.dpad == DPAD_LEFT);
|
||||
assert(f.device.controller.gamepad.buttons == (BUTTON_B | BUTTON_X));
|
||||
expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0);
|
||||
expect_accel();
|
||||
assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL);
|
||||
}
|
||||
|
||||
static void plain_nunchuk_hotplug_and_setup_race(void) {
|
||||
reset_fixture(0x0306, false, EXT_NONE);
|
||||
uni_hid_parser_wii_setup(&f.device);
|
||||
transaction_t accel = stop_at_read(0x16);
|
||||
// An attachment status while EEPROM is being read must not lose that read.
|
||||
f.extension = EXT_NUNCHUK;
|
||||
send_status(true);
|
||||
answer_transaction(&accel);
|
||||
finish_setup();
|
||||
send_nunchuk_controls(false);
|
||||
f.extension = EXT_NONE;
|
||||
send_status(false);
|
||||
assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0);
|
||||
assert(f.device.controller.gamepad.axis_x == 0 && f.device.controller.gamepad.axis_y == 0);
|
||||
assert(f.device.controller.gamepad.buttons == 0);
|
||||
finish_setup();
|
||||
assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL);
|
||||
send_core_and_accel();
|
||||
expect_accel();
|
||||
|
||||
uni_hid_parser_wii_set_mode(&f.device, WII_MODE_VERTICAL);
|
||||
finish_setup();
|
||||
f.extension = EXT_NUNCHUK;
|
||||
send_status(true);
|
||||
finish_setup();
|
||||
send_nunchuk_controls(false);
|
||||
f.extension = EXT_NONE;
|
||||
send_status(false);
|
||||
finish_setup();
|
||||
assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL);
|
||||
assert(f.ready_count == 1);
|
||||
}
|
||||
|
||||
static void expect_nunchuk_stick(uint8_t x, uint8_t y, int expected_x, int expected_y) {
|
||||
uint8_t report[22] = {0x35};
|
||||
put_accel_report(report, 614, 442, 534);
|
||||
report[6] = x;
|
||||
report[7] = y;
|
||||
report[10] = 1; // Extension connected.
|
||||
report[11] = f.mp_active ? 12 : 3; // C/Z released in the selected format.
|
||||
feed(report, sizeof(report));
|
||||
assert(f.device.controller.gamepad.axis_x == expected_x);
|
||||
assert(f.device.controller.gamepad.axis_y == expected_y);
|
||||
assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0);
|
||||
}
|
||||
|
||||
static void calibrated_nunchuk_left_stick_endpoints_and_replacement(void) {
|
||||
reset_fixture(0x0330, true, EXT_NUNCHUK);
|
||||
set_nunchuk_stick_calibration((const uint8_t[]){220, 40, 124, 210, 30, 126});
|
||||
connect_device();
|
||||
expect_nunchuk_stick(124, 126, 0, 0);
|
||||
expect_nunchuk_stick(220, 126, 511, 0);
|
||||
expect_nunchuk_stick(40, 126, -512, 0);
|
||||
expect_nunchuk_stick(124, 210, 0, -512);
|
||||
expect_nunchuk_stick(124, 30, 0, 511);
|
||||
expect_nunchuk_stick(172, 168, 256, -256);
|
||||
expect_nunchuk_stick(82, 78, -256, 256);
|
||||
expect_nunchuk_stick(255, 0, 511, 511);
|
||||
expect_nunchuk_stick(0, 255, -512, -512);
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
assert(f.device.controller.gamepad.axis_x == -512 && f.device.controller.gamepad.axis_y == -512);
|
||||
f.extension = EXT_NONE;
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
finish_setup();
|
||||
assert(f.device.controller.gamepad.axis_x == 0 && f.device.controller.gamepad.axis_y == 0);
|
||||
// A different Nunchuk must not inherit the previous extension's center/range.
|
||||
set_nunchuk_stick_calibration((const uint8_t[]){230, 50, 130, 220, 40, 120});
|
||||
f.extension = EXT_NUNCHUK;
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
finish_setup();
|
||||
expect_nunchuk_stick(130, 120, 0, 0);
|
||||
expect_nunchuk_stick(230, 40, 511, 511);
|
||||
}
|
||||
|
||||
static void unavailable_nunchuk_calibration_keeps_safe_nominal_stick(void) {
|
||||
reset_fixture(0x0306, false, EXT_NUNCHUK);
|
||||
f.fail_read_address = 0xa40020;
|
||||
connect_device();
|
||||
expect_nunchuk_stick(128, 128, 0, 0);
|
||||
expect_nunchuk_stick(224, 32, 511, 511);
|
||||
reset_fixture(0x0330, true, EXT_NUNCHUK);
|
||||
set_nunchuk_stick_calibration((const uint8_t[]){128, 32, 128, 224, 32, 128});
|
||||
connect_device(); // Correct checksum but zero positive span.
|
||||
expect_nunchuk_stick(160, 96, 170, 170);
|
||||
reset_fixture(0x0330, true, EXT_NUNCHUK);
|
||||
f.nunchuk_calibration[15] ^= 1;
|
||||
connect_device();
|
||||
expect_nunchuk_stick(128, 128, 0, 0);
|
||||
expect_nunchuk_stick(224, 32, 511, 511);
|
||||
}
|
||||
|
||||
static void run_case(const char* name, void (*test)(void)) {
|
||||
printf("Wii parser: %s\n", name);
|
||||
fflush(stdout);
|
||||
test();
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
run_case("calibrated Nunchuk left-stick endpoints and replacement", calibrated_nunchuk_left_stick_endpoints_and_replacement);
|
||||
run_case("unavailable Nunchuk calibration uses safe nominal travel", unavailable_nunchuk_calibration_keeps_safe_nominal_stick);
|
||||
run_case("integrated MotionPlus calibration and per-axis slow bits", integrated_motionplus_calibration_and_slow_bits);
|
||||
run_case("external MotionPlus with absent extension status bit", external_motionplus_without_extension_status);
|
||||
run_case("absent MotionPlus retains calibrated remote", absent_motionplus_keeps_calibrated_remote);
|
||||
run_case("setup read/write errors leave buttons ready", setup_read_and_write_errors_leave_buttons_ready);
|
||||
run_case("invalid calibration suppresses only unavailable motion", failed_calibration_never_fabricates_motion);
|
||||
run_case("accelerometer backup and EEPROM errors", accel_backup_and_memory_error_recovery);
|
||||
run_case("malformed and mismatched setup replies", malformed_and_mismatched_setup_replies_do_not_advance);
|
||||
run_case("short reports reject partial motion samples", short_motion_reports_do_not_publish_partial_samples);
|
||||
run_case("Nunchuk passthrough retains and releases held controls", nunchuk_passthrough_retains_controls_between_samples);
|
||||
run_case("legacy plain Nunchuk and Wii U Pro mappings", legacy_plain_nunchuk_and_wii_u_pro);
|
||||
run_case("connection-time plus selects vertical motion mode", plus_selects_vertical_without_disabling_motion);
|
||||
run_case("unsolicited status restores reporting exactly once", unsolicited_status_restores_stream_without_duplicate_ready);
|
||||
run_case("initializing and vendor Nunchuk identities", initializing_and_vendor_nunchuk_ids_at_connection);
|
||||
run_case("integrated MotionPlus downstream hotplug and detach", motionplus_nunchuk_hotplug_and_detach);
|
||||
run_case("external MotionPlus preserves selected standalone orientation", external_motionplus_hotplug_retains_selected_orientation);
|
||||
run_case("plain Nunchuk hotplug and setup-time status race", plain_nunchuk_hotplug_and_setup_race);
|
||||
puts("Wii parser native contracts passed");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue