mode_xinput.cpp 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. #include "mode_xinput.h"
  2. #include "triton.h"
  3. #include "config.h"
  4. #include "haptics.h"
  5. #include "bonds.h"
  6. #include <Adafruit_TinyUSB.h>
  7. #include <Arduino.h>
  8. #include <string.h>
  9. // custom class-driver API (usbd_class_driver_t, usbd_edpt_*) for the XInput interface
  10. extern "C" {
  11. #include "device/usbd_pvt.h"
  12. }
  13. XboxController g_xboxCtl;
  14. // XInput button bits
  15. enum { XB_DUP=0x0001,XB_DDOWN=0x0002,XB_DLEFT=0x0004,XB_DRIGHT=0x0008, XB_START=0x0010,XB_BACK=0x0020,
  16. XB_L3=0x0040,XB_R3=0x0080, XB_LB=0x0100,XB_RB=0x0200,XB_GUIDE=0x0400, XB_A=0x1000,XB_B=0x2000,XB_X=0x4000,XB_Y=0x8000 };
  17. // ===================== XInput custom TinyUSB class driver =====================
  18. // Custom class driver (registered via usbd_app_driver_get_cb) + an Adafruit_USBD_Interface subclass that emits
  19. // the interface + 0x21-blob + 2 endpoints. (Verified against Adafruit nRF52 1.7.0 / TinyUSB 0.18.)
  20. #define XINPUT_DESC_LEN (9 + 17 + 7 + 7) // interface(9)+vendor0x21(17)+IN ep(7)+OUT ep(7) = 40
  21. static uint8_t g_xiItf=0xFF, g_xiEpIn=0, g_xiEpOut=0;
  22. static uint8_t g_xiInBuf[32], g_xiOutBuf[32];
  23. // legacy XInput rumble strength from the host's OUT packet (0=off); drives the haptic relay
  24. static volatile uint8_t g_rumble = 0;
  25. static volatile unsigned long g_rumbleMs = 0; // millis of last rumble OUT packet (stuck-rumble watchdog)
  26. #define RUMBLE_STUCK_MS 2500u // release a held rumble if no OUT packet refreshes it for this long (covers a lost stop without cutting normal short rumbles)
  27. static void xi_init(void){}
  28. static bool xi_deinit(void){return true;}
  29. static void xi_reset(uint8_t rhport){(void)rhport; g_xiEpIn=g_xiEpOut=0;}
  30. static uint16_t xi_open(uint8_t rhport, tusb_desc_interface_t const* itf, uint16_t max_len){
  31. if(!(itf->bInterfaceClass==0xFF && itf->bInterfaceSubClass==0x5D && itf->bInterfaceProtocol==0x01)) return 0;
  32. g_xiItf=itf->bInterfaceNumber;
  33. uint8_t const* p=(uint8_t const*)itf; uint8_t const* end=p+max_len; uint16_t used=itf->bLength; p+=itf->bLength;
  34. uint8_t opened=0;
  35. while(p<end && opened<itf->bNumEndpoints){
  36. uint8_t blen=p[0], btype=p[1];
  37. if(btype==TUSB_DESC_ENDPOINT){ tusb_desc_endpoint_t const* ep=(tusb_desc_endpoint_t const*)p;
  38. usbd_edpt_open(rhport,ep);
  39. if(tu_edpt_dir(ep->bEndpointAddress)==TUSB_DIR_IN) g_xiEpIn=ep->bEndpointAddress; else g_xiEpOut=ep->bEndpointAddress;
  40. opened++; }
  41. used+=blen; p+=blen;
  42. }
  43. if(g_xiEpOut) usbd_edpt_xfer(rhport,g_xiEpOut,g_xiOutBuf,sizeof g_xiOutBuf); // arm OUT (rumble/LED)
  44. return used;
  45. }
  46. static bool xi_ctrl(uint8_t rhport,uint8_t stage,tusb_control_request_t const* req){(void)rhport;(void)req; return stage!=CONTROL_STAGE_SETUP;}
  47. static bool xi_xfer(uint8_t rhport,uint8_t ep,xfer_result_t res,uint32_t n){(void)res;
  48. if(ep==g_xiEpOut){
  49. // XInput rumble packet: [00][08][00][bigMotor][smallMotor][00][00][00]; LED pkt is [01][03][led]
  50. if(n>=5 && g_xiOutBuf[0]==0x00 && g_xiOutBuf[1]==0x08){
  51. uint8_t big=g_xiOutBuf[3], sml=g_xiOutBuf[4]; g_rumble = big>sml?big:sml; g_rumbleMs = millis(); // stamp for the stuck-rumble watchdog
  52. }
  53. usbd_edpt_xfer(rhport,g_xiEpOut,g_xiOutBuf,sizeof g_xiOutBuf); // re-arm OUT
  54. }
  55. return true; }
  56. static const usbd_class_driver_t g_xiDriver = {
  57. #if CFG_TUSB_DEBUG>=2
  58. .name="XINPUT",
  59. #endif
  60. .init=xi_init, .deinit=xi_deinit, .reset=xi_reset, .open=xi_open,
  61. .control_xfer_cb=xi_ctrl, .xfer_cb=xi_xfer, .sof=NULL };
  62. extern "C" const usbd_class_driver_t* usbd_app_driver_get_cb(uint8_t* count){ *count=1; return &g_xiDriver; }
  63. class Adafruit_USBD_XInput : public Adafruit_USBD_Interface {
  64. public:
  65. uint16_t getInterfaceDescriptor(uint8_t, uint8_t* buf, uint16_t bufsize) override {
  66. if(!buf) return XINPUT_DESC_LEN; if(bufsize<XINPUT_DESC_LEN) return 0;
  67. uint8_t itfnum=TinyUSBDevice.allocInterface(1);
  68. uint8_t epin=TinyUSBDevice.allocEndpoint(TUSB_DIR_IN), epout=TinyUSBDevice.allocEndpoint(TUSB_DIR_OUT);
  69. const uint8_t t[XINPUT_DESC_LEN]={
  70. 9,TUSB_DESC_INTERFACE,itfnum,0x00,0x02,0xFF,0x5D,0x01,_strid,
  71. 0x11,0x21,0x00,0x01,0x01,0x25,epin,0x14,0x00,0x00,0x00,0x00,0x13,epout,0x08,0x00,0x00,
  72. 7,TUSB_DESC_ENDPOINT,epin, TUSB_XFER_INTERRUPT,U16_TO_U8S_LE(0x20),1, // bInterval 1ms (1000Hz) so the RF rate is the only limit
  73. 7,TUSB_DESC_ENDPOINT,epout,TUSB_XFER_INTERRUPT,U16_TO_U8S_LE(0x20),8 };
  74. memcpy(buf,t,XINPUT_DESC_LEN); return XINPUT_DESC_LEN;
  75. }
  76. bool begin(){ return TinyUSBDevice.addInterface(*this); }
  77. };
  78. static Adafruit_USBD_XInput g_xinput;
  79. static void xinputSend(uint16_t buttons,uint8_t lt,uint8_t rt,int16_t lx,int16_t ly,int16_t rx,int16_t ry){
  80. if(!tud_mounted() || g_xiEpIn==0 || usbd_edpt_busy(0,g_xiEpIn)) return;
  81. uint8_t* r=g_xiInBuf;
  82. r[0]=0x00; r[1]=0x14; r[2]=buttons&0xFF; r[3]=buttons>>8; r[4]=lt; r[5]=rt;
  83. r[6]=lx&0xFF; r[7]=lx>>8; r[8]=ly&0xFF; r[9]=ly>>8; r[10]=rx&0xFF; r[11]=rx>>8; r[12]=ry&0xFF; r[13]=ry>>8;
  84. memset(r+14,0,6);
  85. if(usbd_edpt_claim(0,g_xiEpIn)){ if(!usbd_edpt_xfer(0,g_xiEpIn,r,20)) usbd_edpt_release(0,g_xiEpIn); }
  86. }
  87. // ===================== right-pad mouse interface =====================
  88. static const uint8_t MOUSE_HID_DESC[] = { TUD_HID_REPORT_DESC_MOUSE() };
  89. static Adafruit_USBD_HID g_mouse; // Xbox-mode mouse interface (right trackpad)
  90. // ===================== report 0x45 -> XInput + mouse =====================
  91. // button code (g_back[], g_abSwap targets) -> legacy XInput bit. 0=none 1=A 2=B 3=X 4=Y 5=LB 6=RB 7=L3 8=R3 9=Back 10=Start 11=Guide
  92. static uint16_t codeToXB(uint8_t c){
  93. switch(c){ case 1:return XB_A; case 2:return XB_B; case 3:return XB_X; case 4:return XB_Y;
  94. case 5:return XB_LB; case 6:return XB_RB; case 7:return XB_L3; case 8:return XB_R3;
  95. case 9:return XB_BACK; case 10:return XB_START; case 11:return XB_GUIDE; default:return 0; }
  96. }
  97. static void rfXboxGamepad(const uint8_t* r){
  98. uint32_t b=btnsOf(r);
  99. uint16_t btn=0; // map Triton buttons -> XInput button word
  100. if(b&TB_DUP)btn|=XB_DUP; if(b&TB_DDN)btn|=XB_DDOWN; if(b&TB_DLF)btn|=XB_DLEFT; if(b&TB_DRT)btn|=XB_DRIGHT;
  101. if(b&TB_VIEW)btn|=XB_START; if(b&TB_MENU)btn|=XB_BACK; if(b&TB_STEAM)btn|=XB_GUIDE;
  102. if(b&TB_LB)btn|=XB_LB; if(b&TB_RB)btn|=XB_RB;
  103. if(b&TB_L3)btn|=XB_L3; if(b&TB_R3)btn|=XB_R3;
  104. // face buttons, with optional A/B + X/Y swap (Nintendo layout)
  105. uint16_t fA=g_abSwap?XB_B:XB_A, fB=g_abSwap?XB_A:XB_B, fX=g_abSwap?XB_Y:XB_X, fY=g_abSwap?XB_X:XB_Y;
  106. if(b&TB_A)btn|=fA; if(b&TB_B)btn|=fB; if(b&TB_X)btn|=fX; if(b&TB_Y)btn|=fY;
  107. // back paddles -> configurable mapping (default L4->LB, R4->RB, L5->L3, R5->R3)
  108. if(b&TB_L4)btn|=codeToXB(g_back[0]); if(b&TB_R4)btn|=codeToXB(g_back[1]);
  109. if(b&TB_L5)btn|=codeToXB(g_back[2]); if(b&TB_R5)btn|=codeToXB(g_back[3]);
  110. uint8_t lt=trigU8(u16off(r,4)), rt=trigU8(u16off(r,6)); // triggers u16 (half-scale) -> full-range u8
  111. xinputSend(btn, lt, rt, (int16_t)s16off(r,8), (int16_t)s16off(r,10), // L stick X/Y
  112. (int16_t)s16off(r,12), (int16_t)s16off(r,14)); // R stick X/Y
  113. }
  114. // Right pad -> mouse, riding ALONGSIDE the XInput gamepad on a second HID-mouse interface. Same glide model as
  115. // Lizard's right pad. Purely a USB-side translation -- touches NOTHING about the RF poll or relay. RPad click =
  116. // left button, LPad click = right.
  117. static void rfXboxMouse(const uint8_t* r){
  118. uint32_t b=btnsOf(r);
  119. static int prx=0,pry=0; static bool prt=false; static float vx=0,vy=0,rmx=0,rmy=0; static uint8_t pmb=0;
  120. bool rtouch=b&TB_RPADT; int rx=s16off(r,22), ry=s16off(r,24);
  121. if(rtouch){ if(prt){ vx+=(rx-prx); vy+=(ry-pry); } prx=rx; pry=ry; } prt=rtouch;
  122. float mxf=vx/(float)(g_mDiv*10)+rmx, myf=-(vy/(float)(g_mDiv*10))+rmy; // Y inverted for screen coords
  123. int dx=(int)mxf, dy=(int)myf; rmx=mxf-dx; rmy=myf-dy; // sub-pixel carry
  124. if(dx>127)dx=127; if(dx<-127)dx=-127; if(dy>127)dy=127; if(dy<-127)dy=-127;
  125. float f=g_mFric/100.0f; vx*=f; vy*=f; if(vx>-1&&vx<1)vx=0; if(vy>-1&&vy<1)vy=0; // friction = glide/decay
  126. uint8_t mb=((b&TB_RPADC)?1:0)|((b&TB_LPADC)?2:0); // RPad click = left, LPad click = right
  127. if(dx||dy||mb!=pmb){ pmb=mb;
  128. hid_mouse_report_t m; m.buttons=mb; m.x=(int8_t)dx; m.y=(int8_t)dy; m.wheel=0; m.pan=0;
  129. if(g_mouse.ready()) g_mouse.sendReport(0,&m,sizeof m);
  130. }
  131. }
  132. // ===================== IController =====================
  133. void XboxController::begin(){
  134. g_rumble=0;
  135. USBDevice.setID(0x045E, 0x028E); // device-level 045E:028E match -> Windows xusb / SDL / Linux xpad all bind it
  136. USBDevice.setDeviceVersion(0x0114);
  137. USBDevice.setManufacturerDescriptor("Microsoft");
  138. USBDevice.setProductDescriptor("Controller");
  139. g_xinput.setStringDescriptor("Controller"); g_xinput.begin(); // XInput vendor interface (FF/5D/01) -> MI_00
  140. g_mouse.setStringDescriptor("OpenPuck Mouse");
  141. g_mouse.setBootProtocol(HID_ITF_PROTOCOL_MOUSE);
  142. g_mouse.setReportDescriptor(MOUSE_HID_DESC, sizeof MOUSE_HID_DESC); g_mouse.setPollInterval(1); g_mouse.begin();
  143. }
  144. void XboxController::onReport45(const uint8_t* rep, bool fresh, uint8_t bodyTlen){
  145. (void)fresh; (void)bodyTlen;
  146. rfXboxGamepad(rep); rfXboxMouse(rep); // standard gamepad + right-pad mouse (2nd interface)
  147. }
  148. // Legacy XInput rumble -> relay the haptic (0x82 [01 01 gain]) to the controller while the host commands it,
  149. // re-queued ~40/s like Steam's glide haptic so it sustains. Not active in the current HID Xbox presentation.
  150. void XboxController::task(){
  151. if (g_rumble && millis()-g_rumbleMs > RUMBLE_STUCK_MS) g_rumble=0;
  152. if (g_rumble && !g_relayPend && g_connSlot>=0) {
  153. static unsigned long lastRumble=0;
  154. if (millis()-lastRumble>=25) { lastRumble=millis();
  155. uint8_t gain = g_rumble<0x30?0x30:g_rumble; // floor so low rumble is still feelable
  156. g_relayBuf[0]=0x82; g_relayBuf[1]=3; g_relayBuf[2]=0x01; g_relayBuf[3]=0x01; g_relayBuf[4]=gain; g_relayPend=true;
  157. }
  158. }
  159. }