Skip to content

Commit 0f7c331

Browse files
authored
1147 feature be able to cancel connection (#1149)
* Add cancelling drone connection mid way * Fix some bugs with connection cancellation * Address copilot review comments * Try fixing broken test
1 parent 0e96371 commit 0f7c331

8 files changed

Lines changed: 234 additions & 45 deletions

File tree

gcs/src/components/navbar.jsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { useDispatch, useSelector } from "react-redux"
3737
import {
3838
ConnectionType,
3939
emitConnectToDrone,
40+
emitDisconnectFromDrone,
4041
emitGetComPorts,
4142
emitStartForwarding,
4243
emitStopForwarding,
@@ -169,6 +170,9 @@ export default function Navbar() {
169170
<Modal
170171
opened={openedModal}
171172
onClose={() => {
173+
if (connecting) {
174+
dispatch(emitDisconnectFromDrone())
175+
}
172176
dispatch(setConnectionModal(false))
173177
dispatch(setConnecting(false))
174178
}}
@@ -297,12 +301,14 @@ export default function Navbar() {
297301
variant="filled"
298302
color={"red"}
299303
onClick={() => {
304+
if (connecting) {
305+
dispatch(emitDisconnectFromDrone())
306+
}
300307
dispatch(setConnectionModal(false))
301308
dispatch(setConnecting(false))
302309
}}
303-
disabled={connecting}
304310
>
305-
Close
311+
{connecting ? "Cancel" : "Close"}
306312
</Button>
307313
<Button
308314
variant="filled"

gcs/src/redux/middleware/emitters.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,17 @@ export function handleEmitters(socket, store, action) {
9999
},
100100
{
101101
emitter: emitDisconnectFromDrone,
102-
callback: () => socket.socket.emit("disconnect_from_drone"),
102+
callback: () => {
103+
const storeState = store.getState()
104+
const isConnecting = storeState.droneConnection.connecting
105+
106+
if (isConnecting) {
107+
socket.socket.emit("cancel_connect_to_drone")
108+
return
109+
}
110+
111+
socket.socket.emit("disconnect_from_drone")
112+
},
103113
},
104114
{
105115
emitter: emitConnectToDrone,

radio/app/controllers/paramsController.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,17 @@ def fetchAllParamsBlocking(
7777
self,
7878
timeout_secs: int = 120,
7979
progress_update_callback: Optional[Callable[[dict], None]] = None,
80+
should_cancel_callback: Optional[Callable[[], bool]] = None,
8081
) -> Response:
8182
"""
8283
Fetches all parameters from the drone in a blocking manner.
8384
"""
85+
if should_cancel_callback and should_cancel_callback():
86+
return {
87+
"success": False,
88+
"message": "Connection cancelled by user.",
89+
}
90+
8491
start_response = self._startFetchAll()
8592
if not start_response.get("success"):
8693
return start_response
@@ -89,6 +96,13 @@ def fetchAllParamsBlocking(
8996

9097
try:
9198
while self.is_requesting_params:
99+
if should_cancel_callback and should_cancel_callback():
100+
self.drone.logger.info("Parameter fetch cancelled by user")
101+
return {
102+
"success": False,
103+
"message": "Connection cancelled by user.",
104+
}
105+
92106
if time.time() > timeout:
93107
self.drone.logger.error(
94108
f"Fetching all parameters timed out after {timeout_secs} seconds"

radio/app/drone.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def __init__(
8585
droneConnectStatusCb: Optional[Callable] = None,
8686
linkDebugStatsCb: Optional[Callable] = None,
8787
fetchingParameterCb: Optional[Callable] = None,
88+
connectionCancelEvent: Optional[Event] = None,
8889
) -> None:
8990
"""
9091
The drone class interfaces with the UAS via MavLink.
@@ -95,6 +96,9 @@ def __init__(
9596
droneErrorCb (Optional[Callable], optional): Callback function for drone errors. Defaults to None.
9697
droneDisconnectCb (Optional[Callable], optional): Callback function for drone disconnection. Defaults to None.
9798
droneConnectStatusCb (Optional[Callable], optional): Callback function for drone connection providing an update as the drone connects. Defaults to None.
99+
linkDebugStatsCb (Optional[Callable], optional): Callback function for link debug stats. Defaults to None.
100+
fetchingParameterCb (Optional[Callable], optional): Callback function for when parameters are being fetched. Defaults to None.
101+
connectionCancelEvent (Optional[Event], optional): Event to signal if the connection process should be cancelled. Defaults to None.
98102
"""
99103
self.port = port
100104
self.baud = baud
@@ -104,6 +108,7 @@ def __init__(
104108
self.droneConnectStatusCb = droneConnectStatusCb
105109
self.linkDebugStatsCb = linkDebugStatsCb
106110
self.fetchingParameterCb = fetchingParameterCb
111+
self.connection_cancel_event: Event = connectionCancelEvent or Event()
107112

108113
self.connectionError: Optional[str] = None
109114
self._last_connect_progress: float = 0.0
@@ -148,12 +153,20 @@ def __init__(
148153
self.connectionError = str(e)
149154
return
150155

156+
if self._isConnectionCancelRequested():
157+
self._setCancelledConnectionErrorAndCloseMaster()
158+
return
159+
151160
try:
152161
initial_heartbeat = None
153162
heartbeat_timeout_secs = 5.0
154163
deadline = time.monotonic() + heartbeat_timeout_secs
155164

156165
while True:
166+
if self._isConnectionCancelRequested():
167+
self._setCancelledConnectionErrorAndCloseMaster()
168+
return
169+
157170
now = time.monotonic()
158171
if now >= deadline:
159172
break
@@ -249,6 +262,12 @@ def __init__(
249262

250263
self.addMessageListener("STATUSTEXT", sendMessage)
251264

265+
if self._isConnectionCancelRequested():
266+
self._setCancelledConnectionErrorAndCloseMaster()
267+
self.is_active.clear()
268+
self.stopAllThreads()
269+
return
270+
252271
self.getAutopilotVersion()
253272

254273
if self.flight_sw_version is None:
@@ -289,6 +308,7 @@ def __init__(
289308
fetch_all_params_result = self.paramsController.fetchAllParamsBlocking(
290309
timeout_secs=120,
291310
progress_update_callback=self.sendParamFetchConnectionStatusUpdate,
311+
should_cancel_callback=self._isConnectionCancelRequested,
292312
)
293313

294314
if not fetch_all_params_result.get("success"):
@@ -311,6 +331,24 @@ def __init__(
311331
mavutil.mavlink.MAV_SEVERITY_INFO, "FGCS connected to aircraft"
312332
)
313333

334+
def _isConnectionCancelRequested(self) -> bool:
335+
return self.connection_cancel_event.is_set()
336+
337+
def requestConnectionCancel(self) -> None:
338+
self.connection_cancel_event.set()
339+
if getattr(self, "paramsController", None) is not None:
340+
self.paramsController.is_requesting_params = False
341+
342+
def _setCancelledConnectionErrorAndCloseMaster(self) -> None:
343+
self.logger.info("Connection cancelled by user")
344+
self.connectionError = "Connection cancelled by user."
345+
if getattr(self, "master", None) is not None:
346+
try:
347+
self.master.close()
348+
except Exception:
349+
self.logger.exception("Failed to close connection during cancellation")
350+
self.master = None
351+
314352
def __getNextLogFilePath(self, line: str) -> str:
315353
return line.split("==NEXT_FILE==")[-1].split("==END==")[0]
316354

@@ -931,8 +969,13 @@ def sendHeartbeatMessage(self) -> None:
931969
if sleep_time > 0:
932970
time.sleep(sleep_time)
933971

972+
master = getattr(self, "master", None)
973+
if master is None:
974+
# Connection teardown can clear master while this thread is winding down.
975+
break
976+
934977
try:
935-
self.master.mav.heartbeat_send(
978+
master.mav.heartbeat_send(
936979
mavutil.mavlink.MAV_TYPE_GCS,
937980
mavutil.mavlink.MAV_AUTOPILOT_INVALID,
938981
0,
@@ -1228,10 +1271,14 @@ def close(self) -> None:
12281271

12291272
self.is_active.clear()
12301273

1231-
self.stopAllDataStreams()
1274+
if getattr(self, "master", None) is not None:
1275+
self.stopAllDataStreams()
12321276
self.stopForwarding()
12331277
self.stopAllThreads()
1234-
self.master.close()
1278+
1279+
if getattr(self, "master", None) is not None:
1280+
self.master.close()
1281+
self.master = None
12351282

12361283
if len(self.log_file_names) == 0:
12371284
self.logger.debug("No logs to save")

radio/app/droneStatus.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
imports this and access it, it can update the values and every other file will also have the update due to passing by reference (probably).
44
"""
55

6+
from threading import Event, Lock
67
from typing import List, Optional
78

8-
99
from app.drone import Drone
1010

1111
correct_ports: List[str] = []
1212
drone: Optional[Drone] = None
1313
state: Optional[str] = None
14+
connection_state_lock: Lock = Lock()
15+
connection_in_progress: bool = False
16+
connect_cancel_event: Optional[Event] = None

radio/app/endpoints/comPorts.py

Lines changed: 87 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import sys
22
import time
3+
from threading import Event
34
from typing import Optional
45

56
from serial.tools import list_ports
@@ -11,9 +12,9 @@
1112
from app.utils import (
1213
droneConnectStatusCb,
1314
droneErrorCb,
15+
fetchingParameterCb,
1416
getComPortNames,
1517
getFlightSwVersionString,
16-
fetchingParameterCb,
1718
)
1819

1920

@@ -78,13 +79,6 @@ def connectToDrone(data: ConnectionDataType) -> None:
7879
Args:
7980
data: The message passed in from the client containing the form sent (select com port, baud rate)
8081
"""
81-
if droneStatus.drone:
82-
droneStatus.drone.logger.warning(
83-
"Attempting a connection to drone when connection is already established."
84-
)
85-
droneStatus.drone.close()
86-
droneStatus.drone = None
87-
8882
connectionType = data.get("connectionType")
8983

9084
if connectionType not in ["serial", "network"]:
@@ -133,45 +127,100 @@ def connectToDrone(data: ConnectionDataType) -> None:
133127
droneStatus.drone = None
134128
return
135129

136-
drone = Drone(
137-
port,
138-
baud=baud,
139-
forwarding_address=forwarding_address,
140-
droneErrorCb=droneErrorCb,
141-
droneDisconnectCb=disconnectFromDrone,
142-
droneConnectStatusCb=droneConnectStatusCb,
143-
linkDebugStatsCb=sendLinkDebugStats,
144-
fetchingParameterCb=fetchingParameterCb,
145-
)
146-
147-
if drone.connectionError is not None:
148-
socketio.emit("connection_error", {"message": drone.connectionError})
149-
droneStatus.drone = None
150-
return
130+
old_drone = None
131+
with droneStatus.connection_state_lock:
132+
if droneStatus.connection_in_progress:
133+
socketio.emit(
134+
"connection_error",
135+
{"message": "A drone connection is already in progress."},
136+
)
137+
return
151138

152-
# Set droneStatus drone to local drone
153-
droneStatus.drone = drone
139+
if droneStatus.drone:
140+
old_drone = droneStatus.drone
141+
droneStatus.drone = None
154142

155-
# Sleeping for buffer time, if errors occur try changing back to 1 second
156-
time.sleep(0.2)
157-
logger.debug("Created drone instance")
158-
socketio.emit(
159-
"connected_to_drone",
160-
{
161-
"aircraft_type": drone.aircraft_type,
162-
"flight_sw_version": getFlightSwVersionString(drone.flight_sw_version),
163-
},
164-
)
143+
cancel_event = Event()
144+
droneStatus.connect_cancel_event = cancel_event
145+
droneStatus.connection_in_progress = True
146+
147+
if old_drone is not None:
148+
old_drone.logger.warning(
149+
"Attempting a connection to drone when connection is already established."
150+
)
151+
old_drone.close()
152+
153+
try:
154+
drone = Drone(
155+
port,
156+
baud=baud,
157+
forwarding_address=forwarding_address,
158+
droneErrorCb=droneErrorCb,
159+
droneDisconnectCb=disconnectFromDrone,
160+
droneConnectStatusCb=droneConnectStatusCb,
161+
linkDebugStatsCb=sendLinkDebugStats,
162+
fetchingParameterCb=fetchingParameterCb,
163+
connectionCancelEvent=cancel_event,
164+
)
165+
166+
if drone.connectionError is not None:
167+
socketio.emit("connection_error", {"message": drone.connectionError})
168+
droneStatus.drone = None
169+
return
170+
171+
# Set droneStatus drone to local drone
172+
droneStatus.drone = drone
173+
174+
# Sleeping for buffer time, if errors occur try changing back to 1 second
175+
time.sleep(0.2)
176+
logger.debug("Created drone instance")
177+
socketio.emit(
178+
"connected_to_drone",
179+
{
180+
"aircraft_type": drone.aircraft_type,
181+
"flight_sw_version": getFlightSwVersionString(drone.flight_sw_version),
182+
},
183+
)
184+
finally:
185+
with droneStatus.connection_state_lock:
186+
droneStatus.connection_in_progress = False
187+
droneStatus.connect_cancel_event = None
188+
189+
190+
@socketio.on("cancel_connect_to_drone")
191+
def cancelConnectToDrone() -> None:
192+
"""Cancel an in-progress drone connection attempt."""
193+
with droneStatus.connection_state_lock:
194+
if droneStatus.connection_in_progress and droneStatus.connect_cancel_event:
195+
logger.info("Connection cancel requested by user")
196+
droneStatus.connect_cancel_event.set()
197+
else:
198+
logger.debug(
199+
"Connection cancel requested, but no connection is in progress"
200+
)
165201

166202

167203
@socketio.on("disconnect_from_drone")
168204
def disconnectFromDrone() -> None:
169205
"""
170206
Disconnect from drone and reset all global variables, send a message to client disconnecting as well
171207
"""
172-
if droneStatus.drone:
173-
droneStatus.drone.close()
174-
droneStatus.drone = None
208+
with droneStatus.connection_state_lock:
209+
if droneStatus.drone:
210+
drone = droneStatus.drone
211+
droneStatus.drone = None
212+
else:
213+
drone = None
214+
215+
if (
216+
drone is None
217+
and droneStatus.connection_in_progress
218+
and droneStatus.connect_cancel_event
219+
):
220+
droneStatus.connect_cancel_event.set()
221+
222+
if drone is not None:
223+
drone.close()
175224

176225
droneStatus.state = None
177226
socketio.emit("disconnected_from_drone")

0 commit comments

Comments
 (0)