899 add a failsafes config page#1021
Conversation
…tion this isn't how enum's are supposed to be used in the first place (consistently using `.value`) but this makes the codebase clearer at least until a proper refactor can take place
There was a problem hiding this comment.
Pull request overview
This PR adds a failsafes configuration page to the Ground Control Station (GCS), allowing users to configure battery, radio, GCS, and EKF failsafe parameters for both copter and plane aircraft types. It also refactors hardcoded aircraft type values throughout the codebase to use the VehicleType enum for improved maintainability.
Changes:
- Added new backend endpoint
/endpoints/failsafe.pyto handle failsafe parameter retrieval and configuration - Updated tests and multiple controller files to use
VehicleTypeenum instead of magic numbers - Added Redux state management, socket middleware, and emitters for failsafe configuration in the frontend
- Created a new React component for the failsafe configuration UI with vehicle-specific parameters
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 19 comments.
Show a summary per file
| File | Description |
|---|---|
| radio/app/endpoints/failsafe.py | New endpoint implementing get/set operations for failsafe parameters with vehicle-type-specific parameter lists |
| radio/app/endpoints/init.py | Imports the new failsafe endpoint module |
| radio/tests/conftest.py | Updated aircraft type checks to use VehicleType enum instead of magic numbers |
| radio/app/controllers/navController.py | Refactored to use VehicleType.FIXED_WING.value instead of hardcoded 1 |
| radio/app/controllers/frameController.py | Refactored to use VehicleType.FIXED_WING.value instead of hardcoded 1 |
| radio/app/controllers/flightModesController.py | Refactored to use VehicleType.FIXED_WING.value instead of hardcoded 1 |
| gcs/src/redux/slices/configSlice.js | Added failsafe state management including config data, refresh state, actions, and emitters |
| gcs/src/redux/middleware/socketMiddleware.js | Added socket event handlers for failsafe config retrieval and parameter updates |
| gcs/src/redux/middleware/emitters.js | Added emitters for failsafe config operations |
| gcs/src/config.jsx | Added Failsafes tab and panel to the config page |
| gcs/src/components/config/failsafe.jsx | New React component rendering failsafe configuration UI with vehicle-specific sections |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if failsafe_params[param]["success"]: | ||
| failsafe_config[param] = failsafe_params[param]["data"].param_value | ||
| else: | ||
| failsafe_config[param] = "UNKNOWN" |
There was a problem hiding this comment.
When a parameter fails to load, it's set to "UNKNOWN" (a string), but the NumberInput components expect numeric values. This could cause the input to malfunction if any parameter fails to load. Consider handling the "UNKNOWN" case by either setting a default numeric value (e.g., 0 or undefined) or disabling the input with a visual indicator that the parameter couldn't be loaded.
| failsafe_config[param] = "UNKNOWN" | |
| failsafe_config[param] = None |
|
some tests pass with Copter simulator, the other tests pass on the Plane simulator |
|
Also follow convention and make a failsafe controller which does all failsafe parameter management etc |
|
I don't think a controller is needed as there isn't any logic behind failsafes, it's just a more convenient way to set relevant parameters so it uses the params controller instead |
|
I've done everything else you said but for the battery failsafe stage 1 and 2 that was taken straight from the docs, how would you want it presented instead @1Blademaster ? |
A controller is needed; it makes it organised to understand what is responsible for what. It also will remove the list of variables at the top of the current failsafe route definition file. It will also allow us to do more things in the future, may not be applicable to failsafes but it doesn't mean it shouldn't follow procedure. There is also some logic that is needed, e.g. if there is low voltage failsafe set then critical voltage failsafe cannot be a higher value than the low voltage. Etc. |
"Low" and "Critical" are just fine and it makes it clear what params it's changing as well. |
|
not sure why the other tests are failing but ready for review |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Gets the gripper related parameters from the drone. | ||
| """ | ||
| self.drone.logger.debug("Fetching gripper parameters") |
There was a problem hiding this comment.
getFailsafeParams docstring and log messages look copy/pasted from the gripper controller (mentions "gripper" and "from the drone"), but this method is reading from the cached params controller. This makes debugging confusing; please update the wording to refer to failsafe params and clarify it's reading from cache.
| Gets the gripper related parameters from the drone. | |
| """ | |
| self.drone.logger.debug("Fetching gripper parameters") | |
| Loads the failsafe-related parameters via the cached params controller. | |
| """ | |
| self.drone.logger.debug("Refreshing cached failsafe parameters") |
| onChange={(value) => | ||
| debouncedUpdate("THR_FAILSAFE", Number(value)) |
There was a problem hiding this comment.
Mantine Switch onChange receives an event, but this handler treats the argument as a boolean and does Number(value), which will yield NaN and send an invalid parameter value. Use event.currentTarget.checked (and map to 1/0 if the param is numeric) instead.
| onChange={(value) => | |
| debouncedUpdate("THR_FAILSAFE", Number(value)) | |
| onChange={(event) => | |
| debouncedUpdate( | |
| "THR_FAILSAFE", | |
| event.currentTarget.checked ? 1 : 0, | |
| ) |
| value={failsafeConfig.BATT_LOW_VOLT} | ||
| onChange={(value) => | ||
| debouncedUpdate("BATT_LOW_VOLT", Number(value)) | ||
| } | ||
| disabled={failsafeConfig.BATT_LOW_VOLT == undefined} |
There was a problem hiding this comment.
NumberInput onChange can provide ""/null during editing; coercing with Number(value) will turn empty input into 0 (or NaN) and can unintentionally push param updates. Consider guarding like the gripper config does (ignore empty/NaN) or using a shared sanitisation helper.
| <Select | ||
| label="Failsafe Low Action" | ||
| allowDeselect={false} | ||
| value={String(failsafeConfig.BATT_FS_LOW_ACT)} | ||
| data={Object.keys(paramDefs.BATT_FS_LOW_ACT.Values).map( | ||
| (key) => ({ | ||
| value: `${key}`, | ||
| label: `${key}: ${paramDefs.BATT_FS_LOW_ACT.Values[key]}`, | ||
| }), | ||
| )} |
There was a problem hiding this comment.
This Select assumes paramDefs.BATT_FS_LOW_ACT is always present and has a Values map. If the loaded param definitions are missing this param for a given vehicle/version, this will throw at render time. Please use optional chaining / fallback (e.g., paramDefs.BATT_FS_LOW_ACT?.Values) and handle the empty-options case; the same pattern applies to the other paramDefs.<PARAM>.Values usages in this component.
| if (msg.success) { | ||
| showSuccessNotification(msg.message) | ||
| store.dispatch( | ||
| updateFailsafeConfigParam({ | ||
| param_id: msg.param_id, | ||
| value: msg.value, | ||
| }), | ||
| ) |
There was a problem hiding this comment.
Can you also update the params dict in redux when a param is set successfully so it updates on the params page as well; look at the flight modes config to see how that's done.
| Gets the gripper related parameters from the drone. | ||
| """ | ||
| self.drone.logger.debug("Fetching gripper parameters") |
|
|
||
| return self.drone.paramsController.setParam(param_id, value, param_type) | ||
|
|
||
| def getFailsafeParams(self) -> None: |
There was a problem hiding this comment.
What's the difference between getFailsafeParams and getConfig? They look like they do the same thing?
| success = droneStatus.drone.failsafeController.setFailsafeParam(param_id, value) | ||
| if success: | ||
| result = { | ||
| "success": True, | ||
| "message": f"Parameter {param_id} successfully set to {value}.", | ||
| "param_id": param_id, | ||
| "value": value, | ||
| } | ||
| else: | ||
| result = { | ||
| "success": False, | ||
| "message": f"Failed to set parameter {param_id} to {value}.", | ||
| } | ||
| socketio.emit("set_failsafe_param_result", result) |
There was a problem hiding this comment.
Result object should be returned by the controller, not generated inside this endpoint.
|
|
||
| socketio.emit( | ||
| "failsafe_config", | ||
| {"params": failsafe_config}, |
There was a problem hiding this comment.
Responses need to be generated inside the controller. Also needs to follow the Response type.
There was a problem hiding this comment.
Can you separate these tests so each test is it's own function and is only testing one thing? I want to do that in all the tests as a future ticket so would help doing it now.

Please double check this is how failsafes are supposed to work (I've never used them and am going purely off terrible documentation)
Also any comments on the loading of params when the failsafe tab is clicked would be appreciated (you'll see what I mean)