Skip to content

Commit fd587d0

Browse files
frontend: show snackbar notifications on parameter changes
Emit an info-level snackbar when a MAVLink parameter value changes after initial loading is complete, only in developer mode. Refactor Alerter to render a stack of v-alert components so multiple notifications are visible at once, with auto-dismiss timers and a cap of 5 visible alerts. Persistent alerts (error/critical) are preserved when evicting to stay within the cap. Made-with: Cursor
1 parent c024745 commit fd587d0

4 files changed

Lines changed: 148 additions & 42 deletions

File tree

Lines changed: 115 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,127 @@
11
<template>
2-
<v-snackbar
3-
v-model="show"
4-
:timeout="timeout"
5-
>
6-
{{ message }}
7-
8-
<template #action="{ attrs }">
9-
<v-btn
10-
:color="color"
11-
text
12-
v-bind="attrs"
13-
@click="show = false"
2+
<div class="alerter-stack">
3+
<v-slide-y-reverse-transition
4+
group
5+
leave-absolute
6+
>
7+
<v-alert
8+
v-for="alert in alerts"
9+
:key="alert.id"
10+
:value="true"
11+
:type="alertType(alert.level)"
12+
class="alerter-item"
13+
dense
14+
dismissible
15+
elevation="6"
16+
@input="dismiss(alert.id)"
1417
>
15-
Close
16-
</v-btn>
17-
</template>
18-
</v-snackbar>
18+
{{ alert.message }}
19+
</v-alert>
20+
</v-slide-y-reverse-transition>
21+
</div>
1922
</template>
2023

