Procházet zdrojové kódy

Merge branch 'main' into copilot/20

Jariullah Safi před 4 měsíci
rodič
revize
e26c19c5a7

+ 1 - 1
OpenPuck/config.cpp

@@ -14,7 +14,7 @@ static uint8_t g_debugCdc = 0;   // persisted one-shot arm, stored in Cfg.rsvd0
 
 int     g_mDiv = 64, g_mFric = 94;
 uint8_t g_abSwap = 0;
-uint8_t g_back[4] = {5,6,7,8};   // L4->LB R4->RB L5->L3 R5->R3 (see codeToXB)
+uint8_t g_back[4] = {5,6,7,8};   // L4->LB R4->RB L5->L3 R5->R3 (0..11 buttons, 12..15 D-pad U/D/L/R)
 
 // poll rate is fixed. Faster than the controller can refresh wastes airtime; slower adds latency. Any rate
 // persisted by an older build is ignored and overwritten with the default on boot (see loadCfg).

+ 1 - 1
OpenPuck/config.h

@@ -53,7 +53,7 @@ void armDebugCdcNextBoot();          // persist the one-shot (caller reboots)
 // persisted, runtime-tunable config:
 extern int     g_mDiv, g_mFric; // xbox/lizard mouse sensitivity divisor / friction%
 extern uint8_t g_abSwap;        // 1 = swap A/B and X/Y (Nintendo face-button layout)
-extern uint8_t g_back[4];       // back paddles L4,R4,L5,R5 -> button codes (5=LB 6=RB 7=L3 8=R3)
+extern uint8_t g_back[4];       // back paddles L4,R4,L5,R5 -> button codes (0..11 buttons, 12..15 D-pad U/D/L/R)
 
 #define POLL_US_DEFAULT 4000u   // 250 Hz -- matches SC2 input report rate (1000000/250 = 4000 us)
 #define USB_STREAM_MS   4u      // host-side HID stream cadence for translated modes (~250 Hz)

+ 2 - 1
OpenPuck/gamepad_util.cpp

