Quellcode durchsuchen

Add QAM button remapping support

Introduce g_qamMap to allow remapping the QAM (three-dots) physical button to any existing button code (0 = default/unmapped). Persist the new field in the config struct (bumped CFG_MAGIC), and load/save it. Apply the remap in multiple mode handlers (Xbox, Switch, Pro, Lizard, PS packing) by replacing TB_MENU with the mapped TB_* flag via a new tritonFromCode helper in gamepad_util.h. Expose the mapping through WebUSB (blob field + UI dropdown) and the serial console (Q command). Update docs/index.html to add the QAM UI and explanatory note.
Jariullah Safi vor 4 Monaten
Ursprung
Commit
817f348e79

+ 5 - 3
OpenPuck/config.cpp

@@ -16,19 +16,20 @@ 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 (0..11 buttons, 12..15 D-pad U/D/L/R)
+uint8_t g_qamMap = 0;            // 0 = default (unmapped, uses hardcoded behavior per mode)
 
 // 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).
 const uint32_t g_pollUs = POLL_US_DEFAULT;
 
 #define CFG_FILE "/cfg.bin"
-#define CFG_MAGIC 0xC7
-struct Cfg { uint8_t magic, mode, mDiv, mFric, rsvd0, abSwap, back[4], pollU100, persistMode, bootMode, chordBtn[3]; };
+#define CFG_MAGIC 0xC8
+struct Cfg { uint8_t magic, mode, mDiv, mFric, rsvd0, abSwap, back[4], pollU100, persistMode, bootMode, chordBtn[3], qamMap; };
 
 void saveCfg(){
   Cfg c={CFG_MAGIC,g_usbMode,(uint8_t)g_mDiv,(uint8_t)g_mFric,g_debugCdc,g_abSwap,
          {g_back[0],g_back[1],g_back[2],g_back[3]},(uint8_t)(g_pollUs/100),(uint8_t)(g_persistMode?1:0),g_bootMode,
-         {g_chordBtn[0],g_chordBtn[1],g_chordBtn[2]}};
+         {g_chordBtn[0],g_chordBtn[1],g_chordBtn[2]},g_qamMap};
   InternalFS.remove(CFG_FILE); File f(InternalFS);
   if(f.open(CFG_FILE,FILE_O_WRITE)){ f.write((uint8_t*)&c,sizeof c); f.close(); }
 }
@@ -52,6 +53,7 @@ void loadCfg(){
       else                 g_usbMode = g_persistMode ? (modeValid(c.mode)?c.mode:0) : 0;
       static const uint8_t CHORD_DEF[3]={MODE_LIZARD,MODE_XBOX,MODE_SW_HORI};
       for(int i=0;i<3;i++) g_chordBtn[i]=modeValid(c.chordBtn[i])?c.chordBtn[i]:CHORD_DEF[i];
+      g_qamMap = c.qamMap;
     }
     f.close();
   }

+ 1 - 0
OpenPuck/config.h

@@ -64,6 +64,7 @@ extern int     g_mDiv, g_mFric; // xbox/lizard mouse sensitivity divisor / frict
 
 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 (0..15 standard, 16=PS Touch Click, 17=PS5 Mute)
+extern uint8_t g_qamMap;        // QAM (3 dots) physical button -> same code space (0 = default/unmapped)
 
 #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)

+ 1 - 0
OpenPuck/gamepad_util.cpp

