From a4a80f4f1588042cf8ac9b8449bde02eb5311a63 Mon Sep 17 00:00:00 2001 From: ripps818 Date: Tue, 13 Oct 2026 03:05:34 -0500 Subject: [PATCH] feat: trackpad D-pad in every mode, D-pad feedback, per-type QAM + Select Trackpad D-pad (on touch or on click, diagonals included) was Switch Pro only; every emulated mode now offers it: Xbox 360 and OG, Switch (Pro and Hori), DS4, DS5, HIDGYRO and PS3. A pad on D-pad on click spends its click on the D-pad, so the click's own mapping does not fire with it, and the panel greys that pad's Click setting out. The Switch-only D-pad click feedback becomes a per-type setting, D-pad feedback (grid setting 7, on by default; the Switch's starts from the old byte). It pulses the pad each time its direction changes, so sliding from down to down-right is felt without lifting a finger, and on each D-pad click while Trackpad haptics is off (every click already pulses then). Quick Access + Select, also Switch-only (screenshot), is now a target per controller type, set in a Quick Access group of the Button map card. It is always on and none by default, so the enable switch is gone; a file from before keeps the Switch's target only if that switch was on. Any button works, including the Guide, Home or PS button. Panel layout: Trackpads (Touch and Click per pad) and a new Trackpad feedback card are separate; Trackpad feedback, Adaptive triggers and Lights form one column, Rumble and Haptics another; Switch Pro mode only follows the Button map card. The status blob grows to p[251]. Co-Authored-By: Claude Sonnet 5.5 --- OpenPuck/config.cpp | 50 ++++++-- OpenPuck/config.h | 15 ++- OpenPuck/gamepad_util.cpp | 133 ++++++++++++++++---- OpenPuck/gamepad_util.h | 39 +++++- OpenPuck/haptics.cpp | 4 + OpenPuck/mode_hidgyro.cpp | 2 +- OpenPuck/mode_ps3.cpp | 2 +- OpenPuck/mode_ps5.cpp | 2 +- OpenPuck/mode_switch_hori.cpp | 2 +- OpenPuck/mode_switch_pro.cpp | 42 +------ OpenPuck/mode_xbox_og.cpp | 7 +- OpenPuck/mode_xinput.cpp | 4 +- OpenPuck/triton.h | 7 +- OpenPuck/webusb_config.cpp | 27 ++-- README.md | 4 +- docs/CONTROLLER_FEATURES.md | 8 +- docs/PROTOCOL.md | 24 +++- docs/TESTING_GUIDE.md | 2 +- docs/index.html | 121 +++++++++++++----- panel/app.js | 1 - panel/backup.js | 10 +- panel/profiles.js | 41 +++++- panel/status.js | 17 ++- panel/types.js | 56 +++++---- tests/button_map/run.py | 9 +- tests/button_profiles/panel.cjs | 30 ++++- tests/final_webusb/run.py | 10 +- tests/switch_pro_shortcuts/configurator.cjs | 16 ++- tests/switch_pro_shortcuts/run.py | 81 ++++++++---- tests/type_haptics/run.py | 37 +++++- 30 files changed, 586 insertions(+), 217 deletions(-) diff --git a/OpenPuck/config.cpp b/OpenPuck/config.cpp index 4a00ed52..a6702231 100644 --- a/OpenPuck/config.cpp +++ b/OpenPuck/config.cpp @@ -69,6 +69,7 @@ uint16_t g_audioSpeaker = 0; uint8_t g_hapticLimitKnee = HAPTIC_LIMIT_KNEE_DEFAULT; uint8_t g_hapticLift = HAPTIC_LIFT_DEFAULT; uint8_t g_trigHaptics = TRIG_HAPTICS_DEFAULT; +uint8_t g_dpadClick = DPAD_CLICK_DEFAULT; uint8_t g_padClick = PAD_CLICK_DEFAULT; uint8_t g_padTick = PAD_TICK_DEFAULT; uint8_t g_padTickGain = PAD_TICKGAIN_DEFAULT; @@ -101,6 +102,9 @@ uint8_t g_typeHaptic[HAP_COUNT][ET_COUNT] = { /* HAP_TRIG */ { TRIG_HAPTICS_DEFAULT, TRIG_HAPTICS_DEFAULT, TRIG_HAPTICS_DEFAULT, TRIG_HAPTICS_DEFAULT }, + /* HAP_DPAD */ + { DPAD_CLICK_DEFAULT, DPAD_CLICK_DEFAULT, DPAD_CLICK_DEFAULT, + DPAD_CLICK_DEFAULT }, }; struct TypeHapticSpec { uint8_t min, max; @@ -114,12 +118,21 @@ static const TypeHapticSpec kTypeHaptic[HAP_COUNT] = { /* HAP_TICKGAIN */ { PAD_CLICK_MIN, PAD_CLICK_MAX, 0xF }, /* HAP_LIFT */ { 0, HAPTIC_LIFT_MAX, (1u << ET_SWITCH) | (1u << ET_DS5) }, /* HAP_TRIG */ { 0, TRIG_HAPTICS_BOTH, 1u << ET_DS5 }, + /* HAP_DPAD */ { 0, 1, 0xF }, }; static bool typeHapticInRange(uint8_t setting, uint8_t value) { return value >= kTypeHaptic[setting].min && value <= kTypeHaptic[setting].max; } +bool typeQamSelectSet(uint8_t et, uint8_t value) +{ + if (et >= ET_COUNT || value > QAM_SELECT_MAX) + return false; + g_typeQamSelect[et] = value; + applyActiveType(); + return true; +} bool typeHapticSet(uint8_t setting, uint8_t et, uint8_t value) { if (setting >= HAP_COUNT || et >= ET_COUNT || @@ -134,8 +147,9 @@ uint8_t g_createAsTouch = 0; uint8_t g_trigInner = 0, g_trigOuter = 100; uint8_t g_ledBright = 0; -uint8_t g_swDpadHaptics = 1; -uint8_t g_swQamSelect = 18; +// no type has a target until one is chosen, and the chord does nothing without one +uint8_t g_typeQamSelect[ET_COUNT] = { 0, 0, 0, 0 }; +uint8_t g_qamSelect = 0; // Default = the original shortcut arrangement: all four back buttons, controller modes, no D-pad haptic // shortcuts, no confirmation pulses, no Quick Access + Select capture. uint8_t g_shortcutFlags = SHORTCUT_ENABLED; @@ -159,8 +173,7 @@ void captureFeedbackChord(uint8_t slot, uint32_t buttons) static bool held[NSLOT] = {}; if (slot >= NSLOT) return; - bool active = g_usbMode == MODE_SW_PRO && g_swQamSelect && - (g_shortcutFlags & SHORTCUT_CAPTURE) && + bool active = g_qamSelect && (buttons & (TB_QAM | TB_MENU)) == (TB_QAM | TB_MENU); if (active && !held[slot]) hapticShortcutFeedback(slot, 1); @@ -177,6 +190,7 @@ void applyActiveType() g_back[2] = 7; g_back[3] = 8; g_qamMap = 0; + g_qamSelect = 0; g_abSwap = 0; g_padHaptics = 1; g_rumble = 1; @@ -206,6 +220,8 @@ void applyActiveType() g_hapticLimitKnee = g_typeHaptic[HAP_KNEE][g_etype]; g_hapticLift = g_typeHaptic[HAP_LIFT][g_etype]; g_trigHaptics = g_typeHaptic[HAP_TRIG][ET_DS5]; + g_qamSelect = g_typeQamSelect[g_etype]; + g_dpadClick = g_typeHaptic[HAP_DPAD][g_etype]; g_audioHapticStyle = g_typeHaptic[HAP_STYLE][ET_DS5]; g_padClick = g_typeHaptic[HAP_CLICK][g_etype]; g_padTick = g_typeHaptic[HAP_TICK][g_etype]; @@ -274,6 +290,9 @@ struct Cfg { // per-type haptic settings, g_typeHaptic's grid. 0xFF or out of range (an older file) -> the default, with the // limiter and the DualSense style taken from the legacy shared ext bytes 14 and 11 uint8_t typeHaptic[HAP_COUNT][ET_COUNT]; + // per-type Quick Access + Select target, g_typeQamSelect. 0xFF (older file) -> the Switch's from swQamSelect + // when its enable switch was on, the rest none + uint8_t typeQamSelect[ET_COUNT]; }; // rsvd0 = ex-padSmooth, now the one-shot debug-CDC arm // Shortest cfg.bin we still accept: the layout as of CFG_MAGIC 0xCF, i.e. everything before the appended tail. @@ -354,14 +373,14 @@ void saveCfg() g_suspendOff, {}, {}, - g_swDpadHaptics, + g_typeHaptic[HAP_DPAD][ET_SWITCH], (uint8_t)(g_hdPadScale / 2), { 0xFF, 0xFF }, 0xFF, { 0xFF, 0xFF }, 0xFF, 0xFF, - g_swQamSelect, + g_typeQamSelect[ET_SWITCH], g_shortcutFlags }; memcpy(c.ext, g_cfgExt, sizeof c.ext); c.createAsTouch = g_createAsTouch; @@ -372,6 +391,7 @@ void saveCfg() c.typeRumbleScale2[i] = (uint8_t)(g_typeRumbleScale[i] / 2); for (int k = 0; k < HAP_COUNT; k++) c.typeHaptic[k][i] = g_typeHaptic[k][i]; + c.typeQamSelect[i] = g_typeQamSelect[i]; c.padStick[i][0] = g_padStickCfg[i][0]; c.padStick[i][1] = g_padStickCfg[i][1]; } @@ -552,9 +572,10 @@ void loadCfg() g_typeHaptic[k][et] = g_hapticLimitKnee; else if (k == HAP_STYLE && et == ET_DS5) g_typeHaptic[k][et] = g_audioHapticStyle; + else if (k == HAP_DPAD && et == ET_SWITCH && + c.swDpadHaptics <= 1) + g_typeHaptic[k][et] = c.swDpadHaptics; } - if (c.swDpadHaptics <= 1) - g_swDpadHaptics = c.swDpadHaptics; if (c.hdPadScale2 <= 250) g_hdPadScale = (uint16_t)c.hdPadScale2 * 2; // per-type grip strength; a file from before it seeds every type with the legacy global value, unless that @@ -568,8 +589,17 @@ void loadCfg() g_typeRumbleScaleDefault[et] : g_rumbleScale; } - if (c.swQamSelect <= 20) - g_swQamSelect = c.swQamSelect; + for (uint8_t et = 0; et < ET_COUNT; et++) { + if (c.typeQamSelect[et] <= QAM_SELECT_MAX) + g_typeQamSelect[et] = c.typeQamSelect[et]; + // the shortcut was Switch-only and needed its enable switch: a file from then keeps the Switch's target + // only if that switch was on + else if (et == ET_SWITCH && c.shortcutFlags <= 63 && + (c.shortcutFlags & SHORTCUT_CAPTURE)) + g_typeQamSelect[et] = c.swQamSelect <= QAM_SELECT_MAX ? + c.swQamSelect : + 18; + } // bits 1 and 2 were the removed profiles and D-pad haptic shortcut behaviors if (c.shortcutFlags <= 63) g_shortcutFlags = c.shortcutFlags & ~6u; diff --git a/OpenPuck/config.h b/OpenPuck/config.h index 462ad3bd..77bfe176 100644 --- a/OpenPuck/config.h +++ b/OpenPuck/config.h @@ -205,10 +205,15 @@ extern uint8_t // applyActiveType() at boot and after any edit to the active type. extern uint8_t g_abSwap; // 1 = swap A/B and X/Y (Nintendo face-button layout) extern uint8_t g_back[4]; -extern uint8_t g_swDpadHaptics; extern uint8_t g_qamMap; -extern uint8_t g_swQamSelect; +// What Quick Access + Select (Menu) acts as, a remap target code (0 none; 18 is the Switch's Capture), per +// controller type; g_qamSelect is the active type's copy. None until chosen: the chord needs no other switch. +extern uint8_t g_typeQamSelect[ET_COUNT]; +extern uint8_t g_qamSelect; +#define QAM_SELECT_MAX 20 +// Store a target for a type (false when out of range) and refresh the live copy. +bool typeQamSelectSet(uint8_t et, uint8_t value); extern uint8_t g_shortcutFlags; void shortcutModeRequest(uint8_t mode, uint8_t slot); void shortcutModeTask(); @@ -252,6 +257,9 @@ extern uint16_t g_audioSpeaker; // by up to 1.5x / 2x / 3x / 4x (hapticLiftGain). Switch and DualSense. g_hapticLift is the active type's copy. // HAP_TRIG: where the DualSense adaptive-trigger effects are felt (TRIG_HAPTICS_*): off, the trackpads, the // grips, or both. DualSense only. g_trigHaptics is the live copy. +// HAP_DPAD: 1 = a click on a trackpad in a D-pad mode pulses that pad while Trackpad haptics is off +// (padDpadClickFeedback). Every type; the Switch's starts from the old Switch-only setting. +// g_dpadClick is the active type's copy. // Click, tick and tick gain apply to every type. enum { HAP_KNEE, @@ -261,6 +269,7 @@ enum { HAP_TICKGAIN, HAP_LIFT, HAP_TRIG, + HAP_DPAD, HAP_COUNT }; // Measured on the controller's IMU (Switch HD and DualSense wave), the grips stay linear to ~0.7 of full scale @@ -279,10 +288,12 @@ enum { #define TRIG_HAPTICS_GRIPS 2 #define TRIG_HAPTICS_BOTH 3 #define TRIG_HAPTICS_DEFAULT TRIG_HAPTICS_PADS +#define DPAD_CLICK_DEFAULT 1 extern uint8_t g_typeHaptic[HAP_COUNT][ET_COUNT]; extern uint8_t g_hapticLimitKnee; extern uint8_t g_hapticLift; extern uint8_t g_trigHaptics; +extern uint8_t g_dpadClick; // Store a value if it is in range and the type has the setting, and refresh the live mirrors. bool typeHapticSet(uint8_t setting, uint8_t et, uint8_t value); // Live copies of the active type's click strength and tick level (the defaults in puck modes) diff --git a/OpenPuck/gamepad_util.cpp b/OpenPuck/gamepad_util.cpp index 9f770f01..3cb60f5b 100644 --- a/OpenPuck/gamepad_util.cpp +++ b/OpenPuck/gamepad_util.cpp @@ -203,33 +203,42 @@ void padStickBlend(uint32_t b, int16_t lpx, int16_t lpy, int16_t rpx, } } -uint32_t padDpadButtons(const PuckInput &in) +static bool padIsDpad(uint8_t p) { - if (shortcutHeld(in.buttons)) + return g_padStick[p] == PS_DPAD_TOUCH || g_padStick[p] == PS_DPAD_CLICK; +} + +// The D-pad directions pad p (0 left, 1 right) is pointing at, or 0 when it isn't touched, is not on a D-pad +// mode, or (on click) isn't clicked. +static uint32_t padDpadOne(uint8_t p, uint32_t raw, int16_t px, int16_t py) +{ + static const uint32_t touched[2] = { TB_LPADT, TB_RPADT }; + static const uint32_t clicked[2] = { TB_LPADC, TB_RPADC }; + uint8_t mode = g_padStick[p]; + if (!padIsDpad(p) || !(raw & touched[p])) + return 0; + if (mode == PS_DPAD_CLICK && !(raw & clicked[p])) + return 0; + int32_t x = px, y = py; + int32_t ax = x < 0 ? -x : x, ay = y < 0 ? -y : y; + if (ax < 6000 && ay < 6000 && ax * ax + ay * ay < 36000000) return 0; - const int16_t px[2] = { in.lpx, in.rpx }; - const int16_t py[2] = { in.lpy, in.rpy }; - const uint32_t touched[2] = { TB_LPADT, TB_RPADT }; - const uint32_t clicked[2] = { TB_LPADC, TB_RPADC }; uint32_t out = 0; - for (int p = 0; p < 2; p++) { - uint8_t mode = g_padStick[p]; - if (mode != PS_DPAD_TOUCH && mode != PS_DPAD_CLICK) - continue; - if (!(in.buttons & touched[p])) - continue; - if (mode == PS_DPAD_CLICK && !(in.buttons & clicked[p])) - continue; - int32_t x = px[p], y = py[p]; - int32_t ax = x < 0 ? -x : x, ay = y < 0 ? -y : y; - if (ax < 6000 && ay < 6000 && ax * ax + ay * ay < 36000000) - continue; - // tan(22.5 degrees) separates cardinal sectors from diagonals. - if (ax * 1000 >= ay * 414) - out |= x < 0 ? TB_DLF : TB_DRT; - if (ay * 1000 >= ax * 414) - out |= y < 0 ? TB_DDN : TB_DUP; - } + // tan(22.5 degrees) separates cardinal sectors from diagonals. + if (ax * 1000 >= ay * 414) + out |= x < 0 ? TB_DLF : TB_DRT; + if (ay * 1000 >= ax * 414) + out |= y < 0 ? TB_DDN : TB_DUP; + return out; +} + +uint32_t padDpadBits(uint32_t raw, int16_t lpx, int16_t lpy, int16_t rpx, + int16_t rpy) +{ + if (shortcutHeld(raw)) + return 0; + uint32_t out = padDpadOne(0, raw, lpx, lpy) | + padDpadOne(1, raw, rpx, rpy); // Opposing pads must not produce an invalid D-pad combination. if ((out & (TB_DLF | TB_DRT)) == (TB_DLF | TB_DRT)) out &= ~(TB_DLF | TB_DRT); @@ -238,6 +247,62 @@ uint32_t padDpadButtons(const PuckInput &in) return out; } +uint32_t padDpadApply(uint32_t raw, int16_t lpx, int16_t lpy, int16_t rpx, + int16_t rpy) +{ + uint32_t out = raw | padDpadBits(raw, lpx, lpy, rpx, rpy); + if (shortcutHeld(raw)) + return out; + if (g_padStick[0] == PS_DPAD_CLICK) + out &= ~TB_LPADC; + if (g_padStick[1] == PS_DPAD_CLICK) + out &= ~TB_RPADC; + return out; +} + +void padDpadClickFeedback(uint8_t slot) +{ + static uint32_t held[NSLOT] = {}; + static uint32_t dir[NSLOT][2] = {}; + if (slot >= NSLOT) + return; + const PuckInput &in = g_in[slot]; + uint32_t buttons = in.buttons; + uint32_t clicks = buttons & (TB_LPADC | TB_RPADC); + uint32_t pressed = clicks & ~held[slot]; + held[slot] = clicks; + bool held4 = shortcutHeld(buttons); + bool moved[2]; + for (int p = 0; p < 2; p++) { + uint32_t now = held4 ? 0 : + padDpadOne(p, buttons, + p ? in.rpx : in.lpx, + p ? in.rpy : in.lpy); + // A new direction pulses, so sliding from down to down-right is felt without lifting. Going from none + // to a direction on click is the click itself, which the click pulse below covers. + moved[p] = now && now != dir[slot][p] && + !(g_padStick[p] == PS_DPAD_CLICK && !dir[slot][p]); + dir[slot][p] = now; + } + if (!g_dpadClick || held4 || haptic82Blocked(slot)) + return; + // With Trackpad haptics on, every click already pulses (the controller's own, or the puck's), so a click + // pulse here would play twice; direction changes are not clicks, so they always pulse. + bool left = moved[0], right = moved[1]; + if (g_padHaptics == PAD_HAPTICS_OFF) { + left |= (pressed & TB_LPADC) && (buttons & TB_LPADT) && + padIsDpad(0); + right |= (pressed & TB_RPADC) && (buttons & TB_RPADT) && + padIsDpad(1); + } + if (left || right) { + const uint8_t pulse[3] = { hsidePads(left, right), 2, + padClickGain() }; + // ahead of any queued PCM stream frames, which would delay the click by their length + relayEnqueueFront(0x82, pulse, sizeof pulse, true, slot); + } +} + void slotSticks(uint8_t slot, int16_t *lx, int16_t *ly, int16_t *rx, int16_t *ry) { @@ -378,9 +443,25 @@ void steamPadsToTouch(uint32_t b, uint16_t touchH, int16_t lpx, int16_t lpy, } } -uint32_t psButtonsFromSteam(uint32_t raw) +uint32_t remapHostButtons(uint8_t slot, uint32_t raw, bool *capture) { - return remapButtons(shortcutHostButtons(raw)); + uint32_t b = raw; + bool qamSelect = qamSelectShortcut(slot, b); + b = shortcutHostButtons(b); + // QAM + Select is its own shortcut: QAM does not also act as its own mapping + if (qamSelect) + b &= ~(uint32_t)TB_QAM; + bool cap = false; + b = remapButtons(b, &cap); + if (qamSelect) { + const RemapTarget t = + remapTarget(g_qamSelect, remapFacesSwapped(g_btnMap)); + b |= t.tb; + cap |= t.capture; + } + if (capture) + *capture = cap; + return b; } // DualSense / DS4 buttons[1]: L1..R3, Create(Share), Options(Start). The analog trigger values come from // the per-slot `g_in[slot]` (pass lt/rt explicitly so a single shoulder byte never leaks across slots). diff --git a/OpenPuck/gamepad_util.h b/OpenPuck/gamepad_util.h index 8596edd5..7a6955b9 100644 --- a/OpenPuck/gamepad_util.h +++ b/OpenPuck/gamepad_util.h @@ -72,15 +72,46 @@ void padStickBlend(uint32_t b, int16_t lpx, int16_t lpy, int16_t rpx, int16_t rpy, int16_t *lx, int16_t *ly, int16_t *rx, int16_t *ry); -// g_in[slot] sticks with padStickBlend applied -- what every emulated mode should send to the host. -uint32_t padDpadButtons(const PuckInput &in); +// D-pad bits (TB_D*) from the trackpads set to a D-pad mode (PS_DPAD_TOUCH / PS_DPAD_CLICK). raw = TB_* buttons. +uint32_t padDpadBits(uint32_t raw, int16_t lpx, int16_t lpy, int16_t rpx, + int16_t rpy); +static inline uint32_t padDpadButtons(const PuckInput &in) +{ + return padDpadBits(in.buttons, in.lpx, in.lpy, in.rpx, in.rpy); +} +// raw with the D-pad bits added, for every emulated mode's report builder ahead of the remap. A pad on +// D-pad-on-click spends its click on the D-pad, so the click's own mapping (touchpad click, mouse button) +// doesn't fire with it. +uint32_t padDpadApply(uint32_t raw, int16_t lpx, int16_t lpy, int16_t rpx, + int16_t rpy); +static inline uint32_t padDpadApplyIn(const PuckInput &in) +{ + return padDpadApply(in.buttons, in.lpx, in.lpy, in.rpx, in.rpy); +} +// the same from a raw 0x45 report, for the modes that decode it in place (16/18 left pad, 22/24 right pad) +static inline uint32_t padDpadApplyRaw(const uint8_t *r) +{ + return padDpadApply(btnsOf(r), (int16_t)s16off(r, 16), + (int16_t)s16off(r, 18), (int16_t)s16off(r, 22), + (int16_t)s16off(r, 24)); +} +// The pulse for a click on a pad in a D-pad mode (the active type's D-pad click feedback). Called for every +// controller each loop in the emulated modes. +void padDpadClickFeedback(uint8_t slot); +// g_in[slot] sticks with padStickBlend applied -- what every emulated mode should send to the host. void slotSticks(uint8_t slot, int16_t *lx, int16_t *ly, int16_t *rx, int16_t *ry); +// The host's buttons for one controller: the shortcut chords taken out, the button remap applied, and the +// Quick Access + Select target added while that chord is held (capture reports the Switch's Capture button, for +// the modes that have one). raw = TB_* buttons of the controller in `slot`. +uint32_t remapHostButtons(uint8_t slot, uint32_t raw, bool *capture = nullptr); // PlayStation-layout button packing (shared by mode_ps5 + mode_hidgyro). -uint32_t -psButtonsFromSteam(uint32_t raw); // apply chord-guard + the button remap +static inline uint32_t psButtonsFromSteam(uint8_t slot, uint32_t raw) +{ + return remapHostButtons(slot, raw); +} // L1..R3 + Create/Options byte. lt/rt are the per-slot analog trigger values so the digital ZL/ZR trip // doesn't leak across slots when several slots are active. uint8_t psShouldersByte(uint32_t b, uint8_t lt, uint8_t rt); diff --git a/OpenPuck/haptics.cpp b/OpenPuck/haptics.cpp index 2e562703..7aa20c20 100644 --- a/OpenPuck/haptics.cpp +++ b/OpenPuck/haptics.cpp @@ -1801,6 +1801,10 @@ void hapticTask() padClickEdge(s, shortcutHostButtons( g_in[s].buttons) & (TB_LPADC | TB_RPADC)); + if (!modeIsPuck(g_usbMode)) + for (uint8_t s = 0; s < NSLOT; s++) + if (g_slot[s].used) + padDpadClickFeedback(s); // Per-slot link-edge detect (backup for hapticOnReconnect in rf_link). static bool wasHapticLinkUp[NSLOT] = { 0 }; for (int s = 0; s < NSLOT; s++) { diff --git a/OpenPuck/mode_hidgyro.cpp b/OpenPuck/mode_hidgyro.cpp index 95bbdaea..5b6368a9 100644 --- a/OpenPuck/mode_hidgyro.cpp +++ b/OpenPuck/mode_hidgyro.cpp @@ -209,7 +209,7 @@ static ds4_setcb_t const DS4_SETCB[NSLOT] = { hidGyroSet0, hidGyroSet1, // usbSlot drives the per-HID counters; slot (bond) is the controller whose decoded input feeds the report. static void hidGyroBuild(uint8_t usbSlot, uint8_t slot, uint8_t out[63]) { - uint32_t b = psButtonsFromSteam(g_in[slot].buttons); + uint32_t b = psButtonsFromSteam(slot, padDpadApplyIn(g_in[slot])); padClickEdge(slot, b & (TB_LPADC | TB_RPADC)); // A pad mapped to a stick must NOT also report as a touchpad contact -- the host would read the same // finger twice (stick deflection AND a cursor drag). A disabled pad reports none either. diff --git a/OpenPuck/mode_ps3.cpp b/OpenPuck/mode_ps3.cpp index 83ee9e11..17c204b0 100644 --- a/OpenPuck/mode_ps3.cpp +++ b/OpenPuck/mode_ps3.cpp @@ -251,7 +251,7 @@ static void ds3Imu(uint8_t *out, int16_t v) // Offsets are the genuine report's rd[] minus one (rd[0] is the prepended id). static void ds3Build(uint8_t slot, uint8_t out[48]) { - uint32_t b = psButtonsFromSteam(g_in[slot].buttons); + uint32_t b = psButtonsFromSteam(slot, padDpadApplyIn(g_in[slot])); bool l2 = (g_in[slot].lt > SW_TRIG_ON) || (b & TB_L2); bool r2 = (g_in[slot].rt > SW_TRIG_ON) || (b & TB_R2); memset(out, 0, 48); diff --git a/OpenPuck/mode_ps5.cpp b/OpenPuck/mode_ps5.cpp index 7ee670fa..e9d688e5 100644 --- a/OpenPuck/mode_ps5.cpp +++ b/OpenPuck/mode_ps5.cpp @@ -509,7 +509,7 @@ static ps5_setcb_t const PS5_SETCB[NSLOT] = { ps5Set0, ps5Set1, ps5Set2, // usbSlot drives the per-HID sequence counter; bond is the controller whose decoded input feeds the report. static void ps5Build(uint8_t usbSlot, uint8_t slot, uint8_t out[63]) { - uint32_t b = psButtonsFromSteam(g_in[slot].buttons); + uint32_t b = psButtonsFromSteam(slot, padDpadApplyIn(g_in[slot])); padClickEdge(slot, b & (TB_LPADC | TB_RPADC)); // A pad mapped to a stick must NOT also report as a touchpad contact -- the host would read the same // finger twice (stick deflection AND a cursor drag). A disabled pad reports none either. diff --git a/OpenPuck/mode_switch_hori.cpp b/OpenPuck/mode_switch_hori.cpp index fbb5758a..e35c1f61 100644 --- a/OpenPuck/mode_switch_hori.cpp +++ b/OpenPuck/mode_switch_hori.cpp @@ -40,7 +40,7 @@ static void switchBuildHoripad(uint8_t slot, uint8_t out[8]) { bool capture; uint32_t b = - remapButtons(shortcutHostButtons(g_in[slot].buttons), &capture); + remapHostButtons(slot, padDpadApplyIn(g_in[slot]), &capture); uint16_t btn = 0; if (b & TB_Y) btn |= 0x01; diff --git a/OpenPuck/mode_switch_pro.cpp b/OpenPuck/mode_switch_pro.cpp index a840be22..f2fd8bce 100644 --- a/OpenPuck/mode_switch_pro.cpp +++ b/OpenPuck/mode_switch_pro.cpp @@ -445,20 +445,9 @@ static void jcInputPrefix(uint8_t slot, uint8_t *out) { uint8_t bond = jcBondOf( slot); // input data comes from the mapped controller; timer/state stay per USB slot - uint32_t b = g_in[bond].buttons | padDpadButtons(g_in[bond]); - bool qamSelect = switchSelectShortcut(bond, b); - b = shortcutHostButtons(b); - // QAM + Select is the screenshot shortcut: QAM does not also act as its own mapping - if (qamSelect) - b &= ~(uint32_t)TB_QAM; + uint32_t b = padDpadApplyIn(g_in[bond]); bool capture; - b = remapButtons(b, &capture); - if (qamSelect) { - const RemapTarget t = - remapTarget(g_swQamSelect, remapFacesSwapped(g_btnMap)); - b |= t.tb; - capture |= t.capture; - } + b = remapHostButtons(bond, b, &capture); uint32_t jc = 0; if (b & TB_Y) jc |= JC_BTN_Y; @@ -986,36 +975,9 @@ void SwitchProController::mountSlots(uint8_t k) USBDevice.addInterface(g_swPro[u]); } } -static void swDpadClickFeedback(uint8_t bond, uint32_t buttons) -{ - static uint32_t held[NSLOT] = {}; - if (bond >= NSLOT) - return; - uint32_t clicks = buttons & (TB_LPADC | TB_RPADC); - uint32_t pressed = clicks & ~held[bond]; - held[bond] = clicks; - if (!g_swDpadHaptics || shortcutHeld(buttons) || haptic82Blocked(bond)) - return; - // PS_DISABLED comes after the D-pad modes, so it is not a range test - bool left = (pressed & TB_LPADC) && (buttons & TB_LPADT) && - (g_padStick[0] == PS_DPAD_TOUCH || - g_padStick[0] == PS_DPAD_CLICK); - bool right = (pressed & TB_RPADC) && (buttons & TB_RPADT) && - (g_padStick[1] == PS_DPAD_TOUCH || - g_padStick[1] == PS_DPAD_CLICK); - if (left || right) { - const uint8_t pulse[3] = { hsidePads(left, right), 2, - padClickGain() }; - // ahead of any queued HD-rumble frames, which would delay the click by their length - relayEnqueueFront(0x82, pulse, sizeof pulse, true, bond); - } -} - void SwitchProController::task() { for (uint8_t s = 0; s < g_usbMountCount; s++) { - uint8_t bond = jcBondOf(s); - swDpadClickFeedback(bond, g_in[bond].buttons); if (!g_swPro[s].ready()) continue; // Deferred user-cal flash write (queued by the USB ISR; debounced so a calibration write-burst diff --git a/OpenPuck/mode_xbox_og.cpp b/OpenPuck/mode_xbox_og.cpp index 6d35deee..c0502918 100644 --- a/OpenPuck/mode_xbox_og.cpp +++ b/OpenPuck/mode_xbox_og.cpp @@ -534,9 +534,10 @@ static Adafruit_USBD_XboxOg g_xboxOgInterface; XboxOgController g_xboxOgCtl; -static void xboxOgBuildReport(XboxOgInputReport &report, const uint8_t *raw) +static void xboxOgBuildReport(XboxOgInputReport &report, const uint8_t *raw, + uint8_t slot) { - uint32_t buttons = remapButtons(shortcutHostButtons(btnsOf(raw))); + uint32_t buttons = remapHostButtons(slot, padDpadApplyRaw(raw)); xboxOgNeutralReport(report); if (buttons & TB_DUP) @@ -670,7 +671,7 @@ void XboxOgController::onReport45(int slot, const uint8_t *rep, bool fresh, uint32_t generation = g_xboxOgGeneration; XboxOgInputReport report; - xboxOgBuildReport(report, rep); + xboxOgBuildReport(report, rep, (uint8_t)slot); xboxOgPublishBondReport(report, slot, generation); } diff --git a/OpenPuck/mode_xinput.cpp b/OpenPuck/mode_xinput.cpp index 5c37aaf6..4e4353f5 100644 --- a/OpenPuck/mode_xinput.cpp +++ b/OpenPuck/mode_xinput.cpp @@ -291,7 +291,7 @@ static Adafruit_USBD_HID // ===================== report 0x45 -> XInput + mouse ===================== static void rfXboxGamepad(uint8_t slot, const uint8_t *r) { - uint32_t b = remapButtons(shortcutHostButtons(btnsOf(r))); + uint32_t b = remapHostButtons(slot, padDpadApplyRaw(r)); uint16_t btn = 0; if (b & TB_DUP) btn |= XB_DUP; @@ -347,7 +347,7 @@ static void rfXboxGamepad(uint8_t slot, const uint8_t *r) // one mouse. Slot 0's right-pad drives it; other slots' right-pad input is intentionally ignored here. static void rfXboxMouse(const uint8_t *r) { - uint32_t b = remapButtons(shortcutHostButtons(btnsOf(r))); + uint32_t b = remapButtons(shortcutHostButtons(padDpadApplyRaw(r))); static int prx = 0, pry = 0; static bool prt = false; static float vx = 0, vy = 0, rmx = 0, rmy = 0; diff --git a/OpenPuck/triton.h b/OpenPuck/triton.h index aa75ecfb..23a60bfb 100644 --- a/OpenPuck/triton.h +++ b/OpenPuck/triton.h @@ -57,9 +57,12 @@ // bit 1 (2u) was SHORTCUT_PROFILES, removed with the Switch Pro profiles // bit 2 (4u) was SHORTCUT_HAPTICS, the removed D-pad haptic shortcuts #define SHORTCUT_FEEDBACK 8u +// the old enable switch of Quick Access + Select; the chord now needs only a target, but a saved file still tells +// whether the Switch's was on #define SHORTCUT_CAPTURE 16u #define SHORTCUT_ENABLED 32u extern uint8_t g_shortcutFlags; +extern uint8_t g_qamSelect; // The modifier is held, whether or not the mode shortcuts are switched on: the profile switch uses it too. static inline bool shortcutModifierHeld(uint32_t buttons) @@ -93,12 +96,12 @@ static inline uint32_t shortcutHostButtons(uint32_t buttons) return buttons; } -static inline bool switchSelectShortcut(uint8_t slot, uint32_t &buttons) +static inline bool qamSelectShortcut(uint8_t slot, uint32_t &buttons) { static bool consumed[4] = {}; if (slot >= 4) return false; - bool active = (g_shortcutFlags & SHORTCUT_CAPTURE) && + bool active = g_qamSelect && (buttons & (TB_QAM | TB_MENU)) == (TB_QAM | TB_MENU); if (!(buttons & TB_MENU)) consumed[slot] = false; diff --git a/OpenPuck/webusb_config.cpp b/OpenPuck/webusb_config.cpp index 8e27e6cb..0e46905f 100644 --- a/OpenPuck/webusb_config.cpp +++ b/OpenPuck/webusb_config.cpp @@ -126,8 +126,9 @@ static bool boardCommand(uint8_t op) // profile per type, 2 bits each; v30: p[216 + 4 * setting + type] per-type haptic settings // (HAP_* in config.h: grip limiter knee, haptic style, trackpad click strength, movement-tick // spacing and strength), WebUSB field 120 + 4 * setting + type; p[205] / p[209] stay the DualSense's style and -// the active type's knee for older panels] -#define WB_PAYLEN 242 +// the active type's knee for older panels; p[248 + type] Quick Access + Select target per type, WebUSB +// field 152 + type] +#define WB_PAYLEN 250 // The blob send is drop-on-full (never blocks loop), so the vendor TX FIFO MUST be able to hold a whole blob // -- otherwise tud_vendor_write_available() never reaches the frame size and EVERY frame is dropped (blank // panel / stale mappings). The Makefile sets -DCFG_TUD_VENDOR_TX_BUFSIZE=256; guard it here so a build without @@ -401,6 +402,7 @@ static void webusbSendBlob() p[212 + et] = (uint8_t)(g_typeRumbleScale[et] / 2); for (uint8_t k = 0; k < HAP_COUNT; k++) p[216 + 4 * k + et] = g_typeHaptic[k][et]; + p[248 + et] = g_typeQamSelect[et]; } // CRITICAL: usb_web.write() SPINS (`while (remain && _connected) yield();`) until the IN FIFO drains or the // panel disconnects. If the panel holds the WebUSB interface open but stops reading its IN endpoint -- a @@ -428,11 +430,11 @@ static void webusbSendSwitchFrame() p[1] = WB_SW_PAYLEN; p[2] = 1; memset(p + 3, 0, 37); - p[40] = g_swDpadHaptics; + p[40] = g_typeHaptic[HAP_DPAD][ET_SWITCH]; p[41] = g_storageState; p[42] = (uint8_t)(g_hdPadScale / 2); memset(p + 43, 0, 4); // ex rumble presets + slot - p[47] = g_swQamSelect; + p[47] = g_typeQamSelect[ET_SWITCH]; p[48] = g_shortcutFlags; // the status blob has no room left: this frame is read on every poll too, so a profile switched on the // controller in Lizard mode shows without asking @@ -1527,6 +1529,15 @@ void webusbPoll() n -= need; continue; } + // Quick Access + Select target per type: field 152 + type + if (f >= 152 && f < 152 + ET_COUNT) { + typeQamSelectSet((uint8_t)(f - 152), v); + saveCfgSoon(); + g_blobRequest = true; + memmove(buf, buf + need, n - need); + n -= need; + continue; + } // fields 190..229 were the Switch Pro profiles (removed): ignored, but still answered // so an older panel waiting on the status reply doesn't stall if (f >= 190 && f < 230) { @@ -1536,8 +1547,10 @@ void webusbPoll() continue; } switch (f) { + // the Switch-only D-pad click feedback of older panels, now the Switch's per-type entry case 230: - g_swDpadHaptics = v ? 1 : 0; + typeHapticSet(HAP_DPAD, ET_SWITCH, + v ? 1 : 0); break; case 1: g_mDiv = v < 4 ? 4 : v; @@ -1715,9 +1728,9 @@ void webusbPoll() case 251: case 252: break; + // the Switch-only target of older panels, now the Switch's per-type entry (field 153) case 239: - if (v <= 20) - g_swQamSelect = v; + typeQamSelectSet(ET_SWITCH, v); break; case 39: // rumble style: no longer a setting (it follows the mode) break; diff --git a/README.md b/README.md index d7fcac8d..041b9683 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Upstream's configurator doesn't have the settings below. - **Grip limiter:** strong grip vibration above a set level (70% by default) is rounded off instead of clipping into a pop. On the Switch and DS5 tabs, with a separate setting for Switch HD rumble and DualSense audio haptics, as is the Haptics style. - **Trackpad haptic strength:** each mode's tab has click strength, tick spacing and tick strength sliders. With a tick spacing chosen the puck plays the movement ticks and the clicks itself, so both have a strength you set. This is experimental. Details: [docs/CONTROLLER_FEATURES.md](docs/CONTROLLER_FEATURES.md). - **Per-mode rumble:** each mode keeps its own rumble switch and grip strength, and the rumble style follows the mode. See [Rumble](#rumble) below. -- **Switch Pro trackpad D-pad and screenshot shortcut:** trackpads can act as a D-pad on touch or on click (diagonals included), and Quick Access + Minus can take a screenshot. Details: [docs/CONTROLLER_FEATURES.md](docs/CONTROLLER_FEATURES.md). +- **Trackpad D-pad and Quick Access + Select:** in every emulated mode, trackpads can act as a D-pad on touch or on click (diagonals included), and Quick Access + Select can act as a button of your choice (a screenshot on the Switch). Details: [docs/CONTROLLER_FEATURES.md](docs/CONTROLLER_FEATURES.md). - **Full-rate haptic streams in Steam mode:** the controller's PCM haptic commands are relayed at full rate, so audio-to-haptics apps such as Fancy Haptics play as cleanly through OpenPuck as over USB. - **Trigger deadzone and full-press point:** emulated modes can ignore the first part of the trigger travel and treat a pull past a set point as a full press. This helps games made for DualSense resistive triggers that never see a full pull. - **Shortcut modifier choice:** mode shortcuts can use Quick Access as the modifier instead of all four back buttons. Shortcuts can also play a confirmation pulse or be turned off entirely. See [Mode shortcuts](#mode-shortcuts). @@ -67,7 +67,7 @@ Hold the modifier and press a button. The modifier is all four back buttons by d | D-pad Down | Switch HORIPAD | Configurable | | Left Right Trackpad click | Toggle touchpad | High buzz = on, low buzz = off | -Also in every mode: **Steam + Y held for 2 seconds** turns the controller off. In Switch Pro mode, **Quick Access + Minus** can take a screenshot (turn on "Quick Access + Select action" in the Mode shortcuts card). +Also in every mode: **Steam + Y held for 2 seconds** turns the controller off. **Quick Access + Select** can act as any button you pick for each mode (a screenshot on the Switch, for example); it does nothing until you choose one in the Button map card. ### Modes | Mode (configurator name) | What it is | diff --git a/docs/CONTROLLER_FEATURES.md b/docs/CONTROLLER_FEATURES.md index 58f4a595..dd72dc29 100644 --- a/docs/CONTROLLER_FEATURES.md +++ b/docs/CONTROLLER_FEATURES.md @@ -2,13 +2,13 @@ ## Trackpads -Switch Pro trackpad mapping includes D-pad on touch and D-pad while clicked, including diagonals. Trackpad D-pad click feedback has a separate enable control. These and the other Switch Pro-only settings (Quick Access + Minus target, gyro mapping) are on the Switch tab of Button mapping in the configurator. +Every emulated mode can use a trackpad as a D-pad, on touch or while clicked, including diagonals. On click, the click presses the D-pad instead of its own mapping (touchpad click, mouse button). Each type's **D-pad feedback** pulses the pad each time its direction changes, even without lifting your finger, so sliding from down to down-right is felt, and on each D-pad click while Trackpad haptics is Off (with it on, every click already pulses). A pad on a D-pad mode has its Click setting greyed out. The Switch Pro-only settings (Quick Access + Minus target, gyro mapping) are on the Switch tab of Button mapping in the configurator. The Switch Pro back-button profiles from the upstream PR were removed from this fork. Every emulated type now has mapping profiles of its own (below). ### Trackpad haptic strength -Each mode's tab has three sliders under **Trackpads**. **Click strength** sets the clicks the puck sends itself (Clicks only mode, the PlayStation modes, the Switch D-pad click feedback and the test haptic), from -20 dB to +12 dB, +7 dB by default; Steam's own range tops out at 12 dB. **Tick spacing** and **Tick strength** are for the movement ticks with Trackpad haptics On. The controller plays its own ticks and clicks at a fixed strength, and Controller's own, the lowest spacing, leaves them that way. Any other spacing (Sparse, Normal, Dense, Densest) makes the puck play both itself, the ticks at Tick strength and the clicks at Click strength, with the ticks paused around a click so it stands out. To do that the puck sets the controller's tick spacing to its maximum and its trackpad click pressure to -1, which turns off its own tick and click pulses; going back to Controller's own restores them at once. Clicks only has no ticks. Tick spacing and strength are untested on the Steam Controller 2. Steam mode relays Steam's own settings, so these sliders don't apply there. All three are part of the per-type haptic settings in [PROTOCOL.md](PROTOCOL.md). +Each mode's tab has three sliders under **Trackpads**. **Click strength** sets the clicks the puck sends itself (Clicks only mode, the PlayStation modes, the D-pad feedback and the test haptic), from -20 dB to +12 dB, +7 dB by default; Steam's own range tops out at 12 dB. **Tick spacing** and **Tick strength** are for the movement ticks with Trackpad haptics On. The controller plays its own ticks and clicks at a fixed strength, and Controller's own, the lowest spacing, leaves them that way. Any other spacing (Sparse, Normal, Dense, Densest) makes the puck play both itself, the ticks at Tick strength and the clicks at Click strength, with the ticks paused around a click so it stands out. To do that the puck sets the controller's tick spacing to its maximum and its trackpad click pressure to -1, which turns off its own tick and click pulses; going back to Controller's own restores them at once. Clicks only has no ticks. Tick spacing and strength are untested on the Steam Controller 2. Steam mode relays Steam's own settings, so these sliders don't apply there. All three are part of the per-type haptic settings in [PROTOCOL.md](PROTOCOL.md). ## Rumble @@ -38,7 +38,7 @@ Hold the modifier (Quick Access or all four back buttons) plus a button to switc | D-pad Left / Up / Right / Down | Assigned mode | | A | Return to Steam | -Quick Access + Minus/Select/Menu has an independent Switch Pro mapping, defaulting to Take screenshot. It works with either modifier setting. Minus is suppressed until release, including if Quick Access is released first. Holding Capture retains the console's normal capture hold behavior. +Quick Access + Minus/Select/Menu has a target of its own for each controller type, in the Button map card beside the Quick Access mapping. It is always on and defaults to none for every type, so nothing changes until you pick a button (on the Switch, Take screenshot is the usual choice; Guide, Home or PS gives the Steam button's role). It works with either modifier setting. Minus is suppressed until release, including if Quick Access is released first. Holding Capture retains the console's normal capture hold behavior. Quick Access retains its normal per-type mapping when it is not reserved as the enabled modifier. Selecting the four-back modifier suppresses the four paddles while held together; otherwise normal paddle mappings apply. @@ -54,7 +54,7 @@ In the configurator, each type's Button mapping tab edits one profile at a time: On the PlayStation tabs, **Create = touchpad click** makes the Create button press the touchpad in the profile being edited (it changes that one entry; while it is set the button reads **Revert Create button**). It replaces the old Create = touchpad click switch; a puck that had the switch on carries it into its profiles on the first boot with profiles. -The **Trackpads** card has a column for each pad. *Touch* is what touching the pad does: the touchpad on the PlayStation types, the mouse on Xbox's right pad (its speed and glide are in the same card), a stick, or on the Switch types the D-pad. **Disabled** (PlayStation pads and the Xbox right pad) ignores the touch altogether; the other pads already do nothing when Touch is Off. *Click* is what the click acts as, the same setting as the button map used to hold, so a pad that drives a stick, or is disabled, can keep or change its click; set Click to none to silence the pad entirely. Trackpad haptics, and on the Switch types the D-pad click feedback and HD trackpad strength, are in the card too. In Xbox 360 mode a click is the mouse's left button on the right pad and the right button on the left pad, unless you map it elsewhere. +The **Trackpads** card has a column for each pad. *Touch* is what touching the pad does: the touchpad on the PlayStation types, the mouse on Xbox's right pad (its speed and glide are in the same card), a stick, or the D-pad. **Disabled** (PlayStation pads and the Xbox right pad) ignores the touch altogether; the other pads already do nothing when Touch is Off. *Click* is what the click acts as, the same setting as the button map used to hold, so a pad that drives a stick, or is disabled, can keep or change its click; set Click to none to silence the pad entirely. Trackpad haptics, D-pad feedback, and on the Switch types HD trackpad strength, are in the card too. In Xbox 360 mode a click is the mouse's left button on the right pad and the right button on the left pad, unless you map it elsewhere. To switch profiles on the controller, hold the shortcut modifier (Quick Access or all four back buttons, as set in Mode shortcuts) and press the previous or next profile button, LB and RB by default. The controller buzzes the new profile's number, even when shortcut confirmation pulses are off. While the modifier is held, those two buttons do not reach the game. This works whether or not mode shortcuts are on. The **Switch profiles from the controller** card turns it off or picks other buttons (LB, RB, L3, R3, Select or Start); it is one setting for every type. The new profile is saved 3 seconds after the last switch, so stepping through several writes flash once. diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 70fdf833..cc1ce3f1 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -531,9 +531,9 @@ Messages: controller is live). The fields other than `97` answer with the first page. Switch Pro / HD rumble / shortcuts (blob version ≥ 23): `190`-`229` were the removed Switch Pro back-button profiles and are ignored (still answered with a status blob), `230` trackpad D-pad click - feedback, `231` HD trackpad strength as percent/2, `239` Quick Access + Select target, `240` shortcut + feedback (now the Switch's entry of per-type setting 7, which `0xAE` byte 38 also reports), `231` HD trackpad strength as percent/2, `239` Quick Access + Select target of the Switch (now its entry of the per-type targets below, field `152 + type`), `240` shortcut flags (bit0 QAM modifier, bits 1-2 unused (were profiles / D-pad haptic shortcuts; cleared), bit3 - confirmation pulses, bit4 QAM + Select, bit5 enabled). `241`-`252` were the removed D-pad haptic + confirmation pulses, bit4 (was QAM + Select enable; unused now), bit5 enabled). `241`-`252` were the removed D-pad haptic shortcut presets / strength steps / slots and are ignored. Per-type grip strength (blob version >= 25): `108`-`111` as percent/2 for emulated types 0-3 (Xbox, Switch, DS4, DS5); `104`-`107` (a per-type rumble style during development) are ignored. Status blob @@ -549,7 +549,7 @@ Messages: DualSense audio haptics and speaker and Switch HD rumble grips; from version 30 each type has its own (below), field 115 still sets both the Switch's and the DualSense's, and status blob `p[209]` (payload byte 207) reports the active type's. - Per-type haptic settings (blob version >= 30): one grid of seven settings by four types (Xbox, Switch, DS4, DS5). + Per-type haptic settings (blob version >= 30): one grid of eight settings by four types (Xbox, Switch, DS4, DS5). Setting `k` of type `t` is field `120 + 4k + t` and status blob `p[216 + 4k + t]` (payload byte `214 + 4k + t`), and the config file keeps the same grid in its tail. A write that is out of range, or for a type without the setting, is ignored. A config from the v29 release has no grid: the limiter and the DualSense style start from @@ -564,6 +564,7 @@ Messages: | 4 | movement-tick gain step | all | as setting 2 | 11 (-9 dB) | | 5 | soft boost of the grip wave stream | Switch, DS5 | `0` off, `1`-`4` raise quiet passages by up to 1.5× / 2× / 3× / 4× | 1 (1.5×) | | 6 | where adaptive-trigger effects are felt | DS5 | `0` off, `1` trackpads, `2` grips, `3` both | 1 | + | 7 | trackpad D-pad feedback | all | `0` off, `1` a click pulse on a pad in a D-pad mode each time its direction changes, and on each click while Trackpad haptics is Off | 1 (the Switch's starts from field 230's old value) | Soft boost is an upward compressor on the PCM stream to the grips (Wave style). It follows the louder grip's peak level (instant rise, ~80 ms fall) and, below 20% of full scale, raises it 2:1 toward that level, capped @@ -571,10 +572,22 @@ Messages: noise in a quiet track stays quiet. The grips play down to ~0.5% of full scale in proportion (IMU), but a hand barely feels a few percent. Firmware before it sends a shorter status blob (no `p[236..239]`). + Quick Access + Select target, per controller type: field `152 + type` (0-20, a remap target code, `0` none, `18` + the Switch's Capture button) and status blob `p[248 + type]` (payload byte `246 + type`). Default `0` for every + type. The chord acts whenever the active type has a target, with no other switch; bit 4 of the shortcut flags + (field 240), which used to enable it, is ignored. The target replaces both Quick Access and Select for the + host. The config file keeps the four bytes at the end of its tail, and the Switch's still in the old + `swQamSelect` byte. A file from before keeps the Switch's target only if its bit 4 was set (`18` when no target + was saved). + Trigger haptics plays a game's adaptive-trigger effects (ticks into a resistance zone, a weapon's snap, trigger vibration) on the chosen actuators; the trigger status itself is always reported. See DUALSENSE_HAPTICS.md §10. Firmware before it sends a shorter status blob (no `p[240..243]`). + D-pad feedback skips the click pulse while Trackpad haptics is On or Clicks only, because every click already + pulses then; the pulse for a direction change plays either way. On D-pad on click, going from none to a + direction is the click itself, so only a change between directions adds a pulse. Firmware before it is Switch-only (field 230) and sends a shorter status blob (no `p[244..247]`). + Ticks and clicks apply with Trackpad haptics On; Clicks only has no ticks. With a tick spacing set, the puck plays both as `0x82` pulses at the gains above and sets the controller's tick spacing (controller setting 14) to its maximum and its trackpad click pressures (settings 52 and 53) to -1, which turns off its own tick and @@ -803,8 +816,9 @@ Status blob payload: Later fields are appended (the version byte says how far the payload goes). From version 20 the tail carries the per-emulated-type trackpad-to-stick mapping at payload bytes 187..194 — two bytes per type -(`{left pad, right pad}`), each `0` off / `1` left stick / `2` right stick (`3` / `4` the Switch Pro D-pad on -touch / on click, from version 23; `5` disabled, from version 30). Set with +(`{left pad, right pad}`), each `0` off / `1` left stick / `2` right stick (`3` / `4` the D-pad on touch / on +click, from version 23 for the Switch Pro and in every emulated mode since; `5` disabled, from version 30). A pad on +D-pad on click spends its click on the D-pad, so the click's own mapping doesn't fire with it. Set with `0x02 <80 + type*2 + pad> `. Fields 40..75 are the per-type config block, so the mapping starts at 80. A mapped pad **blends** with its stick: while the pad is touched each axis reports whichever of the two sources is deflected further from center (signed); an untouched pad contributes nothing and the physical diff --git a/docs/TESTING_GUIDE.md b/docs/TESTING_GUIDE.md index 31ccb574..18872b9b 100644 --- a/docs/TESTING_GUIDE.md +++ b/docs/TESTING_GUIDE.md @@ -19,7 +19,7 @@ - A fresh config starts the Switch tab on Tone with 100% grip strength, and the other tabs at 200% (the DS5 tab on Wave). A config that already has per-type strengths keeps them. - Trackpad click strength, tick spacing and tick strength (each tab, under Trackpads): Click strength at +12 dB must - feel clearly stronger than the +7 dB default, in a PlayStation mode, Clicks only, the Switch D-pad click feedback + feel clearly stronger than the +7 dB default, in a PlayStation mode, Clicks only, the D-pad feedback (Trackpad haptics Off) and the Test haptic. With Trackpad haptics On, Tick spacing at Controller's own leaves the controller's ticks and clicks alone; any other spacing makes the puck play both, so Tick strength and Click strength must change them, the ticks must pause around a click, and the physical click must still register. Clicks only has no ticks. diff --git a/docs/index.html b/docs/index.html index ab944476..586133f1 100644 --- a/docs/index.html +++ b/docs/index.html @@ -367,7 +367,6 @@

Mode shortcuts

-
@@ -430,7 +429,6 @@

Trackpad mouse

A short pulse on the clicked pad, once per press, in either trackpad D-pad mode. Independent of Trackpad haptics above.

-
@@ -2002,7 +2000,7 @@

Firmware update

]; const GROUPS = [ ["Face buttons", [0,1,2,3]], ["Shoulders and triggers", [4,5,22,23]], ["Sticks", [6,7]], - ["Menu buttons", [8,9,10]], ["D-pad", [11,12,13,14]], ["Back buttons and Quick Access", [15,16,17,18,19]], + ["Menu buttons", [8,9,10]], ["D-pad", [11,12,13,14]], ["Back buttons", [15,16,17,18]], ["Quick Access", [19]], ]; // the trackpad click targets (21/22) are in a type's list only where its mode does something with them: the // touchpad click on the PlayStation types, the mouse buttons on Xbox (TYPE_DEFS padTargets). "none" is 0. @@ -2138,6 +2136,29 @@

Firmware update

// ---- the UI: three cards per type, built by buildProfileCards (types.js) ---- const recs = []; // by type: {strip, use, copy, copyFrom, reset, nintendo, title, sources[], gesture{...}, cards[]} +// Quick Access + Select (the Select button held with Quick Access): what the game sees instead of either. One +// setting per controller type, not part of a profile; the Switch's starts as Take screenshot. Firmware without +// it sends a shorter status blob, which hides the row (status.js). +const QAM_SELECT_FIELD0 = 152, QAM_SELECT_INDEX0 = 246; +function qamSelectRow(et, def, rec){ + const r = document.createElement("div"); r.className = "row hide"; + r.title = "While Quick Access and Select are held together the game sees this button instead of either."; + const l = document.createElement("label"); + const plus = document.createElement("span"); plus.className = "ic-plus"; plus.textContent = "+"; + l.append(iconEl("qam"), plus, iconEl("view")); + const s = document.createElement("select"); + for(const [v, lbl, glyph] of targetOptions(def)){ + if(v > 20) continue; + const o = document.createElement("option"); o.value = v; + o.textContent = v === 18 && def.key === "SWITCH" ? "Take screenshot" : lbl; + if(glyph) o.dataset.glyph = glyph; + s.appendChild(o); + } + r.append(l, glyphSelect(s)); + s.addEventListener("change", () => setField(QAM_SELECT_FIELD0 + et, +s.value)); + rec.qamSelect = s; + return r; +} function buildProfileCards(sec, et, def){ const card = cls => { const c = document.createElement("div"); c.className = "card prof-card " + (cls || ""); sec.appendChild(c); return c; }; const head = (c, text) => { const h = document.createElement("div"); h.className = "card-head"; const t = document.createElement("h2"); t.textContent = text; h.appendChild(t); c.appendChild(h); return t; }; @@ -2227,6 +2248,7 @@

Firmware update

r.append(l, glyphSelect(s)); g.appendChild(r); s.addEventListener("change", () => profileOp([0x2D, et, sel0(et), i, +s.value])); rec.sources[i] = s; + if(i === SRC_QAM) g.appendChild(qamSelectRow(et, def, rec)); } grid.appendChild(g); } @@ -2355,8 +2377,23 @@

Firmware update

// the trackpad -> stick selects of this type's Trackpads card show the selected profile's const pads = typeEls[et]; if(pads && pads.padStick) pads.padStick.forEach((s, pad) => { if(!held(s)) s.value = st.pads[sel][pad]; }); + padClickLock(et); renderGesture(); } +// A pad on one of the D-pad modes (3 touch, 4 click) spends its click on the D-pad, so the pad's Click target +// has nothing left to do; grey it out rather than let it look like it would still apply. +function padClickLock(et){ + const rec = recs[et], pads = typeEls[et]; + if(!rec || !pads || !pads.padStick) return; + pads.padStick.forEach((s, pad) => { + const c = rec.sources[SRC_PADC + pad]; + if(!c) return; + const lock = s.value === "3" || s.value === "4", row = c.closest(".row"); + c.disabled = lock; + if(lock) row.title = "This trackpad is a D-pad, so its click presses the D-pad. Change Touch to use the click for something else."; + else row.removeAttribute("title"); + }); +} // the switch buttons are puck-wide: every type's card shows them function renderGesture(){ if(S.gesture) recs.forEach(r => { @@ -2401,7 +2438,7 @@

Firmware update

{key:"DS5", name:"DS5", labels:{1:"Cross",2:"Circle",3:"Square",4:"Triangle",5:"L1",6:"R1",7:"L3",8:"R3",9:"Create",10:"Options",11:"PS",19:"L2",20:"R2",...DPAD,16:"Touchpad Click",17:"Mute"}, padTargets:PS_PADS}, ]; // Grip strength (percent) and haptic style a type starts with: the firmware's g_typeRumbleScaleDefault and kHapticStyleDefault -const typeDefaults=def=>({rumble:200,style:3,click:26,tick:0,tickGain:11,knee:85,lift:1,trig:1,...def.haptics}); +const typeDefaults=def=>({rumble:200,style:3,click:26,tick:0,tickGain:11,knee:85,lift:1,trig:1,dpad:1,...def.haptics}); // movement-tick spacing: 0 leaves the ticks to the controller (fixed strength), 1..4 have the puck play them, a wider to a // narrower distance apart const PAD_TICK_LABELS=["Controller's own","Sparse","Normal","Dense","Densest"]; @@ -2425,7 +2462,7 @@