@@ -55,7 +55,8 @@ void steamPadsToTouch(uint32_t b, uint16_t touchH, int16_t lpx, int16_t lpy, int
 static void psOrBackCode(uint32_t* b, uint8_t c){
   switch(c){ case 1:*b|=TB_A; break; case 2:*b|=TB_B; break; case 3:*b|=TB_X; break; case 4:*b|=TB_Y; break;
     case 5:*b|=TB_LB; break; case 6:*b|=TB_RB; break; case 7:*b|=TB_L3; break; case 8:*b|=TB_R3; break;
-    case 9:*b|=TB_VIEW; break; case 10:*b|=TB_MENU; break; case 11:*b|=TB_STEAM; break; default: break; }
+    case 9:*b|=TB_VIEW; break; case 10:*b|=TB_MENU; break; case 11:*b|=TB_STEAM; break;
+    case 12:*b|=TB_DUP; break; case 13:*b|=TB_DDN; break; case 14:*b|=TB_DLF; break; case 15:*b|=TB_DRT; break; default: break; }
 }
 uint32_t psButtonsFromSteam(uint32_t raw){
   uint32_t b=raw;

+ 14 - 3
OpenPuck/haptics.cpp

@@ -17,6 +17,8 @@ static unsigned long g_haptic82Ms = 0;     // millis of last 0x82 haptic OUTPUT
 static bool          g_haptic82On = false; // a non-zero 0x82 haptic is currently active (awaiting host stop)
 static unsigned long g_reinitAt = 0;       // when to fire the next post-reconnect haptic re-init (0 = none scheduled)
 static uint8_t       g_reinitLeft = 0;     // how many re-init shots remain in this connect window
+static bool          g_hapClearArmed=false;// haptic activity happened -> arm a clear once it goes idle (catches a
+                                           // latch that engaged during/after use, even seconds after connect)
 
 // ---- relay ring: multi-producer (USB ISR + loop-context console/xinput), single consumer (poll flush) ----
 // Producers serialize through a brief PRIMASK critical section (copy is <=62 bytes); the consumer only ever
@@ -41,6 +43,9 @@ bool relayEnqueue(uint8_t rid, const uint8_t* payload, uint8_t plen){
   g_rq[h].rid = rid; g_rq[h].len = plen;
   if (plen) memcpy(g_rq[h].data, payload, plen);
   g_rqHead = nx;
+  // Any haptic relay (Steam OR Xbox rumble OR test) arms the idle-clear and refreshes its timer -- so the
+  // during-use buzz gets cleared in EVERY USB mode, not just Steam. The re-init's own 0x81/0x87 don't match.
+  if (rid==0x82){ g_haptic82Ms = millis(); g_hapClearArmed = true; }
   __set_PRIMASK(pm); return true;
 }
 
@@ -119,6 +124,7 @@ static void hapticCancelPendingOn(){   // void queued 0x82-ON entries (stale Ste
 void haptic82HostReport(const uint8_t* p, uint16_t n){
   if(n<3) return;
   g_haptic82Ms = millis();
+  g_hapClearArmed = true;   // any haptic activity arms a clear when it next goes idle (kills a latch from this use)
   // Track on/off only. Do NOT synthesize a stop burst when Steam's own stop arrives: that stop is already
   // forwarded verbatim, so adding 0x82-zero frames on top just makes the controller see the stop several times
   // over. Each 0x82 is a discrete pad click, so the extra frames are exactly the spurious end-of-movement
@@ -169,16 +175,16 @@ void hapticReinit(){
   static const uint8_t H30[]={0x30,0x00,0x00,0x07,0x07,0x00,0x08,0x07,0x00,0x31,0x02,0x00,0x52,0x03,0x00};
   static const uint8_t H18[]={0x18,0x00,0x00,0x2e,0x00,0x00,0x34,0xff,0xff,0x35,0xff,0xff,0x34,0xff,0xff};
   static const uint8_t H35[]={0x35,0xff,0xff,0x2e,0x00,0x00};
-  static const uint8_t H2D[]={0x2d,0x64,0x00};                       // brightness (Steam sets it here too)
   static const uint8_t T81A[]={0x00,0x00,0x00,0x00,0x00,0x00,0x00};
   static const uint8_t T81B[]={0x01,0x00,0x00,0x00,0x00,0x00,0x00};
   relayEnqueue(0x81, nullptr, 0);            // reset action (FUN_0001f554) -- Steam sends this first
   relayEnqueue(0x87, H30, sizeof H30);
-  relayEnqueue(0x87, H18, sizeof H18);
+  relayEnqueue(0x87, H18, sizeof H18);       // haptic config (enabled/amplifier/gain): the part that clears a latch
   relayEnqueue(0x87, H35, sizeof H35);
-  relayEnqueue(0x87, H2D, sizeof H2D);
   relayEnqueue(0x81, T81A, sizeof T81A);
   relayEnqueue(0x81, T81B, sizeof T81B);
+  // NOTE: the brightness write (0x87 2d) that Steam includes is deliberately OMITTED -- it's an LED setting,
+  // not haptic, and this re-init now fires often (after every haptic-idle), so we must not flicker the LED.
 }
 void hapticInit(){
   g_rqHead = g_rqTail = 0; g_haptic82On=false;
@@ -222,4 +228,9 @@ void hapticTask(){
   // are one-shot pulses, so firing a 0x82-zero ~HAPTIC_QUIET_MS after a swipe ends is the extra end-of-movement
   // click the real puck doesn't make. Steam forwards its own stop for any sustained haptic.
   if (!g_xbox && g_haptic82On && millis()-g_haptic82Ms > HAPTIC_QUIET_MS) g_haptic82On=false;
+  // Haptic activity has gone idle for a while -> fire one re-init to clear any latch it left behind (the buzz
+  // that engages during/after use and won't self-clear, incl. after a mode switch). Fires only after a quiet
+  // gap, so it never interrupts active haptics; the brightness-less re-init is silent (settings, no play, no
+  // LED). Runs in ALL modes (the controller-side latch is mode-independent).
+  if (g_hapClearArmed && (millis()-g_haptic82Ms) > HAPTIC_CLEAR_IDLE_MS){ g_hapClearArmed=false; hapticReinit(); }
 }

+ 4 - 0
OpenPuck/haptics.h

@@ -35,6 +35,10 @@
 // comes up -- covers ~0.2s..3s so the brief controller-side connect buzz gets reset before it can sustain.
 #define HAPTIC_REINIT_SHOTS  8u
 #define HAPTIC_REINIT_GAP_MS 350u
+// After haptic activity, if it's been idle this long, fire one clear-re-init -- kills a latch that engaged
+// during use (a buzz that starts seconds after connect and won't self-clear). Long enough not to fire between
+// rapid in-game haptics; short enough to clear a stuck buzz soon after the user pauses.
+#define HAPTIC_CLEAR_IDLE_MS 1200u
 
 // ---- relay queue (written by puck_hid.cpp, mode_xinput.cpp, serial_console.cpp; drained by rf_link.cpp) ----
 // Enqueue one host->controller report: rid = report/command id, payload = the bytes AFTER [cmd][len] (what

+ 11 - 0
OpenPuck/mode_switch_hori.cpp

@@ -28,6 +28,13 @@ static uint16_t codeToSwitch(uint8_t c, uint16_t fA,uint16_t fB,uint16_t fX,uint
     case 5:return 0x10; case 6:return 0x20; case 7:return 0x400; case 8:return 0x800;
     case 9:return 0x100; case 10:return 0x200; case 11:return 0x1000; default:return 0; }
 }
+// Back-paddle code 12..15 map to D-pad Up/Down/Left/Right; fold them into the hat direction flags.
+static inline void backCodeToHatDirs(uint8_t c, bool& u, bool& d, bool& l, bool& r){
+  if(c==12) u=true;
+  else if(c==13) d=true;
+  else if(c==14) l=true;
+  else if(c==15) r=true;
+}
 // HORIPAD/Switch button bits: Y=1 B=2 A=4 X=8 L=10 R=20 ZL=40 ZR=80 Minus=100 Plus=200 LClick=400 RClick=800 Home=1000 Capture=2000
 static void switchBuildHoripad(uint8_t out[8]){
   uint32_t b=g_in.buttons; uint16_t btn=0;
@@ -46,6 +53,10 @@ static void switchBuildHoripad(uint8_t out[8]){
   if(b&TB_L4)btn|=codeToSwitch(g_back[0],fA,fB,fX,fY); if(b&TB_R4)btn|=codeToSwitch(g_back[1],fA,fB,fX,fY);
   if(b&TB_L5)btn|=codeToSwitch(g_back[2],fA,fB,fX,fY); if(b&TB_R5)btn|=codeToSwitch(g_back[3],fA,fB,fX,fY);
   bool u=b&TB_DUP,d=b&TB_DDN,l=b&TB_DLF,r=b&TB_DRT;           // hat: 0=N..7=NW, 8=neutral
+  if(b&TB_L4) backCodeToHatDirs(g_back[0],u,d,l,r);
+  if(b&TB_R4) backCodeToHatDirs(g_back[1],u,d,l,r);
+  if(b&TB_L5) backCodeToHatDirs(g_back[2],u,d,l,r);
+  if(b&TB_R5) backCodeToHatDirs(g_back[3],u,d,l,r);
   uint8_t hat=8;
   if(u&&r)hat=1; else if(r&&d)hat=3; else if(d&&l)hat=5; else if(l&&u)hat=7;
   else if(u)hat=0; else if(r)hat=2; else if(d)hat=4; else if(l)hat=6;

+ 2 - 1
OpenPuck/mode_switch_pro.cpp

@@ -42,7 +42,8 @@ static const uint8_t SWPRO_HID_DESC[]={
 static uint32_t codeToJc(uint8_t c, uint32_t fA,uint32_t fB,uint32_t fX,uint32_t fY){
   switch(c){ case 1:return fA; case 2:return fB; case 3:return fX; case 4:return fY;
     case 5:return JC_BTN_L; case 6:return JC_BTN_R; case 7:return JC_BTN_LSTICK; case 8:return JC_BTN_RSTICK;
-    case 9:return JC_BTN_MINUS; case 10:return JC_BTN_PLUS; case 11:return JC_BTN_HOME; default:return 0; }
+    case 9:return JC_BTN_MINUS; case 10:return JC_BTN_PLUS; case 11:return JC_BTN_HOME;
+    case 12:return JC_BTN_UP; case 13:return JC_BTN_DOWN; case 14:return JC_BTN_LEFT; case 15:return JC_BTN_RIGHT; default:return 0; }
 }
 static unsigned long g_swProLastMs=0;
 static Adafruit_USBD_HID g_swPro;

+ 3 - 2
OpenPuck/mode_xinput.cpp

@@ -96,11 +96,12 @@ static const uint8_t MOUSE_HID_DESC[] = { TUD_HID_REPORT_DESC_MOUSE() };
 static Adafruit_USBD_HID g_mouse;   // Xbox-mode mouse interface (right trackpad)
 
 // ===================== report 0x45 -> XInput + mouse =====================
-// 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
+// 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 12=Dup 13=Ddown 14=Dleft 15=Dright
 static uint16_t codeToXB(uint8_t c){
   switch(c){ case 1:return XB_A; case 2:return XB_B; case 3:return XB_X; case 4:return XB_Y;
     case 5:return XB_LB; case 6:return XB_RB; case 7:return XB_L3; case 8:return XB_R3;
-    case 9:return XB_BACK; case 10:return XB_START; case 11:return XB_GUIDE; default:return 0; }
+    case 9:return XB_BACK; case 10:return XB_START; case 11:return XB_GUIDE;
+    case 12:return XB_DUP; case 13:return XB_DDOWN; case 14:return XB_DLEFT; case 15:return XB_DRIGHT; default:return 0; }
 }
 static void rfXboxGamepad(const uint8_t* r){
   uint32_t b=btnsOf(r);

+ 1 - 1
OpenPuck/serial_console.cpp

@@ -54,7 +54,7 @@ void serialConsolePoll(){
       else if (line[0]=='E'){ g_mDiv=strtoul(line+1,0,10); if(g_mDiv<4)g_mDiv=4; saveCfg(); Serial.printf("# xbox-mouse sensitivity divisor=%d (lower=faster)\n",g_mDiv); }
       else if (line[0]=='F'){ g_mFric=strtoul(line+1,0,10); if(g_mFric>99)g_mFric=99; saveCfg(); Serial.printf("# xbox-mouse friction=%d%% (higher=more glide/momentum)\n",g_mFric); }
       else if (line[0]=='W'){ g_abSwap=!g_abSwap; saveCfg(); Serial.printf("# A/B + X/Y swap %s (Nintendo layout)\n",g_abSwap?"ON":"off"); }
-      else if (line[0]=='K'){ int i=line[1]-'0'; uint8_t code=strtoul(line+2,0,10); if(i>=0&&i<4){ g_back[i]=code; saveCfg(); Serial.printf("# back[%d] (%s) -> code %u  [0=none 1=A 2=B 3=X 4=Y 5=LB 6=RB 7=L3 8=R3 9=Back 10=Start 11=Guide]\n",i,(const char*[]){"L4","R4","L5","R5"}[i],code); } else Serial.println("# usage: K<0-3> <code>  (0=L4 1=R4 2=L5 3=R5)"); }
+      else if (line[0]=='K'){ int i=line[1]-'0'; uint8_t code=strtoul(line+2,0,10); if(i>=0&&i<4){ g_back[i]=code; saveCfg(); Serial.printf("# back[%d] (%s) -> code %u  [0=none 1=A 2=B 3=X 4=Y 5=LB 6=RB 7=L3 8=R3 9=Back 10=Start 11=Guide 12=Dup 13=Ddown 14=Dleft 15=Dright]\n",i,(const char*[]){"L4","R4","L5","R5"}[i],code); } else Serial.println("# usage: K<0-3> <code>  (0=L4 1=R4 2=L5 3=R5)"); }
       else if (line[0]=='J'){ char* sp=0; uint8_t id=strtoul(line+1,&sp,0); uint16_t val=sp?strtoul(sp,0,0):0;  // inject SET-SETTINGS to controller: report 0x87 [id][val u16 LE]
         uint8_t pl[3]={id,(uint8_t)(val&0xFF),(uint8_t)(val>>8)}; relayEnqueue(0x87,pl,3);
         Serial.printf("# queued SET-SETTINGS id=0x%02X val=%u (relay 0x87) — watch new=/s\n",id,val); }

+ 1 - 1
docs/index.html

@@ -134,7 +134,7 @@
 <script>
 const MODE_NAMES = ["Steam (puck)","Xbox 360","Switch (HORIPAD)","Lizard (always)","Switch Pro + gyro","PS5 DualSense","HID gyro (DS4)","Xbox One"];
 const CHORD_FIELD = [17, 18, 19];
-const BTN_CODES = [[0,"— none —"],[1,"A"],[2,"B"],[3,"X"],[4,"Y"],[5,"LB"],[6,"RB"],[7,"L3"],[8,"R3"],[9,"Back"],[10,"Start"],[11,"Guide"]];
+const BTN_CODES = [[0,"— none —"],[1,"A"],[2,"B"],[3,"X"],[4,"Y"],[5,"LB"],[6,"RB"],[7,"L3"],[8,"R3"],[9,"Back"],[10,"Start"],[11,"Guide"],[12,"D-pad Up"],[13,"D-pad Down"],[14,"D-pad Left"],[15,"D-pad Right"]];
 let dev=null, epIn=0, epOut=0, ifNum=0, polling=false, capturing=false;
 const $ = s=>document.querySelector(s);
 function log(m){ const l=$("#log"); l.textContent=(new Date().toLocaleTimeString()+"  "+m+"\n"+l.textContent).slice(0,2000); }