Add Quick Test of LED lights

This commit is contained in:
Mathias Malmqvist
2025-09-29 23:03:15 +02:00
committed by dualshock-tools
parent 1252f43d23
commit 794485c265
2 changed files with 229 additions and 36 deletions

View File

@@ -737,6 +737,89 @@ class DS5Controller extends BaseController {
}
}
/**
* Set lightbar color
* @param {number} red - Red component (0-255)
* @param {number} green - Green component (0-255)
* @param {number} blue - Blue component (0-255)
*/
async setLightbarColor(red = 0, green = 0, blue = 0) {
try {
const { validFlag1 } = this.currentOutputState;
const outputStruct = new DS5OutputStruct({
...this.currentOutputState,
ledCRed: Math.max(0, Math.min(255, red)),
ledCGreen: Math.max(0, Math.min(255, green)),
ledCBlue: Math.max(0, Math.min(255, blue)),
validFlag1: validFlag1 | DS5_VALID_FLAG1.LIGHTBAR_COLOR,
});
await this.sendOutputReport(outputStruct.pack(), 'set lightbar color');
outputStruct.validFlag1 &= ~DS5_VALID_FLAG1.LIGHTBAR_COLOR;
// Update current state to reflect the changes
this.updateCurrentOutputState(outputStruct);
} catch (error) {
throw new Error("Failed to set lightbar color", { cause: error });
}
}
/**
* Set player indicator lights
* @param {number} pattern - Player indicator pattern (0-31, each bit represents a light)
*/
async setPlayerIndicator(pattern = 0) {
try {
const { validFlag1 } = this.currentOutputState;
const outputStruct = new DS5OutputStruct({
...this.currentOutputState,
playerIndicator: Math.max(0, Math.min(31, pattern)),
validFlag1: validFlag1 | DS5_VALID_FLAG1.PLAYER_INDICATOR,
});
await this.sendOutputReport(outputStruct.pack(), 'set player indicator');
outputStruct.validFlag1 &= ~DS5_VALID_FLAG1.PLAYER_INDICATOR;
// Update current state to reflect the changes
this.updateCurrentOutputState(outputStruct);
} catch (error) {
throw new Error("Failed to set player indicator", { cause: error });
}
}
/**
* Reset lights to default state (turn off)
*/
async resetLights() {
try {
await this.setLightbarColor(0, 0, 0);
await this.setPlayerIndicator(0);
await this.setMuteLed(0);
} catch (error) {
throw new Error("Failed to reset lights", { cause: error });
}
}
/**
* Set mute button LED state
* @param {number} state - Mute LED state (0 = off, 1 = solid, 2 = pulsing)
*/
async setMuteLed(state = 0) {
try {
const { validFlag1 } = this.currentOutputState;
const outputStruct = new DS5OutputStruct({
...this.currentOutputState,
muteLedControl: Math.max(0, Math.min(2, state)),
validFlag1: validFlag1 | DS5_VALID_FLAG1.MUTE_LED,
});
await this.sendOutputReport(outputStruct.pack(), 'set mute LED');
outputStruct.validFlag1 &= ~DS5_VALID_FLAG1.MUTE_LED;
// Update current state to reflect the changes
this.updateCurrentOutputState(outputStruct);
} catch (error) {
throw new Error("Failed to set mute LED", { cause: error });
}
}
/**
* Parse DS5 battery status from input data
*/

View File

