diff --git a/css/main.css b/css/main.css index cfa634d..a457ed0 100644 --- a/css/main.css +++ b/css/main.css @@ -26,3 +26,43 @@ dl.row dd { background-color: #0d6efd !important; color: white !important; } + +/* Quick Test Icon Animations */ +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); } + 20%, 40%, 60%, 80% { transform: translateX(2px); } +} + +@keyframes pulse { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.1); opacity: 0.8; } +} + +@keyframes bounce { + 0%, 20%, 50%, 80%, 100% { transform: translateY(0); } + 40% { transform: translateY(-3px); } + 60% { transform: translateY(-2px); } +} + +@keyframes glow { + 0%, 100% { text-shadow: 0 0 5px rgba(13, 110, 253, 0.5); } + 50% { text-shadow: 0 0 15px rgba(13, 110, 253, 0.8), 0 0 25px rgba(13, 110, 253, 0.6); } +} + +/* Animation classes for different test types */ +.test-icon-haptic { + animation: shake 0.5s ease-in-out infinite; +} + +.test-icon-adaptive { + animation: pulse 1s ease-in-out infinite; +} + +.test-icon-speaker { + animation: bounce 0.6s ease-in-out infinite; +} + +.test-icon-microphone { + animation: glow 1.5s ease-in-out infinite; +} diff --git a/index.html b/index.html index 4a2f1a7..de3aebb 100644 --- a/index.html +++ b/index.html @@ -150,21 +150,9 @@ Calibrate stick range Finetune stick calibration (beta) Fast calibrate stick center (OLD) - - - Enable Adaptive Trigger - - - - Test Haptic Feedback - - - - Test Speaker Tone - - - - Test Microphone + + + Quick Test Save changes permanently diff --git a/js/controller-manager.js b/js/controller-manager.js index ccff057..810f684 100644 --- a/js/controller-manager.js +++ b/js/controller-manager.js @@ -349,19 +349,20 @@ class ControllerManager { // } // } - return await this.currentController.setAdaptiveTrigger(leftPreset, rightPreset); + 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 {Object} options - Vibration options + * @param {number} options.heavyLeft - Left motor intensity (0-255) + * @param {number} options.lightRight - Right motor intensity (0-255) + * @param {number} options.duration - Duration in milliseconds (optional) * @param {Function} doneCb - Callback function called when vibration ends (optional) */ - async setVibration(leftMotor, rightMotor, duration = 0, doneCb = ({success}) => {}) { + async setVibration({heavyLeft, lightRight, duration = 0}, doneCb = ({success}) => {}) { try { - await this.currentController.setVibration(leftMotor, rightMotor); + await this.currentController.setVibration(heavyLeft, lightRight); // If duration is specified, automatically turn off vibration after the duration if (duration > 0) { diff --git a/js/controllers/ds5-controller.js b/js/controllers/ds5-controller.js index 3b53d10..e2f1817 100644 --- a/js/controllers/ds5-controller.js +++ b/js/controllers/ds5-controller.js @@ -661,16 +661,16 @@ class DS5Controller extends BaseController { /** * Set vibration motors for haptic feedback - * @param {number} leftMotor - Left motor intensity (0-255) - * @param {number} rightMotor - Right motor intensity (0-255) + * @param {number} heavyLeft - Left motor intensity (0-255) + * @param {number} lightRight - Right motor intensity (0-255) */ - async setVibration(leftMotor = 0, rightMotor = 0) { + async setVibration(heavyLeft = 0, lightRight = 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)), + bcVibrationLeft: Math.max(0, Math.min(255, heavyLeft)), + bcVibrationRight: Math.max(0, Math.min(255, lightRight)), validFlag0: validFlag0 | DS5_VALID_FLAG0.LEFT_VIBRATION | DS5_VALID_FLAG0.RIGHT_VIBRATION, // Update both vibration motors }); await this.sendOutputReport(outputStruct.pack(), 'set vibration'); diff --git a/js/core.js b/js/core.js index abf0823..063f114 100644 --- a/js/core.js +++ b/js/core.js @@ -9,6 +9,12 @@ import { draw_stick_position, CIRCULARITY_DATA_SIZE } from './stick-renderer.js' import { ds5_finetune, isFinetuneVisible, finetune_handle_controller_input } from './modals/finetune-modal.js'; import { calibrate_stick_centers, auto_calibrate_stick_centers } from './modals/calib-center-modal.js'; import { calibrate_range } from './modals/calib-range-modal.js'; +import { + updateQuickTestButtonVisibility, + show_quick_test_modal, + isQuickTestVisible, + quicktest_handle_controller_input +} from './modals/quick-test-modal.js'; // Application State - manages app-wide state and UI const app = { @@ -248,10 +254,7 @@ async function continue_connection({data, device}) { $("#mainmenu").show(); $("#resetBtn").show(); - updateAdaptiveTriggerButtonVisibility(); - updateHapticFeedbackButtonVisibility(); - updateSpeakerToneButtonVisibility(); - updateMicrophoneTestButtonVisibility(); + updateQuickTestButtonVisibility(controller); $("#d-nvstatus").text = l("Unknown"); $("#d-bdaddr").text = l("Unknown"); @@ -320,10 +323,7 @@ async function disconnect() { $("#onlinebar").hide(); $("#mainmenu").hide(); - updateAdaptiveTriggerButtonVisibility(); - updateHapticFeedbackButtonVisibility(); - updateSpeakerToneButtonVisibility(); - updateMicrophoneTestButtonVisibility(); + updateQuickTestButtonVisibility(controller); } // Wrapper function for HTML onclick handlers @@ -665,6 +665,11 @@ function get_current_test_tab() { function handleControllerInput({ changes, inputConfig, touchPoints, batteryStatus }) { const { buttonMap } = inputConfig; + // Handle Quick Test Modal input (can be open from any tab) + if (isQuickTestVisible()) { + quicktest_handle_controller_input(changes); + } + const current_active_tab = get_current_main_tab(); switch (current_active_tab) { case 'controller-tab': // Main controller tab @@ -696,7 +701,7 @@ function handle_test_input(/* changes */) { const l2 = controller.button_states.l2_analog || 0; const r2 = controller.button_states.r2_analog || 0; if (l2 || r2) { - trigger_haptic_motors(l2, r2); + // trigger_haptic_motors(l2, r2); } break; @@ -882,59 +887,6 @@ function board_model_info() { show_popup(l3 + "" + l1 + " " + l2, true); } - -const trigger_haptic_motors = (() => { - let haptic_timeout = undefined; - let haptic_last_trigger = 0; - - return async function(strong_motor /*left*/, weak_motor /*right*/) { - // The DS4 contoller has a strong (left) and a weak (right) motor. - // The DS5 emulates the same behavior, but the left and right motors are the same. - - const now = Date.now(); - if (now - haptic_last_trigger < 200) { - return; // Rate limited - ignore calls within 200ms - } - - haptic_last_trigger = now; - - try { - if (!controller.isConnected()) return; - - const model = controller.getModel(); - const device = controller.getDevice(); - if (model == "DS4") { - const data = new Uint8Array([0x05, 0x00, 0, weak_motor, strong_motor]); - await device.sendReport(0x05, data); - } else if (model.startsWith("DS5")) { - const data = new Uint8Array([0x02, 0x00, weak_motor, strong_motor]); - await device.sendReport(0x02, data); - } - - // Stop rumble after duration - clearTimeout(haptic_timeout); - haptic_timeout = setTimeout(stop_haptic_motors, 250); - } catch(error) { - throw new Error(l("Error triggering rumble"), { cause: error }); - } - }; -})(); - -async function stop_haptic_motors() { - if (!controller.isConnected()) return; - - const model = controller.getModel(); - const device = controller.getDevice(); - if (model == "DS4") { - const data = new Uint8Array([0x05, 0x00, 0, 0, 0]); - await device.sendReport(0x05, data); - } else if (model.startsWith("DS5")) { - const data = new Uint8Array([0x02, 0x00, 0, 0]); - await device.sendReport(0x02, data); - } -} - - // Alert Management Functions let alertCounter = 0; @@ -1009,287 +961,9 @@ function infoAlert(message, duration = 5_000) { 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'; -} // Export functions to global scope for HTML onclick handlers @@ -1311,10 +985,7 @@ 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; +window.show_quick_test_modal = () => show_quick_test_modal(controller, { l }); // Auto-initialize the application when the module loads gboot(); \ No newline at end of file diff --git a/js/modals/quick-test-modal.js b/js/modals/quick-test-modal.js new file mode 100644 index 0000000..147184c --- /dev/null +++ b/js/modals/quick-test-modal.js @@ -0,0 +1,614 @@ +'use strict'; + +const ACCORDION_ELEMENTS = [ + 'usb-test-collapse', + 'haptic-test-collapse', + 'adaptive-test-collapse', + 'speaker-test-collapse', + 'microphone-test-collapse' +]; + +const TEST_SEQUENCE = ['usb', 'haptic', 'adaptive', 'speaker', 'microphone']; + +/** + * Quick Test Modal Class + * Handles controller feature testing including haptic feedback, adaptive triggers, speaker, and microphone functionality + */ +export class QuickTestModal { + constructor(controllerInstance, { l }) { + this.controller = controllerInstance; + this.l = l; + + // Test state + this.state = { + haptic: null, + adaptive: null, + speaker: null, + microphone: null, + microphoneStream: null, + microphoneContext: null, + microphoneMonitoring: false + }; + + // Bind event handlers to maintain proper context + 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(); + }; + + this._initEventListeners(); + } + + /** + * Start icon animation for a specific test type + */ + _startIconAnimation(testType) { + const $accordionItem = $(`#${testType}-test-item`); + const $icon = $accordionItem.find('.accordion-button i'); + $icon.addClass(`test-icon-${testType}`); + } + + /** + * Stop icon animation for a specific test type + */ + _stopIconAnimation(testType) { + const $accordionItem = $(`#${testType}-test-item`); + const $icon = $accordionItem.find('.accordion-button i'); + $icon.removeClass(`test-icon-${testType}`); + } + + /** + * Update the instruction text based on current test state + */ + _updateInstructions() { + const $instructionsText = $('#quick-test-instructions-text'); + const activeTest = this._getCurrentActiveTest(); + const allTestsCompleted = this._areAllTestsCompleted(); + + if (activeTest) { + $instructionsText.html(this.l('Press Square to Pass or Cross to Fail')); + } else if (allTestsCompleted) { + $instructionsText.html(this.l('Press Circle to close, or Square to start over')); + } else { + $instructionsText.html(this.l('Press Square to begin')); + } + } + + /** + * Check if all tests have been completed + */ + _areAllTestsCompleted() { + return TEST_SEQUENCE.every(test => this.state[test] !== null); + } + + /** + * Initialize event listeners for the quick test modal + */ + // Set up event listeners for accordion collapse events to auto-start tests + _initEventListeners() { + ACCORDION_ELEMENTS.forEach(elementId => { + const $element = $(`#${elementId}`); + if ($element.length) { + $element.on('shown.bs.collapse', this._boundAccordionShown); + $element.on('hidden.bs.collapse', this._boundAccordionHidden); + } + }); + + $('#quickTestModal').on('hidden.bs.modal', this._boundModalHidden); + $('#quickTestModal').on('shown.bs.modal', () => { + this._updateInstructions(); + }); + } + + /** + * Remove event listeners + */ + removeEventListeners() { + console.log("Removing event listeners"); + ACCORDION_ELEMENTS.forEach(elementId => { + const $element = $(`#${elementId}`); + if ($element.length) { + $element.off('shown.bs.collapse', this._boundAccordionShown); + $element.off('hidden.bs.collapse', this._boundAccordionHidden); + } + }); + + $('#quickTestModal').off('hidden.bs.modal', this._boundModalHidden); + } + + /** + * Open the Quick Test modal + */ + open() { + bootstrap.Modal.getOrCreateInstance('#quickTestModal').show(); + } + + /** + * Handle accordion section being shown (expanded) + */ + _handleAccordionShown(event) { + const collapseId = event.target.id; + const testType = collapseId.replace('-test-collapse', ''); + + // Update instructions when a test becomes active + this._updateInstructions(); + + // Always auto-start test when section is expanded + // Small delay to ensure UI is fully expanded + setTimeout(() => { + switch (testType) { + case 'haptic': + this._startHapticTest(); + break; + case 'adaptive': + this._startAdaptiveTest(); + break; + case 'speaker': + this._startSpeakerTest(); + break; + case 'microphone': + this._startMicrophoneTest(); + break; + } + }, 100); + } + + /** + * Handle accordion section being hidden (collapsed) + */ + _handleAccordionHidden(event) { + const collapseId = event.target.id; + const testType = collapseId.replace('-test-collapse', ''); + + // Stop ongoing tests when section is collapsed + switch (testType) { + case 'adaptive': + this._stopAdaptiveTest(); + break; + case 'microphone': + this._stopMicrophoneTest(); + break; + } + + // Update instructions when a test is collapsed + setTimeout(() => { + this._updateInstructions(); + }, 300); + } + + /** + * Start haptic vibration test + */ + async _startHapticTest() { + this._startIconAnimation('haptic'); + await this.controller.setVibration({ heavyLeft: 255, lightRight: 255, duration: 1000 }); + setTimeout(() => { this._stopIconAnimation('haptic'); }, 1000); } + + /** + * Start adaptive trigger test + */ + async _startAdaptiveTest() { + this._startIconAnimation('adaptive'); + await this.controller.setAdaptiveTriggerPreset({ left: 'heavy', right: 'heavy' }); + } + + /** + * Stop adaptive trigger test + */ + async _stopAdaptiveTest() { + this._stopIconAnimation('adaptive'); + console.log("Stopping Adaptive Trigger Test", this.controller); + await this.controller.setAdaptiveTriggerPreset({ left: 'off', right: 'off' }); + } + + /** + * Start speaker tone test + */ + async _startSpeakerTest() { + this._startIconAnimation('speaker'); + await this.controller.setSpeakerTone(100); + setTimeout(() => { this._stopIconAnimation('speaker'); }, 1000); + } + + /** + * Start microphone test + */ + async _startMicrophoneTest() { + const $levelContainer = $('#mic-level-container'); + const $levelBar = $('#mic-level-bar'); + + if (this.state.microphoneMonitoring) { + // Stop monitoring + this._stopMicrophoneTest(); + return; + } + + try { + // 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; + const bufferLength = analyzer.frequencyBinCount; + const dataArray = new Uint8Array(bufferLength); + + source.connect(analyzer); + + this.state.microphoneStream = stream; + this.state.microphoneContext = audioContext; + this.state.microphoneMonitoring = true; + + this._startIconAnimation('microphone'); + + $levelContainer.show(); + + // Monitor audio levels + let isVibrating = false; + const vibrationThreshold = 30; // Audio level threshold to trigger vibration + let count = 0; + + const updateLevel = () => { + if (!this.state.microphoneMonitoring) return; + + analyzer.getByteFrequencyData(dataArray); + + // Calculate average level + const sum = dataArray.reduce((acc, value) => acc + value, 0); + const average = sum / bufferLength; + const percentage = Math.min(100, (average / 255) * 100); + + $levelBar.css('width', percentage + '%'); + $levelBar.attr('aria-valuenow', percentage); + + // Trigger vibration when audio level exceeds threshold + if (percentage > vibrationThreshold && !isVibrating) { + this.controller.setVibration({ heavyLeft: 50, duration: 50 }, () => { isVibrating = false; }); + isVibrating = true; + count++; + } + + if(count > 5){ + const activeTest = this._getCurrentActiveTest(); + this.markTestResult(activeTest, true); + } + + requestAnimationFrame(updateLevel); + }; + + updateLevel(); + + } catch (error) { + console.error('Microphone test failed:', error); + } + } + + /** + * Stop microphone test + */ + _stopMicrophoneTest() { + const $levelContainer = $('#mic-level-container'); + + this.state.microphoneMonitoring = false; + + this._stopIconAnimation('microphone'); + + if (this.state.microphoneStream) { + this.state.microphoneStream.getTracks().forEach(track => track.stop()); + this.state.microphoneStream = null; + } + + if (this.state.microphoneContext) { + this.state.microphoneContext.close(); + this.state.microphoneContext = null; + } + + $levelContainer.hide(); + } + + /** + * Mark test result and update UI + */ + markTestResult(testType, passed) { + this.state[testType] = passed; + + this._stopIconAnimation(testType); + + const $statusBadge = $(`#${testType}-test-status`); + const $accordionItem = $(`#${testType}-test-item`); + const $accordionButton = $accordionItem.find('.accordion-button'); + + $accordionItem.removeClass('border-success border-danger'); + + if (passed) { + $statusBadge.attr('class', 'badge bg-success me-2'); + $statusBadge.text(this.l('Passed')); + $accordionItem.addClass('border-success'); + $accordionButton.css('backgroundColor', 'rgba(25, 135, 84, 0.1)'); // Light green background + } else { + $statusBadge.attr('class', 'badge bg-danger me-2'); + $statusBadge.text(this.l('Failed')); + $accordionItem.addClass('border-danger'); + $accordionButton.css('backgroundColor', 'rgba(220, 53, 69, 0.1)'); // Light red background + } + + // Clean up any active tests + if (testType === 'adaptive') { + this._stopAdaptiveTest(); + } else if (testType === 'microphone') { + this._stopMicrophoneTest(); + } + + this._updateTestSummary(); + + // Auto-expand next test + this._expandNextTest(testType); + } + + /** + * Update test summary display + */ + _updateTestSummary() { + const $summary = $('#test-summary'); + + let completed = 0; + let passed = 0; + + TEST_SEQUENCE.forEach(test => { + if (this.state[test] !== null) { + completed++; + if (this.state[test]) passed++; + } + }); + + if (completed === 0) { + $summary.text(this.l('No tests completed yet.')); + $summary.attr('class', 'text-muted ds-i18n'); + } else { + $summary.text(this.l(`${completed}/4 tests completed. ${passed} passed, ${completed - passed} failed.`)); + $summary.attr('class', completed === 4 ? 'text-success' : 'text-info'); + } + } + + /** + * Expand the next untested item + */ + _expandNextTest(currentTest) { + const currentIndex = TEST_SEQUENCE.indexOf(currentTest); + + // Always collapse the current test first + const $currentCollapse = $(`#${currentTest}-test-collapse`); + bootstrap.Collapse.getInstance($currentCollapse[0])?.hide(); + + // Find next untested item + for (let i = currentIndex + 1; i < TEST_SEQUENCE.length; i++) { + const nextTest = TEST_SEQUENCE[i]; + if (this.state[nextTest] === null) { + const $nextCollapse = $(`#${nextTest}-test-collapse`); + + // Expand next + setTimeout(() => { + bootstrap.Collapse.getOrCreateInstance($nextCollapse[0]).show(); + }, 300); + + break; + } + } + } + + /** + * Get the currently active (expanded) test type + */ + _getCurrentActiveTest() { + for (const test of TEST_SEQUENCE) { + const $collapse = $(`#${test}-test-collapse`); + if ($collapse.hasClass('show')) { + return test; + } + } + return null; + } + + /** + * Handle controller input for test navigation and control + */ + handleControllerInput(changes) { + const activeTest = this._getCurrentActiveTest(); + + // Handle Cross button (Start test sequence OR mark test as passed) + if (changes.square === true) { + if (!activeTest) { + this._startTestSequence(); + } else { + this.markTestResult(activeTest, true); + } + } + + // Handle Square button (Pass) + if (activeTest && changes.cross === true) { + this.markTestResult(activeTest, false); + } + + // Handle Triangle button (Move to previous test) + if (changes.triangle === true) { + this._moveToPreviousTest(); + } + + // Handle Circle button (Close the modal) + if (changes.circle === true) { + bootstrap.Modal.getOrCreateInstance('#quickTestModal').hide(); + } + } + + /** + * Start the test sequence from the beginning + */ + _startTestSequence() { + // First, reset all tests to ensure clean state + this.resetAllTests(); + + // After a short delay, start with the first test + setTimeout(() => { + const [firstTest] = TEST_SEQUENCE; + const $firstCollapse = $(`#${firstTest}-test-collapse`); + bootstrap.Collapse.getOrCreateInstance($firstCollapse[0]).show(); + }, 300); + } + + /** + * Move to the previous test in the sequence + */ + _moveToPreviousTest() { + const activeTest = this._getCurrentActiveTest(); + if (!activeTest) return; + + const currentIndex = TEST_SEQUENCE.indexOf(activeTest); + const previousIndex = currentIndex > 0 ? currentIndex - 1 : 0; + if(previousIndex == currentIndex) return; + + const previousTest = TEST_SEQUENCE[previousIndex]; + + // Collapse current test + const $currentCollapse = $(`#${activeTest}-test-collapse`); + bootstrap.Collapse.getInstance($currentCollapse[0])?.hide(); + + + // Expand previous test after a short delay + setTimeout(() => { + const $previousCollapse = $(`#${previousTest}-test-collapse`); + bootstrap.Collapse.getOrCreateInstance($previousCollapse[0]).show(); + }, 300); + } + + /** + * Reset all tests to initial state + */ + resetAllTests() { + // Reset state + this.state = { + haptic: null, + adaptive: null, + speaker: null, + microphone: null, + microphoneStream: null, + microphoneContext: null, + microphoneMonitoring: false + }; + + // Clean up any active tests + this._stopAdaptiveTest(); + this._stopMicrophoneTest(); + + // Reset UI + TEST_SEQUENCE.forEach(test => { + this._stopIconAnimation(test); + + const $statusBadge = $(`#${test}-test-status`); + const $accordionItem = $(`#${test}-test-item`); + const $accordionButton = $accordionItem.find('.accordion-button'); + + $statusBadge.attr('class', 'badge bg-secondary me-2'); + $statusBadge.text(this.l('Not tested')); + $accordionItem.removeClass('border-success border-danger'); + $accordionButton.css('backgroundColor', ''); // Clear background color + + if (test === 'microphone') { + const $levelContainer = $('#mic-level-container'); + $levelContainer.hide(); + } + }); + + this._updateTestSummary(); + + // Update instructions after reset + this._updateInstructions(); + + // Collapse all accordions + const $accordions = $('#quickTestAccordion .accordion-collapse'); + $accordions.each((index, accordion) => { + bootstrap.Collapse.getInstance(accordion)?.hide(); + }); + } +} + +// Global reference to the current quick test instance +let currentQuickTestInstance = null; + +/** + * Helper function to safely clear the current quick test instance + */ +function destroyCurrentInstance() { + if (currentQuickTestInstance) { + console.log("Destroying current quick test instance"); + currentQuickTestInstance.removeEventListeners(); + currentQuickTestInstance = null; + } +} + +/** + * Update quick test button visibility based on controller type + */ +export function updateQuickTestButtonVisibility(controller) { + const $button = $('#quick-test-btn'); + const model = controller?.getModel(); + const supported = (controller?.isConnected() && (model === "DS5" || model === "DS5_Edge")); + $button.css('display', supported ? 'block' : 'none'); +} + +/** + * Check if the Quick Test Modal is currently visible + */ +export function isQuickTestVisible() { + const $modal = $('#quickTestModal'); + return $modal.hasClass('show'); +} + +/** + * Handle controller input for the Quick Test Modal + */ +export function quicktest_handle_controller_input(changes) { + if (currentQuickTestInstance && isQuickTestVisible()) { + currentQuickTestInstance.handleControllerInput(changes); + } +} + +/** + * Show the Quick Test modal (legacy function for backward compatibility) + */ +export function show_quick_test_modal(controller, { l } = {}) { + // Destroy any existing instance + destroyCurrentInstance(); + + // Create new instance + currentQuickTestInstance = new QuickTestModal(controller, { l }); + currentQuickTestInstance.open(); +} + +// Legacy function exports for backward compatibility (used by HTML onclick handlers) +function markTestResult(testType, passed) { + if (currentQuickTestInstance) { + currentQuickTestInstance.markTestResult(testType, passed); + } +} + +function resetAllTests() { + if (currentQuickTestInstance) { + currentQuickTestInstance.resetAllTests(); + } +} + +// Legacy compatibility - expose functions to window for HTML onclick handlers +window.markTestResult = markTestResult; +window.resetAllTests = resetAllTests; \ No newline at end of file diff --git a/js/template-loader.js b/js/template-loader.js index 880e043..d3d5cbf 100644 --- a/js/template-loader.js +++ b/js/template-loader.js @@ -82,10 +82,11 @@ export async function loadAllTemplates() { const edgeProgressModalHtml = await loadTemplate('edge-progress-modal'); const edgeModalHtml = await loadTemplate('edge-modal'); const donateModalHtml = await loadTemplate('donate-modal'); + const quickTestModalHtml = await loadTemplate('quick-test-modal'); // Create modals container const modalsContainer = document.createElement('div'); modalsContainer.id = 'modals-container'; - modalsContainer.innerHTML = faqModalHtml + popupModalHtml + finetuneModalHtml + calibCenterModalHtml + welcomeModalHtml + calibrateModalHtml + rangeModalHtml + edgeProgressModalHtml + edgeModalHtml + donateModalHtml; + modalsContainer.innerHTML = faqModalHtml + popupModalHtml + finetuneModalHtml + calibCenterModalHtml + welcomeModalHtml + calibrateModalHtml + rangeModalHtml + edgeProgressModalHtml + edgeModalHtml + donateModalHtml + quickTestModalHtml; document.body.appendChild(modalsContainer); } diff --git a/templates/quick-test-modal.html b/templates/quick-test-modal.html new file mode 100644 index 0000000..713c7b2 --- /dev/null +++ b/templates/quick-test-modal.html @@ -0,0 +1,173 @@ + + + + + + Quick Test + + + + Run through these tests to verify your controller's functionality. Click on each test to expand and follow the instructions. + + + Press Square to begin + + + + + + + + + + USB Connector Test + Not tested + + + + + + This test checks the reliability of the USB port. + Instructions: Wiggle the USB cable to see if the controller disconnects. + + + Be gentle avoid damage. + + + + Pass + + + Fail + + + + + + + + + + + + + Haptic Vibration Test + Not tested + + + + + + This test will activate the controller's vibration motors for 3 seconds. + Instructions: Feel for vibration in the controller. + + + Pass + + + Fail + + + + + + + + + + + + + Adaptive Trigger Test + Not tested + + + + + + This test will enable heavy resistance on both L2 and R2 triggers. + Instructions: Press L2 and R2 triggers to feel the trigger resistance. + + + Pass + + + Fail + + + + + + + + + + + + + Speaker Test + Not tested + + + + + + This test will play a tone through the controller's built-in speaker. + Instructions: Listen for a tone from the controller speaker. + + + Pass + + + Fail + + + + + + + + + + + + + Microphone Test + Not tested + + + + + + This test will monitor the controller's microphone input levels. + Instructions: Blow gently into the controller's microphone. You should see the audio level indicator respond. + + Microphone Level: + + + + + + + Pass + + + Fail + + + + + + + + + Test Summary: + No tests completed yet. + + + + + + \ No newline at end of file
Run through these tests to verify your controller's functionality. Click on each test to expand and follow the instructions.
This test checks the reliability of the USB port.
Instructions: Wiggle the USB cable to see if the controller disconnects.
This test will activate the controller's vibration motors for 3 seconds.
Instructions: Feel for vibration in the controller.
This test will enable heavy resistance on both L2 and R2 triggers.
Instructions: Press L2 and R2 triggers to feel the trigger resistance.
This test will play a tone through the controller's built-in speaker.
Instructions: Listen for a tone from the controller speaker.
This test will monitor the controller's microphone input levels.
Instructions: Blow gently into the controller's microphone. You should see the audio level indicator respond.