From 522e977a835a361a302391f12656ae06bc015128 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Mon, 15 Sep 2025 20:49:53 +0100 Subject: [PATCH 1/3] Add context menu items to add waypoints --- .../contextMenuSpecificCommandItems.jsx | 153 ++++++++++++++++++ gcs/src/components/missions/missionsMap.jsx | 6 +- gcs/src/redux/slices/missionSlice.js | 32 +++- 3 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 gcs/src/components/mapComponents/contextMenuSpecificCommandItems.jsx diff --git a/gcs/src/components/mapComponents/contextMenuSpecificCommandItems.jsx b/gcs/src/components/mapComponents/contextMenuSpecificCommandItems.jsx new file mode 100644 index 000000000..1ed4d1f16 --- /dev/null +++ b/gcs/src/components/mapComponents/contextMenuSpecificCommandItems.jsx @@ -0,0 +1,153 @@ +import { useDispatch, useSelector } from "react-redux" +import { coordToInt } from "../../helpers/dataFormatters" +import { + COPTER_MISSION_ITEM_COMMANDS_LIST, + FENCE_ITEM_COMMANDS_LIST, +} from "../../helpers/mavlinkConstants" +import { + createNewSpecificMissionItem, + selectActiveTab, + selectContextMenu, +} from "../../redux/slices/missionSlice" +import ContextMenuItem from "./contextMenuItem" + +function getMissionCommandIdByName(name) { + return parseInt( + Object.keys(COPTER_MISSION_ITEM_COMMANDS_LIST).find( + (key) => COPTER_MISSION_ITEM_COMMANDS_LIST[key] === name, + ), + ) +} + +function getFenceCommandIdByName(name) { + return parseInt( + Object.keys(FENCE_ITEM_COMMANDS_LIST).find( + (key) => FENCE_ITEM_COMMANDS_LIST[key] === name, + ), + ) +} + +const RALLY_POINT = 5100 + +const defaultCommandAltitude = 30 // TODO: make this user configurable + +export default function ContextMenuSpecificCommandItems() { + const dispatch = useDispatch() + const activeTab = useSelector(selectActiveTab) + const contextMenuState = useSelector(selectContextMenu) + + function addSpecificMissionItem(commandData) { + dispatch(createNewSpecificMissionItem(commandData)) + } + + if (activeTab === "mission") { + return ( + <> + + addSpecificMissionItem({ + command: getMissionCommandIdByName("MAV_CMD_NAV_WAYPOINT"), + x: coordToInt(contextMenuState.gpsCoords.lat), + y: coordToInt(contextMenuState.gpsCoords.lng), + z: defaultCommandAltitude, + }) + } + > +

Add waypoint

+
+ + addSpecificMissionItem({ + command: getMissionCommandIdByName("MAV_CMD_NAV_TAKEOFF"), + x: 0, + y: 0, + z: 30, + }) + } + > +

Add takeoff

+
+ + addSpecificMissionItem({ + command: getMissionCommandIdByName("MAV_CMD_NAV_LAND"), + x: coordToInt(contextMenuState.gpsCoords.lat), + y: coordToInt(contextMenuState.gpsCoords.lng), + z: 1, + }) + } + > +

Add land

+
+ + addSpecificMissionItem({ + command: getMissionCommandIdByName( + "MAV_CMD_NAV_RETURN_TO_LAUNCH", + ), + x: 0, + y: 0, + z: 0, + }) + } + > +

Add RTL

+
+ + ) + } else if (activeTab === "fence") { + return ( + <> + + addSpecificMissionItem({ + command: getFenceCommandIdByName( + "MAV_CMD_NAV_FENCE_CIRCLE_INCLUSION", + ), + x: coordToInt(contextMenuState.gpsCoords.lat), + y: coordToInt(contextMenuState.gpsCoords.lng), + z: 0, + frame: "MAV_FRAME_GLOBAL", + params: { param1: 5 }, + }) + } + > +

Add circle inclusion

+
+ + addSpecificMissionItem({ + command: getFenceCommandIdByName( + "MAV_CMD_NAV_FENCE_CIRCLE_EXCLUSION", + ), + x: coordToInt(contextMenuState.gpsCoords.lat), + y: coordToInt(contextMenuState.gpsCoords.lng), + z: 0, + frame: "MAV_FRAME_GLOBAL", + params: { param1: 5 }, + }) + } + > +

Add circle exclusion