@@ -5,16 +5,18 @@ const ACCORDION_ELEMENTS = [
'buttons-test-collapse',
'haptic-test-collapse',
'adaptive-test-collapse',
'lights-test-collapse',
'speaker-test-collapse',
'microphone-test-collapse'
];
const TEST_SEQUENCE = ['usb', 'buttons', 'haptic', 'adaptive', 'speaker', 'microphone'];
const TEST_SEQUENCE = ['usb', 'buttons', 'haptic', 'adaptive', 'lights', 'speaker', 'microphone'];
const TEST_NAMES = {
'usb': 'USB Connector',
'buttons': 'Buttons',
'haptic': 'Haptic Vibration',
'adaptive': 'Adaptive Trigger',
'lights': 'Lights',
'speaker': 'Speaker',
'microphone': 'Microphone'
};
@@ -49,7 +51,6 @@ export class QuickTestModal {
constructor(controllerInstance, { l }) {
this.controller = controllerInstance;
this.l = l;
this._modalListenersAdded = false;
this.resetAllTests();
@@ -59,7 +60,6 @@ export class QuickTestModal {
this._boundAccordionShown = (event) => this._handleAccordionShown(event);
this._boundAccordionHidden = (event) => this._handleAccordionHidden(event);
this._boundModalHidden = () => {
console.log("Quick Test modal hidden event triggered");
this.resetAllTests();
destroyCurrentInstance();
};
@@ -73,6 +73,7 @@ export class QuickTestModal {
buttons: null,
haptic: null,
adaptive: null,
lights: null,
speaker: null,
microphone: null,
microphoneStream: null,
@@ -83,6 +84,7 @@ export class QuickTestModal {
longPressThreshold: 400,
isTransitioning: false,
skippedTests: [],
lightsAnimationInterval: null,
};
}
@@ -154,6 +156,7 @@ export class QuickTestModal {
'buttons': 'fas fa-gamepad',
'haptic': 'fas fa-mobile-alt',
'adaptive': 'fas fa-hand-pointer',
'lights': 'fas fa-lightbulb',
'speaker': 'fas fa-volume-up',
'microphone': 'fas fa-microphone'
};
@@ -207,7 +210,7 @@ export class QuickTestModal {
`;
case 'buttons':
return `
<p class="ds-i18n">This test checks all controller buttons by requiring you to press each button three times.</p>
<p class="ds-i18n">This test checks all controller buttons by requiring you to press each button up to three times.</p>
<p class="ds-i18n"><strong>Instructions:</strong> Press each button until they turn green.</p>
<div class="d-flex justify-content-center mb-3">
<div style="width: 80%; max-width: 400px;" id="quick-test-controller-svg-placeholder">
@@ -256,6 +259,23 @@ export class QuickTestModal {
</button>
</div>
`;
case 'lights':
return `
<p class="ds-i18n">This test will cycle through red, green, and blue colors on the controller lightbar, animate the player indicator lights, and flash the mute button.</p>
<p class="ds-i18n"><strong>Instructions:</strong> Watch the controller lights change colors, the player lights animate, and the mute button flash.</p>
<div class="alert alert-info mb-3">
<i class="fas fa-info-circle me-2"></i>
<span class="ds-i18n">The lights will automatically cycle through colors and patterns until you mark the test as passed or failed.</span>
</div>
<div class="d-flex gap-2 mt-3">
<button type="button" class="btn btn-success" id="lights-pass-btn" onclick="markTestResult('lights', true)">
<i class="fas fa-check me-1"></i><span class="ds-i18n">Pass</span>
</button>
<button type="button" class="btn btn-danger" id="lights-fail-btn" onclick="markTestResult('lights', false)">
<i class="fas fa-times me-1"></i><span class="ds-i18n">Fail</span>
</button>
</div>
`;
case 'speaker':
return `
<p class="ds-i18n">This test will play a tone through the controller's built-in speaker.</p>
@@ -367,14 +387,13 @@ export class QuickTestModal {
}
});
// Only add modal listeners once
if (!this._modalListenersAdded) {
$('#quickTestModal').on('hidden.bs.modal', this._boundModalHidden);
$('#quickTestModal').on('shown.bs.modal', () => {
this._updateInstructions();
});
this._modalListenersAdded = true;
}
// Always try to add modal listeners (remove first to avoid duplicates)
this._removeModalEventListeners();
const $modal = $('#quickTestModal');
$modal.on('hidden.bs.modal', this._boundModalHidden);
$modal.on('shown.bs.modal', () => {
this._updateInstructions();
});
}
/**
@@ -386,20 +405,27 @@ export class QuickTestModal {
const elementId = `${testType}-test-collapse`;
const $element = $(`#${elementId}`);
if ($element.length) {
$element.off('shown.bs.collapse', this._boundAccordionShown);
$element.off('hidden.bs.collapse', this._boundAccordionHidden);
$element.off('shown.bs.collapse');
$element.off('hidden.bs.collapse');
}
});
}
/**
* Remove modal event listeners only
*/
_removeModalEventListeners() {
const $modal = $('#quickTestModal');
$modal.off('hidden.bs.modal', this._boundModalHidden);
$modal.off('shown.bs.modal');
}
/**
* Remove event listeners
*/
removeEventListeners() {
console.log("Removing event listeners");
this._removeAccordionEventListeners();
$('#quickTestModal').off('hidden.bs.modal', this._boundModalHidden);
this._modalListenersAdded = false;
this._removeModalEventListeners();
}
/**
@@ -523,6 +549,9 @@ export class QuickTestModal {
case 'adaptive':
this._startAdaptiveTest();
break;
case 'lights':
this._startLightsTest();
break;
case 'speaker':
this._startSpeakerTest();
break;
@@ -551,6 +580,9 @@ export class QuickTestModal {
case 'adaptive':
this._stopAdaptiveTest();
break;
case 'lights':
this._stopLightsTest();
break;
case 'microphone':
this._stopMicrophoneTest();
break;
@@ -613,14 +645,9 @@ export class QuickTestModal {
const buttonElement = this._getQuickTestElement(BUTTON_INFILL_MAPPING[button]);
if (buttonElement) {
let color;
// Special buttons (create, options, mute, ps) go straight to green on first press
if (['create', 'options', 'mute', 'ps'].includes(button)) {
color = ['orange'][count] || '#16c016ff';
} else {
// Other buttons follow the 3-press sequence
color = ['orange', '#a5c9fcff', '#287ffaff'][count] || '#16c016ff';
}
const checkOnce = ['create', 'touchpad', 'options', 'l3', 'ps', 'mute', 'r3'].includes(button);
const colors = checkOnce ? ['orange'] : ['orange', '#a5c9fcff', '#287ffaff'];
const color = colors[count] || '#16c016ff';
this._setSvgGroupColor(buttonElement, color);
}
}
@@ -632,8 +659,8 @@ export class QuickTestModal {
const allPressed = BUTTONS.every(button => {
const count = this.state.buttonPressCount[button] || 0;
// Special buttons (create, options, mute, ps) only need 1 press
const isSpecialButton = ['create', 'options', 'mute', 'ps'].includes(button);
return isSpecialButton ? count >= 1 : count >= 3;
const checkOnce = ['create', 'touchpad', 'options', 'l3', 'ps', 'mute', 'r3'].includes(button);
return checkOnce ? count >= 1 : count >= 3;
});
if (allPressed) {
// Auto-pass the test
@@ -692,6 +719,84 @@ export class QuickTestModal {
await this.controller.setAdaptiveTriggerPreset({ left: 'off', right: 'off' });
}
/**
* Start lights test - cycles through colors and animates player lights
*/
async _startLightsTest() {
this._startIconAnimation('lights');
const { currentController } = this.controller;
if (!currentController?.setLightbarColor || !currentController?.setPlayerIndicator) {
console.warn('Controller does not support light control');
alert('This controller does not support light control. Only DualSense (DS5) controllers support this feature.');
this._stopIconAnimation('lights');
return;
}
const colors = [
{ r: 255, g: 0, b: 0 }, // Red
{ r: 0, g: 255, b: 0 }, // Green
{ r: 0, g: 0, b: 255 }, // Blue
];
const playerPatterns = [
0b10001, // Light 1 & 5
0b01010, // Light 2 & 4
0b00100, // Light 3
0b01010, // Light 4 & 2
0b10001, // Light 5 & 1
0b11111, // All lights
0b00000, // No lights
0b11111, // All lights
0b00000, // No lights
];
let colorIndex = 0;
let patternIndex = 0;
// Set mute LED - cycle through off, solid, pulsing
if (currentController.setMuteLed) {
await currentController.setMuteLed(2); // pulsing
}
// Start the animation
this.state.lightsAnimationInterval = setInterval(async () => {
try {
const color = colors[colorIndex];
const pattern = playerPatterns[patternIndex];
// Set lightbar color and player indicator
await currentController.setLightbarColor(color.r, color.g, color.b);
await currentController.setPlayerIndicator(pattern);
// Cycle through colors every 3 pattern changes
patternIndex = (patternIndex + 1) % playerPatterns.length;
if (patternIndex === 0) {
colorIndex = (colorIndex + 1) % colors.length;
}
} catch (error) {
console.error('Error during lights test:', error);
}
}, 200);
}
/**
* Stop lights test and reset lights
*/
async _stopLightsTest() {
if(!this.state) return;
this._stopIconAnimation('lights');
// Clear the animation interval
if (this.state.lightsAnimationInterval) {
clearInterval(this.state.lightsAnimationInterval);
this.state.lightsAnimationInterval = null;
}
await this.controller.currentController.resetLights();
}
/**
* Start speaker tone test
*/
@@ -787,6 +892,8 @@ export class QuickTestModal {
* Stop microphone test
*/
_stopMicrophoneTest() {
if(!this.state) return;
const $levelContainer = $('#mic-level-container');
this.state.microphoneMonitoring = false;
@@ -944,7 +1051,7 @@ export class QuickTestModal {
}
});
const numTests = TEST_SEQUENCE.length;
const numTests = TEST_SEQUENCE.length - skipped;
const totalProcessed = completed + skipped;
if (totalProcessed === 0) {
@@ -1134,6 +1241,8 @@ export class QuickTestModal {
* Clear all active long-press timers
*/
_clearAllLongPressTimers() {
if(!this.state) return;
Object.keys(this.state.longPressTimers).forEach(button => {
this._clearLongPressTimer(button);
});
@@ -1210,20 +1319,21 @@ export class QuickTestModal {
* Reset all tests to initial state
*/
resetAllTests() {
// Clear any active long-press timers before resetting state
this._clearAllLongPressTimers();
// Clean up any active tests BEFORE resetting state
this._stopButtonsTest();
this._stopAdaptiveTest();
this._stopLightsTest();
this._stopMicrophoneTest();
// Reset state
this._initializeState();
// Load saved skipped tests from localStorage
this._loadSkippedTestsFromStorage();
// Clear any active long-press timers before resetting state
this._clearAllLongPressTimers();
// Clean up any active tests
this._stopButtonsTest();
this._stopAdaptiveTest();
this._stopMicrophoneTest();
// Reset button colors to initial state
this._resetButtonColors();