/* * USB Gadget HID driver for Tegra X1 * * Copyright (c) 2019-2022 CTCaer * * This program is free software; you can redistribute it and/or modify it * under the terms and conditions of the GNU General Public License, * version 2, as published by the Free Software Foundation. * * This program is distributed in the hope it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include //#define DPRINTF(...) gfx_printf(__VA_ARGS__) #define DPRINTF(...) typedef struct _gamepad_report_t { u8 x; u8 y; u8 z; u8 rz; u8 hat:4; u8 btn1:1; u8 btn2:1; u8 btn3:1; u8 btn4:1; u8 btn5:1; u8 btn6:1; u8 btn7:1; u8 btn8:1; u8 btn9:1; u8 btn10:1; u8 btn11:1; u8 btn12:1; } __attribute__((packed)) gamepad_report_t; typedef struct _jc_cal_t { // 15ms * JC_CAL_MAX_STEPS = 240 ms. #define JC_CAL_MAX_STEPS 16 u32 cl_step; u32 cr_step; u16 clx_max; u16 clx_min; u16 cly_max; u16 cly_min; u16 crx_max; u16 crx_min; u16 cry_max; u16 cry_min; } jc_cal_t; enum { INPUT_POLL_HAS_PACKET, INPUT_POLL_NO_PACKET, INPUT_POLL_EXIT, }; static jc_cal_t jc_cal_ctx; static usb_ops_t usb_ops; static bool _jc_calibration(const jc_gamepad_rpt_t *jc_pad) { // Calibrate left stick. if (jc_cal_ctx.cl_step != JC_CAL_MAX_STEPS) { if (jc_pad->conn_l && jc_pad->lstick_x > 0x400 && jc_pad->lstick_y > 0x400 && jc_pad->lstick_x < 0xC00 && jc_pad->lstick_y < 0xC00) { jc_cal_ctx.cl_step++; jc_cal_ctx.clx_max = jc_pad->lstick_x + 0x72; jc_cal_ctx.clx_min = jc_pad->lstick_x - 0x72; jc_cal_ctx.cly_max = jc_pad->lstick_y + 0x72; jc_cal_ctx.cly_min = jc_pad->lstick_y - 0x72; if (jc_cal_ctx.cl_step != JC_CAL_MAX_STEPS) return false; } else return false; } // Calibrate right stick. if (jc_cal_ctx.cr_step != JC_CAL_MAX_STEPS) { if (jc_pad->conn_r && jc_pad->rstick_x > 0x400 && jc_pad->rstick_y > 0x400 && jc_pad->rstick_x < 0xC00 && jc_pad->rstick_y < 0xC00) { jc_cal_ctx.cr_step++; jc_cal_ctx.crx_max = jc_pad->rstick_x + 0x72; jc_cal_ctx.crx_min = jc_pad->rstick_x - 0x72; jc_cal_ctx.cry_max = jc_pad->rstick_y + 0x72; jc_cal_ctx.cry_min = jc_pad->rstick_y - 0x72; if (jc_cal_ctx.cr_step != JC_CAL_MAX_STEPS) return false; } else return false; } return true; } static int _jc_poll(gamepad_report_t *rpt) { static gamepad_report_t prev_rpt = {0}; // Poll Joy-Con. jc_gamepad_rpt_t *jc_pad = joycon_poll(); if (!jc_pad) return INPUT_POLL_NO_PACKET; // Exit emulation if Left stick and Home are pressed. if (jc_pad->l3 && jc_pad->home) return INPUT_POLL_EXIT; if (jc_cal_ctx.cl_step != JC_CAL_MAX_STEPS || jc_cal_ctx.cr_step != JC_CAL_MAX_STEPS) { if (!_jc_calibration(jc_pad)) return INPUT_POLL_NO_PACKET; } // Re-calibrate on disconnection. if (!jc_pad->conn_l) jc_cal_ctx.cl_step = 0; if (!jc_pad->conn_r) jc_cal_ctx.cr_step = 0; // Calculate left analog stick. if (jc_pad->lstick_x <= jc_cal_ctx.clx_max && jc_pad->lstick_x >= jc_cal_ctx.clx_min) rpt->x = 0x7F; else if (jc_pad->lstick_x > jc_cal_ctx.clx_max) { u16 x_raw = (jc_pad->lstick_x - jc_cal_ctx.clx_max) / 7; if (x_raw > 0x7F) x_raw = 0x7F; rpt->x = 0x7F + x_raw; } else { u16 x_raw = (jc_cal_ctx.clx_min - jc_pad->lstick_x) / 7; if (x_raw > 0x7F) x_raw = 0x7F; rpt->x = 0x7F - x_raw; } if (jc_pad->lstick_y <= jc_cal_ctx.cly_max && jc_pad->lstick_y >= jc_cal_ctx.cly_min) rpt->y = 0x7F; else if (jc_pad->lstick_y > jc_cal_ctx.cly_max) { u16 y_raw = (jc_pad->lstick_y - jc_cal_ctx.cly_max) / 7; if (y_raw > 0x7F) y_raw = 0x7F; // Hoag has inverted Y axis. if (!jc_pad->sio_mode) rpt->y = 0x7F - y_raw; else rpt->y = 0x7F + y_raw; } else { u16 y_raw = (jc_cal_ctx.cly_min - jc_pad->lstick_y) / 7; if (y_raw > 0x7F) y_raw = 0x7F; // Hoag has inverted Y axis. if (!jc_pad->sio_mode) rpt->y = 0x7F + y_raw; else rpt->y = 0x7F - y_raw; } // Calculate right analog stick. if (jc_pad->rstick_x <= jc_cal_ctx.crx_max && jc_pad->rstick_x >= jc_cal_ctx.crx_min) rpt->z = 0x7F; else if (jc_pad->rstick_x > jc_cal_ctx.crx_max) { u16 x_raw = (jc_pad->rstick_x - jc_cal_ctx.crx_max) / 7; if (x_raw > 0x7F) x_raw = 0x7F; rpt->z = 0x7F + x_raw; } else { u16 x_raw = (jc_cal_ctx.crx_min - jc_pad->rstick_x) / 7; if (x_raw > 0x7F) x_raw = 0x7F; rpt->z = 0x7F - x_raw; } if (jc_pad->rstick_y <= jc_cal_ctx.cry_max && jc_pad->rstick_y >= jc_cal_ctx.cry_min) rpt->rz = 0x7F; else if (jc_pad->rstick_y > jc_cal_ctx.cry_max) { u16 y_raw = (jc_pad->rstick_y - jc_cal_ctx.cry_max) / 7; if (y_raw > 0x7F) y_raw = 0x7F; // Hoag has inverted Y axis. if (!jc_pad->sio_mode) rpt->rz = 0x7F - y_raw; else rpt->rz = 0x7F + y_raw; } else { u16 y_raw = (jc_cal_ctx.cry_min - jc_pad->rstick_y) / 7; if (y_raw > 0x7F) y_raw = 0x7F; // Hoag has inverted Y axis. if (!jc_pad->sio_mode) rpt->rz = 0x7F + y_raw; else rpt->rz = 0x7F - y_raw; } // Set D-pad. switch ((jc_pad->buttons >> 16) & 0xF) { case 0: // none rpt->hat = 0xF; break; case 1: // down rpt->hat = 4; break; case 2: // up rpt->hat = 0; break; case 4: // right rpt->hat = 2; break; case 5: // down + right rpt->hat = 3; break; case 6: // up + right rpt->hat = 1; break; case 8: // left rpt->hat = 6; break; case 9: // down + left rpt->hat = 5; break; case 10: // up + left rpt->hat = 7; break; default: rpt->hat = 0xF; break; } // Set buttons. rpt->btn1 = jc_pad->b; // x. rpt->btn2 = jc_pad->a; // a. rpt->btn3 = jc_pad->y; // b. rpt->btn4 = jc_pad->x; // y. rpt->btn5 = jc_pad->l; rpt->btn6 = jc_pad->r; rpt->btn7 = jc_pad->zl; rpt->btn8 = jc_pad->zr; rpt->btn9 = jc_pad->minus; rpt->btn10 = jc_pad->plus; rpt->btn11 = jc_pad->l3; rpt->btn12 = jc_pad->r3; //rpt->btn13 = jc_pad->cap; //rpt->btn14 = jc_pad->home; if (!memcmp(rpt, &prev_rpt, sizeof(gamepad_report_t))) return INPUT_POLL_NO_PACKET; memcpy(&prev_rpt, rpt, sizeof(gamepad_report_t)); return INPUT_POLL_HAS_PACKET; } typedef struct _touchpad_report_t { u8 rpt_id; u8 tip_switch:1; u8 count:7; u8 id; //u16 z; u16 x; u16 y; } __attribute__((packed)) touchpad_report_t; static bool _fts_touch_read(touchpad_report_t *rpt) { static touch_event touchpad; touch_poll(&touchpad); rpt->rpt_id = 5; rpt->count = 1; // Decide touch enable. switch (touchpad.type & STMFTS_MASK_EVENT_ID) { //case STMFTS_EV_MULTI_TOUCH_ENTER: case STMFTS_EV_MULTI_TOUCH_MOTION: rpt->x = touchpad.x; rpt->y = touchpad.y; //rpt->z = touchpad.z; rpt->id = touchpad.fingers ? touchpad.fingers - 1 : 0; rpt->tip_switch = 1; break; case STMFTS_EV_MULTI_TOUCH_LEAVE: rpt->x = touchpad.x; rpt->y = touchpad.y; //rpt->z = touchpad.z; rpt->id = touchpad.fingers ? touchpad.fingers - 1 : 0; rpt->tip_switch = 0; break; case STMFTS_EV_NO_EVENT: return false; } return true; } static u8 _hid_transfer_start(usb_ctxt_t *usbs, u32 len) { u8 status = usb_ops.usb_device_ep1_in_write((u8 *)USB_EP_BULK_IN_BUF_ADDR, len, NULL, USB_XFER_SYNCED_CMD); if (status == USB_ERROR_XFER_ERROR) { usbs->set_text(usbs->label, "#FFDD00 Error:# EP IN transfer!"); if (usb_ops.usbd_flush_endpoint) usb_ops.usbd_flush_endpoint(USB_EP_BULK_IN); } // Linux mitigation: If timed out, clear status. if (status == USB_ERROR_TIMEOUT) return 0; return status; } static bool _hid_poll_jc(usb_ctxt_t *usbs) { int res = _jc_poll((gamepad_report_t *)USB_EP_BULK_IN_BUF_ADDR); if (res == INPUT_POLL_EXIT) return true; // Send HID report. if (res == INPUT_POLL_HAS_PACKET) if (_hid_transfer_start(usbs, sizeof(gamepad_report_t))) return true; // EP Error. return false; } static bool _hid_poll_touch(usb_ctxt_t *usbs) { _fts_touch_read((touchpad_report_t *)USB_EP_BULK_IN_BUF_ADDR); // Send HID report. if (_hid_transfer_start(usbs, sizeof(touchpad_report_t))) return true; // EP Error. return false; } int usb_device_gadget_hid(usb_ctxt_t *usbs) { int res = 0; u32 gadget_type; u32 polling_time; // Get USB Controller ops. if (hw_get_chip_id() == GP_HIDREV_MAJOR_T210) usb_device_get_ops(&usb_ops); else xusb_device_get_ops(&usb_ops); if (usbs->type == USB_HID_GAMEPAD) { polling_time = 15000; gadget_type = USB_GADGET_HID_GAMEPAD; } else { polling_time = 4000; gadget_type = USB_GADGET_HID_TOUCHPAD; } usbs->set_text(usbs->label, "#C7EA46 Status:# Started USB"); if (usb_ops.usb_device_init()) { usb_ops.usbd_end(false, true); return 1; } usbs->set_text(usbs->label, "#C7EA46 Status:# Waiting for connection"); // Initialize Control Endpoint. if (usb_ops.usb_device_enumerate(gadget_type)) goto error; usbs->set_text(usbs->label, "#C7EA46 Status:# Waiting for HID report request"); if (usb_ops.usb_device_class_send_hid_report()) goto error; usbs->set_text(usbs->label, "#C7EA46 Status:# Started HID emulation"); u32 timer_sys = get_tmr_ms() + 5000; while (true) { u32 timer = get_tmr_us(); // Parse input device. if (usbs->type == USB_HID_GAMEPAD) { if (_hid_poll_jc(usbs)) break; } else { if (_hid_poll_touch(usbs)) break; } // Check for suspended USB in case the cable was pulled. if (usb_ops.usb_device_get_suspended()) break; // Disconnected. // Handle control endpoint. usb_ops.usbd_handle_ep0_ctrl_setup(); // Wait max gadget timing. timer = get_tmr_us() - timer; if (timer < polling_time) usleep(polling_time - timer); if (timer_sys < get_tmr_ms()) { usbs->system_maintenance(true); timer_sys = get_tmr_ms() + 5000; } } usbs->set_text(usbs->label, "#C7EA46 Status:# HID ended"); goto exit; error: usbs->set_text(usbs->label, "#FFDD00 Error:# Timed out or canceled"); res = 1; exit: usb_ops.usbd_end(true, false); return res; }