@@ -61,6 +61,7 @@ static void psOrBackCode(uint32_t* b, uint8_t c){
 }
 uint32_t psButtonsFromSteam(uint32_t raw){
   uint32_t b=raw;
+  if(g_qamMap && (b&TB_MENU)){ b &= ~(uint32_t)TB_MENU; b |= tritonFromCode(g_qamMap); }
   if((b&CHORD_BACK4)==CHORD_BACK4) b&=~(uint32_t)(TB_A|TB_B|TB_X|TB_Y);
   if(b&TB_L4) psOrBackCode(&b, g_back[0]); if(b&TB_R4) psOrBackCode(&b, g_back[1]);
   if(b&TB_L5) psOrBackCode(&b, g_back[2]); if(b&TB_R5) psOrBackCode(&b, g_back[3]);

+ 11 - 1
OpenPuck/gamepad_util.h

@@ -6,6 +6,7 @@
 // are here.
 #pragma once
 #include <stdint.h>
+#include "triton.h"
 
 // int16 stick (center 0) -> uint8 (center 0x80), optional axis invert (HID Y is down-positive).
 uint8_t swStick(int16_t v, bool invert);
@@ -22,8 +23,17 @@ void touchPackPads(uint8_t* pts, bool lTouch, bool rTouch, uint16_t lx, uint16_t
 void steamPadsToTouch(uint32_t b, uint16_t touchH, int16_t lpx, int16_t lpy, int16_t rpx, int16_t rpy,
     uint16_t* lx, uint16_t* ly, uint16_t* rx, uint16_t* ry);
 
+// Convert back-paddle button code -> TB_* flag (shared across all modes).
+static inline uint32_t tritonFromCode(uint8_t c){
+  switch(c){ case 1:return TB_A; case 2:return TB_B; case 3:return TB_X; case 4:return TB_Y;
+    case 5:return TB_LB; case 6:return TB_RB; case 7:return TB_L3; case 8:return TB_R3;
+    case 9:return TB_VIEW; case 10:return TB_MENU; case 11:return TB_STEAM;
+    case 12:return TB_DUP; case 13:return TB_DDN; case 14:return TB_DLF; case 15:return TB_DRT;
+    case 16:return TB_TOUCH; case 17:return TB_MUTE; default:return 0; }
+}
+
 // PlayStation-layout button packing (shared by mode_ps5 + mode_hidgyro).
-uint32_t psButtonsFromSteam(uint32_t raw);   // apply back-paddle + chord-guard remapping
+uint32_t psButtonsFromSteam(uint32_t raw);   // apply back-paddle + chord-guard + QAM remapping
 uint8_t  psShouldersByte(uint32_t b);        // L1..R3 + Create/Options byte
 uint8_t  psHatNibble(uint32_t b);            // d-pad -> 8-way hat (8 = neutral)
 uint8_t  psFaceNibble(uint32_t b);           // face buttons (with A/B + X/Y swap)

+ 2 - 0
OpenPuck/mode_lizard.cpp

@@ -1,9 +1,11 @@
 #include "mode_lizard.h"
 #include "triton.h"
 #include "config.h"
+#include "gamepad_util.h"
 
 void rfLizard(const uint8_t* r, Adafruit_USBD_HID* mdev, Adafruit_USBD_HID* kdev, uint8_t mrid, uint8_t krid){
   uint32_t b=btnsOf(r);
+  if(g_qamMap && (b&TB_MENU)){ b &= ~(uint32_t)TB_MENU; b |= tritonFromCode(g_qamMap); }
   // --- right pad -> mouse motion with glide (mirrors mode_xinput's rfXboxMouse) ---
   static int prx=0,pry=0; static bool prt=false; static float vx=0,vy=0,rmx=0,rmy=0;
   bool rtouch=b&TB_RPADT; int rx=s16off(r,22), ry=s16off(r,24);

+ 1 - 0
OpenPuck/mode_switch_hori.cpp

@@ -38,6 +38,7 @@ static inline void backCodeToHatDirs(uint8_t c, bool& u, bool& d, bool& l, bool&
 // 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;
+  if(g_qamMap && (b&TB_MENU)){ b &= ~(uint32_t)TB_MENU; b |= tritonFromCode(g_qamMap); }
   // Mode-switch chord (all 4 back + A/X/Y): don't pass the face press to the console while the back-4 are held.
   if((b&CHORD_BACK4)==CHORD_BACK4) b &= ~(uint32_t)(TB_A|TB_X|TB_Y);
   // face buttons with optional A/B + X/Y swap (Nintendo physical-vs-label layout)

+ 2 - 0
OpenPuck/mode_switch_pro.cpp

@@ -1,5 +1,6 @@
 #include "mode_switch_pro.h"
 #include "triton.h"
+#include "gamepad_util.h"
 #include "config.h"
 #include <Adafruit_TinyUSB.h>
 #include <Arduino.h>
@@ -64,6 +65,7 @@ static void jcPackStick(uint8_t s[3], int16_t x, int16_t y){   // pack two 12-bi
 static uint8_t g_jcTimer=0;
 static void jcInputPrefix(uint8_t* out){
   uint32_t b=g_in.buttons;
+  if(g_qamMap && (b&TB_MENU)){ b &= ~(uint32_t)TB_MENU; b |= tritonFromCode(g_qamMap); }
   if((b&CHORD_BACK4)==CHORD_BACK4) 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 fX=g_abSwap?JC_BTN_Y:JC_BTN_X, fY=g_abSwap?JC_BTN_X:JC_BTN_Y;

+ 2 - 0
OpenPuck/mode_xinput.cpp

@@ -1,5 +1,6 @@
 #include "mode_xinput.h"
 #include "triton.h"
+#include "gamepad_util.h"
 #include "config.h"
 #include "haptics.h"
 #include "bonds.h"
@@ -105,6 +106,7 @@ static uint16_t codeToXB(uint8_t c){
 }
 static void rfXboxGamepad(const uint8_t* r){
   uint32_t b=btnsOf(r);
+  if(g_qamMap && (b&TB_MENU)){ b &= ~(uint32_t)TB_MENU; b |= tritonFromCode(g_qamMap); }
   uint16_t btn=0;                                   // map Triton buttons -> XInput button word
   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;
   if(b&TB_VIEW)btn|=XB_START; if(b&TB_MENU)btn|=XB_BACK; if(b&TB_STEAM)btn|=XB_GUIDE;

+ 1 - 0
OpenPuck/serial_console.cpp

@@ -60,6 +60,7 @@ void serialConsolePoll(){
       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=QAM 11=Guide 12=Dup 13=Ddown 14=Dleft 15=Dright 16=TouchClick 17=Mute]\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]=='Q'){ uint8_t code=strtoul(line+1,0,10); g_qamMap=code; saveCfg(); Serial.printf("# QAM -> code %u  [0=default 1=A 2=B 3=X 4=Y 5=LB 6=RB 7=L3 8=R3 9=Back 10=QAM 11=Guide 12=Dup 13=Ddown 14=Dleft 15=Dright 16=TouchClick 17=Mute]\n",code); }
       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); }

+ 2 - 1
OpenPuck/webusb_config.cpp

@@ -23,7 +23,7 @@ static void webusbSendBlob(){
   uint8_t p[2+WB_PAYLEN];
   p[0]=0xA5; p[1]=WB_PAYLEN;
   p[2]=4;                          // protocol version (4 = +battery/rssi/git; 2 = chordBtn[3])
-  p[3]=g_usbMode; p[4]=(uint8_t)g_mDiv; p[5]=(uint8_t)g_mFric; p[6]=0 /*rsvd: ex-padSmooth*/; p[7]=g_abSwap;
+  p[3]=g_usbMode; p[4]=(uint8_t)g_mDiv; p[5]=(uint8_t)g_mFric; p[6]=g_qamMap; p[7]=g_abSwap;
   p[8]=g_back[0]; p[9]=g_back[1]; p[10]=g_back[2]; p[11]=g_back[3];
   p[12]=(g_connSlot>=0)?(uint8_t)g_connSlot:0xFF;
   p[13]=up?1:0;
@@ -116,6 +116,7 @@ void webusbPoll(){
           case 16: g_persistMode = v?true:false; break;   // persist last mode across reboots (else always boot Steam)
           case 17: case 18: case 19: if(modeValid(v)) g_chordBtn[f-17]=v; break;   // back4+B/X/Y mode assignments
           case 20: armDebugCdcNextBoot(); usb_web.flush(); delay(40); NVIC_SystemReset(); break;  // reboot once WITH the CDC serial console (puck mode), then auto-revert
+          case 21: g_qamMap = v; break;   // QAM physical button remap code (0=default/unmapped)
         }
         if(persist) saveCfg();
         webusbSendBlob();

+ 10 - 2
docs/index.html

@@ -121,7 +121,8 @@
       <div class="row"><label>R4 (back upper-right)</label><select class="back" data-i="1"></select></div>
       <div class="row"><label>L5 (back lower-left)</label><select class="back" data-i="2"></select></div>
       <div class="row"><label>R5 (back lower-right)</label><select class="back" data-i="3"></select></div>
-      <p class="note">Back-paddle → button, applied in Xbox &amp; Switch modes.</p>
+      <div class="row"><label>QAM (3 dots)</label><select id="qamMap"></select></div>
+      <p class="note">Back-paddle → button, applied in Xbox &amp; Switch modes. QAM remap applies to all modes (0 = default per-mode behavior).</p>
     </div>
 
     <div class="card logonly">
@@ -152,6 +153,11 @@ function log(m){ const l=$("#log"); l.textContent=(new Date().toLocaleTimeString
 for(const sel of document.querySelectorAll("select.back")){
   for(const [c,n] of BTN_CODES){ const o=document.createElement("option"); o.value=c; o.textContent=n; sel.appendChild(o); }
 }
+// QAM remap dropdown: same codes but with a "Default" entry at 0
+{ const sel = document.getElementById("qamMap");
+  const d = document.createElement("option"); d.value=0; d.textContent="Default (varies by mode)"; sel.appendChild(d);
+  for(const [c,n] of BTN_CODES){ if(c===0) continue; const o=document.createElement("option"); o.value=c; o.textContent=n; sel.appendChild(o); }
+}
 for(const sel of document.querySelectorAll("select.chord")){
   MODE_NAMES.forEach((n,i)=>{ const o=document.createElement("option"); o.value=i; o.textContent=n; sel.appendChild(o); });
 }
@@ -211,7 +217,7 @@ async function readBlob(){
 }
 function applyBlob(p){
   // [ver][mode][mDiv][mFric][rsvd][abSwap][b0..b3][slot][up][f1lo][f1hi][pollU100(unused)][newlo][newhi]
-  const mode=p[1], mDiv=p[2], mFric=p[3], abSwap=p[5];   // p[4] reserved (ex-padSmooth)
+  const mode=p[1], mDiv=p[2], mFric=p[3], qamMap=p[4], abSwap=p[5];   // p[4] = QAM remap code (0 = default)
   const back=[p[6],p[7],p[8],p[9]], slot=p[10], up=p[11], f1=(p[12]|(p[13]<<8));
   const newps=(p.length>16?(p[15]|(p[16]<<8)):0), persistMode=(p.length>22?p[22]:0);
   const chord=(p.length>25)?[p[23],p[24],p[25]]:[3,1,2];   // v2 blob: chordBtn after qos/persist
@@ -252,6 +258,7 @@ function applyBlob(p){
   $("#abSwap").textContent = abSwap? "on":"off"; $("#abSwap").classList.toggle("active",!!abSwap);
   $("#persistMode").textContent = persistMode? "on":"off"; $("#persistMode").classList.toggle("active",!!persistMode);
   for(const sel of document.querySelectorAll("select.back")){ if(document.activeElement!==sel) sel.value=back[+sel.dataset.i]; }
+  { const sel=$("#qamMap"); if(document.activeElement!==sel) sel.value=qamMap; }
   for(const sel of document.querySelectorAll("select.chord")){ if(document.activeElement!==sel) sel.value=chord[+sel.dataset.i]; }
 }
 function fmtSlider(id,v){ return ""+v; }
@@ -365,6 +372,7 @@ for(const id of ["mDiv","mFric"]){
 for(const sel of document.querySelectorAll("select.back")){
   sel.addEventListener("change", ()=>setField(5 + +sel.dataset.i, +sel.value));
 }
+$("#qamMap").addEventListener("change", ()=>setField(21, +$("#qamMap").value));
 for(const sel of document.querySelectorAll("select.chord")){
   sel.addEventListener("change", ()=>setField(CHORD_FIELD[+sel.dataset.i], +sel.value));
 }