2124
<script lang="ts">
2225
import Vue from 'vue'
2326
2427
import message_manager, { MessageLevel } from '@/libs/message-manager'
2528
29+
const MAX_VISIBLE = 5
30+
const DRAIN_INTERVAL_MS = 1000
31+
32+
interface AlertEntry {
33+
id: number
34+
level: MessageLevel
35+
message: string
36+
}
37+
38+
let nextId = 0
39+
2640
export default Vue.extend({
2741
name: 'ErrorMessage',
2842
data() {
2943
return {
30-
level: undefined as MessageLevel|undefined,
31-
message: '',
32-
show: false,
44+
alerts: [] as AlertEntry[],
45+
queue: [] as AlertEntry[],
46+
drainTimer: null as ReturnType<typeof setInterval> | null,
47+
boundCallback: null as ((level: MessageLevel, msg: string) => void) | null,
48+
}
49+
},
50+
mounted() {
51+
this.boundCallback = (level: MessageLevel, message: string) => {
52+
nextId += 1
53+
const entry = { id: nextId, level, message }
54+
if (this.alerts.length < MAX_VISIBLE) {
55+
this.showAlert(entry)
56+
} else {
57+
this.queue.push(entry)
58+
this.startDrain()
59+
}
60+
}
61+
message_manager.addCallback(this.boundCallback)
62+
},
63+
beforeDestroy() {
64+
if (this.boundCallback) {
65+
message_manager.removeCallback(this.boundCallback)
66+
this.boundCallback = null
3367
}
68+
this.stopDrain()
3469
},
35-
computed: {
36-
color(): string {
37-
switch (this.level) {
70+
methods: {
71+
showAlert(entry: AlertEntry) {
72+
this.alerts.push(entry)
73+
const timeout = this.getTimeout(entry.level)
74+
if (timeout > 0) {
75+
setTimeout(() => this.dismiss(entry.id), timeout)
76+
}
77+
},
78+
dismiss(id: number) {
79+
const idx = this.alerts.findIndex((a) => a.id === id)
80+
if (idx !== -1) {
81+
this.alerts.splice(idx, 1)
82+
this.promoteFromQueue()
83+
}
84+
},
85+
promoteFromQueue() {
86+
while (this.queue.length > 0 && this.alerts.length < MAX_VISIBLE) {
87+
this.showAlert(this.queue.shift()!)
88+
}
89+
if (this.queue.length === 0) {
90+
this.stopDrain()
91+
}
92+
},
93+
startDrain() {
94+
if (this.drainTimer) return
95+
this.drainTimer = setInterval(() => {
96+
const evictIdx = this.alerts.findIndex((a) => this.getTimeout(a.level) > 0)
97+
if (evictIdx !== -1) {
98+
this.dismiss(this.alerts[evictIdx].id)
99+
} else if (this.queue.length > 0 && this.alerts.length > 0) {
100+
this.dismiss(this.alerts[0].id)
101+
}
102+
}, DRAIN_INTERVAL_MS)
103+
},
104+
stopDrain() {
105+
if (this.drainTimer) {
106+
clearInterval(this.drainTimer)
107+
this.drainTimer = null
108+
}
109+
},
110+
alertType(level: MessageLevel): string {
111+
switch (level) {
38112
case MessageLevel.Success:
39113
return 'success'
40114
case MessageLevel.Error:
115+
case MessageLevel.Critical:
41116
return 'error'
42-
case MessageLevel.Info:
43-
return 'info'
44117
case MessageLevel.Warning:
45118
return 'warning'
46-
case MessageLevel.Critical:
47-
return 'critical'
48119
default:
49120
return 'info'
50121
}
51122
},
52-
timeout(): number {
53-
switch (this.level) {
123+
getTimeout(level: MessageLevel): number {
124+
switch (level) {
54125
case MessageLevel.Success:
55126
case MessageLevel.Info:
56127
return 5000
@@ -61,12 +132,21 @@ export default Vue.extend({
61132
}
62133
},
63134
},
64-
mounted() {
65-
message_manager.addCallback((level: MessageLevel, message: string) => {
66-
this.level = level
67-
this.message = message
68-
this.show = true
69-
})
70-
},
71135
})
72136
</script>
137+
138+
<style scoped>
139+
.alerter-stack {
140+
position: fixed;
141+
bottom: 16px;
142+
left: 50%;
143+
transform: translateX(-50%);
144+
z-index: 1000;
145+
min-width: 344px;
146+
max-width: 672px;
147+
}
148+
149+
.alerter-item {
150+
margin-bottom: 8px;
151+
}
152+
</style>

core/frontend/src/libs/message-manager.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ class MessageManager {
4848
this.callbacks.push(callback)
4949
}
5050

51+
/**
52+
* Remove a previously added callback
53+
*/
54+
removeCallback(callback:(level: MessageLevel, msg: string) => void): void {
55+
const index = this.callbacks.indexOf(callback)
56+
if (index !== -1) {
57+
this.callbacks.splice(index, 1)
58+
}
59+
}
60+
5161
/**
5262
* Emit a new message to be used in all callbacks
5363
*/

core/frontend/src/types/autopilot/parameter-fetcher.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import mavlink2rest from '@/libs/MAVLink2Rest'
2+
import message_manager, { MessageLevel } from '@/libs/message-manager'
3+
import settings from '@/libs/settings'
24
// eslint-disable-next-line import/no-cycle
35
import ardupilot_data from '@/store/autopilot'
46
import { AutopilotStore } from '@/store/autopilot'
@@ -33,6 +35,10 @@ export default class ParameterFetcher {
3335
this.store = store
3436
}
3537

38+
allParametersLoaded(): boolean {
39+
return this.total_params_count !== null && this.loaded_params_count >= this.total_params_count
40+
}
41+
3642
reset(): void {
3743
this.loaded_params_count = 0
3844
this.total_params_count = null
@@ -57,9 +63,7 @@ export default class ParameterFetcher {
5763
}
5864

5965
requestParamsWatchdog(): void {
60-
if (this.total_params_count !== null
61-
&& this.loaded_params_count > 0
62-
&& this.loaded_params_count >= this.total_params_count) {
66+
if (this.loaded_params_count > 0 && this.allParametersLoaded()) {
6367
return
6468
}
6569
if (autopilot.restarting) {
@@ -107,7 +111,13 @@ export default class ParameterFetcher {
107111
// We need this due to mismatches between js 64-bit floats and REAL32 in MAVLink
108112
const trimmed_value = Math.round(param_value * 10000) / 10000
109113
if (param_index === 65535) {
110-
this.parameter_table.updateParam(param_name, trimmed_value)
114+
const change = this.parameter_table.updateParam(param_name, trimmed_value)
115+
if (change && this.allParametersLoaded() && settings.is_dev_mode) {
116+
message_manager.emitMessage(
117+
MessageLevel.Info,
118+
`Parameter ${param_name} changed: ${change.oldValue}${trimmed_value}`,
119+
)
120+
}
111121
} else {
112122
this.parameter_table.addParam(
113123
{

core/frontend/src/types/autopilot/parameter-table.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,15 +190,21 @@ export default class ParametersTable {
190190
this.parametersDict[param.id] = param
191191
}
192192

193-
updateParam(param_name: string, param_value: number): void {
193+
updateParam(param_name: string, param_value: number): { oldValue: number } | null {
194194
const index = Object.entries(this.parametersDict).find(([_key, value]) => value.name === param_name)
195195
if (!index) {
196196
// This is benign and will happen if we receive a parameter update before the parameters table
197197
// is fully populated. We can safely ignore it.
198198
console.info(`Unable to update param in store: ${param_name}. Parameter not yet loaded into ParametersTable.`)
199-
return
199+
return null
200+
}
201+
const paramKey = parseInt(index[0], 10)
202+
const oldValue = this.parametersDict[paramKey].value
203+
this.parametersDict[paramKey].value = param_value
204+
if (oldValue !== param_value) {
205+
return { oldValue }
200206
}
201-
this.parametersDict[parseInt(index[0], 10)].value = param_value
207+
return null
202208
}
203209

204210
parameters(): Parameter[] {

0 commit comments

Comments
 (0)