From b143d7a03e6b5dcd267818d9056dc9538598075e Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Sat, 4 Oct 2025 21:14:49 +0100 Subject: [PATCH 1/6] Add basic EKF display to dashboard telemetry display --- gcs/src/components/dashboard/ekfDisplay.jsx | 34 +++++++++ gcs/src/components/dashboard/telemetry.jsx | 76 ++++++++++++-------- gcs/src/helpers/mavlinkConstants.js | 28 ++++++++ gcs/src/redux/middleware/socketMiddleware.js | 14 ++++ gcs/src/redux/slices/droneInfoSlice.js | 53 +++++++++++++- radio/app/endpoints/states.py | 1 + 6 files changed, 175 insertions(+), 31 deletions(-) create mode 100644 gcs/src/components/dashboard/ekfDisplay.jsx diff --git a/gcs/src/components/dashboard/ekfDisplay.jsx b/gcs/src/components/dashboard/ekfDisplay.jsx new file mode 100644 index 000000000..60e7b477e --- /dev/null +++ b/gcs/src/components/dashboard/ekfDisplay.jsx @@ -0,0 +1,34 @@ +import { useMemo } from "react" +import { useSelector } from "react-redux" +import { selectEkfCalculatedStatus } from "../../redux/slices/droneInfoSlice" + +// Tailwind styling +import resolveConfig from "tailwindcss/resolveConfig" +import tailwindConfig from "../../../tailwind.config" +const tailwindColors = resolveConfig(tailwindConfig).theme.colors + +const RED = tailwindColors.red[500] +const YELLOW = tailwindColors.yellow[500] + +export default function EkfDisplay({ telemetryFontSize }) { + const ekfCalculatedStatus = useSelector(selectEkfCalculatedStatus) + + const textColour = useMemo(() => { + if (ekfCalculatedStatus > 0.8) return RED + if (ekfCalculatedStatus > 0.5) return YELLOW + return "" + }, [ekfCalculatedStatus]) + + return ( +
+ EKF +
+ ) +} diff --git a/gcs/src/components/dashboard/telemetry.jsx b/gcs/src/components/dashboard/telemetry.jsx index 9e54356ee..49e52b293 100644 --- a/gcs/src/components/dashboard/telemetry.jsx +++ b/gcs/src/components/dashboard/telemetry.jsx @@ -4,9 +4,9 @@ */ // Custom Components +import { MAV_STATE } from "../../helpers/mavlinkConstants" import { AttitudeIndicator, HeadingIndicator } from "./indicator" import TelemetryValueDisplay from "./telemetryValueDisplay" -import { MAV_STATE } from "../../helpers/mavlinkConstants" // Redux import { useSelector } from "react-redux" @@ -21,6 +21,7 @@ import { selectPrearmEnabled, selectTelemetry, } from "../../redux/slices/droneInfoSlice" +import EkfDisplay from "./ekfDisplay" export default function TelemetrySection({ calcIndicatorSize, @@ -175,36 +176,51 @@ export default function TelemetrySection({ - {/* Battery information */} -
-

BATTERY

