diff --git a/gcs/src/components/mapComponents/missionItems.jsx b/gcs/src/components/mapComponents/missionItems.jsx index d624cb3dd..d1ad022ef 100644 --- a/gcs/src/components/mapComponents/missionItems.jsx +++ b/gcs/src/components/mapComponents/missionItems.jsx @@ -31,33 +31,53 @@ export default function MissionItems({ filterMissionItems(missionItems), ) const [listOfLineCoords, setListOfLineCoords] = useState([]) + const [listOfDottedLineCoords, setListOfDottedLineCoords] = useState([]) useEffect(() => { setFilteredMissionItems(filterMissionItems(missionItems)) }, [missionItems]) useEffect(() => { - setListOfLineCoords(getListOfLineCoordinates(filteredMissionItems)) + const { solid: solidLineCoords, dotted: dottedLineCoords } = + getListOfLineCoordinates(filteredMissionItems) + + setListOfLineCoords(solidLineCoords) + setListOfDottedLineCoords(dottedLineCoords) }, [filteredMissionItems]) function getListOfLineCoordinates(filteredMissionItems) { - if (filteredMissionItems.length === 0) return [] + if (filteredMissionItems.length === 0) return { solid: [], dotted: [] } const lineCoordsList = [] - - filteredMissionItems.forEach((item) => { + const dottedLineCoordsList = [] + + // Stop processing waypoints after a land command + const landCommandIndex = filteredMissionItems.findIndex((item) => + [21, 189].includes(item.command), + ) + const itemsToProcess = + landCommandIndex === -1 + ? filteredMissionItems + : filteredMissionItems.slice(0, landCommandIndex + 1) + + itemsToProcess.forEach((item) => { lineCoordsList.push([intToCoord(item.y), intToCoord(item.x)]) }) - // Join the last item to first item if aircraft does not land + // Join the last item to first item if aircraft does not land, with a + // dotted line if ( ![21, 189].includes( - filteredMissionItems[filteredMissionItems.length - 1].command, + itemsToProcess[itemsToProcess.length - 1].command, // Use itemsToProcess here ) ) { - lineCoordsList.push([ - intToCoord(filteredMissionItems[0].y), - intToCoord(filteredMissionItems[0].x), + dottedLineCoordsList.push([ + intToCoord(itemsToProcess[0].y), // Use itemsToProcess here + intToCoord(itemsToProcess[0].x), + ]) + dottedLineCoordsList.push([ + intToCoord(itemsToProcess[itemsToProcess.length - 1].y), // Use itemsToProcess here + intToCoord(itemsToProcess[itemsToProcess.length - 1].x), ]) } @@ -80,7 +100,7 @@ export default function MissionItems({ lineCoordsList.push([intToCoord(nextItem.y), intToCoord(nextItem.x)]) }) - return lineCoordsList + return { solid: lineCoordsList, dotted: dottedLineCoordsList } } return ( @@ -107,6 +127,12 @@ export default function MissionItems({ coordinates={listOfLineCoords} colour={tailwindColors.yellow[400]} /> + + ) } diff --git a/gcs/src/components/missions/missionItemsTable.jsx b/gcs/src/components/missions/missionItemsTable.jsx index acd7fef14..1563227a6 100644 --- a/gcs/src/components/missions/missionItemsTable.jsx +++ b/gcs/src/components/missions/missionItemsTable.jsx @@ -10,9 +10,11 @@ function MissionItemsTableNonMemo({ missionItems, aircraftType, updateMissionItem, + deleteMissionItem, + updateMissionItemOrder, }) { return ( - +
@@ -22,9 +24,10 @@ function MissionItemsTableNonMemo({ Param 3 Param 4 Lat - Long + Lng Alt Frame + @@ -45,6 +48,8 @@ function MissionItemsTableNonMemo({ aircraftType={aircraftType} missionItem={missionItem} updateMissionItem={updateMissionItem} + deleteMissionItem={deleteMissionItem} + updateMissionItemOrder={updateMissionItemOrder} /> ) })} diff --git a/gcs/src/components/missions/missionItemsTableRow.jsx b/gcs/src/components/missions/missionItemsTableRow.jsx index 29ae943ec..6ac80dee7 100644 --- a/gcs/src/components/missions/missionItemsTableRow.jsx +++ b/gcs/src/components/missions/missionItemsTableRow.jsx @@ -2,7 +2,14 @@ This component displays the row for a mission item in a table. */ -import { NumberInput, Select, TableTd, TableTr } from "@mantine/core" +import { + ActionIcon, + NumberInput, + Select, + TableTd, + TableTr, +} from "@mantine/core" +import { IconArrowDown, IconArrowUp, IconTrash } from "@tabler/icons-react" import { useEffect, useState } from "react" import { coordToInt, intToCoord } from "../../helpers/dataFormatters" import { @@ -18,6 +25,8 @@ export default function MissionItemsTableRow({ aircraftType, missionItem, updateMissionItem, + deleteMissionItem, + updateMissionItemOrder, }) { const [missionItemData, setMissionItemData] = useState(missionItem) @@ -131,6 +140,24 @@ export default function MissionItemsTableRow({ /> {getFrameName(missionItemData.frame)} + + updateMissionItemOrder(missionItemData.id, -1)} + > + + + updateMissionItemOrder(missionItemData.id, 1)} + > + + + deleteMissionItem(missionItemData.id)} + color="red" + > + + + ) } diff --git a/gcs/src/components/missions/missionsMap.jsx b/gcs/src/components/missions/missionsMap.jsx index 523f93331..0a7577505 100644 --- a/gcs/src/components/missions/missionsMap.jsx +++ b/gcs/src/components/missions/missionsMap.jsx @@ -53,6 +53,7 @@ function MapSectionNonMemo({ currentTab, markerDragEndCallback, rallyDragEndCallback, + addNewMissionItem, mapId = "dashboard", }) { const [connected] = useSessionStorage({ @@ -198,6 +199,12 @@ function MapSectionNonMemo({ }, }) }} + onClick={(e) => { + setClicked(false) + let lat = e.lngLat.lat + let lon = e.lngLat.lng + addNewMissionItem(lat, lon) + }} cursor="default" > {/* Show marker on map if the position is set */} diff --git a/gcs/src/components/missions/rallyItemsTable.jsx b/gcs/src/components/missions/rallyItemsTable.jsx index d1b73a02d..95abf6ba6 100644 --- a/gcs/src/components/missions/rallyItemsTable.jsx +++ b/gcs/src/components/missions/rallyItemsTable.jsx @@ -7,7 +7,7 @@ import RallyItemsTableRow from "./rallyItemsTableRow" function RallyItemsTableNonMemo({ rallyItems, updateRallyItem }) { return ( -
+
@@ -17,7 +17,7 @@ function RallyItemsTableNonMemo({ rallyItems, updateRallyItem }) { Param 3 Param 4 Lat - Long + Lng Alt Frame diff --git a/gcs/src/missions.jsx b/gcs/src/missions.jsx index 149cd704e..1b6f57b3f 100644 --- a/gcs/src/missions.jsx +++ b/gcs/src/missions.jsx @@ -17,10 +17,11 @@ import MissionItemsTable from "./components/missions/missionItemsTable" import MissionsMapSection from "./components/missions/missionsMap" import RallyItemsTable from "./components/missions/rallyItemsTable" import NoDroneConnected from "./components/noDroneConnected" -import { intToCoord } from "./helpers/dataFormatters" +import { coordToInt, intToCoord } from "./helpers/dataFormatters" import { COPTER_MODES_FLIGHT_MODE_MAP, MAV_AUTOPILOT_INVALID, + MAV_FRAME_LIST, PLANE_MODES_FLIGHT_MODE_MAP, } from "./helpers/mavlinkConstants" import { @@ -60,6 +61,12 @@ export default function Missions() { key: "homePosition", defaultValue: null, }) + const [targetInfo, setTargetInfo] = useSessionStorage({ + key: "targetInfo", + defaultValue: { target_component: 0, target_system: 255 }, + }) + + const newMissionItemAltitude = 30 // TODO: Make this configurable // Heartbeat data const [heartbeatData, setHeartbeatData] = useState({ system_status: 0 }) @@ -92,6 +99,7 @@ export default function Missions() { } else { socket.emit("set_state", { state: "missions" }) socket.emit("get_home_position") + socket.emit("get_target_info") } socket.on("incoming_msg", (msg) => { @@ -108,14 +116,18 @@ export default function Missions() { } }) + socket.on("target_info", (data) => { + if (data) { + setTargetInfo(data) + } + }) + socket.on("current_mission", (data) => { if (!data.success) { showErrorNotification(data.message) return } - console.log(data) - if (data.mission_type === "mission") { const missionItemsWithIds = [] for (let missionItem of data.items) { @@ -146,6 +158,7 @@ export default function Missions() { return () => { socket.off("incoming_msg") socket.off("home_position_result") + socket.off("target_info") socket.off("current_mission") socket.off("write_mission_result") } @@ -168,6 +181,34 @@ export default function Missions() { return missionItem } + function addNewMissionItem(lat, lon) { + const newMissionItem = { + id: uuidv4(), + seq: missionItems.length, + x: coordToInt(lat), + y: coordToInt(lon), + z: newMissionItemAltitude, + frame: parseInt( + Object.keys(MAV_FRAME_LIST).find( + (key) => MAV_FRAME_LIST[key] === "MAV_FRAME_GLOBAL_RELATIVE_ALT", + ), + ), + command: 16, // MAV_CMD_NAV_WAYPOINT + param1: 0, + param2: 0, + param3: 0, + param4: 0, + current: 0, + autocontinue: 1, + target_component: targetInfo.target_component, + target_system: targetInfo.target_system, + mission_type: 0, + mavpackettype: "MISSION_ITEM_INT", + } + + setMissionItems((prevItems) => [...prevItems, newMissionItem]) + } + function updateMissionItem(updatedMissionItem) { setMissionItems((prevItems) => prevItems.map((item) => @@ -187,6 +228,51 @@ export default function Missions() { ) } + function deleteMissionItem(missionItemId) { + setMissionItems((prevItems) => { + const updatedItems = prevItems.filter((item) => item.id !== missionItemId) + + return updatedItems.map((item, index) => ({ + ...item, + seq: index, // Reassign seq based on the new order + })) + }) + } + + function updateMissionItemOrder(missionItemId, indexIncrement) { + setMissionItems((prevItems) => { + const currentIndex = prevItems.findIndex( + (item) => item.id === missionItemId, + ) + + // Ensure the item exists and the swap is within bounds + if ( + currentIndex === -1 || + (indexIncrement === -1 && currentIndex === 0) || + (indexIncrement === 1 && currentIndex === prevItems.length - 1) + ) { + return prevItems // No changes if out of bounds + } + + // Calculate the new index + const newIndex = currentIndex + indexIncrement + + // Create a copy of the items array + const updatedItems = [...prevItems] + + // Swap the items + const temp = updatedItems[currentIndex] + updatedItems[currentIndex] = updatedItems[newIndex] + updatedItems[newIndex] = temp + + // Update the seq values + updatedItems[currentIndex].seq = currentIndex + updatedItems[newIndex].seq = newIndex + + return updatedItems + }) + } + function readMissionFromDrone() { socket.emit("get_current_mission", { type: activeTab }) } @@ -317,6 +403,7 @@ export default function Missions() { currentTab={activeTab} markerDragEndCallback={updateMissionItem} rallyDragEndCallback={updateRallyItem} + addNewMissionItem={addNewMissionItem} mapId="missions" /> @@ -350,6 +437,8 @@ export default function Missions() { missionItems={missionItems} aircraftType={aircraftType} updateMissionItem={updateMissionItem} + deleteMissionItem={deleteMissionItem} + updateMissionItemOrder={updateMissionItemOrder} /> diff --git a/radio/app/endpoints/connections.py b/radio/app/endpoints/connections.py index a3ff891bd..9b3fa7b89 100644 --- a/radio/app/endpoints/connections.py +++ b/radio/app/endpoints/connections.py @@ -28,3 +28,20 @@ def isConnectedToDrone() -> None: Handle client asking if we're connected to the drone or not """ socketio.emit("is_connected_to_drone", bool(droneStatus.drone)) + + +@socketio.on("get_target_info") +def getTargetInfo() -> None: + """ + Return the target component and target system + """ + if droneStatus.drone: + socketio.emit( + "target_info", + { + "target_component": droneStatus.drone.target_component, + "target_system": droneStatus.drone.target_system, + }, + ) + else: + socketio.emit("target_info", None) diff --git a/radio/tests/test_connections.py b/radio/tests/test_connections.py index 6ae6aab89..5db228747 100644 --- a/radio/tests/test_connections.py +++ b/radio/tests/test_connections.py @@ -1,7 +1,6 @@ import pytest -from serial.tools.list_ports_common import ListPortInfo - from flask_socketio.test_client import SocketIOTestClient +from serial.tools.list_ports_common import ListPortInfo from . import falcon_test @@ -12,6 +11,7 @@ def run_once_after_all_tests(): Saves the valid connection string then ensures that the drone connection is established again after the tests have run """ from app import droneStatus + from .conftest import setupDrone assert droneStatus.drone is not None @@ -49,6 +49,20 @@ def test_isConnectedToDrone_with_drone( assert socketio_result[0]["name"] == "is_connected_to_drone" # Correct name emitted +@falcon_test(pass_drone_status=True) +def test_getTargetInfo(socketio_client: SocketIOTestClient, droneStatus): + socketio_client.emit("get_target_info") + socketio_result = socketio_client.get_received() + + assert len(socketio_result) == 1 + assert socketio_result[0]["name"] == "target_info" + assert socketio_result[0]["args"][0] == { + "target_component": 0, + "target_system": 1, + } + + +# Has to be the final test otherwise the socket disconnects @falcon_test(pass_drone_status=True) def test_disconnect(socketio_client: SocketIOTestClient, droneStatus): """Test disconnecting from socket"""