Skip to content

Commit b4f5860

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 b4f5860

4 files changed

Lines changed: 146 additions & 42 deletions

File tree

Lines changed: 113 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,125 @@
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+
let boundCallback: ((level: MessageLevel, msg: string) => void) | null = null
40+
let drainTimer: ReturnType<typeof setInterval> | null = null
41+
2642
export default Vue.extend({
2743
name: 'ErrorMessage',
2844
data() {
2945
return {
30-
level: undefined as MessageLevel|undefined,
31-
message: '',
32-
show: false,
46+
alerts: [] as AlertEntry[],
47+
queue: [] as AlertEntry[],
48+
}
49+
},
50+
mounted() {
51+
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(boundCallback)
62+
},
63+
beforeDestroy() {
64+
if (boundCallback) {
65+
message_manager.removeCallback(boundCallback)
66+
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 (drainTimer) return
95+
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+
}
100+
}, DRAIN_INTERVAL_MS)
101+
},
102+
stopDrain() {
103+
if (drainTimer) {
104+
clearInterval(drainTimer)
105+
drainTimer = null
106+
}
107+
},
108+
alertType(level: MessageLevel): string {
109+
switch (level) {
38110
case MessageLevel.Success:
39111
return 'success'
40112
case MessageLevel.Error:
113+
case MessageLevel.Critical:
41114
return 'error'
42-
case MessageLevel.Info:
43-
return 'info'
44115
case MessageLevel.Warning:
45116
return 'warning'
46-
case MessageLevel.Critical:
47-
return 'critical'
48117
default:
49118
return 'info'
50119
}
51120
},
52-
timeout(): number {
53-
switch (this.level) {
121+
getTimeout(level: MessageLevel): number {
122+
switch (level) {
54123
case MessageLevel.Success:
55124
case MessageLevel.Info:
56125
return 5000
@@ -61,12 +130,21 @@ export default Vue.extend({
61130
}
62131
},
63132
},
64-
mounted() {
65-
message_manager.addCallback((level: MessageLevel, message: string) => {
66-
this.level = level
67-
this.message = message
68-
this.show = true
69-
})
70-
},
71133
})
72134
</script>
135+
136+
<style scoped>
137+
.alerter-stack {
138+
position: fixed;
139+
bottom: 16px;
140+
left: 50%;
141+
transform: translateX(-50%);
142+
z-index: 1000;
143+
min-width: 344px;
144+
max-width: 672px;
145+
}
146+
147+
.alerter-item {
148+
margin-bottom: 8px;
149+
}
150+
</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)