'use strict';
import { sleep, float_to_str, dec2hex, dec2hex32, lerp_color, initAnalyticsApi, la } from './utils.js';
import { Storage } from './storage.js';
import { initControllerManager } from './controller-manager.js';
import ControllerFactory from './controllers/controller-factory.js';
import { lang_init, l } from './translations.js';
import { loadAllTemplates } from './template-loader.js';
import { draw_stick_dial, CIRCULARITY_DATA_SIZE, calculateCircularityError } from './stick-renderer.js';
import { ds5_finetune, isFinetuneVisible, finetune_handle_controller_input } from './modals/finetune-modal.js';
import { calibrate_stick_centers, auto_calibrate_stick_centers } from './modals/calib-center-modal.js';
import { calibrate_range, rangeCalibHandleControllerInput } from './modals/calib-range-modal.js';
import {
show_quick_test_modal,
isQuickTestVisible,
quicktest_handle_controller_input
} from './modals/quick-test-modal.js';
import { show_calibration_history_modal } from './modals/calibration-history-modal.js';
import { FinetuneHistory } from './finetune-history.js';
// Application State - manages app-wide state and UI
const app = {
// Button disable state management
disable_btn: 0,
last_disable_btn: 0,
shownRangeCalibrationWarning: false,
failedCalibrationDetectionsCount: 0,
failedCalibrationModalShownCount: 0,
// Calibration method preference
centerCalibrationMethod: 'four-step', // 'quick' or 'four-step'
rangeCalibrationMethod: 'normal', // 'normal' or 'expert'
// Language and UI state
lang_orig_text: {},
lang_orig_text: {},
lang_cur: {},
lang_disabled: true,
lang_cur_direction: "ltr",
// Session tracking
gj: 0,
gu: 0
};
const ll_data = new Array(CIRCULARITY_DATA_SIZE);
const rr_data = new Array(CIRCULARITY_DATA_SIZE);
let controller = null;
function gboot() {
app.gu = crypto.randomUUID();
async function initializeApp() {
window.addEventListener("error", (event) => {
console.error(event.error?.stack || event.message);
show_popup(event.error?.message || event.message);
});
window.addEventListener("unhandledrejection", async (event) => {
console.error("Unhandled rejection:", event.reason?.stack || event.reason);
close_all_modals();
// show_popup(event.reason?.message || event.reason);
// Format the error message for better readability
let errorMessage = "An unexpected error occurred";
if (event.reason) {
if (event.reason.message) {
errorMessage = `Error: ${event.reason.message}`;
} else if (typeof event.reason === 'string') {
errorMessage = `Error: ${event.reason}`;
}
// Collect all stack traces (main error and causes) for a single expandable section
let allStackTraces = '';
if (event.reason.stack) {
const stackTrace = event.reason.stack.replace(/\n/g, '
').replace(/ /g, ' ');
allStackTraces += `Main Error Stack:
${stackTrace}`;
}
// Add error chain information if available (ES2022 error chaining)
let currentError = event.reason;
let chainLevel = 0;
while (currentError?.cause && chainLevel < 5) {
chainLevel++;
currentError = currentError.cause;
if (currentError.stack) {
const causeStackTrace = currentError.stack.replace(/\n/g, '
').replace(/ /g, ' ');
if (allStackTraces) allStackTraces += '
';
allStackTraces += `Cause ${chainLevel} Stack:
${causeStackTrace}`;
}
}
// Add single expandable section if we have any stack traces
if (allStackTraces) {
errorMessage += `
Details
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 to revert back to the previous state.") }
`, true); } // Save finetune parameters for DS5 and Edge controllers if (model === "DS5" || model === "DS5_Edge") { if (!controller.has_changes_to_write) { const finetuneData = await controllerInstance.getInMemoryModuleData(); const serialNumber = await controllerInstance.getSerialNumber(); FinetuneHistory.save(finetuneData, serialNumber); } } } catch(err) { await disconnect(); throw err; } finally { $("#btnconnect").prop("disabled", false); $("#connectspinner").hide(); } } async function disconnect() { la("disconnect"); if(!controller?.isConnected()) { controller = null; return; } app.gj = 0; app.disable_btn = 0; app.shownRangeCalibrationWarning = false; app.failedCalibrationDetectionsCount = 0; update_disable_btn(); await controller.disconnect(); controller = null; // Tear everything down close_all_modals(); $("#offlinebar").show(); $("#onlinebar").hide(); $("#mainmenu").hide(); $("#aboutdrift").show(); updateLastConnectedInfo(); } function updateLastConnectedInfo() { const $lastConnected = $("#lastConnected"); const $infoDiv = $("#lastConnectedInfo"); const info = Storage.lastConnectedController.get(); if (!info) { console.log("No last connected info found.", $lastConnected); $lastConnected.hide(); return; } try { const parts = []; if (info.color) parts.push(l(info.color)); if (info.boardModel) parts.push(info.boardModel); if (info.deviceName) parts.push(info.deviceName); let text = parts.join(" "); if (info.serialNumber) { text += ", " + l("serial number") + " " + info.serialNumber; } $infoDiv.text(text); if (info.serialNumber) { const hasChanges = Storage.hasChangesState.get(info.serialNumber); const $warning = $("#lastConnectedWarning"); $warning.toggle(hasChanges); } $lastConnected.show(); } catch (error) { console.error("Error parsing last connected info:", error); $lastConnected.hide(); } } // Wrapper function for HTML onclick handlers function disconnectSync() { disconnect().catch(error => { throw new Error("Failed to disconnect", { cause: error }); }); } async function handleDisconnectedDevice(e) { la("disconnected"); console.log("Disconnected: " + e.device.productName) await disconnect(); } function render_nvstatus_to_dom(nv) { if(!nv?.status) { throw new Error("Invalid NVS status data", { cause: nv?.error }); } switch (nv.status) { case 'locked': $("#d-nvstatus").html("" + l("locked") + ""); break; case 'unlocked': $("#d-nvstatus").html("" + l("unlocked") + ""); break; case 'pending_reboot': // Keep consistent styling with unknown/purple, but indicate reboot pending if possible const pendingTxt = nv.raw !== undefined ? ("0x" + dec2hex32(nv.raw)) : String(nv.code ?? ''); $("#d-nvstatus").html("unk " + pendingTxt + ""); break; case 'unknown': const unknownTxt = nv.device === 'ds5' && nv.raw !== undefined ? ("0x" + dec2hex32(nv.raw)) : String(nv.code ?? ''); $("#d-nvstatus").html("unk " + unknownTxt + ""); break; case 'error': $("#d-nvstatus").html("" + l("error") + ""); break; } } async function refresh_nvstatus() { if (!controller.isConnected()) { return null; } return await controller.queryNvStatus(); } function set_edge_progress(score) { $("#dsedge-progress").css({ "width": score + "%" }) } function show_welcome_modal() { const already_accepted = Storage.getString("welcome_accepted"); if(already_accepted == "1") return; bootstrap.Modal.getOrCreateInstance('#welcomeModal').show(); } function welcome_accepted() { la("welcome_accepted"); Storage.setString("welcome_accepted", "1"); $("#welcomeModal").modal("hide"); } async function init_svg_controller(model) { const svgContainer = document.getElementById('controller-svg-placeholder'); // Determine which SVG to load based on controller model let svgFileName; if (model === 'DS4') { svgFileName = 'dualshock-controller.svg'; } else if (model === 'DS5' || model === 'DS5_Edge') { svgFileName = 'dualsense-controller.svg'; } else if (model === 'VR2') { // Disable SVG controller for VR2 svgContainer.innerHTML = ''; return; } else { throw new Error(`Unknown controller model: ${model}`); } let svgContent; // Check if we have bundled assets (production mode) if (window.BUNDLED_ASSETS && window.BUNDLED_ASSETS.svg && window.BUNDLED_ASSETS.svg[svgFileName]) { svgContent = window.BUNDLED_ASSETS.svg[svgFileName]; } else { // Fallback to fetching from server (development mode) const response = await fetch(`assets/${svgFileName}`); if (!response.ok) { throw new Error(`Failed to load controller SVG: ${svgFileName}`); } svgContent = await response.text(); } svgContainer.innerHTML = svgContent; // Reset trackpad bounding box so it's recalculated for the new SVG trackpadBbox = undefined; const lightBlue = '#7ecbff'; const midBlue = '#3399cc'; const dualshock = document.getElementById('Controller'); set_svg_group_color(dualshock, lightBlue); ['Button_outlines', 'Button_outlines_behind', 'L3_outline', 'R3_outline', 'Trackpad_outline'].forEach(id => { const group = document.getElementById(id); set_svg_group_color(group, midBlue); }); ['Controller_infills', 'Button_infills', 'L3_infill', 'R3_infill', 'Trackpad_infill'].forEach(id => { const group = document.getElementById(id); set_svg_group_color(group, 'white'); }); } /** * Collects circularity data for both analog sticks during testing mode. * This function tracks the maximum distance reached at each angular position * around the stick's circular range, creating a polar coordinate map of * stick movement capabilities. */ function collectCircularityData(stickStates, leftData, rightData) { const { left, right } = stickStates || {}; const MAX_N = CIRCULARITY_DATA_SIZE; for(const [stick, data] of [[left, leftData], [right, rightData]]) { if (!stick) return; // Skip if no stick changed position const { x, y } = stick; // Calculate distance from center (magnitude of stick position vector) const distance = Math.sqrt(x * x + y * y); // Convert cartesian coordinates to angular index (0 to MAX_N-1) // atan2 gives angle in radians, convert to array index with proper wrapping const angleIndex = (parseInt(Math.round(Math.atan2(y, x) * MAX_N / 2.0 / Math.PI)) + MAX_N) % MAX_N; // Store maximum distance reached at this angle (for circularity analysis) const oldValue = data[angleIndex] ?? 0; data[angleIndex] = Math.max(oldValue, distance); } } function clear_circularity(leftOrRight = 'both') { if(['left', 'both'].includes(leftOrRight)) ll_data.fill(0); if(['right', 'both'].includes(leftOrRight)) rr_data.fill(0); } function reset_circularity_mode() { clear_circularity(); $("#normalMode").prop('checked', true); refresh_stick_pos(); } function refresh_stick_pos() { if(!controller) return; const hasSingleStick = (controller.currentController?.getNumberOfSticks() == 1); const c = document.getElementById("stickCanvas"); const ctx = c.getContext("2d"); const sz = 60; const yb = 15 + sz; const w = c.width; const hb = hasSingleStick ? w / 2 : 20 + sz; ctx.clearRect(0, 0, c.width, c.height); const { left: { x: plx, y: ply }, right: { x: prx, y: pry } } = controller.button_states.sticks; const enable_zoom_center = center_zoom_checked(); const enable_circ_test = circ_checked(); // Draw left stick draw_stick_dial(ctx, hb, yb, sz, plx, ply, { circularity_data: enable_circ_test ? ll_data : null, enable_zoom_center, }); if(!hasSingleStick) { // Draw right stick draw_stick_dial(ctx, w-hb, yb, sz, prx, pry, { circularity_data: enable_circ_test ? rr_data : null, enable_zoom_center, }); } const precision = enable_zoom_center ? 3 : 2; $("#lx-lbl").text(float_to_str(plx, precision)); $("#ly-lbl").text(float_to_str(ply, precision)); if(!hasSingleStick) { $("#rx-lbl").text(float_to_str(prx, precision)); $("#ry-lbl").text(float_to_str(pry, precision)); } // Move L3 and R3 SVG elements according to stick position try { switch(controller.getModel()) { case "DS4": // These values are tuned for the SVG's coordinate system and visual effect const ds4_max_stick_offset = 25; // L3 center in SVG coordinates (from path: cx=295.63, cy=461.03) const ds4_l3_cx = 295.63, ds4_l3_cy = 461.03; // R3 center in SVG coordinates (from path: cx=662.06, cy=419.78) const ds4_r3_cx = 662.06, ds4_r3_cy = 419.78; const ds4_l3_x = ds4_l3_cx + plx * ds4_max_stick_offset; const ds4_l3_y = ds4_l3_cy + ply * ds4_max_stick_offset; const ds4_l3_group = document.querySelector('g#L3'); ds4_l3_group?.setAttribute('transform', `translate(${ds4_l3_x - ds4_l3_cx},${ds4_l3_y - ds4_l3_cy})`); const ds4_r3_x = ds4_r3_cx + prx * ds4_max_stick_offset; const ds4_r3_y = ds4_r3_cy + pry * ds4_max_stick_offset; const ds4_r3_group = document.querySelector('g#R3'); ds4_r3_group?.setAttribute('transform', `translate(${ds4_r3_x - ds4_r3_cx},${ds4_r3_y - ds4_r3_cy})`); break; case "DS5": case "DS5_Edge": // These values are tuned for the SVG's coordinate system and visual effect const ds5_max_stick_offset = 25; // L3 center in SVG coordinates (from path: cx=295.63, cy=461.03) const ds5_l3_cx = 295.63, ds5_l3_cy = 461.03; // R3 center in SVG coordinates (from path: cx=662.06, cy=419.78) const ds5_r3_cx = 662.06, ds5_r3_cy = 419.78; const ds5_l3_x = ds5_l3_cx + plx * ds5_max_stick_offset; const ds5_l3_y = ds5_l3_cy + ply * ds5_max_stick_offset; const ds5_l3_group = document.querySelector('g#L3'); ds5_l3_group?.setAttribute('transform', `translate(${ds5_l3_x - ds5_l3_cx},${ds5_l3_y - ds5_l3_cy}) scale(0.70)`); const ds5_r3_x = ds5_r3_cx + prx * ds5_max_stick_offset; const ds5_r3_y = ds5_r3_cy + pry * ds5_max_stick_offset; const ds5_r3_group = document.querySelector('g#R3'); ds5_r3_group?.setAttribute('transform', `translate(${ds5_r3_x - ds5_r3_cx},${ds5_r3_y - ds5_r3_cy}) scale(0.70)`); break; default: return; // Unsupported model, skip } } catch (e) { // Fail silently if SVG not present } const circularityCheckIcon = document.getElementById('circularityCheckIcon'); if (!enable_circ_test) { circularityCheckIcon.style.display = 'none'; return; } const ll_error = calculateCircularityError(ll_data); const rr_error = calculateCircularityError(rr_data); const isTooSmall = (ll_error && ll_error < 5 || rr_error && rr_error < 5); circularityCheckIcon.style.display = isTooSmall ? 'block' : 'none'; } const circ_checked = () => $("#checkCircularityMode").is(':checked'); const center_zoom_checked = () => $("#centerZoomMode").is(':checked'); function resetStickDiagrams() { clear_circularity(); refresh_stick_pos(); } // Helper functions to switch display modes function switchTo10xZoomMode() { $("#centerZoomMode").prop('checked', true); resetStickDiagrams(); } function switchToRangeMode() { $("#checkCircularityMode").prop('checked', true); resetStickDiagrams(); } const on_stick_mode_change = () => resetStickDiagrams(); const throttled_refresh_sticks = (() => { let delay = null; return function(changes) { if (!changes.sticks) return; if (delay) return; refresh_stick_pos(); delay = setTimeout(() => { delay = null; refresh_stick_pos(); }, 20); }; })(); const update_stick_graphics = (changes) => throttled_refresh_sticks(changes); function update_battery_status({/* charge_level, cable_connected, is_charging, is_error, */ bat_txt, changed}) { if(changed) { $("#d-bat").html(bat_txt); } } function update_ds_button_svg(changes, BUTTON_MAP) { if (!changes || Object.keys(changes).length === 0) return; const pressedColor = '#1a237e'; // pleasing dark blue // Update L2/R2 analog infill for (const trigger of ['l2', 'r2']) { const key = trigger + '_analog'; if (changes.hasOwnProperty(key)) { const val = changes[key]; const t = val / 255; const color = lerp_color('#ffffff', pressedColor, t); const svg = trigger.toUpperCase() + '_infill'; const infill = document.getElementById(svg); set_svg_group_color(infill, color); // Update percentage text const percentage = Math.round((val / 255) * 100); const percentageText = document.getElementById(trigger.toUpperCase() + '_percentage'); if (percentageText) { percentageText.textContent = `${percentage} %`; percentageText.setAttribute('opacity', percentage > 0 ? '1' : '0'); percentageText.setAttribute('fill', percentage < 35 ? pressedColor : 'white'); } } } // Update dpad buttons for (const dir of ['up', 'right', 'down', 'left']) { if (changes.hasOwnProperty(dir)) { const pressed = changes[dir]; const group = document.getElementById(dir.charAt(0).toUpperCase() + dir.slice(1) + '_infill'); set_svg_group_color(group, pressed ? pressedColor : 'white'); } } // Update other buttons for (const btn of BUTTON_MAP) { if (['up', 'right', 'down', 'left'].includes(btn.name)) continue; // Dpad handled above if (changes.hasOwnProperty(btn.name) && btn.svg) { const pressed = changes[btn.name]; const group = document.getElementById(btn.svg + '_infill'); set_svg_group_color(group, pressed ? pressedColor : 'white'); } } } function set_svg_group_color(group, color) { if (group) { const elements = group.querySelectorAll('path,rect,circle,ellipse,line,polyline,polygon'); elements.forEach(el => { // Set up a smooth transition for fill and stroke if not already set if (!el.style.transition) { el.style.transition = 'fill 0.10s, stroke 0.10s'; } el.setAttribute('fill', color); el.setAttribute('stroke', color); }); } } let hasActiveTouchPoints = false; let trackpadBbox = undefined; function update_touchpad_circles(points) { const hasActivePointsNow = points.some(pt => pt.active); if(!hasActivePointsNow && !hasActiveTouchPoints) return; // Find the Trackpad_infill group in the SVG const svg = document.getElementById('controller-svg'); const trackpad = svg?.querySelector('g#Trackpad_infill'); if (!trackpad) return; // Remove the previous touch points, if any trackpad.querySelectorAll('circle.ds-touch').forEach(c => c.remove()); hasActiveTouchPoints = hasActivePointsNow; trackpadBbox = trackpadBbox ?? trackpad.querySelector('path')?.getBBox(); // Draw up to 2 circles points.forEach((pt, idx) => { if (!pt.active) return; // Map raw x/y to SVG // DS4/DS5 touchpad is 1920x943 units (raw values) const RAW_W = 1920, RAW_H = 943; const pointRadius = trackpadBbox.width * 0.05; const cx = trackpadBbox.x + pointRadius + (pt.x / RAW_W) * (trackpadBbox.width - pointRadius*2); const cy = trackpadBbox.y + pointRadius + (pt.y / RAW_H) * (trackpadBbox.height - pointRadius*2); const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('class', 'ds-touch'); circle.setAttribute('cx', cx); circle.setAttribute('cy', cy); circle.setAttribute('r', pointRadius); circle.setAttribute('fill', idx === 0 ? '#2196f3' : '#e91e63'); circle.setAttribute('fill-opacity', '0.5'); circle.setAttribute('stroke', '#3399cc'); circle.setAttribute('stroke-width', '4'); trackpad.appendChild(circle); }); } function get_current_main_tab() { const mainTabs = document.getElementById('mainTabs'); const activeBtn = mainTabs?.querySelector('.nav-link.active'); return activeBtn?.id || 'controller-tab'; } function get_current_test_tab() { const testsList = document.getElementById('tests-list'); const activeBtn = testsList?.querySelector('.list-group-item.active'); return activeBtn?.id || 'haptic-test-tab'; } function detectFailedRangeCalibration(changes) { if (!changes.sticks || app.shownRangeCalibrationWarning) return; const { left, right } = changes.sticks; const failedCalibration = [left, right].some(({x, y}) => Math.abs(x) + Math.abs(y) == 2); const hasOpenModals = document.querySelectorAll('.modal.show').length > 0; if (failedCalibration && !app.shownRangeCalibrationWarning && !hasOpenModals) { app.failedCalibrationDetectionsCount++; if (app.failedCalibrationDetectionsCount < 5) { return; // require 5 consecutive detections } app.failedCalibrationModalShownCount++; Storage.failedCalibrationCount.set(app.failedCalibrationModalShownCount); app.shownRangeCalibrationWarning = true; if (app.failedCalibrationCount <= 6) { // keep it from getting annoying show_popup(l("Range calibration appears to have failed. Please try again and make sure you rotate the sticks.")); } } } function isRangeCalibrationVisible() { const modal = document.getElementById('rangeModal'); if (!modal) return false; return modal.classList.contains('show'); } // Callback function to handle UI updates after controller input processing function handleControllerInput({ changes, inputConfig, touchPoints, batteryStatus }) { const { buttonMap } = inputConfig; // Open Quick Test modal if options button is pressed while L1 is held down if (changes.options && controller.button_states.l1) { update_ds_button_svg({ l1: false }, buttonMap); // Clear L1 show_quick_test_modal(controller); return; } // Update range calibration modal stick visualization if visible if (isRangeCalibrationVisible() && changes.sticks) { collectCircularityData(changes.sticks, ll_data, rr_data); rangeCalibHandleControllerInput(changes); return; } // Handle Quick Test Modal input (can be open from any tab) if (isQuickTestVisible()) { quicktest_handle_controller_input(changes, batteryStatus); return; } const current_active_tab = get_current_main_tab(); switch (current_active_tab) { case 'controller-tab': // Main controller tab collectCircularityData(changes.sticks, ll_data, rr_data); if(isFinetuneVisible()) { finetune_handle_controller_input(changes); } else { update_stick_graphics(changes); update_ds_button_svg(changes, buttonMap); update_touchpad_circles(touchPoints); detectFailedRangeCalibration(changes); } break; case 'tests-tab': handle_test_input(changes); break; } update_battery_status(batteryStatus); } function handle_test_input(/* changes */) { const current_test_tab = get_current_test_tab(); // Handle different test tabs switch (current_test_tab) { case 'haptic-test-tab': // Handle L2/R2 for haptic feedback const l2 = controller.button_states.l2_analog || 0; const r2 = controller.button_states.r2_analog || 0; if (l2 || r2) { // trigger_haptic_motors(l2, r2); } break; // Add more test tabs here as needed default: console.log("Unknown test tab:", current_test_tab); break; } } function update_disable_btn() { const { disable_btn, last_disable_btn } = app; if(disable_btn == last_disable_btn) return; if(disable_btn == 0) { $(".ds-btn").prop("disabled", false); app.last_disable_btn = 0; return; } // Disable all buttons except Quick Test $(".ds-btn").not("#quick-test-btn").prop("disabled", true); // show only one popup if(disable_btn & 1 && !(last_disable_btn & 1)) { show_popup(l("The device appears to be a clone. All calibration functionality is disabled.")); } else if(disable_btn & 2 && !(last_disable_btn & 2)) { show_popup(l("This DualSense controller has outdated firmware.") + "${l("Sony controllers come from the factory calibrated to have an average circularity error of nearly 10 %, and this is now what games expect. Too perfect circularity can make movements and aim feel stiff and unresponsive in some games.") }
${l("Aim for a circularity error of around 7-9 % for the best playing experience.")}`; show_popup(message, true); } // Alert Management Functions let alertCounter = 0; /** * Push a new alert message to the bottom of the screen * @param {string} message - The message to display * @param {string} type - Bootstrap alert type: 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark' * @param {number} duration - Auto-dismiss duration in milliseconds (0 = no auto-dismiss) * @param {boolean} dismissible - Whether the alert can be manually dismissed * @returns {string} - The ID of the created alert element */ function pushAlert(message, type = 'info', duration = 0, dismissible = true) { const alertContainer = document.getElementById('alert-container'); if (!alertContainer) { console.error('Alert container not found'); return null; } const alertId = `alert-${++alertCounter}`; const alertDiv = document.createElement('div'); alertDiv.id = alertId; alertDiv.className = `alert alert-${type} alert-dismissible fade show`; alertDiv.setAttribute('role', 'alert'); alertDiv.innerHTML = ` ${message} ${dismissible ? '' : ''} `; alertContainer.appendChild(alertDiv); if (duration > 0) { setTimeout(() => { dismissAlert(alertId); }, duration); } return alertId; } function dismissAlert(alertId) { const alertElement = document.getElementById(alertId); if (alertElement) { const bsAlert = new bootstrap.Alert(alertElement); bsAlert.close(); } } function clearAllAlerts() { const alertContainer = document.getElementById('alert-container'); if (alertContainer) { const alerts = alertContainer.querySelectorAll('.alert'); alerts.forEach(alert => { const bsAlert = new bootstrap.Alert(alert); bsAlert.close(); }); } } function successAlert(message, duration = 1_500) { return pushAlert(message, 'success', duration, false); } function errorAlert(message, duration = 15_000) { return pushAlert(message, 'danger', /* duration */); } function warningAlert(message, duration = 8_000) { return pushAlert(message, 'warning', duration); } function infoAlert(message, duration = 5_000) { return pushAlert(message, 'info', duration, false); } // Export functions to global scope for HTML onclick handlers window.gboot = gboot; window.connect = connect; window.disconnect = disconnectSync; window.show_faq_modal = show_faq_modal; window.show_info_tab = show_info_tab; window.copyValueToClipboard = copyValueToClipboard; window.calibrate_stick_centers = () => calibrate_stick_centers( controller, (success, message) => { if (success) { resetStickDiagrams(); infoAlert(message, 2_000); switchTo10xZoomMode(); } } ); window.ds5_finetune = () => ds5_finetune( controller, { ll_data, rr_data, clear_circularity }, (success) => success && switchToRangeMode() ); window.openCalibrationHistoryModal = async () => { let currentFinetuneData = null; let controllerSerialNumber = null; try { if (controller && typeof controller.getInMemoryModuleData === 'function') { currentFinetuneData = await controller.getInMemoryModuleData('finetune'); } 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); } la("calibration_history_modal_open"); await show_calibration_history_modal(controller, currentFinetuneData, controllerSerialNumber, (success, message) => { if(!message) return; success ? infoAlert(message) : errorAlert(message); }); }; window.flash_all_changes = flash_all_changes; window.reboot_controller = reboot_controller; window.refresh_nvstatus = refresh_nvstatus; window.nvsunlock = nvsunlock; // Calibration method selection window.setCenterCalibrationMethod = (method, event) => { if (event) { event.preventDefault(); event.stopPropagation(); } app.centerCalibrationMethod = method; Storage.centerCalibrationMethod.set(method); updateCalibrationMethodUI(); // Close the dropdown const dropdownButton = event?.target?.closest('.dropdown-menu')?.previousElementSibling; if (dropdownButton) { const dropdown = bootstrap.Dropdown.getInstance(dropdownButton); if (dropdown) dropdown.hide(); } }; window.executeSelectedCenterCalibration = () => { if (app.centerCalibrationMethod === 'quick') { auto_calibrate_stick_centers( controller, (success, message) => { if (success) { resetStickDiagrams(); infoAlert(message, 2_000); switchTo10xZoomMode(); } } ); } else { calibrate_stick_centers( controller, (success, message) => { if (success) { resetStickDiagrams(); infoAlert(message, 2_000); switchTo10xZoomMode(); } } ); } }; window.setRangeCalibrationMethod = (method, event) => { if (event) { event.preventDefault(); event.stopPropagation(); } app.rangeCalibrationMethod = method; Storage.rangeCalibrationMethod.set(method); updateCalibrationMethodUI(); // Close the dropdown const dropdownButton = event?.target?.closest('.dropdown-menu')?.previousElementSibling; if (dropdownButton) { const dropdown = bootstrap.Dropdown.getInstance(dropdownButton); if (dropdown) dropdown.hide(); } }; window.executeSelectedRangeCalibration = () => { calibrate_range( controller, { ll_data, rr_data }, (success, message) => { resetStickDiagrams(); if(message) { infoAlert(message, 2_000); } switchToRangeMode(); }, app.rangeCalibrationMethod === 'expert' ); }; function updateCalibrationMethodUI() { $('#check-quick').toggle(app.centerCalibrationMethod === 'quick'); $('#check-four-step').toggle(app.centerCalibrationMethod === 'four-step'); $('#check-range-normal').toggle(app.rangeCalibrationMethod === 'normal'); $('#check-range-expert').toggle(app.rangeCalibrationMethod === 'expert'); } function initCalibrationMethod() { const savedCenterMethod = Storage.centerCalibrationMethod.get(); if (savedCenterMethod && (savedCenterMethod === 'quick' || savedCenterMethod === 'four-step')) { app.centerCalibrationMethod = savedCenterMethod; } const savedRangeMethod = Storage.rangeCalibrationMethod.get(); if (savedRangeMethod && (savedRangeMethod === 'normal' || savedRangeMethod === 'expert')) { app.rangeCalibrationMethod = savedRangeMethod; } const savedFailedCalibrationCount = Storage.failedCalibrationCount.get(); if (savedFailedCalibrationCount > 0) { app.failedCalibrationModalShownCount = savedFailedCalibrationCount; } updateCalibrationMethodUI(); } window.nvslock = nvslock; window.welcome_accepted = welcome_accepted; window.show_donate_modal = show_donate_modal; window.show_circularity_warning = show_circularity_warning; window.show_quick_test_modal = () => { show_quick_test_modal(controller).catch(error => { throw new Error("Failed to show quick test modal", { cause: error }); }); }; // Auto-initialize the application when the module loads gboot();