Bläddra i källkod

dynamic calibration

Billie Hilton 4 månader sedan
förälder
incheckning
320ae0dbb8
3 ändrade filer med 144 tillägg och 2 borttagningar
  1. 6 0
      index.html
  2. 107 2
      src/gamepad-mapper.js
  3. 31 0
      src/index.js

+ 6 - 0
index.html

@@ -139,6 +139,12 @@
                     <label class="control-label">Buttons</label>
                     <div id="buttons-display">A B X Y</div>
                 </div>
+                <div class="control-group">
+                    <label class="control-label">Calibration (Raw Min/Max)</label>
+                    <div id="calibration-display" style="font-size: 10px; line-height: 1.2;">
+                        Move sticks to calibrate...
+                    </div>
+                </div>
             </div>
         </div>
         

+ 107 - 2
src/gamepad-mapper.js

@@ -25,6 +25,9 @@ export class GamepadMapper {
         this.eventListeners = new Map();
         this.isPolling = false;
 
+        // Dynamic calibration tracking
+        this.calibrationData = new Map(); // gamepadIndex -> { axisName -> { min, max } }
+
         // Standard controller mappings
         this.controllerMappings = this.initializeControllerMappings();
 
@@ -170,6 +173,9 @@ export class GamepadMapper {
             axes: {}
         });
 
