Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
95eb65a
I took computer science because I like computers, I dont like compute…
Matthew-Dobson Feb 23, 2026
90bbdfb
Merge branch 'main' into 977-display-graphs-as-popout-windows
Matthew-Dobson Feb 23, 2026
28b4f1a
github comments
Matthew-Dobson Feb 23, 2026
c2448ed
Merge branch '977-display-graphs-as-popout-windows' of https://github…
Matthew-Dobson Feb 23, 2026
a62404c
linters again
Matthew-Dobson Feb 23, 2026
325977a
more linters, silly me
Matthew-Dobson Feb 23, 2026
0cac3c6
Merge branch 'main' into 977-display-graphs-as-popout-windows
1Blademaster Feb 25, 2026
3ab4fe5
replaced custom checkbox with mantime checkbox with label prop
Matthew-Dobson Feb 26, 2026
7972fe2
fixed an invalid tailwind class name
Matthew-Dobson Feb 26, 2026
e406e75
made function name more descriptive
Matthew-Dobson Feb 26, 2026
2689547
clarified comment
Matthew-Dobson Feb 26, 2026
72e2a55
switched search bar to mantine component
Matthew-Dobson Feb 26, 2026
0384de9
kush change
Matthew-Dobson Feb 26, 2026
d9f33bc
changed button to mantine component and tidied up mantine imports
Matthew-Dobson Feb 26, 2026
5d96d6a
fix popout electron window resizing to match graph size
Matthew-Dobson Feb 26, 2026
bf9c725
fixed checbox so that it 'unticks' when graph popout is closed manually
Matthew-Dobson Feb 26, 2026
5fd8827
added a ready handshake to ensure the graph window init is never miss…
Matthew-Dobson Feb 26, 2026
75804af
linters
Matthew-Dobson Feb 26, 2026
ba7e2dd
updated streaming duration to 30s
Matthew-Dobson Feb 27, 2026
52ecbb5
removed use effect
Matthew-Dobson Feb 27, 2026
634cb2f
removed title from graph windows
Matthew-Dobson Feb 27, 2026
8ae822f
linters
Matthew-Dobson Feb 27, 2026
6021cee
linters again
Matthew-Dobson Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sitl_setup/run.sh eol=lf
sitl_setup/run.sh eol=lf
14 changes: 7 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contributing

This document outlines the full process for creating, working on, and merging tickets within the FGCS repository.
This document outlines the full process for creating, working on, and merging tickets within the FGCS repository.
All steps can be completed directly through GitHub and VS Code.

---
Expand Down Expand Up @@ -35,7 +35,7 @@ You can either:

2. Drag your ticket into the **“In Progress”** column on the board.

3. Open the ticket again, locate the **Development** section on the right-hand side, and click **“Create a new branch.”**
3. Open the ticket again, locate the **Development** section on the right-hand side, and click **“Create a new branch.”**
Then click **“Create Branch.”**

<p align="center">
Expand All @@ -58,10 +58,10 @@ You can commit changes using **VS Code (recommended)** or **Bash**.

### Method 1: VS Code (Recommended)

1. Open the **Source Control** tab on the left-hand side.
2. Right-click the files you want to commit and select **“Stage Changes.”**
1. Open the **Source Control** tab on the left-hand side.
2. Right-click the files you want to commit and select **“Stage Changes.”**
Confirm the correct files appear under the *Staged Changes* section.
3. Add a clear commit message describing your changes.
3. Add a clear commit message describing your changes.
4. Press **Commit**, then click the **arrows in the bottom-left corner** to *Synchronize Changes* (push your commit).

<p align="center">
Expand Down Expand Up @@ -92,8 +92,8 @@ git push origin <branch name>
---

### Merging
Navigate to the ticket again and open a pull request. Wait until the automatic tests are ran and fix any changes suggested by those.
Navigate to the ticket again and open a pull request. Wait until the automatic tests are ran and fix any changes suggested by those.

Then, in the top right click reviewers then Copilot (GitHub Pro required). Copilot will then offer a code review, it is strongly suggested you make these changes.
Then, in the top right click reviewers then Copilot (GitHub Pro required). Copilot will then offer a code review, it is strongly suggested you make these changes.

Finally, request the "Avis Code team" as a reviewer, and one of the official code reviewers will look over your work. If they deem it suitable to merge, you will be cleared to merge; alternatively, changes will be suggested which you will make and then request another review.
2 changes: 0 additions & 2 deletions gcs/.env_sample

This file was deleted.

