Skip to content

Commit 6c2bb6f

Browse files
authored
Add modal for force disarming if normal disarm fails (#915)
* Add modal for force disarming if normal disarm fails * Update arm tests * Update arming tests * Fix arm tests
1 parent dd6fee8 commit 6c2bb6f

8 files changed

Lines changed: 322 additions & 64 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Button, Modal, Text } from "@mantine/core"
2+
import { useDispatch, useSelector } from "react-redux"
3+
import {
4+
emitArmDisarm,
5+
selectForceDisarmModalOpened,
6+
setForceDisarmModalOpened,
7+
} from "../../redux/slices/droneConnectionSlice"
8+
9+
export default function ForceDisarmModal() {
10+
const dispatch = useDispatch()
11+
const isOpen = useSelector(selectForceDisarmModalOpened)
12+
13+
const handleForceDisarm = () => {
14+
dispatch(emitArmDisarm({ arm: false, force: true }))
15+
dispatch(setForceDisarmModalOpened(false))
16+
}
17+
18+
const handleCancel = () => {
19+
dispatch(setForceDisarmModalOpened(false))
20+
}
21+
22+
return (
23+
<Modal
24+
opened={isOpen}
25+
onClose={handleCancel}
26+
title="Failed to Disarm"
27+
centered
28+
>
29+
<div className="flex flex-col gap-4">
30+
<Text>
31+
The aircraft failed to disarm normally. This could be because the
32+
aircraft is still in the air or has other safety concerns.
33+
</Text>
34+
<Text weight={700} color="red">
35+
Do you want to force disarm the aircraft?
36+
</Text>
37+
<Text size="sm" color="dimmed">
38+
Warning: Force disarming bypasses safety checks and could cause the
39+
aircraft to crash if it's still airborne.
40+
</Text>
41+
<div className="flex gap-2">
42+
<Button onClick={handleCancel} variant="default" className="grow">
43+
Cancel
44+
</Button>
45+
<Button onClick={handleForceDisarm} color="red" className="grow">
46+
Force Disarm
47+
</Button>
48+
</div>
49+
</div>
50+
</Modal>
51+
)
52+
}

gcs/src/dashboard.jsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,24 @@ import {
3434
selectBatteryData,
3535
selectDroneCoords,
3636
selectFlightMode,
37-
selectGPSRawInt,
3837
selectGPS2RawInt,
38+
selectGPSRawInt,
39+
selectHasSecondaryGps,
3940
selectNotificationSound,
4041
selectRSSI,
4142
soundPlayed,
42-
selectHasSecondaryGps,
4343
} from "./redux/slices/droneInfoSlice"
44-
import { selectMessages } from "./redux/slices/statusTextSlice"
4544
import { selectCurrentMission } from "./redux/slices/missionSlice"
45+
import { selectMessages } from "./redux/slices/statusTextSlice"
4646

4747
import { useSettings } from "./helpers/settings"
4848

4949
// Helper javascript files
5050
import { GPS_FIX_TYPES } from "./helpers/mavlinkConstants"
5151

52+
// Import components
53+
import ForceDisarmModal from "./components/dashboard/ForceDisarmModal"
54+
5255
// Custom component
5356
import useSound from "use-sound"
5457
import FloatingToolbar from "./components/dashboard/floatingToolbar"
@@ -69,9 +72,9 @@ const tailwindColors = resolveConfig(tailwindConfig).theme.colors
6972
// Sounds
7073
import armSound from "./assets/sounds/armed.mp3"
7174
import disarmSound from "./assets/sounds/disarmed.mp3"
75+
import flightModeChangedSound from "./assets/sounds/flightmodechanged.mp3"
7276
import lowBatterySound from "./assets/sounds/lowbattery.mp3"
7377
import waypointReachedSound from "./assets/sounds/waypointreached.mp3"
74-
import flightModeChangedSound from "./assets/sounds/flightmodechanged.mp3"
7578

