// Copyright 2020-2021, N Madsen. // Copyright 2020-2023, Collabora, Ltd. // Copyright 2020-2023, Jan Schmidt // SPDX-License-Identifier: BSL-1.0 /*! * @file * @brief Driver for WMR Controllers. * @author Jan Schmidt * @ingroup drv_wmr */ #include "math/m_api.h" #include "util/u_device.h" #include "util/u_trace_marker.h" #include "util/u_var.h" #include #include #include #include #include #include "wmr_common.h" #include "wmr_controller.h" #ifdef XRT_DOXYGEN #define WMR_PACKED #else #define WMR_PACKED __attribute__((packed)) #endif /*! * Indices in input list of each input. */ enum wmr_controller_og_input_index { WMR_CONTROLLER_INDEX_MENU_CLICK, WMR_CONTROLLER_INDEX_HOME_CLICK, WMR_CONTROLLER_INDEX_SQUEEZE_CLICK, WMR_CONTROLLER_INDEX_TRIGGER_VALUE, WMR_CONTROLLER_INDEX_THUMBSTICK_CLICK, WMR_CONTROLLER_INDEX_THUMBSTICK, WMR_CONTROLLER_INDEX_TRACKPAD_CLICK, WMR_CONTROLLER_INDEX_TRACKPAD_TOUCH, WMR_CONTROLLER_INDEX_TRACKPAD, WMR_CONTROLLER_INDEX_GRIP_POSE, WMR_CONTROLLER_INDEX_AIM_POSE, }; #define SET_WMR_INPUT(wcb, NAME) (wcb->base.inputs[WMR_CONTROLLER_INDEX_##NAME].name = XRT_INPUT_WMR_##NAME) #define SET_ODYSSEY_INPUT(wcb, NAME) \ (wcb->base.inputs[WMR_CONTROLLER_INDEX_##NAME].name = XRT_INPUT_ODYSSEY_CONTROLLER_##NAME) /* * * Bindings * */ static struct xrt_binding_input_pair simple_inputs_og[4] = { {XRT_INPUT_SIMPLE_SELECT_CLICK, XRT_INPUT_WMR_TRIGGER_VALUE}, {XRT_INPUT_SIMPLE_MENU_CLICK, XRT_INPUT_WMR_MENU_CLICK}, {XRT_INPUT_SIMPLE_GRIP_POSE, XRT_INPUT_WMR_GRIP_POSE}, {XRT_INPUT_SIMPLE_AIM_POSE, XRT_INPUT_WMR_AIM_POSE}, }; static struct xrt_binding_output_pair simple_outputs_og[1] = { {XRT_OUTPUT_NAME_SIMPLE_VIBRATION, XRT_OUTPUT_NAME_WMR_HAPTIC}, }; static struct xrt_binding_input_pair vive_inputs_og[10] = { {XRT_INPUT_VIVE_SYSTEM_CLICK, XRT_INPUT_WMR_HOME_CLICK}, {XRT_INPUT_VIVE_SQUEEZE_CLICK, XRT_INPUT_WMR_SQUEEZE_CLICK}, {XRT_INPUT_VIVE_MENU_CLICK, XRT_INPUT_WMR_MENU_CLICK}, {XRT_INPUT_VIVE_TRIGGER_CLICK, XRT_INPUT_WMR_TRIGGER_VALUE}, {XRT_INPUT_VIVE_TRIGGER_VALUE, XRT_INPUT_WMR_TRIGGER_VALUE}, {XRT_INPUT_VIVE_TRACKPAD, XRT_INPUT_WMR_TRACKPAD}, {XRT_INPUT_VIVE_TRACKPAD_CLICK, XRT_INPUT_WMR_TRACKPAD_CLICK}, {XRT_INPUT_VIVE_TRACKPAD_TOUCH, XRT_INPUT_WMR_TRACKPAD_TOUCH}, {XRT_INPUT_VIVE_GRIP_POSE, XRT_INPUT_WMR_GRIP_POSE}, {XRT_INPUT_VIVE_AIM_POSE, XRT_INPUT_WMR_AIM_POSE}, }; static struct xrt_binding_output_pair vive_outputs_og[1] = { {XRT_OUTPUT_NAME_VIVE_HAPTIC, XRT_OUTPUT_NAME_WMR_HAPTIC}, }; static struct xrt_binding_profile binding_profiles_og[2] = { { .name = XRT_DEVICE_VIVE_WAND, .inputs = vive_inputs_og, .input_count = ARRAY_SIZE(vive_inputs_og), .outputs = vive_outputs_og, .output_count = ARRAY_SIZE(vive_outputs_og), }, { .name = XRT_DEVICE_SIMPLE_CONTROLLER, .inputs = simple_inputs_og, .input_count = ARRAY_SIZE(simple_inputs_og), .outputs = simple_outputs_og, .output_count = ARRAY_SIZE(simple_outputs_og), }, }; static struct xrt_binding_input_pair simple_inputs_odyssey[4] = { {XRT_INPUT_SIMPLE_SELECT_CLICK, XRT_INPUT_ODYSSEY_CONTROLLER_TRIGGER_VALUE}, {XRT_INPUT_SIMPLE_MENU_CLICK, XRT_INPUT_ODYSSEY_CONTROLLER_MENU_CLICK}, {XRT_INPUT_SIMPLE_GRIP_POSE, XRT_INPUT_ODYSSEY_CONTROLLER_GRIP_POSE}, {XRT_INPUT_SIMPLE_AIM_POSE, XRT_INPUT_ODYSSEY_CONTROLLER_AIM_POSE}, }; static struct xrt_binding_output_pair simple_outputs_odyssey[1] = { {XRT_OUTPUT_NAME_SIMPLE_VIBRATION, XRT_OUTPUT_NAME_ODYSSEY_CONTROLLER_HAPTIC}, }; static struct xrt_binding_input_pair wmr_inputs_odyssey[11] = { {XRT_INPUT_WMR_MENU_CLICK, XRT_INPUT_ODYSSEY_CONTROLLER_MENU_CLICK}, {XRT_INPUT_WMR_SQUEEZE_CLICK, XRT_INPUT_ODYSSEY_CONTROLLER_SQUEEZE_CLICK}, {XRT_INPUT_WMR_TRIGGER_VALUE, XRT_INPUT_ODYSSEY_CONTROLLER_TRIGGER_VALUE}, {XRT_INPUT_WMR_THUMBSTICK_CLICK, XRT_INPUT_ODYSSEY_CONTROLLER_THUMBSTICK_CLICK}, {XRT_INPUT_WMR_THUMBSTICK, XRT_INPUT_ODYSSEY_CONTROLLER_THUMBSTICK}, {XRT_INPUT_WMR_TRACKPAD_CLICK, XRT_INPUT_ODYSSEY_CONTROLLER_TRACKPAD_CLICK}, {XRT_INPUT_WMR_TRACKPAD_TOUCH, XRT_INPUT_ODYSSEY_CONTROLLER_TRACKPAD_TOUCH}, {XRT_INPUT_WMR_TRACKPAD, XRT_INPUT_ODYSSEY_CONTROLLER_TRACKPAD}, {XRT_INPUT_WMR_GRIP_POSE, XRT_INPUT_ODYSSEY_CONTROLLER_GRIP_POSE}, {XRT_INPUT_WMR_AIM_POSE, XRT_INPUT_ODYSSEY_CONTROLLER_AIM_POSE}, {XRT_INPUT_WMR_HOME_CLICK, XRT_INPUT_ODYSSEY_CONTROLLER_HOME_CLICK}, }; static struct xrt_binding_output_pair wmr_outputs_odyssey[1] = { {XRT_OUTPUT_NAME_WMR_HAPTIC, XRT_OUTPUT_NAME_ODYSSEY_CONTROLLER_HAPTIC}, }; static struct xrt_binding_input_pair vive_inputs_odyssey[10] = { {XRT_INPUT_VIVE_SYSTEM_CLICK, XRT_INPUT_ODYSSEY_CONTROLLER_HOME_CLICK}, {XRT_INPUT_VIVE_SQUEEZE_CLICK, XRT_INPUT_ODYSSEY_CONTROLLER_SQUEEZE_CLICK}, {XRT_INPUT_VIVE_MENU_CLICK, XRT_INPUT_ODYSSEY_CONTROLLER_MENU_CLICK}, {XRT_INPUT_VIVE_TRIGGER_CLICK, XRT_INPUT_ODYSSEY_CONTROLLER_TRIGGER_VALUE}, {XRT_INPUT_VIVE_TRIGGER_VALUE, XRT_INPUT_ODYSSEY_CONTROLLER_TRIGGER_VALUE}, {XRT_INPUT_VIVE_TRACKPAD, XRT_INPUT_ODYSSEY_CONTROLLER_TRACKPAD}, {XRT_INPUT_VIVE_TRACKPAD_CLICK, XRT_INPUT_ODYSSEY_CONTROLLER_TRACKPAD_CLICK}, {XRT_INPUT_VIVE_TRACKPAD_TOUCH, XRT_INPUT_ODYSSEY_CONTROLLER_TRACKPAD_TOUCH}, {XRT_INPUT_VIVE_GRIP_POSE, XRT_INPUT_ODYSSEY_CONTROLLER_GRIP_POSE}, {XRT_INPUT_VIVE_AIM_POSE, XRT_INPUT_ODYSSEY_CONTROLLER_AIM_POSE}, }; static struct xrt_binding_output_pair vive_outputs_odyssey[1] = { {XRT_OUTPUT_NAME_VIVE_HAPTIC, XRT_OUTPUT_NAME_ODYSSEY_CONTROLLER_HAPTIC}, }; static struct xrt_binding_profile binding_profiles_odyssey[3] = { { .name = XRT_DEVICE_WMR_CONTROLLER, .inputs = wmr_inputs_odyssey, .input_count = ARRAY_SIZE(wmr_inputs_odyssey), .outputs = wmr_outputs_odyssey, .output_count = ARRAY_SIZE(wmr_outputs_odyssey), }, { .name = XRT_DEVICE_VIVE_WAND, .inputs = vive_inputs_odyssey, .input_count = ARRAY_SIZE(vive_inputs_odyssey), .outputs = vive_outputs_odyssey, .output_count = ARRAY_SIZE(vive_outputs_odyssey), }, { .name = XRT_DEVICE_SIMPLE_CONTROLLER, .inputs = simple_inputs_odyssey, .input_count = ARRAY_SIZE(simple_inputs_odyssey), .outputs = simple_outputs_odyssey, .output_count = ARRAY_SIZE(simple_outputs_odyssey), }, }; /* OG WMR Controller inputs struct */ struct wmr_controller_og_input { // buttons clicked bool menu; bool home; bool bt_pairing; bool squeeze; // Actually a "squeeze" click float trigger; struct { bool click; struct xrt_vec2 values; } thumbstick; struct { bool click; bool touch; struct xrt_vec2 values; } trackpad; uint8_t battery; struct { uint64_t timestamp_ticks; struct xrt_vec3 acc; struct xrt_vec3 gyro; int32_t temperature; } imu; }; #undef WMR_PACKED /* OG WMR Controller device struct */ struct wmr_controller_og { struct wmr_controller_base base; //! The last decoded package of IMU and button data struct wmr_controller_og_input last_inputs; }; /* * * WMR Motion Controller protocol helpers * */ static inline void vec3_from_wmr_controller_accel(const int32_t sample[3], struct xrt_vec3 *out_vec) { // Reverb G1 observation: 1g is approximately 490,000. out_vec->x = (float)sample[0] / (98000 / 2); out_vec->y = (float)sample[1] / (98000 / 2); out_vec->z = (float)sample[2] / (98000 / 2); } static inline void vec3_from_wmr_controller_gyro(const int32_t sample[3], struct xrt_vec3 *out_vec) { out_vec->x = (float)sample[0] * 0.00001f; out_vec->y = (float)sample[1] * 0.00001f; out_vec->z = (float)sample[2] * 0.00001f; } static bool wmr_controller_og_packet_parse(struct wmr_controller_og *ctrl, const unsigned char *buffer, size_t len) { struct wmr_controller_og_input *last_input = &ctrl->last_inputs; struct wmr_controller_base *wcb = (struct wmr_controller_base *)(ctrl); if (len != 44) { U_LOG_IFL_E(wcb->log_level, "WMR Controller: unexpected message length: %zd", len); return false; } const unsigned char *p = buffer; // Read buttons uint8_t buttons = read8(&p); last_input->thumbstick.click = buttons & 0x01; last_input->home = buttons & 0x02; last_input->menu = buttons & 0x04; last_input->squeeze = buttons & 0x08; // squeeze-click last_input->trackpad.click = buttons & 0x10; last_input->bt_pairing = buttons & 0x20; last_input->trackpad.touch = buttons & 0x40; // Read thumbstick coordinates (12 bit resolution) int16_t stick_x = read8(&p); uint8_t nibbles = read8(&p); stick_x += ((nibbles & 0x0F) << 8); int16_t stick_y = (nibbles >> 4); stick_y += (read8(&p) << 4); last_input->thumbstick.values.x = (float)(stick_x - 0x07FF) / 0x07FF; if (last_input->thumbstick.values.x > 1.0f) { last_input->thumbstick.values.x = 1.0f; } last_input->thumbstick.values.y = (float)(stick_y - 0x07FF) / 0x07FF; if (last_input->thumbstick.values.y > 1.0f) { last_input->thumbstick.values.y = 1.0f; } // Read trigger value (0x00 - 0xFF) last_input->trigger = (float)read8(&p) / 0xFF; // Read trackpad coordinates (0x00 - 0x64. Both are 0xFF when untouched) uint8_t trackpad_x = read8(&p); uint8_t trackpad_y = read8(&p); last_input->trackpad.values.x = (trackpad_x == 0xFF) ? 0.0f : (float)(trackpad_x - 0x32) / 0x32; last_input->trackpad.values.y = (trackpad_y == 0xFF) ? 0.0f : (float)(trackpad_y - 0x32) / 0x32; last_input->battery = read8(&p); int32_t acc[3]; acc[0] = read24(&p); // x acc[1] = read24(&p); // y acc[2] = read24(&p); // z vec3_from_wmr_controller_accel(acc, &last_input->imu.acc); math_matrix_3x3_transform_vec3(&wcb->config.sensors.accel.mix_matrix, &last_input->imu.acc, &last_input->imu.acc); math_vec3_accum(&wcb->config.sensors.accel.bias_offsets, &last_input->imu.acc); math_quat_rotate_vec3(&wcb->config.sensors.transforms.P_oxr_acc.orientation, &last_input->imu.acc, &last_input->imu.acc); U_LOG_IFL_T(wcb->log_level, "Accel [m/s^2] : %f", sqrtf(last_input->imu.acc.x * last_input->imu.acc.x + last_input->imu.acc.y * last_input->imu.acc.y + last_input->imu.acc.z * last_input->imu.acc.z)); last_input->imu.temperature = read16(&p); int32_t gyro[3]; gyro[0] = read24(&p); gyro[1] = read24(&p); gyro[2] = read24(&p); vec3_from_wmr_controller_gyro(gyro, &last_input->imu.gyro); math_matrix_3x3_transform_vec3(&wcb->config.sensors.gyro.mix_matrix, &last_input->imu.gyro, &last_input->imu.gyro); math_vec3_accum(&wcb->config.sensors.gyro.bias_offsets, &last_input->imu.gyro); math_quat_rotate_vec3(&wcb->config.sensors.transforms.P_oxr_gyr.orientation, &last_input->imu.gyro, &last_input->imu.gyro); uint32_t prev_ticks = last_input->imu.timestamp_ticks & UINT32_C(0xFFFFFFFF); // Write the new ticks value into the lower half of timestamp_ticks last_input->imu.timestamp_ticks &= (UINT64_C(0xFFFFFFFF) << 32u); last_input->imu.timestamp_ticks += (uint32_t)read32(&p); if ((last_input->imu.timestamp_ticks & UINT64_C(0xFFFFFFFF)) < prev_ticks) { // Timer overflow, so increment the upper half of timestamp_ticks last_input->imu.timestamp_ticks += (UINT64_C(0x1) << 32u); } /* Todo: More decoding here read16(&p); // Unknown. Seems to depend on controller orientation (probably mag) read32(&p); // Unknown. read16(&p); // Unknown. Device state, etc. read16(&p); read16(&p); */ return true; } static bool handle_input_packet(struct wmr_controller_base *wcb, uint64_t time_ns, uint8_t *buffer, uint32_t buf_size) { struct wmr_controller_og *ctrl = (struct wmr_controller_og *)(wcb); bool b = wmr_controller_og_packet_parse(ctrl, buffer, buf_size); if (b) { m_imu_3dof_update(&wcb->fusion, ctrl->last_inputs.imu.timestamp_ticks * WMR_MOTION_CONTROLLER_NS_PER_TICK, &ctrl->last_inputs.imu.acc, &ctrl->last_inputs.imu.gyro); wcb->last_imu_timestamp_ns = time_ns; wcb->last_angular_velocity = ctrl->last_inputs.imu.gyro; } return b; } static xrt_result_t wmr_controller_og_update_inputs(struct xrt_device *xdev) { DRV_TRACE_MARKER(); struct wmr_controller_og *ctrl = (struct wmr_controller_og *)(xdev); struct wmr_controller_base *wcb = (struct wmr_controller_base *)(xdev); os_mutex_lock(&wcb->data_lock); struct xrt_input *xrt_inputs = xdev->inputs; struct wmr_controller_og_input *cur_inputs = &ctrl->last_inputs; xrt_inputs[WMR_CONTROLLER_INDEX_MENU_CLICK].value.boolean = cur_inputs->menu; xrt_inputs[WMR_CONTROLLER_INDEX_HOME_CLICK].value.boolean = cur_inputs->home; xrt_inputs[WMR_CONTROLLER_INDEX_SQUEEZE_CLICK].value.boolean = cur_inputs->squeeze; xrt_inputs[WMR_CONTROLLER_INDEX_TRIGGER_VALUE].value.vec1.x = cur_inputs->trigger; xrt_inputs[WMR_CONTROLLER_INDEX_THUMBSTICK_CLICK].value.boolean = cur_inputs->thumbstick.click; xrt_inputs[WMR_CONTROLLER_INDEX_THUMBSTICK].value.vec2 = cur_inputs->thumbstick.values; xrt_inputs[WMR_CONTROLLER_INDEX_TRACKPAD_CLICK].value.boolean = cur_inputs->trackpad.click; xrt_inputs[WMR_CONTROLLER_INDEX_TRACKPAD_TOUCH].value.boolean = cur_inputs->trackpad.touch; xrt_inputs[WMR_CONTROLLER_INDEX_TRACKPAD].value.vec2 = cur_inputs->trackpad.values; os_mutex_unlock(&wcb->data_lock); return XRT_SUCCESS; } static void wmr_controller_og_destroy(struct xrt_device *xdev) { struct wmr_controller_base *wcb = (struct wmr_controller_base *)(xdev); wmr_controller_base_deinit(wcb); free(wcb); } struct wmr_controller_base * wmr_controller_og_create(struct wmr_controller_connection *conn, enum xrt_device_type controller_type, uint16_t pid, enum u_logging_level log_level) { DRV_TRACE_MARKER(); enum u_device_alloc_flags flags = U_DEVICE_ALLOC_TRACKING_NONE; struct wmr_controller_og *ctrl = U_DEVICE_ALLOCATE(struct wmr_controller_og, flags, 11, 1); struct wmr_controller_base *wcb = (struct wmr_controller_base *)(ctrl); if (!wmr_controller_base_init(wcb, conn, controller_type, log_level, wmr_controller_og_destroy)) { wmr_controller_og_destroy(&wcb->base); return NULL; } wcb->handle_input_packet = handle_input_packet; // Only set those we want to overwrite. wcb->base.update_inputs = wmr_controller_og_update_inputs; if (pid == ODYSSEY_CONTROLLER_PID) { wcb->base.name = XRT_DEVICE_SAMSUNG_ODYSSEY_CONTROLLER; } else { wcb->base.name = XRT_DEVICE_WMR_CONTROLLER; } if (pid == ODYSSEY_CONTROLLER_PID) { SET_ODYSSEY_INPUT(wcb, MENU_CLICK); SET_ODYSSEY_INPUT(wcb, HOME_CLICK); SET_ODYSSEY_INPUT(wcb, SQUEEZE_CLICK); SET_ODYSSEY_INPUT(wcb, TRIGGER_VALUE); SET_ODYSSEY_INPUT(wcb, THUMBSTICK_CLICK); SET_ODYSSEY_INPUT(wcb, THUMBSTICK); SET_ODYSSEY_INPUT(wcb, TRACKPAD_CLICK); SET_ODYSSEY_INPUT(wcb, TRACKPAD_TOUCH); SET_ODYSSEY_INPUT(wcb, TRACKPAD); SET_ODYSSEY_INPUT(wcb, GRIP_POSE); SET_ODYSSEY_INPUT(wcb, AIM_POSE); wcb->base.outputs[0].name = XRT_OUTPUT_NAME_ODYSSEY_CONTROLLER_HAPTIC; wcb->base.binding_profiles = binding_profiles_odyssey; wcb->base.binding_profile_count = ARRAY_SIZE(binding_profiles_odyssey); } else { SET_WMR_INPUT(wcb, MENU_CLICK); SET_WMR_INPUT(wcb, HOME_CLICK); SET_WMR_INPUT(wcb, SQUEEZE_CLICK); SET_WMR_INPUT(wcb, TRIGGER_VALUE); SET_WMR_INPUT(wcb, THUMBSTICK_CLICK); SET_WMR_INPUT(wcb, THUMBSTICK); SET_WMR_INPUT(wcb, TRACKPAD_CLICK); SET_WMR_INPUT(wcb, TRACKPAD_TOUCH); SET_WMR_INPUT(wcb, TRACKPAD); SET_WMR_INPUT(wcb, GRIP_POSE); SET_WMR_INPUT(wcb, AIM_POSE); wcb->base.outputs[0].name = XRT_OUTPUT_NAME_WMR_HAPTIC; wcb->base.binding_profiles = binding_profiles_og; wcb->base.binding_profile_count = ARRAY_SIZE(binding_profiles_og); } for (uint32_t i = 0; i < wcb->base.input_count; i++) { wcb->base.inputs[0].active = true; } ctrl->last_inputs.imu.timestamp_ticks = 0; u_var_add_bool(wcb, &ctrl->last_inputs.menu, "input.menu"); u_var_add_bool(wcb, &ctrl->last_inputs.home, "input.home"); u_var_add_bool(wcb, &ctrl->last_inputs.bt_pairing, "input.bt_pairing"); u_var_add_bool(wcb, &ctrl->last_inputs.squeeze, "input.squeeze"); u_var_add_f32(wcb, &ctrl->last_inputs.trigger, "input.trigger"); u_var_add_u8(wcb, &ctrl->last_inputs.battery, "input.battery"); u_var_add_bool(wcb, &ctrl->last_inputs.thumbstick.click, "input.thumbstick.click"); u_var_add_f32(wcb, &ctrl->last_inputs.thumbstick.values.x, "input.thumbstick.values.y"); u_var_add_f32(wcb, &ctrl->last_inputs.thumbstick.values.y, "input.thumbstick.values.x"); u_var_add_bool(wcb, &ctrl->last_inputs.trackpad.click, "input.trackpad.click"); u_var_add_bool(wcb, &ctrl->last_inputs.trackpad.touch, "input.trackpad.touch"); u_var_add_f32(wcb, &ctrl->last_inputs.trackpad.values.x, "input.trackpad.values.x"); u_var_add_f32(wcb, &ctrl->last_inputs.trackpad.values.y, "input.trackpad.values.y"); u_var_add_ro_vec3_f32(wcb, &ctrl->last_inputs.imu.acc, "imu.acc"); u_var_add_ro_vec3_f32(wcb, &ctrl->last_inputs.imu.gyro, "imu.gyro"); u_var_add_i32(wcb, &ctrl->last_inputs.imu.temperature, "imu.temperature"); return wcb; }