Add adaptive trigger support to the DS5

This commit is contained in:
Mathias Malmqvist
2025-09-21 01:20:35 +02:00
committed by dualshock-tools
parent 3b2b3d5c13
commit b240d1d65c
5 changed files with 840 additions and 53 deletions

View File

@@ -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>&nbsp;&nbsp;
<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>&nbsp;&nbsp;
<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>&nbsp;&nbsp;
<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>&nbsp;&nbsp;
<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>

View File

@@ -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
*/

View File

@@ -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;

View File

@@ -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
*/

View File

@@ -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();