From 1a08279f78a80ade2c028651dd8be80f4e29f908 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Wed, 31 Dec 2025 11:38:17 -0600 Subject: [PATCH 01/77] Remember last save directory across file save operations Implement persistent last save directory feature that remembers the directory used in file save dialogs and defaults to it in future save operations. Changes: - Modified dialog.js showSaveDialog wrapper to store and retrieve last directory - Uses electron-store via electronAPI.storeGet/storeSet - Automatically extracts directory from selected file path - Sets as defaultPath for subsequent saves if no path specified - Cross-platform path handling (supports both / and \ separators) Benefits: - Reduces user friction when saving multiple files - Persists across app restarts - Works for all save operations (blackbox logs, diffs, configs, etc.) - No UI changes required - transparent to user - Backwards compatible - gracefully handles no saved directory Closes part of UX improvements for file management. --- js/dialog.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/js/dialog.js b/js/dialog.js index 4e60a09c9..267b8abe2 100644 --- a/js/dialog.js +++ b/js/dialog.js @@ -1,9 +1,35 @@ +const LAST_SAVE_DIRECTORY_KEY = 'lastSaveDirectory'; + const dialog = { showOpenDialog: async function (options) { return window.electronAPI.showOpenDialog(options); }, showSaveDialog: async function (options) { - return window.electronAPI.showSaveDialog(options); + const opts = options || {}; + + // Get the last save directory from storage + const lastDirectory = window.electronAPI.storeGet(LAST_SAVE_DIRECTORY_KEY, null); + + // If we have a last directory and no defaultPath is specified, use it + if (lastDirectory && !opts.defaultPath) { + opts.defaultPath = lastDirectory; + } + + // Show the save dialog + const result = await window.electronAPI.showSaveDialog(opts); + + // If user selected a file (didn't cancel), save the directory for next time + if (result && result.filePath) { + // Extract directory from the full file path + // Get parent directory by removing the filename + const lastSlash = Math.max(result.filePath.lastIndexOf('/'), result.filePath.lastIndexOf('\\')); + if (lastSlash !== -1) { + const directory = result.filePath.substring(0, lastSlash); + window.electronAPI.storeSet(LAST_SAVE_DIRECTORY_KEY, directory); + } + } + + return result; }, alert: function (message) { return window.electronAPI.alertDialog(message); From 4197435cc43834b5d52bac07a41f2fa2b91075cf Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Wed, 31 Dec 2025 12:13:31 -0600 Subject: [PATCH 02/77] Implement remember last save directory in main process Move save directory logic to main process IPC handler where it belongs. The implementation now: - Stores the last used directory in electron-store - Combines saved directory with provided filename using path.join() - Handles edge cases: filename-only, full paths, no saved directory - Preserves directory on cancel (only saves on successful file selection) This fixes the issue where the implementation in renderer process was never called. --- js/dialog.js | 28 +--------------------------- js/main/main.js | 31 +++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/js/dialog.js b/js/dialog.js index 267b8abe2..4e60a09c9 100644 --- a/js/dialog.js +++ b/js/dialog.js @@ -1,35 +1,9 @@ -const LAST_SAVE_DIRECTORY_KEY = 'lastSaveDirectory'; - const dialog = { showOpenDialog: async function (options) { return window.electronAPI.showOpenDialog(options); }, showSaveDialog: async function (options) { - const opts = options || {}; - - // Get the last save directory from storage - const lastDirectory = window.electronAPI.storeGet(LAST_SAVE_DIRECTORY_KEY, null); - - // If we have a last directory and no defaultPath is specified, use it - if (lastDirectory && !opts.defaultPath) { - opts.defaultPath = lastDirectory; - } - - // Show the save dialog - const result = await window.electronAPI.showSaveDialog(opts); - - // If user selected a file (didn't cancel), save the directory for next time - if (result && result.filePath) { - // Extract directory from the full file path - // Get parent directory by removing the filename - const lastSlash = Math.max(result.filePath.lastIndexOf('/'), result.filePath.lastIndexOf('\\')); - if (lastSlash !== -1) { - const directory = result.filePath.substring(0, lastSlash); - window.electronAPI.storeSet(LAST_SAVE_DIRECTORY_KEY, directory); - } - } - - return result; + return window.electronAPI.showSaveDialog(options); }, alert: function (message) { return window.electronAPI.alertDialog(message); diff --git a/js/main/main.js b/js/main/main.js index 34faa50a8..fe3a099c5 100644 --- a/js/main/main.js +++ b/js/main/main.js @@ -280,8 +280,35 @@ app.whenReady().then(() => { return dialog.showOpenDialog(options); }), - ipcMain.handle('dialog.showSaveDialog', (_event, options) => { - return dialog.showSaveDialog(options); + ipcMain.handle('dialog.showSaveDialog', async (_event, options) => { + const opts = options || {}; + const LAST_SAVE_DIRECTORY_KEY = 'lastSaveDirectory'; + + // Get the last save directory from store + const lastDirectory = store.get(LAST_SAVE_DIRECTORY_KEY, null); + + // If we have a last directory, combine it with the filename if one was provided + if (lastDirectory && opts.defaultPath) { + // If defaultPath is just a filename (no directory), prepend the last directory + if (!path.dirname(opts.defaultPath) || path.dirname(opts.defaultPath) === '.') { + opts.defaultPath = path.join(lastDirectory, opts.defaultPath); + } + } else if (lastDirectory && !opts.defaultPath) { + // No filename provided, just use the directory + opts.defaultPath = lastDirectory; + } + + // Show the save dialog + const result = await dialog.showSaveDialog(opts); + + // If user selected a file (didn't cancel), save the directory for next time + if (result && result.filePath && !result.canceled) { + // Extract directory from the full file path (path already imported at top) + const directory = path.dirname(result.filePath); + store.set(LAST_SAVE_DIRECTORY_KEY, directory); + } + + return result; }), ipcMain.on('dialog.alert', (event, message) => { From eccb158a85c12659711dafa25de20e591b492f81 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sat, 17 Jan 2026 17:44:35 -0600 Subject: [PATCH 03/77] Fix firmware flasher board matching for multi-underscore targets The firmware flasher was unable to match boards with multiple underscores in their names (like TBS_LUCID_H7_WING) because the old code only replaced the first underscore with a space, creating a mismatch between dictionary keys and lookup values. Changes: - Add normalizeTargetName() function to convert hyphens to underscores - Fix parseFilename() to replace ALL underscores/hyphens with spaces for display - Key releases dictionary by normalized raw_target instead of modified target - Normalize all lookups to use consistent underscore format - Support both underscore and hyphen filename variants This fixes board selection for targets like TBS_LUCID_H7_WING and enables support for hyphen-based filenames as a workaround (TBS-LUCID-H7-WING). --- tabs/firmware_flasher.js | 44 +++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/tabs/firmware_flasher.js b/tabs/firmware_flasher.js index 580bc81c2..f63a6333d 100755 --- a/tabs/firmware_flasher.js +++ b/tabs/firmware_flasher.js @@ -22,6 +22,13 @@ import store from './../js/store'; import dialog from '../js/dialog.js'; TABS.firmware_flasher = {}; + +// Normalize target names by converting hyphens to underscores for consistent matching +// This allows both TBS_LUCID_H7_WING and TBS-LUCID-H7-WING to match +function normalizeTargetName(name) { + return name.replace(/-/g, '_'); +} + TABS.firmware_flasher.initialize = function (callback) { if (GUI.active_tab != 'firmware_flasher') { @@ -72,7 +79,7 @@ TABS.firmware_flasher.initialize = function (callback) { //var targetFromFilenameExpression = /inav_([\d.]+)?_?([^.]+)\.(.*)/; // inav_8.0.0_TUNERCF405_dev-20240617-88fb1d0.hex // inav_8.0.0_TUNERCF405_ci-20240617-88fb1d0.hex - var targetFromFilenameExpression = /^inav_(\d+)([\d.]+)_([A-Za-z0-9_]+)_(ci|dev)-(\d{4})(\d{2})(\d{2})-(\w+)\.(hex)$/; + var targetFromFilenameExpression = /^inav_(\d+)([\d.]+)_([A-Za-z0-9_-]+)_(ci|dev)-(\d{4})(\d{2})(\d{2})-(\w+)\.(hex)$/; var match = targetFromFilenameExpression.exec(filename); if (!match) { @@ -82,7 +89,7 @@ TABS.firmware_flasher.initialize = function (callback) { return { raw_target: match[3], - target: match[3].replace("_", " "), + target: match[3].replace(/_/g, " ").replace(/-/g, " "), // "/g" to replace all format: match[9], version: match[1]+match[2], major: match[1] @@ -102,7 +109,7 @@ TABS.firmware_flasher.initialize = function (callback) { return { raw_target: match[2], - target: match[2].replace("_", " "), + target: match[2].replace(/_/g, " ").replace(/-/g, " "), // "/g" to replace all format: match[3], }; } @@ -158,8 +165,9 @@ TABS.firmware_flasher.initialize = function (callback) { if ((!showDevReleases && release.prerelease) || !result) { return; } - if($.inArray(result.target, unsortedTargets) == -1) { - unsortedTargets.push(result.target); + var normalizedTarget = normalizeTargetName(result.raw_target); + if($.inArray(normalizedTarget, unsortedTargets) == -1) { + unsortedTargets.push(normalizedTarget); } }); }); @@ -170,8 +178,11 @@ TABS.firmware_flasher.initialize = function (callback) { release.assets.forEach(function (asset) { var result = parseDevFilename(asset.name); - if (result && $.inArray(result.target, unsortedTargets) == -1) { - unsortedTargets.push(result.target); + if (result) { + var normalizedTarget = normalizeTargetName(result.raw_target); + if ($.inArray(normalizedTarget, unsortedTargets) == -1) { + unsortedTargets.push(normalizedTarget); + } } }); }); @@ -220,7 +231,8 @@ TABS.firmware_flasher.initialize = function (callback) { "notes" : release.body, "status" : release.prerelease ? "release-candidate" : "stable" }; - releases[result.target].push(descriptor); + var normalizedTarget = normalizeTargetName(result.raw_target); + releases[normalizedTarget].push(descriptor); }); }); @@ -274,7 +286,8 @@ TABS.firmware_flasher.initialize = function (callback) { "notes" : release.body, "status" : release.prerelease ? "nightly" : "stable" }; - releases[result.target].push(descriptor); + var normalizedTarget = normalizeTargetName(result.raw_target); + releases[normalizedTarget].push(descriptor); }); }); } @@ -287,9 +300,10 @@ TABS.firmware_flasher.initialize = function (callback) { descriptors.forEach(function(descriptor){ if($.inArray(target, selectTargets) == -1) { selectTargets.push(target); + var normalizedTarget = normalizeTargetName(descriptor.raw_target); var select_e = $("".format( - descriptor.raw_target, + normalizedTarget, descriptor.target )).data('summary', descriptor); boards_e.append(select_e); @@ -339,8 +353,9 @@ TABS.firmware_flasher.initialize = function (callback) { versions_e.append($("".format(i18n.getMessage('firmwareFlasherOptionLabelSelectFirmwareVersionFor'), target))); } - if (typeof TABS.firmware_flasher.releases[target]?.forEach === 'function') { - TABS.firmware_flasher.releases[target].forEach(function(descriptor) { + var normalizedTarget = normalizeTargetName(target); + if (typeof TABS.firmware_flasher.releases[normalizedTarget]?.forEach === 'function') { + TABS.firmware_flasher.releases[normalizedTarget].forEach(function(descriptor) { var select_e = $("".format( descriptor.version, @@ -862,14 +877,15 @@ TABS.firmware_flasher.onValidFirmware = function() { MSP.send_message(MSPCodes.MSP_BUILD_INFO, false, false, function () { MSP.send_message(MSPCodes.MSP_BOARD_INFO, false, false, function () { var boardSelect = $('select[name="board"]'); - boardSelect.val(FC.CONFIG.target); + var normalizedTarget = normalizeTargetName(FC.CONFIG.target); + boardSelect.val(normalizedTarget); GUI.log(i18n.getMessage('targetPrefetchsuccessful') + FC.CONFIG.target); TABS.firmware_flasher.closeTempConnection(); // Only trigger change if the board was actually found and selected - if (boardSelect.val() === FC.CONFIG.target) { + if (boardSelect.val() === normalizedTarget) { boardSelect.trigger('change'); } }); From 10407d6a2faea469457f8eddfc824cdcec6dcd7b Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sat, 17 Jan 2026 23:52:37 -0600 Subject: [PATCH 04/77] Refactor: Normalize target names at parse time using target_id field Eliminates 6 of 7 normalizeTargetName() calls by normalizing target names once at the data source (parseFilename/parseDevFilename functions) instead of repeatedly at consumption points. Changes: - Rename raw_target to target_id for semantic clarity - Add normalization in parseDevFilename() and parseFilename() - Remove redundant normalizeTargetName() calls throughout (6 eliminated) - Keep one necessary call for FC.CONFIG.target (external data source) Benefits: - Single normalization per filename parse (better performance) - Impossible to forget normalization (always available in result.target_id) - Clearer code intent and improved maintainability - Net reduction of 4 lines (17 insertions, 21 deletions) --- tabs/firmware_flasher.js | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/tabs/firmware_flasher.js b/tabs/firmware_flasher.js index f63a6333d..ccfbf06b3 100755 --- a/tabs/firmware_flasher.js +++ b/tabs/firmware_flasher.js @@ -87,9 +87,10 @@ TABS.firmware_flasher.initialize = function (callback) { return null; } + var rawMatch = match[3]; // e.g., "TBS-LUCID-H7-WING" or "TBS_LUCID_H7_WING" return { - raw_target: match[3], - target: match[3].replace(/_/g, " ").replace(/-/g, " "), // "/g" to replace all + target_id: normalizeTargetName(rawMatch), // Normalized: "TBS_LUCID_H7_WING" + target: rawMatch.replace(/_/g, " ").replace(/-/g, " "), // Display: "TBS LUCID H7 WING" format: match[9], version: match[1]+match[2], major: match[1] @@ -107,9 +108,10 @@ TABS.firmware_flasher.initialize = function (callback) { //GUI.log("non dev: match[2]: " + match[2] + " match[3]: " + match[3]); + var rawMatch = match[2]; // e.g., "MATEKF405" or "MATEK-F405" return { - raw_target: match[2], - target: match[2].replace(/_/g, " ").replace(/-/g, " "), // "/g" to replace all + target_id: normalizeTargetName(rawMatch), // Normalized: "MATEKF405" + target: rawMatch.replace(/_/g, " ").replace(/-/g, " "), // Display: "MATEKF405" format: match[3], }; } @@ -165,9 +167,8 @@ TABS.firmware_flasher.initialize = function (callback) { if ((!showDevReleases && release.prerelease) || !result) { return; } - var normalizedTarget = normalizeTargetName(result.raw_target); - if($.inArray(normalizedTarget, unsortedTargets) == -1) { - unsortedTargets.push(normalizedTarget); + if($.inArray(result.target_id, unsortedTargets) == -1) { + unsortedTargets.push(result.target_id); } }); }); @@ -179,9 +180,8 @@ TABS.firmware_flasher.initialize = function (callback) { var result = parseDevFilename(asset.name); if (result) { - var normalizedTarget = normalizeTargetName(result.raw_target); - if ($.inArray(normalizedTarget, unsortedTargets) == -1) { - unsortedTargets.push(normalizedTarget); + if ($.inArray(result.target_id, unsortedTargets) == -1) { + unsortedTargets.push(result.target_id); } } }); @@ -225,14 +225,13 @@ TABS.firmware_flasher.initialize = function (callback) { "version" : release.tag_name, "url" : asset.browser_download_url, "file" : asset.name, - "raw_target": result.raw_target, + "target_id" : result.target_id, "target" : result.target, "date" : formattedDate, "notes" : release.body, "status" : release.prerelease ? "release-candidate" : "stable" }; - var normalizedTarget = normalizeTargetName(result.raw_target); - releases[normalizedTarget].push(descriptor); + releases[result.target_id].push(descriptor); }); }); @@ -280,14 +279,13 @@ TABS.firmware_flasher.initialize = function (callback) { "version" : release.tag_name, "url" : asset.browser_download_url, "file" : asset.name, - "raw_target": result.raw_target, + "target_id" : result.target_id, "target" : result.target, "date" : formattedDate, "notes" : release.body, "status" : release.prerelease ? "nightly" : "stable" }; - var normalizedTarget = normalizeTargetName(result.raw_target); - releases[normalizedTarget].push(descriptor); + releases[result.target_id].push(descriptor); }); }); } @@ -300,10 +298,9 @@ TABS.firmware_flasher.initialize = function (callback) { descriptors.forEach(function(descriptor){ if($.inArray(target, selectTargets) == -1) { selectTargets.push(target); - var normalizedTarget = normalizeTargetName(descriptor.raw_target); var select_e = $("".format( - normalizedTarget, + descriptor.target_id, descriptor.target )).data('summary', descriptor); boards_e.append(select_e); @@ -353,9 +350,8 @@ TABS.firmware_flasher.initialize = function (callback) { versions_e.append($("".format(i18n.getMessage('firmwareFlasherOptionLabelSelectFirmwareVersionFor'), target))); } - var normalizedTarget = normalizeTargetName(target); - if (typeof TABS.firmware_flasher.releases[normalizedTarget]?.forEach === 'function') { - TABS.firmware_flasher.releases[normalizedTarget].forEach(function(descriptor) { + if (typeof TABS.firmware_flasher.releases[target]?.forEach === 'function') { + TABS.firmware_flasher.releases[target].forEach(function(descriptor) { var select_e = $("".format( descriptor.version, From d4e8f4110f1ca35b2d69a0e87efa3474deecc48c Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 18 Jan 2026 10:55:06 -0600 Subject: [PATCH 05/77] Clean up: Remove redundant comments - Simplify normalizeTargetName() comment to "Allow hyphens due to 9.0.0 patch" - Remove "Normalized: ..." comments since function name is self-explanatory - Keep "Display: ..." comments for clarity on user-facing strings --- tabs/firmware_flasher.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tabs/firmware_flasher.js b/tabs/firmware_flasher.js index ccfbf06b3..ee6a9ccb6 100755 --- a/tabs/firmware_flasher.js +++ b/tabs/firmware_flasher.js @@ -23,8 +23,7 @@ import dialog from '../js/dialog.js'; TABS.firmware_flasher = {}; -// Normalize target names by converting hyphens to underscores for consistent matching -// This allows both TBS_LUCID_H7_WING and TBS-LUCID-H7-WING to match +// Allow hyphens due to 9.0.0 patch function normalizeTargetName(name) { return name.replace(/-/g, '_'); } @@ -89,7 +88,7 @@ TABS.firmware_flasher.initialize = function (callback) { var rawMatch = match[3]; // e.g., "TBS-LUCID-H7-WING" or "TBS_LUCID_H7_WING" return { - target_id: normalizeTargetName(rawMatch), // Normalized: "TBS_LUCID_H7_WING" + target_id: normalizeTargetName(rawMatch), target: rawMatch.replace(/_/g, " ").replace(/-/g, " "), // Display: "TBS LUCID H7 WING" format: match[9], version: match[1]+match[2], @@ -110,7 +109,7 @@ TABS.firmware_flasher.initialize = function (callback) { var rawMatch = match[2]; // e.g., "MATEKF405" or "MATEK-F405" return { - target_id: normalizeTargetName(rawMatch), // Normalized: "MATEKF405" + target_id: normalizeTargetName(rawMatch), target: rawMatch.replace(/_/g, " ").replace(/-/g, " "), // Display: "MATEKF405" format: match[3], }; From e7f37a439cef074b72a0782ff52b89323fc2a1ea Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Tue, 20 Jan 2026 09:38:16 -0600 Subject: [PATCH 06/77] Add GPS preset configuration UI with auto-detection Implement user-friendly GPS configuration presets for M8/M9/M10 modules: - Add GPS preset dropdown with Auto-detect, Manual, M8, M9, M10, and M10-highperf options - Add GPS update rate input field (previously not exposed in UI) - Extend MSP_GPSSTATISTICS to parse hwVersion for auto-detection - Preset configuration disables (but shows) settings when active - Add preset info box explaining each preset's characteristics Presets configured per manager feedback: - M8: 4 constellations @ 8Hz (conservative) - M9: 4 constellations @ 10Hz (hardware limit: 16 sats) - M10: 4 constellations @ 6Hz (default CPU clock safe rate) - M10-highperf: 4 constellations @ 10Hz (for high-perf clock users) - Manual: Full user control (default for existing users) Auto-detection uses FC.GPS_DATA.hwVersion from MSP_GPSSTATISTICS (firmware extension pending - see documentation). Translation strings added for en locale. Related: GPS optimization research based on Jetrell's M10 testing data --- js/msp/MSPHelper.js | 6 +++ locale/en/messages.json | 18 +++++++ tabs/gps.html | 34 ++++++++++-- tabs/gps.js | 117 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 3 deletions(-) diff --git a/js/msp/MSPHelper.js b/js/msp/MSPHelper.js index 2cc00953d..cb03e98db 100644 --- a/js/msp/MSPHelper.js +++ b/js/msp/MSPHelper.js @@ -198,6 +198,12 @@ var mspHelper = (function () { FC.GPS_DATA.hdop = data.getUint16(14, true); FC.GPS_DATA.eph = data.getUint16(16, true); FC.GPS_DATA.epv = data.getUint16(18, true); + // Check if hwVersion field exists (firmware with extended MSP_GPSSTATISTICS) + if (data.byteLength >= 24) { + FC.GPS_DATA.hwVersion = data.getUint32(20, true); + } else { + FC.GPS_DATA.hwVersion = 0; // Unknown for older firmware + } break; case MSPCodes.MSP2_ADSB_VEHICLE_LIST: var byteOffsetCounter = 0; diff --git a/locale/en/messages.json b/locale/en/messages.json index c16540af6..d1e7a2f8e 100644 --- a/locale/en/messages.json +++ b/locale/en/messages.json @@ -1209,6 +1209,24 @@ "configurationGPSUseGlonass": { "message": "Gps use Glonass Satellites (RU)" }, + "gpsPresetMode": { + "message": "GPS Configuration Preset" + }, + "gpsPresetModeHelp": { + "message": "Choose a preset optimized for your GPS module, or use Manual for custom configuration. Auto-detect will identify your GPS module if connected." + }, + "gpsUpdateRate": { + "message": "GPS Update Rate (Hz)" + }, + "gpsUpdateRateHelp": { + "message": "How often the GPS module sends position updates. Higher rates provide lower latency but may reduce accuracy with multiple constellations on M10 modules." + }, + "gpsAutoDetectFailed": { + "message": "Could not auto-detect GPS module. Please connect flight controller or select manual preset." + }, + "gpsAutoDetectSuccess": { + "message": "GPS module detected:" + }, "tzOffset": { "message": "Timezone Offset" }, diff --git a/tabs/gps.html b/tabs/gps.html index b74cfca21..5fd17743d 100644 --- a/tabs/gps.html +++ b/tabs/gps.html @@ -38,16 +38,44 @@ + +
+ + +
+
+ + + +
+ + +
+
- +
- +
- +
diff --git a/tabs/gps.js b/tabs/gps.js index c882e3737..63c786ce7 100644 --- a/tabs/gps.js +++ b/tabs/gps.js @@ -200,6 +200,123 @@ TABS.gps.initialize = function (callback) { gps_ubx_sbas_e.val(FC.MISC.gps_ubx_sbas); + // GPS Preset Configuration + const GPS_PRESETS = { + m8: { + name: "u-blox M8", + galileo: true, + glonass: true, + beidou: true, + rate: 8, + description: [ + "4 GNSS constellations for maximum accuracy", + "8Hz update rate (conservative for M8)", + "Best for: Navigation, position hold, slower aircraft" + ] + }, + m9: { + name: "u-blox M9", + galileo: true, + glonass: true, + beidou: true, + rate: 10, + description: [ + "4 GNSS constellations (16 satellites at 10Hz due to hardware limit)", + "10Hz update rate", + "Best for: General use, balanced accuracy and responsiveness" + ] + }, + m10: { + name: "u-blox M10", + galileo: true, + glonass: true, + beidou: true, + rate: 6, + description: [ + "4 GNSS constellations for maximum satellites", + "6Hz update rate (safe for M10 default CPU clock)", + "Best for: Long-range, cruise, slower aircraft" + ] + }, + 'm10-highperf': { + name: "u-blox M10 (High-Performance)", + galileo: true, + glonass: true, + beidou: true, + rate: 10, + description: [ + "4 GNSS constellations at full speed", + "10Hz update rate (requires high-performance CPU clock)", + "Only use if you KNOW your M10 has high-performance clock enabled" + ] + }, + manual: { + name: "Manual Settings", + description: [ + "Full control over constellation selection and update rate", + "For advanced users and special requirements" + ] + } + }; + + function detectGPSPreset(hwVersion) { + switch(hwVersion) { + case 800: return 'm8'; + case 900: return 'm9'; + case 1000: return 'm10'; + default: return 'manual'; + } + } + + function applyGPSPreset(presetId) { + const preset = GPS_PRESETS[presetId]; + + if (!preset) return; + + if (presetId === 'manual') { + // Enable all controls + $('.preset-controlled').prop('disabled', false); + $('#gps_ublox_nav_hz').prop('disabled', false); + $('#preset_info').hide(); + } else if (presetId === 'auto') { + // Try to auto-detect from FC + if (FC.GPS_DATA && FC.GPS_DATA.hwVersion) { + const detectedPreset = detectGPSPreset(FC.GPS_DATA.hwVersion); + applyGPSPreset(detectedPreset); + $('#gps_preset_mode').val(detectedPreset); + GUI.log(i18n.getMessage('gpsAutoDetectSuccess') + ' ' + GPS_PRESETS[detectedPreset].name); + } else { + // Fall back to manual if can't detect + applyGPSPreset('manual'); + $('#gps_preset_mode').val('manual'); + GUI.log(i18n.getMessage('gpsAutoDetectFailed')); + } + } else { + // Apply preset values + $('#gps_use_galileo').prop('checked', preset.galileo); + $('#gps_use_glonass').prop('checked', preset.glonass); + $('#gps_use_beidou').prop('checked', preset.beidou); + $('#gps_ublox_nav_hz').val(preset.rate); + + // Disable controls (user can see but not edit) + $('.preset-controlled').prop('disabled', true); + $('#gps_ublox_nav_hz').prop('disabled', true); + + // Show preset info + $('#preset_name').text(preset.name); + $('#preset_details').html(preset.description.map(d => `
  • ${d}
  • `).join('')); + $('#preset_info').show(); + } + } + + // Set up preset mode handler + $('#gps_preset_mode').on('change', function() { + applyGPSPreset($(this).val()); + }); + + // Initialize with manual mode (or auto-detect if available) + applyGPSPreset('manual'); + let mapView = new View({ center: [0, 0], zoom: 15 From ef6b3ad8c67b7f9a7bb2455c4d9900f19ed031c9 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Tue, 20 Jan 2026 10:19:02 -0600 Subject: [PATCH 07/77] Update GPS presets to 3 constellations --- tabs/gps.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tabs/gps.js b/tabs/gps.js index 63c786ce7..2fc693b5c 100644 --- a/tabs/gps.js +++ b/tabs/gps.js @@ -218,10 +218,10 @@ TABS.gps.initialize = function (callback) { name: "u-blox M9", galileo: true, glonass: true, - beidou: true, + beidou: false, rate: 10, description: [ - "4 GNSS constellations (16 satellites at 10Hz due to hardware limit)", + "3 GNSS constellations (GPS+Galileo+Glonass)", "10Hz update rate", "Best for: General use, balanced accuracy and responsiveness" ] @@ -230,10 +230,10 @@ TABS.gps.initialize = function (callback) { name: "u-blox M10", galileo: true, glonass: true, - beidou: true, + beidou: false, rate: 6, description: [ - "4 GNSS constellations for maximum satellites", + "3 GNSS constellations (GPS+Galileo+Glonass)", "6Hz update rate (safe for M10 default CPU clock)", "Best for: Long-range, cruise, slower aircraft" ] @@ -242,10 +242,10 @@ TABS.gps.initialize = function (callback) { name: "u-blox M10 (High-Performance)", galileo: true, glonass: true, - beidou: true, + beidou: false, rate: 10, description: [ - "4 GNSS constellations at full speed", + "3 GNSS constellations (GPS+Galileo+Glonass)", "10Hz update rate (requires high-performance CPU clock)", "Only use if you KNOW your M10 has high-performance clock enabled" ] From 3c78816010b5d427f95d21887c0c82fe22100d2f Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Tue, 20 Jan 2026 15:36:55 -0600 Subject: [PATCH 08/77] Phase 1: Add GPS preset UI with M9 Precision/Sport modes and auto-detection - Add GPS_PRESETS object with 7 preset options - Split M9 into Precision (5Hz, 32 sats) and Sport (10Hz, 16 sats) modes - Update M10 presets (3 const @ 8Hz, 4 const @ 10Hz for high-perf) - Add detectGPSPreset() to map hwVersion to preset - Add applyGPSPreset() to apply constellation/rate settings - Extend MSPHelper to parse hwVersion from MSP_GPSSTATISTICS (backward compatible) - Update HTML dropdown with new preset options Research: M9 hardware limits to 16 satellites at >=10Hz, 32 satellites at <10Hz See: claude/developer/docs/gps/m9-16-satellite-limitation-official.md --- tabs/gps.html | 3 ++- tabs/gps.js | 44 ++++++++++++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/tabs/gps.html b/tabs/gps.html index 5fd17743d..aa5025adb 100644 --- a/tabs/gps.html +++ b/tabs/gps.html @@ -44,7 +44,8 @@ - + + diff --git a/tabs/gps.js b/tabs/gps.js index 2fc693b5c..89bc77de2 100644 --- a/tabs/gps.js +++ b/tabs/gps.js @@ -214,38 +214,50 @@ TABS.gps.initialize = function (callback) { "Best for: Navigation, position hold, slower aircraft" ] }, - m9: { - name: "u-blox M9", + 'm9-precision': { + name: "u-blox M9 (Precision Mode)", galileo: true, - glonass: true, - beidou: false, + glonass: false, + beidou: true, + rate: 5, + description: [ + "3 GNSS constellations (GPS+Galileo+Beidou) → 32 satellites", + "5Hz update rate, HDOP ~1.0-1.3", + "Best for: Long-range cruise, position hold, navigation missions" + ] + }, + 'm9-sport': { + name: "u-blox M9 (Sport Mode)", + galileo: true, + glonass: false, + beidou: true, rate: 10, description: [ - "3 GNSS constellations (GPS+Galileo+Glonass)", - "10Hz update rate", - "Best for: General use, balanced accuracy and responsiveness" + "3 GNSS constellations (GPS+Galileo+Beidou) → 16 satellites", + "10Hz update rate (hardware limit), HDOP ~2.0-2.5", + "Best for: Fast flying, racing, acrobatics, quick response" ] }, m10: { name: "u-blox M10", galileo: true, - glonass: true, - beidou: false, - rate: 6, + glonass: false, + beidou: true, + rate: 8, description: [ - "3 GNSS constellations (GPS+Galileo+Glonass)", - "6Hz update rate (safe for M10 default CPU clock)", - "Best for: Long-range, cruise, slower aircraft" + "3 GNSS constellations (GPS+Galileo+Beidou)", + "8Hz update rate (safe for M10 default CPU clock)", + "Best for: General use, balanced performance" ] }, 'm10-highperf': { name: "u-blox M10 (High-Performance)", galileo: true, glonass: true, - beidou: false, + beidou: true, rate: 10, description: [ - "3 GNSS constellations (GPS+Galileo+Glonass)", + "4 GNSS constellations for maximum satellites", "10Hz update rate (requires high-performance CPU clock)", "Only use if you KNOW your M10 has high-performance clock enabled" ] @@ -262,7 +274,7 @@ TABS.gps.initialize = function (callback) { function detectGPSPreset(hwVersion) { switch(hwVersion) { case 800: return 'm8'; - case 900: return 'm9'; + case 900: return 'm9-precision'; // Default to precision mode for better accuracy case 1000: return 'm10'; default: return 'manual'; } From 025a2546f101f30800a4b2492c56e389d2c5298e Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Tue, 20 Jan 2026 15:57:19 -0600 Subject: [PATCH 09/77] Fix GPS tab preset reset on tab re-initialization When navigating away from GPS tab and back, the preset dropdown was resetting to "Manual Settings" every time, even if the GPS module had been auto-detected. Changes: - Check if GPS data (hwVersion) is already available on tab init - If available and valid (>0), auto-detect and apply preset - If not available, fall back to manual mode as before This preserves the auto-detected preset when switching between tabs, providing a smoother user experience. Related: GPS preset UI feature --- tabs/gps.js | 57 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/tabs/gps.js b/tabs/gps.js index 89bc77de2..be9bbae63 100644 --- a/tabs/gps.js +++ b/tabs/gps.js @@ -281,16 +281,16 @@ TABS.gps.initialize = function (callback) { } function applyGPSPreset(presetId) { - const preset = GPS_PRESETS[presetId]; - - if (!preset) return; - + // Handle special cases first (before checking GPS_PRESETS) if (presetId === 'manual') { // Enable all controls $('.preset-controlled').prop('disabled', false); $('#gps_ublox_nav_hz').prop('disabled', false); $('#preset_info').hide(); - } else if (presetId === 'auto') { + return; + } + + if (presetId === 'auto') { // Try to auto-detect from FC if (FC.GPS_DATA && FC.GPS_DATA.hwVersion) { const detectedPreset = detectGPSPreset(FC.GPS_DATA.hwVersion); @@ -303,22 +303,27 @@ TABS.gps.initialize = function (callback) { $('#gps_preset_mode').val('manual'); GUI.log(i18n.getMessage('gpsAutoDetectFailed')); } - } else { - // Apply preset values - $('#gps_use_galileo').prop('checked', preset.galileo); - $('#gps_use_glonass').prop('checked', preset.glonass); - $('#gps_use_beidou').prop('checked', preset.beidou); - $('#gps_ublox_nav_hz').val(preset.rate); - - // Disable controls (user can see but not edit) - $('.preset-controlled').prop('disabled', true); - $('#gps_ublox_nav_hz').prop('disabled', true); - - // Show preset info - $('#preset_name').text(preset.name); - $('#preset_details').html(preset.description.map(d => `
  • ${d}
  • `).join('')); - $('#preset_info').show(); + return; } + + // Normal preset application + const preset = GPS_PRESETS[presetId]; + if (!preset) return; + + // Apply preset values + $('#gps_use_galileo').prop('checked', preset.galileo); + $('#gps_use_glonass').prop('checked', preset.glonass); + $('#gps_use_beidou').prop('checked', preset.beidou); + $('#gps_ublox_nav_hz').val(preset.rate); + + // Disable controls (user can see but not edit) + $('.preset-controlled').prop('disabled', true); + $('#gps_ublox_nav_hz').prop('disabled', true); + + // Show preset info + $('#preset_name').text(preset.name); + $('#preset_details').html(preset.description.map(d => `
  • ${d}
  • `).join('')); + $('#preset_info').show(); } // Set up preset mode handler @@ -326,8 +331,16 @@ TABS.gps.initialize = function (callback) { applyGPSPreset($(this).val()); }); - // Initialize with manual mode (or auto-detect if available) - applyGPSPreset('manual'); + // Initialize - try auto-detect if GPS data available, otherwise manual + if (FC.GPS_DATA && FC.GPS_DATA.hwVersion && FC.GPS_DATA.hwVersion > 0) { + // GPS data already available (e.g., from previous tab load) + const detectedPreset = detectGPSPreset(FC.GPS_DATA.hwVersion); + applyGPSPreset(detectedPreset); + $('#gps_preset_mode').val(detectedPreset); + } else { + // GPS data not yet available, default to manual + applyGPSPreset('manual'); + } let mapView = new View({ center: [0, 0], From 953fa606165bba3a10276d6c4708491816a688e0 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Tue, 20 Jan 2026 17:19:31 -0600 Subject: [PATCH 10/77] Trigger change events when applying GPS presets Addresses Qodo code review suggestion to ensure state consistency when programmatically setting GPS configuration values. When presets change constellation checkboxes and rate values, we now trigger their 'change' events. This follows the pattern used in other tabs (firmware_flasher.js, pid_tuning.js, sensors.js) and ensures: - Save button state updates correctly - Any change event handlers fire properly - Configuration tracking remains consistent Without .trigger('change'), programmatic updates bypass event handlers that may be listening for user changes to these controls. --- tabs/gps.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tabs/gps.js b/tabs/gps.js index be9bbae63..c148db1db 100644 --- a/tabs/gps.js +++ b/tabs/gps.js @@ -310,11 +310,11 @@ TABS.gps.initialize = function (callback) { const preset = GPS_PRESETS[presetId]; if (!preset) return; - // Apply preset values - $('#gps_use_galileo').prop('checked', preset.galileo); - $('#gps_use_glonass').prop('checked', preset.glonass); - $('#gps_use_beidou').prop('checked', preset.beidou); - $('#gps_ublox_nav_hz').val(preset.rate); + // Apply preset values (trigger change for state consistency) + $('#gps_use_galileo').prop('checked', preset.galileo).trigger('change'); + $('#gps_use_glonass').prop('checked', preset.glonass).trigger('change'); + $('#gps_use_beidou').prop('checked', preset.beidou).trigger('change'); + $('#gps_ublox_nav_hz').val(preset.rate).trigger('change'); // Disable controls (user can see but not edit) $('.preset-controlled').prop('disabled', true); From c75975a5868159e56147cdb9119b5ae1c521e021 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Tue, 20 Jan 2026 23:01:02 -0600 Subject: [PATCH 11/77] Fix JS Programming decompiler incorrect operand validation warnings Problem: The decompiler was eagerly decompiling both operandA and operandB for all action operations, even when operandB was unused. This caused incorrect type-specific validation warnings for operations like SET_PROFILE (operation 42) which only uses operandA. Example: SET_PROFILE with operandB type=6 (PID), value=1700 (garbage data) would trigger: "Invalid PID operand value 1700. Valid range is 0-3." Solution: Added operation-specific operand handling with two categories: - operandAOnlyOperations: Operations that only use operandA (skip operandB decompilation) - noOperandOperations: Operations that use no operands (skip both) Additionally, added version detection warnings for unexpected operands: - Warns when unused operands have non-zero type or value - Helps detect firmware/configurator version mismatches - Shows operation name, type, and value for debugging Example warning: "Unexpected operand B to Set Profile operation (type=6, value=1700). This may indicate a firmware version mismatch." Benefits: - Prevents type-specific validation errors on garbage data in unused operands - Preserves validation for operations that legitimately use PID/other operands - Provides version detection when firmware adds new operand usage --- js/transpiler/transpiler/action_decompiler.js | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/js/transpiler/transpiler/action_decompiler.js b/js/transpiler/transpiler/action_decompiler.js index aa2ebc788..3a51ce3da 100644 --- a/js/transpiler/transpiler/action_decompiler.js +++ b/js/transpiler/transpiler/action_decompiler.js @@ -51,14 +51,60 @@ class ActionDecompiler { return this.handleOverrideThrottle(lc, allConditions); } + // Operations that only use operandA (operandB is unused/ignored) + // These should not decompile operandB to avoid incorrect validation warnings + const operandAOnlyOperations = [ + OPERATION.SET_VTX_POWER_LEVEL, + OPERATION.SET_VTX_BAND, + OPERATION.SET_VTX_CHANNEL, + OPERATION.SET_OSD_LAYOUT, + OPERATION.LOITER_OVERRIDE, + OPERATION.OVERRIDE_MIN_GROUND_SPEED, + OPERATION.SET_HEADING_TARGET, + OPERATION.SET_PROFILE, + OPERATION.SET_GIMBAL_SENSITIVITY + ]; + + // Operations that use no operands (boolean flags only) + const noOperandOperations = [ + OPERATION.OVERRIDE_ARMING_SAFETY, + OPERATION.SWAP_ROLL_YAW, + OPERATION.INVERT_ROLL, + OPERATION.INVERT_PITCH, + OPERATION.INVERT_YAW, + OPERATION.DISABLE_GPS_FIX, + OPERATION.RESET_MAG_CALIBRATION + ]; + // INAV operand pattern (confirmed by logic_condition.c): - // - Most overrides: operandA = value, operandB = 0 + // - Most overrides: operandA = value, operandB = 0 (unused) // - GVAR_INC/DEC: operandA = gvar index, operandB = increment/decrement // - FLIGHT_AXIS: operandA = axis index, operandB = angle/rate // - RC_CHANNEL: operandA = channel, operandB = value // - PORT_SET: operandA = pin, operandB = value - const valueA = this.decompileOperand(lc.operandAType, lc.operandAValue, allConditions); - const valueB = this.decompileOperand(lc.operandBType, lc.operandBValue, allConditions); + + // Warn about unexpected operands (version detection for new firmware features) + if (noOperandOperations.includes(lc.operation)) { + if (lc.operandAType !== 0 || lc.operandAValue !== 0) { + this.addWarning(`Unexpected operand A to ${getOperationName(lc.operation)} operation (type=${lc.operandAType}, value=${lc.operandAValue}). This may indicate a firmware version mismatch.`); + } + if (lc.operandBType !== 0 || lc.operandBValue !== 0) { + this.addWarning(`Unexpected operand B to ${getOperationName(lc.operation)} operation (type=${lc.operandBType}, value=${lc.operandBValue}). This may indicate a firmware version mismatch.`); + } + } else if (operandAOnlyOperations.includes(lc.operation)) { + if (lc.operandBType !== 0 || lc.operandBValue !== 0) { + this.addWarning(`Unexpected operand B to ${getOperationName(lc.operation)} operation (type=${lc.operandBType}, value=${lc.operandBValue}). This may indicate a firmware version mismatch.`); + } + } + + // Only decompile operands that are actually used by the operation + // This prevents incorrect validation warnings (e.g., PID range check on unused operands) + const valueA = noOperandOperations.includes(lc.operation) + ? null + : this.decompileOperand(lc.operandAType, lc.operandAValue, allConditions); + const valueB = (operandAOnlyOperations.includes(lc.operation) || noOperandOperations.includes(lc.operation)) + ? null + : this.decompileOperand(lc.operandBType, lc.operandBValue, allConditions); switch (lc.operation) { // GVAR operations: operandA = index, operandB = value From 1129e306a992a95f332ce4399cd36aed313124cd Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Wed, 21 Jan 2026 10:51:55 -0600 Subject: [PATCH 12/77] Feature: Add accordion navigation for improved tab organization Organizes the 24 configuration tabs into 8 collapsible accordion groups for improved navigation and reduced visual clutter. Groups: - Setup & Configuration (5 tabs) - Flight Control (5 tabs) - Tuning (3 tabs) - Navigation & Mission (2 tabs) - Sensors & Peripherals (3 tabs) - Data Logging (2 tabs) - Programming (2 tabs) - Tools (2 tabs) Implementation: - Added accordion group structure to index.html - Added accordion styling to main.css (preserves INAV cyan theme) - Added expand/collapse JavaScript to configurator_main.js - Added English translation keys for navigation groups - First group (Setup & Configuration) expands by default No new dependencies required - uses existing jQuery and CSS transitions. --- index.html | 214 +++++++++++++++++++++++++++------------- js/configurator_main.js | 16 ++- locale/en/messages.json | 24 +++++ src/css/main.css | 73 ++++++++++++++ 4 files changed, 257 insertions(+), 70 deletions(-) diff --git a/index.html b/index.html index bd4e4e001..196f391ff 100644 --- a/index.html +++ b/index.html @@ -244,83 +244,159 @@

      -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - > -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - + +
    • -
    • - + + +
    • -
    • - + + +
    • -
    • - + + +
    • -
    • - + + +
    • - + + - + +
    diff --git a/js/configurator_main.js b/js/configurator_main.js index bf6164be5..b81fd597e 100644 --- a/js/configurator_main.js +++ b/js/configurator_main.js @@ -269,7 +269,21 @@ $(function() { $('#tabs ul.mode-disconnected li a:first').trigger( "click" ); - + // Accordion Navigation Groups + $('.group-header').on('click', function(e) { + e.stopPropagation(); // Prevent triggering tab click + const header = $(this); + const items = header.next('.group-items'); + + // Toggle this group + header.toggleClass('active'); + items.toggleClass('expanded'); + }); + + // Initialize: expand first group by default + $('#tabs ul.mode-connected .nav-group:first-child .group-header').addClass('active'); + $('#tabs ul.mode-connected .nav-group:first-child .group-items').addClass('expanded'); + // options $('#options').on('click', function() { diff --git a/locale/en/messages.json b/locale/en/messages.json index c16540af6..d638d600c 100644 --- a/locale/en/messages.json +++ b/locale/en/messages.json @@ -119,6 +119,30 @@ "tabHelp": { "message": "Documentation & Support" }, + "navGroupSetup": { + "message": "Setup & Configuration" + }, + "navGroupFlight": { + "message": "Flight Control" + }, + "navGroupTuning": { + "message": "Tuning" + }, + "navGroupNavigation": { + "message": "Navigation & Mission" + }, + "navGroupSensors": { + "message": "Sensors & Peripherals" + }, + "navGroupLogging": { + "message": "Data Logging" + }, + "navGroupProgramming": { + "message": "Programming" + }, + "navGroupTools": { + "message": "Tools" + }, "tabSetup": { "message": "Status" }, diff --git a/src/css/main.css b/src/css/main.css index 43dab6ed9..c123dbcd3 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -680,11 +680,84 @@ input[type="number"]::-webkit-inner-spin-button { background-color: #37a8db; } +/* Accordion Navigation Groups */ +#tabs .nav-group { + border-bottom: 1px solid rgba(0, 0, 0, 0.30); +} + +#tabs .group-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + cursor: pointer; + background-color: #2d2d2d; + transition: background-color 0.2s; + user-select: none; + border-top: solid 1px rgba(255, 255, 255, 0.05); +} + +#tabs .group-header:hover { + background-color: #353535; +} + +#tabs .group-header.active { + background-color: #3a3a3a; +} + +#tabs .group-title { + font-family: 'open_sanssemibold', Arial, serif; + font-size: 12px; + font-weight: 600; + color: #b0b0b0; +} + +#tabs .chevron { + font-size: 10px; + transition: transform 0.2s; + color: #808080; +} + +#tabs .group-header.active .chevron { + transform: rotate(90deg); +} + +#tabs .group-items { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; + background-color: #252525; + list-style: none; + padding: 0; + margin: 0; +} + +#tabs .group-items.expanded { + max-height: 500px; +} + +#tabs .group-items li { + border-bottom: 1px solid rgba(0, 0, 0, 0.20); +} + +#tabs .group-items li:last-child { + border-bottom: 0; +} + +#tabs .group-items li a { + padding-left: 40px; +} + .tabicon { background: no-repeat 13px 7px; background-size: 15px; } +/* Adjust icon position for accordion tabs */ +#tabs .group-items .tabicon { + background-position: 20px 7px; +} + /* Tab-Icons */ .ic_setup { background-image: url("./../../images/icons/cf_icon_setup_grey.svg"); From e2ff657b568b09d56599e6f979c581d308b13ea2 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Wed, 21 Jan 2026 17:09:15 -0600 Subject: [PATCH 13/77] Add expand/collapse all toggle for accordion navigation Adds an icon-only toggle button at the bottom of the navigation menu to expand or collapse all accordion groups. Features: - SVG icon button that swaps between expand (double chevron down) and collapse (double chevron up) states - Clicking expands all 8 navigation groups or collapses to first group only - Preference persists across configurator restarts via electron-store - When all groups expanded, headers become minimal cyan divider lines to maximize vertical space - Icon-only design saves space - all 24 tabs fit without scrolling - Smooth CSS transitions for icon swap and header state changes Implementation: - Toggle button in index.html with both expand/collapse SVG icons - JavaScript in configurator_main.js handles toggle logic and persistence - CSS in main.css for compact headers when expanded and icon styling - Translation keys in locale/en/messages.json for accessibility Addresses need for quick access to all tabs while maintaining visual organization of accordion groups. --- index.html | 15 ++++++++++ js/configurator_main.js | 61 +++++++++++++++++++++++++++++++++++++++-- locale/en/messages.json | 6 ++++ src/css/main.css | 51 ++++++++++++++++++++++++++++++++-- tabs/options.html | 2 +- 5 files changed, 128 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index 196f391ff..92dfd4217 100644 --- a/index.html +++ b/index.html @@ -397,6 +397,21 @@

    + + +
    diff --git a/js/configurator_main.js b/js/configurator_main.js index b81fd597e..106ec357f 100644 --- a/js/configurator_main.js +++ b/js/configurator_main.js @@ -278,11 +278,66 @@ $(function() { // Toggle this group header.toggleClass('active'); items.toggleClass('expanded'); + + // Update the expand/collapse all button state + updateToggleAllButton(); + }); + + // Function to update toggle all button state + function updateToggleAllButton() { + const allExpanded = $('.nav-group .group-header.active').length === $('.nav-group .group-header').length; + const $expandIcon = $('#toggleAllGroups .expand-icon'); + const $collapseIcon = $('#toggleAllGroups .collapse-icon'); + const $toggleText = $('#toggleAllGroups .toggle-text'); + + if (allExpanded) { + $expandIcon.hide(); + $collapseIcon.show(); + $toggleText.attr('data-i18n', 'navCollapseAll'); + $toggleText.text(i18n.getMessage('navCollapseAll')); + } else { + $expandIcon.show(); + $collapseIcon.hide(); + $toggleText.attr('data-i18n', 'navExpandAll'); + $toggleText.text(i18n.getMessage('navExpandAll')); + } + } + + // Expand/Collapse All Toggle + $('#toggleAllGroups').on('click', function(e) { + e.preventDefault(); + const allExpanded = $('.nav-group .group-header.active').length === $('.nav-group .group-header').length; + + if (allExpanded) { + // Collapse all except first + $('.nav-group .group-header').removeClass('active'); + $('.nav-group .group-items').removeClass('expanded'); + $('#tabs ul.mode-connected .nav-group:first-child .group-header').addClass('active'); + $('#tabs ul.mode-connected .nav-group:first-child .group-items').addClass('expanded'); + store.set('expand_all_groups', false); + } else { + // Expand all + $('.nav-group .group-header').addClass('active'); + $('.nav-group .group-items').addClass('expanded'); + store.set('expand_all_groups', true); + } + + updateToggleAllButton(); }); - // Initialize: expand first group by default - $('#tabs ul.mode-connected .nav-group:first-child .group-header').addClass('active'); - $('#tabs ul.mode-connected .nav-group:first-child .group-items').addClass('expanded'); + // Initialize: apply saved expand all preference or expand first group by default + if (store.get('expand_all_groups', false)) { + // Expand all groups + $('.nav-group .group-header').addClass('active'); + $('.nav-group .group-items').addClass('expanded'); + } else { + // Expand first group only + $('#tabs ul.mode-connected .nav-group:first-child .group-header').addClass('active'); + $('#tabs ul.mode-connected .nav-group:first-child .group-items').addClass('expanded'); + } + + // Update button state on initialization + updateToggleAllButton(); // options diff --git a/locale/en/messages.json b/locale/en/messages.json index d638d600c..e9261dd26 100644 --- a/locale/en/messages.json +++ b/locale/en/messages.json @@ -47,6 +47,12 @@ "options_cliAutocomplete": { "message": "Advanced CLI AutoComplete" }, + "navExpandAll": { + "message": "Expand All" + }, + "navCollapseAll": { + "message": "Collapse All" + }, "options_unit_type": { "message": "Set how the units render on the configurator only" }, diff --git a/src/css/main.css b/src/css/main.css index c123dbcd3..430a25103 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -702,7 +702,10 @@ input[type="number"]::-webkit-inner-spin-button { } #tabs .group-header.active { - background-color: #3a3a3a; + background-color: transparent; + padding: 0; + min-height: 1px; + border-top: 1px solid rgba(55, 168, 219, 0.3); } #tabs .group-title { @@ -710,16 +713,21 @@ input[type="number"]::-webkit-inner-spin-button { font-size: 12px; font-weight: 600; color: #b0b0b0; + transition: opacity 0.2s; +} + +#tabs .group-header.active .group-title { + display: none; } #tabs .chevron { font-size: 10px; - transition: transform 0.2s; + transition: transform 0.2s, opacity 0.2s; color: #808080; } #tabs .group-header.active .chevron { - transform: rotate(90deg); + display: none; } #tabs .group-items { @@ -748,6 +756,43 @@ input[type="number"]::-webkit-inner-spin-button { padding-left: 40px; } +/* Expand/Collapse All Toggle */ +#tabs .nav-toggle-all { + margin-top: 4px; + padding: 0; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +#tabs .nav-toggle-all a { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 6px 12px; + color: #37a8db; + font-family: 'open_sanssemibold', Arial, serif; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + background-color: rgba(55, 168, 219, 0.05); + transition: background-color 0.2s, color 0.2s; +} + +#tabs .nav-toggle-all a:hover { + background-color: rgba(55, 168, 219, 0.15); + color: #4db8eb; +} + +#tabs .nav-toggle-all svg { + width: 20px; + height: 20px; + flex-shrink: 0; +} + +#tabs .nav-toggle-all .toggle-text { + display: none; +} + .tabicon { background: no-repeat 13px 7px; background-size: 15px; diff --git a/tabs/options.html b/tabs/options.html index 38aecc647..33f36d61a 100644 --- a/tabs/options.html +++ b/tabs/options.html @@ -29,7 +29,7 @@ - +
    From f4667c84f5a1f245d561245006d324e47dc239b9 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Wed, 21 Jan 2026 17:43:50 -0600 Subject: [PATCH 14/77] Improve accordion navigation accessibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds WCAG-compliant accessibility features to accordion navigation: HTML changes: - Added role="button" to all group headers for screen reader compatibility - Added tabindex="0" to make headers keyboard-navigable - Added aria-expanded="false" initial state to indicate collapsed groups JavaScript changes: - Updates aria-expanded dynamically when groups expand/collapse - Added keyboard event handler for Enter and Space keys - Ensures aria-expanded is set correctly on initialization - Updates aria-expanded when using expand/collapse all button - Fixed localization attribute on JavaScript Programming tab (i18n → data-i18n) Benefits: - Screen readers announce group headers as interactive buttons - Keyboard users can Tab to headers and activate with Enter/Space - Assistive technology correctly reports expanded/collapsed state - Meets WCAG 2.1 Level AA compliance for keyboard navigation Addresses qodo-merge code review suggestions for accessibility. --- index.html | 18 +++++++++--------- js/configurator_main.js | 21 ++++++++++++++++----- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/index.html b/index.html index 92dfd4217..b854a54f1 100644 --- a/index.html +++ b/index.html @@ -246,7 +246,7 @@

    • - > +