+        // Initialize calibration data for this gamepad
+        this.calibrationData.set(gamepad.index, {});
+
         this.emit('connected', {
             index: gamepad.index,
             id: gamepad.id,
@@ -183,6 +189,7 @@ export class GamepadMapper {
 
         this.connectedGamepads.delete(index);
         this.previousStates.delete(index);
+        this.calibrationData.delete(index);
 
         // Clear any pending debounce timers
         if (this.buttonDebounceTimers.has(index)) {
@@ -298,13 +305,18 @@ export class GamepadMapper {
             const axisName = mapping[axisIndex];
             if (!axisName) return;
 
+            // Update calibration data for non-DPAD axes
+            if (axisName !== 'DPAD_AXIS') {
+                this.updateCalibration(gamepad.index, axisName, axisValue);
+            }
+
             // DPAD_AXIS is immune to deadzone filtering and gets special processing
             let processedValue;
             if (axisName === 'DPAD_AXIS') {
                 processedValue = this.processDpadAxis(axisValue);
             } else {
-                // Apply deadzone to all other axes
-                processedValue = this.applyDeadzone(axisValue);
+                // Apply dynamic calibration and deadzone to all other axes
+                processedValue = this.applyDynamicCalibration(gamepad.index, axisName, axisValue);
             }
 
             const previousValue = previousState.axes[axisName] || 0;
@@ -366,6 +378,94 @@ export class GamepadMapper {
         return null;
     }
 
+    updateCalibration(gamepadIndex, axisName, rawValue) {
+        const gamepadCalibration = this.calibrationData.get(gamepadIndex);
+
+        if (!gamepadCalibration[axisName]) {
+            gamepadCalibration[axisName] = {
+                min: rawValue, // Start with actual observed value
+                max: rawValue, // Start with actual observed value
+                neutral: 0.0, // Assume neutral is zero instead of first raw value
+                neutralSamples: [0.0],
+                positiveMax: 0.3, // Start with conservative positive range
+                negativeMin: -0.3, // Start with conservative negative range
+                dynamicDeadzone: 0.01 // Start with small default deadzone
+            };
+        } else {
+            const cal = gamepadCalibration[axisName];
+
+            // Update overall min/max
+            cal.min = Math.min(cal.min, rawValue);
+            cal.max = Math.max(cal.max, rawValue);
+
+            // Track neutral position (values close to initial position)
+            const neutralThreshold = 0.05;
+            if (Math.abs(rawValue - cal.neutral) < neutralThreshold) {
+                cal.neutralSamples.push(rawValue);
+                // Keep only recent samples for rolling average
+                if (cal.neutralSamples.length > 100) {
+                    cal.neutralSamples = cal.neutralSamples.slice(-50);
+                }
+                // Update neutral as average of samples
+                cal.neutral = cal.neutralSamples.reduce((a, b) => a + b, 0) / cal.neutralSamples.length;
+            }
+
+            // Track separate positive and negative extremes
+            if (rawValue > cal.neutral) {
+                cal.positiveMax = Math.max(cal.positiveMax, rawValue);
+            } else if (rawValue < cal.neutral) {
+                cal.negativeMin = Math.min(cal.negativeMin, rawValue);
+            }
+
+            // Calculate dynamic deadzone based on neutral drift
+            const neutralRange = Math.max(...cal.neutralSamples) - Math.min(...cal.neutralSamples);
+            cal.dynamicDeadzone = Math.max(neutralRange * 1.5, 0.01); // 1.5x the neutral drift range
+        }
+    }
+
+    applyDynamicCalibration(gamepadIndex, axisName, rawValue) {
+        const gamepadCalibration = this.calibrationData.get(gamepadIndex);
+        const axisCalibration = gamepadCalibration[axisName];
+
+        // If no calibration data yet, use standard deadzone
+        if (!axisCalibration) {
+            return this.applyDeadzone(rawValue);
+        }
+
+        const { neutral, positiveMax, negativeMin, dynamicDeadzone } = axisCalibration;
+
+        // Use dynamic deadzone if available, otherwise fall back to config deadzone
+        const effectiveDeadzone = Math.max(dynamicDeadzone, this.config.deadzone);
+
+        // Apply deadzone around neutral position
+        const distanceFromNeutral = Math.abs(rawValue - neutral);
+        if (distanceFromNeutral < effectiveDeadzone) {
+            return 0;
+        }
+
+        // Handle positive and negative sides separately
+        let normalizedValue;
+        if (rawValue > neutral) {
+            // Positive side: map from neutral to positiveMax -> 0 to 1
+            const positiveRange = positiveMax - neutral;
+            if (positiveRange < 0.01) return 0; // Range too small
+
+            const adjustedValue = rawValue - neutral - effectiveDeadzone;
+            const adjustedRange = positiveRange - effectiveDeadzone;
+            normalizedValue = Math.min(1, Math.max(0, adjustedValue / adjustedRange));
+        } else {
+            // Negative side: map from negativeMin to neutral -> -1 to 0
+            const negativeRange = neutral - negativeMin;
+            if (negativeRange < 0.01) return 0; // Range too small
+
+            const adjustedValue = neutral - rawValue - effectiveDeadzone;
+            const adjustedRange = negativeRange - effectiveDeadzone;
+            normalizedValue = -Math.min(1, Math.max(0, adjustedValue / adjustedRange));
+        }
+
+        return normalizedValue;
+    }
+
     applyDeadzone(value) {
         if (Math.abs(value) < this.config.deadzone) {
             return 0;
@@ -456,6 +556,11 @@ export class GamepadMapper {
         this.config.debounceTime = Math.max(0, time);
     }
 
+    // Get calibration data for debugging
+    getCalibrationData(gamepadIndex) {
+        return this.calibrationData.get(gamepadIndex) || {};
+    }
+
     // Vibration support (if available)
     vibrate(gamepadIndex, duration = 200, strongMagnitude = 1.0, weakMagnitude = 1.0) {
         if (!this.config.enableVibration) return false;

+ 31 - 0
src/index.js

@@ -37,6 +37,7 @@ class DawdlehornApp {
             rightStickDisplay: document.getElementById('right-stick-display'),
             dpadDisplay: document.getElementById('dpad-display'),
             buttonsDisplay: document.getElementById('buttons-display'),
+            calibrationDisplay: document.getElementById('calibration-display'),
             visualizer: document.getElementById('visualizer')
         };
 
@@ -181,12 +182,42 @@ class DawdlehornApp {
                 });
 
                 this.elements.buttonsDisplay.textContent = buttonDisplay || 'None pressed';
+
+                // Update calibration display
+                this.updateCalibrationDisplay();
             }
             requestAnimationFrame(updateLoop);
         };
         updateLoop();
     }
 
+    updateCalibrationDisplay() {
+        if (!this.gamepadConnected || this.gamepadIndex === null) {
+            this.elements.calibrationDisplay.textContent = 'Move sticks to calibrate...';
+            return;
+        }
+
+        const calibrationData = this.gamepadMapper.getCalibrationData(this.gamepadIndex);
+
+        if (Object.keys(calibrationData).length === 0) {
+            this.elements.calibrationDisplay.textContent = 'Move sticks to calibrate...';
+            return;
+        }
+
+        let calibrationText = '';
+        const axisOrder = ['LEFT_STICK_X', 'LEFT_STICK_Y', 'RIGHT_STICK_X', 'RIGHT_STICK_Y'];
+
+        axisOrder.forEach(axisName => {
+            if (calibrationData[axisName]) {
+                const { neutral, negativeMin, positiveMax, dynamicDeadzone } = calibrationData[axisName];
+                const shortName = axisName.replace('_STICK_', '').replace('LEFT', 'L').replace('RIGHT', 'R');
+                calibrationText += `${shortName}: ${negativeMin.toFixed(3)} ← ${neutral.toFixed(3)} → ${positiveMax.toFixed(3)} (DZ:${dynamicDeadzone.toFixed(3)})\n`;
+            }
+        });
+
+        this.elements.calibrationDisplay.textContent = calibrationText.trim() || 'Move sticks to calibrate...';
+    }
+
     handleButtonPress(button, value) {
         try {
             console.log(`🎮 Button pressed: ${button} (${value})`);