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"
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
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 >
221246</template >
222247
223248<script lang="ts">
224- import { AxiosRequestConfig } from ' axios'
225249import Vue from ' vue'
226250
227251import Notifier from ' @/libs/notifier'
@@ -236,7 +260,7 @@ import {
236260 Vehicle ,
237261} from ' @/types/autopilot'
238262import { autopilot_service } from ' @/types/frontend_services'
239- import back_axios , { isBackendOffline } from ' @/utils/api'
263+ import back_axios from ' @/utils/api'
240264
241265const 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 >
0 commit comments