mirror of
https://github.com/dualshock-tools/dualshock-tools.github.io.git
synced 2026-03-01 11:19:54 +03:00
Refactor into separate files with abstractions for controller types
This commit is contained in:
committed by
dualshock-tools
parent
f82cdcf663
commit
d4ba4a5fdd
127
controllers/base-controller.js
Normal file
127
controllers/base-controller.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base Controller class that provides common functionality for all controller types
|
||||||
|
*/
|
||||||
|
class BaseController {
|
||||||
|
constructor(device, uiDependencies = {}) {
|
||||||
|
this.device = device;
|
||||||
|
this.type = "undefined"; // to be set by subclasses
|
||||||
|
|
||||||
|
// UI dependencies injected from core
|
||||||
|
this.l = uiDependencies.l;
|
||||||
|
}
|
||||||
|
|
||||||
|
getType() {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the underlying HID device
|
||||||
|
* @returns {HIDDevice} The HID device
|
||||||
|
*/
|
||||||
|
getDevice() {
|
||||||
|
return this.device;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputConfig() {
|
||||||
|
throw new Error('getInputConfig() must be implemented by subclass');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set input report handler
|
||||||
|
* @param {Function} handler Input report handler function
|
||||||
|
*/
|
||||||
|
setInputReportHandler(handler) {
|
||||||
|
this.device.oninputreport = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate request buffer with proper size based on device feature reports
|
||||||
|
* @param {number} id Report ID
|
||||||
|
* @param {Array} data Data array to include in the request
|
||||||
|
* @returns {Uint8Array} Allocated request buffer
|
||||||
|
*/
|
||||||
|
alloc_req(id, data = []) {
|
||||||
|
const fr = this.device.collections[0].featureReports;
|
||||||
|
const report = fr.find(e => e.reportId === id);
|
||||||
|
const maxLen = report?.reportCount || data.length;
|
||||||
|
|
||||||
|
const out = new Uint8Array(data.length);
|
||||||
|
out.set(data.slice(0, maxLen));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send feature report to device
|
||||||
|
* @param {number} reportId Report ID
|
||||||
|
* @param {ArrayBuffer|Array} data Data to send (if Array, will be processed through allocReq)
|
||||||
|
*/
|
||||||
|
async sendFeatureReport(reportId, data) {
|
||||||
|
// If data is an array, use allocReq to create proper buffer
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
data = this.alloc_req(reportId, data);
|
||||||
|
}
|
||||||
|
return await this.device.sendFeatureReport(reportId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive feature report from device
|
||||||
|
* @param {number} reportId Report ID
|
||||||
|
*/
|
||||||
|
async receiveFeatureReport(reportId) {
|
||||||
|
return await this.device.receiveFeatureReport(reportId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the HID device connection
|
||||||
|
*/
|
||||||
|
async close() {
|
||||||
|
if (this.device && this.device.opened) {
|
||||||
|
await this.device.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abstract methods that must be implemented by subclasses
|
||||||
|
async getInfo() {
|
||||||
|
throw new Error('getInfo() must be implemented by subclass');
|
||||||
|
}
|
||||||
|
|
||||||
|
async flash(progressCallback = null) {
|
||||||
|
throw new Error('flash() must be implemented by subclass');
|
||||||
|
}
|
||||||
|
|
||||||
|
async reset() {
|
||||||
|
throw new Error('reset() must be implemented by subclass');
|
||||||
|
}
|
||||||
|
|
||||||
|
async nvsLock() {
|
||||||
|
throw new Error('nvsLock() must be implemented by subclass');
|
||||||
|
}
|
||||||
|
|
||||||
|
async nvsUnlock() {
|
||||||
|
throw new Error('nvsUnlock() must be implemented by subclass');
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateSticksBegin() {
|
||||||
|
throw new Error('calibrateSticksBegin() must be implemented by subclass');
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateSticksEnd() {
|
||||||
|
throw new Error('calibrateSticksEnd() must be implemented by subclass');
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateSticksSample() {
|
||||||
|
throw new Error('calibrateSticksSample() must be implemented by subclass');
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateRangeBegin() {
|
||||||
|
throw new Error('calibrateRangeBegin() must be implemented by subclass');
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateRangeEnd() {
|
||||||
|
throw new Error('calibrateRangeEnd() must be implemented by subclass');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseController;
|
||||||
101
controllers/controller-factory.js
Normal file
101
controllers/controller-factory.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import DS4Controller from './ds4-controller.js';
|
||||||
|
import DS5Controller from './ds5-controller.js';
|
||||||
|
import DS5EdgeController from './ds5-edge-controller.js';
|
||||||
|
import { dec2hex } from '../utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller Factory - Creates the appropriate controller instance based on device type
|
||||||
|
*/
|
||||||
|
class ControllerFactory {
|
||||||
|
static getSupportedModels() {
|
||||||
|
const ds4v1 = { vendorId: 0x054c, productId: 0x05c4 };
|
||||||
|
const ds4v2 = { vendorId: 0x054c, productId: 0x09cc };
|
||||||
|
const ds5 = { vendorId: 0x054c, productId: 0x0ce6 };
|
||||||
|
const ds5edge = { vendorId: 0x054c, productId: 0x0df2 };
|
||||||
|
return [ds4v1, ds4v2, ds5, ds5edge];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = {}) {
|
||||||
|
switch (device.productId) {
|
||||||
|
case 0x05c4: // DS4 v1
|
||||||
|
case 0x09cc: // DS4 v2
|
||||||
|
return new DS4Controller(device, uiDependencies);
|
||||||
|
|
||||||
|
case 0x0ce6: // DS5
|
||||||
|
return new DS5Controller(device, uiDependencies);
|
||||||
|
|
||||||
|
case 0x0df2: // DS5 Edge
|
||||||
|
return new DS5EdgeController(device, uiDependencies);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported device: ${dec2hex(device.vendorId)}:${dec2hex(device.productId)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get device name based on product ID
|
||||||
|
* @param {number} productId Product ID
|
||||||
|
* @returns {string} Device name
|
||||||
|
*/
|
||||||
|
static getDeviceName(productId) {
|
||||||
|
switch (productId) {
|
||||||
|
case 0x05c4:
|
||||||
|
return "Sony DualShock 4 V1";
|
||||||
|
case 0x09cc:
|
||||||
|
return "Sony DualShock 4 V2";
|
||||||
|
case 0x0ce6:
|
||||||
|
return "Sony DualSense";
|
||||||
|
case 0x0df2:
|
||||||
|
return "Sony DualSense Edge";
|
||||||
|
default:
|
||||||
|
return "Unknown Device";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get UI configuration based on product ID
|
||||||
|
* @param {number} productId Product ID
|
||||||
|
* @returns {Object} UI configuration
|
||||||
|
*/
|
||||||
|
static getUIConfig(productId) {
|
||||||
|
switch (productId) {
|
||||||
|
case 0x05c4: // DS4 v1
|
||||||
|
case 0x09cc: // DS4 v2
|
||||||
|
return {
|
||||||
|
showInfo: false,
|
||||||
|
showFinetune: false,
|
||||||
|
showMute: false,
|
||||||
|
showInfoTab: false
|
||||||
|
};
|
||||||
|
|
||||||
|
case 0x0ce6: // DS5
|
||||||
|
case 0x0df2: // DS5 Edge
|
||||||
|
return {
|
||||||
|
showInfo: true,
|
||||||
|
showFinetune: true,
|
||||||
|
showMute: true,
|
||||||
|
showInfoTab: true
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
showInfo: false,
|
||||||
|
showFinetune: false,
|
||||||
|
showMute: false,
|
||||||
|
showInfoTab: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for use in other modules
|
||||||
|
export default ControllerFactory;
|
||||||
560
controllers/controller-manager.js
Normal file
560
controllers/controller-manager.js
Normal file
@@ -0,0 +1,560 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { sleep, la } from '../utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller Manager - Manages the current controller instance and provides unified interface
|
||||||
|
*/
|
||||||
|
class ControllerManager {
|
||||||
|
constructor(uiDependencies = {}) {
|
||||||
|
this.currentController = null;
|
||||||
|
this.l = uiDependencies.l || ((text) => text); // fallback to identity function
|
||||||
|
this.has_changes_to_write = null;
|
||||||
|
this.inputHandler = null; // Callback function for input processing
|
||||||
|
|
||||||
|
// Button and stick states for UI updates
|
||||||
|
this.button_states = {
|
||||||
|
// e.g. 'square': false, 'cross': false, ...
|
||||||
|
sticks: {
|
||||||
|
left: {
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Touch points for touchpad input
|
||||||
|
this.touchPoints = [];
|
||||||
|
|
||||||
|
// Battery status tracking
|
||||||
|
this.batteryStatus = {
|
||||||
|
bat_txt: "",
|
||||||
|
changed: false,
|
||||||
|
bat_capacity: 0,
|
||||||
|
cable_connected: false,
|
||||||
|
is_charging: false,
|
||||||
|
is_error: false
|
||||||
|
};
|
||||||
|
this._lastBatteryText = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current controller instance
|
||||||
|
* @param {BaseController} controller Controller instance
|
||||||
|
*/
|
||||||
|
setControllerInstance(instance) {
|
||||||
|
this.currentController = instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current device (for backward compatibility)
|
||||||
|
* @returns {HIDDevice|null} Current device or null if none set
|
||||||
|
*/
|
||||||
|
getDevice() {
|
||||||
|
return this.currentController?.getDevice() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputConfig() {
|
||||||
|
return this.currentController.getInputConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeviceInfo() {
|
||||||
|
return await this.currentController.getInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set input report handler on the underlying device
|
||||||
|
* @param {Function|null} handler Input report handler function or null to clear
|
||||||
|
*/
|
||||||
|
setInputReportHandler(handler) {
|
||||||
|
this.currentController.device.oninputreport = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query NVS (Non-Volatile Storage) status
|
||||||
|
* @returns {Promise<Object>} NVS status object
|
||||||
|
*/
|
||||||
|
async queryNvStatus() {
|
||||||
|
return await this.currentController.queryNvStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get in-memory module data (finetune data)
|
||||||
|
* @returns {Promise<Array>} Module data array
|
||||||
|
*/
|
||||||
|
async getInMemoryModuleData() {
|
||||||
|
return await this.currentController.getInMemoryModuleData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write finetune data to controller
|
||||||
|
* @param {Array} data Finetune data array
|
||||||
|
*/
|
||||||
|
async writeFinetuneData(data) {
|
||||||
|
await this.currentController.writeFinetuneData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
controllerType() {
|
||||||
|
return this.currentController.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a controller is connected
|
||||||
|
* @returns {boolean} True if controller is connected
|
||||||
|
*/
|
||||||
|
isConnected() {
|
||||||
|
return this.currentController !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the input callback function
|
||||||
|
* @param {Function} callback - Function to call after processing input
|
||||||
|
*/
|
||||||
|
setInputHandler(callback) {
|
||||||
|
this.inputHandler = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect the current controller
|
||||||
|
*/
|
||||||
|
async disconnect() {
|
||||||
|
if (this.currentController) {
|
||||||
|
await this.currentController.close();
|
||||||
|
this.currentController = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update NVS changes status and UI
|
||||||
|
* @param {boolean} new_value Changes status
|
||||||
|
*/
|
||||||
|
setHasChangesToWrite(new_value) {
|
||||||
|
if (new_value === this.has_changes_to_write)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (new_value == true) {
|
||||||
|
$("#savechanges").prop("disabled", false);
|
||||||
|
$("#savechanges").addClass("btn-success").removeClass("btn-outline-secondary");
|
||||||
|
} else {
|
||||||
|
$("#savechanges").prop("disabled", true);
|
||||||
|
$("#savechanges").removeClass("btn-success").addClass("btn-outline-secondary");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.has_changes_to_write = new_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unified controller operations that delegate to the current controller
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flash/save changes to the controller
|
||||||
|
*/
|
||||||
|
async flash(progressCallback = null) {
|
||||||
|
const result = await this.currentController.flash(progressCallback);
|
||||||
|
this.setHasChangesToWrite(false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the controller
|
||||||
|
*/
|
||||||
|
async reset() {
|
||||||
|
await this.currentController.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlock NVS (Non-Volatile Storage)
|
||||||
|
*/
|
||||||
|
async nvsUnlock() {
|
||||||
|
await this.currentController.nvsUnlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock NVS (Non-Volatile Storage)
|
||||||
|
*/
|
||||||
|
async nvsLock() {
|
||||||
|
const res = await this.currentController.nvsLock();
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(this.l("NVS Lock failed: ") + String(res.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin stick calibration
|
||||||
|
*/
|
||||||
|
async calibrateSticksBegin() {
|
||||||
|
const res = await this.currentController.calibrateSticksBegin();
|
||||||
|
if (!res.ok) {
|
||||||
|
const detail = res.code ? (this.l("Error ") + String(res.code)) : String(res.error || "");
|
||||||
|
throw new Error(this.l("Stick calibration failed: ") + detail);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End stick calibration
|
||||||
|
*/
|
||||||
|
async calibrateSticksEnd() {
|
||||||
|
const res = await this.currentController.calibrateSticksEnd();
|
||||||
|
if (!res.ok) {
|
||||||
|
await sleep(500);
|
||||||
|
const detail = res.code ? (this.l("Error ") + String(res.code)) : String(res.error || "");
|
||||||
|
throw new Error(this.l("Stick calibration failed: ") + detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setHasChangesToWrite(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample stick position during calibration
|
||||||
|
*/
|
||||||
|
async calibrateSticksSample() {
|
||||||
|
const res = await this.currentController.calibrateSticksSample();
|
||||||
|
if (!res.ok) {
|
||||||
|
await sleep(500);
|
||||||
|
const detail = res.code ? (this.l("Error ") + String(res.code)) : String(res.error || "");
|
||||||
|
throw new Error(this.l("Stick calibration failed: ") + detail);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin stick range calibration (for UI-driven calibration)
|
||||||
|
*/
|
||||||
|
async calibrateRangeBegin() {
|
||||||
|
const ret = await this.currentController.calibrateRangeBegin();
|
||||||
|
if (!ret.ok) {
|
||||||
|
const detail = ret.code ? (this.l("Error ") + String(ret.code)) : String(ret.error || "");
|
||||||
|
throw new Error(this.l("Range calibration failed: ") + detail);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle range calibration on close
|
||||||
|
*/
|
||||||
|
async calibrateRangeOnClose() {
|
||||||
|
const res = await this.currentController.calibrateRangeEnd();
|
||||||
|
if(res?.ok) {
|
||||||
|
this.setHasChangesToWrite(true);
|
||||||
|
return { success: true, message: this.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
|
||||||
|
if (res?.code === 3 || res?.code === 4 || res?.code === 5) {
|
||||||
|
console.log("Range calibration end returned expected error code", res.code, "- treating as successful completion");
|
||||||
|
// This is likely not an error - the calibration may have already been completed
|
||||||
|
// or the user closed the window without starting calibration
|
||||||
|
return { success: true, message: this.l("Range calibration window closed") };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ") + String(res.code)) : (this.l("Range calibration failed: ") + String(res?.error || ""));
|
||||||
|
return { success: false, message: msg };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full stick calibration process ("OLD" fully automated calibration)
|
||||||
|
* @param {Function} progressCallback - Callback function to report progress (0-100)
|
||||||
|
*/
|
||||||
|
async calibrateSticks(progressCallback) {
|
||||||
|
try {
|
||||||
|
la("multi_calibrate_sticks");
|
||||||
|
|
||||||
|
progressCallback(20);
|
||||||
|
|
||||||
|
const okBegin = await this.calibrateSticksBegin();
|
||||||
|
if (!okBegin) {
|
||||||
|
return { success: false, message: this.l("Stick calibration failed to begin") };
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCallback(30);
|
||||||
|
|
||||||
|
// Sample multiple times during the process
|
||||||
|
const sampleCount = 5;
|
||||||
|
for (let i = 0; i < sampleCount; i++) {
|
||||||
|
await sleep(100);
|
||||||
|
const okSample = await this.calibrateSticksSample();
|
||||||
|
if (!okSample) {
|
||||||
|
return { success: false, message: this.l("Stick calibration sampling failed") };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress from 30% to 80% during sampling
|
||||||
|
const sampleProgress = 30 + ((i + 1) / sampleCount) * 50;
|
||||||
|
progressCallback(Math.round(sampleProgress));
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCallback(90);
|
||||||
|
|
||||||
|
const okEnd = await this.calibrateSticksEnd();
|
||||||
|
if (!okEnd) {
|
||||||
|
return { success: false, message: this.l("Stick calibration failed to complete") };
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCallback(100);
|
||||||
|
return { success: true, message: this.l("Stick calibration completed") };
|
||||||
|
} catch (e) {
|
||||||
|
la("multi_calibrate_sticks_failed", {"r": e});
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to check if stick positions have changed
|
||||||
|
*/
|
||||||
|
sticksChanged(current, newValues) {
|
||||||
|
return current.left.x !== newValues.left.x || current.left.y !== newValues.left.y ||
|
||||||
|
current.right.x !== newValues.right.x || current.right.y !== newValues.right.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic button processing for DS4/DS5
|
||||||
|
* Records button states and returns changes
|
||||||
|
*/
|
||||||
|
recordButtonStates(data, BUTTON_MAP, dpad_byte, l2_analog_byte, r2_analog_byte) {
|
||||||
|
const changes = {};
|
||||||
|
|
||||||
|
// Stick positions (always at bytes 0-3)
|
||||||
|
const [new_lx, new_ly, new_rx, new_ry] = [0, 1, 2, 3]
|
||||||
|
.map(i => data.getUint8(i))
|
||||||
|
.map(v => Math.round((v - 127.5) / 128 * 100) / 100);
|
||||||
|
|
||||||
|
const newSticks = {
|
||||||
|
left: { x: new_lx, y: new_ly },
|
||||||
|
right: { x: new_rx, y: new_ry }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.sticksChanged(this.button_states.sticks, newSticks)) {
|
||||||
|
this.button_states.sticks = newSticks;
|
||||||
|
changes.sticks = newSticks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// L2/R2 analog values
|
||||||
|
[
|
||||||
|
['l2', l2_analog_byte],
|
||||||
|
['r2', r2_analog_byte]
|
||||||
|
].forEach(([name, byte]) => {
|
||||||
|
const val = data.getUint8(byte);
|
||||||
|
const key = name + '_analog';
|
||||||
|
if (val !== this.button_states[key]) {
|
||||||
|
this.button_states[key] = val;
|
||||||
|
changes[key] = val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dpad is a 4-bit hat value
|
||||||
|
const hat = data.getUint8(dpad_byte) & 0x0F;
|
||||||
|
const dpad_map = {
|
||||||
|
up: (hat === 0 || hat === 1 || hat === 7),
|
||||||
|
right: (hat === 1 || hat === 2 || hat === 3),
|
||||||
|
down: (hat === 3 || hat === 4 || hat === 5),
|
||||||
|
left: (hat === 5 || hat === 6 || hat === 7)
|
||||||
|
};
|
||||||
|
for (const dir of ['up', 'right', 'down', 'left']) {
|
||||||
|
const pressed = dpad_map[dir];
|
||||||
|
if (this.button_states[dir] !== pressed) {
|
||||||
|
this.button_states[dir] = pressed;
|
||||||
|
changes[dir] = pressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other buttons
|
||||||
|
for (const btn of BUTTON_MAP) {
|
||||||
|
if (['up', 'right', 'down', 'left'].includes(btn.name)) continue; // Dpad handled above
|
||||||
|
const pressed = (data.getUint8(btn.byte) & btn.mask) !== 0;
|
||||||
|
if (this.button_states[btn.name] !== pressed) {
|
||||||
|
this.button_states[btn.name] = pressed;
|
||||||
|
changes[btn.name] = pressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process controller input data and call callback if set
|
||||||
|
* This is the first part of the split process_controller_input function
|
||||||
|
* @param {Object} inputData - The input data from the controller
|
||||||
|
* @returns {Object} Changes object containing processed input data
|
||||||
|
*/
|
||||||
|
processControllerInput(inputData) {
|
||||||
|
const { data } = inputData;
|
||||||
|
|
||||||
|
const inputConfig = this.currentController.getInputConfig();
|
||||||
|
const { buttonMap, dpadByte, l2AnalogByte, r2AnalogByte } = inputConfig;
|
||||||
|
const { touchpadOffset, batteryByte, isDS4 } = inputConfig;
|
||||||
|
|
||||||
|
// Process button states using the device-specific configuration
|
||||||
|
const changes = this.recordButtonStates(data, buttonMap, dpadByte, l2AnalogByte, r2AnalogByte);
|
||||||
|
|
||||||
|
// Parse and store touch points if touchpad data is available
|
||||||
|
if (touchpadOffset) {
|
||||||
|
this.touchPoints = this.parseTouchPoints(data, touchpadOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and store battery status if battery data is available
|
||||||
|
this.batteryStatus = this.parseBatteryStatus(data, batteryByte, isDS4);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
changes,
|
||||||
|
inputConfig: { buttonMap, isDS4 },
|
||||||
|
touchPoints: this.touchPoints,
|
||||||
|
batteryStatus: this.batteryStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.inputHandler(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse touch points from input data
|
||||||
|
* @param {DataView} data - Input data view
|
||||||
|
* @param {number} offset - Offset to touchpad data
|
||||||
|
* @returns {Array} Array of touch points with {active, id, x, y} properties
|
||||||
|
*/
|
||||||
|
parseTouchPoints(data, offset) {
|
||||||
|
// Returns array of up to 2 points: {active, id, x, y}
|
||||||
|
const points = [];
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
const base = offset + i * 4;
|
||||||
|
const arr = [];
|
||||||
|
for (let j = 0; j < 4; j++) arr.push(data.getUint8(base + j));
|
||||||
|
const b0 = data.getUint8(base);
|
||||||
|
const active = (b0 & 0x80) === 0; // 0 = finger down, 1 = up
|
||||||
|
const id = b0 & 0x7F;
|
||||||
|
const b1 = data.getUint8(base + 1);
|
||||||
|
const b2 = data.getUint8(base + 2);
|
||||||
|
const b3 = data.getUint8(base + 3);
|
||||||
|
// x: 12 bits, y: 12 bits
|
||||||
|
const x = ((b2 & 0x0F) << 8) | b1;
|
||||||
|
const y = (b3 << 4) | (b2 >> 4);
|
||||||
|
points.push({ active, id, x, y });
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse battery status from input data
|
||||||
|
* @param {DataView} data - Input data view
|
||||||
|
* @param {number} byte - Byte offset for battery data
|
||||||
|
* @param {boolean} isDS4 - Whether this is a DS4 controller
|
||||||
|
* @returns {Object} Battery status object with bat_txt, changed, bat_capacity, etc.
|
||||||
|
*/
|
||||||
|
parseBatteryStatus(data, byte, isDS4 = false) {
|
||||||
|
const bat = data.getUint8(byte);
|
||||||
|
let bat_capacity = 0, cable_connected = false, is_charging = false, is_error = false;
|
||||||
|
|
||||||
|
if (isDS4) {
|
||||||
|
// DS4: bat_data = low 4 bits, bat_status = bit 4
|
||||||
|
const bat_data = bat & 0x0f;
|
||||||
|
const bat_status = (bat >> 4) & 1;
|
||||||
|
if (bat_status == 1) {
|
||||||
|
cable_connected = true;
|
||||||
|
if (bat_data < 10) {
|
||||||
|
bat_capacity = Math.min(bat_data * 10 + 5, 100);
|
||||||
|
is_charging = true;
|
||||||
|
} else if (bat_data == 10) {
|
||||||
|
bat_capacity = 100;
|
||||||
|
is_charging = true;
|
||||||
|
} else if (bat_data == 11) {
|
||||||
|
bat_capacity = 100;
|
||||||
|
// charged
|
||||||
|
} else {
|
||||||
|
bat_capacity = 0;
|
||||||
|
is_error = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cable_connected = false;
|
||||||
|
if (bat_data < 10) {
|
||||||
|
bat_capacity = bat_data * 10 + 5;
|
||||||
|
} else {
|
||||||
|
bat_capacity = 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// DS5: bat_charge = low 4 bits, bat_status = high 4 bits
|
||||||
|
const bat_charge = bat & 0x0f;
|
||||||
|
const bat_status = bat >> 4;
|
||||||
|
if (bat_status == 0) {
|
||||||
|
bat_capacity = Math.min(bat_charge * 10 + 5, 100);
|
||||||
|
} else if (bat_status == 1) {
|
||||||
|
bat_capacity = Math.min(bat_charge * 10 + 5, 100);
|
||||||
|
is_charging = true;
|
||||||
|
cable_connected = true;
|
||||||
|
} else if (bat_status == 2) {
|
||||||
|
bat_capacity = 100;
|
||||||
|
cable_connected = true;
|
||||||
|
} else {
|
||||||
|
is_error = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate battery text with icons
|
||||||
|
const bat_txt = this.batteryPercentToText(bat_capacity, is_charging, is_error);
|
||||||
|
|
||||||
|
// Check if battery text has changed
|
||||||
|
const changed = bat_txt !== this._lastBatteryText;
|
||||||
|
this._lastBatteryText = bat_txt;
|
||||||
|
|
||||||
|
// Update internal battery status
|
||||||
|
const batteryStatus = {
|
||||||
|
bat_txt,
|
||||||
|
changed,
|
||||||
|
bat_capacity,
|
||||||
|
cable_connected,
|
||||||
|
is_charging,
|
||||||
|
is_error
|
||||||
|
};
|
||||||
|
|
||||||
|
return batteryStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert battery percentage to display text with icons
|
||||||
|
* @param {number} bat_charge - Battery charge percentage
|
||||||
|
* @param {boolean} is_charging - Whether battery is charging
|
||||||
|
* @param {boolean} is_error - Whether there's a battery error
|
||||||
|
* @returns {string} HTML string with battery status and icons
|
||||||
|
*/
|
||||||
|
batteryPercentToText(bat_charge, is_charging, is_error) {
|
||||||
|
if (is_error) {
|
||||||
|
return '<font color="red">' + this.l("error") + '</font>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const batteryIcons = [
|
||||||
|
{ threshold: 20, icon: 'fa-battery-empty' },
|
||||||
|
{ threshold: 40, icon: 'fa-battery-quarter' },
|
||||||
|
{ threshold: 60, icon: 'fa-battery-half' },
|
||||||
|
{ threshold: 80, icon: 'fa-battery-three-quarters' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const icon_txt = batteryIcons.find(item => bat_charge < item.threshold)?.icon || 'fa-battery-full';
|
||||||
|
const icon_full = '<i class="fa-solid ' + icon_txt + '"></i>';
|
||||||
|
const bolt_txt = is_charging ? '<i class="fa-solid fa-bolt"></i>' : '';
|
||||||
|
return bat_charge + "%" + ' ' + bolt_txt + ' ' + icon_full;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a bound input handler function that can be assigned to device.oninputreport
|
||||||
|
* @returns {Function} Bound input handler function
|
||||||
|
*/
|
||||||
|
getInputHandler() {
|
||||||
|
return this.processControllerInput.bind(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to initialize the controller manager with dependencies
|
||||||
|
export function initControllerManager(dependencies = {}) {
|
||||||
|
const self = new ControllerManager(dependencies);
|
||||||
|
|
||||||
|
// This disables the save button until something actually changes
|
||||||
|
self.setHasChangesToWrite(false);
|
||||||
|
return self;
|
||||||
|
}
|
||||||
332
controllers/ds4-controller.js
Normal file
332
controllers/ds4-controller.js
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import BaseController from './base-controller.js';
|
||||||
|
import {
|
||||||
|
sleep,
|
||||||
|
dec2hex,
|
||||||
|
dec2hex32,
|
||||||
|
format_mac_from_view,
|
||||||
|
lf,
|
||||||
|
la
|
||||||
|
} from '../utils.js';
|
||||||
|
|
||||||
|
// DS4 Button mapping configuration
|
||||||
|
const DS4_BUTTON_MAP = [
|
||||||
|
{ name: 'up', byte: 4, mask: 0x0 }, // Dpad handled separately
|
||||||
|
{ name: 'right', byte: 4, mask: 0x1 },
|
||||||
|
{ name: 'down', byte: 4, mask: 0x2 },
|
||||||
|
{ name: 'left', byte: 4, mask: 0x3 },
|
||||||
|
{ name: 'square', byte: 4, mask: 0x10, svg: 'Square' },
|
||||||
|
{ name: 'cross', byte: 4, mask: 0x20, svg: 'Cross' },
|
||||||
|
{ name: 'circle', byte: 4, mask: 0x40, svg: 'Circle' },
|
||||||
|
{ name: 'triangle', byte: 4, mask: 0x80, svg: 'Triangle' },
|
||||||
|
{ name: 'l1', byte: 5, mask: 0x01, svg: 'L1' },
|
||||||
|
{ name: 'l2', byte: 5, mask: 0x04, svg: 'L2' }, // analog handled separately
|
||||||
|
{ name: 'r1', byte: 5, mask: 0x02, svg: 'R1' },
|
||||||
|
{ name: 'r2', byte: 5, mask: 0x08, svg: 'R2' }, // analog handled separately
|
||||||
|
{ name: 'share', byte: 5, mask: 0x10, svg: 'Create' },
|
||||||
|
{ name: 'options', byte: 5, mask: 0x20, svg: 'Options' },
|
||||||
|
{ name: 'l3', byte: 5, mask: 0x40, svg: 'L3' },
|
||||||
|
{ name: 'r3', byte: 5, mask: 0x80, svg: 'R3' },
|
||||||
|
{ name: 'ps', byte: 6, mask: 0x01, svg: 'PS' },
|
||||||
|
{ name: 'touchpad', byte: 6, mask: 0x02, svg: 'Trackpad' },
|
||||||
|
// No mute button on DS4
|
||||||
|
];
|
||||||
|
|
||||||
|
// DS4 Input processing configuration
|
||||||
|
const DS4_INPUT_CONFIG = {
|
||||||
|
buttonMap: DS4_BUTTON_MAP,
|
||||||
|
dpadByte: 4,
|
||||||
|
l2AnalogByte: 7,
|
||||||
|
r2AnalogByte: 8,
|
||||||
|
touchpadOffset: 34,
|
||||||
|
batteryByte: 29,
|
||||||
|
isDS4: true
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DualShock 4 Controller implementation
|
||||||
|
*/
|
||||||
|
class DS4Controller extends BaseController {
|
||||||
|
constructor(device, uiDependencies = {}) {
|
||||||
|
super(device, uiDependencies);
|
||||||
|
this.type = "DS4";
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputConfig() {
|
||||||
|
return DS4_INPUT_CONFIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInfo() {
|
||||||
|
// Device-only: collect info and return a common structure; do not touch the DOM
|
||||||
|
try {
|
||||||
|
let deviceTypeText = this.l("unknown");
|
||||||
|
let is_clone = false;
|
||||||
|
|
||||||
|
const view = lf("ds4_info", await this.receiveFeatureReport(0xa3));
|
||||||
|
|
||||||
|
const cmd = view.getUint8(0, true);
|
||||||
|
|
||||||
|
if(cmd != 0xa3 || view.buffer.byteLength < 49) {
|
||||||
|
if(view.buffer.byteLength != 49) {
|
||||||
|
deviceTypeText = this.l("clone");
|
||||||
|
is_clone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const k1 = new TextDecoder().decode(view.buffer.slice(1, 0x10)).replace(/\0/g, '');
|
||||||
|
const k2 = new TextDecoder().decode(view.buffer.slice(0x10, 0x20)).replace(/\0/g, '');
|
||||||
|
|
||||||
|
const hw_ver_major= view.getUint16(0x21, true);
|
||||||
|
const hw_ver_minor= view.getUint16(0x23, true);
|
||||||
|
const sw_ver_major= view.getUint32(0x25, true);
|
||||||
|
const sw_ver_minor= view.getUint16(0x25+4, true);
|
||||||
|
try {
|
||||||
|
if(!is_clone) {
|
||||||
|
// If this feature report succeeds, it's an original device
|
||||||
|
await this.receiveFeatureReport(0x81);
|
||||||
|
deviceTypeText = this.l("original");
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
la("clone");
|
||||||
|
is_clone = true;
|
||||||
|
deviceTypeText = this.l("clone");
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoItems = [
|
||||||
|
{ key: this.l("Build Date"), value: k1 + " " + k2, cat: "fw" },
|
||||||
|
{ key: this.l("HW Version"), value: "" + dec2hex(hw_ver_major) + ":" + dec2hex(hw_ver_minor), cat: "hw" },
|
||||||
|
{ key: this.l("SW Version"), value: dec2hex32(sw_ver_major) + ":" + dec2hex(sw_ver_minor), cat: "fw" },
|
||||||
|
{ key: this.l("Device Type"), value: deviceTypeText, cat: "hw", severity: is_clone ? 'danger' : undefined },
|
||||||
|
];
|
||||||
|
|
||||||
|
if(!is_clone) {
|
||||||
|
// Add Board Model (UI will append the info icon)
|
||||||
|
infoItems.push({ key: this.l("Board Model"), value: this.hwToBoardModel(hw_ver_minor), cat: "hw", addInfoIcon: 'board' });
|
||||||
|
|
||||||
|
const bd_addr = await this.getBdAddr();
|
||||||
|
infoItems.push({ key: this.l("Bluetooth Address"), value: bd_addr, cat: "hw" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const nv = await this.queryNvStatus();
|
||||||
|
const rare = this.isRare(hw_ver_minor);
|
||||||
|
const disable_bits = is_clone ? 1 : 0; // 1: clone
|
||||||
|
|
||||||
|
return { ok: true, infoItems, nv, disable_bits, rare };
|
||||||
|
} catch(e) {
|
||||||
|
// Return error but do not touch DOM
|
||||||
|
return { ok: false, error: e, disable_bits: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async flash(progressCallback = null) {
|
||||||
|
la("ds4_flash");
|
||||||
|
try {
|
||||||
|
await this.nvsUnlock();
|
||||||
|
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") };
|
||||||
|
} catch(error) {
|
||||||
|
throw new Error(this.l("Error while saving changes: ") + String(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reset() {
|
||||||
|
la("ds4_reset");
|
||||||
|
try {
|
||||||
|
await this.sendFeatureReport(0xa0, [4,1,0]);
|
||||||
|
} catch(error) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async nvsLock() {
|
||||||
|
la("ds4_nvlock");
|
||||||
|
try {
|
||||||
|
await this.sendFeatureReport(0xa0, [10,1,0]);
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
return { ok: false, error: e };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async nvsUnlock() {
|
||||||
|
la("ds4_nvunlock");
|
||||||
|
try {
|
||||||
|
await this.sendFeatureReport(0xa0, [10,2,0x3e,0x71,0x7f,0x89]);
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
return { ok: false, error: e };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBdAddr() {
|
||||||
|
const view = lf("ds4_getbdaddr", await this.receiveFeatureReport(0x12));
|
||||||
|
return format_mac_from_view(view, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateRangeBegin() {
|
||||||
|
la("ds4_calibrate_range_begin");
|
||||||
|
try {
|
||||||
|
// Begin
|
||||||
|
await this.sendFeatureReport(0x90, [1,1,2]);
|
||||||
|
await sleep(200);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const data = await this.receiveFeatureReport(0x91);
|
||||||
|
const data2 = await this.receiveFeatureReport(0x92);
|
||||||
|
const d1 = data.getUint32(0, false);
|
||||||
|
const d2 = data2.getUint32(0, false);
|
||||||
|
if(d1 != 0x91010201 || d2 != 0x920102ff) {
|
||||||
|
la("ds4_calibrate_range_begin_failed", {"d1": d1, "d2": d2});
|
||||||
|
return { ok: false, code: 1, d1, d2 };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
la("ds4_calibrate_range_begin_failed", {"r": e});
|
||||||
|
return { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateRangeEnd() {
|
||||||
|
la("ds4_calibrate_range_end");
|
||||||
|
try {
|
||||||
|
// Write
|
||||||
|
await this.sendFeatureReport(0x90, [2,1,2]);
|
||||||
|
await sleep(200);
|
||||||
|
|
||||||
|
const data = await this.receiveFeatureReport(0x91);
|
||||||
|
const data2 = await this.receiveFeatureReport(0x92);
|
||||||
|
const d1 = data.getUint32(0, false);
|
||||||
|
const d2 = data2.getUint32(0, false);
|
||||||
|
if(d1 != 0x91010202 || d2 != 0x92010201) {
|
||||||
|
la("ds4_calibrate_range_end_failed", {"d1": d1, "d2": d2});
|
||||||
|
return { ok: false, code: 3, d1, d2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
la("ds4_calibrate_range_end_failed", {"r": e});
|
||||||
|
return { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateSticksBegin() {
|
||||||
|
la("ds4_calibrate_sticks_begin");
|
||||||
|
try {
|
||||||
|
// Begin
|
||||||
|
await this.sendFeatureReport(0x90, [1,1,1]);
|
||||||
|
await sleep(200);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const data = await this.receiveFeatureReport(0x91);
|
||||||
|
const data2 = await this.receiveFeatureReport(0x92);
|
||||||
|
const d1 = data.getUint32(0, false);
|
||||||
|
const d2 = data2.getUint32(0, false);
|
||||||
|
if(d1 != 0x91010101 || d2 != 0x920101ff) {
|
||||||
|
la("ds4_calibrate_sticks_begin_failed", {"d1": d1, "d2": d2});
|
||||||
|
return { ok: false, code: 1, d1, d2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
la("ds4_calibrate_sticks_begin_failed", {"r": e});
|
||||||
|
return { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateSticksSample() {
|
||||||
|
la("ds4_calibrate_sticks_sample");
|
||||||
|
try {
|
||||||
|
// Sample
|
||||||
|
await this.sendFeatureReport(0x90, [3,1,1]);
|
||||||
|
await sleep(200);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const data = await this.receiveFeatureReport(0x91);
|
||||||
|
const data2 = await this.receiveFeatureReport(0x92);
|
||||||
|
if(data.getUint32(0, false) != 0x91010101 || data2.getUint32(0, false) != 0x920101ff) {
|
||||||
|
const d1 = dec2hex32(data.getUint32(0, false));
|
||||||
|
const d2 = dec2hex32(data2.getUint32(0, false));
|
||||||
|
la("ds4_calibrate_sticks_sample_failed", {"d1": d1, "d2": d2});
|
||||||
|
return { ok: false, code: 2, d1, d2 };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
return { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateSticksEnd() {
|
||||||
|
la("ds4_calibrate_sticks_end");
|
||||||
|
try {
|
||||||
|
// Write
|
||||||
|
await this.sendFeatureReport(0x90, [2,1,1]);
|
||||||
|
await sleep(200);
|
||||||
|
|
||||||
|
const data = await this.receiveFeatureReport(0x91);
|
||||||
|
const data2 = await this.receiveFeatureReport(0x92);
|
||||||
|
if(data.getUint32(0, false) != 0x91010102 || data2.getUint32(0, false) != 0x92010101) {
|
||||||
|
const d1 = dec2hex32(data.getUint32(0, false));
|
||||||
|
const d2 = dec2hex32(data2.getUint32(0, false));
|
||||||
|
la("ds4_calibrate_sticks_end_failed", {"d1": d1, "d2": d2});
|
||||||
|
return { ok: false, code: 3, d1, d2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
la("ds4_calibrate_sticks_end_failed", {"r": e});
|
||||||
|
return { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryNvStatus() {
|
||||||
|
try {
|
||||||
|
await this.sendFeatureReport(0x08, [0xff,0, 12]);
|
||||||
|
const data = lf("ds4_nvstatus", await this.receiveFeatureReport(0x11));
|
||||||
|
const ret = data.getUint8(1, false);
|
||||||
|
if (ret === 1) {
|
||||||
|
return { device: 'ds4', status: 'locked', locked: true, mode: 'temporary', code: 1 };
|
||||||
|
} else if (ret === 0) {
|
||||||
|
return { device: 'ds4', status: 'unlocked', locked: false, mode: 'permanent', code: 0 };
|
||||||
|
} else {
|
||||||
|
return { device: 'ds4', status: 'unknown', locked: null, code: ret };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { device: 'ds4', status: 'error', locked: null, code: 2, error: e };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hwToBoardModel(hw_ver) {
|
||||||
|
const a = hw_ver >> 8;
|
||||||
|
if(a == 0x31) {
|
||||||
|
return "JDM-001";
|
||||||
|
} else if(a == 0x43) {
|
||||||
|
return "JDM-011";
|
||||||
|
} else if(a == 0x54) {
|
||||||
|
return "JDM-030";
|
||||||
|
} else if(a >= 0x64 && a <= 0x74) {
|
||||||
|
return "JDM-040";
|
||||||
|
} else if((a > 0x80 && a < 0x84) || a == 0x93) {
|
||||||
|
return "JDM-020";
|
||||||
|
} else if(a == 0xa4 || a == 0x90 || a == 0xa0) {
|
||||||
|
return "JDM-050";
|
||||||
|
} else if(a == 0xb0) {
|
||||||
|
return "JDM-055 (Scuf?)";
|
||||||
|
} else if(a == 0xb4) {
|
||||||
|
return "JDM-055";
|
||||||
|
} else {
|
||||||
|
if(this.isRare(hw_ver))
|
||||||
|
return "WOW!";
|
||||||
|
return this.l("Unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isRare(hw_ver) {
|
||||||
|
const a = hw_ver >> 8;
|
||||||
|
const b = a >> 4;
|
||||||
|
return ((b == 7 && a > 0x74) || (b == 9 && a != 0x93 && a != 0x90));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DS4Controller;
|
||||||
395
controllers/ds5-controller.js
Normal file
395
controllers/ds5-controller.js
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import BaseController from './base-controller.js';
|
||||||
|
import {
|
||||||
|
sleep,
|
||||||
|
buf2hex,
|
||||||
|
dec2hex,
|
||||||
|
dec2hex32,
|
||||||
|
dec2hex8,
|
||||||
|
format_mac_from_view,
|
||||||
|
reverse_str,
|
||||||
|
la,
|
||||||
|
lf
|
||||||
|
} from '../utils.js';
|
||||||
|
|
||||||
|
// DS5 Button mapping configuration
|
||||||
|
const DS5_BUTTON_MAP = [
|
||||||
|
{ name: 'up', byte: 7, mask: 0x0 }, // Dpad handled separately
|
||||||
|
{ name: 'right', byte: 7, mask: 0x1 },
|
||||||
|
{ name: 'down', byte: 7, mask: 0x2 },
|
||||||
|
{ name: 'left', byte: 7, mask: 0x3 },
|
||||||
|
{ name: 'square', byte: 7, mask: 0x10, svg: 'Square' },
|
||||||
|
{ name: 'cross', byte: 7, mask: 0x20, svg: 'Cross' },
|
||||||
|
{ name: 'circle', byte: 7, mask: 0x40, svg: 'Circle' },
|
||||||
|
{ name: 'triangle', byte: 7, mask: 0x80, svg: 'Triangle' },
|
||||||
|
{ name: 'l1', byte: 8, mask: 0x01, svg: 'L1' },
|
||||||
|
{ name: 'l2', byte: 4, mask: 0xff }, // analog handled separately
|
||||||
|
{ name: 'r1', byte: 8, mask: 0x02, svg: 'R1' },
|
||||||
|
{ name: 'r2', byte: 5, mask: 0xff }, // analog handled separately
|
||||||
|
{ name: 'create', byte: 8, mask: 0x10, svg: 'Create' },
|
||||||
|
{ name: 'options', byte: 8, mask: 0x20, svg: 'Options' },
|
||||||
|
{ name: 'l3', byte: 8, mask: 0x40, svg: 'L3' },
|
||||||
|
{ name: 'r3', byte: 8, mask: 0x80, svg: 'R3' },
|
||||||
|
{ name: 'ps', byte: 9, mask: 0x01, svg: 'PS' },
|
||||||
|
{ name: 'touchpad', byte: 9, mask: 0x02, svg: 'Trackpad' },
|
||||||
|
{ name: 'mute', byte: 9, mask: 0x04, svg: 'Mute' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// DS5 Input processing configuration
|
||||||
|
const DS5_INPUT_CONFIG = {
|
||||||
|
buttonMap: DS5_BUTTON_MAP,
|
||||||
|
dpadByte: 7,
|
||||||
|
l2AnalogByte: 4,
|
||||||
|
r2AnalogByte: 5,
|
||||||
|
touchpadOffset: 32,
|
||||||
|
batteryByte: 52,
|
||||||
|
isDS4: false
|
||||||
|
};
|
||||||
|
|
||||||
|
function ds5_color(x) {
|
||||||
|
const colorMap = {
|
||||||
|
'00': 'White',
|
||||||
|
'01': 'Midnight Black',
|
||||||
|
'02': 'Cosmic Red',
|
||||||
|
'03': 'Nova Pink',
|
||||||
|
'04': 'Galactic Purple',
|
||||||
|
'05': 'Starlight Blue',
|
||||||
|
'06': 'Grey Camouflage',
|
||||||
|
'07': 'Volcanic Red',
|
||||||
|
'08': 'Sterling Silver',
|
||||||
|
'09': 'Cobalt Blue',
|
||||||
|
'10': 'Chroma Teal',
|
||||||
|
'11': 'Chroma Indigo',
|
||||||
|
'12': 'Chroma Pearl',
|
||||||
|
'30': '30th Anniversary',
|
||||||
|
'Z1': 'God of War Ragnarok',
|
||||||
|
'Z2': 'Spider-Man 2',
|
||||||
|
'Z3': 'Astro Bot',
|
||||||
|
'Z4': 'Fortnite',
|
||||||
|
'Z6': 'The Last of Us',
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorCode = x.slice(4, 6);
|
||||||
|
const colorName = colorMap[colorCode] || 'Unknown';
|
||||||
|
return colorName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DualSense (DS5) Controller implementation
|
||||||
|
*/
|
||||||
|
class DS5Controller extends BaseController {
|
||||||
|
constructor(device, uiDependencies = {}) {
|
||||||
|
super(device, uiDependencies);
|
||||||
|
this.type = "DS5";
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputConfig() {
|
||||||
|
return DS5_INPUT_CONFIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInfo() {
|
||||||
|
return await this._getInfo(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getInfo(is_edge) {
|
||||||
|
// Device-only: collect info and return a common structure; do not touch the DOM
|
||||||
|
try {
|
||||||
|
const view = lf("ds5_info", await this.receiveFeatureReport(0x20));
|
||||||
|
const cmd = view.getUint8(0, true);
|
||||||
|
if(cmd != 0x20 || view.buffer.byteLength != 64)
|
||||||
|
return { ok: false, error: new Error("Invalid response for ds5_info") };
|
||||||
|
|
||||||
|
const build_date = new TextDecoder().decode(view.buffer.slice(1, 1+11));
|
||||||
|
const build_time = new TextDecoder().decode(view.buffer.slice(12, 20));
|
||||||
|
|
||||||
|
const fwtype = view.getUint16(20, true);
|
||||||
|
const swseries = view.getUint16(22, true);
|
||||||
|
const hwinfo = view.getUint32(24, true);
|
||||||
|
const fwversion = view.getUint32(28, true);
|
||||||
|
|
||||||
|
const updversion = view.getUint16(44, true);
|
||||||
|
const unk = view.getUint8(46, true);
|
||||||
|
|
||||||
|
const fwversion1 = view.getUint32(48, true);
|
||||||
|
const fwversion2 = view.getUint32(52, true);
|
||||||
|
const fwversion3 = view.getUint32(56, true);
|
||||||
|
|
||||||
|
const serial_number = await this.getSystemInfo(1, 19, 17);
|
||||||
|
const color = ds5_color(serial_number);
|
||||||
|
const infoItems = [
|
||||||
|
{ key: this.l("Serial Number"), value: serial_number, cat: "hw" },
|
||||||
|
{ key: this.l("MCU Unique ID"), value: await this.getSystemInfo(1, 9, 9, false), cat: "hw", isExtra: true },
|
||||||
|
{ key: this.l("PCBA ID"), value: reverse_str(await this.getSystemInfo(1, 17, 14)), cat: "hw", isExtra: true },
|
||||||
|
{ key: this.l("Battery Barcode"), value: await this.getSystemInfo(1, 24, 23), cat: "hw", isExtra: true },
|
||||||
|
{ key: this.l("VCM Left Barcode"), value: await this.getSystemInfo(1, 26, 16), cat: "hw", isExtra: true },
|
||||||
|
{ key: this.l("VCM Right Barcode"), value: await this.getSystemInfo(1, 28, 16), cat: "hw", isExtra: true },
|
||||||
|
|
||||||
|
{ key: this.l("Color"), value: this.l(color), cat: "hw", addInfoIcon: 'color' },
|
||||||
|
|
||||||
|
...(is_edge ? [] : [{ key: this.l("Board Model"), value: this.hwToBoardModel(hwinfo), cat: "hw", addInfoIcon: 'board' }]),
|
||||||
|
|
||||||
|
{ key: this.l("FW Build Date"), value: build_date + " " + build_time, cat: "fw" },
|
||||||
|
{ key: this.l("FW Type"), value: "0x" + dec2hex(fwtype), cat: "fw", isExtra: true },
|
||||||
|
{ key: this.l("FW Series"), value: "0x" + dec2hex(swseries), cat: "fw", isExtra: true },
|
||||||
|
{ key: this.l("HW Model"), value: "0x" + dec2hex32(hwinfo), cat: "hw", isExtra: true },
|
||||||
|
{ key: this.l("FW Version"), value: "0x" + dec2hex32(fwversion), cat: "fw" },
|
||||||
|
{ key: this.l("FW Update"), value: "0x" + dec2hex(updversion), cat: "fw" },
|
||||||
|
{ key: this.l("FW Update Info"), value: "0x" + dec2hex8(unk), cat: "fw", isExtra: true },
|
||||||
|
{ key: this.l("SBL FW Version"), value: "0x" + dec2hex32(fwversion1), cat: "fw", isExtra: true },
|
||||||
|
{ key: this.l("Venom FW Version"), value: "0x" + dec2hex32(fwversion2), cat: "fw", isExtra: true },
|
||||||
|
{ key: this.l("Spider FW Version"), value: "0x" + dec2hex32(fwversion3), cat: "fw", isExtra: true },
|
||||||
|
|
||||||
|
{ key: this.l("Touchpad ID"), value: await this.getSystemInfo(5, 2, 8, false), cat: "hw", isExtra: true },
|
||||||
|
{ key: this.l("Touchpad FW Version"), value: await this.getSystemInfo(5, 4, 8, false), cat: "fw", isExtra: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const old_controller = build_date.search(/ 2020| 2021/);
|
||||||
|
let disable_bits = 0;
|
||||||
|
if(old_controller != -1) {
|
||||||
|
la("ds5_info_error", {"r": "old"})
|
||||||
|
disable_bits |= 2; // 2: outdated firmware
|
||||||
|
}
|
||||||
|
|
||||||
|
const nv = await this.queryNvStatus();
|
||||||
|
const bd_addr = await this.getBdAddr();
|
||||||
|
infoItems.push({ key: this.l("Bluetooth Address"), value: bd_addr, cat: "hw" });
|
||||||
|
|
||||||
|
const pending_reboot = (nv?.status === 'pending_reboot');
|
||||||
|
|
||||||
|
return { ok: true, infoItems, nv, disable_bits, pending_reboot };
|
||||||
|
} catch(e) {
|
||||||
|
la("ds5_info_error", {"r": e})
|
||||||
|
return { ok: false, error: e, disable_bits: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async flash(progressCallback = null) {
|
||||||
|
la("ds5_flash");
|
||||||
|
try {
|
||||||
|
await this.nvsUnlock();
|
||||||
|
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") };
|
||||||
|
} catch(error) {
|
||||||
|
throw new Error(this.l("Error while saving changes: ") + String(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reset() {
|
||||||
|
la("ds5_reset");
|
||||||
|
try {
|
||||||
|
await this.sendFeatureReport(0x80, [1,1]);
|
||||||
|
} catch(error) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async nvsLock() {
|
||||||
|
la("ds5_nvlock");
|
||||||
|
try {
|
||||||
|
await this.sendFeatureReport(0x80, [3,1]);
|
||||||
|
await this.receiveFeatureReport(0x81);
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
return { ok: false, error: e };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async nvsUnlock() {
|
||||||
|
la("ds5_nvunlock");
|
||||||
|
try {
|
||||||
|
await this.sendFeatureReport(0x80, [3,2, 101, 50, 64, 12]);
|
||||||
|
const data = await this.receiveFeatureReport(0x81);
|
||||||
|
} catch(e) {
|
||||||
|
await sleep(500);
|
||||||
|
throw new Error(this.l("NVS Unlock failed: ") + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBdAddr() {
|
||||||
|
await this.sendFeatureReport(0x80, [9,2]);
|
||||||
|
const data = lf("ds5_getbdaddr", await this.receiveFeatureReport(0x81));
|
||||||
|
return format_mac_from_view(data, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSystemInfo(base, num, length, decode = true) {
|
||||||
|
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");
|
||||||
|
} else {
|
||||||
|
if(decode)
|
||||||
|
return new TextDecoder().decode(pcba_id.buffer.slice(4, 4+length));
|
||||||
|
else
|
||||||
|
return buf2hex(pcba_id.buffer.slice(4, 4+length));
|
||||||
|
}
|
||||||
|
return this.l("Unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateSticksBegin() {
|
||||||
|
la("ds5_calibrate_sticks_begin");
|
||||||
|
try {
|
||||||
|
// Begin
|
||||||
|
await this.sendFeatureReport(0x82, [1,1,1]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const data = await this.receiveFeatureReport(0x83);
|
||||||
|
if(data.getUint32(0, false) != 0x83010101) {
|
||||||
|
const d1 = dec2hex32(data.getUint32(0, false));
|
||||||
|
la("ds5_calibrate_sticks_begin_failed", {"d1": d1});
|
||||||
|
return { ok: false, code: 1, d1 };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
la("ds5_calibrate_sticks_begin_failed", {"r": e});
|
||||||
|
return { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateSticksSample() {
|
||||||
|
la("ds5_calibrate_sticks_sample");
|
||||||
|
try {
|
||||||
|
// Sample
|
||||||
|
await this.sendFeatureReport(0x82, [3,1,1]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const data = await this.receiveFeatureReport(0x83);
|
||||||
|
if(data.getUint32(0, false) != 0x83010101) {
|
||||||
|
const d1 = dec2hex32(data.getUint32(0, false));
|
||||||
|
la("ds5_calibrate_sticks_sample_failed", {"d1": d1});
|
||||||
|
return { ok: false, code: 2, d1 };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
la("ds5_calibrate_sticks_sample_failed", {"r": e});
|
||||||
|
return { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateSticksEnd() {
|
||||||
|
la("ds5_calibrate_sticks_end");
|
||||||
|
try {
|
||||||
|
// Write
|
||||||
|
await this.sendFeatureReport(0x82, [2,1,1]);
|
||||||
|
|
||||||
|
let data = await this.receiveFeatureReport(0x83);
|
||||||
|
|
||||||
|
if(data.getUint32(0, false) != 0x83010102) {
|
||||||
|
const d1 = dec2hex32(data.getUint32(0, false));
|
||||||
|
la("ds5_calibrate_sticks_failed", {"s": 3, "d1": d1});
|
||||||
|
return { ok: false, code: 3, d1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
la("ds5_calibrate_sticks_end_failed", {"r": e});
|
||||||
|
return { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateRangeBegin() {
|
||||||
|
la("ds5_calibrate_range_begin");
|
||||||
|
try {
|
||||||
|
// Begin
|
||||||
|
await this.sendFeatureReport(0x82, [1,1,2]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const data = await this.receiveFeatureReport(0x83);
|
||||||
|
if(data.getUint32(0, false) != 0x83010201) {
|
||||||
|
const d1 = dec2hex32(data.getUint32(0, false));
|
||||||
|
la("ds5_calibrate_range_begin_failed", {"d1": d1});
|
||||||
|
return { ok: false, code: 1, d1 };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
la("ds5_calibrate_range_begin_failed", {"r": e});
|
||||||
|
return { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateRangeEnd() {
|
||||||
|
la("ds5_calibrate_range_end");
|
||||||
|
try {
|
||||||
|
// Write
|
||||||
|
await this.sendFeatureReport(0x82, [2,1,2]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let data = await this.receiveFeatureReport(0x83);
|
||||||
|
|
||||||
|
if(data.getUint32(0, false) != 0x83010202) {
|
||||||
|
const d1 = dec2hex32(data.getUint32(0, false));
|
||||||
|
la("ds5_calibrate_range_end_failed", {"d1": d1});
|
||||||
|
return { ok: false, code: 3, d1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
la("ds5_calibrate_range_end_failed", {"r": e});
|
||||||
|
return { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryNvStatus() {
|
||||||
|
try {
|
||||||
|
await this.sendFeatureReport(0x80, [3,3]);
|
||||||
|
const data = lf("ds5_nvstatus", await this.receiveFeatureReport(0x81));
|
||||||
|
const ret = data.getUint32(1, false);
|
||||||
|
if (ret === 0x15010100) {
|
||||||
|
return { device: 'ds5', status: 'pending_reboot', locked: null, code: 4, raw: ret };
|
||||||
|
}
|
||||||
|
if (ret === 0x03030201) {
|
||||||
|
return { device: 'ds5', status: 'locked', locked: true, mode: 'temporary', code: 1, raw: ret };
|
||||||
|
}
|
||||||
|
if (ret === 0x03030200) {
|
||||||
|
return { device: 'ds5', status: 'unlocked', locked: false, mode: 'permanent', code: 0, raw: ret };
|
||||||
|
}
|
||||||
|
if (ret === 1 || ret === 2) {
|
||||||
|
return { device: 'ds5', status: 'unknown', locked: null, code: 2, raw: ret };
|
||||||
|
}
|
||||||
|
return { device: 'ds5', status: 'unknown', locked: null, code: ret, raw: ret };
|
||||||
|
} catch (e) {
|
||||||
|
return { device: 'ds5', status: 'error', locked: null, code: 2, error: e };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInMemoryModuleData() {
|
||||||
|
// DualSense
|
||||||
|
await this.sendFeatureReport(0x80, [12, 2]);
|
||||||
|
await sleep(100);
|
||||||
|
const data = await this.receiveFeatureReport(0x81);
|
||||||
|
const cmd = data.getUint8(0, true);
|
||||||
|
const p1 = data.getUint8(1, true);
|
||||||
|
const p2 = data.getUint8(2, true);
|
||||||
|
const p3 = data.getUint8(3, true);
|
||||||
|
|
||||||
|
if(cmd != 129 || p1 != 12 || (p2 != 2 && p2 != 4) || p3 != 2)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return Array.from({ length: 12 }, (_, i) => data.getUint16(4 + i * 2, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFinetuneData(data) {
|
||||||
|
const pkg = data.reduce((acc, val) => acc.concat([val & 0xff, val >> 8]), [12, 1]);
|
||||||
|
await this.sendFeatureReport(0x80, pkg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DS5Controller;
|
||||||
248
controllers/ds5-edge-controller.js
Normal file
248
controllers/ds5-edge-controller.js
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import DS5Controller from './ds5-controller.js';
|
||||||
|
import {
|
||||||
|
sleep,
|
||||||
|
dec2hex32,
|
||||||
|
la,
|
||||||
|
lf
|
||||||
|
} from '../utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DualSense Edge (DS5 Edge) Controller implementation
|
||||||
|
*/
|
||||||
|
class DS5EdgeController extends DS5Controller {
|
||||||
|
constructor(device) {
|
||||||
|
super(device);
|
||||||
|
this.type = "DS5Edge";
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInfo() {
|
||||||
|
// DS5 Edge uses the same info structure as DS5 but with is_edge=true
|
||||||
|
const result = await this._getInfo(true);
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
// DS Edge extra module info
|
||||||
|
const empty = '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00';
|
||||||
|
try {
|
||||||
|
const sticks_barcode = (await this.getBarcode()).map(barcode => barcode === empty ? this.l("Unknown") : barcode);
|
||||||
|
result.infoItems.push({ key: this.l("Left Module Barcode"), value: sticks_barcode[1], cat: "fw" });
|
||||||
|
result.infoItems.push({ key: this.l("Right Module Barcode"), value: sticks_barcode[0], cat: "fw" });
|
||||||
|
} catch(_e) {
|
||||||
|
// ignore module read errors here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async flash(progressCallback = null) {
|
||||||
|
la("ds5_edge_flash");
|
||||||
|
try {
|
||||||
|
const ret = await this.flashModules(progressCallback);
|
||||||
|
if(ret) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "<b>" + this.l("Changes saved successfully") + "</b>.<br><br>" + this.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: ") + String(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBarcode() {
|
||||||
|
await this.sendFeatureReport(0x80, [21,34]);
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
|
const data = lf("ds5_edge_get_barcode", await this.receiveFeatureReport(0x81));
|
||||||
|
const td = new TextDecoder();
|
||||||
|
const r_bc = td.decode(data.buffer.slice(21, 21+17));
|
||||||
|
const l_bc = td.decode(data.buffer.slice(40, 40+17));
|
||||||
|
return [r_bc, l_bc];
|
||||||
|
}
|
||||||
|
|
||||||
|
async unlockModule(i) {
|
||||||
|
const m_name = i == 0 ? "left module" : "right module";
|
||||||
|
|
||||||
|
await this.sendFeatureReport(0x80, [21, 6, i, 11]);
|
||||||
|
await sleep(200);
|
||||||
|
const ret = await this.waitUntilWritten([21, 6, 2]);
|
||||||
|
if(!ret) {
|
||||||
|
throw new Error(this.l("Cannot unlock") + " " + this.l(m_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async lockModule(i) {
|
||||||
|
const m_name = i == 0 ? "left module" : "right module";
|
||||||
|
|
||||||
|
await this.sendFeatureReport(0x80, [21, 4, i, 8]);
|
||||||
|
await sleep(200);
|
||||||
|
const ret = await this.waitUntilWritten([21, 4, 2]);
|
||||||
|
if(!ret) {
|
||||||
|
throw new Error(this.l("Cannot lock") + " " + this.l(m_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeDataInto(i) {
|
||||||
|
const m_name = i == 0 ? "left module" : "right module";
|
||||||
|
|
||||||
|
await this.sendFeatureReport(0x80, [21, 5, i]);
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async flashModules(progressCallback) {
|
||||||
|
la("ds5_edge_flash_modules");
|
||||||
|
try {
|
||||||
|
progressCallback(0);
|
||||||
|
|
||||||
|
// Reload data, this ensures correctly writing data in the controller
|
||||||
|
await sleep(100);
|
||||||
|
progressCallback(10);
|
||||||
|
|
||||||
|
// Unlock modules
|
||||||
|
await this.unlockModule(0);
|
||||||
|
progressCallback(15);
|
||||||
|
await this.unlockModule(1);
|
||||||
|
progressCallback(30);
|
||||||
|
|
||||||
|
// Unlock NVS
|
||||||
|
await this.nvsUnlock();
|
||||||
|
await sleep(50);
|
||||||
|
progressCallback(45);
|
||||||
|
|
||||||
|
// This should trigger write into modules
|
||||||
|
const data = await this.getInMemoryModuleData();
|
||||||
|
await sleep(50);
|
||||||
|
progressCallback(60);
|
||||||
|
await this.writeFinetuneData(data);
|
||||||
|
|
||||||
|
// Extra delay
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
|
// Lock back modules
|
||||||
|
await this.lockModule(0);
|
||||||
|
progressCallback(80);
|
||||||
|
await this.lockModule(1);
|
||||||
|
progressCallback(100);
|
||||||
|
|
||||||
|
// Lock back NVS
|
||||||
|
await sleep(100);
|
||||||
|
const lockRes = await this.nvsLock();
|
||||||
|
if(!lockRes.ok) throw (lockRes.error || new Error("NVS lock failed"));
|
||||||
|
|
||||||
|
await sleep(250);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch(error) {
|
||||||
|
la("ds5_edge_flash_modules_failed", {"r": error});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitUntilWritten(expected) {
|
||||||
|
for(let it=0;it<10;it++) {
|
||||||
|
const data = await this.receiveFeatureReport(0x81);
|
||||||
|
|
||||||
|
let again = false
|
||||||
|
for(let i=0;i<expected.length;i++) {
|
||||||
|
if(data.getUint8(1+i, true) != expected[i]) {
|
||||||
|
again = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!again) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await sleep(50);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateSticksEnd() {
|
||||||
|
la("ds5_calibrate_sticks_end");
|
||||||
|
try {
|
||||||
|
// Write
|
||||||
|
await this.sendFeatureReport(0x82, [2,1,1]);
|
||||||
|
|
||||||
|
let data = await this.receiveFeatureReport(0x83);
|
||||||
|
|
||||||
|
if(data.getUint32(0, false) != 0x83010101) {
|
||||||
|
const d1 = dec2hex32(data.getUint32(0, false));
|
||||||
|
la("ds5_calibrate_sticks_failed", {"s": 3, d1});
|
||||||
|
return { ok: false, code: 4, d1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sendFeatureReport(0x82, [2,1,1]);
|
||||||
|
data = await this.receiveFeatureReport(0x83);
|
||||||
|
if(data.getUint32(0, false) != 0x83010103 && data.getUint32(0, false) != 0x83010312) {
|
||||||
|
const d1 = dec2hex32(data.getUint32(0, false));
|
||||||
|
la("ds5_calibrate_sticks_failed", {"s": 3, d1});
|
||||||
|
return { ok: false, code: 5, d1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
la("ds5_calibrate_sticks_end_failed", {"r": e});
|
||||||
|
return { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async calibrateRangeEnd() {
|
||||||
|
la("ds5_calibrate_range_end");
|
||||||
|
try {
|
||||||
|
// Write
|
||||||
|
await this.sendFeatureReport(0x82, [2,1,2]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let data = await this.receiveFeatureReport(0x83);
|
||||||
|
|
||||||
|
if(data.getUint32(0, false) != 0x83010201) {
|
||||||
|
const d1 = dec2hex32(data.getUint32(0, false));
|
||||||
|
la("ds5_calibrate_range_end_failed", {d1});
|
||||||
|
return { ok: false, code: 4, d1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sendFeatureReport(0x82, [2,1,2]);
|
||||||
|
data = await this.receiveFeatureReport(0x83)
|
||||||
|
if(data.getUint32(0, false) != 0x83010203) {
|
||||||
|
const d1 = dec2hex32(data.getUint32(0, false));
|
||||||
|
la("ds5_calibrate_range_end_failed", {d1});
|
||||||
|
return { ok: false, code: 5, d1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
} catch(e) {
|
||||||
|
la("ds5_calibrate_range_end_failed", {"r": e});
|
||||||
|
return { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInMemoryModuleData() {
|
||||||
|
// DualSense Edge
|
||||||
|
await this.sendFeatureReport(0x80, [12, 4]);
|
||||||
|
await sleep(100);
|
||||||
|
const data = await this.receiveFeatureReport(0x81);
|
||||||
|
const cmd = data.getUint8(0, true);
|
||||||
|
const p1 = data.getUint8(1, true);
|
||||||
|
const p2 = data.getUint8(2, true);
|
||||||
|
const p3 = data.getUint8(3, true);
|
||||||
|
|
||||||
|
if(cmd != 129 || p1 != 12 || (p2 != 2 && p2 != 4) || p3 != 2)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return Array.from({ length: 12 }, (_, i) => data.getUint16(4 + i * 2, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFinetuneData(data) {
|
||||||
|
const pkg = data.reduce((acc, val) => acc.concat([val & 0xff, val >> 8]), [12, 1]);
|
||||||
|
await this.sendFeatureReport(0x80, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DS5EdgeController;
|
||||||
26
index.html
26
index.html
@@ -26,7 +26,15 @@
|
|||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<meta http-equiv="Permissions-Policy" content="interest-cohort=()">
|
<meta http-equiv="Permissions-Policy" content="interest-cohort=()">
|
||||||
<script src="core.js"></script>
|
<script type="module" src="utils.js"></script>
|
||||||
|
<script type="module" src="translations.js"></script>
|
||||||
|
<script type="module" src="controllers/base-controller.js"></script>
|
||||||
|
<script type="module" src="controllers/ds4-controller.js"></script>
|
||||||
|
<script type="module" src="controllers/ds5-controller.js"></script>
|
||||||
|
<script type="module" src="controllers/ds5-edge-controller.js"></script>
|
||||||
|
<script type="module" src="controllers/controller-factory.js"></script>
|
||||||
|
<script type="module" src="controllers/controller-manager.js"></script>
|
||||||
|
<script type="module" src="core.js"></script>
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
@@ -1184,6 +1192,20 @@ input[id^="finetune"] {
|
|||||||
function gtag(){dataLayer.push(arguments);}
|
function gtag(){dataLayer.push(arguments);}
|
||||||
gtag('js', new Date());
|
gtag('js', new Date());
|
||||||
gtag('config', 'G-FSXPMDXLLS');
|
gtag('config', 'G-FSXPMDXLLS');
|
||||||
gboot();
|
|
||||||
|
// Wait for the module to load before calling gboot
|
||||||
|
if (window.gboot) {
|
||||||
|
gboot();
|
||||||
|
} else {
|
||||||
|
// If gboot isn't available yet, wait for it
|
||||||
|
const checkGboot = () => {
|
||||||
|
if (window.gboot) {
|
||||||
|
gboot();
|
||||||
|
} else {
|
||||||
|
setTimeout(checkGboot, 10);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkGboot();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
193
translations.js
Normal file
193
translations.js
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { createCookie, readCookie } from './utils.js';
|
||||||
|
|
||||||
|
// Alphabetical order
|
||||||
|
const available_langs = {
|
||||||
|
"ar_ar": { "name": "العربية", "file": "ar_ar.json", "direction": "rtl"},
|
||||||
|
"bg_bg": { "name": "Български", "file": "bg_bg.json", "direction": "ltr"},
|
||||||
|
"cz_cz": { "name": "Čeština", "file": "cz_cz.json", "direction": "ltr"},
|
||||||
|
"da_dk": { "name": "Dansk", "file": "da_dk.json", "direction": "ltr"},
|
||||||
|
"de_de": { "name": "Deutsch", "file": "de_de.json", "direction": "ltr"},
|
||||||
|
"es_es": { "name": "Español", "file": "es_es.json", "direction": "ltr"},
|
||||||
|
"fr_fr": { "name": "Français", "file": "fr_fr.json", "direction": "ltr"},
|
||||||
|
"hu_hu": { "name": "Magyar", "file": "hu_hu.json", "direction": "ltr"},
|
||||||
|
"it_it": { "name": "Italiano", "file": "it_it.json", "direction": "ltr"},
|
||||||
|
"jp_jp": { "name": "日本語", "file": "jp_jp.json", "direction": "ltr"},
|
||||||
|
"ko_kr": { "name": "한국어", "file": "ko_kr.json", "direction": "ltr"},
|
||||||
|
"nl_nl": { "name": "Nederlands", "file": "nl_nl.json", "direction": "ltr"},
|
||||||
|
"pl_pl": { "name": "Polski", "file": "pl_pl.json", "direction": "ltr"},
|
||||||
|
"pt_br": { "name": "Português do Brasil", "file": "pt_br.json", "direction": "ltr"},
|
||||||
|
"pt_pt": { "name": "Português", "file": "pt_pt.json", "direction": "ltr"},
|
||||||
|
"rs_rs": { "name": "Srpski", "file": "rs_rs.json", "direction": "ltr"},
|
||||||
|
"ru_ru": { "name": "Русский", "file": "ru_ru.json", "direction": "ltr"},
|
||||||
|
"tr_tr": { "name": "Türkçe", "file": "tr_tr.json", "direction": "ltr"},
|
||||||
|
"ua_ua": { "name": "Українська", "file": "ua_ua.json", "direction": "ltr"},
|
||||||
|
"zh_cn": { "name": "中文", "file": "zh_cn.json", "direction": "ltr"},
|
||||||
|
"zh_tw": { "name": "中文(繁)", "file": "zh_tw.json", "direction": "ltr"}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Translation state - will be imported from core.js app object
|
||||||
|
let translationState = null;
|
||||||
|
let la = null;
|
||||||
|
let welcomeModal = null;
|
||||||
|
let handleLanguageChange = null;
|
||||||
|
|
||||||
|
export function lang_init(appState, handleLanguageChangeCb, welcomeModalCb, laCb) {
|
||||||
|
translationState = appState;
|
||||||
|
handleLanguageChange = handleLanguageChangeCb;
|
||||||
|
welcomeModal = welcomeModalCb;
|
||||||
|
la = laCb;
|
||||||
|
|
||||||
|
let id_iter = 0;
|
||||||
|
const items = document.getElementsByClassName('ds-i18n');
|
||||||
|
for(let item of items) {
|
||||||
|
if (item.id.length == 0) {
|
||||||
|
item.id = `ds-i18n-${id_iter++}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
translationState.lang_orig_text[item.id] = $(item).html();
|
||||||
|
}
|
||||||
|
translationState.lang_orig_text[".title"] = document.title;
|
||||||
|
|
||||||
|
const force_lang = readCookie("force_lang");
|
||||||
|
if (force_lang != null) {
|
||||||
|
lang_set(force_lang, true).catch(error => {
|
||||||
|
console.error("Failed to set forced language:", error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const nlang = navigator.language.replace('-', '_').toLowerCase();
|
||||||
|
const ljson = available_langs[nlang];
|
||||||
|
if(ljson) {
|
||||||
|
la("lang_init", {"l": nlang});
|
||||||
|
lang_translate(ljson["file"], nlang, ljson["direction"]).catch(error => {
|
||||||
|
console.error("Failed to load initial language:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const langs = Object.keys(available_langs);
|
||||||
|
const olangs = [
|
||||||
|
'<li><a class="dropdown-item" href="#" onclick="lang_set(\'en_us\');">English</a></li>',
|
||||||
|
...langs.map(lang => {
|
||||||
|
const name = available_langs[lang]["name"];
|
||||||
|
return `<li><a class="dropdown-item" href="#" onclick="lang_set('${lang}');">${name}</a></li>`;
|
||||||
|
}),
|
||||||
|
'<li><hr class="dropdown-divider"></li>',
|
||||||
|
'<li><a class="dropdown-item" href="https://github.com/dualshock-tools/dualshock-tools.github.io/blob/main/TRANSLATIONS.md" target="_blank">Missing your language?</a></li>'
|
||||||
|
].join('');
|
||||||
|
$("#availLangs").html(olangs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function lang_set(lang, skip_modal=false) {
|
||||||
|
la("lang_set", { l: lang });
|
||||||
|
|
||||||
|
lang_reset_page();
|
||||||
|
if(lang != "en_us") {
|
||||||
|
const { file, direction } = available_langs[lang];
|
||||||
|
await lang_translate(file, lang, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleLanguageChange(lang);
|
||||||
|
createCookie("force_lang", lang);
|
||||||
|
if(!skip_modal && welcomeModal) {
|
||||||
|
createCookie("welcome_accepted", "0");
|
||||||
|
welcomeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function lang_reset_page() {
|
||||||
|
lang_set_direction("ltr", "en_us");
|
||||||
|
|
||||||
|
// Reset translation state to disable translations
|
||||||
|
translationState.lang_cur = {};
|
||||||
|
translationState.lang_disabled = true;
|
||||||
|
|
||||||
|
const { lang_orig_text } = translationState;
|
||||||
|
const items = document.getElementsByClassName('ds-i18n');
|
||||||
|
for(let item of items) {
|
||||||
|
$(item).html(lang_orig_text[item.id]);
|
||||||
|
};
|
||||||
|
$("#authorMsg").html("");
|
||||||
|
$("#curLang").html("English");
|
||||||
|
document.title = lang_orig_text[".title"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function lang_set_direction(new_direction, lang_name) {
|
||||||
|
const lang_prefix = lang_name.split("_")[0]
|
||||||
|
$("html").attr("lang", lang_prefix);
|
||||||
|
|
||||||
|
if(new_direction == translationState.lang_cur_direction)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(new_direction == "rtl") {
|
||||||
|
$('#bootstrap-css').attr('integrity', 'sha384-dpuaG1suU0eT09tx5plTaGMLBsfDLzUCCUXOY2j/LSvXYuG6Bqs43ALlhIqAJVRb');
|
||||||
|
$('#bootstrap-css').attr('href', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.rtl.min.css');
|
||||||
|
} else {
|
||||||
|
$('#bootstrap-css').attr('integrity', 'sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH');
|
||||||
|
$('#bootstrap-css').attr('href', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css');
|
||||||
|
}
|
||||||
|
$("html").attr("dir", new_direction);
|
||||||
|
translationState.lang_cur_direction = new_direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function l(text) {
|
||||||
|
if(!translationState || translationState.lang_disabled)
|
||||||
|
return text;
|
||||||
|
|
||||||
|
const [out] = translationState.lang_cur[text] || [];
|
||||||
|
if(out) return out;
|
||||||
|
|
||||||
|
console.log("Missing translation for: '" + text + "'");
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lang_translate(target_file, target_lang, target_direction) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
$.getJSON("lang/" + target_file)
|
||||||
|
.done(function(data) {
|
||||||
|
const { lang_orig_text, lang_cur } = translationState;
|
||||||
|
lang_set_direction(target_direction, target_lang);
|
||||||
|
|
||||||
|
$.each(data, function( key, val ) {
|
||||||
|
if(lang_cur[key]) {
|
||||||
|
console.log("Warn: already exists " + key);
|
||||||
|
} else {
|
||||||
|
lang_cur[key] = [val];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(Object.keys(lang_cur).length > 0) {
|
||||||
|
translationState.lang_disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = document.getElementsByClassName('ds-i18n');
|
||||||
|
for(let item of items) {
|
||||||
|
const originalText = lang_orig_text[item.id];
|
||||||
|
const [translatedText] = lang_cur[originalText] || [];
|
||||||
|
if (translatedText) {
|
||||||
|
$(item).html(translatedText);
|
||||||
|
} else {
|
||||||
|
console.log("Cannot find mapping for " + originalText);
|
||||||
|
$(item).html(originalText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const old_title = lang_orig_text[".title"];
|
||||||
|
document.title = lang_cur[old_title];
|
||||||
|
if(lang_cur[".authorMsg"]) {
|
||||||
|
$("#authorMsg").html(lang_cur[".authorMsg"]);
|
||||||
|
}
|
||||||
|
$("#curLang").html(available_langs[target_lang]["name"]);
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.fail(function(jqxhr, textStatus, error) {
|
||||||
|
console.error("Failed to load translation file:", target_file, error);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make lang_set available globally for onclick handlers in HTML
|
||||||
|
window.lang_set = lang_set;
|
||||||
160
utils.js
Normal file
160
utils.js
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for DualShock controller operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sleep for specified milliseconds
|
||||||
|
* @param {number} ms Milliseconds to sleep
|
||||||
|
* @returns {Promise} Promise that resolves after the specified time
|
||||||
|
*/
|
||||||
|
export async function sleep(ms) {
|
||||||
|
await new Promise(r => setTimeout(r, ms));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Convert float to string with specified precision
|
||||||
|
* @param {number} f Float number to convert
|
||||||
|
* @param {number} precision Number of decimal places
|
||||||
|
* @returns {string} Formatted string
|
||||||
|
*/
|
||||||
|
export function float_to_str(f, precision = 2) {
|
||||||
|
if(precision <=2 && f < 0.004 && f >= -0.004) return "+0.00";
|
||||||
|
return (f<0?"":"+") + f.toFixed(precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert buffer to hexadecimal string
|
||||||
|
* @param {ArrayBuffer} buffer Buffer to convert
|
||||||
|
* @returns {string} Hexadecimal string representation
|
||||||
|
*/
|
||||||
|
export function buf2hex(buffer) {
|
||||||
|
return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert decimal to 16-bit hexadecimal string
|
||||||
|
* @param {number} i Decimal number
|
||||||
|
* @returns {string} 4-character uppercase hex string
|
||||||
|
*/
|
||||||
|
export function dec2hex(i) {
|
||||||
|
return (i + 0x10000).toString(16).substr(-4).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert decimal to 32-bit hexadecimal string
|
||||||
|
* @param {number} i Decimal number
|
||||||
|
* @returns {string} 8-character uppercase hex string
|
||||||
|
*/
|
||||||
|
export function dec2hex32(i) {
|
||||||
|
return (i + 0x100000000).toString(16).substr(-8).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert decimal to 8-bit hexadecimal string
|
||||||
|
* @param {number} i Decimal number
|
||||||
|
* @returns {string} 2-character uppercase hex string
|
||||||
|
*/
|
||||||
|
export function dec2hex8(i) {
|
||||||
|
return (i + 0x100).toString(16).substr(-2).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format MAC address from DataView
|
||||||
|
* @returns {string} Formatted MAC address (XX:XX:XX:XX:XX:XX)
|
||||||
|
*/
|
||||||
|
export function format_mac_from_view(view, start_index_inclusive) {
|
||||||
|
const bytes = [];
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const idx = start_index_inclusive + (5 - i);
|
||||||
|
bytes.push(dec2hex8(view.getUint8(idx, false)));
|
||||||
|
}
|
||||||
|
return bytes.join(":");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse a string (for ASCII strings only, not UTF)
|
||||||
|
* @param {string} s String to reverse
|
||||||
|
* @returns {string} Reversed string
|
||||||
|
*/
|
||||||
|
export function reverse_str(s) {
|
||||||
|
return s.split('').reverse().join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analytics/logging function
|
||||||
|
* @param {string} k Key/event name
|
||||||
|
* @param {Object} v Optional values object
|
||||||
|
*/
|
||||||
|
export function la(k, v = {}) {
|
||||||
|
// Get app state from global scope if available
|
||||||
|
if (typeof window !== 'undefined' && window.app) {
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: "https://the.al/ds4_a/l",
|
||||||
|
data: JSON.stringify({u: window.app.gu, j: window.app.gj, k, v}),
|
||||||
|
contentType: "application/json",
|
||||||
|
dataType: 'json'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lf(operation, data) { la(operation, buf2hex(data.buffer)); return data; }
|
||||||
|
|
||||||
|
export function lerp_color(a, b, t) {
|
||||||
|
// a, b: hex color strings, t: 0.0-1.0
|
||||||
|
function hex2rgb(hex) {
|
||||||
|
hex = hex.replace('#', '');
|
||||||
|
if (hex.length === 3) hex = hex.split('').map(x => x + x).join('');
|
||||||
|
const num = parseInt(hex, 16);
|
||||||
|
return [(num >> 16) & 255, (num >> 8) & 255, num & 255];
|
||||||
|
}
|
||||||
|
function rgb2hex(r, g, b) {
|
||||||
|
return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
const c1 = hex2rgb(a);
|
||||||
|
const c2 = hex2rgb(b);
|
||||||
|
const c = [
|
||||||
|
Math.round(c1[0] + (c2[0] - c1[0]) * t),
|
||||||
|
Math.round(c1[1] + (c2[1] - c1[1]) * t),
|
||||||
|
Math.round(c1[2] + (c2[2] - c1[2]) * t)
|
||||||
|
];
|
||||||
|
return rgb2hex(c[0], c[1], c[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a cookie with specified name, value, and expiration days
|
||||||
|
* @param {string} name Cookie name
|
||||||
|
* @param {string} value Cookie value
|
||||||
|
* @param {number} days Number of days until expiration
|
||||||
|
*/
|
||||||
|
export function createCookie(name, value, days) {
|
||||||
|
const expires = days ? "; expires=" + new Date(Date.now() + days * 24 * 60 * 60 * 1000).toGMTString() : "";
|
||||||
|
document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + expires + "; path=/";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a cookie value by name
|
||||||
|
* @param {string} name Cookie name
|
||||||
|
* @returns {string|null} Cookie value or null if not found
|
||||||
|
*/
|
||||||
|
export function readCookie(name) {
|
||||||
|
const nameEQ = encodeURIComponent(name) + "=";
|
||||||
|
const ca = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < ca.length; i++) {
|
||||||
|
let c = ca[i];
|
||||||
|
while (c.charAt(0) === ' ')
|
||||||
|
c = c.substring(1, c.length);
|
||||||
|
if (c.indexOf(nameEQ) === 0)
|
||||||
|
return decodeURIComponent(c.substring(nameEQ.length, c.length));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a cookie by setting its expiration to the past
|
||||||
|
* @param {string} name Cookie name to delete
|
||||||
|
*/
|
||||||
|
export function eraseCookie(name) {
|
||||||
|
createCookie(name, "", -1);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user