Firmware update

const o = [[0,off]]; if(off !== "Off") o.push([PAD_DISABLED,"Disabled"]); o.push([1,"Left stick","stickL"],[2,"Right stick","stickR"]); - if(def.key==="SWITCH") o.push([3,"D-pad on touch (Switch Pro)"],[4,"D-pad on click (Switch Pro)"]); + o.push([3,"D-pad on touch"],[4,"D-pad on click"]); return o; } function mkSelect(def, includeNone){ @@ -2442,7 +2479,9 @@

Firmware update

// [swQamSelect][shortcutFlags][8 unused] function swStatusApply(s){ const capable=!!s; - $("#swClickControls").classList.toggle("hide",!capable); + // firmware with the per-type D-pad click feedback shows it on every tab instead + const perTypeDpad=!!(S.lastP && S.lastP.length>hapIndex(HAP.DPAD,3)); + $("#swClickControls").classList.toggle("hide",!capable || perTypeDpad); $("#swBonds").textContent=S.lastP && S.lastP.length>60?S.lastP[60]:"—"; $("#swStorage").textContent=capable?(["Unavailable","Ready","Initialized","Save failed"][s[39]]||"Unknown"):"Not reported"; const storageBad=capable && (s[39]===0 || s[39]===3); @@ -2483,6 +2522,7 @@

Firmware update

if(sw||ds5) f.push(perType ? [hapField(HAP.KNEE,et),typeDefaults(TYPE_DEFS[et]).knee,30] : [115,70,27]); if((sw||ds5) && S.lastP && S.lastP.length>hapIndex(HAP.LIFT,et)) f.push([hapField(HAP.LIFT,et),typeDefaults(TYPE_DEFS[et]).lift,30]); if(ds5 && S.lastP && S.lastP.length>hapIndex(HAP.TRIG,et)) f.push([hapField(HAP.TRIG,et),typeDefaults(TYPE_DEFS[et]).trig,30]); + if(S.lastP && S.lastP.length>hapIndex(HAP.DPAD,et)) f.push([hapField(HAP.DPAD,et),typeDefaults(TYPE_DEFS[et]).dpad,30]); const style=typeDefaults(TYPE_DEFS[et]).style; if(sw) f.push([hapField(HAP.STYLE,et),style,30]); if(ds5) f.push([31,1,0],perType ? [hapField(HAP.STYLE,et),style,30] : [88,style,22],[30,0,0],[114,0,26],[116,0,29]); @@ -2521,9 +2561,9 @@

Firmware update

// Protocol 30 keeps the per-type settings in one grid (firmware HAP_* in config.h): WebUSB field // 120 + 4 * setting + type, status blob byte 216 + 4 * setting + type (index 214 + ... here, the header being cut). // The release before it (29) has one limiter for all types (field 115) and the style on the DualSense only (field 88). -const HAP={KNEE:0,STYLE:1,CLICK:2,TICK:3,TICKGAIN:4,LIFT:5,TRIG:6}; +const HAP={KNEE:0,STYLE:1,CLICK:2,TICK:3,TICKGAIN:4,LIFT:5,TRIG:6,DPAD:7}; // per setting, in HAP order: the backup key and its range -const HAP_KEYS=[["limiter",50,100],["hapticStyle",0,3],["padClick",0,31],["padTick",0,4],["padTickGain",0,31],["softBoost",0,4],["triggerHaptics",0,3]]; +const HAP_KEYS=[["limiter",50,100],["hapticStyle",0,3],["padClick",0,31],["padTick",0,4],["padTickGain",0,31],["softBoost",0,4],["triggerHaptics",0,3],["dpadClick",0,1]]; const hapField=(k,et)=>120+4*k+et; const hapIndex=(k,et)=>214+4*k+et; const hapValue=(p,k,et)=>p[hapIndex(k,et)]; @@ -2533,8 +2573,6 @@

Firmware update

const HD_PAD_SCALES=Array.from({length:251},(_,i)=>i*2); function initTypes(){ - for(const o of mkSelect(TYPE_DEFS[1],true).options){const c=o.cloneNode(true);if(+c.value===18)c.textContent="Take screenshot";$("#qamSelect").appendChild(c);} - glyphSelect($("#qamSelect")); $("#swClickFeedback").onchange=()=>setField(230,+$("#swClickFeedback").value); (function buildTypeCfgs(){ const host=document.getElementById("typeCfgs"), tabs=document.getElementById("mapTabs"); @@ -2574,7 +2612,7 @@

Firmware update

// with profiles the whole card is replaced: its Create switch is a preset now gBtn.classList.add("prof-legacy"); relabel(ab,iconEl("A"),iconEl("B"),plus(),iconEl("X"),iconEl("Y")," swap"); - // Trackpads: a column per pad, what touching it does and what its click acts as; then what they share + // Trackpads: a column per pad, what touching it does and what its click acts as. Their haptics have their own card below. const gPad=group("Trackpads"); gPad.classList.add("span"); const cols=document.createElement("div"); cols.className="mapsrc"; gPad.appendChild(cols); for(let pad=0; pad<2; pad++){ @@ -2583,7 +2621,7 @@

Firmware update

const sel=document.createElement("select"); for(const [v,lbl,glyph] of padTouchOptions(def,pad)){ const o=document.createElement("option"); o.value=v; o.textContent=lbl; if(v!==PAD_DISABLED) o.dataset.glyph=glyph||(v===4?(pad?"padClickR":"padClickL"):(pad?"padR":"padL")); sel.appendChild(o); } row(col,"Touch",glyphSelect(sel)); - sel.addEventListener("change",()=>setPadStick(et,pad,+sel.value,PAD_STICK_FIELD0+et*2+pad)); + sel.addEventListener("change",()=>{ padClickLock(et); setPadStick(et,pad,+sel.value,PAD_STICK_FIELD0+et*2+pad); }); rec.padStick.push(sel); buildPadClick(et,pad,col); } @@ -2591,13 +2629,25 @@

Firmware update

note.textContent={ XBOX:"In Xbox 360 mode the right trackpad moves a mouse pointer while its Touch is Mouse, and a click acts as set here: by default the right pad is the left mouse button and the left pad the right one. Disabled stops the pointer; the click is separate. The Original Xbox has no mouse.", SWITCH:"A click does nothing in the Switch modes unless you map it to a button here. The D-pad modes read the trackpad themselves, so they work whatever Click is set to.", - }[def.key] || "A trackpad that is disabled or drives a stick no longer reports as a touchpad, but its click still presses the touchpad unless you map Click to something else."; + }[def.key] || "A trackpad that is disabled, drives a stick or acts as a D-pad no longer reports as a touchpad, but its click still presses the touchpad unless you map Click to something else."; + note.textContent+=" On D-pad on click, the click presses the D-pad instead."; gPad.appendChild(note); } + // one grid cell for Trackpad feedback and the small cards under it (Adaptive triggers, Lights), balancing the Rumble column + const stackL=document.createElement("div"); stackL.className="stack"; + const gPadH=group("Trackpad feedback"); gPad.after(stackL); stackL.append(gPadH); // 1 = controller's own haptics (ticks while moving + clicks), 2 = clicks only, 0 = off const pad=document.createElement("select"); for(const [v,lbl] of [[1,"On"],[2,"Clicks only"],[0,"Off"]]){ const o=document.createElement("option"); o.value=v; o.textContent=lbl; pad.appendChild(o); } - row(gPad,"Trackpad haptics",pad); + row(gPadH,"Trackpad haptics",pad); pad.addEventListener("change",()=>setField(40+et*9+6, +pad.value)); + // D-pad click feedback (every type): a pulse when a trackpad in a D-pad mode is clicked + { const sel=document.createElement("select"); + for(const [v,lbl] of [[1,"On (default)"],[0,"Off"]]){ const o=document.createElement("option"); o.value=v; o.textContent=lbl; sel.appendChild(o); } + sel.addEventListener("change",()=>setField(hapField(HAP.DPAD,et),+sel.value)); + const note=document.createElement("p"); note.className="note"; + note.textContent="A short pulse on a trackpad that is a D-pad each time its direction changes, so sliding from down to down-right is felt without lifting your finger, and on each click while Trackpad haptics is Off (with it on, every click already pulses)."; + const wrap=document.createElement("div"); wrap.className="hide"; wrap.append(row(gPadH,"D-pad feedback",sel),note); gPadH.appendChild(wrap); + rec.dpad=sel; rec.dpadWrap=wrap; } // Click strength (protocol v30): the slider position is the firmware's step, weak -> strong. // Movement ticks: the controller plays its own at a fixed strength (spacing 0); any other spacing has the puck play them at the tick strength. { const gHap=document.createElement("div"); gHap.className="hide"; @@ -2616,8 +2666,8 @@

Firmware update

tickG.addEventListener("change",()=>setField(hapField(HAP.TICKGAIN,et), +tickG.value)); const note=document.createElement("p"); note.className="note"; note.textContent="Click strength applies to the clicks the puck sends itself: Clicks only, the PlayStation modes and the Switch D-pad click feedback. Movement ticks only play with Trackpad haptics On. The controller plays its own ticks and clicks at a fixed strength; choose a spacing and the puck plays both instead, ticks at the Tick strength and clicks at the Click strength, pausing the ticks around a click. Back on Controller's own they return at once. Clicks only has no ticks. Tick spacing and strength are untested on the Steam Controller 2."; - gHap.append(row(gPad,"Click strength",click,clickV),row(gPad,"Tick spacing",tick,tickV),row(gPad,"Tick strength",tickG,tickGV),note); - gPad.appendChild(gHap); + gHap.append(row(gPadH,"Click strength",click,clickV),row(gPadH,"Tick spacing",tick,tickV),row(gPadH,"Tick strength",tickG,tickGV),note); + gPadH.appendChild(gHap); Object.assign(rec,{padHap:gHap,padClick:click,padClickV:clickV,padClickFmt:fmtClick,padTick:tick,padTickV:tickV,padTickG:tickG,padTickGV:tickGV}); } // the Xbox pointer's speed is one setting shared with Lizard mode: setTab() moves its card in or out of here if(def.key==="XBOX"){ rec.mouseHost=document.createElement("div"); rec.mouseHost.className="pad-mouse"; gPad.appendChild(rec.mouseHost); } @@ -2625,11 +2675,12 @@

