Skip to content

Commit ce41c28

Browse files
committed
Merge branch 'feature/dfu-on-main-page' into dev
2 parents a9842bf + 50e9ddd commit ce41c28

File tree

6 files changed

+409
-317
lines changed

6 files changed

+409
-317
lines changed

frontend/src/app/layouts/Main/MainLayout.vue

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,40 @@
3636
<q-card-section class="q-pa-none q-ma-md" align="center">
3737
<template
3838
v-if="
39-
flipperStore.flags.isBridgeReady &&
40-
!flipperStore.flags.flipperIsInitialized
39+
!flipperStore.flags.isBridgeReady ||
40+
flipperStore.flags.flipperIsInitialized
4141
"
4242
>
43+
<Loading label="Flipper is initialized..." />
44+
</template>
45+
<template v-else-if="flipperStore.availableDfuFlippers.length">
46+
<q-list class="q-gutter-y-md full-width">
47+
<template
48+
v-for="flipper in flipperStore.availableDfuFlippers"
49+
:key="flipper.name"
50+
>
51+
<FlipperDfuItem :flipper>
52+
<template #default="{ flipper }">
53+
<q-btn
54+
unelevated
55+
dense
56+
color="primary"
57+
label="Repair"
58+
@click="flipperStore.recovery(flipper.info)"
59+
/>
60+
</template>
61+
</FlipperDfuItem>
62+
</template>
63+
</q-list>
64+
</template>
65+
<template v-else>
4366
<q-img
4467
src="~assets/flipper_alert.svg"
4568
width="70px"
4669
no-spinner
4770
/>
4871
<div class="text-h6 q-my-sm">Flipper not connected</div>
4972
</template>
50-
<template v-else>
51-
<Loading label="Flipper is initialized..." />
52-
</template>
5373
</q-card-section>
5474
</q-card>
5575
</q-page>
@@ -98,6 +118,13 @@
98118
<FlipperLogCard isDialog />
99119
</q-dialog>
100120
<FlipperDownloadPathDialog v-model="flipperStore.dialogs.downloadPath" />
121+
<FlipperRecoveryDialog
122+
v-model="flipperStore.dialogs.recovery"
123+
:persistent="
124+
flipperStore.flags.recovering && !flipperStore.recoveryError
125+
"
126+
@hide="flipperStore.resetRecovery(true)"
127+
/>
101128
</q-page-container>
102129
</q-layout>
103130
</template>
@@ -115,7 +142,9 @@ import {
115142
FlipperConnectFlipperDialog,
116143
FlipperMobileDetectedDialog,
117144
FlipperUnsupportedBrowserDialog,
118-
FlipperDownloadPathDialog
145+
FlipperDownloadPathDialog,
146+
FlipperRecoveryDialog,
147+
FlipperDfuItem
119148
} from 'entity/Flipper'
120149
import { AppsModel, AppOutdatedFirmwareDialog } from 'entity/Apps'
121150
const appsStore = AppsModel.useAppsStore()

frontend/src/entities/Flipper/model/stores.ts

Lines changed: 220 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { ref, unref, computed, reactive } from 'vue'
22
import { Platform } from 'quasar'
3+
34
import { defineStore } from 'pinia'
45
import { FlipperWeb, FlipperElectron } from 'shared/lib/flipperJs'
56

67
import { showNotif } from 'shared/lib/utils/useShowNotif'
7-
import { logger } from 'shared/lib/utils/useLog'
8+
import { logger, type LogLevel } from 'shared/lib/utils/useLog'
89

