Skip to content

Commit 37bd996

Browse files
frontend: add failsafe configurations
1 parent deb3124 commit 37bd996

File tree

12 files changed

+1068
-0
lines changed

12 files changed

+1068
-0
lines changed

core/frontend/src/assets/img/configuration/failsafes/battery.svg

Lines changed: 144 additions & 0 deletions
Loading

core/frontend/src/assets/img/configuration/failsafes/crash.svg

Lines changed: 85 additions & 0 deletions
Loading

core/frontend/src/assets/img/configuration/failsafes/ekf.svg

Lines changed: 94 additions & 0 deletions
Loading

core/frontend/src/assets/img/configuration/failsafes/heartbeat.svg

Lines changed: 41 additions & 0 deletions
Loading

core/frontend/src/assets/img/configuration/failsafes/leak.svg

Lines changed: 45 additions & 0 deletions
Loading

core/frontend/src/assets/img/configuration/failsafes/pilot-input.svg

Lines changed: 37 additions & 0 deletions
Loading

core/frontend/src/assets/img/configuration/failsafes/pressure.svg

Lines changed: 53 additions & 0 deletions
Loading
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
<template>
2+
<v-form
3+
ref="form"
4+
v-model="is_form_valid"
5+
@submit.prevent="saveEditedParam(false)"
6+
>
7+
<template v-if="!custom_input && param?.bitmask">
8+
<v-checkbox
9+
v-for="(key, keyvalue) in param?.bitmask"
10+
:key="keyvalue"
11+
v-model="selected_bitflags"
12+
dense
13+
hide-details
14+
:loading="waiting_for_param_update"
15+
:label="key"
16+
:value="2 ** keyvalue"
17+
/>
18+
</template>
19+
<v-autocomplete
20+
v-else-if="!custom_input && Object.entries(param?.options ?? []).length > 10"
21+
v-model.number="new_value"
22+
variant="solo"
23+
:items="as_select_items"
24+
/>
25+
<v-select
26+
v-else-if="!custom_input && param?.options"
27+
v-model.number="new_value"
28+
dense
29+
:items="as_select_items"
30+
:rules="[() => true]"
31+
:indeterminate="waiting_for_param_update"
32+
@change="updateVariables"
33+
/>
34+
<v-text-field
35+
v-if="
36+
custom_input
37+
|| (param
38+
&& !param.options
39+
&& !param.bitmask
40+
)
41+
"
42+
v-model.number="new_value"
43+
dense
44+
type="number"
45+
:step="param.increment ?? 0.01"
46+
:suffix="param.units"
47+
:rules="forcing_input ? [] : [isInRange, isValidType]"
48+
:loading="waiting_for_param_update"
49+
@blur="updateVariables"
50+
/>
51+
52+
<v-checkbox
53+
v-if="show_advanced_checkbox"
54+
v-model="forcing_input"
55+
dense
56+
:label="'Force'"
57+
/>
58+
<v-checkbox
59+
v-if="show_custom_checkbox"
60+
v-model="custom_input"
61+
dense
62+
:label="'Custom'"
63+
/>
64+
</v-form>
65+
</template>
66+
67+
<script lang="ts">
68+
import Vue, { PropType } from 'vue'
69+
70+
import mavlink2rest from '@/libs/MAVLink2Rest'
71+
import autopilot_data from '@/store/autopilot'
72+
import Parameter from '@/types/autopilot/parameter'
73+
74+
export default Vue.extend({
75+
name: 'InlineParameterEditor',
76+
props: {
77+
param: {
78+
type: Object as PropType<Parameter> | undefined,
79+
default: undefined,
80+
},
81+
allowCustom: {
82+
type: Boolean,
83+
default: false,
84+
},
85+
},
86+
data() {
87+
return {
88+
custom_input: false,
89+
forcing_input: false,
90+
// Form can't be computed correctly, so we save it's state under data
91+
is_form_valid: false,
92+
new_value: 0,
93+
selected_bitflags: [] as number[],
94+
}
95+
},
96+
computed: {
97+
as_select_items() {
98+
const entries = Object.entries(this.param?.options ?? [])
99+
return entries.map(([value, name]) => ({ text: name, value: parseFloat(value), disabled: false }))
100+
},
101+
edited_bitmask_value(): number {
102+
return this.selected_bitflags.reduce((accumulator, current) => accumulator + current, 0)
103+
},
104+
show_advanced_checkbox(): boolean {
105+
return typeof this.isInRange(this.new_value ?? 0) === 'string'
106+
},
107+
show_custom_checkbox(): boolean {
108+
return !!(this.param?.options || this.param?.bitmask) && this.allowCustom
109+
},
110+
waiting_for_param_update(): boolean {
111+
return this.param.value !== this.new_value
112+
},
113+
param_value() {
114+
return this.param?.value ?? 0
115+
},
116+
},
117+
watch: {
118+
is_form_valid(valid) {
119+
if (!valid) {
120+
this.forcing_input = true
121+
}
122+
},
123+
selected_bitflags() {
124+
this.new_value = this.edited_bitmask_value
125+
this.updateVariables()
126+
},
127+
param_value() {
128+
this.updateSelectedFlags()
129+
},
130+
},
131+
mounted() {
132+
this.new_value = this.param?.value ?? 0
133+
this.updateVariables()
134+
this.updateSelectedFlags()
135+
},
136+
methods: {
137+
isInRange(input: number | string): boolean | string {
138+
// The input value is an empty string when the field is empty
139+
if (typeof input === 'string' && input?.trim().length === 0) {
140+
return 'This should be a number between min and max'
141+
}
142+
input = Number(input)
143+
144+
if (!this.param?.range) {
145+
return true
146+
}
147+
148+
if (this.param?.bitmask && input < 0) {
149+
return 'Value should be greater or equal to 0'
150+
}
151+
152+
if (input > this.param.range.high) {
153+
return `Value should be smaller than ${this.param.range.high}`
154+
}
155+
if (input < this.param.range.low) {
156+
return `Value should be greater than ${this.param.range.low}`
157+
}
158+
return true
159+
},
160+
isValidType(input: number): boolean | string {
161+
if (this.param?.paramType.type.includes('UINT')) {
162+
if (input < 0) {
163+
return 'This parameter must be a positive Integer'
164+
}
165+
}
166+
if (this.param?.paramType.type.includes('INT')) {
167+
if (!Number.isInteger(input)) {
168+
return 'This parameter must be an Integer'
169+
}
170+
}
171+
return true
172+
},
173+
updateSelectedFlags(): void {
174+
const value = this.param.value ?? 0
175+
if (value < 0) {
176+
// No bitmask checking for negative values
177+
return
178+
}
179+
180+
const output = []
181+
for (let bit = 0; bit < 64; bit += 1) {
182+
const bitmask_value = 2 ** bit
183+
// eslint-disable-next-line no-bitwise
184+
if (value & bitmask_value) {
185+
output.push(bitmask_value)
186+
}
187+
}
188+
this.selected_bitflags = output
189+
},
190+
191+
async saveEditedParam() {
192+
if (this.param_value === this.new_value) {
193+
return
194+
}
195+
if (!this.forcing_input && !this.is_form_valid) {
196+
return
197+
}
198+
if (this.param == null) {
199+
return
200+
}
201+
if (!this.custom_input && this.param?.bitmask !== undefined) {
202+
this.new_value = this.edited_bitmask_value
203+
}
204+
let value = 0
205+
if (typeof this.new_value === 'string') {
206+
value = parseFloat(this.new_value)
207+
} else {
208+
value = this.new_value
209+
}
210+
if (this.param?.rebootRequired) {
211+
autopilot_data.setRebootRequired(false)
212+
}
213+
mavlink2rest.setParam(this.param.name, value, autopilot_data.system_id, this.param.paramType.type)
214+
},
215+
updateVariables(): void {
216+
// Select custom input if value is outside of possible options
217+
// Remove custom once value is known
218+
if (this.custom_input) {
219+
this.custom_input = !Object.keys(this.param?.options ?? [])
220+
.map((value) => parseFloat(value))
221+
.includes(this.new_value)
222+
}
223+
224+
this.saveEditedParam()
225+
if (!this.is_form_valid) {
226+
this.forcing_input = true
227+
}
228+
},
229+
},
230+
})
231+
</script>

