On connecting to a controller, show a message if the last calibration change was not saved

This commit is contained in:
Mathias Malmqvist
2025-11-21 01:35:18 +01:00
parent 55ef87d561
commit dda8fc2ff1
5 changed files with 118 additions and 13 deletions

View File

@@ -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();
}
/**

View File

@@ -100,6 +100,14 @@ class BaseController {
}
}
/**
* Get the serial number of the device
* @returns {Promise<string>} 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');

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -333,13 +333,20 @@ async function continue_connection({data, device}) {
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)
}
// Check for unsaved calibration changes
if (controller.has_changes_to_write) {
show_popup(`<p>${
l("It appears the latest joystick calibration has not been saved.")
}</p><p>${
l("You should save your changes, or reboot the controller if you don't want to keep them.")
}</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) {
if (!controller.has_changes_to_write) {
const finetuneData = await controllerInstance.getInMemoryModuleData();
const serialNumber = await controllerInstance.getSerialNumber();
FinetuneHistory.save(finetuneData, serialNumber);
}
}