Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 48 additions & 9 deletions gcs/src/missions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ResizableBox } from "react-resizable"
import { v4 as uuidv4 } from "uuid"

// Custom component and helpers
import { Button, Divider, Tabs } from "@mantine/core"
import { Button, Divider, FileButton, Tabs } from "@mantine/core"
import Layout from "./components/layout"
import MissionItemsTable from "./components/missions/missionItemsTable"
import MissionsMapSection from "./components/missions/missionsMap"
Expand Down Expand Up @@ -65,6 +65,8 @@ export default function Missions() {
key: "targetInfo",
defaultValue: { target_component: 0, target_system: 255 },
})
const [importFile, setImportFile] = useState(null)
const importFileResetRef = useRef(null)

const newMissionItemAltitude = 30 // TODO: Make this configurable

Expand Down Expand Up @@ -155,15 +157,45 @@ export default function Missions() {
}
})

socket.on("import_mission_result", (data) => {
if (data.success) {
if (data.mission_type === "mission") {
const missionItemsWithIds = []
for (let missionItem of data.items) {
missionItemsWithIds.push(addIdToItem(missionItem))
}
setMissionItems(missionItemsWithIds)
} else if (data.mission_type === "fence") {
setFenceItems(data.items)
} else if (data.mission_type === "rally") {
const rallyItemsWithIds = []
for (let rallyItem of data.items) {
rallyItemsWithIds.push(addIdToItem(rallyItem))
}
setRallyItems(rallyItemsWithIds)
}
showSuccessNotification(data.message)
} else {
showErrorNotification(data.message)
}
})

return () => {
socket.off("incoming_msg")
socket.off("home_position_result")
socket.off("target_info")
socket.off("current_mission")
socket.off("write_mission_result")
socket.off("import_mission_result")
}
}, [connected])

useEffect(() => {
if (importFile) {
importMissionFromFile(importFile.path)
}
}, [importFile])

