Add option to restore previous calibration settings for DS5 and Edge

This commit is contained in:
Mathias Malmqvist
2025-11-13 00:15:52 +01:00
parent ba10cfcbdd
commit 24ec34929c
8 changed files with 463 additions and 10 deletions

View File

@@ -209,6 +209,7 @@
</button>
<hr>
<button id="savechanges" type="button" class="btn btn-success ds-btn ds-i18n" onclick="flash_all_changes()" id="resetBtn">Save changes permanently</button>
<button id="restore-calibration-btn" type="button" class="btn btn-secondary ds-btn ds-i18n" onclick="openCalibrationHistoryModal()">Restore calibration</button>
<button type="button" class="btn btn-danger ds-btn ds-i18n" onclick="reboot_controller()" id="resetBtn">Reboot controller</button>
<div class="card text-bg-light" >

View File

@@ -86,9 +86,10 @@ class ControllerFactory {
showInfo: false,
showFinetune: false,
showInfoTab: false,
showFourStepCalib: true,
showQuickTests: true,
showQuickCalib: false
showFourStepCalib: true,
showQuickCalib: false,
showCalibrationHistory: false
};
case 0x0ce6: // DS5
@@ -97,9 +98,10 @@ class ControllerFactory {
showInfo: true,
showFinetune: true,
showInfoTab: true,
showFourStepCalib: false,
showQuickTests: true,
showQuickCalib: true
showFourStepCalib: false,
showQuickCalib: true,
showCalibrationHistory: true
};
case 0x0e45: // VR2 Left Controller
@@ -108,8 +110,8 @@ class ControllerFactory {
showInfo: true,
showFinetune: false,
showInfoTab: true,
showFourStepCalib: true,
showQuickTests: false,
showFourStepCalib: true,
showQuickCalib: false
};
@@ -118,9 +120,10 @@ class ControllerFactory {
showInfo: false,
showFinetune: false,
showInfoTab: false,
showFourStepCalib: false,
showQuickTests: false,
showQuickCalib: false
showFourStepCalib: false,
showQuickCalib: false,
showCalibrationHistory: false
};
}
}

View File

