|
|
@@ -14,6 +14,7 @@
|
|
|
#include <string.h>
|
|
|
#include <math.h>
|
|
|
#include "src/common/hid_reports.h"
|
|
|
+#include "src/common/hd_rumble.h"
|
|
|
using namespace Adafruit_LittleFS_Namespace;
|
|
|
|
|
|
SwitchProController g_switchPro;
|
|
|
@@ -165,194 +166,14 @@ static inline uint8_t jcBondOf(uint8_t usbSlot)
|
|
|
int b = (usbSlot < NSLOT) ? g_usbToBond[usbSlot] : -1;
|
|
|
return (b >= 0) ? (uint8_t)b : usbSlot;
|
|
|
}
|
|
|
-// --- Switch HD-rumble amplitude decoder.
|
|
|
-//
|
|
|
-// When we emulate a Pro Controller, the console streams us its HD-rumble output
|
|
|
-// every frame. A genuine controller drives dual-band linear actuators from it;
|
|
|
-// OpenPuck drives the SC2's single motor, so all we need is a scalar amplitude.
|
|
|
-// This decodes the on-wire rumble format down to that amplitude.
|
|
|
-//
|
|
|
-// Format (from the public Switch controller protocol, cross-checked against
|
|
|
-// SDL's zlib-licensed hidapi_switch rumble encoder and community RE notes):
|
|
|
-// each motor carries 4 bytes = one little-endian 32-bit word whose top two bits
|
|
|
-// pick how many amplitude/frequency updates are packed and at what width:
|
|
|
-// mode 0 : no change -- hold the running amplitude
|
|
|
-// mode 1 : a single 5-bit or 7-bit update, or a 7-bit + two 5-bit burst
|
|
|
-// mode 2 : two 5-bit updates, or a 7-bit + 5-bit followed by a 5-bit update
|
|
|
-// mode 3 : three 5-bit updates
|
|
|
-// A 7-bit field is an ABSOLUTE amplitude on the documented log2 curve. A 5-bit
|
|
|
-// field is a compact command that either substitutes a preset level or nudges
|
|
|
-// the amplitude by a small step -- so the decoder carries per-motor state across
|
|
|
-// frames. Frequency fields exist but do nothing for an ERM, so we step past them
|
|
|
-// and keep only amplitude.
|
|
|
-//
|
|
|
-// The naive "read fixed bytes as amplitude" decode we shipped before ignored the
|
|
|
-// mode bits, so any game using the packed modes (Fire Emblem: Three Houses,
|
|
|
-// Crash Bandicoot, ...) decoded to spurious nonzero amplitude -> random idle/menu
|
|
|
-// buzzing. Handling every mode is what fixes that.
|
|
|
-//
|
|
|
-// Amplitude is carried in log2-linear 1/32 fixed-point over [-8.0, 0.0] -> the
|
|
|
-// integer range [-256, 0], and mapped to a 16-bit motor level via a boot-built
|
|
|
-// exp2 table so no floating point runs in the USB ISR. The curve constants below
|
|
|
-// are protocol facts (any correct decoder lands on the same numbers).
|
|
|
-enum { HDR_AMP_MIN = -256, HDR_AMP_OFF = -256 }; // -8.0 log2 units == silent
|
|
|
-
|
|
|
-// Absolute 7-bit amplitude code -> 1/32 log2 units. The documented curve is
|
|
|
-// piecewise linear with progressively finer steps toward full scale (slopes
|
|
|
-// 1/4, 1/16, 1/32); code 0 is silence.
|
|
|
-static inline int16_t hdrAmp7(uint8_t code)
|
|
|
-{
|
|
|
- if (code == 0)
|
|
|
- return HDR_AMP_MIN;
|
|
|
- if (code < 16)
|
|
|
- return (int16_t)(8 * (int)code - 248); // slope 1/4
|
|
|
- if (code < 32)
|
|
|
- return (int16_t)(2 * (int)code - 158); // slope 1/16
|
|
|
- return (int16_t)((int)code - 127); // slope 1/32
|
|
|
-}
|
|
|
-// Apply a compact 5-bit command to the running amplitude (1/32 log2 units):
|
|
|
-// 0 -> silence
|
|
|
-// 1..11 -> substitute an absolute preset: 0, -0.5, -1.0, ... -5.0
|
|
|
-// 17..22 -> step up (+0.125 for 17-19, +0.03125 for 20-22)
|
|
|
-// 26..31 -> step down (-0.03125 for 26-28, -0.125 for 29-31)
|
|
|
-// other -> amplitude unchanged (the code only carries a frequency command)
|
|
|
-static inline int16_t hdrAmp5(uint8_t code, int16_t cur)
|
|
|
-{
|
|
|
- if (code == 0)
|
|
|
- return HDR_AMP_OFF;
|
|
|
- if (code <= 11)
|
|
|
- return (int16_t)(-16 * (int)(code - 1)); // presets 0..-5.0
|
|
|
- int step = 0;
|
|
|
- if (code >= 17 && code <= 19)
|
|
|
- step = 4;
|
|
|
- else if (code >= 20 && code <= 22)
|
|
|
- step = 1;
|
|
|
- else if (code >= 26 && code <= 28)
|
|
|
- step = -1;
|
|
|
- else if (code >= 29 && code <= 31)
|
|
|
- step = -4;
|
|
|
- int v = (int)cur + step;
|
|
|
- return v < HDR_AMP_MIN ? HDR_AMP_MIN : (v > 0 ? 0 : (int16_t)v);
|
|
|
-}
|
|
|
-// exp2(units/32) scaled to a 16-bit motor level, built once at boot. The two
|
|
|
-// lowest steps are treated as silent (the curve floors out there), matching how
|
|
|
-// the neutral/idle frame -- which decodes to minimum amplitude -- reads as off.
|
|
|
-static uint16_t g_hdrLevel[257];
|
|
|
-static void hdrBuildLevels()
|
|
|
-{
|
|
|
- for (int u = HDR_AMP_MIN; u <= 0; u++) {
|
|
|
- float lin = (float)u / 32.0f;
|
|
|
- float amp = (lin >= -7.9375f) ? exp2f(lin) : 0.0f;
|
|
|
- if (amp > 1.0f)
|
|
|
- amp = 1.0f;
|
|
|
- uint32_t v = (uint32_t)(amp * 65535.0f + 0.5f);
|
|
|
- g_hdrLevel[u - HDR_AMP_MIN] = (v > 0xFFFF) ? 0xFFFF :
|
|
|
- (uint16_t)v;
|
|
|
- }
|
|
|
-}
|
|
|
-// Per-slot, per-motor (0 = left, 1 = right) running band amplitudes. The packed
|
|
|
-// 5-bit commands are relative, so this state must persist between frames.
|
|
|
-struct HdrBands {
|
|
|
- int16_t lo; // low-band amplitude, 1/32 log2 units
|
|
|
- int16_t hi; // high-band amplitude, 1/32 log2 units
|
|
|
-};
|
|
|
-static HdrBands g_hdrState[NSLOT][2];
|
|
|
+// Switch HD-rumble amplitude decoding lives in the shared module
|
|
|
+// (common/hd_rumble.c), byte-for-byte identical with PicoPuck. Per-slot,
|
|
|
+// per-motor band state persists across frames; hdrReset() silences a slot.
|
|
|
+static hdr_band_t g_hdrState[NSLOT][2];
|
|
|
static inline void hdrReset(uint8_t slot)
|
|
|
{
|
|
|
- g_hdrState[slot][0].lo = g_hdrState[slot][0].hi = HDR_AMP_OFF;
|
|
|
- g_hdrState[slot][1].lo = g_hdrState[slot][1].hi = HDR_AMP_OFF;
|
|
|
-}
|
|
|
-// Pull a bit-field out of the 32-bit rumble word.
|
|
|
-static inline uint8_t hdrField(uint32_t w, uint8_t shift, uint8_t width)
|
|
|
-{
|
|
|
- return (uint8_t)((w >> shift) & ((1u << width) - 1u));
|
|
|
-}
|
|
|
-// Decode one motor's 4 rumble bytes, advancing its state, and return the PEAK
|
|
|
-// motor level over the frame's updates (max across both bands and all samples).
|
|
|
-// Peak rather than final-sample keeps short pulses that a multi-update frame
|
|
|
-// packs together, while every idle/neutral frame still resolves to 0.
|
|
|
-static uint16_t hdrDecode(uint8_t slot, uint8_t motor, const uint8_t b[4])
|
|
|
-{
|
|
|
- HdrBands &s = g_hdrState[slot][motor];
|
|
|
- uint32_t w = (uint32_t)b[0] | ((uint32_t)b[1] << 8) |
|
|
|
- ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24);
|
|
|
- uint16_t peak = 0;
|
|
|
-#define HDR_SAMPLE() \
|
|
|
- do { \
|
|
|
- uint16_t la = g_hdrLevel[s.lo - HDR_AMP_MIN]; \
|
|
|
- uint16_t ha = g_hdrLevel[s.hi - HDR_AMP_MIN]; \
|
|
|
- uint16_t lv = la > ha ? la : ha; \
|
|
|
- if (lv > peak) \
|
|
|
- peak = lv; \
|
|
|
- } while (0)
|
|
|
-
|
|
|
- switch (hdrField(w, 30, 2)) {
|
|
|
- case 0: // hold
|
|
|
- HDR_SAMPLE();
|
|
|
- break;
|
|
|
- case 1:
|
|
|
- if ((w & 0xFFFFF) == 0) { // single 5-bit update
|
|
|
- s.lo = hdrAmp5(hdrField(w, 25, 5), s.lo);
|
|
|
- s.hi = hdrAmp5(hdrField(w, 20, 5), s.hi);
|
|
|
- HDR_SAMPLE();
|
|
|
- } else if ((w & 0x3) == 0) { // single 7-bit absolute
|
|
|
- s.lo = hdrAmp7(hdrField(w, 23, 7));
|
|
|
- s.hi = hdrAmp7(hdrField(w, 9, 7));
|
|
|
- HDR_SAMPLE();
|
|
|
- } else { // 7-bit for one band + two 5-bit updates
|
|
|
- bool wantHi = (w & 1) != 0;
|
|
|
- bool isFreq = ((w >> 2) & 1) != 0;
|
|
|
- if (!isFreq) { // else the 7-bit is a frequency: ignore
|
|
|
- if (wantHi)
|
|
|
- s.hi = hdrAmp7(hdrField(w, 23, 7));
|
|
|
- else
|
|
|
- s.lo = hdrAmp7(hdrField(w, 23, 7));
|
|
|
- }
|
|
|
- HDR_SAMPLE();
|
|
|
- s.lo = hdrAmp5(hdrField(w, 18, 5), s.lo);
|
|
|
- s.hi = hdrAmp5(hdrField(w, 13, 5), s.hi);
|
|
|
- HDR_SAMPLE();
|
|
|
- s.lo = hdrAmp5(hdrField(w, 8, 5), s.lo);
|
|
|
- s.hi = hdrAmp5(hdrField(w, 3, 5), s.hi);
|
|
|
- HDR_SAMPLE();
|
|
|
- }
|
|
|
- break;
|
|
|
- case 2:
|
|
|
- if ((w & 0x3FF) == 0) { // two 5-bit updates
|
|
|
- s.lo = hdrAmp5(hdrField(w, 25, 5), s.lo);
|
|
|
- s.hi = hdrAmp5(hdrField(w, 20, 5), s.hi);
|
|
|
- HDR_SAMPLE();
|
|
|
- s.lo = hdrAmp5(hdrField(w, 15, 5), s.lo);
|
|
|
- s.hi = hdrAmp5(hdrField(w, 10, 5), s.hi);
|
|
|
- HDR_SAMPLE();
|
|
|
- } else { // 7-bit + 5-bit, then a 5-bit update
|
|
|
- if (w & 1) {
|
|
|
- s.hi = hdrAmp7(hdrField(w, 23, 7));
|
|
|
- s.lo = hdrAmp5(hdrField(w, 18, 5), s.lo);
|
|
|
- } else {
|
|
|
- s.lo = hdrAmp7(hdrField(w, 23, 7));
|
|
|
- s.hi = hdrAmp5(hdrField(w, 18, 5), s.hi);
|
|
|
- }
|
|
|
- HDR_SAMPLE();
|
|
|
- s.lo = hdrAmp5(hdrField(w, 13, 5), s.lo);
|
|
|
- s.hi = hdrAmp5(hdrField(w, 8, 5), s.hi);
|
|
|
- HDR_SAMPLE();
|
|
|
- }
|
|
|
- break;
|
|
|
- case 3: // three 5-bit updates
|
|
|
- s.lo = hdrAmp5(hdrField(w, 25, 5), s.lo);
|
|
|
- s.hi = hdrAmp5(hdrField(w, 20, 5), s.hi);
|
|
|
- HDR_SAMPLE();
|
|
|
- s.lo = hdrAmp5(hdrField(w, 15, 5), s.lo);
|
|
|
- s.hi = hdrAmp5(hdrField(w, 10, 5), s.hi);
|
|
|
- HDR_SAMPLE();
|
|
|
- s.lo = hdrAmp5(hdrField(w, 5, 5), s.lo);
|
|
|
- s.hi = hdrAmp5(hdrField(w, 0, 5), s.hi);
|
|
|
- HDR_SAMPLE();
|
|
|
- break;
|
|
|
- }
|
|
|
-#undef HDR_SAMPLE
|
|
|
- return peak;
|
|
|
+ hdr_reset(&g_hdrState[slot][0]);
|
|
|
+ hdr_reset(&g_hdrState[slot][1]);
|
|
|
}
|
|
|
// Per-slot: each Pro Controller has its own rumble stream, so the "last" relay tracking must be per-slot.
|
|
|
static uint16_t g_jcLastLo[NSLOT] = { 0 };
|
|
|
@@ -361,7 +182,8 @@ static void jcRumble(uint8_t slot, const uint8_t *p, uint16_t pn)
|
|
|
{
|
|
|
if (pn < 9)
|
|
|
return; // [timer][left rumble x4][right rumble x4]
|
|
|
- uint16_t lo = hdrDecode(slot, 0, p + 1), hi = hdrDecode(slot, 1, p + 5);
|
|
|
+ uint16_t lo = hdr_decode(&g_hdrState[slot][0], p + 1),
|
|
|
+ hi = hdr_decode(&g_hdrState[slot][1], p + 5);
|
|
|
// only relay on change: the Switch streams rumble every frame; re-sending
|
|
|
// unchanged values would flood the RF relay and loop the motor
|
|
|
if (lo == g_jcLastLo[slot] && hi == g_jcLastHi[slot])
|
|
|
@@ -891,7 +713,6 @@ void SwitchProController::usbIdentity()
|
|
|
void SwitchProController::beginPool()
|
|
|
{
|
|
|
jcBuildStickCal();
|
|
|
- hdrBuildLevels();
|
|
|
for (uint8_t s = 0; s < NSLOT; s++)
|
|
|
hdrReset(s);
|
|
|
loadUserCal();
|