+
+ + ) + } else if (activeTab === "rally") { + return ( + + addSpecificMissionItem({ + command: RALLY_POINT, + x: coordToInt(contextMenuState.gpsCoords.lat), + y: coordToInt(contextMenuState.gpsCoords.lng), + z: defaultCommandAltitude, + }) + } + > +

Add rally point

+
+ ) + } else { + return null + } +} diff --git a/gcs/src/components/missions/missionsMap.jsx b/gcs/src/components/missions/missionsMap.jsx index 7aa661322..c4c13edd5 100644 --- a/gcs/src/components/missions/missionsMap.jsx +++ b/gcs/src/components/missions/missionsMap.jsx @@ -51,7 +51,7 @@ import { import { clearDrawingItems, createFencePolygon, - createNewDrawingItem, + createNewDefaultDrawingItem, getFrameKey, removeDrawingItem, selectActiveTab, @@ -61,6 +61,7 @@ import { setPlannedHomePositionToDronesHomePositionThunk, updateContextMenuState, } from "../../redux/slices/missionSlice" +import ContextMenuSpecificCommandItems from "../mapComponents/contextMenuSpecificCommandItems" const tailwindColors = resolveConfig(tailwindConfig).theme.colors @@ -321,7 +322,7 @@ function MapSectionNonMemo({ addNewPolygonVertex(lat, lon) } else { dispatch( - createNewDrawingItem({ x: coordToInt(lat), y: coordToInt(lon) }), + createNewDefaultDrawingItem({ x: coordToInt(lat), y: coordToInt(lon) }), ) } }} @@ -446,6 +447,7 @@ function MapSectionNonMemo({ )} +

Zoom to drone

diff --git a/gcs/src/redux/slices/missionSlice.js b/gcs/src/redux/slices/missionSlice.js index 7e060555b..c828cdfd3 100644 --- a/gcs/src/redux/slices/missionSlice.js +++ b/gcs/src/redux/slices/missionSlice.js @@ -213,7 +213,7 @@ const missionInfoSlice = createSlice({ [state.activeTab]: true, } }, - createNewDrawingItem: (state, action) => { + createNewDefaultDrawingItem: (state, action) => { const { x, y } = action.payload const drawingItem = newMissionItem(x, y, state.targetInfo) @@ -238,6 +238,33 @@ const missionInfoSlice = createSlice({ [state.activeTab]: true, } }, + createNewSpecificMissionItem: (state, action) => { + const { x, y, z, command } = action.payload + + const drawingItem = newMissionItem(x, y, state.targetInfo) + const _type = `${state.activeTab}Items` + + drawingItem.seq = state.drawingItems[_type].length + drawingItem.z = z + drawingItem.command = command + drawingItem.mission_type = { mission: 0, fence: 1, rally: 2 }[ + state.activeTab + ] + if (action.payload.frame) { + drawingItem.frame = getFrameKey(action.payload.frame) + } + if (action.payload.params) { + Object.keys(action.payload.params).forEach((key) => { + drawingItem[key] = action.payload.params[key] + }) + } + + state.drawingItems[_type].push(drawingItem) + state.unwrittenChanges = { + ...state.unwrittenChanges, + [state.activeTab]: true, + } + }, clearDrawingItems: (state) => { const _type = `${state.activeTab}Items` @@ -514,7 +541,8 @@ export const { updateDrawingItem, removeDrawingItem, reorderDrawingItem, - createNewDrawingItem, + createNewDefaultDrawingItem, + createNewSpecificMissionItem, clearDrawingItems, createFencePolygon, setDrawingMissionItems, From 2d9b08fb3514bb2bc60dc8a8ec3a050950808619 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Mon, 15 Sep 2025 20:52:43 +0100 Subject: [PATCH 2/3] Remove fence and rally context menu items --- .../contextMenuSpecificCommandItems.jsx | 71 +------------------ 1 file changed, 3 insertions(+), 68 deletions(-) diff --git a/gcs/src/components/mapComponents/contextMenuSpecificCommandItems.jsx b/gcs/src/components/mapComponents/contextMenuSpecificCommandItems.jsx index 1ed4d1f16..bff0d953d 100644 --- a/gcs/src/components/mapComponents/contextMenuSpecificCommandItems.jsx +++ b/gcs/src/components/mapComponents/contextMenuSpecificCommandItems.jsx @@ -1,9 +1,6 @@ import { useDispatch, useSelector } from "react-redux" import { coordToInt } from "../../helpers/dataFormatters" -import { - COPTER_MISSION_ITEM_COMMANDS_LIST, - FENCE_ITEM_COMMANDS_LIST, -} from "../../helpers/mavlinkConstants" +import { COPTER_MISSION_ITEM_COMMANDS_LIST } from "../../helpers/mavlinkConstants" import { createNewSpecificMissionItem, selectActiveTab, @@ -19,16 +16,6 @@ function getMissionCommandIdByName(name) { ) } -function getFenceCommandIdByName(name) { - return parseInt( - Object.keys(FENCE_ITEM_COMMANDS_LIST).find( - (key) => FENCE_ITEM_COMMANDS_LIST[key] === name, - ), - ) -} - -const RALLY_POINT = 5100 - const defaultCommandAltitude = 30 // TODO: make this user configurable export default function ContextMenuSpecificCommandItems() { @@ -40,6 +27,8 @@ export default function ContextMenuSpecificCommandItems() { dispatch(createNewSpecificMissionItem(commandData)) } + // TODO: Add support for loiter commands in sub-menu, as well as modal input for parameters e.g. takeoff altitude + if (activeTab === "mission") { return ( <> @@ -95,59 +84,5 @@ export default function ContextMenuSpecificCommandItems() {
) - } else if (activeTab === "fence") { - return ( - <> - - addSpecificMissionItem({ - command: getFenceCommandIdByName( - "MAV_CMD_NAV_FENCE_CIRCLE_INCLUSION", - ), - x: coordToInt(contextMenuState.gpsCoords.lat), - y: coordToInt(contextMenuState.gpsCoords.lng), - z: 0, - frame: "MAV_FRAME_GLOBAL", - params: { param1: 5 }, - }) - } - > -

Add circle inclusion

-
- - addSpecificMissionItem({ - command: getFenceCommandIdByName( - "MAV_CMD_NAV_FENCE_CIRCLE_EXCLUSION", - ), - x: coordToInt(contextMenuState.gpsCoords.lat), - y: coordToInt(contextMenuState.gpsCoords.lng), - z: 0, - frame: "MAV_FRAME_GLOBAL", - params: { param1: 5 }, - }) - } - > -

Add circle exclusion

-
- - ) - } else if (activeTab === "rally") { - return ( - - addSpecificMissionItem({ - command: RALLY_POINT, - x: coordToInt(contextMenuState.gpsCoords.lat), - y: coordToInt(contextMenuState.gpsCoords.lng), - z: defaultCommandAltitude, - }) - } - > -

Add rally point

-
- ) - } else { - return null } } From 388cc393d35e16e3c62f7992b41d0afe8281b1d2 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Mon, 15 Sep 2025 20:56:48 +0100 Subject: [PATCH 3/3] Address copilot review comments --- .../contextMenuSpecificCommandItems.jsx | 13 ++++++++----- gcs/src/redux/slices/missionSlice.js | 5 +++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/gcs/src/components/mapComponents/contextMenuSpecificCommandItems.jsx b/gcs/src/components/mapComponents/contextMenuSpecificCommandItems.jsx index bff0d953d..36844c71e 100644 --- a/gcs/src/components/mapComponents/contextMenuSpecificCommandItems.jsx +++ b/gcs/src/components/mapComponents/contextMenuSpecificCommandItems.jsx @@ -9,11 +9,14 @@ import { import ContextMenuItem from "./contextMenuItem" function getMissionCommandIdByName(name) { - return parseInt( - Object.keys(COPTER_MISSION_ITEM_COMMANDS_LIST).find( - (key) => COPTER_MISSION_ITEM_COMMANDS_LIST[key] === name, - ), + const key = Object.keys(COPTER_MISSION_ITEM_COMMANDS_LIST).find( + (k) => COPTER_MISSION_ITEM_COMMANDS_LIST[k] === name, ) + if (key === undefined) { + // Command name not found; return null to indicate error + return null + } + return parseInt(key) } const defaultCommandAltitude = 30 // TODO: make this user configurable @@ -50,7 +53,7 @@ export default function ContextMenuSpecificCommandItems() { command: getMissionCommandIdByName("MAV_CMD_NAV_TAKEOFF"), x: 0, y: 0, - z: 30, + z: defaultCommandAltitude, }) } > diff --git a/gcs/src/redux/slices/missionSlice.js b/gcs/src/redux/slices/missionSlice.js index c828cdfd3..1973582fe 100644 --- a/gcs/src/redux/slices/missionSlice.js +++ b/gcs/src/redux/slices/missionSlice.js @@ -241,6 +241,11 @@ const missionInfoSlice = createSlice({ createNewSpecificMissionItem: (state, action) => { const { x, y, z, command } = action.payload + if (!command) { + console.error("Invalid command for new mission item:", action.payload) + return + } + const drawingItem = newMissionItem(x, y, state.targetInfo) const _type = `${state.activeTab}Items`