@@ -14,6 +14,8 @@ import {
isQuickTestVisible,
quicktest_handle_controller_input
} from './modals/quick-test-modal.js';
import { FinetuneHistory } from './finetune-history.js';
import { CalibrationHistoryModal } from './modals/calibration-history-modal.js';
// Application State - manages app-wide state and UI
const app = {
@@ -108,6 +110,7 @@ function gboot() {
await loadAllTemplates();
CalibrationHistoryModal.init();
initAnalyticsApi(app); // init just with gu for now
lang_init(app, handleLanguageChange, show_welcome_modal);
show_welcome_modal();
@@ -215,14 +218,15 @@ async function continue_connection({data, device}) {
}
// Helper to apply basic UI visibility based on device type
function applyDeviceUI({ showInfo, showFinetune, showInfoTab, showFourStepCalib, showQuickTests, showQuickCalib }) {
function applyDeviceUI({ showInfo, showFinetune, showInfoTab, showQuickTests, showFourStepCalib, showQuickCalib, showCalibrationHistory }) {
$("#infoshowall").toggle(!!showInfo);
$("#ds5finetune").toggle(!!showFinetune);
$("#info-tab").toggle(!!showInfoTab);
$("#four-step-center-calib").toggle(!!showFourStepCalib);
$("#quick-tests-div").css("visibility", showQuickTests ? "visible" : "hidden");
$("#four-step-center-calib").toggle(!!showFourStepCalib);
$("#quick-center-calib").toggle(!!showQuickCalib);
$("#quick-center-calib-group").toggle(!!showQuickCalib);
$("#restore-calibration-btn").toggle(!!showCalibrationHistory);
}
let controllerInstance = null;
@@ -328,6 +332,17 @@ async function continue_connection({data, device}) {
if(model == "VR2") {
show_popup(l("<p>Support for PS VR2 controllers is <b>minimal and highly experimental</b>.</p><p>I currently don't own these controllers, so I cannot verify the calibration process myself.</p><p>If you'd like to help improve full support, you can contribute with a donation or even send the controllers for testing.</p><p>Feel free to contact me on Discord (the_al) or by email at ds4@the.al .</p><br><p>Thank you for your support!</p>"), true)
}
// Save finetune parameters for DS5 and Edge controllers
if (model === "DS5" || model === "DS5_Edge") {
const finetuneData = await controllerInstance.getInMemoryModuleData();
// Extract serial number from info items
const serialNumberItem = info.infoItems?.find(item => item.key === l("Serial Number"));
const serialNumber = serialNumberItem?.value;
if (serialNumber) {
FinetuneHistory.save(finetuneData, serialNumber);
}
}
} catch(err) {
await disconnect();
throw err;
@@ -1118,6 +1133,36 @@ window.ds5_finetune = () => ds5_finetune(
{ ll_data, rr_data, clear_circularity },
(success) => success && switchToRangeMode()
);
window.apply_finetune_revert = async (finetuneData) => {
if (!controller || !controller.isConnected()) {
throw new Error('Controller not connected');
}
if (!Array.isArray(finetuneData) || finetuneData.length !== 12) {
throw new Error('Invalid finetune data');
}
await controller.writeFinetuneData(finetuneData);
};
window.openCalibrationHistoryModal = async () => {
let currentFinetuneData = null;
let controllerSerialNumber = null;
try {
if (controller && typeof controller.getInMemoryModuleData === 'function') {
currentFinetuneData = await controller.getInMemoryModuleData('finetune');
}
// Get serial number from device info
if (controller && typeof controller.getDeviceInfo === 'function') {
const info = await controller.getDeviceInfo();
const serialNumberItem = info?.infoItems?.find(item => item.key === l("Serial Number"));
controllerSerialNumber = serialNumberItem?.value;
}
} catch (error) {
console.warn('Could not retrieve current finetune data or serial number:', error);
}
window.show_calibration_history_modal(currentFinetuneData, controllerSerialNumber);
};
window.flash_all_changes = flash_all_changes;
window.reboot_controller = reboot_controller;
window.refresh_nvstatus = refresh_nvstatus;

183
js/finetune-history.js Normal file
View File

@@ -0,0 +1,183 @@
'use strict';
const STORAGE_KEY = 'finetuneHistory';
const MAX_HISTORY_ENTRIES_PER_CONTROLLER = 10;
/**
* Manages finetune parameter history for DS5 and Edge controllers
* Stores entries per controller identified by serial number
*/
export class FinetuneHistory {
/**
* Save current finetune settings for a controller
* @param {Array} finetuneData - Array of 12 finetune values
* @param {string} controllerSerialNumber - Serial number of the controller
* @returns {string} The ID of the saved entry
*/
static save(finetuneData, controllerSerialNumber) {
if (!Array.isArray(finetuneData) || finetuneData.length !== 12) {
throw new Error(`Finetune data must be an array of 12 values, got "${finetuneData}"`);
}
if (!controllerSerialNumber || typeof controllerSerialNumber !== 'string') {
throw new Error('Controller serial number is required');
}
const allHistory = this._getAllHistory();
const controllerHistory = allHistory[controllerSerialNumber] || [];
// Check if the most recent entry has the same data
if (controllerHistory.length > 0 && this._dataEquals(controllerHistory[0].data, finetuneData)) {
// Update the timestamp of the existing entry
controllerHistory[0].timestamp = Date.now();
allHistory[controllerSerialNumber] = controllerHistory;
this._saveAllHistory(allHistory);
return controllerHistory[0].id;
}
const entry = {
id: this._generateId(),
timestamp: Date.now(),
data: finetuneData
};
controllerHistory.unshift(entry);
// Keep only the latest MAX_HISTORY_ENTRIES_PER_CONTROLLER for this controller
if (controllerHistory.length > MAX_HISTORY_ENTRIES_PER_CONTROLLER) {
controllerHistory.pop();
}
allHistory[controllerSerialNumber] = controllerHistory;
this._saveAllHistory(allHistory);
return entry.id;
}
/**
* Get all saved finetune settings for a specific controller
* @param {string} controllerSerialNumber - Serial number of the controller
* @returns {Array} Array of saved settings entries for the controller
*/
static getAll(controllerSerialNumber) {
if (!controllerSerialNumber || typeof controllerSerialNumber !== 'string') {
return [];
}
const allHistory = this._getAllHistory();
return allHistory[controllerSerialNumber] || [];
}
/**
* Get finetune settings by ID
* @param {string} id - Entry ID
* @param {string} controllerSerialNumber - Serial number of the controller
* @returns {Object|null} Entry object or null if not found
*/
static getById(id, controllerSerialNumber) {
if (!controllerSerialNumber || typeof controllerSerialNumber !== 'string') {
return null;
}
const history = this.getAll(controllerSerialNumber);
return history.find(entry => entry.id === id) || null;
}
/**
* Delete a saved entry
* @param {string} id - Entry ID
* @param {string} controllerSerialNumber - Serial number of the controller
* @returns {boolean} True if deleted, false if not found
*/
static delete(id, controllerSerialNumber) {
if (!controllerSerialNumber || typeof controllerSerialNumber !== 'string') {
return false;
}
const allHistory = this._getAllHistory();
const controllerHistory = allHistory[controllerSerialNumber] || [];
const index = controllerHistory.findIndex(entry => entry.id === id);
if (index >= 0) {
controllerHistory.splice(index, 1);
allHistory[controllerSerialNumber] = controllerHistory;
this._saveAllHistory(allHistory);
return true;
}
return false;
}
/**
* Clear all saved finetune settings for a specific controller
* @param {string} controllerSerialNumber - Serial number of the controller
*/
static clearAll(controllerSerialNumber) {
if (!controllerSerialNumber || typeof controllerSerialNumber !== 'string') {
return;
}
const allHistory = this._getAllHistory();
delete allHistory[controllerSerialNumber];
this._saveAllHistory(allHistory);
}
/**
* Get finetune data from a specific entry
* @param {string} id - Entry ID
* @param {string} controllerSerialNumber - Serial number of the controller
* @returns {Array|null} Finetune data array or null if not found
*/
static getDataById(id, controllerSerialNumber) {
const entry = this.getById(id, controllerSerialNumber);
return entry ? entry.data : null;
}
// ==================== PRIVATE METHODS ====================
/**
* Get all history from localStorage (for all controllers)
* @private
*/
static _getAllHistory() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
return stored ? JSON.parse(stored) : {};
} catch (e) {
console.error('Failed to parse finetune history:', e);
return {};
}
}
/**
* Save all history to localStorage
* @private
*/
static _saveAllHistory(allHistory) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(allHistory));
} catch (e) {
console.error('Failed to save finetune history:', e);
}
}
/**
* Generate unique ID
* @private
*/
static _generateId() {
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Compare two data arrays for equality
* @private
*/
static _dataEquals(data1, data2) {
if (!Array.isArray(data1) || !Array.isArray(data2)) {
return false;
}
if (data1.length !== data2.length) {
return false;
}
return data1.every((val, idx) => val === data2[idx]);
}
}