Firmware update

// same order on the Switch and DS5 tabs. A Switch game sends one signal, HD rumble, so its haptics strength is // the grip rumble strength. A DualSense game sends motor rumble and audio haptics apart, each with its own. const gRum=group("Rumble"); - // one grid cell for Rumble and the small cards under it (Adaptive triggers, Lights), so they stack at any column count + // one grid cell for Rumble and the Haptics card under it, so they stack at any column count const stack=document.createElement("div"); stack.className="stack"; gRum.before(stack); stack.append(gRum); const rumble=toggle(gRum,"Rumble","on"); const sw=def.key==="SWITCH", ds5=def.key==="DS5"; const gHapt=(sw||ds5)?group("Haptics"):null; + if(gHapt) stack.append(gHapt); if(ds5){ const aud=toggle(gHapt,"Haptics","on"); rec.audioHaptics=aud; @@ -2706,7 +2757,7 @@

Firmware update

// Adaptive triggers: where the DualSense trigger effects are felt; its own group, as it isn't part of the grip stream if(ds5){ const gTrig=group("Adaptive triggers"); gTrig.classList.add("hide"); - stack.append(gTrig); + stackL.append(gTrig); const sel=document.createElement("select"); for(const [v,lbl] of [[0,"Off"],[1,"Trackpads"],[2,"Grips"],[3,"Trackpads and grips"]]){ const o=document.createElement("option"); o.value=v; o.textContent=lbl+(v===typeDefaults(def).trig?" (default)":""); sel.appendChild(o); } sel.addEventListener("change",()=>setField(hapField(HAP.TRIG,et),+sel.value)); @@ -2716,7 +2767,7 @@

