Skip to content

Commit cc43160

Browse files
Alpha 0.1.10/571 read mission from file (#594)
* alpha-0.1.9/hf/583-fix-camera-not-working-on-electron Fixed electron build camera issues (#584) * Add mission import from selected file * Add type fix * Add changes from copilot review --------- Co-authored-by: Joe <joantpat@gmail.com>
1 parent ed4b4fc commit cc43160

3 files changed

Lines changed: 174 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: 69 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,71 @@ 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+
"""
573+
Imports a mission from a file into the drone's mission loader, return the waypoints loaded.
574+
575+
Args:
576+
mission_type (int): The type of mission to import. 0=Mission,1=Fence,2=Rally.
577+
file_path (str): The path to the waypoint file to import.
578+
"""
579+
mission_type_check = self._checkMissionType(mission_type)
580+
if not mission_type_check.get("success"):
581+
return mission_type_check
582+
583+
if not file_path or not os.path.exists(file_path):
584+
self.drone.logger.error(f"Waypoint file not found at {file_path}")
585+
return {
586+
"success": False,
587+
"message": f"Waypoint file not found at {file_path}",
588+
}
589+
590+
self.drone.logger.debug(
591+
f"Importing waypoint file from {file_path} for mission type {mission_type}"
592+
)
593+
594+
if mission_type == TYPE_MISSION:
595+
loader = self.missionLoader
596+
elif mission_type == TYPE_FENCE:
597+
loader = self.fenceLoader
598+
else:
599+
loader = self.rallyLoader
600+
601+
try:
602+
loader.load(file_path)
603+
except Exception as e:
604+
self.drone.logger.error(f"Failed to load waypoint file: {e}")
605+
return {
606+
"success": False,
607+
"message": f"Failed to load waypoint file: {e}",
608+
}
609+
610+
# Remove the first point if it's a command 16 as this is usually a home point or placeholder.
611+
if mission_type in [TYPE_FENCE, TYPE_RALLY]:
612+
if loader.count() > 0:
613+
first_wp = loader.item(0)
614+
if first_wp.command == 16:
615+
loader.remove(first_wp)
616+
else:
617+
self.drone.logger.error("Loader is empty; no waypoints to process.")
618+
return {
619+
"success": False,
620+
"message": "Loader is empty; no waypoints to process.",
621+
}
622+
623+
for wp in loader.wpoints:
624+
if hasattr(wp, "x") and hasattr(wp, "y"):
625+
if isinstance(wp.x, float):
626+
wp.x = int(wp.x * 1e7)
627+
if isinstance(wp.y, float):
628+
wp.y = int(wp.y * 1e7)
629+
630+
self.drone.logger.info(
631+
f"Loaded waypoint file with {loader.count()} points successfully"
632+
)
633+
return {
634+
"success": True,
635+
"message": f"Waypoint file loaded {loader.count()} points successfully",
636+
"data": [wp.to_dict() for wp in loader.wpoints],
637+
}

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)