Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cherry pick scripts in wizard back to 1.2 #2491

Merged
merged 3 commits into from Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/frontend/src/components/wizard/DefaultParamLoader.vue
Expand Up @@ -9,7 +9,7 @@
:label="`Parameter Sets (${board} - ${vehicle} - ${version})`"
:loading="is_loading"
:disabled="is_loading_paramsets"
style="min-width: 60%;"
style="min-width: 330px;"
:rules="[isNotEmpty]"
@change="setParamSet(filtered_param_sets[selected_param_set_name])"
/>
Expand Down
150 changes: 150 additions & 0 deletions core/frontend/src/components/wizard/ScriptLoader.vue
@@ -0,0 +1,150 @@
<template>
<div class="d-flex flex-column align-center ma-5" style="width: 100%; height: 100%;">
<v-form>
<v-select
v-model="selected_scripts"
clearable
multiple
chips
persistent-hint
:items="[...filtered_scripts]"
item-text="sanitized"
item-value="full"
:label="`Scripts for (${board} - ${vehicle} - ${version})`"
:loading="is_loading"
:disabled="is_loading_scripts"
style="min-width: 330px;"
:rules="[isNotEmpty]"
@change="setScriptsList(selected_scripts)"
/>
</v-form>
<p v-if="is_loading_scripts">
Loading scripts...
</p>
<p v-else-if="has_error">
Unable to load scripts.
</p>
<p v-else-if="(!loading_timeout_reached && invalid_board_or_version)">
Determining current board and firmware version...
</p>
<p v-else-if="(loading_timeout_reached && invalid_board_or_version)">
Unable to determine current board or firmware version.
</p>
<p v-else-if="filtered_scripts?.length === 0">
No scripts available for this setup
</p>
</div>
</template>

<script lang="ts">
import { SemVer } from 'semver'
import Vue from 'vue'

import autopilot from '@/store/autopilot_manager'
import { Firmware, Vehicle } from '@/types/autopilot'
import { callPeriodically, stopCallingPeriodically } from '@/utils/helper_functions'

import { availableFirmwares, fetchCurrentBoard } from '../autopilot/AutopilotManagerUpdater'

const REPOSITORY_ROOT = 'https://docs.bluerobotics.com/Blueos-Parameter-Repository'
const REPOSITORY_SCRIPTS_URL = `${REPOSITORY_ROOT}/scripts_v1.json`

const MAX_LOADING_TIME_MS = 25000

export default Vue.extend({
name: 'ScriptLoader',
props: {
vehicle: {
type: String,
required: true,
},
},
data: () => ({
all_scripts: [] as string[],
version: undefined as (undefined | SemVer),
selected_scripts: [] as string[],
is_loading_scripts: true,
loading_timeout_reached: false,
has_error: false,
}),
computed: {
filtered_scripts(): string[] | undefined {
// for scripts, we only check major version for now
// TODO: support other vehicles
let match_string: string
if (this.vehicle === 'Rover') {
match_string = `ArduRover/${this.version?.major}.`
} else {
return []
}
console.log(match_string)
return this.all_scripts.filter((name) => name.includes(match_string)).map(
(name) => name.replace('scripts/ardupilot/', ''),
)
},

board(): string | undefined {
return autopilot.current_board?.name
},
invalid_board_or_version(): boolean {
return !this.board || !this.version
},
is_loading(): boolean {
return (this.is_loading_scripts || this.invalid_board_or_version) && !this.loading_timeout_reached
},
},

watch: {
vehicle() {
this.updateLatestFirmwareVersion().then((version: string) => {
this.version = new SemVer(version.split('-')[1])
})
},
},
mounted() {
callPeriodically(fetchCurrentBoard, 10000)
this.updateLatestFirmwareVersion().then((version: string) => {
this.version = new SemVer(version.split('-')[1])
})
this.fetchScripts()
setTimeout(() => { this.onLoadingTimeout() }, MAX_LOADING_TIME_MS)
},
beforeDestroy() {
stopCallingPeriodically(fetchCurrentBoard)
},
methods: {
isNotEmpty(value: string): boolean {
return value !== ''
},
async fetchScripts() {
const response = await fetch(REPOSITORY_SCRIPTS_URL)
const scripts = await response.json()
this.all_scripts = scripts
this.is_loading_scripts = false
},
updateLatestFirmwareVersion() {
return availableFirmwares(this.vehicle as Vehicle)
.then((firmwares: Firmware[]) => {
const found: Firmware | undefined = firmwares.find((firmware) => firmware.name.includes('STABLE'))
if (found === undefined) {
return `Failed to find a stable version for vehicle (${this.vehicle})`
}
return found.name
})
},
setScriptsList(list: string[]) {
this.selected_scripts = list
this.$emit('input', list)
},
onLoadingTimeout() {
if (this.is_loading) {
this.loading_timeout_reached = true

if (!this.selected_scripts) {
this.selected_scripts = []
}
}
},
},
})
</script>
46 changes: 46 additions & 0 deletions core/frontend/src/components/wizard/Wizard.vue
Expand Up @@ -129,6 +129,10 @@
<v-text-field v-model="vehicle_name" label="Vehicle Name" />
<v-text-field v-model="mdns_name" label="MDNS Name" />
</div>
<ScriptLoader
v-model="scripts"
:vehicle="vehicle_type"
/>
<DefaultParamLoader
ref="param_loader"
v-model="params"
Expand Down Expand Up @@ -269,6 +273,7 @@ import {
fetchFirmwareInfo,
installFirmwareFromUrl,
} from '@/components/autopilot/AutopilotManagerUpdater'
import filebrowser from '@/libs/filebrowser'
import mavlink2rest from '@/libs/MAVLink2Rest'
import { MavCmd } from '@/libs/MAVLink2Rest/mavlink2rest-ts/messages/mavlink2rest-enum'
import ardupilot_data from '@/store/autopilot'
Expand All @@ -284,10 +289,12 @@ import { sleep } from '@/utils/helper_functions'
import ActionStepper, { Configuration, ConfigurationStatus } from './ActionStepper.vue'
import DefaultParamLoader from './DefaultParamLoader.vue'
import RequireInternet from './RequireInternet.vue'
import ScriptLoader from './ScriptLoader.vue'