Firmware update

rec.trig=sel; rec.trigWrap=gTrig; } // LED brightness - { const gLed=group("Lights"); stack.append(gLed); + { const gLed=group("Lights"); stackL.append(gLed); const sl=document.createElement("input"); sl.type="range"; sl.min=0; sl.max=100; sl.step=5; const vspan=document.createElement("span"); vspan.className="val"; row(gLed,"LED brightness",sl,vspan); @@ -2744,10 +2795,10 @@

Firmware update

rec.abSwap=ab; rec.pad=pad; rec.rumble=rumble; // Switch Pro-only settings get their own card on the Switch profile if(def.key==="SWITCH"){ - const g=group("Switch Pro mode only"); g.classList.add("span"); gPad.after(g); - for(const id of ["qamSelectRow","swGyroMapBlock","swGyroOld"]) g.appendChild(document.getElementById(id)); - // the D-pad click feedback and the HD trackpad strength belong with the trackpads - for(const id of ["swClickControls","hdPadBlock"]) gPad.appendChild(document.getElementById(id)); + const g=group("Switch Pro mode only"); g.classList.add("span"); rec.prof.cards[2].after(g); // right below the Button map card + for(const id of ["swGyroMapBlock","swGyroOld"]) g.appendChild(document.getElementById(id)); + // the D-pad click feedback and the HD trackpad strength belong with the trackpad feedback + for(const id of ["swClickControls","hdPadBlock"]) gPadH.appendChild(document.getElementById(id)); } host.appendChild(sec); typeEls.push(rec); }); @@ -3861,9 +3912,7 @@

Firmware update

profilesNoteSw(s); $("#shortcutToggles").classList.toggle("hide",!s); $("#hdPadBlock").classList.toggle("hide",!s); - $("#qamSelectRow").classList.toggle("hide",!s); if(!s) return; - if(!held($("#qamSelect"))){ $("#qamSelect").value=s[45]; repaintGlyphSelects(); } if(document.activeElement!==$("#hdPadScale")) $("#hdPadScale").value=s[40]*2; const flags=s[46],enabled=!!(flags&32); // scQam shows the modifier as its glyph (.ic-mod, like the chord labels) instead of on/off @@ -4152,6 +4201,16 @@

Firmware update

rec.liftWrap.classList.toggle("hide",!cap); if(cap && document.activeElement!==rec.lift) rec.lift.value=hapValue(p,HAP.LIFT,et); } + if(rec.prof && rec.prof.qamSelect){ + const cap=p.length>QAM_SELECT_INDEX0+et; + rec.prof.qamSelect.closest(".row").classList.toggle("hide",!cap); + if(cap && !held(rec.prof.qamSelect)){ rec.prof.qamSelect.value=p[QAM_SELECT_INDEX0+et]; repaintGlyphSelects(); } + } + if(rec.dpad){ + const cap=perType && p.length>hapIndex(HAP.DPAD,et); + rec.dpadWrap.classList.toggle("hide",!cap); + if(cap && document.activeElement!==rec.dpad) rec.dpad.value=hapValue(p,HAP.DPAD,et); + } if(rec.trig){ const cap=perType && p.length>hapIndex(HAP.TRIG,et); rec.trigWrap.classList.toggle("hide",!cap); @@ -4189,6 +4248,7 @@

Firmware update

// with profiles the selects show the profile being edited (renderProfiles), not the active one if(padStickCap && !profCap && !held(sel)) sel.value=p[185+et*2+pad]; }); + padClickLock(et); // dot marks the type matching the current puck mode rec.activeDot.style.display = (et===activeEt) ? "" : "none"; }); @@ -4272,7 +4332,7 @@

Firmware update

repaintGlyphSelects(); syncNav(); // cards above may have appeared or hidden (lizard, triggers, RF, logging build) } -const SC_BITS=[['scQam',1],['scFeedback',8],['scCapture',16],['scEnabled',32]]; +const SC_BITS=[['scQam',1],['scFeedback',8],['scEnabled',32]]; // ---- panel/dongle.js ---- // ---- ReversePuck controller dongle: paired-pucks list (0xAC frame) ---- @@ -4563,6 +4623,7 @@

Firmware update

if(p[0]>=21 && p.length>192) t.padStick=[p[185+et*2],p[186+et*2]]; if(p[0]>=25 && p.length>213) t.rumbleScale=p[210+et]*2; if(p[0]>=30) HAP_KEYS.forEach(([key],k)=>{ t[key]=hapValue(p,k,et); }); + if(p.length>QAM_SELECT_INDEX0+et) t.qamSelect=p[QAM_SELECT_INDEX0+et]; types.push(t); } const cfg={ mode:p[1], mDiv:p[2], mFric:p[3], persistMode:p[22], chord:[p[23],p[24],p[25]], types }; // D-pad chords only exist from protocol v18, the Switch gyro mapping from v19; omit the keys entirely on @@ -4652,6 +4713,7 @@

