Skip to content

Commit c75a32a

Browse files
authored
Add gripper related params on config page (#744)
* Add gripper related params on config page * Address copilot review comments * Fix python type hints * Fix type hints * Fix import error
1 parent 9e83d54 commit c75a32a

6 files changed

Lines changed: 356 additions & 34 deletions

File tree

gcs/src/components/config/gripper.jsx

Lines changed: 167 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,148 @@
55
*/
66

77
// Native imports
8-
import { useEffect } from "react"
8+
import { useEffect, useMemo } from "react"
99

1010
// 3rd Party Imports
11-
import { Button } from "@mantine/core"
11+
import {
12+
Button,
13+
LoadingOverlay,
14+
NumberInput,
15+
Select,
16+
Tooltip,
17+
} from "@mantine/core"
1218

1319
// Styling Imports
1420
import resolveConfig from "tailwindcss/resolveConfig"
1521
import tailwindConfig from "../../../tailwind.config"
1622
const tailwindColors = resolveConfig(tailwindConfig).theme.colors
1723

24+
// Data
25+
import apmParamDefsCopter from "../../../data/gen_apm_params_def_copter.json"
26+
import apmParamDefsPlane from "../../../data/gen_apm_params_def_plane.json"
27+
1828
// Redux
29+
import { useDebouncedCallback } from "@mantine/hooks"
30+
import { IconInfoCircle } from "@tabler/icons-react"
1931
import { useDispatch, useSelector } from "react-redux"
20-
import { emitSetGripper } from "../../redux/slices/configSlice"
32+
import {
33+
emitGetGripperConfig,
34+
emitSetGripper,
35+
emitSetGripperConfigParam,
36+
selectGripperConfig,
37+
selectRefreshingGripperConfigData,
38+
} from "../../redux/slices/configSlice"
2139
import {
2240
emitSetState,
2341
selectConnectedToDrone,
2442
} from "../../redux/slices/droneConnectionSlice"
43+
import { selectAircraftTypeString } from "../../redux/slices/droneInfoSlice"
44+
45+
const GRIPPER_PARAMS = [
46+
"GRIP_CAN_ID",
47+
"GRIP_AUTOCLOSE",
48+
"GRIP_GRAB",
49+
"GRIP_NEUTRAL",
50+
"GRIP_REGRAB",
51+
"GRIP_RELEASE",
52+
"GRIP_TYPE",
53+
]
54+
55+
function cleanFloat(value, decimals = 3) {
56+
if (typeof value === "number") {
57+
return Number(value.toFixed(decimals))
58+
}
59+
if (!isNaN(value)) {
60+
return Number(parseFloat(value).toFixed(decimals))
61+
}
62+
return value
63+
}
64+
65+
// Try to handle floats because mantine handles keys internally as strings
66+
// Which leads to floating point rounding errors
67+
function sanitiseInput(value, toString = false) {
68+
let sanitisedValue = value
69+
if (!isNaN(value) && String(value).trim() !== "") {
70+
sanitisedValue = String(value).includes(".")
71+
? parseFloat(value)
72+
: parseInt(value)
73+
}
74+
75+
return toString ? `${sanitisedValue}` : sanitisedValue
76+
}
77+
78+
function InputLabel({ param }) {
79+
return (
80+
<p className="flex flex-row items-center gap-1">
81+
{param.param_id}{" "}
82+
<span>
83+
<Tooltip
84+
className="inline"
85+
label={
86+
<>
87+
<p className="text-wrap max-w-80">
88+
{param.param_def?.Description}
89+
</p>
90+
</>
91+
}
92+
>
93+
<IconInfoCircle size={16} />
94+
</Tooltip>
95+
</span>
96+
</p>
97+
)
98+
}
2599

26100
export default function Gripper() {
27101
const dispatch = useDispatch()
28102
const connected = useSelector(selectConnectedToDrone)
103+
const aircraftTypeString = useSelector(selectAircraftTypeString)
104+
const gripperConfig = useSelector(selectGripperConfig)
105+
const refreshingGripperConfigData = useSelector(
106+
selectRefreshingGripperConfigData,
107+
)
29108

30-
// Set gripper config values
31-
function setGripper(action) {
32-
dispatch(emitSetGripper(action))
33-
}
109+
const params = useMemo(() => {
110+
if (!gripperConfig) {
111+
return []
112+
}
113+
114+
const paramDefs =
115+
aircraftTypeString === "Copter" ? apmParamDefsCopter : apmParamDefsPlane
116+
117+
return GRIPPER_PARAMS.map((param) => {
118+
return {
119+
param_id: param,
120+
param_value: gripperConfig[param] ?? "UNKNOWN",
121+
param_def: paramDefs[param],
122+
}
123+
})
124+
}, [gripperConfig, aircraftTypeString])
34125

35126
useEffect(() => {
36-
if (connected) {
37-
dispatch(emitSetState("config"))
127+
if (!connected) {
128+
return
38129
}
130+
131+
dispatch(emitSetState("config"))
132+
dispatch(emitGetGripperConfig())
39133
}, [connected])
40134

135+
function setGripper(action) {
136+
dispatch(emitSetGripper(action))
137+
}
138+
139+
const debouncedUpdate = useDebouncedCallback((param_id, value) => {
140+
dispatch(emitSetGripperConfigParam({ param_id, value }))
141+
}, 500)
142+
41143
return (
42-
<div className="m-4 w-1/2">
144+
<div className="flex flex-col gap-4 mx-4">
145+
<LoadingOverlay
146+
visible={refreshingGripperConfigData}
147+
zIndex={1000}
148+
overlayProps={{ blur: 2 }}
149+
/>
43150
<div className="flex flex-row gap-2">
44151
<Button
45152
onClick={() => setGripper("release")}
@@ -54,6 +161,56 @@ export default function Gripper() {
54161
Grab Gripper
55162
</Button>
56163
</div>
164+
<div className="flex flex-col gap-2">
165+
{params.map((param) => (
166+
<div key={param.param_id} className="flex flex-row justify-between">
167+
{param.param_def?.Values ? (
168+
<Select
169+
label={<InputLabel param={param} />}
170+
className="w-64"
171+
value={`${cleanFloat(param.param_value)}`}
172+
onChange={(value) => {
173+
dispatch(
174+
emitSetGripperConfigParam({
175+
param_id: param.param_id,
176+
value: sanitiseInput(value),
177+
}),
178+
)
179+
}}
180+
data={Object.keys(param.param_def?.Values).map((key) => ({
181+
value: `${key}`,
182+
label: `${key}: ${param.param_def?.Values[key]}`,
183+
}))}
184+
allowDeselect={false}
185+
/>
186+
) : (
187+
<NumberInput
188+
label={<InputLabel param={param} />}
189+
description={
190+
param.param_def?.Range
191+
? `${param.param_def?.Range.low} - ${param.param_def?.Range.high}`
192+
: ""
193+
}
194+
className="w-64"
195+
value={param.param_value}
196+
onChange={(value) => {
197+
if (value === "" || isNaN(value)) {
198+
return
199+
}
200+
debouncedUpdate(param.param_id, value)
201+
}}
202+
decimalScale={5}
203+
hideControls
204+
min={param.param_def?.Range ? param.param_def?.Range.low : null}
205+
max={
206+
param.param_def?.Range ? param.param_def?.Range.high : null
207+
}
208+
suffix={param.param_def?.Units}
209+
/>
210+
)}
211+
</div>
212+
))}
213+
</div>
57214
</div>
58215
)
59216
}

gcs/src/redux/middleware/emitters.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import {
22
emitGetFlightModeConfig,
33
emitGetFrameConfig,
4+
emitGetGripperConfig,
45
emitGetGripperEnabled,
56
emitGetRcConfig,
67
emitRefreshFlightModeData,
78
emitSetFlightMode,
89
emitSetGripper,
10+
emitSetGripperConfigParam,
911
emitTestAllMotors,
1012
emitTestMotorSequence,
1113
emitTestOneMotor,
14+
setRefreshingGripperConfigData,
1215
} from "../slices/configSlice"
1316
import {
1417
emitArmDisarm,
@@ -223,6 +226,22 @@ export function handleEmitters(socket, store, action) {
223226
emitter: emitGetGripperEnabled,
224227
callback: () => socket.socket.emit("get_gripper_enabled"),
225228
},
229+
{
230+
emitter: emitGetGripperConfig,
231+
callback: () => {
232+
socket.socket.emit("get_gripper_config")
233+
store.dispatch(setRefreshingGripperConfigData(true))
234+
},
235+
},
236+
{
237+
emitter: emitSetGripperConfigParam,
238+
callback: () => {
239+
socket.socket.emit("set_gripper_config_param", {
240+
param_id: action.payload.param_id,
241+
value: action.payload.value,
242+
})
243+
},
244+
},
226245
{
227246
emitter: emitGetFlightModeConfig,
228247
callback: () => socket.socket.emit("get_flight_mode_config"),

gcs/src/redux/middleware/socketMiddleware.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,13 @@ import {
4040
setFrameTypeName,
4141
setFrameTypeOrder,
4242
setGetGripperEnabled,
43+
setGripperConfig,
4344
setNumberOfMotors,
4445
setRadioChannels,
4546
setRefreshingFlightModeData,
47+
setRefreshingGripperConfigData,
4648
setShowMotorTestWarningModal,
49+
updateGripperConfigParam,
4750
} from "../slices/configSlice.js"
4851
import {
4952
setAttitudeData,
@@ -140,6 +143,8 @@ const MissionSpecificSocketEvents = Object.freeze({
140143
const ConfigSpecificSocketEvents = Object.freeze({
141144
onGripperEnabled: "is_gripper_enabled",
142145
onSetGripperResult: "set_gripper_result",
146+
onGripperConfig: "gripper_config",
147+
setGripperParamResult: "set_gripper_param_result",
143148
onMotorTestResult: "motor_test_result",
144149
onFlightModeConfig: "flight_mode_config",
145150
onSetFlightModeResult: "set_flight_mode_result",
@@ -699,6 +704,28 @@ const socketMiddleware = (store) => {
699704
},
700705
)
701706

707+
socket.socket.on(ConfigSpecificSocketEvents.onGripperConfig, (msg) => {
708+
store.dispatch(setGripperConfig(msg.params))
709+
store.dispatch(setRefreshingGripperConfigData(false))
710+
})
711+
712+
socket.socket.on(
713+
ConfigSpecificSocketEvents.setGripperParamResult,
714+
(msg) => {
715+
if (msg.success) {
716+
showSuccessNotification(msg.message)
717+
store.dispatch(
718+
updateGripperConfigParam({
719+
param_id: msg.param_id,
720+
value: msg.value,
721+
}),
722+
)
723+
} else {
724+
showErrorNotification(msg.message)
725+
}
726+
},
727+
)
728+
702729
socket.socket.on(
703730
ConfigSpecificSocketEvents.onMotorTestResult,
704731
(msg) => {

gcs/src/redux/slices/configSlice.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const configSlice = createSlice({
44
name: "config",
55
initialState: {
66
getGripperEnabled: false,
7+
gripperConfig: {},
8+
refreshingGripperConfigData: false,
79
flightModes: [
810
"UNKNOWN",
911
"UNKNOWN",
@@ -46,6 +48,19 @@ const configSlice = createSlice({
4648
if (action.payload === state.getGripperEnabled) return
4749
state.getGripperEnabled = action.payload
4850
},
51+
setGripperConfig: (state, action) => {
52+
if (action.payload === state.gripperConfig) return
53+
state.gripperConfig = action.payload
54+
},
55+
updateGripperConfigParam: (state, action) => {
56+
const { param_id, value } = action.payload
57+
if (state.gripperConfig[param_id] === value) return
58+
state.gripperConfig[param_id] = value
59+
},
60+
setRefreshingGripperConfigData: (state, action) => {
61+
if (action.payload === state.refreshingGripperConfigData) return
62+
state.refreshingGripperConfigData = action.payload
63+
},
4964
setFlightModesList: (state, action) => {
5065
if (action.payload === state.flightModes) return
5166
state.flightModes = action.payload
@@ -97,6 +112,8 @@ const configSlice = createSlice({
97112

98113
// Emits
99114
emitGetGripperEnabled: () => {},
115+
emitGetGripperConfig: () => {},
116+
emitSetGripperConfigParam: () => {},
100117
emitGetFlightModeConfig: () => {},
101118
emitSetFlightMode: () => {},
102119
emitRefreshFlightModeData: () => {},
@@ -109,6 +126,9 @@ const configSlice = createSlice({
109126
},
110127
selectors: {
111128
selectGetGripperEnabled: (state) => state.getGripperEnabled,
129+
selectGripperConfig: (state) => state.gripperConfig,
130+
selectRefreshingGripperConfigData: (state) =>
131+
state.refreshingGripperConfigData,
112132
selectFlightModesList: (state) => state.flightModes,
113133
selectFlightModeChannel: (state) => state.flightModeChannel,
114134
selectRefreshingFlightModeData: (state) => state.refreshingFlightModeData,
@@ -126,6 +146,9 @@ const configSlice = createSlice({
126146

127147
export const {
128148
setGetGripperEnabled,
149+
setGripperConfig,
150+
updateGripperConfigParam,
151+
setRefreshingGripperConfigData,
129152
setFlightModesList,
130153
setFlightModeChannel,
131154
setRefreshingFlightModeData,
@@ -141,6 +164,8 @@ export const {
141164

142165
// Emitters
143166
emitGetGripperEnabled,
167+
emitGetGripperConfig,
168+
emitSetGripperConfigParam,
144169
emitGetFlightModeConfig,
145170
emitSetFlightMode,
146171
emitRefreshFlightModeData,
@@ -154,6 +179,8 @@ export const {
154179

155180
export const {
156181
selectGetGripperEnabled,
182+
selectGripperConfig,
183+
selectRefreshingGripperConfigData,
157184
selectFlightModesList,
158185
selectFlightModeChannel,
159186
selectRefreshingFlightModeData,

0 commit comments

Comments
 (0)