Skip to content

Commit 21adbf6

Browse files
committed
Add mission import from selected file
1 parent 539a6cd commit 21adbf6

3 files changed

Lines changed: 159 additions & 51 deletions

File tree

gcs/src/missions.jsx

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ResizableBox } from "react-resizable"
1111
import { v4 as uuidv4 } from "uuid"
1212

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

6971
const newMissionItemAltitude = 30 // TODO: Make this configurable
7072

@@ -155,15 +157,45 @@ export default function Missions() {
155157
}
156158
})
157159

160+
socket.on("import_mission_result", (data) => {
161+
if (data.success) {
162+
if (data.mission_type === "mission") {
163+
const missionItemsWithIds = []
164+
for (let missionItem of data.items) {
165+
missionItemsWithIds.push(addIdToItem(missionItem))
166+
}
167+
setMissionItems(missionItemsWithIds)
168+
} else if (data.mission_type === "fence") {
169+
setFenceItems(data.items)
170+
} else if (data.mission_type === "rally") {
171+
const rallyItemsWithIds = []
172+
for (let rallyItem of data.items) {
173+
rallyItemsWithIds.push(addIdToItem(rallyItem))
174+
}
175+
setRallyItems(rallyItemsWithIds)
176+
}
177+
showSuccessNotification(data.message)
178+
} else {
179+
showErrorNotification(data.message)
180+
}
181+
})
182+
158183
return () => {
159184
socket.off("incoming_msg")
160185
socket.off("home_position_result")
161186
socket.off("target_info")
162187
socket.off("current_mission")
163188
socket.off("write_mission_result")
189+
socket.off("import_mission_result")
164190
}
165191
}, [connected])
166192