Firmware update

if(hc.rumbleScale!==undefined && (!isInt(hc.rumbleScale,10,500) || !S.lastP || S.lastP[0]<21)){ log("import: invalid or unsupported rumble strength");return; } if(Array.isArray(hc.types) && hc.types.some(t=>t && t.rumbleScale!==undefined && (!isInt(t.rumbleScale,10,500) || t.rumbleScale%2))){ log("import: invalid per-type rumble strength");return; } if(Array.isArray(hc.types) && hc.types.some(t=>t && HAP_KEYS.some(([key,lo,hi])=>t[key]!==undefined && !isInt(t[key],lo,hi)))){ log("import: invalid per-type haptic setting");return; } + if(Array.isArray(hc.types) && hc.types.some(t=>t && t.qamSelect!==undefined && !isInt(t.qamSelect,0,20))){ log("import: invalid Quick Access + Select target");return; } if((hc.swDpadHaptics!==undefined && !isInt(hc.swDpadHaptics,0,1)) || (hc.hdPadScale!==undefined && (!isInt(hc.hdPadScale,0,500) || hc.hdPadScale%2)) || (hc.swQamSelect!==undefined && !isInt(hc.swQamSelect,0,20)) || @@ -4691,7 +4753,8 @@

Firmware update

// mapping fields would only reach the active profile, so they are skipped then const profN=(Array.isArray(c.profiles) && profilesCapable())?c.profiles.length:0; const hapN=(S.lastP && S.lastP[0]>=30 && Array.isArray(c.types))?c.types.slice(0,typeN).reduce((n,t)=>n+HAP_KEYS.filter(([key])=>t && t[key]!==undefined).length,0):0; - const total=baseN + typeN*9 + padStickN + hapN + profN*4 + (profN && c.profileGesture?1:0) + lizardN + 4 + 1 + (c.swDpadHaptics!==undefined?1:0) + (c.rumbleScale!==undefined?1:0) + (c.hdPadScale!==undefined?1:0) + (c.swQamSelect!==undefined?1:0) + (c.shortcutFlags!==undefined?1:0); + const qamN=(S.lastP && S.lastP.length>QAM_SELECT_INDEX0 && Array.isArray(c.types))?c.types.slice(0,typeN).filter((t,et)=>t && t.qamSelect!==undefined && S.lastP.length>QAM_SELECT_INDEX0+et).length:0; + const total=baseN + typeN*9 + padStickN + hapN + qamN + profN*4 + (profN && c.profileGesture?1:0) + lizardN + 4 + 1 + (c.swDpadHaptics!==undefined?1:0) + (c.rumbleScale!==undefined?1:0) + (c.hdPadScale!==undefined?1:0) + (c.swQamSelect!==undefined?1:0) + (c.shortcutFlags!==undefined?1:0); let step=0, stage=""; const tick=()=>modalStage(stage, Math.min(99,Math.floor(step*100/total))); try{ @@ -4742,7 +4805,8 @@

Firmware update

await sf(40+et*9+8, t.rumble!==undefined?t.rumble:1); if(!mapped && Array.isArray(t.padStick)){ await sf(PAD_STICK_FIELD0+et*2, t.padStick[0]); await sf(PAD_STICK_FIELD0+et*2+1, t.padStick[1]); } if(typeRumble && t.rumbleScale!==undefined) await sf(108+et, t.rumbleScale/2); - if(S.lastP && S.lastP[0]>=30) for(let k=0; k=30) for(let k=0; kQAM_SELECT_INDEX0+et) await sf(QAM_SELECT_FIELD0+et, t.qamSelect); } log("settings replayed ("+(baseN+typeN*9)+" fields)"); } else { log("settings replayed ("+baseN+" fields — no per-type config in this backup)"); } if(c.swDpadHaptics!==undefined) await sf(230,c.swDpadHaptics); @@ -4991,7 +5055,6 @@

Firmware update

