From dda8fc2ff17139c7b6e02d04b9eea7be315dc41a Mon Sep 17 00:00:00 2001 From: Mathias Malmqvist Date: Fri, 21 Nov 2025 01:35:18 +0100 Subject: [PATCH] On connecting to a controller, show a message if the last calibration change was not saved --- js/controller-manager.js | 98 ++++++++++++++++++++++++++++--- js/controllers/base-controller.js | 8 +++ js/controllers/ds4-controller.js | 4 ++ js/controllers/ds5-controller.js | 4 ++ js/core.js | 17 ++++-- 5 files changed, 118 insertions(+), 13 deletions(-) diff --git a/js/controller-manager.js b/js/controller-manager.js index d4da53e..4d1b4b6 100644 --- a/js/controller-manager.js +++ b/js/controller-manager.js @@ -45,12 +45,97 @@ class ControllerManager { this._lastBatteryText = ""; } + /** + * Generate a unique storage key for the device + * @param {string} serialNumber The device serial number + * @returns {string} Storage key based on serial number + */ + _getDeviceStorageKey(serialNumber) { + if (!serialNumber) return null; + return `changes_${serialNumber}`; + } + + /** + * Save has_changes_to_write state to localStorage + */ + async _saveHasChangesState() { + if (!this.currentController) return; + try { + const serialNumber = await this.currentController.getSerialNumber(); + const key = this._getDeviceStorageKey(serialNumber); + if (key) { + localStorage.setItem(key, JSON.stringify(this.has_changes_to_write)); + } + } catch (e) { + console.warn('Failed to save changes state:', e); + } + } + + /** + * Restore has_changes_to_write state from localStorage + */ + async _restoreHasChangesState() { + if (!this.currentController) return; + try { + const serialNumber = await this.currentController.getSerialNumber(); + const key = this._getDeviceStorageKey(serialNumber); + if (key) { + const saved = localStorage.getItem(key); + if (saved !== null) { + try { + const restoredState = JSON.parse(saved); + this.has_changes_to_write = restoredState; + this._updateUI(); + } catch (e) { + console.warn('Failed to parse changes state:', e); + } + } + } + } catch (e) { + console.warn('Failed to restore changes state:', e); + } + } + + /** + * Update UI based on current has_changes_to_write state + */ + _updateUI() { + const saveBtn = $("#savechanges"); + saveBtn + .prop('disabled', !this.has_changes_to_write) + .toggleClass('btn-success', this.has_changes_to_write) + .toggleClass('btn-outline-secondary', !this.has_changes_to_write); + } + + /** + * Clear controller state: remove localStorage entry and reset UI + * @private + */ + async _clearControllerState() { + if (this.currentController) { + try { + const serialNumber = await this.currentController.getSerialNumber(); + const key = this._getDeviceStorageKey(serialNumber); + if (key) { + localStorage.removeItem(key); + } + } catch (e) { + console.warn('Failed to clear localStorage:', e); + } + } + this.has_changes_to_write = false; + this._updateUI(); + } + /** * Set the current controller instance * @param {BaseController} controller Controller instance */ setControllerInstance(instance) { this.currentController = instance; + if (instance) { + this._restoreHasChangesState().catch(e => console.warn('Failed to restore changes state:', e)); + } } /** @@ -160,13 +245,9 @@ class ControllerManager { if (hasChanges === this.has_changes_to_write) return; - const saveBtn = $("#savechanges"); - saveBtn - .prop('disabled', !hasChanges) - .toggleClass('btn-success', hasChanges) - .toggleClass('btn-outline-secondary', !hasChanges); - this.has_changes_to_write = hasChanges; + this._updateUI(); + this._saveHasChangesState().catch(e => console.warn('Failed to save changes state:', e)); } // Unified controller operations that delegate to the current controller @@ -175,7 +256,7 @@ class ControllerManager { * Flash/save changes to the controller */ async flash(progressCallback = null) { - this.setHasChangesToWrite(false); + await this._clearControllerState(); return this.currentController.flash(progressCallback); } @@ -183,7 +264,8 @@ class ControllerManager { * Reset the controller */ async reset() { - await this.currentController.reset(); + await this._clearControllerState(); + return this.currentController.reset(); } /** diff --git a/js/controllers/base-controller.js b/js/controllers/base-controller.js index 69263e5..9d4f4f7 100644 --- a/js/controllers/base-controller.js +++ b/js/controllers/base-controller.js @@ -100,6 +100,14 @@ class BaseController { } } + /** + * Get the serial number of the device + * @returns {Promise} The device serial number + */ + async getSerialNumber() { + throw new Error('getSerialNumber() must be implemented by subclass'); + } + // Abstract methods that must be implemented by subclasses async getInfo() { throw new Error('getInfo() must be implemented by subclass'); diff --git a/js/controllers/ds4-controller.js b/js/controllers/ds4-controller.js index 34cef03..0ea5020 100644 --- a/js/controllers/ds4-controller.js +++ b/js/controllers/ds4-controller.js @@ -131,6 +131,10 @@ class DS4Controller extends BaseController { return DS4_INPUT_CONFIG; } + async getSerialNumber() { + return await this.getBdAddr(); + } + async getInfo() { // Device-only: collect info and return a common structure; do not touch the DOM try { diff --git a/js/controllers/ds5-controller.js b/js/controllers/ds5-controller.js index 0f549f1..cb3fd1a 100644 --- a/js/controllers/ds5-controller.js +++ b/js/controllers/ds5-controller.js @@ -266,6 +266,10 @@ class DS5Controller extends BaseController { return DS5_INPUT_CONFIG; } + async getSerialNumber() { + return await this.getSystemInfo(1, 19, 17); + } + async getInfo() { return this._getInfo(false); } diff --git a/js/core.js b/js/core.js index 4388faf..3693141 100644 --- a/js/core.js +++ b/js/core.js @@ -333,13 +333,20 @@ async function continue_connection({data, device}) { 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) } + // Check for unsaved calibration changes + if (controller.has_changes_to_write) { + show_popup(`

${ + l("It appears the latest joystick calibration has not been saved.") + }

${ + l("You should save your changes, or reboot the controller if you don't want to keep them.") + }

`, 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) { + if (!controller.has_changes_to_write) { + const finetuneData = await controllerInstance.getInMemoryModuleData(); + const serialNumber = await controllerInstance.getSerialNumber(); FinetuneHistory.save(finetuneData, serialNumber); } }