-
Version 2.18 beta 1 (2025-10-02) - Support this project
+
Version 2.18 beta 2 (2025-10-03) - Support this project
diff --git a/js/controller-manager.js b/js/controller-manager.js
index 9addff9..12c3eba 100644
--- a/js/controller-manager.js
+++ b/js/controller-manager.js
@@ -1,6 +1,7 @@
'use strict';
import { sleep, la } from './utils.js';
+import { l } from './translations.js'
/**
* Controller Manager - Manages the current controller instance and provides unified interface
@@ -8,7 +9,6 @@ import { sleep, la } from './utils.js';
class ControllerManager {
constructor(uiDependencies = {}) {
this.currentController = null;
- this.l = uiDependencies.l;
this.handleNvStatusUpdate = uiDependencies.handleNvStatusUpdate;
this.has_changes_to_write = null;
this.inputHandler = null; // Callback function for input processing
@@ -109,6 +109,17 @@ class ControllerManager {
return this.currentController.getModel();
}
+ /**
+ * Get the list of supported quick tests for the current controller
+ * @returns {Array} Array of supported test types
+ */
+ getSupportedQuickTests() {
+ if (!this.currentController) {
+ return [];
+ }
+ return this.currentController.getSupportedQuickTests();
+ }
+
/**
* Check if a controller is connected
* @returns {boolean} True if controller is connected
@@ -183,7 +194,7 @@ class ControllerManager {
async nvsLock() {
const res = await this.currentController.nvsLock();
if (!res.ok) {
- throw new Error(this.l("NVS Lock failed"), { cause: res.error });
+ throw new Error(l("NVS Lock failed"), { cause: res.error });
}
await this.queryNvStatus(); // Refresh NVS status
@@ -196,7 +207,7 @@ class ControllerManager {
async calibrateSticksBegin() {
const res = await this.currentController.calibrateSticksBegin();
if (!res.ok) {
- throw new Error(`${this.l("Stick calibration failed")}. ${res.error?.message}`, { cause: res.error });
+ throw new Error(`${l("Stick calibration failed")}. ${res.error?.message}`, { cause: res.error });
}
}
@@ -207,7 +218,7 @@ class ControllerManager {
const res = await this.currentController.calibrateSticksSample();
if (!res.ok) {
await sleep(500);
- throw new Error(this.l("Stick calibration failed"), { cause: res.error });
+ throw new Error(l("Stick calibration failed"), { cause: res.error });
}
}
@@ -218,7 +229,7 @@ class ControllerManager {
const res = await this.currentController.calibrateSticksEnd();
if (!res.ok) {
await sleep(500);
- throw new Error(this.l("Stick calibration failed"), { cause: res.error });
+ throw new Error(l("Stick calibration failed"), { cause: res.error });
}
this.setHasChangesToWrite(true);
@@ -230,7 +241,7 @@ class ControllerManager {
async calibrateRangeBegin() {
const res = await this.currentController.calibrateRangeBegin();
if (!res.ok) {
- throw new Error(`${this.l("Stick calibration failed")}. ${res.error?.message}`, { cause: res.error });
+ throw new Error(`${l("Stick calibration failed")}. ${res.error?.message}`, { cause: res.error });
}
}
@@ -241,7 +252,7 @@ class ControllerManager {
const res = await this.currentController.calibrateRangeEnd();
if(res?.ok) {
this.setHasChangesToWrite(true);
- return { success: true, message: this.l("Range calibration completed") };
+ return { success: true, message: l("Range calibration completed") };
} else {
// Check if the error is code 3 (DS4/DS5) or codes 4/5 (DS5 Edge), which typically means
// the calibration was already ended or the controller is not in range calibration mode
@@ -254,7 +265,7 @@ class ControllerManager {
console.log("Range calibration end failed with unexpected error:", res);
await sleep(500);
- const msg = res?.code ? (`${this.l("Range calibration failed")}. ${this.l("Error")} ${res.code}`) : (`${this.l("Range calibration failed")}. ${res?.error || ""}`);
+ const msg = res?.code ? (`${l("Range calibration failed")}. ${l("Error")} ${res.code}`) : (`${l("Range calibration failed")}. ${res?.error || ""}`);
return { success: false, message: msg, error: res?.error };
}
}
@@ -286,7 +297,7 @@ class ControllerManager {
await this.calibrateSticksEnd();
progressCallback(100);
- return { success: true, message: this.l("Stick calibration completed") };
+ return { success: true, message: l("Stick calibration completed") };
} catch (e) {
la("multi_calibrate_sticks_failed", {"r": e});
throw e;
@@ -299,24 +310,24 @@ class ControllerManager {
*/
async disableLeftAdaptiveTrigger() {
if (!this.currentController) {
- throw new Error(this.l("No controller connected"));
+ throw new Error(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"));
+ throw new Error(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"));
+ throw new Error(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 });
+ throw new Error(l("Failed to disable adaptive trigger"), { cause: error });
}
}
@@ -345,7 +356,7 @@ class ControllerManager {
// 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"));
+ // throw new Error(l("Custom preset requires start, end, and force parameters"));
// }
// }
@@ -374,7 +385,7 @@ class ControllerManager {
}
} catch (error) {
if(duration) doneCb({ success: false});
- throw new Error(this.l("Failed to set vibration"), { cause: error });
+ throw new Error(l("Failed to set vibration"), { cause: error });
}
}
@@ -387,7 +398,7 @@ class ControllerManager {
async setSpeakerTone(duration = 1000, doneCb = ({success}) => {}, output = "speaker") {
try {
if (!this.currentController.setSpeakerTone) {
- throw new Error(this.l("Speaker tone not supported on this controller"));
+ throw new Error(l("Speaker tone not supported on this controller"));
}
await this.currentController.setSpeakerTone(output);
@@ -409,7 +420,7 @@ class ControllerManager {
}
} catch (error) {
if(duration) doneCb({ success: false});
- throw new Error(this.l("Failed to set speaker tone"), { cause: error });
+ throw new Error(l("Failed to set speaker tone"), { cause: error });
}
}
@@ -564,7 +575,7 @@ class ControllerManager {
*/
_batteryPercentToText({bat_capacity, is_charging, is_error}) {
if (is_error) {
- return '' + this.l("error") + '';
+ return '' + l("error") + '';
}
const batteryIcons = [
diff --git a/js/controllers/base-controller.js b/js/controllers/base-controller.js
index f15a7ae..868835a 100644
--- a/js/controllers/base-controller.js
+++ b/js/controllers/base-controller.js
@@ -4,13 +4,10 @@
* Base Controller class that provides common functionality for all controller types
*/
class BaseController {
- constructor(device, uiDependencies = {}) {
+ constructor(device) {
this.device = device;
this.model = "undefined"; // to be set by subclasses
this.finetuneMaxValue; // to be set by subclasses
-
- // UI dependencies injected from core
- this.l = uiDependencies.l;
}
getModel() {
@@ -184,6 +181,15 @@ class BaseController {
// Default no-op implementation for controllers that don't support player indicators
return { success: true, message: "This controller does not support player indicators" };
}
+
+ /**
+ * Get the list of supported quick tests for this controller
+ * @returns {Array} Array of supported test types
+ */
+ getSupportedQuickTests() {
+ // Default implementation - supports all tests
+ return ['usb', 'buttons', 'adaptive', 'haptic', 'lights', 'speaker', 'headphone', 'microphone'];
+ }
}
export default BaseController;
diff --git a/js/controllers/controller-factory.js b/js/controllers/controller-factory.js
index 274af6a..9047f82 100644
--- a/js/controllers/controller-factory.js
+++ b/js/controllers/controller-factory.js
@@ -21,20 +21,19 @@ class ControllerFactory {
/**
* Create a controller instance based on the HID device product ID
* @param {HIDDevice} device The HID device
- * @param {Object} uiDependencies Optional UI dependencies (l function, etc.)
* @returns {BaseController} The appropriate controller instance
*/
- static createControllerInstance(device, uiDependencies = {}) {
+ static createControllerInstance(device) {
switch (device.productId) {
case 0x05c4: // DS4 v1
case 0x09cc: // DS4 v2
- return new DS4Controller(device, uiDependencies);
+ return new DS4Controller(device);
case 0x0ce6: // DS5
- return new DS5Controller(device, uiDependencies);
+ return new DS5Controller(device);
case 0x0df2: // DS5 Edge
- return new DS5EdgeController(device, uiDependencies);
+ return new DS5EdgeController(device);
default:
throw new Error(`Unsupported device: ${dec2hex(device.vendorId)}:${dec2hex(device.productId)}`);
diff --git a/js/controllers/ds4-controller.js b/js/controllers/ds4-controller.js
index 37440d9..ad56fa8 100644
--- a/js/controllers/ds4-controller.js
+++ b/js/controllers/ds4-controller.js
@@ -10,6 +10,7 @@ import {
lf,
la
} from '../utils.js';
+import { l } from '../translations.js';
const NOT_GENUINE_SONY_CONTROLLER_MSG = "Your device might not be a genuine Sony controller. If it is not a clone then please report this issue.";
@@ -111,8 +112,8 @@ class DS4OutputStruct {
* DualShock 4 Controller implementation
*/
class DS4Controller extends BaseController {
- constructor(device, uiDependencies = {}) {
- super(device, uiDependencies);
+ constructor(device) {
+ super(device);
this.model = "DS4";
// Initialize current output state to track controller settings
@@ -134,8 +135,6 @@ class DS4Controller extends BaseController {
}
async getInfo() {
- const { l } = this;
-
// Device-only: collect info and return a common structure; do not touch the DOM
try {
let deviceTypeText = l("unknown");
@@ -204,9 +203,9 @@ class DS4Controller extends BaseController {
const lockRes = await this.nvsLock();
if(!lockRes.ok) throw (lockRes.error || new Error("NVS lock failed"));
- return { success: true, message: this.l("Changes saved successfully") };
+ return { success: true, message: l("Changes saved successfully") };
} catch(error) {
- throw new Error(this.l("Error while saving changes"), { cause: error });
+ throw new Error(l("Error while saving changes"), { cause: error });
}
}
@@ -258,7 +257,7 @@ class DS4Controller extends BaseController {
la("ds4_calibrate_range_begin_failed", {"d1": d1, "d2": d2});
return {
ok: false,
- error: new Error(this.l(NOT_GENUINE_SONY_CONTROLLER_MSG)),
+ error: new Error(l(NOT_GENUINE_SONY_CONTROLLER_MSG)),
code: 1, d1, d2
};
}
@@ -306,7 +305,7 @@ class DS4Controller extends BaseController {
la("ds4_calibrate_sticks_begin_failed", {"d1": d1, "d2": d2});
return {
ok: false,
- error: new Error(this.l(NOT_GENUINE_SONY_CONTROLLER_MSG)),
+ error: new Error(l(NOT_GENUINE_SONY_CONTROLLER_MSG)),
code: 1, d1, d2,
};
}
@@ -401,7 +400,7 @@ class DS4Controller extends BaseController {
} else {
if(this.isRare(hw_ver))
return "WOW!";
- return this.l("Unknown");
+ return l("Unknown");
}
}
@@ -689,6 +688,15 @@ class DS4Controller extends BaseController {
throw new Error('WebRTC getUserMedia API or mediaDevices enumeration not available.');
}
}
+
+ /**
+ * Get the list of supported quick tests for DS4 controller
+ * DS4 does not support adaptive triggers, speaker, or microphone
+ * @returns {Array} Array of supported test types
+ */
+ getSupportedQuickTests() {
+ return ['usb', 'buttons', 'haptic', 'lights', 'headphone'];
+ }
}
export default DS4Controller;
diff --git a/js/controllers/ds5-controller.js b/js/controllers/ds5-controller.js
index 1ff2bfd..422d6c0 100644
--- a/js/controllers/ds5-controller.js
+++ b/js/controllers/ds5-controller.js
@@ -12,6 +12,7 @@ import {
la,
lf
} from '../utils.js';
+import { l } from '../translations.js';
// DS5 Button mapping configuration
const DS5_BUTTON_MAP = [
@@ -225,8 +226,8 @@ function ds5_color(x) {
* DualSense (DS5) Controller implementation
*/
class DS5Controller extends BaseController {
- constructor(device, uiDependencies = {}) {
- super(device, uiDependencies);
+ constructor(device) {
+ super(device);
this.model = "DS5";
this.finetuneMaxValue = 65535; // 16-bit max value for DS5
@@ -271,7 +272,6 @@ class DS5Controller extends BaseController {
}
async _getInfo(is_edge) {
- const { l } = this;
// Device-only: collect info and return a common structure; do not touch the DOM
try {
console.log("Fetching DS5 info...");
@@ -352,9 +352,9 @@ class DS5Controller extends BaseController {
const lockRes = await this.nvsLock();
if(!lockRes.ok) throw (lockRes.error || new Error("NVS lock failed"));
- return { success: true, message: this.l("Changes saved successfully") };
+ return { success: true, message: l("Changes saved successfully") };
} catch(error) {
- throw new Error(this.l("Error while saving changes"), { cause: error });
+ throw new Error(l("Error while saving changes"), { cause: error });
}
}
@@ -384,7 +384,7 @@ class DS5Controller extends BaseController {
const data = await this.receiveFeatureReport(0x81);
} catch(error) {
await sleep(500);
- throw new Error(this.l("NVS Unlock failed"), { cause: error });
+ throw new Error(l("NVS Unlock failed"), { cause: error });
}
}
@@ -398,7 +398,7 @@ class DS5Controller extends BaseController {
await this.sendFeatureReport(128, [base,num])
const pcba_id = lf("ds5_pcba_id", await this.receiveFeatureReport(129));
if(pcba_id.getUint8(1) != base || pcba_id.getUint8(2) != num || pcba_id.getUint8(3) != 2) {
- return this.l("error");
+ return l("error");
}
if(decode)
return new TextDecoder().decode(pcba_id.buffer.slice(4, 4+length));
@@ -539,7 +539,7 @@ class DS5Controller extends BaseController {
if(a == 0x05) return "BDM-030";
if(a == 0x06) return "BDM-040";
if(a == 0x07 || a == 0x08) return "BDM-050";
- return this.l("Unknown");
+ return l("Unknown");
}
async getInMemoryModuleData() {
diff --git a/js/controllers/ds5-edge-controller.js b/js/controllers/ds5-edge-controller.js
index 991fac9..363d52c 100644
--- a/js/controllers/ds5-edge-controller.js
+++ b/js/controllers/ds5-edge-controller.js
@@ -2,20 +2,19 @@
import DS5Controller from './ds5-controller.js';
import { sleep, dec2hex32, la, lf } from '../utils.js';
+import { l } from "../translations.js";
/**
* DualSense Edge (DS5 Edge) Controller implementation
*/
class DS5EdgeController extends DS5Controller {
- constructor(device, uiDependencies = {}) {
- super(device, uiDependencies);
+ constructor(device) {
+ super(device);
this.model = "DS5_Edge";
this.finetuneMaxValue = 4095; // 12-bit max value for DS5 Edge
}
async getInfo() {
- const { l } = this;
-
// DS5 Edge uses the same info structure as DS5 but with is_edge=true
const result = await this._getInfo(true);
@@ -41,12 +40,12 @@ class DS5EdgeController extends DS5Controller {
if(ret) {
return {
success: true,
- message: "" + this.l("Changes saved successfully") + ".
" + this.l("If the calibration is not stored permanently, please double-check the wirings of the hardware mod."),
+ message: "" + l("Changes saved successfully") + ".
" + l("If the calibration is not stored permanently, please double-check the wirings of the hardware mod."),
isHtml: true
};
}
} catch(error) {
- throw new Error(this.l("Error while saving changes"), { cause: error });
+ throw new Error(l("Error while saving changes"), { cause: error });
}
}
@@ -68,7 +67,7 @@ class DS5EdgeController extends DS5Controller {
await sleep(200);
const ret = await this.waitUntilWritten([21, 6, 2]);
if(!ret) {
- throw new Error(this.l("Cannot unlock") + " " + this.l(m_name));
+ throw new Error(l("Cannot unlock") + " " + l(m_name));
}
}
@@ -79,7 +78,7 @@ class DS5EdgeController extends DS5Controller {
await sleep(200);
const ret = await this.waitUntilWritten([21, 4, 2]);
if(!ret) {
- throw new Error(this.l("Cannot lock") + " " + this.l(m_name));
+ throw new Error(l("Cannot lock") + " " + l(m_name));
}
}
@@ -90,7 +89,7 @@ class DS5EdgeController extends DS5Controller {
await sleep(200);
const ret = await this.waitUntilWritten([21, 3, 2]);
if(!ret) {
- throw new Error(this.l("Cannot store data into") + " " + this.l(m_name));
+ throw new Error(l("Cannot store data into") + " " + l(m_name));
}
}
diff --git a/js/core.js b/js/core.js
index 2722005..a09c8c8 100644
--- a/js/core.js
+++ b/js/core.js
@@ -141,7 +141,7 @@ async function connect() {
initAnalyticsApi(app); // init with gu and jg
// Initialize controller manager with translation function
- controller = initControllerManager({ l, handleNvStatusUpdate });
+ controller = initControllerManager({ handleNvStatusUpdate });
controller.setInputHandler(handleControllerInput);
la("begin");
@@ -224,7 +224,7 @@ async function continue_connection({data, device}) {
try {
// Create controller instance using factory
- controllerInstance = ControllerFactory.createControllerInstance(device, { l });
+ controllerInstance = ControllerFactory.createControllerInstance(device);
controller.setControllerInstance(controllerInstance);
info = await controllerInstance.getInfo();
@@ -1058,7 +1058,7 @@ window.show_donate_modal = show_donate_modal;
window.board_model_info = board_model_info;
window.edge_color_info = edge_color_info;
window.show_quick_test_modal = () => {
- show_quick_test_modal(controller, { l }).catch(error => {
+ show_quick_test_modal(controller).catch(error => {
throw new Error("Failed to show quick test modal", { cause: error });
});
};
diff --git a/js/modals/quick-test-modal.js b/js/modals/quick-test-modal.js
index c08ef84..3dcc18b 100644
--- a/js/modals/quick-test-modal.js
+++ b/js/modals/quick-test-modal.js
@@ -1,5 +1,7 @@
'use strict';
+import { l } from '../translations.js';
+
const TEST_SEQUENCE = ['usb', 'buttons', 'adaptive', 'haptic', 'lights', 'speaker', 'headphone', 'microphone'];
const TEST_NAMES = {
'usb': 'USB Connector',
@@ -39,9 +41,8 @@ const BUTTON_INFILL_MAPPING = {
* Handles controller feature testing including haptic feedback, adaptive triggers, speaker, and microphone functionality
*/
export class QuickTestModal {
- constructor(controllerInstance, { l }) {
+ constructor(controllerInstance) {
this.controller = controllerInstance;
- this.l = l;
this.resetAllTests();
@@ -119,8 +120,9 @@ export class QuickTestModal {
/**
* Apply skipped tests to the UI (rebuild accordion with non-skipped tests)
*/
- _applySkippedTestsToUI() {
+ async _applySkippedTestsToUI() {
this._buildDynamicAccordion();
+ await this._initSvgController();
this._updateSkippedTestsDropdown();
}
@@ -131,14 +133,13 @@ export class QuickTestModal {
const $accordion = $('#quickTestAccordion');
$accordion.empty();
- // Get non-skipped tests in order
- let activeTests = TEST_SEQUENCE.filter(testType => !this.state.skippedTests.includes(testType));
+ // Get supported tests from the controller
+ const supportedTests = this.controller.getSupportedQuickTests();
- // Filter out unsupported tests for DS4 controllers
- if (this.controller.getModel() === 'DS4') {
- const ds4UnsupportedTests = ['adaptive', 'speaker', 'microphone'];
- activeTests = activeTests.filter(testType => !ds4UnsupportedTests.includes(testType));
- }
+ // Get non-skipped tests in order, filtered by what the controller supports
+ let activeTests = TEST_SEQUENCE.filter(testType =>
+ !this.state.skippedTests.includes(testType) && supportedTests.includes(testType)
+ );
activeTests.forEach(testType => {
const accordionItem = this._createAccordionItem(testType);
@@ -223,7 +224,7 @@ export class QuickTestModal {