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+ ref =" installLogsContainer"
219+ class =" install-logs pa-2"
220+ >
221+ <div
222+ v-for =" (log, index) in install_logs"
223+ :key =" index"
224+ :class =" { 'error-log': log.stream === 'stderr', 'info-log': log.stream === 'stdout' }"
225+ class =" log-line"
226+ >
227+ {{ log.data.replace(/\r/g, '\n') }}<br >
228+ </div >
229+ </div >
204230 </v-card-text >
205231 </v-card >
206232 </v-dialog >
221247</template >
222248
223249<script lang="ts">
224- import { AxiosRequestConfig } from ' axios'
225250import Vue from ' vue'
226251
227252import Notifier from ' @/libs/notifier'
@@ -236,7 +261,7 @@ import {
236261 Vehicle ,
237262} from ' @/types/autopilot'
238263import { autopilot_service } from ' @/types/frontend_services'
239- import back_axios , { isBackendOffline } from ' @/utils/api'
264+ import back_axios from ' @/utils/api'
240265
241266const notifier = new Notifier (autopilot_service )
242267
@@ -278,11 +303,19 @@ export default Vue.extend({
278303 available_firmwares: [] as Firmware [],
279304 firmware_file: null as (Blob | null ),
280305 install_result_message: ' ' ,
306+ chosen_platform: null as (string | null ),
307+ install_logs: [] as Array <{stream: string , data: string }>,
281308 rebootOnBoardComputer ,
282309 requestOnBoardComputerReboot ,
283310 }
284311 },
285312 computed: {
313+ platforms_available(): string [] {
314+ return Array .from (new Set (this .available_firmwares .map ((firmware ) => firmware .platform )))
315+ },
316+ platform_selector_label(): string {
317+ return this .loading_firmware_options ? ' Fetching available platforms...' : ' Platform'
318+ },
286319 firmware_selector_label(): string {
287320 return this .loading_firmware_options ? ' Fetching available firmware...' : ' Firmware'
288321 },
@@ -333,8 +366,9 @@ export default Vue.extend({
333366 return this .chosen_vehicle == null || this .loading_firmware_options
334367 },
335368 showable_firmwares(): {value: URL , text: string }[] {
336- return this .available_firmwares
337- .map ((firmware ) => ({ value: firmware .url , text: firmware .name }))
369+ return this .available_firmwares .filter (
370+ (firmware ) => firmware .platform === this .chosen_platform ,
371+ ).map ((firmware ) => ({ value: firmware .url , text: firmware .name }))
338372 .filter ((firmware ) => firmware .text !== ' OFFICIAL' )
339373 .sort ((a , b ) => {
340374 const release_show_order = [' dev' , ' beta' , ' stable' ]
@@ -344,6 +378,16 @@ export default Vue.extend({
344378 })
345379 .reverse ()
346380 },
381+ showable_firmware_deduplicated(): {value: URL , text: string }[] {
382+ // qdd the trailing filename from the url to the value of an entry if another entry has the same text
383+ return this .showable_firmwares .map ((firmware ) => {
384+ const same_text_entries = this .showable_firmwares .filter ((f ) => f .text === firmware .text )
385+ if (same_text_entries .length > 1 ) {
386+ return { value: firmware .value , text: ` ${firmware .text } (${firmware .value .toString ().split (' /' ).pop ()}) ` }
387+ }
388+ return firmware
389+ })
390+ },
347391 allow_installing(): boolean {
348392 if (this .install_status === InstallStatus .Installing ) {
349393 return false
@@ -368,25 +412,54 @@ export default Vue.extend({
368412 this .requestOnBoardComputerReboot ()
369413 }
370414 },
415+ platforms_available(new_value : string []): void {
416+ if (new_value .length === 1 ) {
417+ const [chosen_platform] = new_value
418+ this .chosen_platform = chosen_platform
419+ }
420+ },
421+ available_boards(new_value : FlightController []): void {
422+ if (autopilot .current_board ) {
423+ const chosen_board = new_value .find ((board : FlightController ) => board .name === autopilot .current_board ?.name )
424+ if (chosen_board ) {
425+ this .chosen_board = chosen_board
426+ }
427+ }
428+ },
429+ install_logs(): void {
430+ this .$nextTick (() => {
431+ const container = this .$refs .installLogsContainer as HTMLElement | undefined
432+ if (container ) {
433+ container .scrollTop = container .scrollHeight
434+ }
435+ })
436+ },
371437 },
372438 mounted(): void {
373439 if (this .only_bootloader_boards_available ) {
374440 this .setFirstNoSitlBoard ()
375441 }
376442 },
377443 methods: {
444+ clearFirmwareSelection(): void {
445+ this .chosen_firmware_url = null
446+ this .chosen_platform = null
447+ this .available_firmwares = []
448+ },
378449 setFirstNoSitlBoard(): void {
379450 const [first_board] = this .no_sitl_boards
380451 this .chosen_board = first_board
381452 },
382453 async updateAvailableFirmwares(): Promise <void > {
383454 this .chosen_firmware_url = null
455+ this .chosen_platform = null
456+ this .available_firmwares = []
384457 this .cloud_firmware_options_status = CloudFirmwareOptionsStatus .Fetching
385458 await back_axios ({
386459 method: ' get' ,
387460 url: ` ${autopilot .API_URL }/available_firmwares ` ,
388461 timeout: 30000 ,
389- params: { vehicle: this .chosen_vehicle , board_name: this .chosen_board ?.name },
462+ params: { vehicle: this .chosen_vehicle , board_name: this .chosen_board ?.platform . name },
390463 })
391464 .then ((response ) => {
392465 this .available_firmwares = response .data
@@ -405,21 +478,26 @@ export default Vue.extend({
405478 },
406479 async installFirmware(): Promise <void > {
407480 this .install_status = InstallStatus .Installing
408- const axios_request_config: AxiosRequestConfig = {
409- method: ' post' ,
481+ this .install_logs = []
482+
483+ let url = ' '
484+ let requestOptions: RequestInit = {
485+ method: ' POST' ,
410486 }
487+
411488 if (this .upload_type === UploadType .Cloud ) {
412489 // 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 } ,
490+ const params = new URLSearchParams ( {
491+ url: this . chosen_firmware_url ?. toString () ?? ' ' ,
492+ board_name: this .chosen_board ?.platform . name ?? ' ' ,
416493 })
494+ url = ` ${autopilot .API_URL }/install_firmware_from_url?${params } `
417495 } else if (this .upload_type === UploadType .Restore ) {
418496 // 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 },
497+ const params = new URLSearchParams ({
498+ board_name: this .chosen_board ?.platform .name ?? ' ' ,
422499 })
500+ url = ` ${autopilot .API_URL }/restore_default_firmware?${params } `
423501 } else {
424502 // Populate request with data for file install
425503 if (! this .firmware_file ) {
@@ -429,32 +507,91 @@ export default Vue.extend({
429507 }
430508 const form_data = new FormData ()
431509 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 ,
510+ const params = new URLSearchParams ({
511+ board_name: this .chosen_board ?.platform .name ?? ' ' ,
437512 })
513+ url = ` ${autopilot .API_URL }/install_firmware_from_file?${params } `
514+ requestOptions = {
515+ method: ' POST' ,
516+ body: form_data ,
517+ }
438518 }
439519
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
520+ try {
521+ const response = await fetch (url , requestOptions )
522+
523+ if (! response .ok ) {
524+ throw new Error (` HTTP error! status: ${response .status } ` )
525+ }
526+
527+ const reader = response .body ?.getReader ()
528+ const decoder = new TextDecoder ()
529+
530+ if (! reader ) {
531+ throw new Error (' No response body' )
532+ }
533+
534+ let buffer = ' '
535+
536+ // eslint-disable-next-line no-constant-condition
537+ while (true ) {
538+ const { done, value } = await reader .read ()
539+
540+ if (done ) break
541+
542+ buffer += decoder .decode (value , { stream: true })
543+ const lines = buffer .split (' \n ' )
544+
545+ // Keep the last incomplete line in the buffer
546+ buffer = lines .pop () ?? ' '
547+
548+ // Process complete lines
549+ for (const line of lines ) {
550+ if (line .trim ()) {
551+ try {
552+ const log = JSON .parse (line )
553+
554+ // Check if backend sent "done" signal to close connection
555+ if (log .stream .includes (' Started autopilot' )) {
556+ // Close the progress dialog immediately
557+ this .install_status = InstallStatus .Succeeded
558+ this .install_result_message = ' Installation completed'
559+ return
560+ }
561+
562+ this .install_logs .push (log )
563+ } catch (e ) {
564+ console .error (' Failed to parse log line:' , line , e )
565+ }
566+ }
454567 }
568+ }
569+
570+ // Check if there were any error messages in the logs
571+ const hasErrors = this .install_logs .some ((log ) => log .stream === ' stderr' )
572+ const lastError = this .install_logs .filter ((log ) => log .stream === ' stderr' ).pop ()
573+ // edge case. the re-plug message is not an error, but is thrown to stderr to get user's attention
574+ if (hasErrors && ! lastError ?.data .includes (' re-plug the USB connector' )) {
575+ this .install_status = InstallStatus .Failed
576+ this .install_result_message = lastError ?.data || ' Installation failed'
455577 const message = ` Could not install firmware: ${this .install_result_message }. `
456578 notifier .pushError (' FILE_FIRMWARE_INSTALL_FAIL' , message )
457- })
579+ } else {
580+ this .install_status = InstallStatus .Succeeded
581+ this .install_result_message = ' Successfully installed new firmware'
582+ autopilot_data .reset ()
583+ }
584+ } catch (error ) {
585+ this .install_status = InstallStatus .Failed
586+ // Catch Chrome's net:::ERR_UPLOAD_FILE_CHANGED error
587+ if (error .message && error .message === ' Network Error' ) {
588+ this .install_result_message = ' Upload fail. If the file was changed, clean the form and re-select it.'
589+ } else {
590+ this .install_result_message = error .response ?.data ?.detail ?? error .message
591+ }
592+ const message = ` Could not install firmware: ${this .install_result_message }. `
593+ notifier .pushError (' FILE_FIRMWARE_INSTALL_FAIL' , message )
594+ }
458595 },
459596 },
460597})
@@ -498,4 +635,28 @@ export default Vue.extend({
498635 align-items : flex-end ;
499636 }
500637}
638+
639+ .install-logs {
640+ background-color : rgba (0 , 0 , 0 , 0.8 );
641+ border-radius : 4px ;
642+ max-height : 300px ;
643+ overflow-y : auto ;
644+ font-family : ' Courier New' , monospace ;
645+ font-size : 12px ;
646+ }
647+
648+ .log-line {
649+ padding : 2px 4px ;
650+ white-space : pre-wrap ;
651+ word-break : break-word ;
652+ }
653+
654+ .info-log {
655+ color : #ffffff ;
656+ }
657+
658+ .error-log {
659+ color : #ff5252 ;
660+ font-weight : bold ;
661+ }
501662 </style >
0 commit comments