$("#rumbleHdTest").onclick=async()=>{ if(S.dev){ await send([0x36]); log("HD pan test sent"); } }; for(const [id,bit] of SC_BITS)$('#'+id).onclick=()=>{if(S.lastSw)setField(240,S.lastSw[46]^bit);}; $("#hdPadScale").addEventListener("change",()=>setField(231,+$("#hdPadScale").value/2)); -$("#qamSelect").addEventListener("change",()=>setField(239,+$("#qamSelect").value)); $("#rumbleScale").addEventListener("change", ()=>setField(22, (+$("#rumbleScale").value)/2)); $("#trigInner").addEventListener("change", ()=>setField(102, +$("#trigInner").value)); $("#trigOuter").addEventListener("change", ()=>setField(103, +$("#trigOuter").value)); diff --git a/panel/app.js b/panel/app.js index 2d25cd17..d9997fee 100644 --- a/panel/app.js +++ b/panel/app.js @@ -112,7 +112,6 @@ $("#rumbleTest").onclick=async()=>{ if(S.dev){ await send([0x16]); log("test rum $("#rumbleHdTest").onclick=async()=>{ if(S.dev){ await send([0x36]); log("HD pan test sent"); } }; for(const [id,bit] of SC_BITS)$('#'+id).onclick=()=>{if(S.lastSw)setField(240,S.lastSw[46]^bit);}; $("#hdPadScale").addEventListener("change",()=>setField(231,+$("#hdPadScale").value/2)); -$("#qamSelect").addEventListener("change",()=>setField(239,+$("#qamSelect").value)); $("#rumbleScale").addEventListener("change", ()=>setField(22, (+$("#rumbleScale").value)/2)); $("#trigInner").addEventListener("change", ()=>setField(102, +$("#trigInner").value)); $("#trigOuter").addEventListener("change", ()=>setField(103, +$("#trigOuter").value)); diff --git a/panel/backup.js b/panel/backup.js index ff7bfe83..66ba3727 100644 --- a/panel/backup.js +++ b/panel/backup.js @@ -5,7 +5,7 @@ import { CHORD_DPAD_FIELD } from './status.js'; import { HAP_KEYS, PAD_STICK_FIELD0, TYPE_DEFS, hapField, hapValue } from './types.js'; import { LZ_MAX, lizardCapable, lzReadProfile, lzWriteProfile, readLizard } from './lizard.js'; import { modalDone, modalOpen, modalStage } from './firmware.js'; -import { LZ_TYPE, profilesCapable, profilesLoad } from './profiles.js'; +import { LZ_TYPE, QAM_SELECT_FIELD0, QAM_SELECT_INDEX0, profilesCapable, profilesLoad } from './profiles.js'; // ---- Backup / clone ---- // A backup file is { magic, version, bonds[4], config{} }. bonds come from the 0x09 export (0xA7 frame); the @@ -24,6 +24,7 @@ export function buildBackup(p, bp){ if(p[0]>=21 && p.length>192) t.padStick=[p[185+et*2],p[186+et*2]]; if(p[0]>=25 && p.length>213) t.rumbleScale=p[210+et]*2; if(p[0]>=30) HAP_KEYS.forEach(([key],k)=>{ t[key]=hapValue(p,k,et); }); + if(p.length>QAM_SELECT_INDEX0+et) t.qamSelect=p[QAM_SELECT_INDEX0+et]; types.push(t); } const cfg={ mode:p[1], mDiv:p[2], mFric:p[3], persistMode:p[22], chord:[p[23],p[24],p[25]], types }; // D-pad chords only exist from protocol v18, the Switch gyro mapping from v19; omit the keys entirely on @@ -113,6 +114,7 @@ export async function importBackup(file){ if(hc.rumbleScale!==undefined && (!isInt(hc.rumbleScale,10,500) || !S.lastP || S.lastP[0]<21)){ log("import: invalid or unsupported rumble strength");return; } if(Array.isArray(hc.types) && hc.types.some(t=>t && t.rumbleScale!==undefined && (!isInt(t.rumbleScale,10,500) || t.rumbleScale%2))){ log("import: invalid per-type rumble strength");return; } if(Array.isArray(hc.types) && hc.types.some(t=>t && HAP_KEYS.some(([key,lo,hi])=>t[key]!==undefined && !isInt(t[key],lo,hi)))){ log("import: invalid per-type haptic setting");return; } + if(Array.isArray(hc.types) && hc.types.some(t=>t && t.qamSelect!==undefined && !isInt(t.qamSelect,0,20))){ log("import: invalid Quick Access + Select target");return; } if((hc.swDpadHaptics!==undefined && !isInt(hc.swDpadHaptics,0,1)) || (hc.hdPadScale!==undefined && (!isInt(hc.hdPadScale,0,500) || hc.hdPadScale%2)) || (hc.swQamSelect!==undefined && !isInt(hc.swQamSelect,0,20)) || @@ -152,7 +154,8 @@ export async function importBackup(file){ // mapping fields would only reach the active profile, so they are skipped then const profN=(Array.isArray(c.profiles) && profilesCapable())?c.profiles.length:0; const hapN=(S.lastP && S.lastP[0]>=30 && Array.isArray(c.types))?c.types.slice(0,typeN).reduce((n,t)=>n+HAP_KEYS.filter(([key])=>t && t[key]!==undefined).length,0):0; - const total=baseN + typeN*9 + padStickN + hapN + profN*4 + (profN && c.profileGesture?1:0) + lizardN + 4 + 1 + (c.swDpadHaptics!==undefined?1:0) + (c.rumbleScale!==undefined?1:0) + (c.hdPadScale!==undefined?1:0) + (c.swQamSelect!==undefined?1:0) + (c.shortcutFlags!==undefined?1:0); + const qamN=(S.lastP && S.lastP.length>QAM_SELECT_INDEX0 && Array.isArray(c.types))?c.types.slice(0,typeN).filter((t,et)=>t && t.qamSelect!==undefined && S.lastP.length>QAM_SELECT_INDEX0+et).length:0; + const total=baseN + typeN*9 + padStickN + hapN + qamN + profN*4 + (profN && c.profileGesture?1:0) + lizardN + 4 + 1 + (c.swDpadHaptics!==undefined?1:0) + (c.rumbleScale!==undefined?1:0) + (c.hdPadScale!==undefined?1:0) + (c.swQamSelect!==undefined?1:0) + (c.shortcutFlags!==undefined?1:0); let step=0, stage=""; const tick=()=>modalStage(stage, Math.min(99,Math.floor(step*100/total))); try{ @@ -203,7 +206,8 @@ export async function importBackup(file){ await sf(40+et*9+8, t.rumble!==undefined?t.rumble:1); if(!mapped && Array.isArray(t.padStick)){ await sf(PAD_STICK_FIELD0+et*2, t.padStick[0]); await sf(PAD_STICK_FIELD0+et*2+1, t.padStick[1]); } if(typeRumble && t.rumbleScale!==undefined) await sf(108+et, t.rumbleScale/2); - if(S.lastP && S.lastP[0]>=30) for(let k=0; k=30) for(let k=0; kQAM_SELECT_INDEX0+et) await sf(QAM_SELECT_FIELD0+et, t.qamSelect); } log("settings replayed ("+(baseN+typeN*9)+" fields)"); } else { log("settings replayed ("+baseN+" fields — no per-type config in this backup)"); } if(c.swDpadHaptics!==undefined) await sf(230,c.swDpadHaptics); diff --git a/panel/profiles.js b/panel/profiles.js index b51a154f..dc09dd33 100644 --- a/panel/profiles.js +++ b/panel/profiles.js @@ -29,7 +29,7 @@ const SOURCES = [ ]; const GROUPS = [ ["Face buttons", [0,1,2,3]], ["Shoulders and triggers", [4,5,22,23]], ["Sticks", [6,7]], - ["Menu buttons", [8,9,10]], ["D-pad", [11,12,13,14]], ["Back buttons and Quick Access", [15,16,17,18,19]], + ["Menu buttons", [8,9,10]], ["D-pad", [11,12,13,14]], ["Back buttons", [15,16,17,18]], ["Quick Access", [19]], ]; // the trackpad click targets (21/22) are in a type's list only where its mode does something with them: the // touchpad click on the PlayStation types, the mouse buttons on Xbox (TYPE_DEFS padTargets). "none" is 0. @@ -165,6 +165,29 @@ export async function profilesDrain(){ // ---- the UI: three cards per type, built by buildProfileCards (types.js) ---- const recs = []; // by type: {strip, use, copy, copyFrom, reset, nintendo, title, sources[], gesture{...}, cards[]} +// Quick Access + Select (the Select button held with Quick Access): what the game sees instead of either. One +// setting per controller type, not part of a profile; the Switch's starts as Take screenshot. Firmware without +// it sends a shorter status blob, which hides the row (status.js). +export const QAM_SELECT_FIELD0 = 152, QAM_SELECT_INDEX0 = 246; +function qamSelectRow(et, def, rec){ + const r = document.createElement("div"); r.className = "row hide"; + r.title = "While Quick Access and Select are held together the game sees this button instead of either."; + const l = document.createElement("label"); + const plus = document.createElement("span"); plus.className = "ic-plus"; plus.textContent = "+"; + l.append(iconEl("qam"), plus, iconEl("view")); + const s = document.createElement("select"); + for(const [v, lbl, glyph] of targetOptions(def)){ + if(v > 20) continue; + const o = document.createElement("option"); o.value = v; + o.textContent = v === 18 && def.key === "SWITCH" ? "Take screenshot" : lbl; + if(glyph) o.dataset.glyph = glyph; + s.appendChild(o); + } + r.append(l, glyphSelect(s)); + s.addEventListener("change", () => setField(QAM_SELECT_FIELD0 + et, +s.value)); + rec.qamSelect = s; + return r; +} export function buildProfileCards(sec, et, def){ const card = cls => { const c = document.createElement("div"); c.className = "card prof-card " + (cls || ""); sec.appendChild(c); return c; }; const head = (c, text) => { const h = document.createElement("div"); h.className = "card-head"; const t = document.createElement("h2"); t.textContent = text; h.appendChild(t); c.appendChild(h); return t; }; @@ -254,6 +277,7 @@ export function buildProfileCards(sec, et, def){ r.append(l, glyphSelect(s)); g.appendChild(r); s.addEventListener("change", () => profileOp([0x2D, et, sel0(et), i, +s.value])); rec.sources[i] = s; + if(i === SRC_QAM) g.appendChild(qamSelectRow(et, def, rec)); } grid.appendChild(g); } @@ -382,8 +406,23 @@ export function renderProfiles(et){ // the trackpad -> stick selects of this type's Trackpads card show the selected profile's const pads = typeEls[et]; if(pads && pads.padStick) pads.padStick.forEach((s, pad) => { if(!held(s)) s.value = st.pads[sel][pad]; }); + padClickLock(et); renderGesture(); } +// A pad on one of the D-pad modes (3 touch, 4 click) spends its click on the D-pad, so the pad's Click target +// has nothing left to do; grey it out rather than let it look like it would still apply. +export function padClickLock(et){ + const rec = recs[et], pads = typeEls[et]; + if(!rec || !pads || !pads.padStick) return; + pads.padStick.forEach((s, pad) => { + const c = rec.sources[SRC_PADC + pad]; + if(!c) return; + const lock = s.value === "3" || s.value === "4", row = c.closest(".row"); + c.disabled = lock; + if(lock) row.title = "This trackpad is a D-pad, so its click presses the D-pad. Change Touch to use the click for something else."; + else row.removeAttribute("title"); + }); +} // the switch buttons are puck-wide: every type's card shows them function renderGesture(){ if(S.gesture) recs.forEach(r => { diff --git a/panel/status.js b/panel/status.js index d33212b3..93227e53 100644 --- a/panel/status.js +++ b/panel/status.js @@ -9,7 +9,7 @@ import { syncMapNav, syncNav } from './nav.js'; import { syncMotionCap } from './motion.js'; import { repaintGlyphSelects } from './glyphselect.js'; import { fillIcon } from './icons.js'; -import { profilesCapable, profilesNoteBlob, profilesNoteSw, syncProfileVisibility } from './profiles.js'; +import { QAM_SELECT_INDEX0, padClickLock, profilesCapable, profilesNoteBlob, profilesNoteSw, syncProfileVisibility } from './profiles.js'; export const MODE_NAMES = ["Steam","Xbox 360","Switch HORIPAD","Lizard","Switch Pro","PS5 DualSense","PS4 DualShock","PS5 DualSense (single HID)","PS4 DualShock (single HID)","PS3 DualShock","Original Xbox","DirectInput","SInput"]; // display order for mode lists (MODE_NAMES is indexed by the firmware's mode number); single-HID after their base @@ -45,9 +45,7 @@ export function applySw(s){ profilesNoteSw(s); $("#shortcutToggles").classList.toggle("hide",!s); $("#hdPadBlock").classList.toggle("hide",!s); - $("#qamSelectRow").classList.toggle("hide",!s); if(!s) return; - if(!held($("#qamSelect"))){ $("#qamSelect").value=s[45]; repaintGlyphSelects(); } if(document.activeElement!==$("#hdPadScale")) $("#hdPadScale").value=s[40]*2; const flags=s[46],enabled=!!(flags&32); // scQam shows the modifier as its glyph (.ic-mod, like the chord labels) instead of on/off @@ -336,6 +334,16 @@ export function applyBlob(p){ rec.liftWrap.classList.toggle("hide",!cap); if(cap && document.activeElement!==rec.lift) rec.lift.value=hapValue(p,HAP.LIFT,et); } + if(rec.prof && rec.prof.qamSelect){ + const cap=p.length>QAM_SELECT_INDEX0+et; + rec.prof.qamSelect.closest(".row").classList.toggle("hide",!cap); + if(cap && !held(rec.prof.qamSelect)){ rec.prof.qamSelect.value=p[QAM_SELECT_INDEX0+et]; repaintGlyphSelects(); } + } + if(rec.dpad){ + const cap=perType && p.length>hapIndex(HAP.DPAD,et); + rec.dpadWrap.classList.toggle("hide",!cap); + if(cap && document.activeElement!==rec.dpad) rec.dpad.value=hapValue(p,HAP.DPAD,et); + } if(rec.trig){ const cap=perType && p.length>hapIndex(HAP.TRIG,et); rec.trigWrap.classList.toggle("hide",!cap); @@ -373,6 +381,7 @@ export function applyBlob(p){ // with profiles the selects show the profile being edited (renderProfiles), not the active one if(padStickCap && !profCap && !held(sel)) sel.value=p[185+et*2+pad]; }); + padClickLock(et); // dot marks the type matching the current puck mode rec.activeDot.style.display = (et===activeEt) ? "" : "none"; }); @@ -456,4 +465,4 @@ export function applyBlob(p){ repaintGlyphSelects(); syncNav(); // cards above may have appeared or hidden (lizard, triggers, RF, logging build) } -export const SC_BITS=[['scQam',1],['scFeedback',8],['scCapture',16],['scEnabled',32]]; +export const SC_BITS=[['scQam',1],['scFeedback',8],['scEnabled',32]]; diff --git a/panel/types.js b/panel/types.js index e3b6d178..276112f8 100644 --- a/panel/types.js +++ b/panel/types.js @@ -5,7 +5,7 @@ import { MODE_NAMES, MODE_ORDER } from './status.js'; import { openNav } from './nav.js'; import { iconEl } from './icons.js'; import { glyphSelect } from './glyphselect.js'; -import { buildPadClick, buildProfileCards, profileOp, profilesCapable, setPadStick } from './profiles.js'; +import { buildPadClick, buildProfileCards, padClickLock, profileOp, profilesCapable, setPadStick } from './profiles.js'; // Per-emulated-type config (must match firmware ET_* order: Xbox=0, Switch=1, DS4=2, DS5=3). Each type lists // only the remap targets that exist on that controller. Field id sent to firmware = 40 + et*9 + k @@ -32,7 +32,7 @@ export const TYPE_DEFS = [ {key:"DS5", name:"DS5", labels:{1:"Cross",2:"Circle",3:"Square",4:"Triangle",5:"L1",6:"R1",7:"L3",8:"R3",9:"Create",10:"Options",11:"PS",19:"L2",20:"R2",...DPAD,16:"Touchpad Click",17:"Mute"}, padTargets:PS_PADS}, ]; // Grip strength (percent) and haptic style a type starts with: the firmware's g_typeRumbleScaleDefault and kHapticStyleDefault -const typeDefaults=def=>({rumble:200,style:3,click:26,tick:0,tickGain:11,knee:85,lift:1,trig:1,...def.haptics}); +const typeDefaults=def=>({rumble:200,style:3,click:26,tick:0,tickGain:11,knee:85,lift:1,trig:1,dpad:1,...def.haptics}); // movement-tick spacing: 0 leaves the ticks to the controller (fixed strength), 1..4 have the puck play them, a wider to a // narrower distance apart export const PAD_TICK_LABELS=["Controller's own","Sparse","Normal","Dense","Densest"]; @@ -56,7 +56,7 @@ function padTouchOptions(def, pad){ const o = [[0,off]]; if(off !== "Off") o.push([PAD_DISABLED,"Disabled"]); o.push([1,"Left stick","stickL"],[2,"Right stick","stickR"]); - if(def.key==="SWITCH") o.push([3,"D-pad on touch (Switch Pro)"],[4,"D-pad on click (Switch Pro)"]); + o.push([3,"D-pad on touch"],[4,"D-pad on click"]); return o; } function mkSelect(def, includeNone){ @@ -73,7 +73,9 @@ function mkSelect(def, includeNone){ // [swQamSelect][shortcutFlags][8 unused] export function swStatusApply(s){ const capable=!!s; - $("#swClickControls").classList.toggle("hide",!capable); + // firmware with the per-type D-pad click feedback shows it on every tab instead + const perTypeDpad=!!(S.lastP && S.lastP.length>hapIndex(HAP.DPAD,3)); + $("#swClickControls").classList.toggle("hide",!capable || perTypeDpad); $("#swBonds").textContent=S.lastP && S.lastP.length>60?S.lastP[60]:"—"; $("#swStorage").textContent=capable?(["Unavailable","Ready","Initialized","Save failed"][s[39]]||"Unknown"):"Not reported"; const storageBad=capable && (s[39]===0 || s[39]===3); @@ -114,6 +116,7 @@ function typeResetFields(et){ if(sw||ds5) f.push(perType ? [hapField(HAP.KNEE,et),typeDefaults(TYPE_DEFS[et]).knee,30] : [115,70,27]); if((sw||ds5) && S.lastP && S.lastP.length>hapIndex(HAP.LIFT,et)) f.push([hapField(HAP.LIFT,et),typeDefaults(TYPE_DEFS[et]).lift,30]); if(ds5 && S.lastP && S.lastP.length>hapIndex(HAP.TRIG,et)) f.push([hapField(HAP.TRIG,et),typeDefaults(TYPE_DEFS[et]).trig,30]); + if(S.lastP && S.lastP.length>hapIndex(HAP.DPAD,et)) f.push([hapField(HAP.DPAD,et),typeDefaults(TYPE_DEFS[et]).dpad,30]); const style=typeDefaults(TYPE_DEFS[et]).style; if(sw) f.push([hapField(HAP.STYLE,et),style,30]); if(ds5) f.push([31,1,0],perType ? [hapField(HAP.STYLE,et),style,30] : [88,style,22],[30,0,0],[114,0,26],[116,0,29]); @@ -152,9 +155,9 @@ export async function resetTypeDefaults(et){ // Protocol 30 keeps the per-type settings in one grid (firmware HAP_* in config.h): WebUSB field // 120 + 4 * setting + type, status blob byte 216 + 4 * setting + type (index 214 + ... here, the header being cut). // The release before it (29) has one limiter for all types (field 115) and the style on the DualSense only (field 88). -export const HAP={KNEE:0,STYLE:1,CLICK:2,TICK:3,TICKGAIN:4,LIFT:5,TRIG:6}; +export const HAP={KNEE:0,STYLE:1,CLICK:2,TICK:3,TICKGAIN:4,LIFT:5,TRIG:6,DPAD:7}; // per setting, in HAP order: the backup key and its range -export const HAP_KEYS=[["limiter",50,100],["hapticStyle",0,3],["padClick",0,31],["padTick",0,4],["padTickGain",0,31],["softBoost",0,4],["triggerHaptics",0,3]]; +export const HAP_KEYS=[["limiter",50,100],["hapticStyle",0,3],["padClick",0,31],["padTick",0,4],["padTickGain",0,31],["softBoost",0,4],["triggerHaptics",0,3],["dpadClick",0,1]]; export const hapField=(k,et)=>120+4*k+et; export const hapIndex=(k,et)=>214+4*k+et; export const hapValue=(p,k,et)=>p[hapIndex(k,et)]; @@ -164,8 +167,6 @@ export const RUMBLE_SCALES=Array.from({length:246},(_,i)=>10+i*2); const HD_PAD_SCALES=Array.from({length:251},(_,i)=>i*2); export function initTypes(){ - for(const o of mkSelect(TYPE_DEFS[1],true).options){const c=o.cloneNode(true);if(+c.value===18)c.textContent="Take screenshot";$("#qamSelect").appendChild(c);} - glyphSelect($("#qamSelect")); $("#swClickFeedback").onchange=()=>setField(230,+$("#swClickFeedback").value); (function buildTypeCfgs(){ const host=document.getElementById("typeCfgs"), tabs=document.getElementById("mapTabs"); @@ -205,7 +206,7 @@ export function initTypes(){ // with profiles the whole card is replaced: its Create switch is a preset now gBtn.classList.add("prof-legacy"); relabel(ab,iconEl("A"),iconEl("B"),plus(),iconEl("X"),iconEl("Y")," swap"); - // Trackpads: a column per pad, what touching it does and what its click acts as; then what they share + // Trackpads: a column per pad, what touching it does and what its click acts as. Their haptics have their own card below. const gPad=group("Trackpads"); gPad.classList.add("span"); const cols=document.createElement("div"); cols.className="mapsrc"; gPad.appendChild(cols); for(let pad=0; pad<2; pad++){ @@ -214,7 +215,7 @@ export function initTypes(){ const sel=document.createElement("select"); for(const [v,lbl,glyph] of padTouchOptions(def,pad)){ const o=document.createElement("option"); o.value=v; o.textContent=lbl; if(v!==PAD_DISABLED) o.dataset.glyph=glyph||(v===4?(pad?"padClickR":"padClickL"):(pad?"padR":"padL")); sel.appendChild(o); } row(col,"Touch",glyphSelect(sel)); - sel.addEventListener("change",()=>setPadStick(et,pad,+sel.value,PAD_STICK_FIELD0+et*2+pad)); + sel.addEventListener("change",()=>{ padClickLock(et); setPadStick(et,pad,+sel.value,PAD_STICK_FIELD0+et*2+pad); }); rec.padStick.push(sel); buildPadClick(et,pad,col); } @@ -222,13 +223,25 @@ export function initTypes(){ note.textContent={ XBOX:"In Xbox 360 mode the right trackpad moves a mouse pointer while its Touch is Mouse, and a click acts as set here: by default the right pad is the left mouse button and the left pad the right one. Disabled stops the pointer; the click is separate. The Original Xbox has no mouse.", SWITCH:"A click does nothing in the Switch modes unless you map it to a button here. The D-pad modes read the trackpad themselves, so they work whatever Click is set to.", - }[def.key] || "A trackpad that is disabled or drives a stick no longer reports as a touchpad, but its click still presses the touchpad unless you map Click to something else."; + }[def.key] || "A trackpad that is disabled, drives a stick or acts as a D-pad no longer reports as a touchpad, but its click still presses the touchpad unless you map Click to something else."; + note.textContent+=" On D-pad on click, the click presses the D-pad instead."; gPad.appendChild(note); } + // one grid cell for Trackpad feedback and the small cards under it (Adaptive triggers, Lights), balancing the Rumble column + const stackL=document.createElement("div"); stackL.className="stack"; + const gPadH=group("Trackpad feedback"); gPad.after(stackL); stackL.append(gPadH); // 1 = controller's own haptics (ticks while moving + clicks), 2 = clicks only, 0 = off const pad=document.createElement("select"); for(const [v,lbl] of [[1,"On"],[2,"Clicks only"],[0,"Off"]]){ const o=document.createElement("option"); o.value=v; o.textContent=lbl; pad.appendChild(o); } - row(gPad,"Trackpad haptics",pad); + row(gPadH,"Trackpad haptics",pad); pad.addEventListener("change",()=>setField(40+et*9+6, +pad.value)); + // D-pad click feedback (every type): a pulse when a trackpad in a D-pad mode is clicked + { const sel=document.createElement("select"); + for(const [v,lbl] of [[1,"On (default)"],[0,"Off"]]){ const o=document.createElement("option"); o.value=v; o.textContent=lbl; sel.appendChild(o); } + sel.addEventListener("change",()=>setField(hapField(HAP.DPAD,et),+sel.value)); + const note=document.createElement("p"); note.className="note"; + note.textContent="A short pulse on a trackpad that is a D-pad each time its direction changes, so sliding from down to down-right is felt without lifting your finger, and on each click while Trackpad haptics is Off (with it on, every click already pulses)."; + const wrap=document.createElement("div"); wrap.className="hide"; wrap.append(row(gPadH,"D-pad feedback",sel),note); gPadH.appendChild(wrap); + rec.dpad=sel; rec.dpadWrap=wrap; } // Click strength (protocol v30): the slider position is the firmware's step, weak -> strong. // Movement ticks: the controller plays its own at a fixed strength (spacing 0); any other spacing has the puck play them at the tick strength. { const gHap=document.createElement("div"); gHap.className="hide"; @@ -247,8 +260,8 @@ export function initTypes(){ tickG.addEventListener("change",()=>setField(hapField(HAP.TICKGAIN,et), +tickG.value)); const note=document.createElement("p"); note.className="note"; note.textContent="Click strength applies to the clicks the puck sends itself: Clicks only, the PlayStation modes and the Switch D-pad click feedback. Movement ticks only play with Trackpad haptics On. The controller plays its own ticks and clicks at a fixed strength; choose a spacing and the puck plays both instead, ticks at the Tick strength and clicks at the Click strength, pausing the ticks around a click. Back on Controller's own they return at once. Clicks only has no ticks. Tick spacing and strength are untested on the Steam Controller 2."; - gHap.append(row(gPad,"Click strength",click,clickV),row(gPad,"Tick spacing",tick,tickV),row(gPad,"Tick strength",tickG,tickGV),note); - gPad.appendChild(gHap); + gHap.append(row(gPadH,"Click strength",click,clickV),row(gPadH,"Tick spacing",tick,tickV),row(gPadH,"Tick strength",tickG,tickGV),note); + gPadH.appendChild(gHap); Object.assign(rec,{padHap:gHap,padClick:click,padClickV:clickV,padClickFmt:fmtClick,padTick:tick,padTickV:tickV,padTickG:tickG,padTickGV:tickGV}); } // the Xbox pointer's speed is one setting shared with Lizard mode: setTab() moves its card in or out of here if(def.key==="XBOX"){ rec.mouseHost=document.createElement("div"); rec.mouseHost.className="pad-mouse"; gPad.appendChild(rec.mouseHost); } @@ -256,11 +269,12 @@ export function initTypes(){ // same order on the Switch and DS5 tabs. A Switch game sends one signal, HD rumble, so its haptics strength is // the grip rumble strength. A DualSense game sends motor rumble and audio haptics apart, each with its own. const gRum=group("Rumble"); - // one grid cell for Rumble and the small cards under it (Adaptive triggers, Lights), so they stack at any column count + // one grid cell for Rumble and the Haptics card under it, so they stack at any column count const stack=document.createElement("div"); stack.className="stack"; gRum.before(stack); stack.append(gRum); const rumble=toggle(gRum,"Rumble","on"); const sw=def.key==="SWITCH", ds5=def.key==="DS5"; const gHapt=(sw||ds5)?group("Haptics"):null; + if(gHapt) stack.append(gHapt); if(ds5){ const aud=toggle(gHapt,"Haptics","on"); rec.audioHaptics=aud; @@ -337,7 +351,7 @@ export function initTypes(){ // Adaptive triggers: where the DualSense trigger effects are felt; its own group, as it isn't part of the grip stream if(ds5){ const gTrig=group("Adaptive triggers"); gTrig.classList.add("hide"); - stack.append(gTrig); + stackL.append(gTrig); const sel=document.createElement("select"); for(const [v,lbl] of [[0,"Off"],[1,"Trackpads"],[2,"Grips"],[3,"Trackpads and grips"]]){ const o=document.createElement("option"); o.value=v; o.textContent=lbl+(v===typeDefaults(def).trig?" (default)":""); sel.appendChild(o); } sel.addEventListener("change",()=>setField(hapField(HAP.TRIG,et),+sel.value)); @@ -347,7 +361,7 @@ export function initTypes(){ rec.trig=sel; rec.trigWrap=gTrig; } // LED brightness - { const gLed=group("Lights"); stack.append(gLed); + { const gLed=group("Lights"); stackL.append(gLed); const sl=document.createElement("input"); sl.type="range"; sl.min=0; sl.max=100; sl.step=5; const vspan=document.createElement("span"); vspan.className="val"; row(gLed,"LED brightness",sl,vspan); @@ -375,10 +389,10 @@ export function initTypes(){ rec.abSwap=ab; rec.pad=pad; rec.rumble=rumble; // Switch Pro-only settings get their own card on the Switch profile if(def.key==="SWITCH"){ - const g=group("Switch Pro mode only"); g.classList.add("span"); gPad.after(g); - for(const id of ["qamSelectRow","swGyroMapBlock","swGyroOld"]) g.appendChild(document.getElementById(id)); - // the D-pad click feedback and the HD trackpad strength belong with the trackpads - for(const id of ["swClickControls","hdPadBlock"]) gPad.appendChild(document.getElementById(id)); + const g=group("Switch Pro mode only"); g.classList.add("span"); rec.prof.cards[2].after(g); // right below the Button map card + for(const id of ["swGyroMapBlock","swGyroOld"]) g.appendChild(document.getElementById(id)); + // the D-pad click feedback and the HD trackpad strength belong with the trackpad feedback + for(const id of ["swClickControls","hdPadBlock"]) gPadH.appendChild(document.getElementById(id)); } host.appendChild(sec); typeEls.push(rec); }); diff --git a/tests/button_map/run.py b/tests/button_map/run.py index 1e1530f7..b6452ff5 100644 --- a/tests/button_map/run.py +++ b/tests/button_map/run.py @@ -38,7 +38,7 @@ def cut(text, a, b=None): // ---- globals the builders read ---- PuckInput g_in[NSLOT]; -uint8_t g_abSwap, g_back[4], g_qamMap, g_shortcutFlags, g_swQamSelect = 18, g_padStick[2]; +uint8_t g_abSwap, g_back[4], g_qamMap, g_shortcutFlags, g_qamSelect, g_padStick[2]; uint8_t g_trigInner = 0, g_trigOuter = 100, g_swGyroLegacy; bool g_touchpadDisabled; uint32_t g_gestureMask; @@ -48,7 +48,8 @@ def cut(text, a, b=None): // ---- stubs: everything that is not button mapping ---- uint8_t swStick(int16_t, bool) { return 0x80; } void slotSticks(uint8_t, int16_t *a, int16_t *b, int16_t *c, int16_t *d) { *a = *b = *c = *d = 0; } -uint32_t padDpadButtons(const PuckInput &) { return 0; } +uint32_t padDpadBits(uint32_t, int16_t, int16_t, int16_t, int16_t) { return 0; } +uint32_t padDpadApply(uint32_t raw, int16_t, int16_t, int16_t, int16_t) { return raw; } void padStickBlend(uint32_t, int16_t, int16_t, int16_t, int16_t, int16_t *, int16_t *, int16_t *, int16_t *) {} void padClickEdge(uint8_t, uint32_t) {} #define HSIDE_LPAD 0 @@ -100,7 +101,7 @@ def cut(text, a, b=None): slices = [ # shared PlayStation packers: psButtonsFromSteam and the code tables behind it (r / 'OpenPuck/remap.cpp').read_text(), - cut(gu, 'uint32_t psButtonsFromSteam('), + cut(gu, 'uint32_t remapHostButtons('), # Xbox 360 cut(xi, 'enum {\n\tXB_DUP', '// ===================== XInput custom TinyUSB class driver'), cut(xi, 'static void rfXboxGamepad(', '// Right pad -> mouse on a second HID-mouse interface'), @@ -173,7 +174,7 @@ def cut(text, a, b=None): { load(raw); XboxOgInputReport rp; - xboxOgBuildReport(rp, rep); + xboxOgBuildReport(rp, rep, 0); printf("btn=%02x a=%02x b=%02x x=%02x y=%02x blk=%02x wht=%02x lt=%02x rt=%02x", rp.buttons, rp.a, rp.b, rp.x, rp.y, rp.black, rp.white, rp.left_trigger, rp.right_trigger); } diff --git a/tests/button_profiles/panel.cjs b/tests/button_profiles/panel.cjs index 49b05eef..62b0efe5 100644 --- a/tests/button_profiles/panel.cjs +++ b/tests/button_profiles/panel.cjs @@ -144,14 +144,32 @@ for(let et=0;et<4;et++){ } const touch=(et,pad)=>[...typeEls[et].padStick[pad].options].map(o=>o.textContent); // Disabled is offered where the pad's own use is something (touchpad, mouse); an Off pad already does nothing -assert.deepEqual(touch(0,0),['Off','Left stick','Right stick']);assert.deepEqual(touch(0,1),['Mouse','Disabled','Left stick','Right stick']); -assert.deepEqual(touch(2,0),['Touchpad, left half','Disabled','Left stick','Right stick']);assert.deepEqual(touch(3,1),['Touchpad, right half','Disabled','Left stick','Right stick']); +// every type offers the trackpad as a D-pad +const DP=['D-pad on touch','D-pad on click']; +assert.deepEqual(touch(0,0),['Off','Left stick','Right stick',...DP]);assert.deepEqual(touch(0,1),['Mouse','Disabled','Left stick','Right stick',...DP]); +assert.deepEqual(touch(2,0),['Touchpad, left half','Disabled','Left stick','Right stick',...DP]);assert.deepEqual(touch(3,1),['Touchpad, right half','Disabled','Left stick','Right stick',...DP]); assert(!touch(1,0).includes('Disabled')&&!touch(1,1).includes('Disabled')); -assert.deepEqual(touch(1,1),['Off','Left stick','Right stick','D-pad on touch (Switch Pro)','D-pad on click (Switch Pro)']); +assert.deepEqual(touch(1,1),['Off','Left stick','Right stick',...DP]); // a click can only act as what the type's mode does with it: the mouse on Xbox, the touchpad on PlayStation, nothing on Switch const padT=et=>[...rec(et).sources[20].options].filter(o=>+o.value>=21).map(o=>o.value+'='+o.textContent+'|'+o.dataset.glyph); assert.deepEqual(padT(0),['21=Mouse right click|mouseR','22=Mouse left click|mouseL']); assert.deepEqual(padT(1),[]); +// Quick Access + Select: a target per controller type in the Button map card, from the status blob's p[248 + type] +{const qs=et=>rec(et).qamSelect,rowShown=et=>!qs(et).closest('.row').classList.contains('hide'); + assert([0,1,2,3].every(et=>!rowShown(et)),'hidden on a blob without it'); + while(p.length<250)p.push(0);p[246]=0;p[247]=18;p[248]=10;p[249]=0;await apply(); + assert([0,1,2,3].every(rowShown));assert.deepEqual([0,1,2,3].map(et=>qs(et).value),['0','18','10','0']); + assert.equal([...qs(1).options].find(o=>o.value==='18').textContent,'Take screenshot'); + assert([0,1,2,3].every(et=>qs(et).closest('.card').querySelector('h2').textContent.startsWith('Button map')&&![...qs(et).options].some(o=>+o.value>20))); + const n0=writes.length;change(qs(3),9);await new Promise(r=>setTimeout(r,30)); + assert.deepEqual(writes.slice(n0).filter(x=>x[0]===2).pop(),[2,155,9]); + p.length=230;await apply();assert([0,1,2,3].every(et=>!rowShown(et)));} +// a pad on a D-pad mode spends its click on the D-pad, so that pad's Click is greyed out (and back with any other Touch) +for(const et of [0,1,2,3])for(const pad of [0,1]){ + const click=rec(et).sources[20+pad],other=rec(et).sources[21-pad]; + for(const v of [3,4]){await opsOf(()=>change(typeEls[et].padStick[pad],v));assert(click.disabled&&!other.disabled,'type '+et+' pad '+pad+' D-pad '+v);} + await opsOf(()=>change(typeEls[et].padStick[pad],0));assert(!click.disabled&&!other.disabled); +} assert.deepEqual(padT(2),['21=Touchpad click, left half|padClickL','22=Touchpad click, right half|padClickR']);assert.deepEqual(padT(3),padT(2)); assert.deepEqual([rec(0).sources[20].value,rec(0).sources[21].value],['21','22']); // the factory entries on Switch are the pad clicks too, but nothing there uses them: they read as none @@ -161,7 +179,11 @@ assert.deepEqual([rec(1).sources[20].value,rec(1).sources[21].value],['0','0']); assert.deepEqual(await opsOf(()=>change(rec(0).sources[21],1)),[[0x2D,0,sel0,21,1]]);assert.equal(model[0].maps[sel0][21],1); assert.deepEqual(await opsOf(()=>change(rec(0).sources[21],22)),[[0x2D,0,sel0,21,22]]);} assert(padCard(0).contains(document.querySelector('#mouseCard'))); -assert(padCard(1).contains(document.querySelector('#swClickControls'))&&padCard(1).contains(document.querySelector('#hdPadBlock'))); +// the trackpads' haptics are a card of their own, right below the pads +const fbCard=et=>padCard(et).nextElementSibling; +for(const et of [0,1,2,3])assert(fbCard(et).querySelector('h2').textContent==='Trackpad feedback'&&!fbCard(et).contains(padCols(et)[0])&&!padCard(et).contains(rec(et).dpad)); +assert(fbCard(1).contains(document.querySelector('#swClickControls'))&&fbCard(1).contains(document.querySelector('#hdPadBlock'))&&!padCard(1).contains(document.querySelector('#hdPadBlock'))); +assert(fbCard(2).contains(typeEls[2].dpad)&&fbCard(2).contains(typeEls[2].pad)); setTab(LIZARD_TAB);assert(!padCard(0).contains(document.querySelector('#mouseCard')));assert(document.querySelector('#mapLizardPane').contains(document.querySelector('#mouseCard'))); setTab(0);assert(padCard(0).contains(document.querySelector('#mouseCard'))); // disabling a pad is a firmware v30 value: older firmware would ignore it, so the option is not selectable there diff --git a/tests/final_webusb/run.py b/tests/final_webusb/run.py index 6477c335..c1f0c25b 100644 --- a/tests/final_webusb/run.py +++ b/tests/final_webusb/run.py @@ -5,9 +5,13 @@ head='''#include #include #include "triton.h" -uint8_t g_shortcutFlags=61,g_rumblePresets[3]={5,0,8},g_rumbleSlot=2,g_swQamSelect=18,g_strengthSlots[2]={255,255},g_rumbleStyle=8; +uint8_t g_shortcutFlags=61,g_rumblePresets[3]={5,0,8},g_rumbleSlot=2,g_typeQamSelect[4]={0,18,0,0},g_strengthSlots[2]={255,255},g_rumbleStyle=8; uint16_t g_strengthSteps[2][3]={{200,300,500},{200,300,500}},g_hdPadScale=300,g_rumbleScale=200; -int applied=0,stored=0;void applyActiveType(){++applied;}void rumbleStoreActive(){++stored;} +int applied=0,stored=0;void applyActiveType(){++applied;} +#define ET_SWITCH 1 +#define ET_COUNT 4 +#define QAM_SELECT_MAX 20 +bool typeQamSelectSet(uint8_t et,uint8_t v){if(et>=ET_COUNT||v>QAM_SELECT_MAX)return false;g_typeQamSelect[et]=v;applyActiveType();return true;}void rumbleStoreActive(){++stored;} void field(uint8_t f,uint8_t v){switch(f){ ''' body='''}}int main(){ @@ -16,7 +20,7 @@ // removed settings are ignored: rumble style (39 / 104..107), shortcut presets / steps / slots (241..252) stored=0;field(39,5);field(104,5);assert(g_rumbleStyle==8 && stored==0); for(int f=241;f<=252;f++){field(f,1);}assert(g_rumblePresets[0]==5 && g_strengthSteps[0][0]==200 && g_rumbleSlot==2); - field(239,0);assert(g_swQamSelect==0);field(239,18);field(239,21);assert(g_swQamSelect==18); + field(239,0);assert(g_typeQamSelect[ET_SWITCH]==0);field(239,18);field(239,21);assert(g_typeQamSelect[ET_SWITCH]==18); for(int i=232;i<=238;i++){field(i,0);} }''' with tempfile.TemporaryDirectory() as td: diff --git a/tests/switch_pro_shortcuts/configurator.cjs b/tests/switch_pro_shortcuts/configurator.cjs index 0ec36523..5d8e1a61 100644 --- a/tests/switch_pro_shortcuts/configurator.cjs +++ b/tests/switch_pro_shortcuts/configurator.cjs @@ -30,25 +30,23 @@ const apply=async()=>{applyBlob(new Uint8Array(p));applySw(new Uint8Array(s));aw const $=sel=>document.querySelector(sel); const act=async fn=>{const n=writes.length;fn();await settle();return writes.slice(n).filter(x=>x[0]===2).pop();}; // Rumble style is automatic: no style pickers, rumble presets, strength steps, D-pad haptic toggle or save button. -for(const id of ['rumbleStyle','rumbleState1','strengthSteps','scHaptics','shortcutSave','hdTuning','calibrationCard','hdWaveform','swProfilesCard','scProfiles','swProfileChords'])assert(!$('#'+id),id); -assert.equal($('#qamSelect').value,'18'); +for(const id of ['rumbleStyle','rumbleState1','strengthSteps','scHaptics','shortcutSave','hdTuning','calibrationCard','hdWaveform','swProfilesCard','scProfiles','swProfileChords','qamSelectRow','qamSelect'])assert(!$('#'+id),id); // Shortcut toggles live in the Mode shortcuts card; every chord is available. assert(!$('#shortcutToggles').classList.contains('hide'));assert($('#shortcutToggles').closest('.card').querySelector('.chord')); // The modifier shows as a glyph named on hover: the toggle and every chord label follow the setting. const mods=()=>[...document.querySelectorAll('.ic-mod')].map(e=>e.getAttribute('aria-label')+'|'+e.title); assert.equal(mods().length,8);assert(mods().every(m=>m==='Quick Access|Quick Access'),mods().join()); -assert($('#scQam .ic-mod'));assert.equal($('#qamSelectRow label .ic').title,'Quick Access'); +assert($('#scQam .ic-mod')); for(const el of document.querySelectorAll('.chord,.chordD'))assert(!el.parentElement.classList.contains('hide')); assert.deepEqual(await act(()=>$('#scQam').click()),[2,240,56]);s[46]=56;await apply();assert(mods().every(m=>m==='All four back buttons|All four back buttons'),mods().join()); -for(const [id,bit] of [['scFeedback',8],['scCapture',16],['scEnabled',32]])assert.deepEqual(await act(()=>$('#'+id).click()),[2,240,s[46]^bit]); +for(const [id,bit] of [['scFeedback',8],['scEnabled',32]])assert.deepEqual(await act(()=>$('#'+id).click()),[2,240,s[46]^bit]); // Switch Pro-only controls live in the Switch tab of Button mapping; the trackpad ones (click feedback, HD strength) in its Trackpads card. -const swTab=$('#qamSelectRow').parentElement;assert($('#typeCfgs').contains(swTab)); +const swTab=$('#swGyroMapBlock').parentElement;assert($('#typeCfgs').contains(swTab)); for(const id of ['#swGyroMap'])assert(swTab.contains($(id)),id); const padTab=$('#swClickControls').parentElement;assert($('#typeCfgs').contains(padTab)&&padTab!==swTab);assert(padTab.contains($('#hdPadScale'))); assert(!$('#swClickControls').classList.contains('hide'));assert.equal($('#swClickFeedback').value,'1'); const change=el=>act(()=>el.dispatchEvent(new w.Event('change'))); const click=$('#swClickFeedback');click.value='0';assert.deepEqual(await change(click),[2,230,0]); -const capture=$('#qamSelect');capture.value='0';assert.deepEqual(await change(capture),[2,239,0]); // v25: grip strength per type in Button mapping (no style row); the global strength row hides. const secs=[...$('#typeCfgs').children]; // the mapping profile cards (status v30+) are covered by tests/button_profiles/panel.cjs; this test reads the rest @@ -115,11 +113,11 @@ assert(document.querySelectorAll('.modebtn').length>=11);assert($('#lizardList') // The remap lists draw a dropdown over each (hidden) select so every target's glyph shows beside its name. {const gsel=sel=>sel.parentElement,btnOf=sel=>gsel(sel).querySelector('.gsel-btn'),listOf=sel=>gsel(sel).querySelector('.gsel-list'); const sels=[...document.querySelectorAll('.gsel > select')].filter(e=>!e.closest('.prof-card')); - // per profile: four back buttons, QAM, the two trackpad touch pickers and their two click pickers; plus the Switch QAM + View picker - assert.equal(sels.length,4*9+1); + // per profile: four back buttons, QAM, the two trackpad touch pickers and their two click pickers + assert.equal(sels.length,4*9); for(const sel of sels){assert(sel.hidden);assert(listOf(sel).classList.contains('hide'));} // every target on every controller has a glyph (only the none / default entry has none), by the right name - const g=et=>Object.fromEntries([...own(secs[et],'select')[0].options].map(o=>[o.textContent,o.dataset.glyph||''])); + const g=et=>Object.fromEntries([...own(secs[et],'select').find(s=>!s.closest('.card').contains($('#swGyroMap'))).options].map(o=>[o.textContent,o.dataset.glyph||''])); for(let et=0;et<4;et++)for(const [name,gl] of Object.entries(g(et)))if(!/^(— none —|Default)/.test(name))assert(gl,et+' '+name); assert.deepEqual(['A','LB','Back','Start','D-pad Up','Guide','LT'].map(n=>g(0)[n]),['A','LB','view','menu','up','guide','LT']); assert.deepEqual(['Minus','ZL','L','L-Stick','Capture / Screenshot'].map(n=>g(1)[n]),['minus','ZL','swL','L3','capture']); diff --git a/tests/switch_pro_shortcuts/run.py b/tests/switch_pro_shortcuts/run.py index 53a9a13c..918f2414 100644 --- a/tests/switch_pro_shortcuts/run.py +++ b/tests/switch_pro_shortcuts/run.py @@ -33,11 +33,9 @@ ''' head += '#include \n#define PS_DPAD_TOUCH 3\n#define PS_DPAD_CLICK 4\nuint8_t g_padStick[2]={};\n' u=(root/'OpenPuck/gamepad_util.cpp').read_text() -ua=u.index('uint32_t padDpadButtons(');ub=u.index('\nvoid slotSticks(',ua) +ua=u.index('static bool padIsDpad(');ub=u.index('\nvoid slotSticks(',ua) -m=(root/'OpenPuck/mode_switch_pro.cpp').read_text() -ha=m.index('static void swDpadClickFeedback(');hb=m.index('\nvoid SwitchProController::task()',ha) -head += '''uint8_t g_swDpadHaptics=1,g_padHaptics=1,g_swQamSelect=18; +head += '''uint8_t g_dpadClick=1,g_padHaptics=1,g_qamSelect=18,g_typeQamSelect[4]={0,0,0,0};\n#define QAM_SELECT_MAX 20\n#define ET_SWITCH 1\n#define PAD_HAPTICS_OFF 0\nPuckInput g_in[NSLOT];\nuint32_t padDpadBits(uint32_t,int16_t,int16_t,int16_t,int16_t);\nstatic inline uint32_t padDpadButtons(const PuckInput &in){return padDpadBits(in.buttons,in.lpx,in.lpy,in.rpx,in.rpy);}\n bool blocked=false; int pulses=0; uint8_t emittedSide=0, emittedBond=0; @@ -48,7 +46,7 @@ } ''' ta=s.index('\tif (c.hdPadScale2 <= 250)');tb=s.index('\t// resolve the active emulated type',ta) -head += 'struct Tail {uint8_t hdPadScale2,reservedRumbleStyles[2],reservedWaveform,reservedWavePresets[2],reservedWaveThird,reservedWaveSlot,swQamSelect,shortcutFlags,reservedShortcutRumble[12],reservedTypeRumbleStyle[4],typeRumbleScale2[4];};\nvoid loadTail(Tail c){\n'+s[ta:tb]+'}\n' +head += 'struct Tail {uint8_t hdPadScale2,reservedRumbleStyles[2],reservedWaveform,reservedWavePresets[2],reservedWaveThird,reservedWaveSlot,swQamSelect,shortcutFlags,reservedShortcutRumble[12],reservedTypeRumbleStyle[4],typeRumbleScale2[4],typeQamSelect[4];};\nvoid loadTail(Tail c){\n'+s[ta:tb]+'}\n' body='''int main(){ Tail old;memset(&old,255,sizeof old);loadTail(old);assert(g_shortcutFlags==SHORTCUT_ENABLED); // fork default: back-4 modifier // a file without per-type strength gives each type its own default: the Switch starts at 100% @@ -59,6 +57,14 @@ Tail typed=old;typed.typeRumbleScale2[0]=50;typed.typeRumbleScale2[1]=75;typed.typeRumbleScale2[2]=1; g_rumbleScale=200;loadTail(typed); assert(g_typeRumbleScale[0]==100 && g_typeRumbleScale[1]==150 && g_typeRumbleScale[2]==200 && g_typeRumbleScale[3]==200); // invalid -> default + // Quick Access + Select target per type: the Switch keeps its old byte (or the screenshot default), others none + // no target for anyone by default; a file from when the Switch-only shortcut was on keeps its target + assert(g_typeQamSelect[0]==0 && g_typeQamSelect[1]==0 && g_typeQamSelect[2]==0 && g_typeQamSelect[3]==0); + {Tail q=old;q.swQamSelect=7;loadTail(q);assert(g_typeQamSelect[1]==0); // its switch was off + q.shortcutFlags=57;loadTail(q);assert(g_typeQamSelect[1]==7 && g_typeQamSelect[3]==0); // on: the Switch keeps 7 + q.swQamSelect=255;g_typeQamSelect[1]=0;loadTail(q);assert(g_typeQamSelect[1]==18); // on, no target saved: screenshot + q.typeQamSelect[3]=9;q.typeQamSelect[1]=4;q.typeQamSelect[2]=21;loadTail(q);assert(g_typeQamSelect[3]==9 && g_typeQamSelect[1]==4 && g_typeQamSelect[2]==0); + g_typeQamSelect[1]=0;g_typeQamSelect[3]=0;} // bits 1/2 (ex profiles / D-pad haptic shortcuts) are dropped from saved flags Tail savedTail=old;savedTail.hdPadScale2=150;savedTail.shortcutFlags=62; loadTail(savedTail);assert(g_hdPadScale==300 && g_shortcutFlags==56); @@ -67,7 +73,12 @@ g_shortcutFlags=0;assert(shortcutHostButtons(TB_QAM|TB_A)==(TB_QAM|TB_A)); g_shortcutFlags=57;g_usbMode=MODE_SW_PRO; int prior=confirmations;captureFeedbackChord(0,0);captureFeedbackChord(0,TB_QAM|TB_MENU);assert(confirmations==1);confirmations=4;captureFeedbackChord(0,TB_QAM|TB_MENU);assert(confirmations==4);captureFeedbackChord(0,0);captureFeedbackChord(0,TB_QAM|TB_MENU);assert(confirmations==1);(void)prior; - uint32_t buttons=TB_QAM|TB_MENU;assert(switchSelectShortcut(0,buttons) && !(buttons&TB_MENU));buttons=TB_MENU;assert(!switchSelectShortcut(0,buttons) && !buttons);buttons=0;switchSelectShortcut(0,buttons);buttons=TB_MENU;assert(!switchSelectShortcut(0,buttons) && buttons==TB_MENU); + // the chord works in every mode that has a target, and does nothing without one + g_shortcutFlags=0;g_usbMode=0;g_qamSelect=10;captureFeedbackChord(0,0);confirmations=0;captureFeedbackChord(0,TB_QAM|TB_MENU);assert(confirmations==1); + g_qamSelect=0;captureFeedbackChord(0,0);confirmations=0;captureFeedbackChord(0,TB_QAM|TB_MENU);assert(confirmations==0); + {uint32_t held=TB_QAM|TB_MENU;assert(!qamSelectShortcut(0,held) && held==(TB_QAM|TB_MENU));} + g_qamSelect=18;g_usbMode=MODE_SW_PRO;g_shortcutFlags=57; + uint32_t buttons=TB_QAM|TB_MENU;assert(qamSelectShortcut(0,buttons) && !(buttons&TB_MENU));buttons=TB_MENU;assert(!qamSelectShortcut(0,buttons) && !buttons);buttons=0;qamSelectShortcut(0,buttons);buttons=TB_MENU;assert(!qamSelectShortcut(0,buttons) && buttons==TB_MENU); PuckInput in={};g_padStick[0]=3; in.buttons=TB_LPADT;in.lpx=20000;assert(padDpadButtons(in)==TB_DRT); in.lpx=-20000;assert(padDpadButtons(in)==TB_DLF); @@ -83,25 +94,53 @@ g_padStick[0]=0;g_padStick[1]=3;in.buttons=TB_RPADT;in.rpx=-20000;assert(padDpadButtons(in)==TB_DLF); in.rpx=-32768;in.rpy=-32768;assert(padDpadButtons(in)==(TB_DLF|TB_DDN)); g_padStick[0]=3;in.buttons|=TB_LPADT;in.lpx=20000;in.lpy=0;in.rpy=0;assert(!padDpadButtons(in)); - g_padStick[0]=3;g_padStick[1]=4; - swDpadClickFeedback(3,TB_LPADT|TB_LPADC);assert(pulses==1 && emittedSide==HSIDE_LPAD && emittedBond==3); - swDpadClickFeedback(3,TB_LPADT|TB_LPADC);assert(pulses==1); - swDpadClickFeedback(3,0);swDpadClickFeedback(3,TB_RPADT|TB_RPADC);assert(pulses==2 && emittedSide==HSIDE_RPAD); - swDpadClickFeedback(3,0);swDpadClickFeedback(3,TB_LPADT|TB_LPADC|TB_RPADT|TB_RPADC);assert(pulses==3 && emittedSide==HSIDE_PADS); - swDpadClickFeedback(3,0);g_swDpadHaptics=0;swDpadClickFeedback(3,TB_LPADT|TB_LPADC);assert(pulses==3); + // D-pad click feedback (every type now): a pulse per click on a D-pad pad, only with Trackpad haptics off + auto fb=[](uint32_t b){g_in[3].buttons=b;padDpadClickFeedback(3);}; + g_padStick[0]=3;g_padStick[1]=4;g_padHaptics=0; + fb(TB_LPADT|TB_LPADC);assert(pulses==1 && emittedSide==HSIDE_LPAD && emittedBond==3); + fb(TB_LPADT|TB_LPADC);assert(pulses==1); + fb(0);fb(TB_RPADT|TB_RPADC);assert(pulses==2 && emittedSide==HSIDE_RPAD); + fb(0);fb(TB_LPADT|TB_LPADC|TB_RPADT|TB_RPADC);assert(pulses==3 && emittedSide==HSIDE_PADS); + fb(0);g_dpadClick=0;fb(TB_LPADT|TB_LPADC);assert(pulses==3); // a disabled pad (5, past the D-pad modes) is neither a D-pad nor a click pulse - g_swDpadHaptics=1;g_padStick[0]=g_padStick[1]=5;in.buttons=TB_LPADT|TB_LPADC|TB_RPADT|TB_RPADC;in.lpx=20000;in.rpx=20000;assert(!padDpadButtons(in)); - swDpadClickFeedback(3,0);swDpadClickFeedback(3,TB_LPADT|TB_LPADC|TB_RPADT|TB_RPADC);assert(pulses==3); + g_dpadClick=1;g_padStick[0]=g_padStick[1]=5;in.buttons=TB_LPADT|TB_LPADC|TB_RPADT|TB_RPADC;in.lpx=20000;in.rpx=20000;assert(!padDpadButtons(in)); + fb(0);fb(TB_LPADT|TB_LPADC|TB_RPADT|TB_RPADC);assert(pulses==3); g_padStick[0]=3;g_padStick[1]=4; - g_swDpadHaptics=1;swDpadClickFeedback(3,TB_LPADT|TB_LPADC);assert(pulses==3); - swDpadClickFeedback(3,0);blocked=true;swDpadClickFeedback(3,TB_LPADT|TB_LPADC);assert(pulses==3); - blocked=false;swDpadClickFeedback(3,0);swDpadClickFeedback(3,TB_LPADT|TB_LPADC);assert(pulses==4); - swDpadClickFeedback(3,0);swDpadClickFeedback(3,TB_LPADT|TB_LPADC|TB_QAM);assert(pulses==4); - swDpadClickFeedback(3,0);g_padHaptics=0;swDpadClickFeedback(3,TB_LPADT|TB_LPADC);assert(pulses==5); - g_padHaptics=1;swDpadClickFeedback(3,0);g_padStick[0]=1;swDpadClickFeedback(3,TB_LPADT|TB_LPADC);assert(pulses==5); + fb(0);blocked=true;fb(TB_LPADT|TB_LPADC);assert(pulses==3); + blocked=false;fb(0);fb(TB_LPADT|TB_LPADC);assert(pulses==4); + fb(0);fb(TB_LPADT|TB_LPADC|TB_QAM);assert(pulses==4); + // Trackpad haptics on already pulses every click: no second pulse + fb(0);g_padHaptics=1;fb(TB_LPADT|TB_LPADC);assert(pulses==4); + g_padHaptics=0;fb(0);g_padStick[0]=1;fb(TB_LPADT|TB_LPADC);assert(pulses==4); + // padDpadApply: D-pad bits added; D-pad-on-click spends its click, D-pad-on-touch and other pads keep theirs + g_padStick[0]=4;g_padStick[1]=0; + assert(padDpadApply(TB_LPADT|TB_LPADC|TB_RPADC,20000,0,0,0)==(TB_LPADT|TB_DRT|TB_RPADC)); + g_padStick[0]=3; + assert(padDpadApply(TB_LPADT|TB_LPADC,20000,0,0,0)==(TB_LPADT|TB_LPADC|TB_DRT)); + g_padStick[0]=4;assert(padDpadApply(TB_LPADT,20000,0,0,0)==TB_LPADT); + // a pulse each time the direction changes, without lifting the finger; none for staying put + g_padStick[0]=3;g_padStick[1]=0;g_padHaptics=0;g_dpadClick=1;blocked=false; + g_in[3]=PuckInput();fb(0);int base=pulses; + g_in[3].lpx=20000;fb(TB_LPADT);assert(pulses==base+1 && emittedSide==HSIDE_LPAD); // touching down into right + g_in[3].lpx=21000;fb(TB_LPADT);assert(pulses==base+1); // same direction + g_in[3].lpy=-20000;g_in[3].lpx=0;fb(TB_LPADT);assert(pulses==base+2); // right -> down + g_in[3].lpx=20000;fb(TB_LPADT);assert(pulses==base+3); // down -> down-right + g_in[3].lpx=500;g_in[3].lpy=500;fb(TB_LPADT);assert(pulses==base+3); // back to the centre: nothing + g_in[3].lpx=-20000;g_in[3].lpy=0;fb(TB_LPADT);assert(pulses==base+4); // centre -> left + g_dpadClick=0;g_in[3].lpx=20000;fb(TB_LPADT);assert(pulses==base+4); // setting off + g_dpadClick=1;g_padHaptics=1;g_in[3].lpx=-20000;fb(TB_LPADT);assert(pulses==base+5);// still plays with Trackpad haptics on + g_padStick[0]=1;g_in[3].lpx=20000;fb(TB_LPADT);assert(pulses==base+5); // not a D-pad pad + // D-pad on click: the click is the first pulse (Trackpad haptics off), a slide then pulses per direction + g_padStick[0]=0;g_padStick[1]=4;g_padHaptics=0;g_in[3]=PuckInput();fb(0);base=pulses; + g_in[3].rpx=20000;fb(TB_RPADT);assert(pulses==base); // touched, not clicked + fb(TB_RPADT|TB_RPADC);assert(pulses==base+1 && emittedSide==HSIDE_RPAD); // the click alone, once + g_in[3].rpy=20000;fb(TB_RPADT|TB_RPADC);assert(pulses==base+2); // right -> up-right + fb(TB_RPADT|TB_RPADC);assert(pulses==base+2); + g_padHaptics=1;fb(0);g_in[3].rpy=0;fb(TB_RPADT|TB_RPADC);assert(pulses==base+2); // Trackpad haptics on: the click is its own + g_in[3].rpy=20000;fb(TB_RPADT|TB_RPADC);assert(pulses==base+3); }''' with tempfile.TemporaryDirectory() as td: - p=Path(td)/'test.cpp';p.write_text(head+hside+clickgain+s[a:b]+u[ua:ub]+m[ha:hb]+body) + p=Path(td)/'test.cpp';p.write_text(head+hside+clickgain+s[a:b]+u[ua:ub]+body) subprocess.run(['g++','-std=c++11','-Wall','-Wextra','-Werror','-fsanitize=address,undefined','-I'+str(root/'OpenPuck'),str(p),'-o',td+'/test'],check=True) subprocess.run([td+'/test'],check=True,env={'ASAN_OPTIONS':'detect_leaks=0'}) print('Shortcut flags, Switch Pro capture, trackpad D-pad and click feedback tests passed') diff --git a/tests/type_haptics/run.py b/tests/type_haptics/run.py index 1d4b16e5..3f5df168 100644 --- a/tests/type_haptics/run.py +++ b/tests/type_haptics/run.py @@ -11,7 +11,8 @@ helpers = cfg_h[cfg_h.index('// gain in dB of click step'):cfg_h.index('// 1 = the active DualSense profile maps Create')] setter = cfg_c[cfg_c.index('bool typeHapticSet('):] setter = setter[:setter.index('\n}\n') + 3] -load = cfg_c[cfg_c.index('\t// per-type haptic settings; one missing'):cfg_c.index('\tif (c.swDpadHaptics <= 1)')] +load = cfg_c[cfg_c.index('\t// per-type haptic settings; one missing'):cfg_c.index('\tif (c.hdPadScale2 <= 250)')] +load += cfg_c[cfg_c.index('\tfor (uint8_t et = 0; et < ET_COUNT; et++) {\n\t\tif (c.typeQamSelect'):cfg_c.index('\t// bits 1 and 2 were')] field = web[web.index('\t\t\t\tif (f >= 120 && f < 120 + 4 * HAP_COUNT) {'):web.index('\t\t\t\t// fields 190..229 were')] blob = web[web.index('\tfor (uint8_t et = 0; et < ET_COUNT; et++) {\n\t\tp[212 + et]'):web.index('\t// CRITICAL: usb_web.write()')] @@ -28,6 +29,9 @@ #define AUDIO_STYLE_SPLIT 2 #define AUDIO_STYLE_WAVE 3 uint8_t g_hapticLift = 1; +#define QAM_SELECT_MAX 20 +uint8_t g_typeQamSelect[ET_COUNT] = { 0, 0, 0, 0 }; +#define SHORTCUT_CAPTURE 16u uint8_t g_hapticLimitKnee = 70, g_audioHapticStyle = 3, g_padClick = 26, g_padTick = 0, g_padTickGain = 11; uint16_t g_typeRumbleScale[ET_COUNT] = {}; int applied = 0, saved = 0; @@ -36,7 +40,7 @@ void saveCfg() { ++saved; } void saveCfgSoon() { ++saved; } ''' + consts + table + helpers + setter + ''' -struct Cfg { uint8_t typeHaptic[HAP_COUNT][ET_COUNT]; }; +struct Cfg { uint8_t typeHaptic[HAP_COUNT][ET_COUNT]; uint8_t swDpadHaptics, swQamSelect, shortcutFlags, typeQamSelect[ET_COUNT]; }; static void load(const Cfg &c) { ''' + load + '''} @@ -67,6 +71,8 @@ // trigger haptics: off / trackpads / grips / both, DualSense only, trackpads by default assert(g_typeHaptic[HAP_TRIG][ET_DS5] == TRIG_HAPTICS_PADS && typeHapticSet(HAP_TRIG, ET_DS5, TRIG_HAPTICS_BOTH) && !typeHapticSet(HAP_TRIG, ET_DS5, 4) && !typeHapticSet(HAP_TRIG, ET_SWITCH, 1)); typeHapticSet(HAP_TRIG, ET_DS5, TRIG_HAPTICS_PADS); + // D-pad click feedback: on/off for every type, on by default + for (int et = 0; et < ET_COUNT; et++) assert(g_typeHaptic[HAP_DPAD][et] == 1 && typeHapticSet(HAP_DPAD, (uint8_t)et, 0) && !typeHapticSet(HAP_DPAD, (uint8_t)et, 2) && typeHapticSet(HAP_DPAD, (uint8_t)et, 1)); // the setter takes in-range values for the types that have the setting, and refreshes the live copies applied = 0; assert(typeHapticSet(HAP_KNEE, ET_SWITCH, 50) && typeHapticSet(HAP_KNEE, ET_DS5, 100) && g_typeHaptic[HAP_KNEE][ET_SWITCH] == 50 && applied == 2); @@ -91,6 +97,19 @@ assert(g_typeHaptic[HAP_KNEE][ET_SWITCH] == 85 && g_typeHaptic[HAP_KNEE][ET_DS5] == 85); assert(g_typeHaptic[HAP_STYLE][ET_DS5] == AUDIO_STYLE_TONE && g_typeHaptic[HAP_STYLE][ET_SWITCH] == AUDIO_STYLE_WAVE); assert(g_typeHaptic[HAP_CLICK][ET_SWITCH] == 26 && g_typeHaptic[HAP_TICK][ET_SWITCH] == 0); + // D-pad click feedback: the Switch's starts from the old Switch-only byte when it holds a value + Cfg dpadOld = old; dpadOld.swDpadHaptics = 1; g_typeHaptic[HAP_DPAD][ET_SWITCH] = 0; load(dpadOld); + assert(g_typeHaptic[HAP_DPAD][ET_SWITCH] == 1); + dpadOld.swDpadHaptics = 0; load(dpadOld); assert(g_typeHaptic[HAP_DPAD][ET_SWITCH] == 0); + dpadOld.typeHaptic[HAP_DPAD][ET_SWITCH] = 1; load(dpadOld); assert(g_typeHaptic[HAP_DPAD][ET_SWITCH] == 1); + // Quick Access + Select target: none by default; the Switch keeps the old Switch-only byte when its switch was on; a saved + // per-type value wins and an out-of-range one is ignored + Cfg qs = old; qs.swQamSelect = 9; qs.shortcutFlags = 41; g_typeQamSelect[1] = 0; load(qs); assert(g_typeQamSelect[ET_SWITCH] == 0); // its enable switch was off + qs.shortcutFlags = 57; load(qs); assert(g_typeQamSelect[ET_SWITCH] == 9 && g_typeQamSelect[ET_XBOX] == 0); + qs.swQamSelect = 0xFF; g_typeQamSelect[1] = 0; load(qs); assert(g_typeQamSelect[ET_SWITCH] == 18); + qs.swQamSelect = 9; + qs.typeQamSelect[ET_DS5] = 10; qs.typeQamSelect[ET_SWITCH] = 4; load(qs); assert(g_typeQamSelect[ET_DS5] == 10 && g_typeQamSelect[ET_SWITCH] == 4); + qs.typeQamSelect[ET_DS5] = 21; g_typeQamSelect[ET_DS5] = 0; load(qs); assert(g_typeQamSelect[ET_DS5] == 0); // a saved grid wins, value by value; an out-of-range entry falls back Cfg saved_cfg = old; saved_cfg.typeHaptic[HAP_KNEE][ET_SWITCH] = 60; saved_cfg.typeHaptic[HAP_STYLE][ET_SWITCH] = AUDIO_STYLE_RUMBLE; @@ -106,7 +125,11 @@ assert(field(120 + 4 * HAP_TICK + ET_DS4, 2) && g_typeHaptic[HAP_TICK][ET_DS4] == 2); uint8_t xboxKnee = g_typeHaptic[HAP_KNEE][ET_XBOX]; assert(field(120 + 4 * HAP_KNEE + ET_XBOX, 90) && g_typeHaptic[HAP_KNEE][ET_XBOX] == xboxKnee); // that type has no limiter - assert(!field(119, 1) && !field(120 + 4 * HAP_COUNT, 1)); + assert(!field(119, 1) && !field(152 + ET_COUNT, 1)); + // field 152 + type is the Quick Access + Select target + saved = 0; assert(field(152 + ET_DS5, 10) && g_typeQamSelect[ET_DS5] == 10 && saved == 1); + assert(field(152 + ET_XBOX, 99) && g_typeQamSelect[ET_XBOX] == 0 && field(152 + ET_XBOX, 20) && g_typeQamSelect[ET_XBOX] == 20); + g_typeQamSelect[ET_XBOX] = 0; g_typeQamSelect[ET_DS5] = 0; // the click pulse's gain byte: the default is +7 dB, and no valid strength is gain byte 0 // (the legacy haptic stop) @@ -124,11 +147,15 @@ landed = 0xFF; assert(padTickCtrlDue(2, &landed) == PAD_CTRL_TAKE); // the status blob carries the grid from byte 216, setting-major - uint8_t p[244]; memset(p, 0xEE, sizeof p); + uint8_t p[252]; memset(p, 0xEE, sizeof p); for (int k = 0; k < HAP_COUNT; k++) for (int et = 0; et < ET_COUNT; et++) g_typeHaptic[k][et] = (uint8_t)(10 * k + et); blobFill(p); for (int k = 0; k < HAP_COUNT; k++) for (int et = 0; et < ET_COUNT; et++) assert(p[216 + 4 * k + et] == 10 * k + et); - assert(p[243] == 63 && p[211] == 0xEE); // the last byte of the grid; the strength bytes below it are not part of it + assert(p[247] == 73 && p[211] == 0xEE); // the last byte of the grid; the strength bytes below it are not part of it + // the Quick Access + Select targets follow the grid, one byte per type + for (int et = 0; et < ET_COUNT; et++) g_typeQamSelect[et] = (uint8_t)(5 + et); + blobFill(p); + for (int et = 0; et < ET_COUNT; et++) assert(p[248 + et] == 5 + et); return 0; } '''