function getFlightMode() {
if (aircraftType === 1) {
return PLANE_MODES_FLIGHT_MODE_MAP[heartbeatData.custom_mode]
Expand Down Expand Up @@ -290,8 +322,15 @@ export default function Missions() {
}
}

function importMissionFromFile() {
return
function importMissionFromFile(filePath) {
socket.emit("import_mission_from_file", {
type: activeTab,
file_path: filePath,
})

// Reset the import file after sending
setImportFile(null)
importFileResetRef.current?.()
}

function saveMissionToFile() {
Expand Down Expand Up @@ -346,14 +385,14 @@ export default function Missions() {
<Divider className="my-1" />

<div className="flex flex-col gap-4">
<Button
onClick={() => {
importMissionFromFile()
}}
<FileButton
resetRef={importFileResetRef}
onChange={setImportFile}
accept=".waypoints,.txt"
className="grow"
>
Import from file
</Button>
{(props) => <Button {...props}>Import from file</Button>}
</FileButton>
<Button
onClick={() => {
saveMissionToFile()
Expand Down
111 changes: 69 additions & 42 deletions radio/app/controllers/missionController.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,48 +412,6 @@ def clearMission(self, mission_type: int) -> Response:
"message": "Could not clear mission, serial exception",
}

def loadWaypointFile(self, file_path: str, mission_type: int) -> Response:
"""
Loads waypoints from a file into the specified mission type.

Args:
file_path (str): The path to the waypoint file
mission_type (int): The type of mission to load the waypoints into. 0=Mission,1=Fence,2=Rally.
"""
mission_type_check = self._checkMissionType(mission_type)
if not mission_type_check.get("success"):
return mission_type_check

if not os.path.exists(file_path):
self.drone.logger.error(f"Waypoint file not found at {file_path}")
return {
"success": False,
"message": f"Waypoint file not found at {file_path}",
}

if mission_type == TYPE_MISSION:
loader = self.missionLoader
elif mission_type == TYPE_FENCE:
loader = self.fenceLoader
else:
loader = self.rallyLoader

loader.load(file_path)

# Remove the first point if it's a command 16 as this is usually a home point or placeholder.
if mission_type in [TYPE_FENCE, TYPE_RALLY]:
first_wp = loader.item(0)
if first_wp.command == 16:
loader.remove(first_wp)

self.drone.logger.info(
f"Loaded waypoint file with {loader.count()} points successfully"
)
return {
"success": True,
"message": f"Waypoint file loaded {loader.count()} points successfully",
}

def _parseWaypointsListIntoLoader(
self, waypoints: List[dict], mission_type: int
) -> mavwp.MAVWPLoader:
Expand Down Expand Up @@ -514,6 +472,7 @@ def uploadMission(self, mission_type: int, waypoints: List[dict]) -> Response:

Args:
mission_type (int): The type of mission to upload. 0=Mission,1=Fence,2=Rally.
waypoints (List[dict]): The list of waypoints to upload. Each waypoint should be a dict with the required fields.
"""
mission_type_check = self._checkMissionType(mission_type)
if not mission_type_check.get("success"):
Expand Down Expand Up @@ -608,3 +567,71 @@ def uploadMission(self, mission_type: int, waypoints: List[dict]) -> Response:
"success": False,
"message": "Could not upload mission, serial exception",
}

def importMissionFromFile(self, mission_type: int, file_path: str) -> Response:
Comment thread
1Blademaster marked this conversation as resolved.
"""
Imports a mission from a file into the drone's mission loader, return the waypoints loaded.

Args:
mission_type (int): The type of mission to import. 0=Mission,1=Fence,2=Rally.
file_path (str): The path to the waypoint file to import.
"""
mission_type_check = self._checkMissionType(mission_type)
if not mission_type_check.get("success"):
return mission_type_check

if not file_path or not os.path.exists(file_path):
self.drone.logger.error(f"Waypoint file not found at {file_path}")
return {
"success": False,
"message": f"Waypoint file not found at {file_path}",
}

self.drone.logger.debug(
f"Importing waypoint file from {file_path} for mission type {mission_type}"
)

if mission_type == TYPE_MISSION:
loader = self.missionLoader
elif mission_type == TYPE_FENCE:
loader = self.fenceLoader
else:
loader = self.rallyLoader

try:
loader.load(file_path)
except Exception as e:
self.drone.logger.error(f"Failed to load waypoint file: {e}")
return {
"success": False,
"message": f"Failed to load waypoint file: {e}",
}

# Remove the first point if it's a command 16 as this is usually a home point or placeholder.
if mission_type in [TYPE_FENCE, TYPE_RALLY]:
if loader.count() > 0:
first_wp = loader.item(0)
if first_wp.command == 16:
loader.remove(first_wp)
else:
self.drone.logger.error("Loader is empty; no waypoints to process.")
return {
"success": False,
"message": "Loader is empty; no waypoints to process.",
}

for wp in loader.wpoints:
if hasattr(wp, "x") and hasattr(wp, "y"):
if isinstance(wp.x, float):
wp.x = int(wp.x * 1e7)
if isinstance(wp.y, float):
wp.y = int(wp.y * 1e7)

self.drone.logger.info(
f"Loaded waypoint file with {loader.count()} points successfully"
)
return {
"success": True,
"message": f"Waypoint file loaded {loader.count()} points successfully",
"data": [wp.to_dict() for wp in loader.wpoints],
}
57 changes: 57 additions & 0 deletions radio/app/endpoints/mission.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ class WriteCurrentMissionType(TypedDict):
items: list[dict]


class ImportMissionFromFileType(TypedDict):
type: str
file_path: str


class ControlMissionType(TypedDict):
action: str

Expand Down Expand Up @@ -140,6 +145,58 @@ def writeCurrentMission(data: WriteCurrentMissionType) -> None:
socketio.emit("write_mission_result", result)


@socketio.on("import_mission_from_file")
def importMissionFromFile(data: ImportMissionFromFileType) -> None:
if droneStatus.state != "missions":
socketio.emit(
"params_error",
{
"message": "You must be on the missions screen to import a mission from a file."
},
)
logger.debug(f"Current state: {droneStatus.state}")
return

if not droneStatus.drone:
return notConnectedError(action="import mission from file")

mission_type = data.get("type")
mission_type_array = ["mission", "fence", "rally"]

if mission_type not in mission_type_array:
socketio.emit(
"write_mission_result",
{
"success": False,
"message": f"Invalid mission type. Must be 'mission', 'fence', or 'rally', got {mission_type}.",
},
)
logger.error(
f"Invalid mission type: {mission_type}. Must be 'mission', 'fence', or 'rally'."
)
return

file_path = data.get("file_path", "")

result = droneStatus.drone.missionController.importMissionFromFile(
mission_type_array.index(mission_type), file_path
)

if not result.get("success"):
logger.error(result.get("message"))
socketio.emit("import_mission_result", result)
else:
socketio.emit(
"import_mission_result",
{
"success": True,
"message": "Mission imported successfully.",
"items": result.get("data", []),
"mission_type": mission_type,
},
)


@socketio.on("control_mission")
def controlMission(data: ControlMissionType) -> None:
"""
Expand Down
Loading