5 changes: 5 additions & 0 deletions gcs/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ import registerVibeStatusIPC, {
} from "./modules/vibeStatusWindow"
import registerVideoIPC, { destroyVideoWindow } from "./modules/videoWindow"
import { readParamsFile } from "./utils/paramsFile"
import registerGraphWindowIPC, {
destroyAllGraphWindows,
} from "./modules/graphWindow"

// Check if required data files exist
function checkRequiredDataFiles(): {
Expand Down Expand Up @@ -277,6 +280,7 @@ function createWindow() {
registerFFmpegBinaryIPC()
registerRTSPStreamIPC(win)
registerFlaParamsIPC()
registerGraphWindowIPC(win)

// Open links in browser, not within the electron window.
// Note, links must have target="_blank"
Expand Down Expand Up @@ -465,6 +469,7 @@ function closeWindows() {
destroyVibeStatusWindow()
cleanupAllRTSPStreams()
destroyFlaParamsWindow()
destroyAllGraphWindows()
}

// Quit when all windows are closed, except on macOS. There, it's common
Expand Down
212 changes: 212 additions & 0 deletions gcs/electron/modules/graphWindow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { BrowserWindow, ipcMain } from "electron"
import path from "path"

const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]

type GraphKey = "graph_a" | "graph_b" | "graph_c" | "graph_d"

type GraphWindowMeta = {
id: string
msg: string
field: string
title: string
description?: string
label?: string
}

type OpenArgs = {
graphKey: GraphKey
meta: GraphWindowMeta
}

type CloseArgs = {
graphKey: GraphKey
}

type GraphPoint = {
graphKey: GraphKey
data: { x: number; y: number }
}

const graphWins: Partial<Record<GraphKey, BrowserWindow>> = {}
const lastMeta: Partial<Record<GraphKey, GraphWindowMeta>> = {}

// Reference to the main FGCS window (the one with Redux + toolbar)
let mainWin: BrowserWindow | null = null

/** Get a window if it's still usable; auto-clean if destroyed. */
function getGraphWin(graphKey: GraphKey): BrowserWindow | null {
const win = graphWins[graphKey]
if (!win) return null

if (win.isDestroyed()) {
graphWins[graphKey] = undefined
return null
}

if (win.webContents.isDestroyed()) {
graphWins[graphKey] = undefined
return null
}

return win
}

function notifyMainGraphClosed(graphKey: GraphKey) {
try {
if (!mainWin) return
if (mainWin.isDestroyed()) return
if (mainWin.webContents.isDestroyed()) return
mainWin.webContents.send("app:graph-window:closed", { graphKey })
} catch {
// ignore during shutdown / race conditions
}
}

function sendInit(graphKey: GraphKey) {
const win = getGraphWin(graphKey)
const meta = lastMeta[graphKey]
if (!win || !meta) return
try {
win.webContents.send("app:graph-window:init", { graphKey, meta })
} catch {
// ignore (window may be closing)
}
}

export function openGraphWindow({ graphKey, meta }: OpenArgs) {
// Always cache latest meta for this slot (used by ready-handshake)
lastMeta[graphKey] = meta

// Reuse existing window if it's alive
let win = getGraphWin(graphKey)

if (!win) {
win = new BrowserWindow({
width: 700,
height: 350,
frame: true,
icon: path.join(process.env.VITE_PUBLIC!, "app_icon.ico"),
show: false,
title: meta?.title ?? "Graph",
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
},
fullscreen: false,
fullscreenable: false,
alwaysOnTop: true,
})

graphWins[graphKey] = win
win.setMenuBarVisibility(false)

// IMPORTANT: clean up reference when user closes window
win.on("closed", () => {
graphWins[graphKey] = undefined
delete lastMeta[graphKey]
// Tell the main window so Redux can untick the checkbox
notifyMainGraphClosed(graphKey)
})

// Load content only on first creation
if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL + "graphWindow.html")
} else {
win.loadFile(path.join(process.env.DIST!, "graphWindow.html"))
}

// Best-effort init on load (ready-handshake below is the real guarantee)
win.webContents.once("did-finish-load", () => {
const alive = getGraphWin(graphKey)
if (!alive) return
sendInit(graphKey)
})
} else {
// Window already exists: just update title + init payload
try {
win.setTitle(meta?.title ?? "Graph")
sendInit(graphKey)
} catch (e) {
// If it threw, treat it as dead and try again once
graphWins[graphKey] = undefined
return openGraphWindow({ graphKey, meta })
}
}

// Show/focus safely
if (!win.isDestroyed()) {
win.show()
win.focus()
}
}

export function closeGraphWindow({ graphKey }: CloseArgs) {
const win = getGraphWin(graphKey)
if (!win) return
// Do NOT set undefined here — let the 'closed' event clean up.
win.close()
}

