From 42e3f3fab603b3e811768fcd515a4ccecb4cb8a3 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Mon, 8 Sep 2025 21:03:51 +0100 Subject: [PATCH 1/6] Add input to set loiter radius --- gcs/src/components/dashboard/tabsSection.jsx | 9 +-- .../tabsSectionTabs/actionTabsSection.jsx | 58 +++++++++++++++++-- gcs/src/components/layout.jsx | 2 + gcs/src/redux/middleware/emitters.js | 5 ++ gcs/src/redux/middleware/socketMiddleware.js | 39 ++++++++++--- gcs/src/redux/slices/droneConnectionSlice.js | 2 + gcs/src/redux/slices/droneInfoSlice.js | 7 +++ radio/app/controllers/missionController.py | 4 +- radio/app/controllers/navController.py | 53 ++++++++++++++++- radio/app/controllers/paramsController.py | 2 + radio/app/endpoints/nav.py | 58 +++++++++++++++++++ 11 files changed, 221 insertions(+), 18 deletions(-) diff --git a/gcs/src/components/dashboard/tabsSection.jsx b/gcs/src/components/dashboard/tabsSection.jsx index 16ef647d9..aa9839380 100644 --- a/gcs/src/components/dashboard/tabsSection.jsx +++ b/gcs/src/components/dashboard/tabsSection.jsx @@ -1,4 +1,4 @@ -/* +/* Tabs section. This will be a part of the resizable info box located in the bottom half. This contains tabs like data, action, missions, and camera. */ @@ -7,19 +7,19 @@ import { Tabs } from "@mantine/core" // Tab Components -import CameraTabsSection from "./tabsSectionTabs/cameraTabsSection" import ActionTabsSection from "./tabsSectionTabs/actionTabsSection" -import MissionTabsSection from "./tabsSectionTabs/missionTabsSection" +import CameraTabsSection from "./tabsSectionTabs/cameraTabsSection" import DataTabsSection from "./tabsSectionTabs/dataTabsSection" +import MissionTabsSection from "./tabsSectionTabs/missionTabsSection" import PreFlightChecklistTab from "./tabsSectionTabs/preFlightChecklistSection" // Redux import { useSelector } from "react-redux" +import { selectConnectedToDrone } from "../../redux/slices/droneConnectionSlice" import { selectAircraftType, selectNavController, } from "../../redux/slices/droneInfoSlice" -import { selectConnectedToDrone } from "../../redux/slices/droneConnectionSlice" export default function TabsSection({ currentFlightModeNumber }) { const connected = useSelector(selectConnectedToDrone) @@ -46,6 +46,7 @@ export default function TabsSection({ currentFlightModeNumber }) { tabPadding={tabPadding} currentFlightModeNumber={currentFlightModeNumber} aircraftType={aircraftType} + currentLoiterRadius={navControllerOutputData.loiterRadius} > {/* Mission */} diff --git a/gcs/src/components/dashboard/tabsSectionTabs/actionTabsSection.jsx b/gcs/src/components/dashboard/tabsSectionTabs/actionTabsSection.jsx index 6382b7f99..9a6ff0268 100644 --- a/gcs/src/components/dashboard/tabsSectionTabs/actionTabsSection.jsx +++ b/gcs/src/components/dashboard/tabsSectionTabs/actionTabsSection.jsx @@ -4,10 +4,10 @@ */ // Native -import { useState } from "react" +import { useEffect, useState } from "react" // Mantine -import { Button, NumberInput, Popover, Tabs, Select } from "@mantine/core" +import { Button, NumberInput, Popover, Select, Tabs } from "@mantine/core" import { useLocalStorage } from "@mantine/hooks" // Mavlink @@ -17,16 +17,20 @@ import { } from "../../../helpers/mavlinkConstants" // Helper +import { useDispatch, useSelector } from "react-redux" import { socket } from "../../../helpers/socket" +import { + selectArmed, + setLoiterRadius, +} from "../../../redux/slices/droneInfoSlice" import { NoConnectionMsg } from "../tabsSection" -import { useSelector } from "react-redux" -import { selectArmed } from "../../../redux/slices/droneInfoSlice" export default function ActionTabsSection({ connected, tabPadding, currentFlightModeNumber, aircraftType, + currentLoiterRadius, }) { return ( @@ -35,6 +39,8 @@ export default function ActionTabsSection({ ) : (
+ {/** Loiter Radius */} + {/** Flight Mode */} { ) } + +const LoiterRadiusAction = ({ currentLoiterRadius }) => { + const dispatch = useDispatch() + const [newLoiterRadius, setNewLoiterRadius] = useState(currentLoiterRadius) // Default to AUTO mode + + useEffect(() => { + setNewLoiterRadius(currentLoiterRadius) + }, [currentLoiterRadius]) + + function sendNewLoiterRadius(radius) { + if (radius === null || radius === currentLoiterRadius || radius < 0) { + return + } + socket.emit("set_loiter_radius", { radius }) + dispatch(setLoiterRadius(radius)) + } + + return ( + <> + {currentLoiterRadius !== null && ( +
+ + + +
+ )} + + ) +} diff --git a/gcs/src/components/layout.jsx b/gcs/src/components/layout.jsx index b13cd2fd3..6a53c0f7e 100644 --- a/gcs/src/components/layout.jsx +++ b/gcs/src/components/layout.jsx @@ -20,6 +20,7 @@ import { socket } from "../helpers/socket" import { useDispatch, useSelector } from "react-redux" import { emitGetCurrentMissionAll, + emitGetLoiterRadius, emitSetState, selectConnectedToDrone, } from "../redux/slices/droneConnectionSlice" @@ -70,6 +71,7 @@ export default function Layout({ children, currentPage }) { dispatch(emitSetState({ state: currentPage })) if (currentPage.toLowerCase() == "dashboard") { dispatch(emitGetCurrentMissionAll()) + dispatch(emitGetLoiterRadius()) } }, [currentPage, connectedToDrone]) diff --git a/gcs/src/redux/middleware/emitters.js b/gcs/src/redux/middleware/emitters.js index 650115087..acea03ebd 100644 --- a/gcs/src/redux/middleware/emitters.js +++ b/gcs/src/redux/middleware/emitters.js @@ -4,6 +4,7 @@ import { emitGetComPorts, emitGetCurrentMissionAll, emitGetHomePosition, + emitGetLoiterRadius, emitIsConnectedToDrone, emitSetState, } from "../slices/droneConnectionSlice" @@ -51,6 +52,10 @@ export function handleEmitters(socket, store, action) { emitter: emitGetCurrentMissionAll, callback: () => socket.socket.emit("get_current_mission_all"), }, + { + emitter: emitGetLoiterRadius, + callback: () => socket.socket.emit("get_loiter_radius"), + }, /* ============ diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index bb9badda2..7700169b3 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -11,6 +11,7 @@ import { import { emitGetComPorts, emitGetHomePosition, + emitGetLoiterRadius, emitIsConnectedToDrone, emitSetState, setComPorts, @@ -23,6 +24,7 @@ import { } from "../slices/droneConnectionSlice" // socket factory +import { dataFormatters } from "../../helpers/dataFormatters.js" import SocketFactory from "../../helpers/socket" import { setAttitudeData, @@ -33,6 +35,7 @@ import { setGpsRawIntData, setHeartbeatData, setLastGraphMessage, + setLoiterRadius, setNavControllerOutput, setOnboardControlSensorsEnabled, setRSSIData, @@ -58,7 +61,6 @@ import { } from "../slices/notificationSlice" import { pushMessage } from "../slices/statusTextSlice.js" import { handleEmitters } from "./emitters.js" -import { dataFormatters } from "../../helpers/dataFormatters.js" const SocketEvents = Object.freeze({ // socket.on events @@ -78,6 +80,8 @@ const DroneSpecificSocketEvents = Object.freeze({ onNavResult: "nav_result", onHomePositionResult: "home_position_result", onIncomingMsg: "incoming_msg", + onGetLoiterRadiusResult: "nav_get_loiter_radius_result", + onSetLoiterRadiusResult: "nav_set_loiter_radius_result", }) const MissionSpecificSocketEvents = Object.freeze({ @@ -237,6 +241,7 @@ const socketMiddleware = (store) => { store.dispatch(emitSetState({ state: "dashboard" })) store.dispatch(emitGetHomePosition()) + store.dispatch(emitGetLoiterRadius()) }) // Link stats @@ -288,15 +293,35 @@ const socketMiddleware = (store) => { }, ) - /* - Missions - */ socket.socket.on( - MissionSpecificSocketEvents.onCurrentMissionAll, + DroneSpecificSocketEvents.onGetLoiterRadiusResult, (msg) => { - store.dispatch(setCurrentMissionItems(msg)) + store.dispatch( + msg.success + ? setLoiterRadius(msg.data) + : queueNotification({ type: "error", message: msg.message }), + ) }, - ) + ), + socket.socket.on( + DroneSpecificSocketEvents.onSetLoiterRadiusResult, + (msg) => { + store.dispatch( + msg.success + ? queueNotification({ type: "success", message: msg.message }) + : queueNotification({ type: "error", message: msg.message }), + ) + }, + ), + /* + Missions + */ + socket.socket.on( + MissionSpecificSocketEvents.onCurrentMissionAll, + (msg) => { + store.dispatch(setCurrentMissionItems(msg)) + }, + ) socket.socket.on( MissionSpecificSocketEvents.onCurrentMission, diff --git a/gcs/src/redux/slices/droneConnectionSlice.js b/gcs/src/redux/slices/droneConnectionSlice.js index 1686110eb..dc0aa66be 100644 --- a/gcs/src/redux/slices/droneConnectionSlice.js +++ b/gcs/src/redux/slices/droneConnectionSlice.js @@ -112,6 +112,7 @@ const droneConnectionSlice = createSlice({ emitSetState: () => {}, emitGetHomePosition: () => {}, emitGetCurrentMissionAll: () => {}, + emitGetLoiterRadius: () => {}, }, selectors: { selectConnecting: (state) => state.connecting, @@ -154,6 +155,7 @@ export const { emitSetState, emitGetHomePosition, emitGetCurrentMissionAll, + emitGetLoiterRadius, } = droneConnectionSlice.actions export const { selectConnecting, diff --git a/gcs/src/redux/slices/droneInfoSlice.js b/gcs/src/redux/slices/droneInfoSlice.js index ea7fe3150..2413675f7 100644 --- a/gcs/src/redux/slices/droneInfoSlice.js +++ b/gcs/src/redux/slices/droneInfoSlice.js @@ -28,6 +28,7 @@ const droneInfoSlice = createSlice({ navControllerData: { navBearing: 0.0, wpDist: 0.0, + loiterRadius: 80.0, // Default loiter radius }, heartbeatData: { baseMode: 0, @@ -150,6 +151,11 @@ const droneInfoSlice = createSlice({ state.graphs.lastGraphResultsMessage = action.payload } }, + setLoiterRadius: (state, action) => { + if (action.payload !== state.navControllerData.loiterRadius) { + state.navControllerData.loiterRadius = action.payload + } + }, }, selectors: { selectAttitude: (state) => state.attitudeData, @@ -199,6 +205,7 @@ export const { setRSSIData, setGraphValues, setLastGraphMessage, + setLoiterRadius, } = droneInfoSlice.actions // Memoized selectors because redux is a bitch diff --git a/radio/app/controllers/missionController.py b/radio/app/controllers/missionController.py index a33ddd1cf..9c3b8d69b 100644 --- a/radio/app/controllers/missionController.py +++ b/radio/app/controllers/missionController.py @@ -215,6 +215,8 @@ def getMissionItems( blocking=True, timeout=2, ) + self.drone.is_listening = True + if response: if response.mission_type != mission_type: self.drone.logger.error( @@ -234,7 +236,6 @@ def getMissionItems( f"Received count of {response.count} waypoints", 0.0 ) - self.drone.is_listening = True for i in range(0, response.count): retry_count = 0 while retry_count < 3: @@ -274,7 +275,6 @@ def getMissionItems( "data": loader.wpoints, } else: - self.drone.is_listening = True self.drone.logger.error( f"No response received for mission count for mission type {mission_type}." ) diff --git a/radio/app/controllers/navController.py b/radio/app/controllers/navController.py index 9cd3d2eeb..819d2629e 100644 --- a/radio/app/controllers/navController.py +++ b/radio/app/controllers/navController.py @@ -21,6 +21,9 @@ def __init__(self, drone: Drone) -> None: """ self.drone = drone + self.loiter_radius_param_type = mavutil.mavlink.MAV_PARAM_TYPE_INT16 + self.loiter_radius = 80.0 # Default loiter radius + def getHomePosition(self) -> Response: """ Request the current home position from the drone. @@ -33,7 +36,7 @@ def getHomePosition(self) -> Response: try: response = self.drone.master.recv_match( - type="HOME_POSITION", blocking=True, timeout=3 + type="HOME_POSITION", blocking=True, timeout=1.5 ) self.drone.is_listening = True @@ -242,3 +245,51 @@ def reposition(self, lat: float, lon: float, alt: float) -> Response: "success": False, "message": "Could not reposition, serial exception", } + + def getLoiterRadius(self) -> Response: + """ + Get the loiter radius of the drone. + """ + + loiter_radius_data = self.drone.paramsController.getSingleParam( + "WP_LOITER_RAD", timeout=1.5 + ) + + if loiter_radius_data.get("success"): + self.loiter_radius = loiter_radius_data.get("data").param_value + return { + "success": True, + "data": self.loiter_radius, + } + else: + self.drone.logger.error(loiter_radius_data.get("message")) + return { + "success": False, + "message": loiter_radius_data.get("message"), + } + + def setLoiterRadius(self, radius: float) -> Response: + """ + Set the loiter radius of the drone. + + Args: + radius (float): The loiter radius in meters + """ + + param_set_success = self.drone.paramsController.setParam( + "WP_LOITER_RAD", radius, self.loiter_radius_param_type + ) + + if param_set_success: + self.drone.logger.info(f"Loiter radius set to {radius}m") + self.loiter_radius = radius + + return { + "success": True, + "message": f"Loiter radius set to {radius}m", + } + else: + return { + "success": False, + "message": f"Loiter radius set to {radius}m", + } diff --git a/radio/app/controllers/paramsController.py b/radio/app/controllers/paramsController.py index 5c6635e78..522b85b55 100644 --- a/radio/app/controllers/paramsController.py +++ b/radio/app/controllers/paramsController.py @@ -40,6 +40,7 @@ def getSingleParam(self, param_name: str, timeout: Optional[float] = 5) -> Respo Response: The response from the retrieval of the specific parameter """ self.drone.is_listening = False + time.sleep(0.1) # Give some time to stop listening failure_message = f"Failed to get parameter {param_name}" self.drone.master.mav.param_request_read_send( @@ -61,6 +62,7 @@ def getSingleParam(self, param_name: str, timeout: Optional[float] = 5) -> Respo "data": response, } else: + print(response) self.drone.is_listening = True return { "success": False, diff --git a/radio/app/endpoints/nav.py b/radio/app/endpoints/nav.py index da98383af..b4cc97b4c 100644 --- a/radio/app/endpoints/nav.py +++ b/radio/app/endpoints/nav.py @@ -15,6 +15,10 @@ class RepositionDataType(TypedDict): alt: int +class LoiterRadiusDataType(TypedDict): + radius: float + + @socketio.on("get_home_position") def getHomePosition() -> None: """ @@ -125,3 +129,57 @@ def reposition(data: RepositionDataType) -> None: result = droneStatus.drone.navController.reposition(lat, lon, alt) socketio.emit("nav_reposition_result", result) + + +@socketio.on("get_loiter_radius") +def getLoiterRadius() -> None: + """ + Gets the loiter radius of the drone, only works when the dashboard page is loaded. + """ + if droneStatus.state not in ["dashboard"]: + socketio.emit( + "params_error", + { + "message": "You must be on the dashboard screen to get the loiter radius." + }, + ) + logger.debug(f"Current state: {droneStatus.state}") + return + + if not droneStatus.drone: + return notConnectedError(action="get loiter radius") + + result = droneStatus.drone.navController.getLoiterRadius() + + socketio.emit("nav_get_loiter_radius_result", result) + + +@socketio.on("set_loiter_radius") +def setLoiterRadius(data: LoiterRadiusDataType) -> None: + """ + Sets the loiter radius of the drone, only works when the dashboard page is loaded. + """ + if droneStatus.state != "dashboard": + socketio.emit( + "params_error", + { + "message": "You must be on the dashboard screen to set the loiter radius." + }, + ) + logger.debug(f"Current state: {droneStatus.state}") + return + + if not droneStatus.drone: + return notConnectedError(action="set loiter radius") + + radius = data.get("radius", None) + if radius is None or radius < 0: + socketio.emit( + "params_error", + {"message": f"Loiter radius must be a positive number, got {radius}."}, + ) + return + + result = droneStatus.drone.navController.setLoiterRadius(radius) + + socketio.emit("nav_set_loiter_radius_result", result) From dad08a3e6cef4a7f8c7fb0272a0966bde8ef0d13 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Mon, 8 Sep 2025 21:08:55 +0100 Subject: [PATCH 2/6] Address copilot review comments --- gcs/src/redux/middleware/socketMiddleware.js | 21 ++++++++++---------- radio/app/controllers/navController.py | 16 ++++++++------- radio/app/controllers/paramsController.py | 1 - 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index 7700169b3..e24e7c0bc 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -302,17 +302,18 @@ const socketMiddleware = (store) => { : queueNotification({ type: "error", message: msg.message }), ) }, + ) + + socket.socket.on( + DroneSpecificSocketEvents.onSetLoiterRadiusResult, + (msg) => { + store.dispatch( + msg.success + ? queueNotification({ type: "success", message: msg.message }) + : queueNotification({ type: "error", message: msg.message }), + ) + }, ), - socket.socket.on( - DroneSpecificSocketEvents.onSetLoiterRadiusResult, - (msg) => { - store.dispatch( - msg.success - ? queueNotification({ type: "success", message: msg.message }) - : queueNotification({ type: "error", message: msg.message }), - ) - }, - ), /* Missions */ diff --git a/radio/app/controllers/navController.py b/radio/app/controllers/navController.py index 819d2629e..47fff58f7 100644 --- a/radio/app/controllers/navController.py +++ b/radio/app/controllers/navController.py @@ -256,16 +256,18 @@ def getLoiterRadius(self) -> Response: ) if loiter_radius_data.get("success"): - self.loiter_radius = loiter_radius_data.get("data").param_value - return { - "success": True, - "data": self.loiter_radius, - } + loiter_radius_data = loiter_radius_data.get("data") + if loiter_radius_data is not None: + self.loiter_radius = loiter_radius_data.param_value + return { + "success": True, + "data": self.loiter_radius, + } else: self.drone.logger.error(loiter_radius_data.get("message")) return { "success": False, - "message": loiter_radius_data.get("message"), + "message": loiter_radius_data.get("message", ""), } def setLoiterRadius(self, radius: float) -> Response: @@ -291,5 +293,5 @@ def setLoiterRadius(self, radius: float) -> Response: else: return { "success": False, - "message": f"Loiter radius set to {radius}m", + "message": f"Failed to set loiter radius set to {radius}m", } diff --git a/radio/app/controllers/paramsController.py b/radio/app/controllers/paramsController.py index 522b85b55..19dde3ee5 100644 --- a/radio/app/controllers/paramsController.py +++ b/radio/app/controllers/paramsController.py @@ -62,7 +62,6 @@ def getSingleParam(self, param_name: str, timeout: Optional[float] = 5) -> Respo "data": response, } else: - print(response) self.drone.is_listening = True return { "success": False, From a0a658b76e0de6e56ddbedf81bcf5a057b41a479 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Mon, 8 Sep 2025 21:13:16 +0100 Subject: [PATCH 3/6] Fix mypy issues --- radio/app/controllers/navController.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/radio/app/controllers/navController.py b/radio/app/controllers/navController.py index 47fff58f7..5dfa60656 100644 --- a/radio/app/controllers/navController.py +++ b/radio/app/controllers/navController.py @@ -256,13 +256,21 @@ def getLoiterRadius(self) -> Response: ) if loiter_radius_data.get("success"): - loiter_radius_data = loiter_radius_data.get("data") - if loiter_radius_data is not None: - self.loiter_radius = loiter_radius_data.param_value + loiter_radius_param = loiter_radius_data.get("data") + if loiter_radius_param is not None: + self.loiter_radius = loiter_radius_param.param_value return { "success": True, "data": self.loiter_radius, } + else: + self.drone.logger.error( + "Loiter radius parameter found, but parametvalue not found" + ) + return { + "success": False, + "message": "Loiter radius parameter found, but parameter value not found", + } else: self.drone.logger.error(loiter_radius_data.get("message")) return { From da73059211d4eaf6b6b2be3e6d1146d7c3733f5a Mon Sep 17 00:00:00 2001 From: Julian Jones Date: Tue, 9 Sep 2025 15:02:38 +0100 Subject: [PATCH 4/6] aligned inputs --- .../dashboard/tabsSectionTabs/actionTabsSection.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gcs/src/components/dashboard/tabsSectionTabs/actionTabsSection.jsx b/gcs/src/components/dashboard/tabsSectionTabs/actionTabsSection.jsx index 9a6ff0268..0769ed284 100644 --- a/gcs/src/components/dashboard/tabsSectionTabs/actionTabsSection.jsx +++ b/gcs/src/components/dashboard/tabsSectionTabs/actionTabsSection.jsx @@ -24,6 +24,7 @@ import { setLoiterRadius, } from "../../../redux/slices/droneInfoSlice" import { NoConnectionMsg } from "../tabsSection" +import { IconMagnet } from "@tabler/icons-react" export default function ActionTabsSection({ connected, @@ -207,6 +208,13 @@ const LoiterRadiusAction = ({ currentLoiterRadius }) => { data-autofocus suffix="m" decimalScale={2} + className="grow" + // Below is the cursed solution to fixing the misalignment between Mantine's + // OWN components. It's a magic number of 40 as Mantine's Select component has + // a 34px RHS icon and without it the NumberInput is 6px wider than select for uhhhh + // unknown reasons. Thanks Mantine :D + rightSection={
} + rightSectionWidth="40px" />