Skip to content

Commit b6afbb2

Browse files
Autopilot Manager: allow detecting all serial boards, start infra-structure for supporting PX4
frontend: update to deal with boards with multiple platforms cleaning up unused file cleanup add tools for syncing files between local repo and remote blueos on the pi include current board commonwealth: support for coroutines on @temporary_cache() asyncing test decorators core: bump bluerobotics-ping to 0.2.3 wip wip: use exclusive=True on ardupilot_fw_uploader.py wip test test
1 parent 08c3e04 commit b6afbb2

File tree

28 files changed

+1342
-401
lines changed

28 files changed

+1342
-401
lines changed

core/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ RUN /home/pi/tools/install-static-binaries.sh
3333
# Generation of python virtual environment for our libraries and services
3434
FROM base AS install-services-and-libs
3535

36-
RUN apt update && apt install -y --no-install-recommends g++
36+
RUN apt update && apt install -y --no-install-recommends g++ libcap2-bin libcap2-dev
3737

3838
# UV installation
3939
ADD https://astral.sh/uv/install.sh /uv-installer.sh

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

Lines changed: 199 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,35 @@
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>
@@ -221,7 +247,6 @@
221247
</template>
222248

223249
<script lang="ts">
224-
import { AxiosRequestConfig } from 'axios'
225250
import Vue from 'vue'
226251
227252
import Notifier from '@/libs/notifier'
@@ -236,7 +261,7 @@ import {
236261
Vehicle,
237262
} from '@/types/autopilot'
238263
import { autopilot_service } from '@/types/frontend_services'
239-
import back_axios, { isBackendOffline } from '@/utils/api'
264+
import back_axios from '@/utils/api'
240265
241266
const 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>

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)