export function destroyAllGraphWindows() {
;(Object.keys(graphWins) as GraphKey[]).forEach((k) => {
const win = getGraphWin(k)
win?.close()
})
}

// Accept the main window so we can send close events back to Redux UI
export default function registerGraphWindowIPC(appWin?: BrowserWindow) {
if (appWin) mainWin = appWin

ipcMain.removeHandler("app:open-graph-window")
ipcMain.removeHandler("app:close-graph-window")
ipcMain.removeHandler("app:update-graph-windows")

// READY HANDSHAKE:
// popout renderer sends "app:graph-window:ready" after it registers listeners.
// When we receive it, we send init using cached meta so init is never missed.
ipcMain.removeAllListeners("app:graph-window:ready")
ipcMain.on("app:graph-window:ready", (event) => {
const senderId = event.sender.id

const graphKey = (Object.keys(graphWins) as GraphKey[]).find((k) => {
const w = getGraphWin(k)
return w?.webContents?.id === senderId
})

if (!graphKey) return

sendInit(graphKey)
})

ipcMain.handle("app:open-graph-window", (_event, args: OpenArgs) => {
openGraphWindow(args)
})

ipcMain.handle("app:close-graph-window", (_event, args: CloseArgs) => {
closeGraphWindow(args)
})

ipcMain.handle(
"app:update-graph-windows",
(_event, graphResults: GraphPoint[] | false) => {
if (!graphResults) return

for (const result of graphResults) {
const win = getGraphWin(result.graphKey)
if (!win) continue

try {
win.webContents.send("app:send-graph-point", result)
} catch {
// If it errors during close, drop it
graphWins[result.graphKey] = undefined
delete lastMeta[result.graphKey]
// Also tell the main window, in case the close event races
notifyMainGraphClosed(result.graphKey)
}
}
},
)
}
14 changes: 14 additions & 0 deletions gcs/electron/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const ALLOWED_INVOKE_CHANNELS = [
"settings:save-settings",
"app:open-video-window",
"app:close-video-window",
"app:open-graph-window",
"app:close-graph-window",
"app:update-graph-windows",
"app:start-rtsp-stream",
"app:stop-rtsp-stream",
"app:get-current-stream-url",
Expand Down Expand Up @@ -54,6 +57,7 @@ const ALLOWED_SEND_CHANNELS = [
"window:update-title",
// drone state updates (connectedToDrone, isArmed, isFlying)
"app:drone-state",
"app:graph-window:ready",
]

const ALLOWED_ON_CHANNELS = [
Expand All @@ -67,6 +71,9 @@ const ALLOWED_ON_CHANNELS = [
"settings:open",
"mavlink-forwarding:open",
"app:send-fla-params",
"app:graph-window:init",
"app:send-graph-point",
"app:graph-window:closed",
]

contextBridge.exposeInMainWorld("ipcRenderer", {
Expand Down Expand Up @@ -94,6 +101,13 @@ contextBridge.exposeInMainWorld("ipcRenderer", {
throw new Error(`IPC on channel '${channel}' is not allowed`)
},

removeListener: (channel, callback) => {
if (ALLOWED_ON_CHANNELS.includes(channel)) {
return ipcRenderer.removeListener(channel, callback)
}
throw new Error(`IPC removeListener channel '${channel}' is not allowed`)
},

// Secure removeAllListeners - only for whitelisted channels
removeAllListeners: (channel) => {
if (ALLOWED_ON_CHANNELS.includes(channel)) {
Expand Down
12 changes: 12 additions & 0 deletions gcs/graphWindow.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo_dark_icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/graphWindow.jsx"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion gcs/src/components/dashboard/resizableInfoBox.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
Resizable information box. This is the left hand side if the screen that moves and will contain
both the telemetry information and actions, which are both separate components. These
both the telemetry information and actions, which are both separate components. These
components are passed in via the props.children which in this case is from dashboard.jsx
*/

Expand Down
24 changes: 24 additions & 0 deletions gcs/src/components/graphWindow/graphWindow.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect, useState } from "react"

export default function GraphWindow() {
const [meta, setMeta] = useState(null)

useEffect(() => {
window.ipcRenderer.on("app:graph-window:init", (_event, data) => {
setMeta(data)
})
}, [])

return (
<div className="w-full h-full bg-falcongrey-800 p-4">
<div className="text-lg">{meta?.title ?? "Graph"}</div>
<div className="text-sm text-falcongrey-300 opacity-70">
{meta?.id ?? ""}
</div>
<div className="text-sm text-falcongrey-300 opacity-70 mt-1">
{meta?.description ?? ""}
</div>
<div className="mt-6 opacity-70">Graph goes here later.</div>
</div>
)
}
Loading