diff --git a/index.html b/index.html index 908500d..4dce317 100644 --- a/index.html +++ b/index.html @@ -209,6 +209,7 @@
+
diff --git a/js/controllers/controller-factory.js b/js/controllers/controller-factory.js index 1f89a5f..5c84ad3 100644 --- a/js/controllers/controller-factory.js +++ b/js/controllers/controller-factory.js @@ -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 }; } } diff --git a/js/core.js b/js/core.js index 00b8f59..afe9e51 100644 --- a/js/core.js +++ b/js/core.js @@ -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("

Support for PS VR2 controllers is minimal and highly experimental.

I currently don't own these controllers, so I cannot verify the calibration process myself.

If you'd like to help improve full support, you can contribute with a donation or even send the controllers for testing.

Feel free to contact me on Discord (the_al) or by email at ds4@the.al .


Thank you for your support!

"), 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; diff --git a/js/finetune-history.js b/js/finetune-history.js new file mode 100644 index 0000000..40e1d60 --- /dev/null +++ b/js/finetune-history.js @@ -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]); + } +} \ No newline at end of file diff --git a/js/modals/calibration-history-modal.js b/js/modals/calibration-history-modal.js new file mode 100644 index 0000000..10d5ab2 --- /dev/null +++ b/js/modals/calibration-history-modal.js @@ -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 = '

No saved calibration settings found.

'; + document.getElementById('clearAllBtn').style.display = 'none'; + return; + } + + document.getElementById('clearAllBtn').style.display = 'block'; + + let html = '
'; + + history.forEach(entry => { + const date = formatLocalizedDate(entry.timestamp); + const isCurrent = this.currentFinetuneData && this._dataEquals(entry.data, this.currentFinetuneData); + + html += ` +
+
+
+
${date}
+

Values: ${entry.data.join(', ')}

+
+
+ ${isCurrent ? + `` : + ` + ` + } +
+
+
+ `; + }); + + html += '
'; + 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); \ No newline at end of file diff --git a/js/template-loader.js b/js/template-loader.js index 0db4151..c8e7a2b 100644 --- a/js/template-loader.js +++ b/js/template-loader.js @@ -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); } diff --git a/js/utils.js b/js/utils.js index f2befd7..3b82c66 100644 --- a/js/utils.js +++ b/js/utils.js @@ -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); } \ No newline at end of file diff --git a/templates/calibration-history-modal.html b/templates/calibration-history-modal.html new file mode 100644 index 0000000..eeaf33a --- /dev/null +++ b/templates/calibration-history-modal.html @@ -0,0 +1,19 @@ + \ No newline at end of file