7679
export default function Dashboard() {
7780
const dispatch = useDispatch()
@@ -353,6 +356,7 @@ export default function Dashboard() {
353356
</ResizableBox>
354357
</div>
355358
</div>
359+
<ForceDisarmModal />
356360
</Layout>
357361
)
358362
}

gcs/src/redux/middleware/socketMiddleware.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
setConnectionModal,
1818
setConnectionStatus,
1919
setFetchingComPorts,
20+
setForceDisarmModalOpened,
2021
setSelectedComPorts,
2122
} from "../slices/droneConnectionSlice"
2223

@@ -529,7 +530,14 @@ const socketMiddleware = (store) => {
529530
})
530531

531532
socket.socket.on(DroneSpecificSocketEvents.onArmDisarm, (msg) => {
532-
if (!msg.success) showErrorNotification(msg.message)
533+
if (!msg.success) {
534+
// Check if this was a disarm attempt and was not a force disarm
535+
if (msg.data?.was_disarming && !msg.data?.was_force) {
536+
store.dispatch(setForceDisarmModalOpened(true))
537+
} else {
538+
showErrorNotification(msg.message)
539+
}
540+
}
533541
})
534542

535543
socket.socket.on(

gcs/src/redux/slices/droneConnectionSlice.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ const initialState = {
4343
videoSource: null,
4444
videoMaximized: false,
4545
videoScale: 1,
46+
47+
forceDisarmModalOpened: false,
4648
}
4749

4850
const droneConnectionSlice = createSlice({
@@ -143,6 +145,9 @@ const droneConnectionSlice = createSlice({
143145
setVideoScale: (state, action) => {
144146
state.videoScale = action.payload
145147
},
148+
setForceDisarmModalOpened: (state, action) => {
149+
state.forceDisarmModalOpened = action.payload
150+
},
146151

147152
// Emits
148153
emitIsConnectedToDrone: () => {},
@@ -187,6 +192,7 @@ const droneConnectionSlice = createSlice({
187192
selectVideoSource: (state) => state.videoSource,
188193
selectVideoMaximized: (state) => state.videoMaximized,
189194
selectVideoScale: (state) => state.videoScale,
195+
selectForceDisarmModalOpened: (state) => state.forceDisarmModalOpened,
190196
},
191197
})
192198

@@ -213,6 +219,7 @@ export const {
213219
setVideoSource,
214220
setVideoMaximized,
215221
setVideoScale,
222+
setForceDisarmModalOpened,
216223

217224
// Emitters
218225
emitIsConnectedToDrone,
@@ -254,6 +261,7 @@ export const {
254261
selectVideoSource,
255262
selectVideoMaximized,
256263
selectVideoScale,
264+
selectForceDisarmModalOpened,
257265
} = droneConnectionSlice.selectors
258266

259267
export default droneConnectionSlice

radio/app/controllers/armController.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,26 @@ def arm(self, force: bool = False) -> Response:
3434
Returns:
3535
Response: The response from the arm command
3636
"""
37+
return_data = {
38+
"was_disarming": False,
39+
"was_force": force,
40+
}
41+
3742
if self.drone.armed:
38-
return {"success": False, "message": "Already armed"}
43+
return {"success": False, "message": "Already armed", "data": return_data}
3944

4045
if not self.drone.reserve_message_type("COMMAND_ACK", self.controller_id):
4146
return {
4247
"success": False,
4348
"message": "Could not reserve COMMAND_ACK messages",
49+
"data": return_data,
4450
}
4551

4652
try:
4753
self.drone.sendCommand(
4854
mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM,
4955
param1=1, # 0=disarm, 1=arm
50-
param2=2989 if force else 0, # force arm/disarm
56+
param2=21196 if force else 0, # force arm/disarm
5157
)
5258

5359
response = self.drone.wait_for_message(
@@ -63,19 +69,28 @@ def arm(self, force: bool = False) -> Response:
6369
while not self.drone.armed:
6470
time.sleep(0.05)
6571
self.drone.logger.debug("ARMED")
66-
return {"success": True, "message": "Armed successfully"}
72+
return {
73+
"success": True,
74+
"message": "Armed successfully",
75+
"data": return_data,
76+
}
6777
else:
6878
self.drone.logger.debug("Arming failed")
6979
return {
7080
"success": False,
7181
"message": "Could not arm, command not accepted",
82+
"data": return_data,
7283
}
7384

7485
except Exception as e:
7586
self.drone.logger.error(e, exc_info=True)
7687
if self.drone.droneErrorCb:
7788
self.drone.droneErrorCb(str(e))
78-
return {"success": False, "message": "Could not arm, serial exception"}
89+
return {
90+
"success": False,
91+
"message": "Could not arm, serial exception",
92+
"data": return_data,
93+
}
7994
finally:
8095
self.drone.release_message_type("COMMAND_ACK", self.controller_id)
8196

@@ -90,20 +105,30 @@ def disarm(self, force: bool = False) -> Response:
90105
Returns:
91106
Response: The response from the disarm command
92107
"""
108+
return_data = {
109+
"was_disarming": True,
110+
"was_force": force,
111+
}
112+
93113
if not self.drone.armed:
94-
return {"success": False, "message": "Already disarmed"}
114+
return {
115+
"success": False,
116+
"message": "Already disarmed",
117+
"data": return_data,
118+
}
95119

96120
if not self.drone.reserve_message_type("COMMAND_ACK", self.controller_id):
97121
return {
98122
"success": False,
99123
"message": "Could not reserve COMMAND_ACK messages",
124+
"data": return_data,
100125
}
101126

102127
try:
103128
self.drone.sendCommand(
104129
mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM,
105130
param1=0, # 0=disarm, 1=arm
106-
param2=2989 if force else 0, # force arm/disarm
131+
param2=21196 if force else 0, # force arm/disarm
107132
)
108133

109134
response = self.drone.wait_for_message(
@@ -119,18 +144,27 @@ def disarm(self, force: bool = False) -> Response:
119144
while self.drone.armed:
120145
time.sleep(0.05)
121146
self.drone.logger.debug("DISARMED")
122-
return {"success": True, "message": "Disarmed successfully"}
147+
return {
148+
"success": True,
149+
"message": "Disarmed successfully",
150+
"data": return_data,
151+
}
123152
else:
124153
self.drone.logger.debug("Could not disarm, command not accepted")
125154
return {
126155
"success": False,
127156
"message": "Could not disarm, command not accepted",
157+
"data": return_data,
128158
}
129159

130160
except Exception as e:
131161
self.drone.logger.error(e, exc_info=True)
132162
if self.drone.droneErrorCb:
133163
self.drone.droneErrorCb(str(e))
134-
return {"success": False, "message": "Could not disarm, serial exception"}
164+
return {
165+
"success": False,
166+
"message": "Could not disarm, serial exception",
167+
"data": return_data,
168+
}
135169
finally:
136170
self.drone.release_message_type("COMMAND_ACK", self.controller_id)

radio/app/endpoints/arm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import app.droneStatus as droneStatus
44
from app import socketio
5-
from app.utils import notConnectedError, missingParameterError
5+
from app.utils import missingParameterError, notConnectedError
66

77

88
class ArmDisarmType(TypedDict):

radio/tests/helpers.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ class FakeTCP:
1515
"""
1616

1717
@staticmethod
18-
def return_serial_exception(
19-
condition=None, type=None, blocking=False, timeout=None
20-
) -> None:
18+
def return_serial_exception(*args, **kwargs) -> None:
2119
raise SerialException(
2220
"Test SerialException generated by tests.FakeTCP context manager."
2321
)

0 commit comments

Comments
 (0)