View File

@@ -0,0 +1,175 @@
'use strict';
import { FinetuneHistory } from '../finetune-history.js';
import { formatLocalizedDate } from '../utils.js';
export class CalibrationHistoryModal {
static modalElement = null;
static bootstrapModal = null;
static currentFinetuneData = null;
static currentControllerSerialNumber = null;
static init() {
this.modalElement = document.getElementById('calibrationHistoryModal');
if (this.modalElement) {
this.bootstrapModal = new bootstrap.Modal(this.modalElement);
}
}
static async show(currentFinetuneData = null, controllerSerialNumber = null) {
if (!this.bootstrapModal) {
this.init();
}
this.currentFinetuneData = currentFinetuneData;
this.currentControllerSerialNumber = controllerSerialNumber;
await this._populateHistory();
this.bootstrapModal.show();
}
static hide() {
if (this.bootstrapModal) {
this.bootstrapModal.hide();
}
}
/**
* Populate the history list
* @private
*/
static async _populateHistory() {
const history = FinetuneHistory.getAll(this.currentControllerSerialNumber);
const container = document.getElementById('historyListContainer');
if (!history || history.length === 0) {
container.innerHTML = '<p class="text-muted ds-i18n">No saved calibration settings found.</p>';
document.getElementById('clearAllBtn').style.display = 'none';
return;
}
document.getElementById('clearAllBtn').style.display = 'block';
let html = '<div class="list-group">';
history.forEach(entry => {
const date = formatLocalizedDate(entry.timestamp);
const isCurrent = this.currentFinetuneData && this._dataEquals(entry.data, this.currentFinetuneData);
html += `
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1">${date}</h6>
<p class="mb-0 small">Values: ${entry.data.join(', ')}</p>
</div>
<div class="btn-group-sm" role="group">
${isCurrent ?
`<button type="button" class="btn btn-sm btn-success ds-i18n" disabled>Current</button>` :
`<button type="button" class="btn btn-sm btn-primary ds-i18n" onclick="calibration_history_revert('${entry.id}')">Revert</button>
<button type="button" class="btn btn-sm btn-outline-danger ds-i18n" onclick="calibration_history_delete('${entry.id}')">Delete</button>`
}
</div>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
/**
* Compare two data arrays for equality
* @private
*/
static _dataEquals(data1, data2) {
if (!Array.isArray(data1) || !Array.isArray(data2)) {
return false;
}
if (data1.length !== data2.length) {
return false;
}
return data1.every((val, idx) => val === data2[idx]);
}
/**
* Escape HTML special characters
* @private
*/
static _escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Revert to a saved calibration
* @param {string} entryId - The ID of the entry to revert to
*/
static revertTo(entryId) {
const entry = FinetuneHistory.getById(entryId, this.currentControllerSerialNumber);
if (!entry) {
alert('Calibration settings not found.');
return;
}
// Export revert function to window for onclick handlers
window.calibration_history_pending_revert_id = entryId;
window.calibration_history_pending_revert_data = entry.data;
// Show confirmation dialog
const confirmMsg = `Revert to this version?\n\nThis will restore the stored finetune settings.`;
if (confirm(confirmMsg)) {
this._executeRevert(entryId, entry.data);
}
}
/**
* Execute the revert operation
* @private
*/
static _executeRevert(entryId, finetuneData) {
// Call the revert function exposed in core.js
if (typeof window.apply_finetune_revert === 'function') {
window.apply_finetune_revert(finetuneData).then(() => {
this.hide();
alert('Calibration reverted successfully. Remember to save changes permanently.');
}).catch(err => {
alert('Failed to revert calibration: ' + err.message);
});
} else {
alert('Controller not ready. Please try again.');
}
}
/**
* Delete a saved entry
* @param {string} entryId - The ID of the entry to delete
*/
static async delete(entryId) {
const entry = FinetuneHistory.getById(entryId, this.currentControllerSerialNumber);
if (!entry) {
return;
}
if (confirm(`Delete this calibration entry?`)) {
FinetuneHistory.delete(entryId, this.currentControllerSerialNumber);
await this._populateHistory();
}
}
/**
* Clear all saved entries
*/
static async clearAll() {
if (confirm('Delete all calibration history for this controller? This cannot be undone.')) {
FinetuneHistory.clearAll(this.currentControllerSerialNumber);
await this._populateHistory();
}
}
}
// Export functions to window for onclick handlers
window.calibration_history_revert = (entryId) => CalibrationHistoryModal.revertTo(entryId);
window.calibration_history_delete = (entryId) => CalibrationHistoryModal.delete(entryId);
window.calibration_history_clear_all = () => CalibrationHistoryModal.clearAll();
window.show_calibration_history_modal = (currentFinetuneData = null, controllerSerialNumber = null) => CalibrationHistoryModal.show(currentFinetuneData, controllerSerialNumber);

View File

@@ -83,10 +83,11 @@ export async function loadAllTemplates() {
const edgeModalHtml = await loadTemplate('edge-modal');
const donateModalHtml = await loadTemplate('donate-modal');
const quickTestModalHtml = await loadTemplate('quick-test-modal');
const calibrationHistoryModalHtml = await loadTemplate('calibration-history-modal');
// Create modals container
const modalsContainer = document.createElement('div');
modalsContainer.id = 'modals-container';
modalsContainer.innerHTML = faqModalHtml + popupModalHtml + finetuneModalHtml + calibCenterModalHtml + welcomeModalHtml + autoCalibCenterModalHtml + rangeModalHtml + edgeProgressModalHtml + edgeModalHtml + donateModalHtml + quickTestModalHtml;
modalsContainer.innerHTML = faqModalHtml + popupModalHtml + finetuneModalHtml + calibCenterModalHtml + welcomeModalHtml + autoCalibCenterModalHtml + rangeModalHtml + edgeProgressModalHtml + edgeModalHtml + donateModalHtml + quickTestModalHtml + calibrationHistoryModalHtml;
document.body.appendChild(modalsContainer);
}

View File

@@ -152,4 +152,30 @@ export function readCookie(name) {
*/
export function eraseCookie(name) {
createCookie(name, "", -1);
}
/**
* Get the appropriate locale for date formatting based on language and timezone
* @returns {string} Locale string for use with toLocaleString()
*/
export function getLocaleForDateFormatting() {
let lang = readCookie('force_lang') || navigator.language;
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Replace en_US with en_UK if timezone does not start with "America"
if (lang.toLowerCase() === 'en_us' && !timezone.startsWith('America')) {
lang = 'en_UK';
}
return lang.replace('_', '-').toLowerCase();
}
/**
* Format a timestamp as a localized date/time string
* @param {number|string} timestamp Unix timestamp or date string
* @returns {string} Formatted date/time string
*/
export function formatLocalizedDate(timestamp) {
const locale = getLocaleForDateFormatting();
return new Date(timestamp).toLocaleString(locale);
}

View File

@@ -0,0 +1,19 @@
<div class="modal fade" id="calibrationHistoryModal" tabindex="-1" aria-labelledby="calibrationHistoryModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5 ds-i18n" id="calibrationHistoryModalLabel">Calibration History</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="historyListContainer">
<p class="text-muted ds-i18n">No saved calibration settings found.</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary ds-i18n" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger ds-i18n" onclick="calibration_history_clear_all()" id="clearAllBtn" style="display: none;">Clear All</button>
</div>
</div>
</div>
</div>