193+
useEffect(() => {
194+
if (importFile) {
195+
importMissionFromFile(importFile.path)
196+
}
197+
}, [importFile])
198+
167199
function getFlightMode() {
168200
if (aircraftType === 1) {
169201
return PLANE_MODES_FLIGHT_MODE_MAP[heartbeatData.custom_mode]
@@ -290,8 +322,15 @@ export default function Missions() {
290322
}
291323
}
292324

293-
function importMissionFromFile() {
294-
return
325+
function importMissionFromFile(filePath) {
326+
socket.emit("import_mission_from_file", {
327+
type: activeTab,
328+
file_path: filePath,
329+
})
330+
331+
// Reset the import file after sending
332+
setImportFile(null)
333+
importFileResetRef.current?.()
295334
}
296335

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

348387
<div className="flex flex-col gap-4">
349-
<Button
350-
onClick={() => {
351-
importMissionFromFile()
352-
}}
388+
<FileButton
389+
resetRef={importFileResetRef}
390+
onChange={setImportFile}
391+
accept=".waypoints,.txt"
353392
className="grow"
354393
>
355-
Import from file
356-
</Button>
394+
{(props) => <Button {...props}>Import from file</Button>}
395+
</FileButton>
357396
<Button
358397
onClick={() => {
359398
saveMissionToFile()

radio/app/controllers/missionController.py

Lines changed: 54 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -412,48 +412,6 @@ def clearMission(self, mission_type: int) -> Response:
412412
"message": "Could not clear mission, serial exception",
413413
}
414414

415-
def loadWaypointFile(self, file_path: str, mission_type: int) -> Response:
416-
"""
417-
Loads waypoints from a file into the specified mission type.
418-
419-
Args:
420-
file_path (str): The path to the waypoint file
421-
mission_type (int): The type of mission to load the waypoints into. 0=Mission,1=Fence,2=Rally.
422-
"""
423-
mission_type_check = self._checkMissionType(mission_type)
424-
if not mission_type_check.get("success"):
425-
return mission_type_check
426-
427-
if not os.path.exists(file_path):
428-
self.drone.logger.error(f"Waypoint file not found at {file_path}")
429-
return {
430-
"success": False,
431-
"message": f"Waypoint file not found at {file_path}",
432-
}
433-
434-
if mission_type == TYPE_MISSION:
435-
loader = self.missionLoader
436-
elif mission_type == TYPE_FENCE:
437-
loader = self.fenceLoader
438-
else:
439-
loader = self.rallyLoader
440-
441-
loader.load(file_path)
442-
443-
# Remove the first point if it's a command 16 as this is usually a home point or placeholder.
444-
if mission_type in [TYPE_FENCE, TYPE_RALLY]:
445-
first_wp = loader.item(0)
446-
if first_wp.command == 16:
447-
loader.remove(first_wp)
448-
449-
self.drone.logger.info(
450-
f"Loaded waypoint file with {loader.count()} points successfully"
451-
)
452-
return {
453-
"success": True,
454-
"message": f"Waypoint file loaded {loader.count()} points successfully",
455-
}
456-
457415
def _parseWaypointsListIntoLoader(
458416
self, waypoints: List[dict], mission_type: int
459417
) -> mavwp.MAVWPLoader:
@@ -514,6 +472,7 @@ def uploadMission(self, mission_type: int, waypoints: List[dict]) -> Response:
514472
515473
Args:
516474
mission_type (int): The type of mission to upload. 0=Mission,1=Fence,2=Rally.
475+
waypoints (List[dict]): The list of waypoints to upload. Each waypoint should be a dict with the required fields.
517476
"""
518477
mission_type_check = self._checkMissionType(mission_type)
519478
if not mission_type_check.get("success"):
@@ -608,3 +567,56 @@ def uploadMission(self, mission_type: int, waypoints: List[dict]) -> Response:
608567
"success": False,
609568
"message": "Could not upload mission, serial exception",
610569
}
570+
571+
def importMissionFromFile(self, mission_type: int, file_path: str) -> Response:
572+
mission_type_check = self._checkMissionType(mission_type)
573+
if not mission_type_check.get("success"):
574+
return mission_type_check
575+
576+
if not file_path or not os.path.exists(file_path):
577+
self.drone.logger.error(f"Waypoint file not found at {file_path}")
578+
return {
579+
"success": False,
580+
"message": f"Waypoint file not found at {file_path}",
581+
}
582+
583+
self.drone.logger.debug(
584+
f"Importing waypoint file from {file_path} for mission type {mission_type}"
585+
)
586+
587+
if mission_type == TYPE_MISSION:
588+
loader = self.missionLoader
589+
elif mission_type == TYPE_FENCE:
590+
loader = self.fenceLoader
591+
else:
592+
loader = self.rallyLoader
593+
594+
try:
595+
loader.load(file_path)
596+
except Exception as e:
597+
self.drone.logger.error(f"Failed to load waypoint file: {e}")
598+
return {
599+
"success": False,
600+
"message": f"Failed to load waypoint file: {e}",
601+
}
602+
603+
# Remove the first point if it's a command 16 as this is usually a home point or placeholder.
604+
if mission_type in [TYPE_FENCE, TYPE_RALLY]:
605+
first_wp = loader.item(0)
606+
if first_wp.command == 16:
607+
loader.remove(first_wp)
608+
609+
for wp in loader.wpoints:
610+
if isinstance(wp.x, float):
611+
wp.x = int(wp.x * 1e7)
612+
if isinstance(wp.y, float):
613+
wp.y = int(wp.y * 1e7)
614+
615+
self.drone.logger.info(
616+
f"Loaded waypoint file with {loader.count()} points successfully"
617+
)
618+
return {
619+
"success": True,
620+
"message": f"Waypoint file loaded {loader.count()} points successfully",
621+
"data": [wp.to_dict() for wp in loader.wpoints],
622+
}

radio/app/endpoints/mission.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ class WriteCurrentMissionType(TypedDict):
1414
items: list[dict]
1515

1616

17+
class ImportMissionFromFileType(TypedDict):
18+
type: str
19+
file_path: str
20+
21+
1722
class ControlMissionType(TypedDict):
1823
action: str
1924

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

142147

148+
@socketio.on("import_mission_from_file")
149+
def importMissionFromFile(data: ImportMissionFromFileType) -> None:
150+
if droneStatus.state != "missions":
151+
socketio.emit(
152+
"params_error",
153+
{
154+
"message": "You must be on the missions screen to import a mission from a file."
155+
},
156+
)
157+
logger.debug(f"Current state: {droneStatus.state}")
158+
return
159+
160+
if not droneStatus.drone:
161+
return notConnectedError(action="import mission from file")
162+
163+
mission_type = data.get("type")
164+
mission_type_array = ["mission", "fence", "rally"]
165+
166+
if mission_type not in mission_type_array:
167+
socketio.emit(
168+
"write_mission_result",
169+
{
170+
"success": False,
171+
"message": f"Invalid mission type. Must be 'mission', 'fence', or 'rally', got {mission_type}.",
172+
},
173+
)
174+
logger.error(
175+
f"Invalid mission type: {mission_type}. Must be 'mission', 'fence', or 'rally'."
176+
)
177+
return
178+
179+
file_path = data.get("file_path")
180+
181+
result = droneStatus.drone.missionController.importMissionFromFile(
182+
mission_type_array.index(mission_type), file_path
183+
)
184+
185+
if not result.get("success"):
186+
logger.error(result.get("message"))
187+
socketio.emit("import_mission_result", result)
188+
else:
189+
socketio.emit(
190+
"import_mission_result",
191+
{
192+
"success": True,
193+
"message": "Mission imported successfully.",
194+
"items": result.get("data", []),
195+
"mission_type": mission_type,
196+
},
197+
)
198+
199+
143200
@socketio.on("control_mission")
144201
def controlMission(data: ControlMissionType) -> None:
145202
"""

0 commit comments

Comments
 (0)