Skip to content

Commit b13adba

Browse files
Autopilot Manager: allow detecting all serial boards, start infra-structure for supporting PX4
frontend: update to deal with boards with multiple platforms
1 parent 13cd805 commit b13adba

File tree

23 files changed

+1201
-361
lines changed

23 files changed

+1201
-361
lines changed

core/frontend/src/components/autopilot/FirmwareManager.vue

Lines changed: 185 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@
113113
label="Board"
114114
hint="If no board is chosen the system will try to flash the currently running board."
115115
class="ma-1 pa-0"
116-
@change="chosen_vehicle = null"
116+
@change="clearFirmwareSelection()"
117117
/>
118118
<div
119119
v-if="upload_type === UploadType.Cloud"
@@ -126,12 +126,22 @@
126126
class="ma-1 pa-0"
127127
@change="updateAvailableFirmwares"
128128
/>
129+
<v-select
130+
v-if="platforms_available.length > 1"
131+
v-model="chosen_platform"
132+
class="ma-1 pa-0"
133+
:disabled="disable_firmware_selection"
134+
:items="platforms_available"
135+
:label="platform_selector_label"
136+
:loading="loading_firmware_options"
137+
required
138+
/>
129139
<div class="d-flex">
130140
<v-select
131141
v-model="chosen_firmware_url"
132142
class="ma-1 pa-0"
133143
:disabled="disable_firmware_selection"
134-
:items="showable_firmwares"
144+
:items="showable_firmware_deduplicated"
135145
:label="firmware_selector_label"
136146
:loading="loading_firmware_options"
137147
required
@@ -188,19 +198,34 @@
188198
v-model="show_install_progress"
189199
hide-overlay
190200
persistent
191-
width="300"
201+
width="600"
192202
>
193203
<v-card
194204
color="primary"
195205
dark
196206
>
207+
<v-card-title>
208+
Installing firmware
209+
</v-card-title>
197210
<v-card-text>
198-
Installing firmware. Please wait.
199211
<v-progress-linear
200212
indeterminate
201213
color="white"
202-
class="mb-0"
214+
class="mb-4"
203215
/>
216+
<div
217+
v-if="install_logs.length > 0"
218+
class="install-logs pa-2"
219+
>
220+
<div
221+
v-for="(log, index) in install_logs"
222+
:key="index"
223+
:class="{ 'error-log': log.stream === 'stderr', 'info-log': log.stream === 'stdout' }"
224+
class="log-line"
225+
>
226+
{{ log.data.replace(/\r/g, '\n') }}<br>
227+
</div>
228+
</div>
204229
</v-card-text>
205230
</v-card>
206231
</v-dialog>
@@ -221,7 +246,6 @@
221246
</template>
222247