910
import { AppsModel } from 'entity/Apps'
1011
import {
@@ -13,6 +14,7 @@ import {
1314
DataFlipperElectron,
1415
DataDfuFlipperElectron
1516
} from './types'
17+
import { FlipperApi } from 'entity/Flipper'
1618
import { useRoute, useRouter, type RouteLocationRaw } from 'vue-router'
1719

1820
// import type { Emitter, DefaultEvents } from 'nanoevents'
@@ -55,10 +57,10 @@ export const useFlipperStore = defineStore('flipper', () => {
5557
mobileDetected: false,
5658
serialUnsupported: false,
5759
logs: false,
58-
downloadPath: false
60+
downloadPath: false,
61+
recovery: false
5962
})
6063

61-
const recoveringFlipperName = ref('')
6264
const oldFlipper = ref<FlipperElectron | FlipperWeb>()
6365
const flipper = ref<FlipperElectron | FlipperWeb>()
6466

@@ -518,14 +520,218 @@ export const useFlipperStore = defineStore('flipper', () => {
518520
}
519521
}
520522

523+
const recoveringFlipperName = ref('')
524+
const recoveryError = ref(false)
525+
const recoveryUpdateStage = ref('')
526+
const recoveryProgress = ref(0)
527+
const recoveryLogs = ref<string[]>([])
528+
529+
const retry = () => {
530+
resetRecovery(true)
531+
dialogs.recovery = false
532+
dialogs.multiflipper = true
533+
}
534+
const resetRecovery = (clearLogs = false) => {
535+
recoveryUpdateStage.value = ''
536+
recoveryProgress.value = 0
537+
if (clearLogs) {
538+
recoveryLogs.value = []
539+
}
540+
}
541+
const recovery = async (info: DataDfuFlipperElectron['info']) => {
542+
if (!isElectron) {
543+
return
544+
}
545+
546+
if (flipper.value) {
547+
flipper.value.disconnect()
548+
flipper.value = undefined
549+
}
550+
551+
recoveryError.value = false
552+
dialogs.multiflipper = false
553+
dialogs.recovery = true
554+
flags.recovering = true
555+
recoveringFlipperName.value = info.name
556+
flags.waitForReconnect = true
557+
// setAutoReconnectCondition.value(flags.value.autoReconnect)
558+
// flags.value.autoReconnect = false
559+
// autoReconnectFlipperName.value = info.name
560+
561+
// console.log(info)
562+
recoveryUpdateStage.value = 'Loading firmware bundle...'
563+
const firmwareTar = await FlipperApi.fetchFirmwareTar(
564+
`https://update.flipperzero.one/firmware/release/f${info.target}/update_tgz`
565+
)
566+
567+
const saved = await window.fs.saveToTemp({
568+
filename: 'update.tar',
569+
buffer: firmwareTar
570+
})
571+
572+
if (saved.status !== 'ok') {
573+
return
574+
}
575+
576+
let inactivityTimeout: NodeJS.Timeout
577+
const onTimeout = () => {
578+
const messageLong =
579+
'Error: Operation timed out. Please check USB connection and try again.'
580+
const messageShort = `Failed to repair ${info.name}: Repair timeout`
581+
showNotif({
582+
message: messageShort,
583+
color: 'negative'
584+
})
585+
logger.error({
586+
context: componentName,
587+
message: messageShort
588+
})
589+
unbindLogs()
590+
unbindStatus()
591+
recoveryUpdateStage.value = messageLong
592+
recoveryError.value = true
593+
}
594+
const updateInactivityTimeout = (stop = false) => {
595+
if (inactivityTimeout) {
596+
clearTimeout(inactivityTimeout)
597+
}
598+
599+
if (stop) {
600+
return
601+
}
602+
603+
inactivityTimeout = setTimeout(onTimeout, 60 * 1000)
604+
}
605+
606+
window.bridge.send({
607+
type: 'repair',
608+
name: info.name,
609+
data: {
610+
file: saved.path
611+
}
612+
})
613+
614+
updateInactivityTimeout()
615+
616+
const unbindLogs = bridgeEmitter.on('log', (stderr) => {
617+
const logLines = stderr.data.split('\n')
618+
logLines.pop()
619+
logLines.forEach((line: string) => {
620+
recoveryLogs.value.push(line)
621+
let level: LogLevel = 'debug'
622+
if (line.includes('[E]')) {
623+
level = 'error'
624+
} else if (line.includes('[W]')) {
625+
level = 'warn'
626+
} else if (line.includes('[I]')) {
627+
level = 'info'
628+
}
629+
logger[level]({
630+
context: componentName,
631+
message: line
632+
})
633+
})
634+
})
635+
636+
const unbindStatus = bridgeEmitter.on('status', async (status) => {
637+
if (status.error) {
638+
updateInactivityTimeout(true)
639+
let messageLong = `Failed to repair ${info.name}: ${status.error.message}`
640+
let messageShort = messageLong
641+
switch (status.error.message) {
642+
case 'UnknownError':
643+
messageLong =
644+
'Unknown error! Please try again. If the error persists, please contact support.'
645+
messageShort = `Failed to repair ${info.name}: Unknown error`
646+
break
647+
case 'InvalidDevice':
648+
messageLong =
649+
'Error: Cannot determine device type. Please try again.'
650+
messageShort = `Failed to repair ${info.name}: Invalid device`
651+
break
652+
case 'DiskError':
653+
messageLong =
654+
'Error: Cannot read/write to disk. The app may be missing permissions.'
655+
messageShort = `Failed to repair ${info.name}: Disk error`
656+
break
657+
case 'DataError':
658+
messageLong =
659+
'Error: Necessary files are corrupted. Please try again.'
660+
messageShort = `Failed to repair ${info.name}: Data error`
661+
break
662+
case 'SerialAccessError':
663+
messageLong =
664+
'Error: Cannot access device in Serial mode. Please check USB connection and permissions and try again.'
665+
messageShort = `Failed to repair ${info.name}: Serial access error`
666+
break
667+
case 'RecoveryAccessError':
668+
messageLong =
669+
'Error: Cannot access device in Recovery mode. Please check USB connection and permissions and try again.'
670+
messageShort = `Failed to repair ${info.name}: Recovery access error`
671+
break
672+
case 'OperationError':
673+
messageLong =
674+
'Error: Current operation was interrupted. Please try again.'
675+
messageShort = `Failed to repair ${info.name}: Operation error`
676+
break
677+
case 'SerialError':
678+
messageLong =
679+
'Error: Serial port error. Please check USB connection and try again.'
680+
messageShort = `Failed to repair ${info.name}: Serial error`
681+
break
682+
case 'RecoveryError':
683+
messageLong =
684+
'Error: Recovery mode error. Please check USB connection and try again.'
685+
messageShort = `Failed to repair ${info.name}: Recovery error`
686+
break
687+
case 'ProtocolError':
688+
messageLong =
689+
'Error: Protocol error. Please try again. If the error persists, please contact support.'
690+
messageShort = `Failed to repair ${info.name}: Protocol error`
691+
break
692+
case 'TimeoutError':
693+
messageLong =
694+
'Error: Operation timed out. Please check USB connection and try again.'
695+
messageShort = `Failed to repair ${info.name}: Timeout error`
696+
break
697+
}
698+
showNotif({
699+
message: messageShort,
700+
color: 'negative'
701+
})
702+
unbindLogs()
703+
unbindStatus()
704+
recoveryUpdateStage.value = messageLong
705+
recoveryError.value = true
706+
}
707+
if (status.message) {
708+
updateInactivityTimeout()
709+
recoveryUpdateStage.value = status.message
710+
}
711+
if (status.progress) {
712+
updateInactivityTimeout()
713+
recoveryProgress.value = status.progress / 100
714+
}
715+
if (status.finished) {
716+
updateInactivityTimeout(true)
717+
unbindLogs()
718+
unbindStatus()
719+
720+
dialogs.multiflipper = false
721+
dialogs.recovery = false
722+
723+
recoveryUpdateStage.value = 'Finished'
724+
}
725+
})
726+
}
727+
521728
return {
522729
isElectron,
523730

524731
flags,
525732
dialogs,
526733
connect,
527734
disconnect,
528-
recoveringFlipperName,
529735
oldFlipper,
530736
flipper,
531737
flipperReady,
@@ -545,6 +751,15 @@ export const useFlipperStore = defineStore('flipper', () => {
545751
availableDfuFlippers,
546752
availableBridgeFlippers,
547753
connectFlipper,
548-
findMicroSd
754+
findMicroSd,
755+
756+
recoveringFlipperName,
757+
retry,
758+
recovery,
759+
resetRecovery,
760+
recoveryUpdateStage,
761+
recoveryProgress,
762+
recoveryError,
763+
recoveryLogs
549764
}
550765
})
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<template>
2+
<q-item class="row rounded-borders">
3+
<q-item-section class="col-5">
4+
<img
5+
v-if="flipper.info?.color === 1"
6+
src="~assets/flipper_black.svg"
7+
style="width: 100%"
8+
/>
9+
<img
10+
v-else-if="flipper.info?.color === 3"
11+
src="~assets/flipper_transparent.svg"
12+
style="width: 100%"
13+
/>
14+
<img v-else src="~assets/flipper_white.svg" style="width: 100%" />
15+
</q-item-section>
16+
<q-item-section class="col-5 q-pl-md">
17+
<div>
18+
<div class="text-h6">{{ flipper.name }}</div>
19+
<div class="text-caption text-blue-14">Recovery mode</div>
20+
</div>
21+
</q-item-section>
22+
<q-item-section v-if="$slots.default" class="col-2">
23+
<slot name="default" v-bind="{ flipper }" />
24+
</q-item-section>
25+
</q-item>
26+
</template>
27+
28+
<script setup lang="ts">
29+
import { FlipperModel } from 'entity/Flipper'
30+
31+
interface Props {
32+
flipper: FlipperModel.DataDfuFlipperElectron
33+
}
34+
35+
defineProps<Props>()
36+
</script>

0 commit comments

Comments
 (0)