From 79b25d9a18744bf9ae40f32e26d73d7b74393077 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Fri, 19 Sep 2025 13:03:57 +0100 Subject: [PATCH 1/6] Fix reboot connection and modal --- gcs/src/components/params/paramsToolbar.jsx | 1 + gcs/src/params.jsx | 4 ++-- gcs/src/redux/middleware/emitters.js | 6 +++++- gcs/src/redux/middleware/socketMiddleware.js | 18 ++++++++++++---- gcs/src/redux/slices/droneConnectionSlice.js | 10 +++++++++ radio/app.py | 4 ++-- radio/app/drone.py | 2 +- radio/app/endpoints/autopilot.py | 22 ++++++++++++++++---- 8 files changed, 53 insertions(+), 14 deletions(-) diff --git a/gcs/src/components/params/paramsToolbar.jsx b/gcs/src/components/params/paramsToolbar.jsx index b2384350d..81a27b24c 100644 --- a/gcs/src/components/params/paramsToolbar.jsx +++ b/gcs/src/components/params/paramsToolbar.jsx @@ -55,6 +55,7 @@ export default function ParamsToolbar() { function rebootCallback() { dispatch(emitRebootAutopilot()) + console.log("Opening reboot modal from reboot callback") dispatch(setAutoPilotRebootModalOpen(true)) dispatch(resetParamState()) } diff --git a/gcs/src/params.jsx b/gcs/src/params.jsx index 6c2578025..e34ddb75b 100644 --- a/gcs/src/params.jsx +++ b/gcs/src/params.jsx @@ -81,10 +81,10 @@ export default function Params() { return ( + + {connected ? ( <> - - {fetchingVars && ( socket.socket.emit("set_state", action.payload), + callback: () => { + store.dispatch(setState(action.payload)) // Update Redux state + socket.socket.emit("set_state", action.payload) // Emit to socket + }, }, { emitter: emitGetHomePosition, diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index 5f57ec1e5..73c778f15 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -261,11 +261,18 @@ const socketMiddleware = (store) => { store.dispatch(setConnecting(false)) store.dispatch(setConnectionModal(false)) - store.dispatch(emitSetState({ state: "dashboard" })) - store.dispatch(emitGetHomePosition()) // use actual home position - if (msg.aircraft_type === 1) { - store.dispatch(emitGetLoiterRadius()) + const currentState = store.getState().droneConnection + store.dispatch(emitSetState(currentState)) + + if (["dashboard", "missions"].includes(currentState.state)) { + store.dispatch(emitGetHomePosition()) // fetch the actual home position of the drone + if (msg.aircraft_type === 1) { + store.dispatch(emitGetLoiterRadius()) + } } + + store.dispatch(setRebootData({})) + store.dispatch(setAutoPilotRebootModalOpen(false)) }) // Link stats @@ -319,8 +326,11 @@ const socketMiddleware = (store) => { socket.socket.on(ParamSpecificSocketEvents.onRebootAutopilot, (msg) => { store.dispatch(setRebootData(msg)) + console.log("Reboot data received:", msg) if (msg.success) { store.dispatch(setAutoPilotRebootModalOpen(false)) + queueSuccessNotification(msg.message) + store.dispatch(setRebootData({})) } }) diff --git a/gcs/src/redux/slices/droneConnectionSlice.js b/gcs/src/redux/slices/droneConnectionSlice.js index dc0aa66be..f5619592a 100644 --- a/gcs/src/redux/slices/droneConnectionSlice.js +++ b/gcs/src/redux/slices/droneConnectionSlice.js @@ -29,6 +29,8 @@ const initialState = { network_type: "tcp", // local ip: "127.0.0.1", // local port: "5760", // local + + state: "dashboard", } const droneConnectionSlice = createSlice({ @@ -101,6 +103,11 @@ const droneConnectionSlice = createSlice({ state.wireless = action.payload } }, + setState: (state, action) => { + if (action.payload !== state.state) { + state.state = action.payload + } + }, // Emits emitIsConnectedToDrone: () => {}, @@ -128,6 +135,7 @@ const droneConnectionSlice = createSlice({ selectConnectionModal: (state) => state.connection_modal, selectConnectionStatus: (state) => state.connection_status, selectWireless: (state) => state.wireless, + selectState: (state) => state.state, }, }) @@ -146,6 +154,7 @@ export const { setConnectionModal, setConnectionStatus, setWireless, + setState, // Emitters emitIsConnectedToDrone, @@ -171,6 +180,7 @@ export const { selectConnectionModal, selectConnectionStatus, selectWireless, + selectState, } = droneConnectionSlice.selectors export default droneConnectionSlice diff --git a/radio/app.py b/radio/app.py index 218c895d7..5480f0c62 100644 --- a/radio/app.py +++ b/radio/app.py @@ -1,7 +1,8 @@ import os +from pathlib import Path + import app.droneStatus as droneStatus from app import create_app, socketio -from pathlib import Path from dotenv import load_dotenv app = create_app(debug=True) @@ -20,7 +21,6 @@ host = "127.0.0.1" print("Starting backend.") - print(host) socketio.run(app, allow_unsafe_werkzeug=True, host=host, port=port) if droneStatus.drone: droneStatus.drone.close() diff --git a/radio/app/drone.py b/radio/app/drone.py index f03453ecf..dd9aa9b7f 100644 --- a/radio/app/drone.py +++ b/radio/app/drone.py @@ -99,7 +99,7 @@ def __init__( self.connectionError: Optional[str] = None - self.logger.debug("Trying to setup master") + self.logger.debug(f"Trying to setup master with port {port} and baud {baud}") if not Drone.checkBaudrateValid(baud): self.connectionError = ( diff --git a/radio/app/endpoints/autopilot.py b/radio/app/endpoints/autopilot.py index 9b4bd9a24..6641c8ef6 100644 --- a/radio/app/endpoints/autopilot.py +++ b/radio/app/endpoints/autopilot.py @@ -8,8 +8,10 @@ @socketio.on("reboot_autopilot") def rebootAutopilot() -> None: """ - Attempt to reboot the autopilot, this will try to reconnect to the drone 3 times before stopping. This will also stop if the port - is not open for 10 seconds. + Attempt to reboot the autopilot, this will try to reconnect to the drone 3 times before stopping. + + Note: If SITL is running and you are connected via TCP 5763 then rebooting does not work as expected. + Use TCP 5760 instead. """ if not droneStatus.drone: return @@ -20,12 +22,20 @@ def rebootAutopilot() -> None: droneErrorCb = droneStatus.drone.droneErrorCb droneDisconnectCb = droneStatus.drone.droneDisconnectCb droneConnectStatusCb = droneStatus.drone.droneConnectStatusCb + linkDebugStatsCb = droneStatus.drone.linkDebugStatsCb + socketio.emit("disconnected_from_drone") + droneStatus.drone.rebootAutopilot() - while droneStatus.drone is not None and droneStatus.drone.is_active.is_set(): + while droneStatus.drone.is_active.is_set(): + print("Waiting for drone to disconnect...") time.sleep(0.05) + droneStatus.drone = None + + time.sleep(1.5) # Wait for the port to be released and let the autopilot reboot + tries = 0 while tries < 3: droneStatus.drone = Drone( @@ -35,6 +45,7 @@ def rebootAutopilot() -> None: droneErrorCb=droneErrorCb, droneDisconnectCb=droneDisconnectCb, droneConnectStatusCb=droneConnectStatusCb, + linkDebugStatsCb=linkDebugStatsCb, ) if droneStatus.drone.connectionError: tries += 1 @@ -42,6 +53,7 @@ def rebootAutopilot() -> None: else: break else: + droneStatus.drone = None logger.error("Could not reconnect to drone after 3 attempts.") socketio.emit( "reboot_autopilot", @@ -53,7 +65,9 @@ def rebootAutopilot() -> None: return time.sleep(1) - socketio.emit("connected_to_drone") + socketio.emit( + "connected_to_drone", {"aircraft_type": droneStatus.drone.aircraft_type} + ) logger.info("Rebooted autopilot successfully.") socketio.emit( "reboot_autopilot", From e74d9d37643e982376243f8e3095bde08fbba5fb Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Fri, 19 Sep 2025 13:43:42 +0100 Subject: [PATCH 2/6] Fix params not being fetched after reboot --- radio/app/controllers/paramsController.py | 24 ++++++++++++++++------- radio/tests/test_autopilot.py | 1 - 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/radio/app/controllers/paramsController.py b/radio/app/controllers/paramsController.py index a0f9149c6..11ade1afe 100644 --- a/radio/app/controllers/paramsController.py +++ b/radio/app/controllers/paramsController.py @@ -30,7 +30,7 @@ def __init__(self, drone: Drone) -> None: self.getAllParamsThread: Optional[Thread] = None @sendingCommandLock - def getSingleParam(self, param_name: str, timeout: Optional[float] = 5) -> Response: + def getSingleParam(self, param_name: str, timeout: Optional[float] = 3) -> Response: """ Gets a specific parameter value. @@ -53,18 +53,28 @@ def getSingleParam(self, param_name: str, timeout: Optional[float] = 5) -> Respo ) try: - response = self.drone.master.recv_match( - type="PARAM_VALUE", blocking=True, timeout=timeout - ) + start_time = time.time() + response = None + while time.time() - start_time < timeout: + msg = self.drone.master.recv_match( + type="PARAM_VALUE", blocking=True, timeout=0.5 + ) + + if msg is None: + continue - if response and response.param_id == param_name: - self.drone.is_listening = True + if msg.param_id == param_name: + response = msg + break + + self.drone.is_listening = True + if response: return { "success": True, "data": response, } else: - self.drone.is_listening = True + self.drone.logger.error(f"Did not receive {param_name} within timeout") return { "success": False, "message": f"{failure_message}, timed out", diff --git a/radio/tests/test_autopilot.py b/radio/tests/test_autopilot.py index c76a52405..816b6be04 100644 --- a/radio/tests/test_autopilot.py +++ b/radio/tests/test_autopilot.py @@ -3,7 +3,6 @@ from . import falcon_test -# @pytest.mark.skip(reason="Test fails due to reboot_autopilot not functional") @falcon_test() def test_reboot_success(socketio_client: SocketIOTestClient): """ From eed3f090460790735f142be23a761a291e748269 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Fri, 19 Sep 2025 13:44:20 +0100 Subject: [PATCH 3/6] Remove console.logs --- gcs/src/components/params/paramsToolbar.jsx | 1 - gcs/src/redux/middleware/socketMiddleware.js | 1 - 2 files changed, 2 deletions(-) diff --git a/gcs/src/components/params/paramsToolbar.jsx b/gcs/src/components/params/paramsToolbar.jsx index 81a27b24c..b2384350d 100644 --- a/gcs/src/components/params/paramsToolbar.jsx +++ b/gcs/src/components/params/paramsToolbar.jsx @@ -55,7 +55,6 @@ export default function ParamsToolbar() { function rebootCallback() { dispatch(emitRebootAutopilot()) - console.log("Opening reboot modal from reboot callback") dispatch(setAutoPilotRebootModalOpen(true)) dispatch(resetParamState()) } diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index 73c778f15..d30ce8bae 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -326,7 +326,6 @@ const socketMiddleware = (store) => { socket.socket.on(ParamSpecificSocketEvents.onRebootAutopilot, (msg) => { store.dispatch(setRebootData(msg)) - console.log("Reboot data received:", msg) if (msg.success) { store.dispatch(setAutoPilotRebootModalOpen(false)) queueSuccessNotification(msg.message) From ede3a36be53f92b97e58b0a2a8a60ba43bd48742 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Fri, 19 Sep 2025 13:49:36 +0100 Subject: [PATCH 4/6] Fix mypy issue. address copilot review comments --- radio/app/controllers/paramsController.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/radio/app/controllers/paramsController.py b/radio/app/controllers/paramsController.py index 11ade1afe..8c403ba39 100644 --- a/radio/app/controllers/paramsController.py +++ b/radio/app/controllers/paramsController.py @@ -30,7 +30,7 @@ def __init__(self, drone: Drone) -> None: self.getAllParamsThread: Optional[Thread] = None @sendingCommandLock - def getSingleParam(self, param_name: str, timeout: Optional[float] = 3) -> Response: + def getSingleParam(self, param_name: str, timeout: float = 3) -> Response: """ Gets a specific parameter value. @@ -56,8 +56,9 @@ def getSingleParam(self, param_name: str, timeout: Optional[float] = 3) -> Respo start_time = time.time() response = None while time.time() - start_time < timeout: + # timeout of 0.2 to recv_match to allow checking the overall timeout and not block forever msg = self.drone.master.recv_match( - type="PARAM_VALUE", blocking=True, timeout=0.5 + type="PARAM_VALUE", blocking=True, timeout=0.2 ) if msg is None: From 0ceb198c2ca12b3bce5ab3b2ea2335b26df9717a Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Fri, 19 Sep 2025 14:02:26 +0100 Subject: [PATCH 5/6] Fix tests --- radio/tests/helpers.py | 19 +++++++++++-------- radio/tests/test_FlightModesController.py | 8 ++++---- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/radio/tests/helpers.py b/radio/tests/helpers.py index dc00c69c9..db0f571dd 100644 --- a/radio/tests/helpers.py +++ b/radio/tests/helpers.py @@ -1,8 +1,9 @@ -import pytest from typing import Optional, Union -from serial.serialutil import SerialException +import pytest from app import droneStatus, logger +from serial.serialutil import SerialException + from . import socketio_client @@ -121,17 +122,17 @@ def __exit__(self, type, value, traceback) -> None: droneStatus.drone.master.recv_match = self.old_recv -class RecvMsgReturnsFalse: +class RecvMsgReturnsNone: @staticmethod def recv_match_false( condition=None, type=None, blocking=False, timeout=None - ) -> bool: - return False + ) -> None: + return None def __enter__(self) -> None: if droneStatus.drone is not None: self.old_recv = droneStatus.drone.master.recv_match - droneStatus.drone.master.recv_match = RecvMsgReturnsFalse.recv_match_false + droneStatus.drone.master.recv_match = RecvMsgReturnsNone.recv_match_false def __exit__(self, type, value, traceback) -> None: if droneStatus.drone is not None: @@ -167,8 +168,10 @@ def send_and_recieve(endpoint: str, args: Optional[Union[dict, str]] = None) -> dict The data recieved from the client """ - socketio_client.emit(endpoint, args) if args is not None else socketio_client.emit( - endpoint + ( + socketio_client.emit(endpoint, args) + if args is not None + else socketio_client.emit(endpoint) ) return socketio_client.get_received()[0]["args"][0] diff --git a/radio/tests/test_FlightModesController.py b/radio/tests/test_FlightModesController.py index 5e6d43c60..8f063445a 100644 --- a/radio/tests/test_FlightModesController.py +++ b/radio/tests/test_FlightModesController.py @@ -4,7 +4,7 @@ from flask_socketio.test_client import SocketIOTestClient from . import falcon_test -from .helpers import FakeTCP, ParamSetTimeout, RecvMsgReturnsFalse, SetAircraftType +from .helpers import FakeTCP, ParamSetTimeout, RecvMsgReturnsNone, SetAircraftType @pytest.fixture(scope="module", autouse=True) @@ -41,7 +41,7 @@ def test_getFlightModes_success(client: SocketIOTestClient, droneStatus): @falcon_test(pass_drone_status=True) def test_getFlightModes_failure(client: SocketIOTestClient, droneStatus): - with RecvMsgReturnsFalse(): + with RecvMsgReturnsNone(): droneStatus.drone.flightModesController.getFlightModes() assert len(droneStatus.drone.flightModesController.flight_modes) == 6 for items in droneStatus.drone.flightModesController.flight_modes: @@ -50,7 +50,7 @@ def test_getFlightModes_failure(client: SocketIOTestClient, droneStatus): @falcon_test(pass_drone_status=True) def test_getFlightModeChannel_failure(client: SocketIOTestClient, droneStatus): - with RecvMsgReturnsFalse(): + with RecvMsgReturnsNone(): droneStatus.drone.flightModesController.getFlightModeChannel() assert droneStatus.drone.flightModesController.flight_mode_channel == "UNKNOWN" @@ -68,7 +68,7 @@ def test_setCurrentFlightMode(client: SocketIOTestClient, droneStatus): assert response.get("success") is False assert response.get("message") == "Could not set flight mode, serial exception" - with RecvMsgReturnsFalse(): + with RecvMsgReturnsNone(): response = droneStatus.drone.flightModesController.setCurrentFlightMode(1) assert response.get("success") is False assert response.get("message") == "Could not set flight mode" From 8e980604ddedaf26b7d9ba0565d360d36339aea1 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Fri, 19 Sep 2025 15:58:56 +0100 Subject: [PATCH 6/6] Make changes --- gcs/src/redux/middleware/emitters.js | 4 ++-- gcs/src/redux/middleware/socketMiddleware.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gcs/src/redux/middleware/emitters.js b/gcs/src/redux/middleware/emitters.js index e4b9d5959..5a7df3ee2 100644 --- a/gcs/src/redux/middleware/emitters.js +++ b/gcs/src/redux/middleware/emitters.js @@ -49,8 +49,8 @@ export function handleEmitters(socket, store, action) { { emitter: emitSetState, callback: () => { - store.dispatch(setState(action.payload)) // Update Redux state - socket.socket.emit("set_state", action.payload) // Emit to socket + store.dispatch(setState(action.payload)) + socket.socket.emit("set_state", action.payload) }, }, { diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index d30ce8bae..3ef4e831f 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -328,7 +328,7 @@ const socketMiddleware = (store) => { store.dispatch(setRebootData(msg)) if (msg.success) { store.dispatch(setAutoPilotRebootModalOpen(false)) - queueSuccessNotification(msg.message) + store.dispatch(queueSuccessNotification(msg.message)) store.dispatch(setRebootData({})) } })