Skip to content

Commit 8948322

Browse files
authored
894 add a servo config page (#1018)
* set up servoOutput page UI * set up listeners on servo config * added servo controller for consistency * completed servo config * removing extra key in response * formatting * removing unused var * removing unused var * completed requested changes
1 parent e5e3cb5 commit 8948322

13 files changed

Lines changed: 830 additions & 54 deletions

File tree

gcs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"@mantine/tiptap": "^7.17.4",
3737
"@reduxjs/toolkit": "^2.2.7",
3838
"@robloche/chartjs-plugin-streaming": "^3.1.0",
39-
"@tabler/icons-react": "^3.36.1",
39+
"@tabler/icons-react": "^3.37.1",
4040
"@tailwindcss/container-queries": "^0.1.1",
4141
"@tiptap/extension-link": "^2.11.6",
4242
"@tiptap/pm": "^2.11.6",
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
// Servo Output Configuration Page
2+
import { useEffect, useState } from "react"
3+
import {
4+
Table,
5+
Button,
6+
NumberInput,
7+
Checkbox,
8+
Select,
9+
Modal,
10+
Text,
11+
Progress,
12+
} from "@mantine/core"
13+
import resolveConfig from "tailwindcss/resolveConfig"
14+
import tailwindConfig from "../../../tailwind.config"
15+
16+
// Custom components, helpers and data
17+
import apmParamDefsCopter from "../../../data/gen_apm_params_def_copter.json"
18+
import apmParamDefsPlane from "../../../data/gen_apm_params_def_plane.json"
19+
20+
import { useSelector, useDispatch } from "react-redux"
21+
import { selectAircraftType } from "../../redux/slices/droneInfoSlice"
22+
import {
23+
emitGetServoConfig,
24+
emitSetServoConfigParam,
25+
emitTestServoPwm,
26+
selectServoConfig,
27+
selectServoPwmOutputs,
28+
} from "../../redux/slices/configSlice"
29+
import { emitSetState } from "../../redux/slices/droneConnectionSlice"
30+
import { selectConnectedToDrone } from "../../redux/slices/droneConnectionSlice"
31+
const tailwindColors = resolveConfig(tailwindConfig).theme.colors
32+
33+
const PWM_MIN = 1000
34+
const PWM_MAX = 2000
35+
36+
const COLOURS = [
37+
tailwindColors.red[500],
38+
tailwindColors.orange[500],
39+
tailwindColors.yellow[500],
40+
tailwindColors.green[500],
41+
tailwindColors.blue[500],
42+
tailwindColors.indigo[500],
43+
tailwindColors.purple[500],
44+
tailwindColors.pink[500],
45+
]
46+
47+
function getPercentageValueFromPWM(pwmValue) {
48+
// Normalise the PWM value into a percentage value
49+
if (pwmValue == 0) return 0 // Handle case where PWM value is not available
50+
51+
return ((pwmValue - PWM_MIN) / (PWM_MAX - PWM_MIN)) * 100
52+
}
53+
54+
export default function ServoOutput() {
55+
const dispatch = useDispatch()
56+
const aircraftType = useSelector(selectAircraftType)
57+
const servoConfig = useSelector(selectServoConfig)
58+
const servoPwmOutputs = useSelector(selectServoPwmOutputs)
59+
const connected = useSelector(selectConnectedToDrone)
60+
61+
const [testModalOpen, setTestModalOpen] = useState(false)
62+
const [testServoIdx, setTestServoIdx] = useState(null)
63+
const [testPwm, setTestPwm] = useState(1500)
64+
65+
// Helper to get paramDef for a given param_id
66+
function getParamDef(param_id) {
67+
if (aircraftType === 1) return apmParamDefsPlane[param_id]
68+
if (aircraftType === 2) return apmParamDefsCopter[param_id]
69+
return undefined
70+
}
71+
72+
// Helper to handle param change
73+
function handleParamChange(param_id, value) {
74+
dispatch(
75+
emitSetServoConfigParam({
76+
param_id,
77+
value: parseInt(value),
78+
}),
79+
)
80+
}
81+
82+
function handleOpenTestModal(servoNum) {
83+
setTestServoIdx(servoNum)
84+
setTestPwm(1500)
85+
setTestModalOpen(true)
86+
}
87+
88+
function handleSendTestPwm() {
89+
if (testPwm < PWM_MIN || testPwm > PWM_MAX) return
90+
dispatch(
91+
emitTestServoPwm({
92+
servo_instance: testServoIdx,
93+
pwm_value: testPwm,
94+
}),
95+
)
96+
setTestModalOpen(false)
97+
}
98+
99+
// Build servo rows (1-16)
100+
const servoRows = Array.from({ length: 16 }, (_, i) => {
101+
const num = i + 1
102+
const config = servoConfig[num] || {}
103+
return {
104+
number: num,
105+
function: config.function,
106+
min: config.min,
107+
trim: config.trim,
108+
max: config.max,
109+
reversed: config.reversed === 1 || config.reversed === "1",
110+
pwm: servoPwmOutputs[num] || 0,
111+
}
112+
})
113+
114+
useEffect(() => {
115+
if (connected) {
116+
dispatch(emitSetState("config.servo"))
117+
dispatch(emitGetServoConfig())
118+
}
119+
}, [connected, dispatch])
120+
121+
return (
122+
<div className="p-4 overflow-auto">
123+
{/* Modal for sending test PWM */}
124+
<Modal
125+
opened={testModalOpen}
126+
onClose={() => setTestModalOpen(false)}
127+
title={`Test Servo Output #${testServoIdx}`}
128+
centered
129+
>
130+
<Text mb={8}>Enter PWM value to send:</Text>
131+
<NumberInput
132+
value={testPwm}
133+
onChange={setTestPwm}
134+
error={
135+
testPwm < PWM_MIN || testPwm > PWM_MAX
136+
? `Value must be between ${PWM_MIN} and ${PWM_MAX}`
137+
: null
138+
}
139+
/>
140+
<Button
141+
mt={16}
142+
color="blue"
143+
onClick={handleSendTestPwm}
144+
disabled={testPwm < PWM_MIN || testPwm > PWM_MAX}
145+
fullWidth
146+
>
147+
Send PWM
148+
</Button>
149+
</Modal>
150+
151+
<Table withRowBorders={false} className="!w-fit">
152+
<Table.Thead>
153+
<Table.Tr>
154+
<Table.Th>#</Table.Th>
155+
<Table.Th>Position</Table.Th>
156+
<Table.Th>Reversed</Table.Th>
157+
<Table.Th>Function</Table.Th>
158+
<Table.Th className="w-[4.375rem]">Min</Table.Th>
159+
<Table.Th className="w-[4.375rem]">Trim</Table.Th>
160+
<Table.Th className="w-[4.375rem]">Max</Table.Th>
161+
<Table.Th>Test</Table.Th>
162+
</Table.Tr>
163+
</Table.Thead>
164+
<Table.Tbody>
165+
{servoRows.map((servo, idx) => {
166+
const num = servo.number
167+
const fnParam = `SERVO${num}_FUNCTION`
168+
const minParam = `SERVO${num}_MIN`
169+
const trimParam = `SERVO${num}_TRIM`
170+
const maxParam = `SERVO${num}_MAX`
171+
const revParam = `SERVO${num}_REVERSED`
172+
173+
const fnDef = getParamDef(fnParam)
174+
const minDef = getParamDef(minParam)
175+
const trimDef = getParamDef(trimParam)
176+
const maxDef = getParamDef(maxParam)
177+
178+
const fnOptions = fnDef?.Values
179+
? Object.entries(fnDef.Values).map(([value, label]) => ({
180+
value,
181+
label: `${value}: ${label}`,
182+
}))
183+
: []
184+
185+
return (
186+
<Table.Tr key={num} className="h-12">
187+
<Table.Td>{num}</Table.Td>
188+
<Table.Td>
189+
<Progress.Root className="!h-6 !w-96">
190+
<Progress.Section
191+
value={getPercentageValueFromPWM(servo.pwm)}
192+
color={COLOURS[idx % COLOURS.length]}
193+
style={servo.pwm ? { minWidth: "50px" } : {}}
194+
>
195+
<Progress.Label className="!text-lg !font-normal">
196+
{servo.pwm}
197+
</Progress.Label>
198+
</Progress.Section>
199+
</Progress.Root>
200+
</Table.Td>
201+
<Table.Td>
202+
<Checkbox
203+
checked={servo.reversed}
204+
size="sm"
205+
onChange={(event) =>
206+
handleParamChange(
207+
revParam,
208+
event.currentTarget.checked ? 1 : 0,
209+
)
210+
}
211+
/>
212+
</Table.Td>
213+
<Table.Td>
214+
<Select
215+
data={fnOptions}
216+
value={servo.function?.toString() || ""}
217+
placeholder="Select function"
218+
size="xs"
219+
className="min-w-[120px]"
220+
onChange={(val) => handleParamChange(fnParam, val)}
221+
/>
222+
</Table.Td>
223+
<Table.Td>
224+
<NumberInput
225+
value={servo.min || ""}
226+
min={
227+
minDef?.Range?.low ? Number(minDef.Range.low) : PWM_MIN
228+
}
229+
max={
230+
minDef?.Range?.high ? Number(minDef.Range.high) : PWM_MAX
231+
}
232+
size="xs"
233+
className="w-[3.75rem]"
234+
onChange={(val) => handleParamChange(minParam, val)}
235+
/>
236+
</Table.Td>
237+
<Table.Td>
238+
<NumberInput
239+
value={servo.trim || ""}
240+
min={
241+
trimDef?.Range?.low ? Number(trimDef.Range.low) : PWM_MIN
242+
}
243+
max={
244+
trimDef?.Range?.high
245+
? Number(trimDef.Range.high)
246+
: PWM_MAX
247+
}
248+
size="xs"
249+
className="w-[3.75rem]"
250+
onChange={(val) => handleParamChange(trimParam, val)}
251+
/>
252+
</Table.Td>
253+
<Table.Td>
254+
<NumberInput
255+
value={servo.max || ""}
256+
min={
257+
maxDef?.Range?.low ? Number(maxDef.Range.low) : PWM_MIN
258+
}
259+
max={
260+
maxDef?.Range?.high ? Number(maxDef.Range.high) : PWM_MAX
261+
}
262+
size="xs"
263+
className="w-[3.75rem]"
264+
onChange={(val) => handleParamChange(maxParam, val)}
265+
/>
266+
</Table.Td>
267+
<Table.Td>
268+
<Button
269+
size="xs"
270+
color="blue"
271+
onClick={() => handleOpenTestModal(num)}
272+
>
273+
Test
274+
</Button>
275+
</Table.Td>
276+
</Table.Tr>
277+
)
278+
})}
279+
</Table.Tbody>
280+
</Table>
281+
</div>
282+
)
283+
}

gcs/src/config.jsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { useDispatch, useSelector } from "react-redux"
2323
import Ftp from "./components/config/ftp"
2424
import { selectActiveTab, setActiveTab } from "./redux/slices/configSlice"
2525
import { selectConnectedToDrone } from "./redux/slices/droneConnectionSlice"
26+
import ServoOutput from "./components/config/servoOutput"
2627

2728
export default function Config() {
2829
const dispatch = useDispatch()
@@ -53,6 +54,7 @@ export default function Config() {
5354
<Tabs.Tab value="motor_test">Motor Test</Tabs.Tab>
5455
<Tabs.Tab value="rc_calibration">RC Calibration</Tabs.Tab>
5556
<Tabs.Tab value="flightmodes">Flight modes</Tabs.Tab>
57+
<Tabs.Tab value="servo">Servo Output</Tabs.Tab>
5658
<Tabs.Tab value="ftp">FTP</Tabs.Tab>
5759
</Tabs.List>
5860
<Tabs.Panel value="gripper">
@@ -75,6 +77,11 @@ export default function Config() {
7577
<FlightModes />
7678
</div>
7779
</Tabs.Panel>
80+
<Tabs.Panel value="servo">
81+
<div className={paddingTop}>
82+
<ServoOutput />
83+
</div>
84+
</Tabs.Panel>
7885
<Tabs.Panel value="ftp">
7986
<div className={paddingTop}>
8087
<Ftp />

gcs/src/redux/middleware/emitters.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { showErrorNotification } from "../../helpers/notification"
22
import {
33
emitBatchSetRcConfigParams,
4+
emitBatchSetServoConfigParams,
45
emitGetFlightModeConfig,
56
emitGetFrameConfig,
67
emitGetGripperConfig,
78
emitGetGripperEnabled,
89
emitGetRcConfig,
10+
emitGetServoConfig,
911
emitRefreshFlightModeData,
1012
emitSetFlightMode,
1113
emitSetFlightModeChannel,
@@ -14,9 +16,11 @@ import {
1416
emitSetGripperDisabled,
1517
emitSetGripperEnabled,
1618
emitSetRcConfigParam,
19+
emitSetServoConfigParam,
1720
emitTestAllMotors,
1821
emitTestMotorSequence,
1922
emitTestOneMotor,
23+
emitTestServoPwm,
2024
setRefreshingGripperConfigData,
2125
} from "../slices/configSlice"
2226
import {
@@ -434,6 +438,36 @@ export function handleEmitters(socket, store, action) {
434438
})
435439
},
436440
},
441+
{
442+
emitter: emitGetServoConfig,
443+
callback: () => socket.socket.emit("get_servo_config"),
444+
},
445+
{
446+
emitter: emitSetServoConfigParam,
447+
callback: () => {
448+
socket.socket.emit("set_servo_config_param", {
449+
param_id: action.payload.param_id,
450+
value: action.payload.value,
451+
})
452+
},
453+
},
454+
{
455+
emitter: emitBatchSetServoConfigParams,
456+
callback: () => {
457+
socket.socket.emit("batch_set_servo_config_params", {
458+
params: action.payload.params,
459+
})
460+
},
461+
},
462+
{
463+
emitter: emitTestServoPwm,
464+
callback: () => {
465+
socket.socket.emit("test_servo_pwm", {
466+
servo_instance: action.payload.servo_instance,
467+
pwm_value: action.payload.pwm_value,
468+
})
469+
},
470+
},
437471
{
438472
emitter: emitListFiles,
439473
callback: () => {

0 commit comments

Comments
 (0)