const WIZARD_VERSION = 4

const models: Record<string, string> = import.meta.glob('/public/assets/vehicles/models/**', { eager: true })
const REPOSITORY_ROOT = 'https://docs.bluerobotics.com/Blueos-Parameter-Repository'

function get_model(vehicle_name: string, frame_name: string): undefined | string {
const release_path = `assets/vehicles/models/${vehicle_name}/${frame_name}.glb`
Expand Down Expand Up @@ -317,10 +324,12 @@ export default Vue.extend({
components: {
DefaultParamLoader,
RequireInternet,
ScriptLoader,
},
data() {
return {
boat_model: get_model('boat', 'UNDEFINED'),
scripts: [] as string[],
configuration_failed: false,
error_message: 'The operation failed!',
apply_status: ApplyStatus.Waiting,
Expand Down Expand Up @@ -490,6 +499,15 @@ export default Vue.extend({
skip: false,
started: false,
},
{
title: 'Install scripts',
summary: 'Download and install selected scripts',
promise: () => this.installScripts(),
message: undefined,
done: false,
skip: false,
started: false,
},
{
title: 'Disable Wi-Fi hotspot',
summary: 'Wi-Fi hotspot need to be disable to not interfere with onboard radio',
Expand Down Expand Up @@ -651,6 +669,34 @@ export default Vue.extend({
})
.catch((error) => `Failed to fetch available firmware: ${error.message ?? error.response?.data}.`)
},
async installScripts(): Promise<ConfigurationStatus> {
const scripts_folder = 'configs/ardupilot-manager/firmware/scripts/'
try {
// Use allSettled to allow promises to fail in parallel
await Promise.allSettled(
this.scripts.map(
async (script) => filebrowser.createFile(scripts_folder + script.split('/').last(), true),
),
)
await Promise.allSettled(
this.scripts.map(async (script) => {
await filebrowser.writeToFile(
scripts_folder + script.split('/').last(),
await this.fetchScript(script),
)
}),
)
return undefined
} catch (e) {
const error = `Failed to install scripts ${e}`
console.error(error)
return error
}
},
async fetchScript(script: string): Promise<string> {
const response = await fetch(`${REPOSITORY_ROOT}/scripts/ardupilot/${script}`)
return response.text()
},
validateParams(): boolean {
return this.$refs.param_loader?.validateParams()
},
Expand Down
6 changes: 6 additions & 0 deletions core/frontend/src/cosmos.ts
Expand Up @@ -3,6 +3,7 @@ export {}
declare global {
interface Array<T> {
first(): T | undefined;
last(): T | undefined;
isEmpty(): boolean;
}

Expand All @@ -18,6 +19,11 @@ Array.prototype.first = function<T> (this: T[]): T | undefined {
return this.isEmpty() ? undefined : this[0]
}

// eslint-disable-next-line
Array.prototype.last = function<T> (this: T[]): T | undefined {
return this.at(-1)
}

// eslint-disable-next-line
Array.prototype.isEmpty = function<T> (this: T[]): boolean {
return this.length === 0
Expand Down
36 changes: 36 additions & 0 deletions core/frontend/src/libs/filebrowser.ts
Expand Up @@ -66,6 +66,42 @@ class Filebrowser {
})
}

async createFolder(folder_path: string): Promise<void> {
if (!folder_path.endsWith('/')) {
folder_path += '/'
}
this.createFile(folder_path)
}

async createFile(folder_path: string, override: Boolean = false): Promise<void> {
back_axios({
method: 'post',
url: `${filebrowser_url}/resources${folder_path}?override=${override}`,
timeout: 10000,
headers: { 'X-Auth': await this.filebrowserToken() },
})
.catch((error) => {
const message = `Could not create folder ${folder_path}: ${error.message}`
notifier.pushError('FOLDER_CREATE_FAIL', message)
throw new Error(message)
})
}

async writeToFile(file: string, content: string): Promise<void> {
back_axios({
method: 'put',
url: `/file-browser/api/resources${file}`,
timeout: 10000,
headers: { 'X-Auth': await this.filebrowserToken() },
data: content,
})
.catch((error) => {
const message = `Could not write to file ${file}: ${error.message}`
notifier.pushError('FILE_WRITE_FAIL', message)
throw new Error(message)
})
}

/* Delete a single file. */
/* Register a notification and throws if delete fails. */
/**
Expand Down