mirror of
https://github.com/dualshock-tools/dualshock-tools.github.io.git
synced 2026-03-01 11:19:54 +03:00
Add adaptive trigger support to the DS5
This commit is contained in:
committed by
dualshock-tools
parent
3b2b3d5c13
commit
b240d1d65c
16
index.html
16
index.html
@@ -150,6 +150,22 @@
|
||||
<button type="button" class="btn btn-primary ds-btn ds-i18n" onclick="calibrate_range()">Calibrate stick range</button>
|
||||
<button type="button" class="btn btn-primary ds-btn" onclick="ds5_finetune()" id="ds5finetune"><span class="ds-i18n">Finetune stick calibration</span> <i id="ds-i18n">(beta)</i></button>
|
||||
<button id="btnmcs" type="button" class="btn btn-outline-secondary ds-btn ds-i18n" onclick="auto_calibrate_stick_centers()">Fast calibrate stick center (OLD)</button>
|
||||
<button type="button" class="btn btn-info ds-btn ds-i18n" onclick="toggle_adaptive_trigger()" id="adaptive-trigger-btn" style="display: none;">
|
||||
<i class="fas fa-hand-pointer" id="adaptive-trigger-icon"></i>
|
||||
<span id="adaptive-trigger-text">Enable Adaptive Trigger</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary ds-btn ds-i18n" onclick="test_haptic_feedback()" id="haptic-feedback-btn" style="display: none;">
|
||||
<i class="fas fa-mobile-alt" id="haptic-feedback-icon"></i>
|
||||
<span id="haptic-feedback-text">Test Haptic Feedback</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary ds-btn ds-i18n" onclick="test_speaker_tone()" id="speaker-tone-btn" style="display: none;">
|
||||
<i class="fas fa-volume-up" id="speaker-tone-icon"></i>
|
||||
<span id="speaker-tone-text">Test Speaker Tone</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary ds-btn ds-i18n" onclick="test_microphone()" id="microphone-test-btn" style="display: none;">
|
||||
<i class="fas fa-microphone" id="microphone-test-icon"></i>
|
||||
<span id="microphone-test-text">Test Microphone</span>
|
||||
</button>
|
||||
<hr>
|
||||
<button id="savechanges" type="button" class="btn btn-success ds-btn ds-i18n" onclick="flash_all_changes()" id="resetBtn">Save changes permanently</button>
|
||||
<button type="button" class="btn btn-danger ds-btn ds-i18n" onclick="reboot_controller()" id="resetBtn">Reboot controller</button>
|
||||
|
||||
@@ -293,6 +293,124 @@ class ControllerManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable left adaptive trigger effects (DS5 only)
|
||||
* @returns {Promise<Object>} Result object with success status and message
|
||||
*/
|
||||
async disableLeftAdaptiveTrigger() {
|
||||
if (!this.currentController) {
|
||||
throw new Error(this.l("No controller connected"));
|
||||
}
|
||||
|
||||
// Check if the controller supports adaptive triggers (DS5 only)
|
||||
if (this.getModel() !== "DS5") {
|
||||
throw new Error(this.l("Adaptive triggers are only supported on DualSense controllers"));
|
||||
}
|
||||
|
||||
// Check if the controller has the disableLeftAdaptiveTrigger method
|
||||
if (typeof this.currentController.disableLeftAdaptiveTrigger !== 'function') {
|
||||
throw new Error(this.l("Controller does not support adaptive trigger control"));
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.currentController.disableLeftAdaptiveTrigger();
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new Error(this.l("Failed to disable adaptive trigger"), { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set left adaptive trigger with preset configurations (DS5 only)
|
||||
* @param {string} preset - Preset name: 'light', 'medium', 'heavy', 'custom'
|
||||
* @param {Object} customParams - Custom parameters for 'custom' preset {start, end, force}
|
||||
* @returns {Promise<Object>} Result object with success status and message
|
||||
*/
|
||||
async setAdaptiveTriggerPreset({left, right}/* , customParams = {} */) {
|
||||
const presets = {
|
||||
'off': { start: 0, end: 0, force: 0, mode: 'off' },
|
||||
'light': { start: 10, end: 80, force: 150, mode: 'single'},
|
||||
'medium': { start: 15, end: 100, force: 200, mode: 'single' },
|
||||
'heavy': { start: 20, end: 120, force: 255, mode: 'single' },
|
||||
// 'custom': customParams
|
||||
};
|
||||
|
||||
if (!presets[left] || !presets[right]) {
|
||||
throw new Error(`Invalid preset. Available presets: light, medium, heavy, custom. Got "${left}" and "${right}".`);
|
||||
}
|
||||
|
||||
const leftPreset = presets[left];
|
||||
const rightPreset = presets[right];
|
||||
|
||||
// if (preset === 'custom') {
|
||||
// // Validate custom parameters
|
||||
// if (typeof start !== 'number' || typeof end !== 'number' || typeof force !== 'number') {
|
||||
// throw new Error(this.l("Custom preset requires start, end, and force parameters"));
|
||||
// }
|
||||
// }
|
||||
|
||||
return await this.currentController.setAdaptiveTrigger(leftPreset, rightPreset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set vibration motors for haptic feedback (DS5 only)
|
||||
* @param {number} leftMotor - Left motor intensity (0-255)
|
||||
* @param {number} rightMotor - Right motor intensity (0-255)
|
||||
* @param {number} duration - Duration in milliseconds (optional)
|
||||
* @param {Function} doneCb - Callback function called when vibration ends (optional)
|
||||
*/
|
||||
async setVibration(leftMotor, rightMotor, duration = 0, doneCb = ({success}) => {}) {
|
||||
try {
|
||||
await this.currentController.setVibration(leftMotor, rightMotor);
|
||||
|
||||
// If duration is specified, automatically turn off vibration after the duration
|
||||
if (duration > 0) {
|
||||
setTimeout(async () => {
|
||||
if(!this.currentController) return doneCb({success: true});
|
||||
await this.currentController.setVibration(0, 0); // Turn off vibration
|
||||
doneCb({success: true});
|
||||
}, duration);
|
||||
}
|
||||
} catch (error) {
|
||||
if(duration) doneCb({ success: false});
|
||||
throw new Error(this.l("Failed to set vibration"), { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test speaker tone (DS5 only)
|
||||
* @param {number} duration - Duration in milliseconds (optional)
|
||||
* @param {Function} doneCb - Callback function called when tone ends (optional)
|
||||
*/
|
||||
async setSpeakerTone(duration = 1000, doneCb = ({success}) => {}) {
|
||||
try {
|
||||
if (!this.currentController.setSpeakerTone) {
|
||||
throw new Error(this.l("Speaker tone not supported on this controller"));
|
||||
}
|
||||
|
||||
await this.currentController.setSpeakerTone();
|
||||
|
||||
// If duration is specified, automatically reset speaker after the duration
|
||||
if (duration > 0) {
|
||||
setTimeout(async () => {
|
||||
if(!this.currentController) return doneCb({success: true});
|
||||
// Reset speaker settings to default by calling setSpeakerTone with reset parameters
|
||||
try {
|
||||
if (this.currentController.resetSpeakerSettings) {
|
||||
await this.currentController.resetSpeakerSettings();
|
||||
}
|
||||
} catch (resetError) {
|
||||
console.warn("Failed to reset speaker settings:", resetError);
|
||||
}
|
||||
doneCb({success: true});
|
||||
}, duration);
|
||||
}
|
||||
} catch (error) {
|
||||
if(duration) doneCb({ success: false});
|
||||
throw new Error(this.l("Failed to set speaker tone"), { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check if stick positions have changed
|
||||
*/
|
||||
|
||||
@@ -143,6 +143,10 @@ class BaseController {
|
||||
parseBatteryStatus(data) {
|
||||
throw new Error('parseBatteryStatus() must be implemented by subclass');
|
||||
}
|
||||
|
||||
setAdaptiveTrigger(left, right) {
|
||||
throw new Error('setAdaptiveTriggerSingleMode() must be implemented by subclass');
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseController;
|
||||
|
||||
@@ -45,6 +45,154 @@ const DS5_INPUT_CONFIG = {
|
||||
touchpadOffset: 32,
|
||||
};
|
||||
|
||||
// DS5 Adaptive Trigger Effect Modes
|
||||
const DS5_TRIGGER_EFFECT_MODE = {
|
||||
OFF: 0x00, // No effect
|
||||
RESISTANCE: 0x01, // Constant resistance
|
||||
TRIGGER: 0x02, // Single-trigger effect with release
|
||||
AUTO_TRIGGER: 0x06, // Automatic trigger with vibration
|
||||
};
|
||||
|
||||
// DS5 Output Report Constants
|
||||
const DS5_OUTPUT_REPORT = {
|
||||
USB_REPORT_ID: 0x02,
|
||||
BT_REPORT_ID: 0x31,
|
||||
}
|
||||
|
||||
const DS5_VALID_FLAG0 = {
|
||||
RIGHT_VIBRATION: 0x01, // Bit 0 for right vibration motor
|
||||
LEFT_VIBRATION: 0x02, // Bit 1 for left vibration motor
|
||||
LEFT_TRIGGER: 0x04, // Bit 2 for left adaptive trigger
|
||||
RIGHT_TRIGGER: 0x08, // Bit 3 for right adaptive trigger
|
||||
HEADPHONE_VOLUME: 0x10, // Bit 4 for headphone volume control
|
||||
SPEAKER_VOLUME: 0x20, // Bit 5 for speaker volume control
|
||||
MIC_VOLUME: 0x40, // Bit 6 for microphone volume control
|
||||
AUDIO_CONTROL: 0x80, // Bit 7 for audio control
|
||||
};
|
||||
|
||||
const DS5_VALID_FLAG1 = {
|
||||
MUTE_LED: 0x01, // Bit 0 for mute LED control
|
||||
POWER_SAVE_MUTE: 0x02, // Bit 1 for power-save mute control
|
||||
LIGHTBAR_COLOR: 0x04, // Bit 2 for lightbar color control
|
||||
RESERVED_BIT_3: 0x08, // Bit 3 (reserved)
|
||||
PLAYER_INDICATOR: 0x10, // Bit 4 for player indicator LED control
|
||||
LED_BRIGHTNESS: 0x20, // Bit 6 for LED brightness control
|
||||
LIGHTBAR_SETUP: 0x40, // Bit 6 for lightbar setup control
|
||||
RESERVED_BIT_7: 0x80, // Bit 7 (reserved)
|
||||
}
|
||||
|
||||
const DS5_VALID_FLAG2 = {
|
||||
LED_BRIGHTNESS: 0x01, // Bit 0 for LED brightness control
|
||||
LIGHTBAR_SETUP: 0x02, // Bit 1 for lightbar setup control
|
||||
};
|
||||
|
||||
// Basic DS5 Output Structure for adaptive trigger control
|
||||
class DS5OutputStruct {
|
||||
constructor(currentState = null) {
|
||||
// Create a 47-byte buffer for DS5 output report (USB)
|
||||
this.buffer = new ArrayBuffer(47);
|
||||
this.view = new DataView(this.buffer);
|
||||
|
||||
// Control flags
|
||||
this.validFlag0 = currentState.validFlag0 || 0;
|
||||
this.validFlag1 = currentState.validFlag1 || 0;
|
||||
this.validFlag2 = currentState.validFlag2 || 0;
|
||||
|
||||
// Vibration motors
|
||||
this.bcVibrationRight = currentState.bcVibrationRight || 0;
|
||||
this.bcVibrationLeft = currentState.bcVibrationLeft || 0;
|
||||
|
||||
// Audio control
|
||||
this.headphoneVolume = currentState.headphoneVolume || 0;
|
||||
this.speakerVolume = currentState.speakerVolume || 0;
|
||||
this.micVolume = currentState.micVolume || 0;
|
||||
this.audioControl = currentState.audioControl || 0;
|
||||
this.audioControl2 = currentState.audioControl2 || 0;
|
||||
|
||||
// LED and indicator control
|
||||
this.muteLedControl = currentState.muteLedControl || 0;
|
||||
this.powerSaveMuteControl = currentState.powerSaveMuteControl || 0;
|
||||
this.lightbarSetup = currentState.lightbarSetup || 0;
|
||||
this.ledBrightness = currentState.ledBrightness || 0;
|
||||
this.playerIndicator = currentState.playerIndicator || 0;
|
||||
this.ledCRed = currentState.ledCRed || 0;
|
||||
this.ledCGreen = currentState.ledCGreen || 0;
|
||||
this.ledCBlue = currentState.ledCBlue || 0;
|
||||
|
||||
// Adaptive trigger parameters
|
||||
this.adaptiveTriggerLeftMode = currentState.adaptiveTriggerLeftMode || 0;
|
||||
this.adaptiveTriggerLeftParam0 = currentState.adaptiveTriggerLeftParam0 || 0;
|
||||
this.adaptiveTriggerLeftParam1 = currentState.adaptiveTriggerLeftParam1 || 0;
|
||||
this.adaptiveTriggerLeftParam2 = currentState.adaptiveTriggerLeftParam2 || 0;
|
||||
|
||||
this.adaptiveTriggerRightMode = currentState.adaptiveTriggerRightMode || 0;
|
||||
this.adaptiveTriggerRightParam0 = currentState.adaptiveTriggerRightParam0 || 0;
|
||||
this.adaptiveTriggerRightParam1 = currentState.adaptiveTriggerRightParam1 || 0;
|
||||
this.adaptiveTriggerRightParam2 = currentState.adaptiveTriggerRightParam2 || 0;
|
||||
|
||||
// Haptic feedback
|
||||
this.hapticVolume = currentState.hapticVolume || 0;
|
||||
}
|
||||
|
||||
// Pack the data into the output buffer
|
||||
pack() {
|
||||
// Based on DS5 output report structure from HID descriptor
|
||||
// Byte 0-1: Control flags (16-bit little endian)
|
||||
this.view.setUint16(0, (this.validFlag1 << 8) | this.validFlag0, true);
|
||||
|
||||
// Byte 2-3: Vibration motors
|
||||
this.view.setUint8(2, this.bcVibrationRight);
|
||||
this.view.setUint8(3, this.bcVibrationLeft);
|
||||
|
||||
// Bytes 4-7: Audio control (reserved for now)
|
||||
this.view.setUint8(4, this.headphoneVolume);
|
||||
this.view.setUint8(5, this.speakerVolume);
|
||||
this.view.setUint8(6, this.micVolume);
|
||||
this.view.setUint8(7, this.audioControl);
|
||||
|
||||
// Byte 8: Mute LED control
|
||||
this.view.setUint8(8, this.muteLedControl);
|
||||
|
||||
// Byte 9: Reserved
|
||||
this.view.setUint8(9, 0);
|
||||
|
||||
// Bytes 10-20: Right adaptive trigger
|
||||
this.view.setUint8(10, this.adaptiveTriggerRightMode);
|
||||
this.view.setUint8(11, this.adaptiveTriggerRightParam0);
|
||||
this.view.setUint8(12, this.adaptiveTriggerRightParam1);
|
||||
this.view.setUint8(13, this.adaptiveTriggerRightParam2);
|
||||
// Additional trigger parameters (bytes 14-20 reserved for extended params)
|
||||
for (let i = 14; i <= 20; i++) {
|
||||
this.view.setUint8(i, 0);
|
||||
}
|
||||
|
||||
// Bytes 21-31: Left adaptive trigger
|
||||
this.view.setUint8(21, this.adaptiveTriggerLeftMode);
|
||||
this.view.setUint8(22, this.adaptiveTriggerLeftParam0);
|
||||
this.view.setUint8(23, this.adaptiveTriggerLeftParam1);
|
||||
this.view.setUint8(24, this.adaptiveTriggerLeftParam2);
|
||||
// Additional trigger parameters (bytes 25-31 reserved for extended params)
|
||||
for (let i = 25; i <= 31; i++) {
|
||||
this.view.setUint8(i, 0);
|
||||
}
|
||||
|
||||
// Bytes 32-42: Reserved
|
||||
for (let i = 32; i <= 42; i++) {
|
||||
this.view.setUint8(i, 0);
|
||||
}
|
||||
|
||||
// Byte 43: Player LED indicator
|
||||
this.view.setUint8(43, this.playerIndicator);
|
||||
|
||||
// Bytes 44-46: Lightbar RGB
|
||||
this.view.setUint8(44, this.ledCRed);
|
||||
this.view.setUint8(45, this.ledCGreen);
|
||||
this.view.setUint8(46, this.ledCBlue);
|
||||
|
||||
return this.buffer;
|
||||
}
|
||||
}
|
||||
|
||||
function ds5_color(x) {
|
||||
const colorMap = {
|
||||
'00': 'White',
|
||||
@@ -81,6 +229,37 @@ class DS5Controller extends BaseController {
|
||||
super(device, uiDependencies);
|
||||
this.model = "DS5";
|
||||
this.finetuneMaxValue = 65535; // 16-bit max value for DS5
|
||||
|
||||
// Initialize current output state to track controller settings
|
||||
this.currentOutputState = {
|
||||
validFlag0: 0,
|
||||
validFlag1: 0,
|
||||
validFlag2: 0,
|
||||
bcVibrationRight: 0,
|
||||
bcVibrationLeft: 0,
|
||||
headphoneVolume: 0,
|
||||
speakerVolume: 0,
|
||||
micVolume: 0,
|
||||
audioControl: 0,
|
||||
audioControl2: 0,
|
||||
muteLedControl: 0,
|
||||
powerSaveMuteControl: 0,
|
||||
lightbarSetup: 0,
|
||||
ledBrightness: 0,
|
||||
playerIndicator: 0,
|
||||
ledCRed: 0,
|
||||
ledCGreen: 0,
|
||||
ledCBlue: 0,
|
||||
adaptiveTriggerLeftMode: 0,
|
||||
adaptiveTriggerLeftParam0: 0,
|
||||
adaptiveTriggerLeftParam1: 0,
|
||||
adaptiveTriggerLeftParam2: 0,
|
||||
adaptiveTriggerRightMode: 0,
|
||||
adaptiveTriggerRightParam0: 0,
|
||||
adaptiveTriggerRightParam1: 0,
|
||||
adaptiveTriggerRightParam2: 0,
|
||||
hapticVolume: 0
|
||||
};
|
||||
}
|
||||
|
||||
getInputConfig() {
|
||||
@@ -355,19 +534,12 @@ class DS5Controller extends BaseController {
|
||||
|
||||
hwToBoardModel(hw_ver) {
|
||||
const a = (hw_ver >> 8) & 0xff;
|
||||
if(a == 0x03) {
|
||||
return "BDM-010";
|
||||
} else if(a == 0x04) {
|
||||
return "BDM-020";
|
||||
} else if(a == 0x05) {
|
||||
return "BDM-030";
|
||||
} else if(a == 0x06) {
|
||||
return "BDM-040";
|
||||
} else if(a == 0x07 || a == 0x08) {
|
||||
return "BDM-050";
|
||||
} else {
|
||||
return this.l("Unknown");
|
||||
}
|
||||
if(a == 0x03) return "BDM-010";
|
||||
if(a == 0x04) return "BDM-020";
|
||||
if(a == 0x05) return "BDM-030";
|
||||
if(a == 0x06) return "BDM-040";
|
||||
if(a == 0x07 || a == 0x08) return "BDM-050";
|
||||
return this.l("Unknown");
|
||||
}
|
||||
|
||||
async getInMemoryModuleData() {
|
||||
@@ -389,6 +561,182 @@ class DS5Controller extends BaseController {
|
||||
await this.sendFeatureReport(0x80, pkg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send output report to the DS5 controller
|
||||
* @param {ArrayBuffer} data - The output report data
|
||||
*/
|
||||
async sendOutputReport(data, reason = "") {
|
||||
try {
|
||||
console.log(`Sending output report${ reason ? ` to ${reason}` : '' }:`, DS5_OUTPUT_REPORT.USB_REPORT_ID, buf2hex(data));
|
||||
await this.device.sendReport(DS5_OUTPUT_REPORT.USB_REPORT_ID, new Uint8Array(data));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to send output report: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current output state with values from an OutputStruct
|
||||
* @param {DS5OutputStruct} outputStruct - The output structure to copy state from
|
||||
*/
|
||||
updateCurrentOutputState(outputStruct) {
|
||||
this.currentOutputState = { ...outputStruct };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a copy of the current output state
|
||||
* @returns {Object} A copy of the current output state
|
||||
*/
|
||||
getCurrentOutputState() {
|
||||
return { ...this.currentOutputState };
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the current output state when the controller is first connected.
|
||||
* Since DS5 controllers don't provide a way to read the current output state,
|
||||
* this method sets up reasonable defaults and attempts to detect any current settings.
|
||||
*/
|
||||
async initializeCurrentOutputState() {
|
||||
try {
|
||||
// Reset all output state to known defaults
|
||||
this.currentOutputState = {
|
||||
...this.getCurrentOutputState(),
|
||||
validFlag1: 0b1111_0111,
|
||||
ledCRed: 0,
|
||||
ledCGreen: 0,
|
||||
ledCBlue: 255,
|
||||
};
|
||||
|
||||
// Send a "reset" output report to ensure the controller is in a known state
|
||||
// This will turn off any existing effects and set the controller to defaults
|
||||
const resetOutputStruct = new DS5OutputStruct(this.currentOutputState);
|
||||
await this.sendOutputReport(resetOutputStruct.pack(), 'init default states');
|
||||
|
||||
// Update our state to reflect what we just sent
|
||||
this.updateCurrentOutputState(resetOutputStruct);
|
||||
} catch (error) {
|
||||
console.warn("Failed to initialize DS5 output state:", error);
|
||||
// Even if the reset fails, we still have the default state initialized
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set left adaptive trigger to single-trigger mode
|
||||
*/
|
||||
async setAdaptiveTrigger(left, right) {
|
||||
try {
|
||||
const modeMap = {
|
||||
'off': DS5_TRIGGER_EFFECT_MODE.OFF,
|
||||
'single': DS5_TRIGGER_EFFECT_MODE.TRIGGER,
|
||||
'auto': DS5_TRIGGER_EFFECT_MODE.AUTO_TRIGGER,
|
||||
'resistance': DS5_TRIGGER_EFFECT_MODE.RESISTANCE,
|
||||
}
|
||||
|
||||
// Create output structure with current controller state
|
||||
const { validFlag0 } = this.currentOutputState;
|
||||
const outputStruct = new DS5OutputStruct({
|
||||
...this.currentOutputState,
|
||||
adaptiveTriggerLeftMode: modeMap[left.mode],
|
||||
adaptiveTriggerLeftParam0: left.start,
|
||||
adaptiveTriggerLeftParam1: left.end,
|
||||
adaptiveTriggerLeftParam2: left.force,
|
||||
|
||||
adaptiveTriggerRightMode: modeMap[right.mode],
|
||||
adaptiveTriggerRightParam0: right.start,
|
||||
adaptiveTriggerRightParam1: right.end,
|
||||
adaptiveTriggerRightParam2: right.force,
|
||||
|
||||
validFlag0: validFlag0 | DS5_VALID_FLAG0.LEFT_TRIGGER | DS5_VALID_FLAG0.RIGHT_TRIGGER,
|
||||
});
|
||||
await this.sendOutputReport(outputStruct.pack(), 'set adaptive trigger mode');
|
||||
outputStruct.validFlag0 &= ~(DS5_VALID_FLAG0.LEFT_TRIGGER | DS5_VALID_FLAG0.RIGHT_TRIGGER);
|
||||
|
||||
// Update current state to reflect the changes
|
||||
this.updateCurrentOutputState(outputStruct);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
throw new Error("Failed to set left adaptive trigger mode", { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set vibration motors for haptic feedback
|
||||
* @param {number} leftMotor - Left motor intensity (0-255)
|
||||
* @param {number} rightMotor - Right motor intensity (0-255)
|
||||
*/
|
||||
async setVibration(leftMotor = 0, rightMotor = 0) {
|
||||
try {
|
||||
const { validFlag0 } = this.currentOutputState;
|
||||
const outputStruct = new DS5OutputStruct({
|
||||
...this.currentOutputState,
|
||||
bcVibrationLeft: Math.max(0, Math.min(255, leftMotor)),
|
||||
bcVibrationRight: Math.max(0, Math.min(255, rightMotor)),
|
||||
validFlag0: validFlag0 | DS5_VALID_FLAG0.LEFT_VIBRATION | DS5_VALID_FLAG0.RIGHT_VIBRATION, // Update both vibration motors
|
||||
});
|
||||
await this.sendOutputReport(outputStruct.pack(), 'set vibration');
|
||||
outputStruct.validFlag0 &= ~(DS5_VALID_FLAG0.LEFT_VIBRATION | DS5_VALID_FLAG0.RIGHT_VIBRATION);
|
||||
|
||||
// Update current state to reflect the changes
|
||||
this.updateCurrentOutputState(outputStruct);
|
||||
} catch (error) {
|
||||
throw new Error("Failed to set vibration", { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test speaker tone by controlling speaker volume and audio settings
|
||||
* This creates a brief audio feedback through the controller's speaker
|
||||
*/
|
||||
async setSpeakerTone() {
|
||||
try {
|
||||
const { validFlag0 } = this.currentOutputState;
|
||||
const outputStruct = new DS5OutputStruct({
|
||||
...this.currentOutputState,
|
||||
speakerVolume: 85,
|
||||
validFlag0: validFlag0 | DS5_VALID_FLAG0.SPEAKER_VOLUME | DS5_VALID_FLAG0.AUDIO_CONTROL,
|
||||
});
|
||||
await this.sendOutputReport(outputStruct.pack(), 'play speaker tone');
|
||||
outputStruct.validFlag0 &= ~(DS5_VALID_FLAG0.SPEAKER_VOLUME | DS5_VALID_FLAG0.AUDIO_CONTROL);
|
||||
|
||||
// Send feature reports to enable speaker audio
|
||||
// Audio configuration command
|
||||
await this.sendFeatureReport(128, [6, 4, 0, 0, 8]);
|
||||
|
||||
// Enable speaker tone
|
||||
await this.sendFeatureReport(128, [6, 2, 1, 1, 0]);
|
||||
|
||||
// Update current state to reflect the changes
|
||||
this.updateCurrentOutputState(outputStruct);
|
||||
} catch (error) {
|
||||
throw new Error("Failed to set speaker tone", { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset speaker settings to default (turn off speaker)
|
||||
*/
|
||||
async resetSpeakerSettings() {
|
||||
try {
|
||||
// Disable speaker tone first via feature report
|
||||
await this.sendFeatureReport(128, [6, 2, 0, 1, 0]);
|
||||
|
||||
const { validFlag0 } = this.currentOutputState;
|
||||
const outputStruct = new DS5OutputStruct({
|
||||
...this.currentOutputState,
|
||||
speakerVolume: 0,
|
||||
validFlag0: validFlag0 | DS5_VALID_FLAG0.SPEAKER_VOLUME | DS5_VALID_FLAG0.AUDIO_CONTROL,
|
||||
});
|
||||
// outputStruct.audioControl = 0x00;
|
||||
await this.sendOutputReport(outputStruct.pack(), 'stop speaker tone');
|
||||
outputStruct.validFlag0 &= ~(DS5_VALID_FLAG0.SPEAKER_VOLUME | DS5_VALID_FLAG0.AUDIO_CONTROL);
|
||||
|
||||
// Update current state to reflect the changes
|
||||
this.updateCurrentOutputState(outputStruct);
|
||||
} catch (error) {
|
||||
throw new Error("Failed to reset speaker settings", { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse DS5 battery status from input data
|
||||
*/
|
||||
|
||||
381
js/core.js
381
js/core.js
@@ -214,6 +214,11 @@ async function continue_connection({data, device}) {
|
||||
controller.setControllerInstance(controllerInstance);
|
||||
|
||||
info = await controllerInstance.getInfo();
|
||||
|
||||
// Initialize output state for DS5 controllers
|
||||
if (controllerInstance.initializeCurrentOutputState) {
|
||||
await controllerInstance.initializeCurrentOutputState();
|
||||
}
|
||||
} catch (error) {
|
||||
const contextMessage = device
|
||||
? l("Connected invalid device: ") + dec2hex(device.vendorId) + ":" + dec2hex(device.productId)
|
||||
@@ -243,6 +248,11 @@ async function continue_connection({data, device}) {
|
||||
$("#mainmenu").show();
|
||||
$("#resetBtn").show();
|
||||
|
||||
updateAdaptiveTriggerButtonVisibility();
|
||||
updateHapticFeedbackButtonVisibility();
|
||||
updateSpeakerToneButtonVisibility();
|
||||
updateMicrophoneTestButtonVisibility();
|
||||
|
||||
$("#d-nvstatus").text = l("Unknown");
|
||||
$("#d-bdaddr").text = l("Unknown");
|
||||
|
||||
@@ -309,6 +319,11 @@ async function disconnect() {
|
||||
$("#offlinebar").show();
|
||||
$("#onlinebar").hide();
|
||||
$("#mainmenu").hide();
|
||||
|
||||
updateAdaptiveTriggerButtonVisibility();
|
||||
updateHapticFeedbackButtonVisibility();
|
||||
updateSpeakerToneButtonVisibility();
|
||||
updateMicrophoneTestButtonVisibility();
|
||||
}
|
||||
|
||||
// Wrapper function for HTML onclick handlers
|
||||
@@ -381,7 +396,7 @@ async function init_svg_controller() {
|
||||
const svgContainer = document.getElementById('controller-svg-placeholder');
|
||||
|
||||
let svgContent;
|
||||
|
||||
|
||||
// Check if we have bundled assets (production mode)
|
||||
if (window.BUNDLED_ASSETS && window.BUNDLED_ASSETS.svg && window.BUNDLED_ASSETS.svg['dualshock-controller.svg']) {
|
||||
svgContent = window.BUNDLED_ASSETS.svg['dualshock-controller.svg'];
|
||||
@@ -393,7 +408,7 @@ async function init_svg_controller() {
|
||||
}
|
||||
svgContent = await response.text();
|
||||
}
|
||||
|
||||
|
||||
svgContainer.innerHTML = svgContent;
|
||||
|
||||
const lightBlue = '#7ecbff';
|
||||
@@ -932,66 +947,348 @@ let alertCounter = 0;
|
||||
* @returns {string} - The ID of the created alert element
|
||||
*/
|
||||
function pushAlert(message, type = 'info', duration = 0, dismissible = true) {
|
||||
const alertContainer = document.getElementById('alert-container');
|
||||
if (!alertContainer) {
|
||||
console.error('Alert container not found');
|
||||
return null;
|
||||
}
|
||||
const alertContainer = document.getElementById('alert-container');
|
||||
if (!alertContainer) {
|
||||
console.error('Alert container not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
const alertId = `alert-${++alertCounter}`;
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.id = alertId;
|
||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
|
||||
alertDiv.setAttribute('role', 'alert');
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
${dismissible ? '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>' : ''}
|
||||
`;
|
||||
const alertId = `alert-${++alertCounter}`;
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.id = alertId;
|
||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
|
||||
alertDiv.setAttribute('role', 'alert');
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
${dismissible ? '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>' : ''}
|
||||
`;
|
||||
|
||||
alertContainer.appendChild(alertDiv);
|
||||
alertContainer.appendChild(alertDiv);
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
dismissAlert(alertId);
|
||||
}, duration);
|
||||
}
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
dismissAlert(alertId);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return alertId;
|
||||
return alertId;
|
||||
}
|
||||
|
||||
function dismissAlert(alertId) {
|
||||
const alertElement = document.getElementById(alertId);
|
||||
if (alertElement) {
|
||||
const bsAlert = new bootstrap.Alert(alertElement);
|
||||
bsAlert.close();
|
||||
}
|
||||
const alertElement = document.getElementById(alertId);
|
||||
if (alertElement) {
|
||||
const bsAlert = new bootstrap.Alert(alertElement);
|
||||
bsAlert.close();
|
||||
}
|
||||
}
|
||||
|
||||
function clearAllAlerts() {
|
||||
const alertContainer = document.getElementById('alert-container');
|
||||
if (alertContainer) {
|
||||
const alerts = alertContainer.querySelectorAll('.alert');
|
||||
alerts.forEach(alert => {
|
||||
const bsAlert = new bootstrap.Alert(alert);
|
||||
bsAlert.close();
|
||||
});
|
||||
}
|
||||
const alertContainer = document.getElementById('alert-container');
|
||||
if (alertContainer) {
|
||||
const alerts = alertContainer.querySelectorAll('.alert');
|
||||
alerts.forEach(alert => {
|
||||
const bsAlert = new bootstrap.Alert(alert);
|
||||
bsAlert.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function successAlert(message, duration = 1_500) {
|
||||
return pushAlert(message, 'success', duration, false);
|
||||
return pushAlert(message, 'success', duration, false);
|
||||
}
|
||||
|
||||
function errorAlert(message, duration = 15_000) {
|
||||
return pushAlert(message, 'danger', /* duration */);
|
||||
return pushAlert(message, 'danger', /* duration */);
|
||||
}
|
||||
|
||||
function warningAlert(message, duration = 8_000) {
|
||||
return pushAlert(message, 'warning', duration);
|
||||
return pushAlert(message, 'warning', duration);
|
||||
}
|
||||
|
||||
function infoAlert(message, duration = 5_000) {
|
||||
return pushAlert(message, 'info', duration, false);
|
||||
return pushAlert(message, 'info', duration, false);
|
||||
}
|
||||
|
||||
// Adaptive trigger state management
|
||||
let adaptiveTriggerEnabled = false;
|
||||
|
||||
/**
|
||||
* Toggle the left adaptive trigger on/off
|
||||
*/
|
||||
async function toggle_adaptive_trigger() {
|
||||
const button = document.getElementById('adaptive-trigger-btn');
|
||||
const icon = document.getElementById('adaptive-trigger-icon');
|
||||
const text = document.getElementById('adaptive-trigger-text');
|
||||
|
||||
// Disable button during operation
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
if (adaptiveTriggerEnabled) {
|
||||
await controller.setAdaptiveTriggerPreset({ left: 'off', right: 'off'});
|
||||
adaptiveTriggerEnabled = false;
|
||||
|
||||
button.className = 'btn btn-info ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-hand-pointer';
|
||||
text.textContent = l("Enable Adaptive Trigger");
|
||||
|
||||
successAlert(l("Adaptive trigger disabled"));
|
||||
} else {
|
||||
await controller.setAdaptiveTriggerPreset({ left: 'heavy', right: 'heavy' });
|
||||
adaptiveTriggerEnabled = true;
|
||||
|
||||
button.className = 'btn btn-warning ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-hand-paper';
|
||||
text.textContent = l("Disable Adaptive Trigger");
|
||||
|
||||
successAlert(l("Adaptive trigger enabled"));
|
||||
}
|
||||
}
|
||||
finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update adaptive trigger button visibility based on controller type
|
||||
*/
|
||||
function updateAdaptiveTriggerButtonVisibility() {
|
||||
const button = document.getElementById('adaptive-trigger-btn');
|
||||
if (controller?.isConnected() && controller.getModel() === "DS5") {
|
||||
button.style.display = 'block';
|
||||
} else {
|
||||
button.style.display = 'none';
|
||||
adaptiveTriggerEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test haptic feedback vibration for 3 seconds
|
||||
*/
|
||||
async function test_haptic_feedback() {
|
||||
const button = document.getElementById('haptic-feedback-btn');
|
||||
const icon = document.getElementById('haptic-feedback-icon');
|
||||
const text = document.getElementById('haptic-feedback-text');
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
// Update UI to show vibration is active
|
||||
button.className = 'btn btn-warning ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-mobile-alt fa-shake';
|
||||
text.textContent = l("Vibrating...");
|
||||
|
||||
// Set vibration for 3 seconds (medium intensity on both motors)
|
||||
await controller.setVibration(255, 255, 3000, ({success}) => {
|
||||
button.className = 'btn btn-secondary ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-mobile-alt';
|
||||
text.textContent = l("Test Haptic Feedback");
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test speaker tone by playing a 1-second tone through the controller's speaker
|
||||
*/
|
||||
async function test_speaker_tone() {
|
||||
const button = document.getElementById('speaker-tone-btn');
|
||||
const icon = document.getElementById('speaker-tone-icon');
|
||||
const text = document.getElementById('speaker-tone-text');
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
// Update UI to show tone is playing
|
||||
button.className = 'btn btn-warning ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-volume-up fa-bounce';
|
||||
text.textContent = l("Playing tone...");
|
||||
|
||||
// Set speaker tone for 1 second
|
||||
await controller.setSpeakerTone(100, ({success}) => {
|
||||
button.className = 'btn btn-secondary ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-volume-up';
|
||||
text.textContent = l("Test Speaker Tone");
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test microphone by monitoring audio input levels and asking user to blow into it
|
||||
*/
|
||||
async function test_microphone() {
|
||||
const button = document.getElementById('microphone-test-btn');
|
||||
const icon = document.getElementById('microphone-test-icon');
|
||||
const text = document.getElementById('microphone-test-text');
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
// Update UI to show microphone test is starting
|
||||
button.className = 'btn btn-info ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-microphone fa-pulse';
|
||||
text.textContent = l("Starting microphone test...");
|
||||
|
||||
// Request microphone access
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false,
|
||||
autoGainControl: false
|
||||
}
|
||||
});
|
||||
|
||||
// Create audio context and analyzer
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const source = audioContext.createMediaStreamSource(stream);
|
||||
const analyzer = audioContext.createAnalyser();
|
||||
|
||||
analyzer.fftSize = 256;
|
||||
analyzer.smoothingTimeConstant = 0.8;
|
||||
source.connect(analyzer);
|
||||
|
||||
const bufferLength = analyzer.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
|
||||
// Update UI to show listening state
|
||||
button.className = 'btn btn-success ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-microphone fa-pulse';
|
||||
text.textContent = l("Blow into the microphone...");
|
||||
|
||||
let detectionCount = 0;
|
||||
let isDetecting = false;
|
||||
const testDuration = 10000; // 10 seconds
|
||||
const startTime = Date.now();
|
||||
|
||||
const checkAudioLevel = () => {
|
||||
if (Date.now() - startTime > testDuration) {
|
||||
// Test timeout
|
||||
cleanup();
|
||||
button.className = 'btn btn-outline-secondary ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-microphone';
|
||||
text.textContent = l("Test completed - Try again");
|
||||
button.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
analyzer.getByteFrequencyData(dataArray);
|
||||
|
||||
// Calculate average volume level
|
||||
let sum = 0;
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
sum += dataArray[i];
|
||||
}
|
||||
const average = sum / bufferLength;
|
||||
|
||||
// Detect significant audio input (blowing into mic)
|
||||
const threshold = 30; // Adjust this value as needed
|
||||
if (average > threshold) {
|
||||
if (!isDetecting) {
|
||||
isDetecting = true;
|
||||
detectionCount++;
|
||||
|
||||
// Update UI to show detection
|
||||
button.className = 'btn btn-warning ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-microphone fa-bounce';
|
||||
text.textContent = l("Microphone input detected!") + ` (${detectionCount})`;
|
||||
|
||||
// Provide haptic feedback if available
|
||||
if (controller?.isConnected() && controller.getModel() === "DS5") {
|
||||
controller.setVibration(100, 100, 200);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isDetecting) {
|
||||
isDetecting = false;
|
||||
// Return to listening state
|
||||
button.className = 'btn btn-success ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-microphone fa-pulse';
|
||||
text.textContent = l("Blow into the microphone...");
|
||||
}
|
||||
}
|
||||
|
||||
// Continue monitoring
|
||||
requestAnimationFrame(checkAudioLevel);
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
// Stop all tracks
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
// Close audio context
|
||||
if (audioContext.state !== 'closed') {
|
||||
audioContext.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Start monitoring
|
||||
checkAudioLevel();
|
||||
|
||||
// Auto-stop after test duration
|
||||
setTimeout(() => {
|
||||
cleanup();
|
||||
if (detectionCount > 0) {
|
||||
button.className = 'btn btn-success ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-microphone fa-check';
|
||||
text.textContent = l("Microphone test passed!") + ` (${detectionCount} detections)`;
|
||||
} else {
|
||||
button.className = 'btn btn-outline-secondary ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-microphone';
|
||||
text.textContent = l("No input detected - Try again");
|
||||
}
|
||||
button.disabled = false;
|
||||
}, testDuration);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Microphone test error:', error);
|
||||
|
||||
// Update UI to show error
|
||||
button.className = 'btn btn-danger ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-microphone-slash';
|
||||
|
||||
if (error.name === 'NotAllowedError') {
|
||||
text.textContent = l("Microphone access denied");
|
||||
} else if (error.name === 'NotFoundError') {
|
||||
text.textContent = l("No microphone found");
|
||||
} else {
|
||||
text.textContent = l("Microphone test failed");
|
||||
}
|
||||
|
||||
button.disabled = false;
|
||||
|
||||
// Reset button after 3 seconds
|
||||
setTimeout(() => {
|
||||
button.className = 'btn btn-secondary ds-btn ds-i18n';
|
||||
icon.className = 'fas fa-microphone';
|
||||
text.textContent = l("Test Microphone");
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update haptic feedback button visibility based on controller type
|
||||
*/
|
||||
function updateHapticFeedbackButtonVisibility() {
|
||||
const button = document.getElementById('haptic-feedback-btn');
|
||||
const model = controller?.getModel();
|
||||
const supported = (controller?.isConnected() && model === "DS5");
|
||||
button.style.display = supported ? 'block' : 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Update speaker tone button visibility based on controller type
|
||||
*/
|
||||
function updateSpeakerToneButtonVisibility() {
|
||||
const button = document.getElementById('speaker-tone-btn');
|
||||
const model = controller?.getModel();
|
||||
const supported = (controller?.isConnected() && model === "DS5");
|
||||
button.style.display = supported ? 'block' : 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Update microphone test button visibility based on controller type
|
||||
*/
|
||||
function updateMicrophoneTestButtonVisibility() {
|
||||
const button = document.getElementById('microphone-test-btn');
|
||||
const model = controller?.getModel();
|
||||
const supported = (controller?.isConnected() && model === "DS5");
|
||||
button.style.display = supported ? 'block' : 'none';
|
||||
}
|
||||
|
||||
|
||||
@@ -1014,6 +1311,10 @@ window.welcome_accepted = welcome_accepted;
|
||||
window.show_donate_modal = show_donate_modal;
|
||||
window.board_model_info = board_model_info;
|
||||
window.edge_color_info = edge_color_info;
|
||||
window.toggle_adaptive_trigger = toggle_adaptive_trigger;
|
||||
window.test_haptic_feedback = test_haptic_feedback;
|
||||
window.test_speaker_tone = test_speaker_tone;
|
||||
window.test_microphone = test_microphone;
|
||||
|
||||
// Auto-initialize the application when the module loads
|
||||
gboot();
|
||||
Reference in New Issue
Block a user