Files
dualshock-tools.github.io/js/modals/finetune-modal.js
2025-09-20 17:01:11 +02:00

755 lines
23 KiB
JavaScript

'use strict';
import { draw_stick_position } from '../stick-renderer.js';
import { dec2hex32, float_to_str } from '../utils.js';
const FINETUNE_INPUT_SUFFIXES = ["LL", "LT", "RL", "RT", "LR", "LB", "RR", "RB", "LX", "LY", "RX", "RY"];
/**
* DS5 Finetuning Class
* Handles controller stick calibration and fine-tuning operations
*/
export class Finetune {
constructor() {
this._mode = 'center'; // 'center' or 'circularity'
this.original_data = [];
this.last_written_data = [];
this.active_stick = null; // 'left', 'right', or null
// Dependencies
this.controller = null;
this.ll_data = null;
this.rr_data = null;
this.clearCircularity = null;
// Closure functions
this.refresh_finetune_sticks = this._createRefreshSticksThrottled();
this.update_finetune_warning_messages = this._createUpdateWarningMessagesClosure();
this.flash_finetune_warning = this._createFlashWarningClosure();
// Continuous adjustment state
this.continuous_adjustment = {
initial_delay: null,
repeat_delay: null,
};
}
get mode() {
return this._mode;
}
set mode(mode) {
if (mode !== 'center' && mode !== 'circularity') {
throw new Error(`Invalid finetune mode: ${mode}. Must be 'center' or 'circularity'`);
}
this._mode = mode;
this._updateUI();
}
async init(controllerInstance, { ll_data, rr_data, clear_circularity }) {
this.controller = controllerInstance;
this.ll_data = ll_data;
this.rr_data = rr_data;
this.clearCircularity = clear_circularity;
this._initEventListeners();
this._restoreShowRawNumbersCheckbox();
// Lock NVS before
const nv = await this.controller.queryNvStatus();
if(!nv.locked) {
const res = await this.controller.nvsLock();
if(!res.ok) {
return;
}
const nv2 = await this.controller.queryNvStatus();
if(!nv2.locked) {
const errTxt = "0x" + dec2hex32(nv2.raw);
throw new Error("ERROR: Cannot lock NVS (" + errTxt + ")");
}
} else if(nv.status !== 'locked') {
throw new Error("ERROR: Cannot read NVS status. Finetuning is not safe on this device.");
}
const data = await this._readFinetuneData();
const modal = new bootstrap.Modal(document.getElementById('finetuneModal'), {})
modal.show();
const maxValue = this.controller.getFinetuneMaxValue();
FINETUNE_INPUT_SUFFIXES.forEach((suffix, i) => {
const el = $("#finetune" + suffix);
el.attr('max', maxValue);
el.val(data[i]);
});
// Start in center mode
this.setMode('center');
this.setStickToFinetune('left');
// Initialize the raw numbers display state
this._showRawNumbersChanged();
this.original_data = data;
this.refresh_finetune_sticks();
}
/**
* Initialize event listeners for the finetune modal
*/
_initEventListeners() {
FINETUNE_INPUT_SUFFIXES.forEach((suffix) => {
$("#finetune" + suffix).on('change', () => this._onFinetuneChange());
});
// Set up mode toggle event listeners
$("#finetuneModeCenter").on('change', (e) => {
if (e.target.checked) {
this.setMode('center');
}
});
$("#finetuneModeCircularity").on('change', (e) => {
if (e.target.checked) {
this.setMode('circularity');
}
});
$("#showRawNumbersCheckbox").on('change', () => {
this._showRawNumbersChanged();
});
$("#left-stick-card").on('click', () => {
console.log("Left stick card clicked");
this.setStickToFinetune('left');
});
$("#right-stick-card").on('click', () => {
this.setStickToFinetune('right');
});
$('#finetuneModal').on('hidden.bs.modal', () => {
console.log("Finetune modal hidden event triggered");
destroyCurrentInstance();
});
}
/**
* Clean up event listeners for the finetune modal
*/
removeEventListeners() {
FINETUNE_INPUT_SUFFIXES.forEach((suffix) => {
$("#finetune" + suffix).off('change');
});
// Remove mode toggle event listeners
$("#finetuneModeCenter").off('change');
$("#finetuneModeCircularity").off('change');
// Remove other event listeners
$("#showRawNumbersCheckbox").off('change');
$("#left-stick-card").off('click');
$("#right-stick-card").off('click');
$('#finetuneModal').off('hidden.bs.modal');
}
/**
* Handle mode switching based on controller input
*/
handleModeSwitching(changes) {
if (changes.l1) {
this.setMode('center');
this._clearFinetuneAxisHighlights();
} else if (changes.r1) {
this.setMode('circularity');
this._clearFinetuneAxisHighlights();
}
}
/**
* Handle stick switching based on controller input
*/
handleStickSwitching(changes) {
if (changes.sticks) {
this._updateActiveStickBasedOnMovement();
}
}
/**
* Handle D-pad adjustments for finetuning
*/
handleDpadAdjustment(changes) {
if(!this.active_stick) return;
if (this._mode === 'center') {
this._handleCenterModeAdjustment(changes);
} else {
this._handleCircularityModeAdjustment(changes);
}
}
/**
* Save finetune changes
*/
save() {
// Unlock save button
this.controller.setHasChangesToWrite(true);
this._close();
}
/**
* Cancel finetune changes and restore original data
*/
async cancel() {
if(this.original_data.length == 12)
await this._writeFinetuneData(this.original_data)
this._close();
}
/**
* Set the finetune mode
*/
setMode(mode) {
this.mode = mode;
}
/**
* Set which stick to finetune
*/
setStickToFinetune(stick) {
if(this.active_stick === stick) {
return;
}
// Stop any continuous adjustments when switching sticks
this.stopContinuousDpadAdjustment();
this._clearFinetuneAxisHighlights();
this.active_stick = stick;
const other_stick = stick === 'left' ? 'right' : 'left';
$(`#${this.active_stick}-stick-card`).addClass("stick-card-active");
$(`#${other_stick}-stick-card`).removeClass("stick-card-active");
}
// Private methods
/**
* Restore the show raw numbers checkbox state from localStorage
*/
_restoreShowRawNumbersCheckbox() {
const savedState = localStorage.getItem('showRawNumbersCheckbox');
if (savedState) {
const isChecked = savedState === 'true';
$("#showRawNumbersCheckbox").prop('checked', isChecked);
}
}
/**
* Check if stick is in extreme position (close to edges)
* @param {Object} stick - Stick object with x and y properties
* @returns {boolean} True if stick is in extreme position
*/
_isStickInExtremePosition(stick) {
const primeAxis = Math.max(Math.abs(stick.x), Math.abs(stick.y));
const otherAxis = Math.min(Math.abs(stick.x), Math.abs(stick.y));
return primeAxis >= 0.5 && otherAxis < 0.2;
}
_updateUI() {
// Clear circularity data - we'll call this from core.js
this.clearCircularity();
const modal = $('#finetuneModal');
if (this._mode === 'center') {
$("#finetuneModeCenter").prop('checked', true);
modal.removeClass('circularity-mode');
} else if (this._mode === 'circularity') {
$("#finetuneModeCircularity").prop('checked', true);
modal.addClass('circularity-mode');
}
}
async _onFinetuneChange() {
const out = FINETUNE_INPUT_SUFFIXES.map((suffix) => {
const el = $("#finetune" + suffix);
const v = parseInt(el.val());
return isNaN(v) ? 0 : v;
});
await this._writeFinetuneData(out);
}
async _readFinetuneData() {
const data = await ds5_get_inmemory_module_data(); //mm there's also a missing await here
if(!data) {
throw new Error("ERROR: Cannot read calibration data");
}
this.last_written_data = data;
return data;
}
async _writeFinetuneData(data) {
if (data.length != 12) {
return;
}
// const deepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
// if (deepEqual(data, this.last_written_data)) {
// if (data == this.last_written_data) { //mm this will never be true, but fixing it (per above) breaks Edge writes
// return;
// }
this.last_written_data = data
if (this.controller.isConnected()) {
await this.controller.writeFinetuneData(data);
}
}
_createRefreshSticksThrottled() {
let timeout = null;
return () => {
if (timeout) return;
timeout = setTimeout(() => {
const { left, right } = this.controller.button_states.sticks;
this._ds5FinetuneUpdate("finetuneStickCanvasL", left.x, left.y);
this._ds5FinetuneUpdate("finetuneStickCanvasR", right.x, right.y);
this.update_finetune_warning_messages();
this._highlightActiveFinetuneAxis();
timeout = null;
}, 10);
};
}
_createUpdateWarningMessagesClosure() {
let timeout = null; // to stop unnecessary flicker in center mode
return () => {
if(!this.active_stick) return;
const currentStick = this.controller.button_states.sticks[this.active_stick];
if (this._mode === 'center') {
const isNearCenter = Math.abs(currentStick.x) <= 0.5 && Math.abs(currentStick.y) <= 0.5;
if(!isNearCenter && timeout) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
if(this._mode !== 'center') return; // in case it changed during timeout
$('#finetuneCenterSuccess').toggle(isNearCenter);
$('#finetuneCenterWarning').toggle(!isNearCenter);
}, isNearCenter ? 0 : 200);
}
if (this._mode === 'circularity') {
// Check if stick is in extreme position (close to edges)
const isInExtremePosition = this._isStickInExtremePosition(currentStick);
$('#finetuneCircularitySuccess').toggle(isInExtremePosition);
$('#finetuneCircularityWarning').toggle(!isInExtremePosition);
}
};
}
_clearFinetuneAxisHighlights(to_clear = {center: true, circularity: true}) {
const { center, circularity } = to_clear;
if(this._mode === 'center' && center || this._mode === 'circularity' && circularity) {
// Clear label highlights
const labelIds = ["Lx-lbl", "Ly-lbl", "Rx-lbl", "Ry-lbl"];
labelIds.forEach(suffix => {
$(`#finetuneStickCanvas${suffix}`).removeClass("text-primary");
});
}
}
_highlightActiveFinetuneAxis(opts = {}) {
if(!this.active_stick) return;
if (this._mode === 'center') {
const { axis } = opts;
if(!axis) return;
this._clearFinetuneAxisHighlights({center: true});
const labelSuffix = `${this.active_stick === 'left' ? "L" : "R"}${axis.toLowerCase()}`;
$(`#finetuneStickCanvas${labelSuffix}-lbl`).addClass("text-primary");
} else {
this._clearFinetuneAxisHighlights({circularity: true});
const sticks = this.controller.button_states.sticks;
const currentStick = sticks[this.active_stick];
// Only highlight if stick is moved significantly from center
const deadzone = 0.5;
if (Math.abs(currentStick.x) >= deadzone || Math.abs(currentStick.y) >= deadzone) {
const quadrant = this._getStickQuadrant(currentStick.x, currentStick.y);
const inputSuffix = this._getFinetuneInputSuffixForQuadrant(this.active_stick, quadrant);
if (inputSuffix) {
// Highlight the corresponding LX/LY label to observe
const labelId = `finetuneStickCanvas${
this.active_stick === 'left' ? 'L' : 'R'}${
quadrant === 'left' || quadrant === 'right' ? 'x' : 'y'}-lbl`;
$(`#${labelId}`).addClass("text-primary");
}
}
}
}
_ds5FinetuneUpdate(name, plx, ply) {
const showRawNumbers = $("#showRawNumbersCheckbox").is(":checked");
const canvasId = `${name}${showRawNumbers ? '' : '_large'}`;
const c = document.getElementById(canvasId);
if (!c) {
console.error(`Canvas element not found: ${canvasId}`);
return;
}
const ctx = c.getContext("2d");
const margins = showRawNumbers ? 15 : 5;
const radius = c.width / 2 - margins;
const sz = c.width/2 - margins;
const hb = radius + margins;
const yb = radius + margins;
ctx.clearRect(0, 0, c.width, c.height);
const isLeftStick = name === "finetuneStickCanvasL";
const highlight = this.active_stick == (isLeftStick ? 'left' : 'right') && this._isDpadAdjustmentActive();
if (this._mode === 'circularity') {
// Draw stick position with circle
draw_stick_position(ctx, hb, yb, sz, plx, ply, {
circularity_data: isLeftStick ? this.ll_data : this.rr_data,
highlight
});
} else {
// Draw stick position with crosshair
draw_stick_position(ctx, hb, yb, sz, plx, ply, {
enable_zoom_center: true,
highlight
});
}
$("#"+ name + "x-lbl").text(float_to_str(plx, 3));
$("#"+ name + "y-lbl").text(float_to_str(ply, 3));
}
_showRawNumbersChanged() {
const showRawNumbers = $("#showRawNumbersCheckbox").is(":checked");
const modal = $("#finetuneModal");
modal.toggleClass("hide-raw-numbers", !showRawNumbers);
localStorage.setItem('showRawNumbersCheckbox', showRawNumbers);
this.refresh_finetune_sticks();
}
_close() {
console.log("Closing finetune modal");
$("#finetuneModal").modal("hide");
}
_isStickAwayFromCenter(stick_pos, deadzone = 0.2) {
return Math.abs(stick_pos.x) >= deadzone || Math.abs(stick_pos.y) >= deadzone;
}
_updateActiveStickBasedOnMovement() {
const sticks = this.controller.button_states.sticks;
const deadzone = 0.2;
const left_is_away = this._isStickAwayFromCenter(sticks.left, deadzone);
const right_is_away = this._isStickAwayFromCenter(sticks.right, deadzone);
if (left_is_away && right_is_away) {
// Both sticks are away from center - clear highlighting
this._clearActiveStick();
} else if (left_is_away && !right_is_away) {
// Only left stick is away from center
this.setStickToFinetune('left');
} else if (right_is_away && !left_is_away) {
// Only right stick is away from center
this.setStickToFinetune('right');
}
// If both sticks are centered, keep current active stick (no change)
}
_clearActiveStick() {
// Remove active class from both cards
$("#left-stick-card").removeClass("stick-card-active");
$("#right-stick-card").removeClass("stick-card-active");
this.active_stick = null; // Clear active stick
this._clearFinetuneAxisHighlights();
}
_getStickQuadrant(x, y) {
// Determine which quadrant the stick is in based on x,y coordinates
// x and y are normalized values between -1 and 1
if (Math.abs(x) > Math.abs(y)) {
return x > 0 ? 'right' : 'left';
} else {
return y > 0 ? 'down' : 'up';
}
}
_getFinetuneInputSuffixForQuadrant(stick, quadrant) {
// This function should only be used in circularity mode
// In center mode, we don't care about quadrants - use direct axis mapping instead
if (this._mode === 'center') {
// This function shouldn't be called in center mode
console.warn('get_finetune_input_suffix_for_quadrant called in center mode - this should not happen');
return null;
}
// Circularity mode: map quadrants to specific calibration points
if (stick === 'left') {
switch (quadrant) {
case 'left': return "LL";
case 'up': return "LT";
case 'right': return "LR";
case 'down': return "LB";
}
} else if (stick === 'right') {
switch (quadrant) {
case 'left': return "RL";
case 'up': return "RT";
case 'right': return "RR";
case 'down': return "RB";
}
}
return null; // Invalid
}
_handleCenterModeAdjustment(changes) {
const adjustmentStep = 5; // Use consistent step size for center mode
// Define button mappings for center mode
const buttonMappings = [
{ buttons: ['left', 'square'], adjustment: adjustmentStep, axis: 'X' },
{ buttons: ['right', 'circle'], adjustment: -adjustmentStep, axis: 'X' },
{ buttons: ['up', 'triangle'], adjustment: adjustmentStep, axis: 'Y' },
{ buttons: ['down', 'cross'], adjustment: -adjustmentStep, axis: 'Y' }
];
// Check if any relevant button was released
const relevantButtons = ['left', 'right', 'square', 'circle', 'up', 'down', 'triangle', 'cross'];
if (relevantButtons.some(button => changes[button] === false)) {
this.stopContinuousDpadAdjustment();
return;
}
// Check for button presses
for (const mapping of buttonMappings) {
// Check if active stick is away from center (> 0.5)
const sticks = this.controller.button_states.sticks;
const currentStick = sticks[this.active_stick];
const stickAwayFromCenter = Math.abs(currentStick.x) > 0.5 || Math.abs(currentStick.y) > 0.5;
if (stickAwayFromCenter && this._isNavigationKeyPressed()) {
this.flash_finetune_warning();
return;
}
if (mapping.buttons.some(button => changes[button])) {
this._highlightActiveFinetuneAxis({axis: mapping.axis});
this._startContinuousDpadAdjustmentCenterMode(this.active_stick, mapping.axis, mapping.adjustment);
return;
}
}
}
_isNavigationKeyPressed() {
const nav_buttons = ['left', 'right', 'up', 'down', 'square', 'circle', 'triangle', 'cross'];
return nav_buttons.some(button => this.controller.button_states[button] === true);
}
_createFlashWarningClosure() {
let timeout = null;
return () => {
function toggle() {
$("#finetuneCenterWarning").toggleClass(['alert-warning', 'alert-danger']);
$("#finetuneCircularityWarning").toggleClass(['alert-warning', 'alert-danger']);
}
if(timeout) return;
toggle(); // on
timeout = setTimeout(() => {
toggle(); // off
timeout = null;
}, 300);
};
}
_handleCircularityModeAdjustment({sticks: _, ...changes}) {
const sticks = this.controller.button_states.sticks;
const currentStick = sticks[this.active_stick];
// Only adjust if stick is moved significantly from center
const isInExtremePosition = this._isStickInExtremePosition(currentStick);
if (!isInExtremePosition) {
this.stopContinuousDpadAdjustment();
if(this._isNavigationKeyPressed()) {
this.flash_finetune_warning();
}
return;
}
const quadrant = this._getStickQuadrant(currentStick.x, currentStick.y);
// Use different step sizes based on quadrant - right/down values are much larger
const adjustmentStep = (quadrant === 'right' || quadrant === 'down') ? 15 : 3;
// Define button mappings for each quadrant type
const horizontalButtons = ['left', 'right', 'square', 'circle'];
const verticalButtons = ['up', 'down', 'triangle', 'cross'];
let adjustment = 0;
let relevantButtons = [];
if (quadrant === 'left' || quadrant === 'right') {
// Horizontal quadrants: left increases, right decreases
relevantButtons = horizontalButtons;
if (changes.left || changes.square) {
adjustment = adjustmentStep;
} else if (changes.right || changes.circle) {
adjustment = -adjustmentStep;
}
} else if (quadrant === 'up' || quadrant === 'down') {
// Vertical quadrants: up increases, down decreases
relevantButtons = verticalButtons;
if (changes.up || changes.triangle) {
adjustment = adjustmentStep;
} else if (changes.down || changes.cross) {
adjustment = -adjustmentStep;
}
}
// Check if any relevant button was released
if (relevantButtons.some(button => changes[button] === false)) {
this.stopContinuousDpadAdjustment();
return;
}
// Start continuous adjustment on button press
if (adjustment !== 0) {
this._startContinuousDpadAdjustment(this.active_stick, quadrant, adjustment);
}
}
_startContinuousDpadAdjustment(stick, quadrant, adjustment) {
const inputSuffix = this._getFinetuneInputSuffixForQuadrant(stick, quadrant);
this._startContinuousAdjustmentWithSuffix(inputSuffix, adjustment);
}
_startContinuousDpadAdjustmentCenterMode(stick, targetAxis, adjustment) {
// In center mode, directly map to X/Y axes
const inputSuffix = stick === 'left' ?
(targetAxis === 'X' ? 'LX' : 'LY') :
(targetAxis === 'X' ? 'RX' : 'RY');
this._startContinuousAdjustmentWithSuffix(inputSuffix, adjustment);
}
_startContinuousAdjustmentWithSuffix(inputSuffix, adjustment) {
this.stopContinuousDpadAdjustment();
const element = $(`#finetune${inputSuffix}`);
if (!element.length) return;
// Perform initial adjustment immediately...
this._performDpadAdjustment(element, adjustment);
this.clearCircularity();
// ...then prime continuous adjustment
this.continuous_adjustment.initial_delay = setTimeout(() => {
this.continuous_adjustment.repeat_delay = setInterval(() => {
this._performDpadAdjustment(element, adjustment);
this.clearCircularity();
}, 150);
}, 400); // Initial delay before continuous adjustment starts (400ms)
}
stopContinuousDpadAdjustment() {
clearInterval(this.continuous_adjustment.repeat_delay);
this.continuous_adjustment.repeat_delay = null;
clearTimeout(this.continuous_adjustment.initial_delay);
this.continuous_adjustment.initial_delay = null;
}
_isDpadAdjustmentActive() {
return !!this.continuous_adjustment.initial_delay;
}
async _performDpadAdjustment(element, adjustment) {
const currentValue = parseInt(element.val()) || 0;
const maxValue = this.controller.getFinetuneMaxValue();
const newValue = Math.max(0, Math.min(maxValue, currentValue + adjustment));
element.val(newValue);
// Trigger the change event to update the finetune data
await this._onFinetuneChange();
}
}
// Global reference to the current finetune instance
let currentFinetuneInstance = null;
/**
* Helper function to safely clear the current finetune instance
*/
function destroyCurrentInstance() {
if (currentFinetuneInstance) {
currentFinetuneInstance.stopContinuousDpadAdjustment();
currentFinetuneInstance.removeEventListeners();
currentFinetuneInstance = null;
}
}
// Function to create and initialize finetune instance
export async function ds5_finetune(controller, dependencies) {
// Create new instance
currentFinetuneInstance = new Finetune();
await currentFinetuneInstance.init(controller, dependencies);
}
export function finetune_handle_controller_input(changes) {
if (currentFinetuneInstance) {
currentFinetuneInstance.refresh_finetune_sticks();
currentFinetuneInstance.handleModeSwitching(changes);
currentFinetuneInstance.handleStickSwitching(changes);
currentFinetuneInstance.handleDpadAdjustment(changes);
}
}
function finetune_save() {
console.log("Saving finetune changes");
if (currentFinetuneInstance) {
currentFinetuneInstance.save();
}
}
async function finetune_cancel() {
console.log("Cancelling finetune changes");
if (currentFinetuneInstance) {
await currentFinetuneInstance.cancel();
}
}
export function isFinetuneVisible() {
return !!currentFinetuneInstance;
}
window.finetune_cancel = finetune_cancel;
window.finetune_save = finetune_save;