Skip to content

Commit 061e7b5

Browse files
committed
Add progress modal for reading and writing mission to drone
1 parent c3aee87 commit 061e7b5

3 files changed

Lines changed: 128 additions & 9 deletions

File tree

gcs/src/missions.jsx

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,24 @@
66
import { useCallback, useEffect, useRef, useState } from "react"
77

88
// 3rd Party Imports
9-
import { useLocalStorage, useSessionStorage } from "@mantine/hooks"
9+
import {
10+
useDisclosure,
11+
useLocalStorage,
12+
useSessionStorage,
13+
} from "@mantine/hooks"
1014
import { ResizableBox } from "react-resizable"
1115
import { v4 as uuidv4 } from "uuid"
1216

1317
// Custom component and helpers
14-
import { Button, Divider, FileButton, Tabs, Tooltip } from "@mantine/core"
18+
import {
19+
Button,
20+
Divider,
21+
FileButton,
22+
Modal,
23+
Progress,
24+
Tabs,
25+
Tooltip,
26+
} from "@mantine/core"
1527
import { IconInfoCircle } from "@tabler/icons-react"
1628
import Layout from "./components/layout"
1729
import MissionItemsTable from "./components/missions/missionItemsTable"
@@ -71,6 +83,15 @@ export default function Missions() {
7183

7284
const newMissionItemAltitude = 30 // TODO: Make this configurable
7385

86+
const [
87+
missionProgressModalOpened,
88+
{ open: openMissionProgressModal, close: closeMissionProgressModal },
89+
] = useDisclosure(false)
90+
const [missionProgressModalTitle, setMissionProgressModalTitle] = useState(
91+
"Mission progress update",
92+
)
93+
const [missionProgressModalData, setMissionProgressModalData] = useState({})
94+
7495
// Heartbeat data
7596
const [heartbeatData, setHeartbeatData] = useState({ system_status: 0 })
7697

@@ -126,6 +147,8 @@ export default function Missions() {
126147
})
127148

128149
socket.on("current_mission", (data) => {
150+
closeMissionProgressModal()
151+
129152
if (!data.success) {
130153
showErrorNotification(data.message)
131154
return
@@ -152,6 +175,8 @@ export default function Missions() {
152175
})
153176

154177
socket.on("write_mission_result", (data) => {
178+
closeMissionProgressModal()
179+
155180
if (data.success) {
156181
showSuccessNotification(data.message)
157182
} else {
@@ -194,6 +219,10 @@ export default function Missions() {
194219
}
195220
})
196221

222+
socket.on("current_mission_progress", (data) => {
223+
setMissionProgressModalData(data)
224+
})
225+
197226
return () => {
198227
socket.off("incoming_msg")
199228
socket.off("home_position_result")
@@ -202,6 +231,7 @@ export default function Missions() {
202231
socket.off("write_mission_result")
203232
socket.off("import_mission_result")
204233
socket.off("export_mission_result")
234+
socket.off("current_mission_progress")
205235
}
206236
}, [connected])
207237

@@ -211,6 +241,13 @@ export default function Missions() {
211241
}
212242
}, [importFile])
213243