core/frontend/src/components/vehiclesetup/Configure.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import autopilot from '@/store/autopilot_manager'
3838
import SpinningLogo from '../common/SpinningLogo.vue'
3939
import ArdupilotAccelerometerSetup from './configuration/accelerometer/ArdupilotAccelerometerSetup.vue'
4040
import ArdupilotMavlinkCompassSetup from './configuration/compass/ArdupilotMavlinkCompassSetup.vue'
41+
import FailsafesConfigration from './configuration/failsafes/Failsafes.vue'
4142
import LightsConfigration from './configuration/lights.vue'
4243
import BaroCalib from './overview/BaroCalib.vue'
4344
import GripperInfo from './overview/gripper.vue'
@@ -77,6 +78,7 @@ export default Vue.extend({
7778
{ title: 'Compass', component: ArdupilotMavlinkCompassSetup },
7879
{ title: 'Baro', component: BaroCalib },
7980
{ title: 'Lights', component: LightsConfigration, filter: () => autopilot.vehicle_type === 'Submarine' },
81+
{ title: 'Failsafes', component: FailsafesConfigration },
8082
] as Item[],
8183
}
8284
},
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<template>
2+
<v-card
3+
v-if="all_required_params_are_available"
4+
elevation="2"
5+
class="mb-4 mt-4 pa-4 d-flex flex-row flex-grow-0 justify-left failsafe-card"
6+
>
7+
<div class="ma-4">
8+
<!-- this is theoretically not safe, but we have a command that gives users root access, so... -->
9+
<!-- eslint-disable vue/no-v-html -->
10+
<i :class="`${svg_outside_style} svg-icon`" v-html="image" />
11+
</div>
12+
<div class="d-flex flex-column justify-center">
13+
<v-card-title> {{ failsafeDefinition.name }}</v-card-title>
14+
<v-card-text>
15+
{{ failsafeDefinition.generalDescription }}
16+
</v-card-text>
17+
<div>
18+
<div v-for="param in available_params" :key="param.name">
19+
<v-row class="justify-right">
20+
<v-col :key="param.name" class="action-col" cols="7">
21+
<v-icon v-if="param.icon">
22+
{{ param.icon }}
23+
</v-icon>
24+
{{ param.replacementTitle ?? param.name }}
25+
</v-col>
26+
<v-col :key="`${param.name}-editor`" cols="5" class="pt-1 pb-1">
27+
<inline-parameter-editor :key="failsafeDefinition.name" :param="params[param.name]" />
28+
</v-col>
29+
</v-row>
30+
</div>
31+
</div>
32+
</div>
33+
</v-card>
34+
</template>
35+
36+
<script lang="ts">
37+
import axios from 'axios'
38+
import Vue, { PropType } from 'vue'
39+
import { Dictionary } from 'vue-router/types/router.js'
40+
41+
import { FailsafeDefinition, ParamDefinitions } from '@/components/vehiclesetup/configuration/failsafes/types'
42+
import autopilot_data from '@/store/autopilot'
43+
import settings from '@/store/settings'
44+
import Parameter from '@/types/autopilot/parameter'
45+
46+
export default Vue.extend({
47+
name: 'FailsafeCard',
48+
components: {
49+
'inline-parameter-editor': () => import('@/components/parameter-editor/InlineParameterEditor.vue'),
50+
},
51+
props: {
52+
failsafeDefinition: {
53+
type: Object as PropType<FailsafeDefinition>,
54+
required: true,
55+
},
56+
57+
},
58+
data() {
59+
return {
60+
image: undefined as string | undefined,
61+
}
62+
},
63+
computed: {
64+
svg_outside_style(): string {
65+
return `mr-0 ${settings.is_dark_theme ? 'svg-outline-dark' : 'svg-outline-light'}`
66+
},
67+
params(): Dictionary<Parameter> {
68+
return autopilot_data.parameters
69+
.filter((param) => this.failsafeDefinition.params.map((parameter) => parameter.name)
70+
.includes(param.name))
71+
.reduce((dict: Dictionary<Parameter>, param: Parameter) => {
72+
dict[param.name] = param
73+
return dict
74+
}, {})
75+
},
76+
all_required_params_are_available(): boolean {
77+
return this.failsafeDefinition.params.every((param) => param.name in this.params || param.optional)
78+
},
79+
available_params(): ParamDefinitions[] {
80+
return this.failsafeDefinition.params.filter((param) => param.name in this.params)
81+
},
82+
},
83+
mounted() {
84+
this.loadImage()
85+
},
86+
methods: {
87+
loadImage() {
88+
axios.get(this.failsafeDefinition.image).then((response) => {
89+
this.image = response.data
90+
})
91+
},
92+
},
93+
94+
})
95+
</script>
96+
97+
<style>
98+
99+
i.svg-icon svg {
100+
height: 100% !important;
101+
min-width: 180px;
102+
}
103+
104+
i.svg-outline-dark path {
105+
fill: white;
106+
}
107+
108+
i.svg-outline-light path {
109+
fill: black;
110+
}
111+
112+
.failsafe-card {
113+
margin-left: auto;
114+
margin-right: auto;
115+
width: 700px;
116+
}
117+
118+
.action-col {
119+
text-align: end;
120+
margin: auto;
121+
}
122+
</style>

0 commit comments

Comments
 (0)