- - - - {batteryData.map((battery) => ( - - - - - - - ))} - -
BATTERY{battery.id} - {(battery.voltages ? battery.voltages[0] / 1000 : 0).toFixed( - 2, - )} - V - - {(battery.current_battery - ? battery.current_battery / 100 - : 0 - ).toFixed(2)} - A - - {battery.battery_remaining ? battery.battery_remaining : 0}% -
+ {/* EKF and VIBE labels */} +
+ +
+ VIBE +
+ + {/* Battery information */} + {batteryData.length > 0 && ( +
+ + + {batteryData.map((battery) => ( + + + + + + + ))} + +
BATTERY{battery.id} + {(battery.voltages + ? battery.voltages[0] / 1000 + : 0 + ).toFixed(2)} + V + + {(battery.current_battery + ? battery.current_battery / 100 + : 0 + ).toFixed(2)} + A + + {battery.battery_remaining ? battery.battery_remaining : 0}% +
+
+ )}
) } diff --git a/gcs/src/helpers/mavlinkConstants.js b/gcs/src/helpers/mavlinkConstants.js index 72c74fff3..7a9acc625 100644 --- a/gcs/src/helpers/mavlinkConstants.js +++ b/gcs/src/helpers/mavlinkConstants.js @@ -478,3 +478,31 @@ export const MAV_FRAME_LIST = { 22: "MAV_FRAME_LOCAL_FLU", 23: "MAV_FRAME_ENUM_END", } + +export const EKF_STATUS_FLAGS = { + 1: "EKF_ATTITUDE", + 2: "EKF_VELOCITY_HORIZ", + 4: "EKF_VELOCITY_VERT", + 8: "EKF_POS_HORIZ_REL", + 16: "EKF_POS_HORIZ_ABS", + 32: "EKF_POS_VERT_ABS", + 64: "EKF_POS_VERT_AGL", + 128: "EKF_CONST_POS_MODE", + 256: "EKF_PRED_POS_HORIZ_REL", + 512: "EKF_PRED_POS_HORIZ_ABS", + 1024: "EKF_UNINITIALIZED", + 32768: "EKF_GPS_GLITCHING", +} + +export function getActiveEKFFlags(statusValue) { + const activeFlags = [] + + for (const [flag, name] of Object.entries(EKF_STATUS_FLAGS)) { + const flagValue = parseInt(flag) + if (statusValue & flagValue) { + activeFlags.push(name) + } + } + + return activeFlags +} diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index f9aa83210..56af81305 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -53,6 +53,7 @@ import { setAttitudeData, setBatteryData, setDroneAircraftType, + setEkfStatusReportData, setExtraData, setGpsData, setGpsRawIntData, @@ -206,6 +207,19 @@ const socketMiddleware = (store) => { case "BATTERY_STATUS": store.dispatch(setBatteryData(msg)) break + case "EKF_STATUS_REPORT": + store.dispatch( + setEkfStatusReportData({ + airspeed_variance: msg.airspeed_variance ?? 0, + compass_variance: msg.compass_variance, + pos_horiz_variance: msg.pos_horiz_variance, + pos_vert_variance: msg.pos_vert_variance, + terrain_alt_variance: msg.terrain_alt_variance, + velocity_variance: msg.velocity_variance, + flags: msg.flags, + }), + ) + break } } diff --git a/gcs/src/redux/slices/droneInfoSlice.js b/gcs/src/redux/slices/droneInfoSlice.js index aa9888cb5..aafc8b1e7 100644 --- a/gcs/src/redux/slices/droneInfoSlice.js +++ b/gcs/src/redux/slices/droneInfoSlice.js @@ -1,6 +1,10 @@ import { createSelector, createSlice } from "@reduxjs/toolkit" import { defaultDataMessages } from "../../helpers/dashboardDefaultDataMessages" -import { getFlightModeMap, MAV_STATE } from "../../helpers/mavlinkConstants" +import { + getActiveEKFFlags, + getFlightModeMap, + MAV_STATE, +} from "../../helpers/mavlinkConstants" const droneInfoSlice = createSlice({ name: "droneInfo", @@ -53,6 +57,16 @@ const droneInfoSlice = createSlice({ lon: 0, // Stored in coords not int alt: 0, }, + ekfStatusReportData: { + airspeed_variance: 0, + compass_variance: 0, + pos_horiz_variance: 0, + pos_vert_variance: 0, + terrain_alt_variance: 0, + velocity_variance: 0, + flags: 0, + }, + ekfCalculatedStatus: 0, graphs: { selectedGraphs: { graph_a: null, @@ -170,6 +184,38 @@ const droneInfoSlice = createSlice({ setGuidedModePinData: (state, action) => { state.guidedModePinData = action.payload }, + setEkfStatusReportData: (state, action) => { + state.ekfStatusReportData = action.payload + + // Calculate EKF status value ranging from 0-1 + // https://github.com/ArduPilot/MissionPlanner/blob/4d441bd4b1dbc08adce4d8b26e078e93760da3a7/ExtLibs/ArduPilot/CurrentState.cs#L2645-L2647 + const vel = state.ekfStatusReportData.velocity_variance + const comp = state.ekfStatusReportData.compass_variance + const posHor = state.ekfStatusReportData.pos_horiz_variance + const posVer = state.ekfStatusReportData.pos_vert_variance + const terAlt = state.ekfStatusReportData.terrain_alt_variance + state.ekfCalculatedStatus = Math.max( + vel, + Math.max(comp, Math.max(posHor, Math.max(posVer, terAlt))), + ) + + // Check EKF flags to handle critical errors + // https://github.com/ArduPilot/MissionPlanner/blob/4d441bd4b1dbc08adce4d8b26e078e93760da3a7/ExtLibs/ArduPilot/CurrentState.cs#L2674-L2736 + const activeFlags = getActiveEKFFlags(state.ekfStatusReportData.flags) + if (!activeFlags.includes("EKF_ATTITUDE")) { + // If we have no attitude solution + state.ekfCalculatedStatus = 1 + } else if (!activeFlags.includes("EKF_VELOCITY_HORIZ")) { + // If we have GPS but no horizontal velocity solution + const gpsStatus = state.gpsRawIntData.fixType + if (gpsStatus > 0) { + state.ekfCalculatedStatus = 1 + } + } else if (activeFlags.includes("EKF_UNINITIALIZED")) { + // EKF not initialized at all + state.ekfCalculatedStatus = 1 + } + }, }, selectors: { selectAttitude: (state) => state.attitudeData, @@ -203,6 +249,8 @@ const droneInfoSlice = createSlice({ selectStatusText: (state) => state.statusText, selectGraphValues: (state) => state.graphs.selectedGraphs, selectLastGraphMessage: (state) => state.graphs.lastGraphResultsMessage, + selectEkfStatusReportData: (state) => state.ekfStatusReportData, + selectEkfCalculatedStatus: (state) => state.ekfCalculatedStatus, }, }) @@ -225,6 +273,7 @@ export const { setLastGraphMessage, setLoiterRadius, setGuidedModePinData, + setEkfStatusReportData, } = droneInfoSlice.actions // Memoized selectors because redux is a bitch @@ -298,6 +347,8 @@ export const { selectExtraDroneData, selectGraphValues, selectLastGraphMessage, + selectEkfStatusReportData, + selectEkfCalculatedStatus, } = droneInfoSlice.selectors export default droneInfoSlice diff --git a/radio/app/endpoints/states.py b/radio/app/endpoints/states.py index 4aec449cd..861d210fb 100644 --- a/radio/app/endpoints/states.py +++ b/radio/app/endpoints/states.py @@ -46,6 +46,7 @@ def set_state(data: SetStateType) -> None: "RC_CHANNELS", "ESC_TELEMETRY_5_TO_8", "MISSION_CURRENT", + "EKF_STATUS_REPORT", ], "missions": ["GLOBAL_POSITION_INT", "NAV_CONTROLLER_OUTPUT", "HEARTBEAT"], "graphs": ["VFR_HUD", "ATTITUDE", "SYS_STATUS"], From 910dc103bb60e6d781d8928755fd0e4c150a3ea9 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Sat, 4 Oct 2025 22:07:40 +0100 Subject: [PATCH 2/6] Start adding ekf status popout window --- gcs/ekfStatus.html | 12 +++ gcs/electron/main.ts | 19 +++-- gcs/electron/modules/ekfStatus.ts | 60 +++++++++++++++ gcs/electron/preload.js | 3 + gcs/src/components/dashboard/ekfDisplay.jsx | 3 + .../ekfStatusWindow/ekfStatusWindow.jsx | 75 +++++++++++++++++++ gcs/src/ekfStatus.jsx | 25 +++++++ gcs/src/redux/middleware/socketMiddleware.js | 25 ++++--- gcs/src/redux/slices/droneInfoSlice.js | 5 +- 9 files changed, 205 insertions(+), 22 deletions(-) create mode 100644 gcs/ekfStatus.html create mode 100644 gcs/electron/modules/ekfStatus.ts create mode 100644 gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx create mode 100644 gcs/src/ekfStatus.jsx diff --git a/gcs/ekfStatus.html b/gcs/ekfStatus.html new file mode 100644 index 000000000..d7c0a6610 --- /dev/null +++ b/gcs/ekfStatus.html @@ -0,0 +1,12 @@ + + + + + + + + +
+ + + diff --git a/gcs/electron/main.ts b/gcs/electron/main.ts index 2bd38fc71..ab0d5c7fb 100644 --- a/gcs/electron/main.ts +++ b/gcs/electron/main.ts @@ -22,6 +22,9 @@ import registerAboutIPC, { destroyAboutWindow, openAboutPopout, } from "./modules/aboutWindow" +import registerEkfStatusIPC, { + destroyEkfStatusWindow, +} from "./modules/ekfStatus" import registerLinkStatsIPC, { destroyLinkStatsWindow, openLinkStatsWindow, @@ -220,6 +223,7 @@ function createWindow() { registerWebcamIPC(win) registerAboutIPC() registerLinkStatsIPC() + registerEkfStatusIPC() // Open links in browser, not within the electron window. // Note, links must have target="_blank" @@ -371,14 +375,19 @@ function startBackend() { }) } +function closeWindows() { + destroyWebcamWindow() + destroyAboutWindow() + destroyLinkStatsWindow() + destroyEkfStatusWindow() +} + // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. function closeWithBackend() { // Always close all popout windows first - destroyWebcamWindow() - destroyAboutWindow() - destroyLinkStatsWindow() + closeWindows() console.log("Killing backend") // kill any processes with the name "fgcs_backend.exe" // Windows @@ -400,9 +409,7 @@ app.on("before-quit", () => { console.log("Stopping backend") spawnSync("pkill", ["-f", "fgcs_backend"]) pythonBackend = null - destroyWebcamWindow() - destroyAboutWindow() - destroyLinkStatsWindow() + closeWindows() } }) diff --git a/gcs/electron/modules/ekfStatus.ts b/gcs/electron/modules/ekfStatus.ts new file mode 100644 index 000000000..8c72b402c --- /dev/null +++ b/gcs/electron/modules/ekfStatus.ts @@ -0,0 +1,60 @@ +import { BrowserWindow, ipcMain } from "electron" +import path from "path" + +const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"] + +let ekfStatusWin: BrowserWindow | null = null + +export function openEkfStatusWindow() { + if (ekfStatusWin === null) { + ekfStatusWin = new BrowserWindow({ + width: 500, + height: 300, + frame: true, + icon: path.join(process.env.VITE_PUBLIC, "app_icon.ico"), + show: false, + title: "EKF Status", + webPreferences: { + preload: path.join(__dirname, "preload.js"), + contextIsolation: true, + }, + fullscreen: false, + fullscreenable: false, + }) + } + + if (VITE_DEV_SERVER_URL) { + ekfStatusWin?.loadURL(VITE_DEV_SERVER_URL + "ekfStatus.html") + } else { + ekfStatusWin?.loadFile(path.join(process.env.DIST, "ekfStatus.html")) + } + + ekfStatusWin.on("close", () => { + ekfStatusWin = null + }) + ekfStatusWin.setMenuBarVisibility(false) + ekfStatusWin.show() +} + +export function closeEkfStatusWindow() { + destroyEkfStatusWindow() +} + +export function destroyEkfStatusWindow() { + ekfStatusWin?.close() + ekfStatusWin = null +} + +export default function registerEkfStatusIPC() { + ipcMain.removeHandler("app:open-ekf-status-window") + ipcMain.removeHandler("app:close-ekf-status-window") + ipcMain.removeHandler("app:update-ekf-status") + + ipcMain.handle("app:open-ekf-status-window", () => { + openEkfStatusWindow() + }) + ipcMain.handle("app:close-ekf-status-window", () => closeEkfStatusWindow()) + ipcMain.handle("app:update-ekf-status", (_, ekfStatusData) => { + ekfStatusWin?.webContents.send("app:send-ekf-status", ekfStatusData) + }) +} diff --git a/gcs/electron/preload.js b/gcs/electron/preload.js index 8e090d95d..1fa7dbfaf 100644 --- a/gcs/electron/preload.js +++ b/gcs/electron/preload.js @@ -20,6 +20,8 @@ const ALLOWED_INVOKE_CHANNELS = [ "app:close-link-stats-window", "app:update-link-stats", "window:select-file-in-explorer", + "app:update-ekf-status", + "app:open-ekf-status-window", ] const ALLOWED_SEND_CHANNELS = [ @@ -41,6 +43,7 @@ const ALLOWED_ON_CHANNELS = [ "app:webcam-closed", "app:send-link-stats", "fla:log-parse-progress", + "app:send-ekf-status", ] contextBridge.exposeInMainWorld("ipcRenderer", { diff --git a/gcs/src/components/dashboard/ekfDisplay.jsx b/gcs/src/components/dashboard/ekfDisplay.jsx index 60e7b477e..d603faee8 100644 --- a/gcs/src/components/dashboard/ekfDisplay.jsx +++ b/gcs/src/components/dashboard/ekfDisplay.jsx @@ -27,6 +27,9 @@ export default function EkfDisplay({ telemetryFontSize }) { lineHeight: `${telemetryFontSize * 1.75}rem`, color: textColour, }} + onClick={() => { + window.ipcRenderer.invoke("app:open-ekf-status-window") + }} > EKF diff --git a/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx b/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx new file mode 100644 index 000000000..1d681967a --- /dev/null +++ b/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx @@ -0,0 +1,75 @@ +"use client" + +import { useEffect, useState } from "react" + +export default function EkfStatusWindow() { + const [ekfStatus, setEkfStatus] = useState({ + airspeed_variance: 0, + compass_variance: 0, + pos_horiz_variance: 0, + pos_vert_variance: 0, + terrain_alt_variance: 0, + velocity_variance: 0, + flags: 0, + }) + + useEffect(() => { + window.ipcRenderer.on("app:send-ekf-status", (_event, status) => { + setEkfStatus(status) + }) + }, []) + + return ( +
+
+

EKF Status

+
+
+

+ Airspeed Variance +

+

{ekfStatus.airspeed_variance}

+
+
+

+ Compass Variance +

+

{ekfStatus.compass_variance}

+
+
+

+ Position Horizontal Variance +

+

{ekfStatus.pos_horiz_variance}

+
+
+

+ Position Vertical Variance +

+

{ekfStatus.pos_vert_variance}

+
+
+

+ Terrain Altitude Variance +

+

{ekfStatus.terrain_alt_variance}

+
+
+

+ Velocity Variance +

+

{ekfStatus.velocity_variance}

+
+
+

Flags

+

{ekfStatus.flags}

+
+
+
+
+ ) +} diff --git a/gcs/src/ekfStatus.jsx b/gcs/src/ekfStatus.jsx new file mode 100644 index 000000000..7b860c8ff --- /dev/null +++ b/gcs/src/ekfStatus.jsx @@ -0,0 +1,25 @@ +import "./css/index.css" // Needs to be at the top of the file +import "./css/resizable.css" + +// Style imports +import "@mantine/code-highlight/styles.css" +import "@mantine/core/styles.css" +import "@mantine/notifications/styles.css" +import "@mantine/spotlight/styles.css" +import "@mantine/tiptap/styles.css" + +import { MantineProvider } from "@mantine/core" +import React from "react" +import ReactDOM from "react-dom/client" +import { CustomMantineTheme } from "./components/customMantineTheme" +import EkfStatusWindow from "./components/ekfStatusWindow/ekfStatusWindow" + +ReactDOM.createRoot(document.getElementById("root")).render( + + + + + , +) + +postMessage({ payload: "removeLoading" }, "*") diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index 56af81305..e176ec48c 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -207,19 +207,20 @@ const socketMiddleware = (store) => { case "BATTERY_STATUS": store.dispatch(setBatteryData(msg)) break - case "EKF_STATUS_REPORT": - store.dispatch( - setEkfStatusReportData({ - airspeed_variance: msg.airspeed_variance ?? 0, - compass_variance: msg.compass_variance, - pos_horiz_variance: msg.pos_horiz_variance, - pos_vert_variance: msg.pos_vert_variance, - terrain_alt_variance: msg.terrain_alt_variance, - velocity_variance: msg.velocity_variance, - flags: msg.flags, - }), - ) + case "EKF_STATUS_REPORT": { + const data = { + airspeed_variance: msg.airspeed_variance ?? 0, + compass_variance: msg.compass_variance, + pos_horiz_variance: msg.pos_horiz_variance, + pos_vert_variance: msg.pos_vert_variance, + terrain_alt_variance: msg.terrain_alt_variance, + velocity_variance: msg.velocity_variance, + flags: msg.flags, + } + store.dispatch(setEkfStatusReportData(data)) + window.ipcRenderer.invoke("app:update-ekf-data", data) break + } } } diff --git a/gcs/src/redux/slices/droneInfoSlice.js b/gcs/src/redux/slices/droneInfoSlice.js index aafc8b1e7..8857d6c8f 100644 --- a/gcs/src/redux/slices/droneInfoSlice.js +++ b/gcs/src/redux/slices/droneInfoSlice.js @@ -194,10 +194,7 @@ const droneInfoSlice = createSlice({ const posHor = state.ekfStatusReportData.pos_horiz_variance const posVer = state.ekfStatusReportData.pos_vert_variance const terAlt = state.ekfStatusReportData.terrain_alt_variance - state.ekfCalculatedStatus = Math.max( - vel, - Math.max(comp, Math.max(posHor, Math.max(posVer, terAlt))), - ) + state.ekfCalculatedStatus = Math.max(vel, comp, posHor, posVer, terAlt) // Check EKF flags to handle critical errors // https://github.com/ArduPilot/MissionPlanner/blob/4d441bd4b1dbc08adce4d8b26e078e93760da3a7/ExtLibs/ArduPilot/CurrentState.cs#L2674-L2736 From 60ec5ce08fb649bb1ba2e569d9bb84c62801f13a Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Sat, 4 Oct 2025 22:08:06 +0100 Subject: [PATCH 3/6] Fix states test --- radio/tests/test_states.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radio/tests/test_states.py b/radio/tests/test_states.py index 8e407b40d..c26e8bb0a 100644 --- a/radio/tests/test_states.py +++ b/radio/tests/test_states.py @@ -28,7 +28,7 @@ def test_setState(socketio_client: SocketIOTestClient, droneStatus) -> None: # Success on changing state to dashboard socketio_client.emit("set_state", {"state": "dashboard"}) assert len(socketio_client.get_received()) == 0 - assert len(droneStatus.drone.message_listeners) == 13 + assert len(droneStatus.drone.message_listeners) == 14 droneStatus.drone.message_listeners = {} From 07193cb3a176a64b31c7ac8089e71576981751b9 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Sun, 5 Oct 2025 10:53:51 +0100 Subject: [PATCH 4/6] Remove airspeed variance --- gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx | 7 ------- gcs/src/redux/middleware/socketMiddleware.js | 1 - gcs/src/redux/slices/droneInfoSlice.js | 1 - 3 files changed, 9 deletions(-) diff --git a/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx b/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx index 1d681967a..852a7fada 100644 --- a/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx +++ b/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx @@ -4,7 +4,6 @@ import { useEffect, useState } from "react" export default function EkfStatusWindow() { const [ekfStatus, setEkfStatus] = useState({ - airspeed_variance: 0, compass_variance: 0, pos_horiz_variance: 0, pos_vert_variance: 0, @@ -28,12 +27,6 @@ export default function EkfStatusWindow() { >

EKF Status

-
-

- Airspeed Variance -

-

{ekfStatus.airspeed_variance}

-

Compass Variance diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index e176ec48c..7408f81fb 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -209,7 +209,6 @@ const socketMiddleware = (store) => { break case "EKF_STATUS_REPORT": { const data = { - airspeed_variance: msg.airspeed_variance ?? 0, compass_variance: msg.compass_variance, pos_horiz_variance: msg.pos_horiz_variance, pos_vert_variance: msg.pos_vert_variance, diff --git a/gcs/src/redux/slices/droneInfoSlice.js b/gcs/src/redux/slices/droneInfoSlice.js index 8857d6c8f..c0a1c0571 100644 --- a/gcs/src/redux/slices/droneInfoSlice.js +++ b/gcs/src/redux/slices/droneInfoSlice.js @@ -58,7 +58,6 @@ const droneInfoSlice = createSlice({ alt: 0, }, ekfStatusReportData: { - airspeed_variance: 0, compass_variance: 0, pos_horiz_variance: 0, pos_vert_variance: 0, From 11531d703b36ce957fb99e564dcbc4edb9bccccd Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Sun, 5 Oct 2025 12:55:01 +0100 Subject: [PATCH 5/6] Update ekf status window styling --- gcs/electron/modules/ekfStatus.ts | 5 +- gcs/electron/modules/webcam.ts | 2 +- .../ekfStatusWindow/ekfStatusWindow.jsx | 129 +++++++++++------- gcs/src/redux/middleware/socketMiddleware.js | 2 +- 4 files changed, 88 insertions(+), 50 deletions(-) diff --git a/gcs/electron/modules/ekfStatus.ts b/gcs/electron/modules/ekfStatus.ts index 8c72b402c..ea84640ca 100644 --- a/gcs/electron/modules/ekfStatus.ts +++ b/gcs/electron/modules/ekfStatus.ts @@ -8,8 +8,8 @@ let ekfStatusWin: BrowserWindow | null = null export function openEkfStatusWindow() { if (ekfStatusWin === null) { ekfStatusWin = new BrowserWindow({ - width: 500, - height: 300, + width: 700, + height: 400, frame: true, icon: path.join(process.env.VITE_PUBLIC, "app_icon.ico"), show: false, @@ -20,6 +20,7 @@ export function openEkfStatusWindow() { }, fullscreen: false, fullscreenable: false, + alwaysOnTop: true, }) } diff --git a/gcs/electron/modules/webcam.ts b/gcs/electron/modules/webcam.ts index aa8b40ef0..bd0a7f7ca 100644 --- a/gcs/electron/modules/webcam.ts +++ b/gcs/electron/modules/webcam.ts @@ -49,6 +49,7 @@ export function openWebcamPopout( }, fullscreen: false, fullscreenable: false, + alwaysOnTop: true, }) } else { console.warn("2nd webcam window requested, ignoring") @@ -85,7 +86,6 @@ export function openWebcamPopout( } export function closeWebcamPopout(mainWindow: BrowserWindow | null) { - console.log("Destroying webcam window") destroyWebcamWindow() mainWindow?.webContents.send("app:webcam-closed") } diff --git a/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx b/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx index 852a7fada..e34e53420 100644 --- a/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx +++ b/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx @@ -1,67 +1,104 @@ "use client" -import { useEffect, useState } from "react" +import { Progress } from "@mantine/core" +import { useEffect, useMemo, useState } from "react" +import { + EKF_STATUS_FLAGS, + getActiveEKFFlags, +} from "../../helpers/mavlinkConstants" + +// Tailwind styling +import resolveConfig from "tailwindcss/resolveConfig" +import tailwindConfig from "../../../tailwind.config" +const tailwindColors = resolveConfig(tailwindConfig).theme.colors + +// EKF Flags that are good when enabled +const FLAGS_ENABLED_BAD = ["EKF_UNITIALIZED", "EKF_GPS_GLITCHING"] + +const EKF_VARIANCE_STRINGS = { + compass_variance: "Compass Variance", + pos_horiz_variance: "Horizontal Pos Variance", + pos_vert_variance: "Vertical Pos Variance", + terrain_alt_variance: "Terrain Alt Variance", + velocity_variance: "Velocity Variance", +} + +const RED = tailwindColors.red[500] +const YELLOW = tailwindColors.yellow[500] +const GREEN = tailwindColors.green[500] + +function getPercentageFromValue(value) { + return (value * 100).toFixed(2) +} + +function getColourFromValue(value) { + if (value > 0.8) return RED + if (value > 0.5) return YELLOW + return "" +} export default function EkfStatusWindow() { - const [ekfStatus, setEkfStatus] = useState({ + const [ekfVariances, setEkfVariances] = useState({ compass_variance: 0, pos_horiz_variance: 0, pos_vert_variance: 0, terrain_alt_variance: 0, velocity_variance: 0, - flags: 0, }) + const [ekfFlags, setEkfFlags] = useState(0) + + const activeFlags = useMemo(() => { + return getActiveEKFFlags(ekfFlags) + }, [ekfFlags]) useEffect(() => { window.ipcRenderer.on("app:send-ekf-status", (_event, status) => { - setEkfStatus(status) + console.log(status) + setEkfVariances({ + compass_variance: status.compass_variance, + pos_horiz_variance: status.pos_horiz_variance, + pos_vert_variance: status.pos_vert_variance, + terrain_alt_variance: status.terrain_alt_variance, + velocity_variance: status.velocity_variance, + }) + setEkfFlags(status.flags) }) }, []) return ( -
-
-

EKF Status

-
-
-

- Compass Variance -

-

{ekfStatus.compass_variance}

-
-
-

- Position Horizontal Variance -

-

{ekfStatus.pos_horiz_variance}

-
-
-

- Position Vertical Variance -

-

{ekfStatus.pos_vert_variance}

+
+
+ {Object.entries(ekfVariances).map(([key, value]) => ( +
+

+ {EKF_VARIANCE_STRINGS[key]} - {value.toFixed(2)} +

+ + +
-
-

- Terrain Altitude Variance -

-

{ekfStatus.terrain_alt_variance}

-
-
-

- Velocity Variance -

-

{ekfStatus.velocity_variance}

-
-
-

Flags

-

{ekfStatus.flags}

-
-
+ ))} +
+
+ {Object.values(EKF_STATUS_FLAGS).map((flag) => ( +

+ {flag} +

+ ))}
) diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js index 7408f81fb..0d5823e63 100644 --- a/gcs/src/redux/middleware/socketMiddleware.js +++ b/gcs/src/redux/middleware/socketMiddleware.js @@ -217,7 +217,7 @@ const socketMiddleware = (store) => { flags: msg.flags, } store.dispatch(setEkfStatusReportData(data)) - window.ipcRenderer.invoke("app:update-ekf-data", data) + window.ipcRenderer.invoke("app:update-ekf-status", data) break } } From 8cb9497878352df0626f1deafae2c41553a7c156 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Sun, 5 Oct 2025 12:56:55 +0100 Subject: [PATCH 6/6] Address copilot review comments --- gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx b/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx index e34e53420..5157825c9 100644 --- a/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx +++ b/gcs/src/components/ekfStatusWindow/ekfStatusWindow.jsx @@ -13,7 +13,7 @@ import tailwindConfig from "../../../tailwind.config" const tailwindColors = resolveConfig(tailwindConfig).theme.colors // EKF Flags that are good when enabled -const FLAGS_ENABLED_BAD = ["EKF_UNITIALIZED", "EKF_GPS_GLITCHING"] +const FLAGS_ENABLED_BAD = ["EKF_UNINITIALIZED", "EKF_GPS_GLITCHING"] const EKF_VARIANCE_STRINGS = { compass_variance: "Compass Variance", @@ -53,7 +53,6 @@ export default function EkfStatusWindow() { useEffect(() => { window.ipcRenderer.on("app:send-ekf-status", (_event, status) => { - console.log(status) setEkfVariances({ compass_variance: status.compass_variance, pos_horiz_variance: status.pos_horiz_variance,