|
@@ -1103,6 +1103,8 @@ static uint32_t codeToJc(uint8_t c, uint32_t fA,uint32_t fB,uint32_t fX,uint32_t
|
|
|
static int16_t g_swProAX=0,g_swProAY=0,g_swProAZ=0,g_swProGX=0,g_swProGY=0,g_swProGZ=0;
|
|
static int16_t g_swProAX=0,g_swProAY=0,g_swProAZ=0,g_swProGX=0,g_swProGY=0,g_swProGZ=0;
|
|
|
static unsigned long g_swProLastMs=0;
|
|
static unsigned long g_swProLastMs=0;
|
|
|
Adafruit_USBD_HID g_swPro;
|
|
Adafruit_USBD_HID g_swPro;
|
|
|
|
|
+static uint8_t g_swProReportMode=0; // 0 until the host's subcommand 0x03 selects 0x30 -> THEN we stream input
|
|
|
|
|
+static const uint8_t JC_MAC[6]={0x7C,0xBB,0x8A,0x00,0x00,0x01}; // synthetic but stable controller MAC
|
|
|
static void jcPackStick(uint8_t s[3], int16_t lx, int16_t ly){
|
|
static void jcPackStick(uint8_t s[3], int16_t lx, int16_t ly){
|
|
|
uint8_t LX=swStick(lx,false), LY=swStick(ly,true);
|
|
uint8_t LX=swStick(lx,false), LY=swStick(ly,true);
|
|
|
int plx=(int)LX-127;
|
|
int plx=(int)LX-127;
|
|
@@ -1110,7 +1112,10 @@ static void jcPackStick(uint8_t s[3], int16_t lx, int16_t ly){
|
|
|
s[1]=(uint8_t)((plx>>4)&0x0F);
|
|
s[1]=(uint8_t)((plx>>4)&0x0F);
|
|
|
s[2]=(uint8_t)LY;
|
|
s[2]=(uint8_t)LY;
|
|
|
}
|
|
}
|
|
|
-static void switchProBuild(uint8_t out[63]){
|
|
|
|
|
|
|
+// Standard input-report prefix [0..11] (timer, battery/conn, 3 button bytes, both packed sticks, vibrator),
|
|
|
|
|
+// shared by the streamed 0x30 report and the 0x21 subcommand-reply reports the host reads during init.
|
|
|
|
|
+static uint8_t g_jcTimer=0;
|
|
|
|
|
+static void jcInputPrefix(uint8_t* out){
|
|
|
uint32_t b=g_swBtns;
|
|
uint32_t b=g_swBtns;
|
|
|
if((b&(TB_R4|TB_L4|TB_R5|TB_L5))==(TB_R4|TB_L4|TB_R5|TB_L5)) b&=~(uint32_t)(TB_A|TB_B|TB_X|TB_Y);
|
|
if((b&(TB_R4|TB_L4|TB_R5|TB_L5))==(TB_R4|TB_L4|TB_R5|TB_L5)) b&=~(uint32_t)(TB_A|TB_B|TB_X|TB_Y);
|
|
|
uint32_t fA=g_abSwap?JC_BTN_B:JC_BTN_A, fB=g_abSwap?JC_BTN_A:JC_BTN_B;
|
|
uint32_t fA=g_abSwap?JC_BTN_B:JC_BTN_A, fB=g_abSwap?JC_BTN_A:JC_BTN_B;
|
|
@@ -1126,13 +1131,16 @@ static void switchProBuild(uint8_t out[63]){
|
|
|
if(b&TB_DRT)jc|=JC_BTN_RIGHT; if(b&TB_DLF)jc|=JC_BTN_LEFT;
|
|
if(b&TB_DRT)jc|=JC_BTN_RIGHT; if(b&TB_DLF)jc|=JC_BTN_LEFT;
|
|
|
if(b&TB_L4)jc|=codeToJc(g_back[0],fA,fB,fX,fY); if(b&TB_R4)jc|=codeToJc(g_back[1],fA,fB,fX,fY);
|
|
if(b&TB_L4)jc|=codeToJc(g_back[0],fA,fB,fX,fY); if(b&TB_R4)jc|=codeToJc(g_back[1],fA,fB,fX,fY);
|
|
|
if(b&TB_L5)jc|=codeToJc(g_back[2],fA,fB,fX,fY); if(b&TB_R5)jc|=codeToJc(g_back[3],fA,fB,fX,fY);
|
|
if(b&TB_L5)jc|=codeToJc(g_back[2],fA,fB,fX,fY); if(b&TB_R5)jc|=codeToJc(g_back[3],fA,fB,fX,fY);
|
|
|
- memset(out,0,63); static uint8_t ts=0;
|
|
|
|
|
- out[0]=ts++;
|
|
|
|
|
|
|
+ out[0]=g_jcTimer++;
|
|
|
out[1]=0x81; // full battery + USB powered (hid-nintendo bat_con)
|
|
out[1]=0x81; // full battery + USB powered (hid-nintendo bat_con)
|
|
|
out[2]=(uint8_t)(jc); out[3]=(uint8_t)(jc>>8); out[4]=(uint8_t)(jc>>16);
|
|
out[2]=(uint8_t)(jc); out[3]=(uint8_t)(jc>>8); out[4]=(uint8_t)(jc>>16);
|
|
|
jcPackStick(out+5, g_swLX, g_swLY);
|
|
jcPackStick(out+5, g_swLX, g_swLY);
|
|
|
jcPackStick(out+8, g_swRX, g_swRY);
|
|
jcPackStick(out+8, g_swRX, g_swRY);
|
|
|
out[11]=0;
|
|
out[11]=0;
|
|
|
|
|
+}
|
|
|
|
|
+static void switchProBuild(uint8_t out[63]){
|
|
|
|
|
+ memset(out,0,63);
|
|
|
|
|
+ jcInputPrefix(out);
|
|
|
for(int k=0;k<3;k++){
|
|
for(int k=0;k<3;k++){
|
|
|
int o=12+k*12;
|
|
int o=12+k*12;
|
|
|
out[o+0]=g_swProAX&0xFF; out[o+1]=g_swProAX>>8;
|
|
out[o+0]=g_swProAX&0xFF; out[o+1]=g_swProAX>>8;
|
|
@@ -1143,8 +1151,117 @@ static void switchProBuild(uint8_t out[63]){
|
|
|
out[o+10]=g_swProGZ&0xFF; out[o+11]=g_swProGZ>>8;
|
|
out[o+10]=g_swProGZ&0xFF; out[o+11]=g_swProGZ>>8;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-static void swProStream(){
|
|
|
|
|
|
|
+// --- Canonical factory SPI dumps the host reads for calibration. Neutral IMU + centered sticks so a fresh
|
|
|
|
|
+// "device" calibrates sane; user-cal regions (0x80xx) read 0xFF so the host falls back to these factory blocks.
|
|
|
|
|
+static const uint8_t SPI_IMU_CAL[24]={0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x40,0x00,0x40,0x00,0x40,
|
|
|
|
|
+ 0x00,0x00,0x00,0x00,0x00,0x00, 0x3B,0x34,0x3B,0x34,0x3B,0x34};
|
|
|
|
|
+static const uint8_t SPI_PARAMS1[24]={0x50,0xFD,0x00,0x00,0xC6,0x0F, 0x0F,0x30,0x61,0x96,0x30,0xF3,
|
|
|
|
|
+ 0xD4,0x14,0x54,0x41,0x15,0x54, 0xC7,0x79,0x9C,0x33,0x36,0x63};
|
|
|
|
|
+static const uint8_t SPI_PARAMS2[18]={0x0F,0x30,0x61,0x96,0x30,0xF3, 0xD4,0x14,0x54,0x41,0x15,0x54,
|
|
|
|
|
+ 0xC7,0x79,0x9C,0x33,0x36,0x63};
|
|
|
|
|
+static const uint8_t SPI_COLOR[13]={0x32,0x32,0x32, 0xE6,0xE6,0xE6, 0x32,0x32,0x32, 0x32,0x32,0x32, 0xFF};
|
|
|
|
|
+static uint8_t g_spiStickCal[18]; // built at boot: left[9]+right[9] factory stick calibration (packed 12-bit)
|
|
|
|
|
+static void jcPack12(uint8_t* o9, const uint16_t v[6]){ // pack 6 12-bit values into 9 bytes (Switch stick-cal format)
|
|
|
|
|
+ o9[0]=v[0]&0xFF; o9[1]=((v[1]&0x0F)<<4)|((v[0]>>8)&0x0F); o9[2]=(v[1]>>4)&0xFF;
|
|
|
|
|
+ o9[3]=v[2]&0xFF; o9[4]=((v[3]&0x0F)<<4)|((v[2]>>8)&0x0F); o9[5]=(v[3]>>4)&0xFF;
|
|
|
|
|
+ o9[6]=v[4]&0xFF; o9[7]=((v[5]&0x0F)<<4)|((v[4]>>8)&0x0F); o9[8]=(v[5]>>4)&0xFF;
|
|
|
|
|
+}
|
|
|
|
|
+static void jcBuildStickCal(){
|
|
|
|
|
+ const uint16_t C=2048, R=1800; // center, +/- range per axis
|
|
|
|
|
+ uint16_t L[6]={R,R, C,C, R,R}; // left order: max(x,y), center(x,y), min(x,y)
|
|
|
|
|
+ uint16_t Rr[6]={C,C, R,R, R,R}; // right order: center(x,y), min(x,y), max(x,y)
|
|
|
|
|
+ jcPack12(&g_spiStickCal[0], L); jcPack12(&g_spiStickCal[9], Rr);
|
|
|
|
|
+}
|
|
|
|
|
+static void spiRead(uint32_t addr, uint8_t len, uint8_t* dst){
|
|
|
|
|
+ for(uint8_t i=0;i<len;i++){
|
|
|
|
|
+ uint32_t a=addr+i; uint8_t v=0xFF;
|
|
|
|
|
+ if (a>=0x6020 && a<0x6020+24) v=SPI_IMU_CAL[a-0x6020];
|
|
|
|
|
+ else if(a>=0x603D && a<0x603D+18) v=g_spiStickCal[a-0x603D];
|
|
|
|
|
+ else if(a>=0x6050 && a<0x6050+13) v=SPI_COLOR[a-0x6050];
|
|
|
|
|
+ else if(a>=0x6080 && a<0x6080+24) v=SPI_PARAMS1[a-0x6080];
|
|
|
|
|
+ else if(a>=0x6098 && a<0x6098+18) v=SPI_PARAMS2[a-0x6098];
|
|
|
|
|
+ dst[i]=v; // unmapped (incl. user-cal 0x80xx) -> 0xFF = "blank flash" -> host uses factory blocks above
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+// Reply FIFO: the host's handshake/subcommand reports arrive in the USB ISR (jcSet); we enqueue the canonical
|
|
|
|
|
+// 0x81/0x21 reply and drain it from loop() (swProTask) where sendReport is safe. SPSC ring, volatile indices.
|
|
|
|
|
+// Every reply is sent as a full 63-byte report (64 with the id). macOS IOHIDManager silently DROPS any input
|
|
|
|
|
+// report shorter than the descriptor-declared length (0x3F), so short 0x81/0x21 replies never reached the host.
|
|
|
|
|
+#define JC_REPLEN 63
|
|
|
|
|
+struct JcRep { uint8_t rid; uint8_t data[JC_REPLEN]; };
|
|
|
|
|
+#define JCQ_N 8
|
|
|
|
|
+static volatile uint8_t g_jcQh=0, g_jcQt=0;
|
|
|
|
|
+static JcRep g_jcQ[JCQ_N];
|
|
|
|
|
+static void jcEnq(uint8_t rid, const uint8_t* d, uint8_t len){
|
|
|
|
|
+ uint8_t nt=(uint8_t)((g_jcQt+1)%JCQ_N);
|
|
|
|
|
+ if(nt==g_jcQh) return; // full -> drop; the host re-requests on timeout
|
|
|
|
|
+ if(len>JC_REPLEN) len=JC_REPLEN;
|
|
|
|
|
+ g_jcQ[g_jcQt].rid=rid;
|
|
|
|
|
+ memset(g_jcQ[g_jcQt].data,0,JC_REPLEN); memcpy(g_jcQ[g_jcQt].data,d,len); // zero-pad to full report length
|
|
|
|
|
+ g_jcQt=nt;
|
|
|
|
|
+}
|
|
|
|
|
+// Build the 0x21 reply (standard input prefix + ACK + echoed subcommand id + reply data) for a report-0x01 subcommand.
|
|
|
|
|
+static void jcSubcmd(uint8_t sub, const uint8_t* args, uint16_t alen){
|
|
|
|
|
+ uint8_t p[JC_REPLEN]; memset(p,0,sizeof p);
|
|
|
|
|
+ jcInputPrefix(p);
|
|
|
|
|
+ p[13]=sub;
|
|
|
|
|
+ switch(sub){
|
|
|
|
|
+ case 0x02: // request device info
|
|
|
|
|
+ p[12]=0x82;
|
|
|
|
|
+ p[14]=0x04; p[15]=0x21; // firmware version (4.33)
|
|
|
|
|
+ p[16]=0x03; // controller type: Pro Controller
|
|
|
|
|
+ p[17]=0x02; // fixed
|
|
|
|
|
+ memcpy(&p[18], JC_MAC, 6);
|
|
|
|
|
+ p[24]=0x01; // colors stored in SPI (0x6050)
|
|
|
|
|
+ p[25]=0x02; // fixed
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 0x10: { // SPI flash read -> echo [addr][len] then the data
|
|
|
|
|
+ if(alen<5){ p[12]=0x80; break; }
|
|
|
|
|
+ uint32_t a=(uint32_t)args[0]|((uint32_t)args[1]<<8)|((uint32_t)args[2]<<16)|((uint32_t)args[3]<<24);
|
|
|
|
|
+ uint8_t rl=args[4]; if(rl>0x1D) rl=0x1D;
|
|
|
|
|
+ p[12]=0x90;
|
|
|
|
|
+ p[14]=args[0]; p[15]=args[1]; p[16]=args[2]; p[17]=args[3]; p[18]=rl;
|
|
|
|
|
+ spiRead(a, rl, &p[19]);
|
|
|
|
|
+ break; }
|
|
|
|
|
+ case 0x03: // set input report mode (0x30 = standard full) -> begin streaming
|
|
|
|
|
+ if(alen>=1 && args[0]==0x30) g_swProReportMode=0x30;
|
|
|
|
|
+ p[12]=0x80; break;
|
|
|
|
|
+ case 0x04: p[12]=0x83; break; // trigger buttons elapsed time
|
|
|
|
|
+ case 0x21: p[12]=0xA0; break; // set NFC/IR config
|
|
|
|
|
+ default: p[12]=0x80; break; // 0x06/0x08/0x30/0x38/0x40/0x41/0x48/... generic positive ACK
|
|
|
|
|
+ }
|
|
|
|
|
+ jcEnq(0x21, p, JC_REPLEN);
|
|
|
|
|
+}
|
|
|
|
|
+// USB-ISR callback for the Pro Controller's OUT endpoint. Data arrives via the interrupt OUT endpoint, where
|
|
|
|
|
+// this TinyUSB core passes report_id=0 and leaves the real report id in buffer[0] (it does NOT strip it). Also
|
|
|
|
|
+// accept the control SET_REPORT shape (id already in `rid`) for safety. report 0x80 = USB handshake; 0x01 = subcommand.
|
|
|
|
|
+static void jcSet(uint8_t rid, hid_report_type_t type, uint8_t const* b, uint16_t n){
|
|
|
|
|
+ if(type!=HID_REPORT_TYPE_OUTPUT || n<1) return;
|
|
|
|
|
+ uint8_t id; const uint8_t* p; uint16_t pn;
|
|
|
|
|
+ if(rid==0){ id=b[0]; p=b+1; pn=(uint16_t)(n-1); } // EP-OUT: id is the first payload byte
|
|
|
|
|
+ else { id=rid; p=b; pn=n; } // control SET_REPORT: id already split out
|
|
|
|
|
+ if(id==0x80){
|
|
|
|
|
+ if(pn<1) return;
|
|
|
|
|
+ if(p[0]==0x01){ uint8_t d[9]={0x01,0x00,0x03,JC_MAC[0],JC_MAC[1],JC_MAC[2],JC_MAC[3],JC_MAC[4],JC_MAC[5]}; jcEnq(0x81,d,9); } // device type + MAC
|
|
|
|
|
+ else if(p[0]==0x02){ uint8_t d[1]={0x02}; jcEnq(0x81,d,1); } // handshake
|
|
|
|
|
+ else if(p[0]==0x03){ uint8_t d[1]={0x03}; jcEnq(0x81,d,1); } // set baudrate
|
|
|
|
|
+ // 0x04 force-USB / 0x05 enable-timeout / 0x06 reset: no reply expected
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if(id==0x01){ // [timer][rumble x8][subcmd][args...]
|
|
|
|
|
+ if(pn<10) return;
|
|
|
|
|
+ jcSubcmd(p[9], p+10, (pn>10)?(uint16_t)(pn-10):0);
|
|
|
|
|
+ }
|
|
|
|
|
+ // report 0x10 (rumble only) / 0x82: ignored
|
|
|
|
|
+}
|
|
|
|
|
+static void swProTask(){
|
|
|
if(g_usbMode!=MODE_SW_PRO || !g_swPro.ready()) return;
|
|
if(g_usbMode!=MODE_SW_PRO || !g_swPro.ready()) return;
|
|
|
|
|
+ if(g_jcQh!=g_jcQt){ // drain handshake/subcommand replies first (ordered)
|
|
|
|
|
+ JcRep* r=&g_jcQ[g_jcQh];
|
|
|
|
|
+ if(g_swPro.sendReport(r->rid, r->data, JC_REPLEN)) g_jcQh=(uint8_t)((g_jcQh+1)%JCQ_N);
|
|
|
|
|
+ return; // one report per call; the rest next loop
|
|
|
|
|
+ }
|
|
|
|
|
+ if(g_swProReportMode!=0x30) return; // not until the host has finished init + selected 0x30
|
|
|
if(millis()-g_swProLastMs<USB_STREAM_MS) return; g_swProLastMs=millis();
|
|
if(millis()-g_swProLastMs<USB_STREAM_MS) return; g_swProLastMs=millis();
|
|
|
uint8_t p[63]; switchProBuild(p);
|
|
uint8_t p[63]; switchProBuild(p);
|
|
|
g_swPro.sendReport(0x30, p, sizeof p);
|
|
g_swPro.sendReport(0x30, p, sizeof p);
|
|
@@ -1738,7 +1855,9 @@ void setup() {
|
|
|
USBDevice.setDeviceVersion(0x0210);
|
|
USBDevice.setDeviceVersion(0x0210);
|
|
|
USBDevice.setManufacturerDescriptor("Nintendo Co., Ltd.");
|
|
USBDevice.setManufacturerDescriptor("Nintendo Co., Ltd.");
|
|
|
USBDevice.setProductDescriptor("Pro Controller");
|
|
USBDevice.setProductDescriptor("Pro Controller");
|
|
|
|
|
+ jcBuildStickCal();
|
|
|
g_swPro.enableOutEndpoint(true);
|
|
g_swPro.enableOutEndpoint(true);
|
|
|
|
|
+ g_swPro.setReportCallback(NULL, jcSet); // answer the Nintendo USB handshake + subcommands (else Steam never binds it)
|
|
|
g_swPro.setReportDescriptor(SWPRO_HID_DESC, sizeof SWPRO_HID_DESC);
|
|
g_swPro.setReportDescriptor(SWPRO_HID_DESC, sizeof SWPRO_HID_DESC);
|
|
|
g_swPro.setPollInterval(4);
|
|
g_swPro.setPollInterval(4);
|
|
|
g_swPro.begin();
|
|
g_swPro.begin();
|
|
@@ -1802,7 +1921,7 @@ void loop() {
|
|
|
if (g_dirty) { g_dirty = false; saveBonds(); }
|
|
if (g_dirty) { g_dirty = false; saveBonds(); }
|
|
|
webusbPoll();
|
|
webusbPoll();
|
|
|
swStream();
|
|
swStream();
|
|
|
- swProStream();
|
|
|
|
|
|
|
+ swProTask();
|
|
|
ps5Stream();
|
|
ps5Stream();
|
|
|
hidGyroStream();
|
|
hidGyroStream();
|
|
|
rfSerialPoll();
|
|
rfSerialPoll();
|