244+
function resetMissionProgressModalData() {
245+
setMissionProgressModalData({
246+
message: "",
247+
progress: null,
248+
})
249+
}
250+
214251
function isGlobalFrameHomeCommand(waypoint) {
215252
const globalFrameValue = parseInt(
216253
Object.keys(MAV_FRAME_LIST).find(
@@ -414,6 +451,9 @@ export default function Missions() {
414451

415452
function readMissionFromDrone() {
416453
socket.emit("get_current_mission", { type: activeTab })
454+
setMissionProgressModalTitle(`Reading ${activeTab} from drone`)
455+
resetMissionProgressModalData()
456+
openMissionProgressModal()
417457
}
418458

419459
function writeMissionToDrone() {
@@ -427,6 +467,9 @@ export default function Missions() {
427467
} else if (activeTab === "rally") {
428468
socket.emit("write_current_mission", { type: "rally", items: rallyItems })
429469
}
470+
setMissionProgressModalTitle(`Writing ${activeTab} to drone`)
471+
resetMissionProgressModalData()
472+
openMissionProgressModal()
430473
}
431474

432475
function importMissionFromFile(filePath) {
@@ -548,6 +591,40 @@ export default function Missions() {
548591

549592
return (
550593
<Layout currentPage="missions">
594+
<Modal
595+
opened={missionProgressModalOpened}
596+
onClose={closeMissionProgressModal}
597+
title={missionProgressModalTitle}
598+
closeOnClickOutside={false}
599+
closeOnEscape={false}
600+
withCloseButton={false}
601+
centered
602+
overlayProps={{
603+
backgroundOpacity: 0.55,
604+
blur: 3,
605+
}}
606+
>
607+
<div className="flex flex-col items-center justify-center mt-4">
608+
{missionProgressModalData.message && (
609+
<p className="text-center mb-2">
610+
{missionProgressModalData.message}
611+
</p>
612+
)}
613+
614+
{(missionProgressModalData.progress !== null ||
615+
missionProgressModalData !== undefined) && (
616+
<Progress
617+
color="lime"
618+
animated
619+
size="lg"
620+
transitionDuration={300}
621+
value={missionProgressModalData.progress * 100}
622+
className="w-full mx-auto my-auto"
623+
/>
624+
)}
625+
</div>
626+
</Modal>
627+
551628
{/* Banner to let people know that things are still under development */}
552629
<div className="bg-falconred-700 text-white text-center">
553630
Missions is still under development so some features are still missing.

radio/app/controllers/missionController.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import os
4-
from typing import TYPE_CHECKING, Any, List
4+
from typing import TYPE_CHECKING, Any, Callable, List, Optional
55

66
import serial
77
from app.customTypes import Number, Response
@@ -89,9 +89,15 @@ def _getCommandName(self, command: int) -> str:
8989
except KeyError:
9090
return f"Unknown command {command}"
9191

92-
def getCurrentMission(self, mission_type: int) -> Response:
92+
def getCurrentMission(
93+
self, mission_type: int, progressUpdateCallback: Optional[Callable]
94+
) -> Response:
9395
"""
9496
Get the current mission of a specific type from the drone.
97+
98+
Args:
99+
mission_type (int): The type of mission to get. 0=Mission,1=Fence,2=Rally.
100+
progressUpdateCallback (Optional[Callable]): A callback function to update the progress of the mission fetch.
95101
"""
96102
mission_type_check = self._checkMissionType(mission_type)
97103
if not mission_type_check.get("success"):
@@ -100,7 +106,7 @@ def getCurrentMission(self, mission_type: int) -> Response:
100106
failure_message = "Could not get current mission"
101107

102108
try:
103-
mission_items = self.getMissionItems(mission_type=mission_type)
109+
mission_items = self.getMissionItems(mission_type, progressUpdateCallback)
104110
if not mission_items.get("success"):
105111
return {
106112
"success": False,
@@ -152,12 +158,15 @@ def getCurrentMissionAll(self) -> Response:
152158
},
153159
}
154160

155-
def getMissionItems(self, mission_type: int) -> Response:
161+
def getMissionItems(
162+
self, mission_type: int, progressUpdateCallback: Optional[Callable]
163+
) -> Response:
156164
"""
157165
Get all mission items of a specific type from the drone.
158166
159167
Args:
160168
mission_type (int): The type of mission to get. 0=Mission,1=Fence,2=Rally.
169+
progressUpdateCallback (Optional[Callable]): A callback function to update the progress of the mission fetch.
161170
"""
162171
mission_type_check = self._checkMissionType(mission_type)
163172
if not mission_type_check.get("success"):
@@ -213,6 +222,12 @@ def getMissionItems(self, mission_type: int) -> Response:
213222
f"Got response for mission count of {response.count} for mission type {response.mission_type}"
214223
)
215224
loader.clear()
225+
226+
if progressUpdateCallback:
227+
progressUpdateCallback(
228+
f"Received count of {response.count} waypoints", 0
229+
)
230+
216231
self.drone.is_listening = True
217232
for i in range(0, response.count):
218233
retry_count = 0
@@ -231,6 +246,12 @@ def getMissionItems(self, mission_type: int) -> Response:
231246

232247
if item_response_data:
233248
loader.add(item_response_data)
249+
250+
if progressUpdateCallback:
251+
progressUpdateCallback(
252+
f"Received waypoint {i+1}", (i + 1) / response.count
253+
)
254+
234255
break
235256
else:
236257
self.drone.logger.warning(
@@ -512,13 +533,19 @@ def _parseWaypointsListIntoLoader(
512533
)
513534
return loader
514535

515-
def uploadMission(self, mission_type: int, waypoints: List[dict]) -> Response:
536+
def uploadMission(
537+
self,
538+
mission_type: int,
539+
waypoints: List[dict],
540+
progressUpdateCallback: Optional[Callable],
541+
) -> Response:
516542
"""
517543
Uploads the current mission to the drone. This method overwrites the current loader if the upload is successful.
518544
519545
Args:
520546
mission_type (int): The type of mission to upload. 0=Mission,1=Fence,2=Rally.
521547
waypoints (List[dict]): The list of waypoints to upload. Each waypoint should be a dict with the required fields.
548+
progressUpdateCallback (Optional[Callable]): A callback function to update the progress of the mission writing.
522549
"""
523550
mission_type_check = self._checkMissionType(mission_type)
524551
if not mission_type_check.get("success"):
@@ -577,6 +604,12 @@ def uploadMission(self, mission_type: int, waypoints: List[dict]) -> Response:
577604
)
578605
self.drone.master.mav.send(new_loader.item(response.seq))
579606

607+
if progressUpdateCallback:
608+
progressUpdateCallback(
609+
f"Sending waypoint {response.seq + 1}",
610+
(response.seq + 1) / (new_loader.count()),
611+
)
612+
580613
if response.seq == new_loader.count() - 1:
581614
mission_ack_response = self.drone.master.recv_match(
582615
type=[

radio/app/endpoints/mission.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ class ControlMissionType(TypedDict):
2929
action: str
3030

3131

32+
def progressUpdateCallback(message: str, progress: float) -> None:
33+
"""
34+
Callback that is used to update the frontend with the current mission function progress.
35+
"""
36+
socketio.emit(
37+
"current_mission_progress", {"message": message, "progress": progress}
38+
)
39+
40+
3241
@socketio.on("get_current_mission")
3342
def getCurrentMission(data: CurrentMissionType) -> None:
3443
"""
@@ -62,7 +71,7 @@ def getCurrentMission(data: CurrentMissionType) -> None:
6271
return
6372

6473
result = droneStatus.drone.missionController.getCurrentMission(
65-
mission_type_array.index(mission_type)
74+
mission_type_array.index(mission_type), progressUpdateCallback
6675
)
6776

6877
if not result.get("success"):
@@ -143,7 +152,7 @@ def writeCurrentMission(data: WriteCurrentMissionType) -> None:
143152
items = data.get("items", [])
144153

145154
result = droneStatus.drone.missionController.uploadMission(
146-
mission_type_array.index(mission_type), items
155+
mission_type_array.index(mission_type), items, progressUpdateCallback
147156
)
148157
if not result.get("success"):
149158
logger.error(result.get("message"))

0 commit comments

Comments
 (0)