From 75bba12ed5ef66b85baf651c0c6078b56ca69ac0 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Fri, 31 Oct 2025 16:58:47 +0000 Subject: [PATCH 1/3] Add functionality to save params to file --- gcs/electron/main.ts | 23 ++++----- gcs/electron/preload.js | 2 +- gcs/src/components/params/paramsToolbar.jsx | 50 +++++++++++++++----- gcs/src/missions.jsx | 2 +- gcs/src/redux/middleware/emitters.js | 9 ++++ gcs/src/redux/middleware/socketMiddleware.js | 12 +++++ gcs/src/redux/slices/paramsSlice.js | 4 +- radio/app/controllers/paramsController.py | 27 +++++++++++ radio/app/endpoints/params.py | 40 ++++++++++++++++ 9 files changed, 141 insertions(+), 28 deletions(-) diff --git a/gcs/electron/main.ts b/gcs/electron/main.ts index 5c23e4a07..ef1f83ce4 100644 --- a/gcs/electron/main.ts +++ b/gcs/electron/main.ts @@ -20,7 +20,7 @@ import openFile, { clearRecentFiles, getRecentFiles, retrieveMessages, - // @ts-expect-error - no types available + // @ts-expect-error - no types available } from "./fla" import registerAboutIPC, { destroyAboutWindow, @@ -477,18 +477,15 @@ app.whenReady().then(() => { // Load Messages on demand ipcMain.handle("fla:get-messages", retrieveMessages) - // Save mission file - ipcMain.handle( - "missions:get-save-mission-file-path", - async (event, options) => { - const window = BrowserWindow.fromWebContents(event.sender) - if (!window) { - throw new Error("No active window found") - } - const result = await dialog.showSaveDialog(window, options) - return result - }, - ) + // Open native save dialog + ipcMain.handle("app:get-save-file-path", async (event, options) => { + const window = BrowserWindow.fromWebContents(event.sender) + if (!window) { + throw new Error("No active window found") + } + const result = await dialog.showSaveDialog(window, options) + return result + }) ipcMain.handle("app:get-node-env", () => app.isPackaged ? "production" : "development", diff --git a/gcs/electron/preload.js b/gcs/electron/preload.js index 6ebabd1db..cc796b6a3 100644 --- a/gcs/electron/preload.js +++ b/gcs/electron/preload.js @@ -7,7 +7,7 @@ const ALLOWED_INVOKE_CHANNELS = [ "fla:get-recent-logs", "fla:clear-recent-logs", "fla:get-messages", - "missions:get-save-mission-file-path", + "app:get-save-file-path", "app:get-node-env", "app:get-version", "app:is-mac", diff --git a/gcs/src/components/params/paramsToolbar.jsx b/gcs/src/components/params/paramsToolbar.jsx index b2384350d..5a9d600c8 100644 --- a/gcs/src/components/params/paramsToolbar.jsx +++ b/gcs/src/components/params/paramsToolbar.jsx @@ -8,6 +8,7 @@ rebooting the autopilot // 3rd party imports import { Button, TextInput, Tooltip } from "@mantine/core" import { + IconDownload, IconEye, IconPencil, IconPower, @@ -23,6 +24,7 @@ const tailwindColors = resolveConfig(tailwindConfig).theme.colors // Redux import { useDispatch, useSelector } from "react-redux" import { + emitExportParamsToFile, emitRebootAutopilot, emitRefreshParams, emitSetMultipleParams, @@ -59,6 +61,29 @@ export default function ParamsToolbar() { dispatch(resetParamState()) } + async function saveParamsToFile() { + const options = { + title: "Save parameters to a file", + filters: [ + { name: "Param File", extensions: ["param"] }, + { name: "All Files", extensions: ["*"] }, + ], + } + + const result = await window.ipcRenderer.invoke( + "app:get-save-file-path", + options, + ) + + if (!result.canceled) { + dispatch( + emitExportParamsToFile({ + filePath: result.filePath, + }), + ) + } + } + return (
dispatch(toggleShowModifiedParams())} color={tailwindColors.orange[600]} > - {" "} - {showModifiedParams ? ( - - ) : ( - - )}{" "} + {showModifiedParams ? : } @@ -95,8 +115,7 @@ export default function ParamsToolbar() { onClick={() => dispatch(emitSetMultipleParams(modifiedParams))} color={tailwindColors.green[600]} > - {" "} - Save params{" "} + Write params + +
) diff --git a/gcs/src/missions.jsx b/gcs/src/missions.jsx index cee16ab03..6a09c14fb 100644 --- a/gcs/src/missions.jsx +++ b/gcs/src/missions.jsx @@ -260,7 +260,7 @@ export default function Missions() { } const result = await window.ipcRenderer.invoke( - "missions:get-save-mission-file-path", + "app:get-save-file-path", options, ) diff --git a/gcs/src/redux/middleware/emitters.js b/gcs/src/redux/middleware/emitters.js index ac05ec2f2..169e6b5fb 100644 --- a/gcs/src/redux/middleware/emitters.js +++ b/gcs/src/redux/middleware/emitters.js @@ -46,6 +46,7 @@ import { showDashboardMissionFetchingNotificationThunk, } from "../slices/missionSlice" import { + emitExportParamsToFile, emitRebootAutopilot, emitRefreshParams, emitSetMultipleParams, @@ -266,6 +267,14 @@ export function handleEmitters(socket, store, action) { emitter: emitSetMultipleParams, callback: () => socket.socket.emit("set_multiple_params", action.payload), }, + { + emitter: emitExportParamsToFile, + callback: () => { + socket.socket.emit("export_params_to_file", { + file_path: action.payload.filePath, + }) + }, + }, /* ========== diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index 666b43be1..b5f206ae5 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -132,6 +132,7 @@ const ParamSpecificSocketEvents = Object.freeze({ onParamRequestUpdate: "param_request_update", onParamSetSuccess: "param_set_success", onParamError: "params_error", + onExportParamsResult: "export_params_result", }) const MissionSpecificSocketEvents = Object.freeze({ @@ -456,6 +457,17 @@ const socketMiddleware = (store) => { store.dispatch(setFetchingVars(false)) }) + socket.socket.on( + ParamSpecificSocketEvents.onExportParamsResult, + (msg) => { + if (msg.success) { + showSuccessNotification(msg.message) + } else { + showErrorNotification(msg.message) + } + }, + ) + socket.socket.on( DroneSpecificSocketEvents.onNavRepositionResult, (msg) => { diff --git a/gcs/src/redux/slices/paramsSlice.js b/gcs/src/redux/slices/paramsSlice.js index eca4c5397..5196b2b86 100644 --- a/gcs/src/redux/slices/paramsSlice.js +++ b/gcs/src/redux/slices/paramsSlice.js @@ -101,6 +101,7 @@ const paramsSlice = createSlice({ emitRebootAutopilot: () => {}, emitRefreshParams: () => {}, emitSetMultipleParams: () => {}, + emitExportParamsToFile: () => {}, }, selectors: { selectRebootData: (state) => state.rebootData, @@ -134,10 +135,11 @@ export const { updateModifiedParamValue, deleteModifiedParam, resetParamState, + setHasFetchedOnce, emitRebootAutopilot, emitRefreshParams, emitSetMultipleParams, - setHasFetchedOnce, + emitExportParamsToFile, } = paramsSlice.actions export const { selectRebootData, diff --git a/radio/app/controllers/paramsController.py b/radio/app/controllers/paramsController.py index 5b4b2597e..cd18bcefb 100644 --- a/radio/app/controllers/paramsController.py +++ b/radio/app/controllers/paramsController.py @@ -327,3 +327,30 @@ def getCachedParam(self, params: str) -> Union[CachedParam, dict]: else: self.drone.logger.error(f"Invalid params type, got {type(params)}") return {} + + def exportParamsToFile(self, file_path: str) -> Response: + """ + Export all cached parameters to a file. + + Args: + file_path (str): The path to the file to export to + + Returns: + Response: The response from the export operation + """ + try: + with open(file_path, "w") as f: + # order params alphabetically by param_id + ordered_params = sorted(self.params, key=lambda k: k["param_id"]) + for param in ordered_params: + f.write(f"{param['param_id'].upper()},{param['param_value']}\n") + return { + "success": True, + "message": f"Parameters exported successfully to {file_path}", + } + except Exception as e: + self.drone.logger.error(f"Failed to export params to file: {e}") + return { + "success": False, + "message": "Failed to export params to file", + } diff --git a/radio/app/endpoints/params.py b/radio/app/endpoints/params.py index 2ffbd81f5..1f1aa6f72 100644 --- a/radio/app/endpoints/params.py +++ b/radio/app/endpoints/params.py @@ -1,8 +1,15 @@ import time from typing import Any, List +from typing_extensions import TypedDict + import app.droneStatus as droneStatus from app import logger, socketio +from app.utils import notConnectedError + + +class ExportParamsFileType(TypedDict): + file_path: str @socketio.on("set_multiple_params") @@ -82,3 +89,36 @@ def refresh_params() -> None: time.sleep(0.2) socketio.emit("params", droneStatus.drone.paramsController.params) + + +@socketio.on("export_params_to_file") +def export_params_to_file(data: ExportParamsFileType) -> None: + """ + Export parameters to a file. + + Args: + data: The data from the client containing the file path. + """ + if droneStatus.state != "params": + socketio.emit( + "params_error", + {"message": "You must be on the params screen to export parameters."}, + ) + logger.debug(f"Current state: {droneStatus.state}") + return + + if not droneStatus.drone: + return notConnectedError(action="export params to file") + + file_path = data.get("file_path", None) + if not file_path: + socketio.emit( + "export_params_result", + {"success": False, "message": "No file path provided."}, + ) + logger.error("No file path provided for exporting parameters.") + return + + result = droneStatus.drone.paramsController.exportParamsToFile(file_path) + + socketio.emit("export_params_result", result) From c6395d32736032274650476c0447a35f14530887 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Fri, 31 Oct 2025 17:03:24 +0000 Subject: [PATCH 2/3] Fix format issue --- gcs/electron/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcs/electron/main.ts b/gcs/electron/main.ts index ef1f83ce4..ebb17a4ac 100644 --- a/gcs/electron/main.ts +++ b/gcs/electron/main.ts @@ -20,7 +20,7 @@ import openFile, { clearRecentFiles, getRecentFiles, retrieveMessages, - // @ts-expect-error - no types available + // @ts-expect-error - no types available } from "./fla" import registerAboutIPC, { destroyAboutWindow, From 09ded0d0938a0227dd1629997ea8a95b19c19626 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Fri, 31 Oct 2025 17:04:50 +0000 Subject: [PATCH 3/3] Address copilot review comments --- radio/app/controllers/paramsController.py | 2 +- radio/app/endpoints/params.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/radio/app/controllers/paramsController.py b/radio/app/controllers/paramsController.py index cd18bcefb..9e94db195 100644 --- a/radio/app/controllers/paramsController.py +++ b/radio/app/controllers/paramsController.py @@ -352,5 +352,5 @@ def exportParamsToFile(self, file_path: str) -> Response: self.drone.logger.error(f"Failed to export params to file: {e}") return { "success": False, - "message": "Failed to export params to file", + "message": f"Failed to export params to file: {e}", } diff --git a/radio/app/endpoints/params.py b/radio/app/endpoints/params.py index 1f1aa6f72..a45df5ae6 100644 --- a/radio/app/endpoints/params.py +++ b/radio/app/endpoints/params.py @@ -108,7 +108,8 @@ def export_params_to_file(data: ExportParamsFileType) -> None: return if not droneStatus.drone: - return notConnectedError(action="export params to file") + notConnectedError(action="export params to file") + return file_path = data.get("file_path", None) if not file_path: