diff --git a/core/frontend/src/components/vehiclesetup/overview/ParamSets.vue b/core/frontend/src/components/vehiclesetup/overview/ParamSets.vue index e91c23d1ec..204e824d5c 100644 --- a/core/frontend/src/components/vehiclesetup/overview/ParamSets.vue +++ b/core/frontend/src/components/vehiclesetup/overview/ParamSets.vue @@ -44,18 +44,68 @@ These are the recommended parameter sets for your vehicle and firmware version. Curated by Blue Robotics

- + +

+ No parameters available for this setup +

+ - {{ name.split('/').pop() }} + {{ displayName(item.name) }} + v{{ item.version_label }} -

- No parameters available for this setup + +

+ No parameter sets match your firmware version. Older sets are shown below.

+ + + {{ show_older ? 'Hide' : 'Show' }} + {{ outdated_param_sets.length }} + older set{{ outdated_param_sets.length === 1 ? '' : 's' }} + + +
+ version: SemVer + version_label: string + outdated: boolean +} + +type Candidate = Omit & { has_patch: boolean } + +function escapeRegex(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + export default Vue.extend({ name: 'ParamSets', components: { @@ -111,6 +175,7 @@ export default Vue.extend({ erasing: false, settings, show_warning: false, + show_older: false, }), computed: { board(): string | undefined { @@ -122,30 +187,83 @@ export default Vue.extend({ version(): SemVer | undefined { return autopilot.firmware_info?.version }, - filtered_param_sets(): Dictionary> | undefined { - const fw_patch = `${this.vehicle}/${this.version}/${this.board}` - const fw_minor = `${this.vehicle}/${this.version?.major}.${this.version?.minor}/${this.board}` - const fw_major = `${this.vehicle}/${this.version?.major}/${this.board}` - - // returns a new dict where the keys start with the fullname - // e.g. "ArduSub/BlueROV2/4.0.3" -> "ArduSub/BlueROV2/4.0.3/BlueROV2" - - let fw_params = {} - // try to find a paramset that matches the firmware version, starting from patch and walking up to major - for (const string of [fw_patch, fw_minor, fw_major]) { - fw_params = Object.fromEntries( - Object.entries(this.all_param_sets).filter( - // We add a trailing slash to avoid matching Navigator and Navigator64, or any board with suffix - ([name]) => name.toLocaleLowerCase().includes(`${string.toLowerCase()}/`), - ), - ) - if (Object.keys(fw_params).length > 0) { - break + filtered_param_sets(): FilteredParamSet[] { + if (!this.vehicle || !this.board || !this.version) { + return [] + } + + // Match keys shaped like "...////..." + // Trailing slash on the board avoids matching Navigator vs Navigator64, etc. + const pattern = new RegExp( + `/${escapeRegex(this.vehicle)}/(\\d+\\.\\d+(?:\\.\\d+)?)/${escapeRegex(this.board)}/`, + 'i', + ) + + const current = this.version + const candidates: Candidate[] = [] + + for (const [name, paramset] of Object.entries(this.all_param_sets)) { + const match = name.match(pattern) + if (!match) { + continue + } + + const version_label = match[1] + const has_patch = version_label.split('.').length === 3 + let version: SemVer + try { + // Normalize "4.5" -> "4.5.0" so SemVer can parse it + version = new SemVer(has_patch ? version_label : `${version_label}.0`) + } catch { + continue + } + + // Skip paramsets targeting a newer firmware than the one currently installed + if (version.compare(current) > 0) { + continue } + + candidates.push({ + name, paramset, version, version_label, has_patch, + }) } - return { - ...fw_params, + + // Specificity: 2 = exact patch match, 1 = same major.minor, 0 = older + function specificity(c: Candidate): number { + if (c.has_patch && c.version.compare(current) === 0) return 2 + if (c.version.major === current.major && c.version.minor === current.minor) return 1 + return 0 } + const scored = candidates.map((c) => ({ candidate: c, score: specificity(c) })) + const best = scored.reduce((max, s) => Math.max(max, s.score), 0) + + const result: FilteredParamSet[] = scored.map(({ candidate, score }) => ({ + name: candidate.name, + paramset: candidate.paramset, + version: candidate.version, + version_label: candidate.version_label, + outdated: best === 0 || score < best, + })) + + // Current paramsets first, then outdated ones sorted newest-first + result.sort((a, b) => { + if (a.outdated !== b.outdated) { + return a.outdated ? 1 : -1 + } + return b.version.compare(a.version) + }) + + return result + }, + current_param_sets(): FilteredParamSet[] { + return this.filtered_param_sets.filter((p) => !p.outdated) + }, + outdated_param_sets(): FilteredParamSet[] { + return this.filtered_param_sets.filter((p) => p.outdated) + }, + current_version_label(): string { + if (!this.version) return 'unknown' + return `${this.version.major}.${this.version.minor}.${this.version.patch}` }, warningMessage(): string { return 'You will lose ALL your parameters, vehicle setup, and calibrations. Are you sure you want to reset?' @@ -165,6 +283,10 @@ export default Vue.extend({ this.selected_paramset_name = name this.selected_paramset = paramset }, + displayName(name: string): string { + const basename = name.split('/').pop() ?? name + return basename.replace(/\.params$/i, '') + }, async restartAutopilot(): Promise { this.rebooting = true await AutopilotManager.restart() @@ -236,4 +358,14 @@ button { .checkbox-label label { font-weight: 700; } + +.version-tag { + font-size: 0.75em; + font-weight: normal; + opacity: 0.7; +} + +.full-row { + flex-basis: 100%; +}