'use strict'; import { sleep, float_to_str, dec2hex, dec2hex32, lerp_color, la, createCookie, readCookie } from './utils.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_position, CIRCULARITY_DATA_SIZE } 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 } from './modals/calib-range-modal.js'; // Application State - manages app-wide state and UI const app = { // Button disable state management disable_btn: 0, last_disable_btn: 0, // 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(); $("#infoshowall").hide(); async function initializeApp() { await loadAllTemplates(); await init_svg_controller(); lang_init(app, handleLanguageChange, show_welcome_modal, la); show_welcome_modal(); $("input[name='displayMode']").on('change', on_stick_mode_change); window.addEventListener("error", (event) => { console.error(event.error?.stack || event.message); show_popup(event.error?.message || event.message); }); window.addEventListener("unhandledRejection", (event) => { console.error("Unhandled rejection:", event.reason?.stack || event.reason); close_all_modals(); show_popup(event.reason?.message || event.reason); // Prevent the default browser behavior (logging to console, again) event.preventDefault(); }); } // Since modules are deferred, DOM might already be loaded if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', initializeApp); } else { // DOM is already loaded, run immediately initializeApp(); } if (!("hid" in navigator)) { $("#offlinebar").hide(); $("#onlinebar").hide(); $("#missinghid").show(); return; } $("#offlinebar").show(); navigator.hid.addEventListener("disconnect", handleDisconnectedDevice); } async function connect() { app.gj = crypto.randomUUID(); // Initialize controller manager with translation function controller = initControllerManager({ l, handleNvStatusUpdate }); controller.setInputHandler(handleControllerInput); la("begin"); reset_circularity_mode(); try { $("#btnconnect").prop("disabled", true); $("#connectspinner").show(); await sleep(100); const supportedModels = ControllerFactory.getSupportedModels(); const requestParams = { filters: supportedModels }; let devices = await navigator.hid.getDevices(); // Already connected? if (devices.length == 0) { devices = await navigator.hid.requestDevice(requestParams); } if (devices.length == 0) { $("#btnconnect").prop("disabled", false); $("#connectspinner").hide(); return; } if (devices.length > 1) { $("#btnconnect").prop("disabled", false); $("#connectspinner").hide(); throw new Error(l("Please connect only one controller at time.")); } const device = devices[0]; if(device.opened) await device.close(); await device.open(); la("connect", {"p": device.productId, "v": device.vendorId}); device.oninputreport = continue_connection } catch(error) { $("#btnconnect").prop("disabled", false); $("#connectspinner").hide(); throw new Error(l("Error: ") + error); } } async function continue_connection({data, device}) { try { if (!controller || controller.isConnected()) { controller?.setInputReportHandler(null); return; } let connected = false; // Detect if the controller is connected via USB const reportLen = data.byteLength; if(reportLen != 63) { $("#btnconnect").prop("disabled", false); $("#connectspinner").hide(); await disconnect(); throw new Error(l("Please connect the device using a USB cable.")); } // Helper to apply basic UI visibility based on device type function applyDeviceUI({ showInfo, showFinetune, showMute, showInfoTab }) { $("#infoshowall").toggle(!!showInfo); $("#ds5finetune").toggle(!!showFinetune); $("#info-tab").toggle(!!showInfoTab); set_mute_visibility(!!showMute); } let controllerInstance = null; let info = null; try { // Create controller instance using factory controllerInstance = ControllerFactory.createControllerInstance(device, { l }); controller.setControllerInstance(controllerInstance); info = await controllerInstance.getInfo(); } catch (error) { $("#btnconnect").prop("disabled", false); $("#connectspinner").hide(); await disconnect(); if (device) { throw new Error(l("Connected invalid device: ") + dec2hex(device.vendorId) + ":" + dec2hex(device.productId)); } else { throw new Error(l("Failed to connect to device")); } } if(!info?.ok) { // Not connected/failed to fetch info $("#btnconnect").prop("disabled", false); $("#connectspinner").hide(); await disconnect(); if(info) console.error(info.error); throw new Error(l("Connected invalid device: ") + l("Error 1")); } connected = true; // Get UI configuration and device name const ui = ControllerFactory.getUIConfig(device.productId); applyDeviceUI(ui); // Assign input processor for stream device.oninputreport = controller.getInputHandler(); const deviceName = ControllerFactory.getDeviceName(device.productId); $("#devname").text(deviceName + " (" + dec2hex(device.vendorId) + ":" + dec2hex(device.productId) + ")"); $("#offlinebar").hide(); $("#onlinebar").show(); $("#mainmenu").show(); $("#resetBtn").show(); $("#d-nvstatus").text = l("Unknown"); $("#d-bdaddr").text = l("Unknown"); $('#controller-tab').tab('show'); const model = controllerInstance.getModel(); // Edge-specific: pending reboot check (from nv) if (model == "DS5_Edge" && info?.pending_reboot) { $("#btnconnect").prop("disabled", false); $("#connectspinner").hide(); await disconnect(); throw new Error(l("A reboot is needed to continue using this DualSense Edge. Please disconnect and reconnect your controller.")); } // Render info collected from device render_info_to_dom(info.infoItems); // Render NV status if (info.nv) { render_nvstatus_to_dom(info.nv); // Optionally try to lock NVS if unlocked if (info.nv.locked === false) { await nvslock(); } } // Apply disable button flags if (typeof info.disable_bits === 'number' && info.disable_bits) { app.disable_btn |= info.disable_bits; } if(app.disable_btn != 0) update_disable_btn(); // DS4 rare notice if (model == "DS4" && info?.rare) { show_popup("Wow, this is a rare/weird controller! Please write me an email at ds4@the.al or contact me on Discord (the_al)"); } // Edge onboarding modal if(model == "DS5_Edge") { show_edge_modal(); } } 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; await controller.disconnect(); controller = null; // Tear everything down close_all_modals(); $("#offlinebar").show(); $("#onlinebar").hide(); $("#mainmenu").hide(); } // Wrapper function for HTML onclick handlers function disconnectSync() { disconnect().catch(error => { console.error("Error during disconnect:", error); show_popup("Error during disconnect: " + error.message); }); } 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"); } 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 = readCookie("welcome_accepted"); if(already_accepted == "1") return; bootstrap.Modal.getOrCreateInstance('#welcomeModal').show(); } function welcome_accepted() { la("welcome_accepted"); createCookie("welcome_accepted", "1"); $("#welcomeModal").modal("hide"); } async function init_svg_controller() { const svgContainer = document.getElementById('controller-svg-placeholder'); const response = await fetch('assets/dualshock-controller.svg'); // load it from separate HTML file if (!response.ok) { throw new Error('Failed to load controller SVG'); } const svgContent = await response.text(); svgContainer.innerHTML = svgContent; const lightBlue = '#7ecbff'; const midBlue = '#3399cc'; const dualshock = document.getElementById('Controller'); set_svg_group_color(dualshock, lightBlue); ['Button_outlines', 'L3_outline', 'R3_outline', 'Trackpad_outline'].forEach(id => { const group = document.getElementById(id); set_svg_group_color(group, midBlue); }); ['Button_infills', 'L3_infill', 'R3_infill', 'Trackpad_infill'].forEach(id => { const group = document.getElementById(id); set_svg_group_color(group, 'white'); }); } function set_mute_visibility(show) { const muteOutline = document.getElementById('Mute_outline'); const muteInfill = document.getElementById('Mute_infill'); if (muteOutline) muteOutline.style.display = show ? '' : 'none'; if (muteInfill) muteInfill.style.display = show ? '' : 'none'; } /** * 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() { ll_data.fill(0); 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 c = document.getElementById("stickCanvas"); const ctx = c.getContext("2d"); const sz = 60; const hb = 20 + sz; const yb = 15 + sz; const w = c.width; 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_position(ctx, hb, yb, sz, plx, ply, { circularity_data: enable_circ_test ? ll_data : null, enable_zoom_center, }); // Draw right stick draw_stick_position(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)); $("#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 { // These values are tuned for the SVG's coordinate system and visual effect const max_stick_offset = 25; // L3 center in SVG coordinates (from path: cx=295.63, cy=461.03) const l3_cx = 295.63, l3_cy = 461.03; // R3 center in SVG coordinates (from path: cx=662.06, cy=419.78) const r3_cx = 662.06, r3_cy = 419.78; const l3_x = l3_cx + plx * max_stick_offset; const l3_y = l3_cy + ply * max_stick_offset; const l3_group = document.querySelector('g#L3'); l3_group?.setAttribute('transform', `translate(${l3_x - l3_cx},${l3_y - l3_cy}) scale(0.70)`); const r3_x = r3_cx + prx * max_stick_offset; const r3_y = r3_cy + pry * max_stick_offset; const r3_group = document.querySelector('g#R3'); r3_group?.setAttribute('transform', `translate(${r3_x - r3_cx},${r3_y - r3_cy}) scale(0.70)`); } catch (e) { // Fail silently if SVG not present } } const circ_checked = () => $("#checkCircularityMode").is(':checked'); const center_zoom_checked = () => $("#centerZoomMode").is(':checked'); function resetStickDiagrams() { clear_circularity(); refresh_stick_pos(); } 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({/* bat_capacity, 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 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'; } // Callback function to handle UI updates after controller input processing function handleControllerInput({ changes, inputConfig, touchPoints, batteryStatus }) { const { buttonMap } = inputConfig; 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); } 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; } $(".ds-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 DS4 clone. All functionalities are disabled.")); } else if(disable_btn & 2 && !(last_disable_btn & 2)) { show_popup(l("This DualSense controller has outdated firmware.") + "
" + l("Please update the firmware and try again."), true); } else if(disable_btn & 4 && !(last_disable_btn & 4)) { show_popup(l("Please charge controller battery over 30% to use this tool.")); } app.last_disable_btn = disable_btn; } async function handleLanguageChange() { if(!controller) return; const { infoItems } = await controller.getDeviceInfo(); render_info_to_dom(infoItems); } function handleNvStatusUpdate(nv) { // Refresh NVS status display when it changes render_nvstatus_to_dom(nv); } async function flash_all_changes() { // For DS5 Edge controllers, pass the progress callback const progressCallback = controller.getModel() == "DS5_Edge" ? set_edge_progress : null; const result = await controller.flash(progressCallback); if (result?.success) { show_popup(result.message, result.isHtml); } } async function reboot_controller() { await controller.reset(); } async function nvsunlock() { await controller.nvsUnlock(); } async function nvslock() { return await controller.nvsLock(); } function close_all_modals() { $('.modal.show').modal('hide'); // Close any open modals } function set_progress(i) { $(".progress-bar").css('width', '' + i + '%') } function render_info_to_dom(infoItems) { // Clear all info sections $("#fwinfo").html(""); $("#fwinfoextra-hw").html(""); $("#fwinfoextra-fw").html(""); if (!Array.isArray(infoItems)) return; // Add new info items infoItems.forEach(({key, value, addInfoIcon, severity, isExtra, cat}) => { if (!key) return; // Compose value with optional info icon let valueHtml = String(value ?? ""); if (addInfoIcon === 'board') { const icon = ' ' + ''; valueHtml += icon; } else if (addInfoIcon === 'color') { const icon = ' ' + ''; valueHtml += icon; } // Apply severity formatting if requested if (severity) { const colors = { danger: 'red', success: 'green' } const color = colors[severity] || 'black'; valueHtml = `${valueHtml}`; } if (isExtra) { append_info_extra(key, valueHtml, cat || "hw"); } else { append_info(key, valueHtml, cat || "hw"); } }); } function append_info_extra(key, value, cat) { // TODO escape html const s = '
' + key + '
' + value + '
'; $("#fwinfoextra-" + cat).html($("#fwinfoextra-" + cat).html() + s); } function append_info(key, value, cat) { // TODO escape html const s = '
' + key + '
' + value + '
'; $("#fwinfo").html($("#fwinfo").html() + s); append_info_extra(key, value, cat); } function show_popup(text, is_html = false) { if(is_html) { $("#popupBody").html(text); } else { $("#popupBody").text(text); } bootstrap.Modal.getOrCreateInstance('#popupModal').show(); } function show_faq_modal() { la("faq_modal"); bootstrap.Modal.getOrCreateInstance('#faqModal').show(); } function show_donate_modal() { la("donate_modal"); bootstrap.Modal.getOrCreateInstance('#donateModal').show(); } function show_edge_modal() { la("edge_modal"); bootstrap.Modal.getOrCreateInstance('#edgeModal').show(); } function show_info_tab() { la("info_modal"); $('#info-tab').tab('show'); } function discord_popup() { la("discord_popup"); show_popup(l("My handle on discord is: the_al")); } function edge_color_info() { la("cm_info"); const text = l("Color detection thanks to") + ' romek77 from Poland.'; show_popup(text, true); } function board_model_info() { la("bm_info"); const l1 = l("This feature is experimental."); const l2 = l("Please let me know if the board model of your controller is not detected correctly."); const l3 = l("Board model detection thanks to") + ' Battle Beaver Customs.'; show_popup(l3 + "

" + l1 + " " + l2, true); } const trigger_haptic_motors = (() => { let haptic_timeout = undefined; let haptic_last_trigger = 0; return async function(strong_motor /*left*/, weak_motor /*right*/) { // The DS4 contoller has a strong (left) and a weak (right) motor. // The DS5 emulates the same behavior, but the left and right motors are the same. const now = Date.now(); if (now - haptic_last_trigger < 200) { return; // Rate limited - ignore calls within 200ms } haptic_last_trigger = now; try { if (!controller.isConnected()) return; const model = controller.getModel(); const device = controller.getDevice(); if (model == "DS4") { const data = new Uint8Array([0x05, 0x00, 0, weak_motor, strong_motor]); await device.sendReport(0x05, data); } else if (model.startsWith("DS5")) { const data = new Uint8Array([0x02, 0x00, weak_motor, strong_motor]); await device.sendReport(0x02, data); } // Stop rumble after duration clearTimeout(haptic_timeout); haptic_timeout = setTimeout(stop_haptic_motors, 250); } catch(e) { throw new Error(l("Error triggering rumble: ") + e); } }; })(); async function stop_haptic_motors() { if (!controller.isConnected()) return; const model = controller.getModel(); const device = controller.getDevice(); if (model == "DS4") { const data = new Uint8Array([0x05, 0x00, 0, 0, 0]); await device.sendReport(0x05, data); } else if (model.startsWith("DS5")) { const data = new Uint8Array([0x02, 0x00, 0, 0]); await device.sendReport(0x02, data); } } // 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.calibrate_range = () => calibrate_range(controller, { resetStickDiagrams, show_popup }); window.calibrate_stick_centers = () => calibrate_stick_centers(controller, { resetStickDiagrams, show_popup, set_progress }); window.auto_calibrate_stick_centers = () => auto_calibrate_stick_centers(controller, { resetStickDiagrams, show_popup, set_progress }); window.ds5_finetune = () => ds5_finetune(controller, { ll_data, rr_data, clear_circularity }); window.flash_all_changes = flash_all_changes; window.reboot_controller = reboot_controller; window.refresh_nvstatus = refresh_nvstatus; window.nvsunlock = nvsunlock; window.nvslock = nvslock; window.welcome_accepted = welcome_accepted; window.show_donate_modal = show_donate_modal; window.board_model_info = board_model_info; window.edge_color_info = edge_color_info;