223248
<script lang="ts">
224-
import { AxiosRequestConfig } from 'axios'
225249
import Vue from 'vue'
226250
227251
import Notifier from '@/libs/notifier'
@@ -236,7 +260,7 @@ import {
236260
Vehicle,
237261
} from '@/types/autopilot'
238262
import { autopilot_service } from '@/types/frontend_services'
239-
import back_axios, { isBackendOffline } from '@/utils/api'
263+
import back_axios from '@/utils/api'
240264
241265
const notifier = new Notifier(autopilot_service)
242266
@@ -278,11 +302,19 @@ export default Vue.extend({
278302
available_firmwares: [] as Firmware[],
279303
firmware_file: null as (Blob | null),
280304
install_result_message: '',
305+
chosen_platform: null as (string | null),
306+
install_logs: [] as Array<{stream: string, data: string}>,
281307
rebootOnBoardComputer,
282308
requestOnBoardComputerReboot,
283309
}
284310
},
285311
computed: {
312+
platforms_available(): string[] {
313+
return Array.from(new Set(this.available_firmwares.map((firmware) => firmware.platform)))
314+
},
315+
platform_selector_label(): string {
316+
return this.loading_firmware_options ? 'Fetching available platforms...' : 'Platform'
317+
},
286318
firmware_selector_label(): string {
287319
return this.loading_firmware_options ? 'Fetching available firmware...' : 'Firmware'
288320
},
@@ -333,8 +365,9 @@ export default Vue.extend({
333365
return this.chosen_vehicle == null || this.loading_firmware_options
334366
},
335367
showable_firmwares(): {value: URL, text: string}[] {
336-
return this.available_firmwares
337-
.map((firmware) => ({ value: firmware.url, text: firmware.name }))
368+
return this.available_firmwares.filter(
369+
(firmware) => firmware.platform === this.chosen_platform,
370+
).map((firmware) => ({ value: firmware.url, text: firmware.name }))
338371
.filter((firmware) => firmware.text !== 'OFFICIAL')
339372
.sort((a, b) => {
340373
const release_show_order = ['dev', 'beta', 'stable']
@@ -344,6 +377,16 @@ export default Vue.extend({
344377
})
345378
.reverse()
346379
},
380+
showable_firmware_deduplicated(): {value: URL, text: string}[] {
381+
// qdd the trailing filename from the url to the value of an entry if another entry has the same text
382+
return this.showable_firmwares.map((firmware) => {
383+
const same_text_entries = this.showable_firmwares.filter((f) => f.text === firmware.text)
384+
if (same_text_entries.length > 1) {
385+
return { value: firmware.value, text: `${firmware.text} (${firmware.value.toString().split('/').pop()})` }
386+
}
387+
return firmware
388+
})
389+
},
347390
allow_installing(): boolean {
348391
if (this.install_status === InstallStatus.Installing) {
349392
return false
@@ -368,25 +411,38 @@ export default Vue.extend({
368411
this.requestOnBoardComputerReboot()
369412
}
370413
},
414+
platforms_available(new_value: string[]): void {
415+
if (new_value.length === 1) {
416+
const [chosen_platform] = new_value
417+
this.chosen_platform = chosen_platform
418+
}
419+
},
371420
},
372421
mounted(): void {
373422
if (this.only_bootloader_boards_available) {
374423
this.setFirstNoSitlBoard()
375424
}
376425
},
377426
methods: {
427+
clearFirmwareSelection(): void {
428+
this.chosen_firmware_url = null
429+
this.chosen_platform = null
430+
this.available_firmwares = []
431+
},
378432
setFirstNoSitlBoard(): void {
379433
const [first_board] = this.no_sitl_boards
380434
this.chosen_board = first_board
381435
},
382436
async updateAvailableFirmwares(): Promise<void> {
383437
this.chosen_firmware_url = null
438+
this.chosen_platform = null
439+
this.available_firmwares = []
384440
this.cloud_firmware_options_status = CloudFirmwareOptionsStatus.Fetching
385441
await back_axios({
386442
method: 'get',
387443
url: `${autopilot.API_URL}/available_firmwares`,
388444
timeout: 30000,
389-
params: { vehicle: this.chosen_vehicle, board_name: this.chosen_board?.name },
445+
params: { vehicle: this.chosen_vehicle, board_name: this.chosen_board?.platform.name },
390446
})
391447
.then((response) => {
392448
this.available_firmwares = response.data
@@ -405,21 +461,26 @@ export default Vue.extend({
405461
},
406462
async installFirmware(): Promise<void> {
407463
this.install_status = InstallStatus.Installing
408-
const axios_request_config: AxiosRequestConfig = {
409-
method: 'post',
464+
this.install_logs = []
465+
466+
let url = ''
467+
let requestOptions: RequestInit = {
468+
method: 'POST',
410469
}
470+
411471
if (this.upload_type === UploadType.Cloud) {
412472
// Populate request with data for cloud install
413-
Object.assign(axios_request_config, {
414-
url: `${autopilot.API_URL}/install_firmware_from_url`,
415-
params: { url: this.chosen_firmware_url, board_name: this.chosen_board?.name },
473+
const params = new URLSearchParams({
474+
url: this.chosen_firmware_url?.toString() ?? '',
475+
board_name: this.chosen_board?.platform.name ?? '',
416476
})
477+
url = `${autopilot.API_URL}/install_firmware_from_url?${params}`
417478
} else if (this.upload_type === UploadType.Restore) {
418479
// Populate request with data for restore install
419-
Object.assign(axios_request_config, {
420-
url: `${autopilot.API_URL}/restore_default_firmware`,
421-
params: { board_name: this.chosen_board?.name },
480+
const params = new URLSearchParams({
481+
board_name: this.chosen_board?.platform.name ?? '',
422482
})
483+
url = `${autopilot.API_URL}/restore_default_firmware?${params}`
423484
} else {
424485
// Populate request with data for file install
425486
if (!this.firmware_file) {
@@ -429,32 +490,94 @@ export default Vue.extend({
429490
}
430491
const form_data = new FormData()
431492
form_data.append('binary', this.firmware_file)
432-
Object.assign(axios_request_config, {
433-
url: `${autopilot.API_URL}/install_firmware_from_file`,
434-
headers: { 'Content-Type': 'multipart/form-data' },
435-
params: { board_name: this.chosen_board?.name },
436-
data: form_data,
493+
const params = new URLSearchParams({
494+
board_name: this.chosen_board?.platform.name ?? '',
437495
})
496+
url = `${autopilot.API_URL}/install_firmware_from_file?${params}`
497+
requestOptions = {
498+
method: 'POST',
499+
body: form_data,
500+
}
438501
}
439502
440-
await back_axios(axios_request_config)
441-
.then(() => {
442-
this.install_status = InstallStatus.Succeeded
443-
this.install_result_message = 'Successfully installed new firmware'
444-
autopilot_data.reset()
445-
})
446-
.catch((error) => {
447-
this.install_status = InstallStatus.Failed
448-
if (isBackendOffline(error)) { return }
449-
// Catch Chrome's net:::ERR_UPLOAD_FILE_CHANGED error
450-
if (error.message && error.message === 'Network Error') {
451-
this.install_result_message = 'Upload fail. If the file was changed, clean the form and re-select it.'
452-
} else {
453-
this.install_result_message = error.response?.data?.detail ?? error.message
503+
try {
504+
const response = await fetch(url, requestOptions)
505+
506+
if (!response.ok) {
507+
throw new Error(`HTTP error! status: ${response.status}`)
508+
}
509+
510+
const reader = response.body?.getReader()
511+
const decoder = new TextDecoder()
512+
513+
if (!reader) {
514+
throw new Error('No response body')
515+
}
516+
517+
let buffer = ''
518+
519+
// eslint-disable-next-line no-constant-condition
520+
while (true) {
521+
const { done, value } = await reader.read()
522+
523+
if (done) break
524+
525+
buffer += decoder.decode(value, { stream: true })
526+
const lines = buffer.split('\n')
527+
528+
// Keep the last incomplete line in the buffer
529+
buffer = lines.pop() ?? ''
530+
531+
// Process complete lines
532+
for (const line of lines) {
533+
if (line.trim()) {
534+
try {
535+
const log = JSON.parse(line)
536+
537+
// Check if backend sent "done" signal to close connection
538+
if (log.stream === 'done') {
539+
// Close the progress dialog immediately
540+
this.install_status = InstallStatus.Succeeded
541+
this.install_result_message = 'Installation completed'
542+
return
543+
}
544+
545+
this.install_logs.push(log)
546+
} catch (e) {
547+
console.error('Failed to parse log line:', line, e)
548+
}
549+
}
454550
}
551+
}
552+
553+
// Check if there were any error messages in the logs
554+
const hasErrors = this.install_logs.some((log) => log.stream === 'stderr')
555+
556+
if (hasErrors) {
557+
this.install_status = InstallStatus.Failed
558+
// Get the last error message
559+
const lastError = this.install_logs
560+
.filter((log) => log.stream === 'stderr')
561+
.pop()
562+
this.install_result_message = lastError?.data || 'Installation failed'
455563
const message = `Could not install firmware: ${this.install_result_message}.`
456564
notifier.pushError('FILE_FIRMWARE_INSTALL_FAIL', message)
457-
})
565+
} else {
566+
this.install_status = InstallStatus.Succeeded
567+
this.install_result_message = 'Successfully installed new firmware'
568+
autopilot_data.reset()
569+
}
570+
} catch (error) {
571+
this.install_status = InstallStatus.Failed
572+
// Catch Chrome's net:::ERR_UPLOAD_FILE_CHANGED error
573+
if (error.message && error.message === 'Network Error') {
574+
this.install_result_message = 'Upload fail. If the file was changed, clean the form and re-select it.'
575+
} else {
576+
this.install_result_message = error.response?.data?.detail ?? error.message
577+
}
578+
const message = `Could not install firmware: ${this.install_result_message}.`
579+
notifier.pushError('FILE_FIRMWARE_INSTALL_FAIL', message)
580+
}
458581
},
459582
},
460583
})
@@ -498,4 +621,28 @@ export default Vue.extend({
498621
align-items: flex-end;
499622
}
500623
}
624+
625+
.install-logs {
626+
background-color: rgba(0, 0, 0, 0.8);
627+
border-radius: 4px;
628+
max-height: 300px;
629+
overflow-y: auto;
630+
font-family: 'Courier New', monospace;
631+
font-size: 12px;
632+
}
633+
634+
.log-line {
635+
padding: 2px 4px;
636+
white-space: pre-wrap;
637+
word-break: break-word;
638+
}
639+
640+
.info-log {
641+
color: #ffffff;
642+
}
643+
644+
.error-log {
645+
color: #ff5252;
646+
font-weight: bold;
647+
}
501648
</style>

core/frontend/src/components/vehiclesetup/overview/common.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import { Platform } from '@/types/autopilot'
2-
31
export default function toBoardFriendlyChannel(board: string | undefined, servo: string): string {
42
const servo_number = parseInt(servo.replace('SERVO', '').replace('_FUNCTION', ''), 10)
5-
if (board === Platform.Pixhawk1) {
3+
if (board === "Pixhawk1") {
64
if (servo_number >= 9) {
75
return `Aux ${servo_number - 8}`
86
}

0 commit comments

Comments
 (0)