diff --git a/gcs/src/components/config/radioCalibration.jsx b/gcs/src/components/config/radioCalibration.jsx
index 2800b5e2f..06dbf3232 100644
--- a/gcs/src/components/config/radioCalibration.jsx
+++ b/gcs/src/components/config/radioCalibration.jsx
@@ -5,14 +5,10 @@
*/
// Base imports
-import { useEffect } from "react"
-
-// Styling imports
-import resolveConfig from "tailwindcss/resolveConfig"
-import tailwindConfig from "../../../tailwind.config"
+import { useEffect, useMemo } from "react"
// Helper javascript files
-import { Progress } from "@mantine/core"
+import { Checkbox, Progress, Select, Table } from "@mantine/core"
import apmParamDefsCopter from "../../../data/gen_apm_params_def_copter.json"
import apmParamDefsPlane from "../../../data/gen_apm_params_def_plane.json"
@@ -20,20 +16,25 @@ import apmParamDefsPlane from "../../../data/gen_apm_params_def_plane.json"
import { useDispatch, useSelector } from "react-redux"
import {
emitGetRcConfig,
- selectRadioChannels,
+ emitSetRcConfigParam,
selectRadioChannelsConfig,
+ selectRadioPwmChannels,
} from "../../redux/slices/configSlice"
import {
emitSetState,
selectConnectedToDrone,
} from "../../redux/slices/droneConnectionSlice"
-import { selectAircraftType } from "../../redux/slices/droneInfoSlice"
+import { selectAircraftTypeString } from "../../redux/slices/droneInfoSlice"
-// Tailwind
+// Styling imports
+import resolveConfig from "tailwindcss/resolveConfig"
+import tailwindConfig from "../../../tailwind.config"
const tailwindColors = resolveConfig(tailwindConfig).theme.colors
+
const PWM_MIN = 800
const PWM_MAX = 2200
-const colors = [
+
+const COLOURS = [
tailwindColors.red[500],
tailwindColors.orange[500],
tailwindColors.yellow[500],
@@ -52,17 +53,50 @@ function getPercentageValueFromPWM(pwmValue) {
export default function RadioCalibration() {
const dispatch = useDispatch()
const connected = useSelector(selectConnectedToDrone)
- const aircraftType = useSelector(selectAircraftType)
- const channels = useSelector(selectRadioChannels)
+ const aircraftTypeString = useSelector(selectAircraftTypeString)
+ const pwmChannels = useSelector(selectRadioPwmChannels)
const channelsConfig = useSelector(selectRadioChannelsConfig)
- function getReadableRcOption(option) {
- if (option === 0) return null
- if (aircraftType === 1) {
- return apmParamDefsPlane.RC5_OPTION.Values[`${option}`] ?? option
- } else if (aircraftType === 2) {
- return apmParamDefsCopter.RC5_OPTION.Values[`${option}`] ?? option
- }
+ const paramDefs = useMemo(() => {
+ return aircraftTypeString === "Copter"
+ ? apmParamDefsCopter
+ : apmParamDefsPlane
+ }, [aircraftTypeString])
+
+ const rcSelectOptions = useMemo(() => {
+ const rcOptions = paramDefs.RC1_OPTION?.Values
+ if (!rcOptions) return []
+
+ return Object.entries(rcOptions)
+ .sort((a, b) => {
+ // Always put "0" first
+ if (a[0] === "0") return -1
+ if (b[0] === "0") return 1
+ // Then sort by label alphabetically
+ return a[1].localeCompare(b[1])
+ })
+ .map(([value, label]) => ({
+ value: value,
+ label: label,
+ }))
+ }, [paramDefs])
+
+ function handleRcOptionChange(channel, newOption) {
+ dispatch(
+ emitSetRcConfigParam({
+ param_id: `RC${channel}_OPTION`,
+ value: parseInt(newOption),
+ }),
+ )
+ }
+
+ function handleReversedChange(channel, isReversed) {
+ dispatch(
+ emitSetRcConfigParam({
+ param_id: `RC${channel}_REVERSED`,
+ value: isReversed ? 1 : 0,
+ }),
+ )
}
useEffect(() => {
@@ -73,32 +107,60 @@ export default function RadioCalibration() {
}, [connected])
return (
-
-
-
- {Object.keys(channels).map((channel) => (
-
- |
- {channel}
- {channelsConfig[channel]?.map ??
- getReadableRcOption(channelsConfig[channel]?.option)}
- |
-
-
+
+
+
+
+ Channel
+ Function/Option
+ Reversed
+ PWM Value
+
+
+
+ {Object.keys(pwmChannels).map((channel) => (
+
+
+ RC{channel}
+
+
+ {channelsConfig[channel]?.map ? (
+ {channelsConfig[channel].map}
+ ) : (
+
+
+
+ handleReversedChange(channel, event.currentTarget.checked)
+ }
+ />
+
+
+
- {channels[channel]}
+ {pwmChannels[channel]}
-
-
+
+
))}
-
-
+
+ |
)
}
diff --git a/gcs/src/redux/middleware/emitters.js b/gcs/src/redux/middleware/emitters.js
index aa4081378..db55b1b6e 100644
--- a/gcs/src/redux/middleware/emitters.js
+++ b/gcs/src/redux/middleware/emitters.js
@@ -8,6 +8,7 @@ import {
emitSetFlightMode,
emitSetGripper,
emitSetGripperConfigParam,
+ emitSetRcConfigParam,
emitTestAllMotors,
emitTestMotorSequence,
emitTestOneMotor,
@@ -297,6 +298,15 @@ export function handleEmitters(socket, store, action) {
emitter: emitGetRcConfig,
callback: () => socket.socket.emit("get_rc_config"),
},
+ {
+ emitter: emitSetRcConfigParam,
+ callback: () => {
+ socket.socket.emit("set_rc_config_param", {
+ param_id: action.payload.param_id,
+ value: action.payload.value,
+ })
+ },
+ },
]
for (const { emitter, callback } of emitHandlers) {
diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js
index 42afca480..f9aa83210 100644
--- a/gcs/src/redux/middleware/socketMiddleware.js
+++ b/gcs/src/redux/middleware/socketMiddleware.js
@@ -42,10 +42,11 @@ import {
setGetGripperEnabled,
setGripperConfig,
setNumberOfMotors,
- setRadioChannels,
+ setRadioPwmChannels,
setRefreshingFlightModeData,
setRefreshingGripperConfigData,
setShowMotorTestWarningModal,
+ updateChannelsConfigParam,
updateGripperConfigParam,
} from "../slices/configSlice.js"
import {
@@ -150,6 +151,7 @@ const ConfigSpecificSocketEvents = Object.freeze({
onSetFlightModeResult: "set_flight_mode_result",
onFrameTypeConfig: "frame_type_config",
onRcConfig: "rc_config",
+ onSetRcConfigResult: "set_rc_config_result",
})
const socketMiddleware = (store) => {
@@ -162,7 +164,7 @@ const socketMiddleware = (store) => {
chans[i] = msg[`chan${i}_raw`]
}
- store.dispatch(setRadioChannels(chans))
+ store.dispatch(setRadioPwmChannels(chans))
}
const incomingMessageHandler = (msg) => {
@@ -809,6 +811,23 @@ const socketMiddleware = (store) => {
store.dispatch(setChannelsConfig(config))
})
+ socket.socket.on(
+ ConfigSpecificSocketEvents.onSetRcConfigResult,
+ (msg) => {
+ if (msg.success) {
+ showSuccessNotification(msg.message)
+ store.dispatch(
+ updateChannelsConfigParam({
+ param_id: msg.param_id,
+ value: msg.value,
+ }),
+ )
+ } else {
+ showErrorNotification(msg.message)
+ }
+ },
+ )
+
/*
Generic Drone Data
*/
diff --git a/gcs/src/redux/slices/configSlice.js b/gcs/src/redux/slices/configSlice.js
index d6045cded..f8f0efdbd 100644
--- a/gcs/src/redux/slices/configSlice.js
+++ b/gcs/src/redux/slices/configSlice.js
@@ -23,7 +23,7 @@ const configSlice = createSlice({
frameClass: null,
numberOfMotors: 4,
showMotorTestWarningModal: true,
- radioChannels: {
+ radioPwmChannels: {
1: 0,
2: 0,
3: 0,
@@ -101,14 +101,38 @@ const configSlice = createSlice({
if (action.payload === state.showMotorTestWarningModal) return
state.showMotorTestWarningModal = action.payload
},
- setRadioChannels: (state, action) => {
- if (action.payload === state.radioChannels) return
- state.radioChannels = action.payload
+ setRadioPwmChannels: (state, action) => {
+ if (action.payload === state.radioPwmChannels) return
+ state.radioPwmChannels = action.payload
},
setChannelsConfig: (state, action) => {
if (action.payload === state.radioChannelsConfig) return
state.radioChannelsConfig = action.payload
},
+ updateChannelsConfigParam: (state, action) => {
+ const { param_id, value } = action.payload
+ // param_id is like "RC1_OPTION", "RC2_REVERSED", etc. so we need to separate out the channel number
+ const match = param_id.match(/^RC(\d+)_/)[1]
+ if (!match) return
+
+ const channelNum = match[1]
+
+ if (!state.radioChannelsConfig[channelNum]) return
+
+ // Get if its an option or reversed parameter
+ const isOption = param_id.endsWith("_OPTION")
+ const isReversed = param_id.endsWith("_REVERSED")
+ if (!isOption && !isReversed) return
+
+ if (isOption) {
+ // For option, value should be an integer
+ if (state.radioChannelsConfig[channelNum].option === value) return
+ state.radioChannelsConfig[channelNum].option = value
+ } else if (isReversed) {
+ if (state.radioChannelsConfig[channelNum].reversed === value) return
+ state.radioChannelsConfig[channelNum].reversed = value
+ }
+ },
// Emits
emitGetGripperEnabled: () => {},
@@ -123,6 +147,7 @@ const configSlice = createSlice({
emitTestMotorSequence: () => {},
emitTestAllMotors: () => {},
emitGetRcConfig: () => {},
+ emitSetRcConfigParam: () => {},
},
selectors: {
selectGetGripperEnabled: (state) => state.getGripperEnabled,
@@ -139,7 +164,7 @@ const configSlice = createSlice({
selectFrameClass: (state) => state.frameClass,
selectNumberOfMotors: (state) => state.numberOfMotors,
selectShowMotorTestWarningModal: (state) => state.showMotorTestWarningModal,
- selectRadioChannels: (state) => state.radioChannels,
+ selectRadioPwmChannels: (state) => state.radioPwmChannels,
selectRadioChannelsConfig: (state) => state.radioChannelsConfig,
},
})
@@ -159,8 +184,9 @@ export const {
setFrameClass,
setNumberOfMotors,
setShowMotorTestWarningModal,
- setRadioChannels,
+ setRadioPwmChannels,
setChannelsConfig,
+ updateChannelsConfigParam,
// Emitters
emitGetGripperEnabled,
@@ -175,6 +201,7 @@ export const {
emitTestMotorSequence,
emitTestAllMotors,
emitGetRcConfig,
+ emitSetRcConfigParam,
} = configSlice.actions
export const {
@@ -191,7 +218,7 @@ export const {
selectFrameClass,
selectNumberOfMotors,
selectShowMotorTestWarningModal,
- selectRadioChannels,
+ selectRadioPwmChannels,
selectRadioChannelsConfig,
} = configSlice.selectors
diff --git a/radio/app/controllers/gripperController.py b/radio/app/controllers/gripperController.py
index d65f09219..a6d11520b 100644
--- a/radio/app/controllers/gripperController.py
+++ b/radio/app/controllers/gripperController.py
@@ -3,7 +3,7 @@
from typing import TYPE_CHECKING
import serial
-from app.customTypes import Response
+from app.customTypes import Number, Response
from app.utils import commandAccepted
from pymavlink import mavutil
@@ -158,7 +158,7 @@ def getConfig(self) -> dict:
return config
- def setGripperParam(self, param_id: str, value: float) -> bool:
+ def setGripperParam(self, param_id: str, value: Number) -> bool:
"""
Sets a gripper related parameter on the drone.
"""
diff --git a/radio/app/controllers/rcController.py b/radio/app/controllers/rcController.py
index 221610479..9ad889f0c 100644
--- a/radio/app/controllers/rcController.py
+++ b/radio/app/controllers/rcController.py
@@ -2,6 +2,8 @@
from typing import TYPE_CHECKING
+from app.customTypes import Number
+
if TYPE_CHECKING:
from app.drone import Drone
@@ -16,6 +18,7 @@ def __init__(self, drone: Drone) -> None:
"""
self.drone = drone
self.params: dict = {}
+ self.param_types: dict = {}
self.fetchParams()
@@ -33,6 +36,7 @@ def _getAndSetParam(
param = self.drone.paramsController.getSingleParam(param_name).get("data")
if param:
params_dict[param_key] = param.param_value
+ self.param_types[param_name] = param.param_type
def _getAndSetCachedParam(
self, params_dict: dict, param_key: str, param_name: str
@@ -57,6 +61,7 @@ def _getAndSetCachedParam(
)
if fetched_param:
params_dict[param_key] = fetched_param.param_value
+ self.param_types[param_name] = fetched_param.param_type
def fetchParams(self) -> None:
"""
@@ -107,3 +112,11 @@ def getConfig(self) -> dict:
self.params[f"RC_{channel_number}"] = channel_params
return self.params
+
+ def setConfigParam(self, param_id: str, value: Number) -> bool:
+ """
+ Sets a RC configuration related parameter on the drone.
+ """
+ param_type = self.param_types.get(param_id)
+
+ return self.drone.paramsController.setParam(param_id, value, param_type)
diff --git a/radio/app/customTypes.py b/radio/app/customTypes.py
index b05b08789..cfc851665 100644
--- a/radio/app/customTypes.py
+++ b/radio/app/customTypes.py
@@ -41,6 +41,11 @@ class SetFlightModeValueAndNumber(TypedDict):
flight_mode: int
+class SetConfigParam(TypedDict):
+ param_id: str
+ value: Number
+
+
class VehicleType(Enum):
UNKNOWN = 0
FIXED_WING = 1
diff --git a/radio/app/endpoints/gripper.py b/radio/app/endpoints/gripper.py
index 542fccf31..464ad4070 100644
--- a/radio/app/endpoints/gripper.py
+++ b/radio/app/endpoints/gripper.py
@@ -1,5 +1,6 @@
import app.droneStatus as droneStatus
from app import logger, socketio
+from app.customTypes import SetConfigParam
from app.utils import droneErrorCb
@@ -83,7 +84,7 @@ def getGripperConfig() -> None:
@socketio.on("set_gripper_config_param")
-def setGripperParam(data: dict) -> None:
+def setGripperParam(data: SetConfigParam) -> None:
"""
Sets a gripper parameter based off data passed in, only works when the config page is loaded.
"""
diff --git a/radio/app/endpoints/rc.py b/radio/app/endpoints/rc.py
index 9d7956f3f..15603a238 100644
--- a/radio/app/endpoints/rc.py
+++ b/radio/app/endpoints/rc.py
@@ -1,5 +1,6 @@
import app.droneStatus as droneStatus
from app import logger, socketio
+from app.customTypes import SetConfigParam
from app.utils import notConnectedError
@@ -28,3 +29,47 @@ def getRcConfig() -> None:
"rc_config",
droneStatus.drone.rcController.params,
)
+
+
+@socketio.on("set_rc_config_param")
+def setRcConfigParam(data: SetConfigParam) -> None:
+ """
+ Sets a RC config parameter on the drone.
+ """
+ if droneStatus.state != "config.rc":
+ socketio.emit(
+ "params_error",
+ {
+ "message": "You must be on the config screen to set RC config parameters."
+ },
+ )
+ logger.debug(f"Current state: {droneStatus.state}")
+ return
+
+ if not droneStatus.drone:
+ return notConnectedError(action="set a RC config parameter")
+
+ param_id = data.get("param_id", None)
+ value = data.get("value", None)
+
+ if param_id is None or value is None:
+ socketio.emit(
+ "params_error",
+ {"message": "Param ID and value must be specified."},
+ )
+ return
+
+ success = droneStatus.drone.rcController.setConfigParam(param_id, value)
+ if success:
+ result = {
+ "success": True,
+ "message": f"Parameter {param_id} successfully set to {value}.",
+ "param_id": param_id,
+ "value": value,
+ }
+ else:
+ result = {
+ "success": False,
+ "message": f"Failed to set parameter {param_id} to {value}.",
+ }
+ socketio.emit("set_rc_config_result", result)
diff --git a/radio/app/endpoints/states.py b/radio/app/endpoints/states.py
index 0eccecb95..4aec449cd 100644
--- a/radio/app/endpoints/states.py
+++ b/radio/app/endpoints/states.py
@@ -50,6 +50,7 @@ def set_state(data: SetStateType) -> None:
"missions": ["GLOBAL_POSITION_INT", "NAV_CONTROLLER_OUTPUT", "HEARTBEAT"],
"graphs": ["VFR_HUD", "ATTITUDE", "SYS_STATUS"],
"config.flight_modes": ["RC_CHANNELS", "HEARTBEAT"],
+ "config.rc": ["RC_CHANNELS", "HEARTBEAT"],
}
droneStatus.drone.logger.info(f"Changing state to {droneStatus.state}")
@@ -98,3 +99,6 @@ def set_state(data: SetStateType) -> None:
droneStatus.drone.sendDataStreamRequestMessage(
mavutil.mavlink.MAV_DATA_STREAM_RC_CHANNELS, 4
)
+
+ for message in message_listeners["config.rc"]:
+ droneStatus.drone.addMessageListener(message, sendMessage)
diff --git a/radio/tests/test_states.py b/radio/tests/test_states.py
index 81c9c6e9e..8e407b40d 100644
--- a/radio/tests/test_states.py
+++ b/radio/tests/test_states.py
@@ -46,7 +46,7 @@ def test_setState(socketio_client: SocketIOTestClient, droneStatus) -> None:
socketio_client.emit("set_state", {"state": "config.rc"})
assert len(socketio_client.get_received()) == 0
- assert len(droneStatus.drone.message_listeners) == 0
+ assert len(droneStatus.drone.message_listeners) == 2
droneStatus.drone.message_listeners = {}