Add DualSense native pair and Wii-only IMU settling
This commit is contained in:
parent
1748910316
commit
ab70536fdf
31 changed files with 2617 additions and 77 deletions
262
tests/dualsense_parser_native_test.c
Normal file
262
tests/dualsense_parser_native_test.c
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "parser/uni_hid_parser_ds5.h"
|
||||
#include "uni_hid_device.h"
|
||||
#include "uni_utils.h"
|
||||
|
||||
// Real staged DS5 parser; substitute only the radio, virtual mouse and clock.
|
||||
static uni_hid_device_t device;
|
||||
static unsigned ready_count;
|
||||
static uint8_t requested_feature;
|
||||
static uint8_t output[79];
|
||||
static unsigned output_count;
|
||||
static uint32_t now_ms;
|
||||
static bool transport_available = true;
|
||||
static int send_status = ERROR_CODE_SUCCESS;
|
||||
static unsigned disconnect_count;
|
||||
static struct {
|
||||
btstack_timer_source_t* timer;
|
||||
uint32_t deadline;
|
||||
bool active;
|
||||
} timers[2];
|
||||
|
||||
void uni_log(const char* fmt, ...) { (void)fmt; }
|
||||
void uni_hid_device_send_ctrl_report(uni_hid_device_t* d, const uint8_t* bytes, uint16_t len) {
|
||||
assert(d == &device && len == 2 && bytes[0] == 0x43);
|
||||
requested_feature = bytes[1];
|
||||
}
|
||||
void uni_hid_device_send_intr_report(uni_hid_device_t* d, const uint8_t* bytes, uint16_t len) {
|
||||
assert(d == &device && len == sizeof(output));
|
||||
memcpy(output, bytes, len);
|
||||
++output_count;
|
||||
}
|
||||
uint32_t btstack_run_loop_get_time_ms(void) { return now_ms; }
|
||||
int l2cap_can_send_packet_now(uint16_t cid) {
|
||||
assert(cid == device.conn.interrupt_cid);
|
||||
return transport_available;
|
||||
}
|
||||
int l2cap_send(uint16_t cid, uint8_t* bytes, uint16_t len) {
|
||||
assert(cid == device.conn.interrupt_cid);
|
||||
if (send_status != ERROR_CODE_SUCCESS) return send_status;
|
||||
uni_hid_device_send_intr_report(&device, bytes, len);
|
||||
return ERROR_CODE_SUCCESS;
|
||||
}
|
||||
void uni_hid_device_disconnect(uni_hid_device_t* d) {
|
||||
assert(d == &device);
|
||||
++disconnect_count;
|
||||
uni_hid_parser_ds5_bridge_teardown(d);
|
||||
}
|
||||
bool uni_hid_device_set_ready_complete(uni_hid_device_t* d) {
|
||||
assert(d == &device);
|
||||
++ready_count;
|
||||
return true;
|
||||
}
|
||||
uni_hid_device_t* uni_hid_device_create_virtual(uni_hid_device_t* d) { (void)d; return NULL; }
|
||||
void uni_hid_device_set_cod(uni_hid_device_t* d, uint32_t cod) { (void)d; (void)cod; }
|
||||
void uni_hid_device_connect(uni_hid_device_t* d) { (void)d; }
|
||||
void uni_hid_device_process_controller(uni_hid_device_t* d) { (void)d; }
|
||||
uint8_t uni_hid_parser_hat_to_dpad(uint8_t hat) {
|
||||
const uint8_t values[8] = {DPAD_UP, DPAD_UP | DPAD_RIGHT, DPAD_RIGHT,
|
||||
DPAD_RIGHT | DPAD_DOWN, DPAD_DOWN, DPAD_DOWN | DPAD_LEFT,
|
||||
DPAD_LEFT, DPAD_LEFT | DPAD_UP};
|
||||
return hat < 8 ? values[hat] : 0;
|
||||
}
|
||||
|
||||
void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t ms) {
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
if (timers[i].timer != timer && timers[i].timer != NULL) continue;
|
||||
timers[i].timer = timer;
|
||||
timers[i].deadline = now_ms + ms;
|
||||
return;
|
||||
}
|
||||
assert(false);
|
||||
}
|
||||
void btstack_run_loop_add_timer(btstack_timer_source_t* timer) {
|
||||
for (unsigned i = 0; i < 2; ++i)
|
||||
if (timers[i].timer == timer) { timers[i].active = true; return; }
|
||||
assert(false);
|
||||
}
|
||||
bool btstack_run_loop_remove_timer(btstack_timer_source_t* timer) {
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
if (timers[i].timer != timer) continue;
|
||||
const bool was_active = timers[i].active;
|
||||
timers[i].active = false;
|
||||
return was_active;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static void advance(uint32_t time) {
|
||||
now_ms = time;
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
if (!timers[i].active || (int32_t)(now_ms - timers[i].deadline) < 0) continue;
|
||||
timers[i].active = false;
|
||||
timers[i].timer->process(timers[i].timer);
|
||||
}
|
||||
}
|
||||
static void put16(uint8_t* bytes, int16_t value) {
|
||||
bytes[0] = (uint16_t)value;
|
||||
bytes[1] = (uint16_t)value >> 8;
|
||||
}
|
||||
static void put32(uint8_t* bytes, uint32_t value) {
|
||||
for (unsigned i = 0; i < 4; ++i) bytes[i] = value >> (8 * i);
|
||||
}
|
||||
static void seal(uint8_t* bytes, size_t size, uint8_t transaction) {
|
||||
uint32_t crc = uni_crc32_le(UINT32_MAX, &transaction, 1);
|
||||
crc = ~uni_crc32_le(crc, bytes, size - 4);
|
||||
put32(bytes + size - 4, crc);
|
||||
}
|
||||
static void feature(uint8_t* bytes, uint16_t len) {
|
||||
seal(bytes, len, 0xa3);
|
||||
uni_hid_parser_ds5_parse_feature_report(&device, bytes, len);
|
||||
}
|
||||
static void calibration(uint8_t bytes[41], bool fallback) {
|
||||
memset(bytes, 0, 41);
|
||||
bytes[0] = 5;
|
||||
const int16_t bias[3] = {10, -20, 30};
|
||||
for (unsigned axis = 0; axis < 3; ++axis) {
|
||||
put16(bytes + 1 + axis * 2, bias[axis]);
|
||||
put16(bytes + 7 + axis * 4, bias[axis] + 100);
|
||||
put16(bytes + 9 + axis * 4, bias[axis] - 100);
|
||||
put16(bytes + 23 + axis * 4, 8192);
|
||||
put16(bytes + 25 + axis * 4, -8192);
|
||||
}
|
||||
put16(bytes + 19, 100);
|
||||
put16(bytes + 21, 100);
|
||||
if (fallback) {
|
||||
put16(bytes + 23, 0);
|
||||
put16(bytes + 25, 0);
|
||||
}
|
||||
}
|
||||
static void input(uint8_t bytes[78], uint32_t timestamp) {
|
||||
memset(bytes, 0, 78);
|
||||
bytes[0] = 0x31;
|
||||
bytes[2] = bytes[3] = bytes[4] = bytes[5] = 127;
|
||||
bytes[9] = 0x28; // Cross + neutral hat.
|
||||
bytes[11] = 0x02; // Touchpad click, not mute.
|
||||
put16(bytes + 17, 11);
|
||||
put16(bytes + 19, -20);
|
||||
put16(bytes + 21, 30);
|
||||
put16(bytes + 25, 8192);
|
||||
put32(bytes + 29, timestamp);
|
||||
seal(bytes, 78, 0xa1);
|
||||
}
|
||||
static uni_ds5_bridge_snapshot_t feed(uint8_t* bytes, uint16_t len, bool admitted) {
|
||||
uni_hid_parser_ds5_init_report(&device);
|
||||
uni_hid_parser_ds5_parse_input_report(&device, bytes, len);
|
||||
uni_ds5_bridge_snapshot_t snapshot = {0};
|
||||
assert(uni_hid_parser_ds5_bridge_snapshot(&device, &snapshot) == admitted);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
device.controller_type = CONTROLLER_TYPE_PS5Controller;
|
||||
device.product_id = 0x0df2; // Edge takes the real PS5 path.
|
||||
device.report_parser.setup = uni_hid_parser_ds5_setup;
|
||||
device.conn.interrupt_cid = 0x40;
|
||||
uni_hid_parser_ds5_setup(&device);
|
||||
assert(requested_feature == 9);
|
||||
uint8_t pairing[20] = {9};
|
||||
feature(pairing, sizeof(pairing));
|
||||
assert(requested_feature == 0x20);
|
||||
uint8_t firmware[64] = {0x20};
|
||||
feature(firmware, sizeof(firmware));
|
||||
assert(requested_feature == 5);
|
||||
uint8_t calib[41];
|
||||
calibration(calib, false);
|
||||
seal(calib, sizeof(calib), 0xa3);
|
||||
uni_hid_parser_ds5_parse_feature_report(&device, calib, 40);
|
||||
assert(ready_count == 0); // A partial feature cannot initialize calibration.
|
||||
calib[25] ^= 1;
|
||||
uni_hid_parser_ds5_parse_feature_report(&device, calib, sizeof(calib));
|
||||
assert(ready_count == 0); // Nor can a full feature with a corrupt CRC.
|
||||
calibration(calib, true);
|
||||
feature(calib, sizeof(calib));
|
||||
assert(ready_count == 1);
|
||||
uint8_t report[78];
|
||||
input(report, 100);
|
||||
uni_ds5_bridge_snapshot_t snapshot = feed(report, sizeof(report), true);
|
||||
assert(!snapshot.motion_valid && snapshot.motion_sequence == 0);
|
||||
assert(device.controller.gamepad.buttons & BUTTON_A); // Controls survive fallback.
|
||||
calibration(calib, false);
|
||||
feature(calib, sizeof(calib));
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(!snapshot.motion_valid && snapshot.motion_sequence == 0); // Calibration alone is not fresh motion.
|
||||
input(report, 101);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(snapshot.motion_valid && snapshot.motion_sequence == 1);
|
||||
assert(device.controller.gamepad.gyro[0] == 1024 && device.controller.gamepad.gyro[1] == 0);
|
||||
assert(device.controller.gamepad.accel[1] == 8192);
|
||||
assert(device.controller.gamepad.misc_buttons & MISC_BUTTON_CAPTURE);
|
||||
const uint32_t report_sequence = snapshot.report_sequence;
|
||||
uni_ds5_bridge_snapshot_t polled;
|
||||
assert(uni_hid_parser_ds5_bridge_snapshot(&device, &polled) && polled.report_sequence == report_sequence);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(!snapshot.motion_valid && snapshot.motion_sequence == 1);
|
||||
input(report, 99);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(!snapshot.motion_valid && snapshot.motion_sequence == 1);
|
||||
input(report, 102);
|
||||
feed(report, 77, false);
|
||||
report[9] ^= 0x20;
|
||||
feed(report, sizeof(report), false);
|
||||
feed(NULL, 0, false);
|
||||
input(report, 102);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(snapshot.motion_valid && snapshot.motion_sequence == 2);
|
||||
|
||||
// A real uint32 sensor-clock wrap is forward progress, not a duplicate.
|
||||
uni_hid_parser_ds5_setup(&device);
|
||||
calibration(calib, false);
|
||||
feature(calib, sizeof(calib));
|
||||
input(report, UINT32_MAX - 15);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(snapshot.motion_valid && snapshot.motion_sequence == 1);
|
||||
input(report, 16);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(snapshot.motion_valid && snapshot.motion_sequence == 2);
|
||||
|
||||
const unsigned before_busy = output_count;
|
||||
transport_available = false;
|
||||
assert(!uni_hid_parser_ds5_bridge_rumble(&device, 60, 31, 217) && output_count == before_busy);
|
||||
transport_available = true;
|
||||
send_status = BTSTACK_ACL_BUFFERS_FULL;
|
||||
assert(!uni_hid_parser_ds5_bridge_rumble(&device, 60, 31, 217) && output_count == before_busy);
|
||||
send_status = ERROR_CODE_SUCCESS;
|
||||
assert(uni_hid_parser_ds5_bridge_rumble(&device, 60, 31, 217));
|
||||
assert(output[6] == 31 && output[7] == 217); // Wire motor right/left, not callback echoes.
|
||||
advance(59);
|
||||
assert(output[6] == 31 && output[7] == 217);
|
||||
advance(60);
|
||||
assert(output[6] == 0 && output[7] == 0); // Real parser's finite duration timer stops both.
|
||||
uni_hid_parser_ds5_play_dual_rumble(&device, 100, 1000, 90, 0);
|
||||
const unsigned sent = output_count;
|
||||
uni_hid_parser_ds5_bridge_teardown(&device);
|
||||
advance(2000);
|
||||
assert(output_count == sent && !uni_hid_parser_ds5_bridge_snapshot(&device, &snapshot));
|
||||
uni_hid_parser_ds5_play_dual_rumble(&device, 0, 1000, 0, 90);
|
||||
const unsigned active_sent = output_count;
|
||||
uni_hid_parser_ds5_bridge_teardown(&device);
|
||||
advance(4000);
|
||||
assert(output_count == active_sent); // No timer callback into reused parser memory.
|
||||
assert(uni_hid_parser_ds5_bridge_rumble(&device, 60, 31, 217));
|
||||
transport_available = false;
|
||||
advance(4060);
|
||||
assert(output[6] == 31 && output[7] == 217 && disconnect_count == 0);
|
||||
transport_available = true;
|
||||
advance(4065);
|
||||
assert(output[6] == 0 && output[7] == 0 && disconnect_count == 0);
|
||||
assert(uni_hid_parser_ds5_bridge_rumble(&device, 60, 31, 217));
|
||||
transport_available = false;
|
||||
advance(4125);
|
||||
advance(6125);
|
||||
assert(disconnect_count == 1); // OFF cannot stall forever on a live link.
|
||||
const unsigned after_disconnect = output_count;
|
||||
advance(9000);
|
||||
assert(output_count == after_disconnect);
|
||||
puts("DualSense calibrated admission and bounded driver lifetime passed");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue