Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions gcs/data/default_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@
"display": "Sync Dashboard and Missions Map Viewstate"
}
},
"Dashboard": {
"altitudeAlerts": {
"description": "Add as many altitude alerts as you want, they'll show when you fall below the set limit",
"default": [100, 90, 80],
"type": "extendableNumber",
"range": [
0,
100000
],
"display": "Altitude Alerts"
}
},
"Params": {},
"Config": {},
"FGCS": {}
Expand Down
52 changes: 52 additions & 0 deletions gcs/src/components/dashboard/alert.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Alert } from "@mantine/core"
import { IconAlertTriangle } from "@tabler/icons-react"
import { useMemo } from "react"
import { useAlerts } from "./alertProvider"

export const AlertCategory = {

Check warning on line 6 in gcs/src/components/dashboard/alert.jsx

View workflow job for this annotation

GitHub Actions / Run linters

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
Altitude: "Altitude",
Speed: "Speed",
}

export const AlertSeverity = {

Check warning on line 11 in gcs/src/components/dashboard/alert.jsx

View workflow job for this annotation

GitHub Actions / Run linters

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
Yellow: 1,
Orange: 2,
Red: 3,
}

const SeverityColor = {
[AlertSeverity.Yellow]: "yellow",
[AlertSeverity.Orange]: "orange",
[AlertSeverity.Red]: "red",
}

export default function AlertSection() {
const { alerts, dismissAlert } = useAlerts()

const sortedAlerts = useMemo(() => {
return alerts.toSorted((a1, a2) => a2.severity - a1.severity)
}, [alerts])

return (
<div className="space-y-2 max-w-sm">
Comment thread
bensgilbert marked this conversation as resolved.
{sortedAlerts.map((alert) => (
<div
className="bg-falcongrey-900/90"
key={alert.category}
style={{ borderRadius: "var(--mantine-radius-default)" }}
>
<Alert
variant="outline"
color={SeverityColor[alert.severity]}
withCloseButton
title={alert.category}
icon={<IconAlertTriangle />}
onClose={() => dismissAlert(alert.category, true)}
>
{alert.jsx}
</Alert>
</div>
))}
</div>
)
}
53 changes: 53 additions & 0 deletions gcs/src/components/dashboard/alertProvider.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* Alert
* {
* category: AlertCategory,
* severity: AlertSeverity,
* jsx: <></>
* }
*/

import { createContext, useContext, useRef, useState } from "react"

const AlertContext = createContext()

export default function AlertProvider({ children }) {
const [alerts, setAlerts] = useState([])
const dismissedAlerts = useRef(new Map())

function dispatchAlert(alert) {
if (dismissedAlerts.current.get(alert.category) >= alert.severity) return
dismissedAlerts.current.delete(alert.category)

const existingAlertIndex = alerts.findIndex(
Comment thread
bensgilbert marked this conversation as resolved.
(existingAlert) => existingAlert.category == alert.category,
)
if (existingAlertIndex >= 0) {
alerts[existingAlertIndex] = alert
setAlerts([...alerts])
} else {
setAlerts([...alerts, alert])
}
}

function dismissAlert(category, manual) {
setAlerts((prevAlerts) => {
const alert = prevAlerts.find((a) => a.category === category)

if (manual) {
dismissedAlerts.current.set(category, alert.severity)
} else {
dismissedAlerts.current.delete(category)
}

return prevAlerts.filter((a) => a.category !== category)
})
}

return (
<AlertContext.Provider value={{ alerts, dispatchAlert, dismissAlert }}>
{children}
</AlertContext.Provider>
)
}

export const useAlerts = () => useContext(AlertContext)

Check warning on line 53 in gcs/src/components/dashboard/alertProvider.jsx

View workflow job for this annotation

GitHub Actions / Run linters

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
4 changes: 4 additions & 0 deletions gcs/src/components/dashboard/statusBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { IconClock, IconNetwork, IconNetworkOff } from "@tabler/icons-react"
// Helper imports
import { socket } from "../../helpers/socket"
import GetOutsideVisibilityColor from "../../helpers/outsideVisibility"
import AlertSection from "./alert"

export function StatusSection({ icon, value, tooltip }) {
return (
Expand Down Expand Up @@ -66,6 +67,9 @@ export default function StatusBar(props) {
<p className="text-sm text-blue-200">Current heading</p>
<p className="text-sm text-red-200">Desired heading</p>
</div>
<div className="m-2">
<AlertSection />
</div>
</div>
)
}
2 changes: 1 addition & 1 deletion gcs/src/components/dashboard/telemetry.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Telemetry. This file holds all the telemetry indicators and is part of the resizable info box
Telemetry. This file holds all the telemetry indicators and is part of the resizable info box
section, found in the top half.
*/

Expand Down
11 changes: 9 additions & 2 deletions gcs/src/components/mainContent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Dashboard from "../dashboard"
import { store } from "../redux/store"
import { Provider } from "react-redux"
import { ErrorBoundary } from "react-error-boundary"
import AlertProvider from "./dashboard/alertProvider"
import ErrorBoundaryFallback from "./error/errorBoundary"

export default function AppContent() {
Expand All @@ -34,7 +35,14 @@ export default function AppContent() {
<ErrorBoundary fallbackRender={ErrorBoundaryFallback}>
<SettingsModal />
<Routes>
<Route path="/" element={<Dashboard />} />
<Route
path="/"
element={
<AlertProvider>
<Dashboard />
</AlertProvider>
}
/>
<Route path="/missions" element={<Missions />} />
<Route path="/graphs" element={<Graphs />} />
<Route path="/params" element={<Params />} />
Expand All @@ -48,7 +56,6 @@ export default function AppContent() {
</Provider>
}
/>
<Route path="/missions" element={<Missions />} />
</Routes>
{renderUI && <Commands />}
</ErrorBoundary>
Expand Down
112 changes: 96 additions & 16 deletions gcs/src/components/settingsModal.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
import { Checkbox, Input, Modal, NativeSelect, Tabs } from "@mantine/core"
import {
Button,
Checkbox,
Input,
Modal,
NativeSelect,
NumberInput,
Tabs,
} from "@mantine/core"
import { useSettings } from "../helpers/settings"

import { IconTrash } from "@tabler/icons-react"
import { memo, useEffect, useState } from "react"
import DefaultSettings from "../../data/default_settings.json"
import { memo } from "react"

const isValidNumber = (num, range) => {
return (
num &&
parseInt(num) &&
(range === null || (range[0] <= num && num <= range[1]))
)
}

function TextSetting({ settingName, hidden }) {
const { getSetting, setSetting } = useSettings()
Expand Down Expand Up @@ -38,31 +55,94 @@ function OptionSetting({ settingName, options }) {

function NumberSetting({ settingName, range }) {
const { getSetting, setSetting } = useSettings()
const setIfValid = (num) => {
if (
!num ||
(parseInt(num) &&
(range === null || (range[0] <= num && num <= range[1])))
)
setSetting(settingName, num)
}

return (
<Input
value={getSetting(settingName)}
onChange={(e) => setIfValid(e.currentTarget.value)}
onChange={(e) => {
const num = e.currentTarget.value
if (isValidNumber(num, range)) setSetting(settingName, num)
}}
/>
)
}

const generateId = () => Math.random().toString(36).slice(8)

function ExtendableNumberSetting({ settingName, range }) {
const { getSetting, setSetting } = useSettings()

const [values, setValues] = useState(
getSetting(settingName).map((val) => ({ id: generateId(), value: val })),
)

useEffect(() => {
setSetting(
settingName,
values.map((a) => a.value),
)
}, [values])

const updateValue = (id, value) => {
setValues((prev) => prev.map((a) => (a.id === id ? { ...a, value } : a)))
}

const removeValue = (id) => {
setValues((prev) => prev.filter((a) => a.id !== id))
}

return (
<div className="flex flex-col shrink-0 items-end gap-2">
{values.map(({ id, value }) => (
<div key={id} className="flex gap-2 items-center">
<button
className="text-falconred-600 hover:text-falconred-700 p-1 rounded-full"
onClick={() => removeValue(id)}
>
<IconTrash size={20} />
</button>
<NumberInput
value={value}
onChange={(num) => {
if (!isValidNumber(num, range)) return
updateValue(id, parseInt(num))
}}
suffix="m"
/>
</div>
))}
<div className="w-full pl-9">
<Button
fullWidth
onClick={() =>
setValues([...values, { id: generateId(), value: 0 }])
}
>
Add new Alert
</Button>
</div>
</div>
)
}

function Setting({ settingName, df }) {
return (
<div className="flex flex-row justify-between items-center h-[5vh] px-10 ">
<div>{df.display}:</div>
{df.type == "number" ? (
<div
className={`flex flex-row gap-8 justify-between ${df.type != "extendableNumber" && "items-center"} px-10 `}
>
<div className="space-y-px">
<div>{df.display}:</div>
<p className="text-gray-400 text-sm">{df.description}</p>
</div>
{df.type == "extendableNumber" ? (
<ExtendableNumberSetting
settingName={settingName}
range={df.range || null}
/>
) : df.type == "number" ? (
<NumberSetting settingName={settingName} range={df.range || null} />
) : df.type == "boolean" ? (
<BoolSetting settingName={settingName} />
<BoolSetting settingName={settingName} options={df.options} />
) : df.type == "option" ? (
<OptionSetting settingName={settingName} options={df.options} />
) : (
Expand Down Expand Up @@ -111,7 +191,7 @@ function SettingsModal() {
</Tabs.List>
{settingTabs.map((t) => {
return (
<Tabs.Panel value={t} key={t}>
<Tabs.Panel className="space-y-4" value={t} key={t}>
{Object.keys(DefaultSettings[t]).map((s) => {
return (
<Setting
Expand Down
39 changes: 38 additions & 1 deletion gcs/src/dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ const tailwindColors = resolveConfig(tailwindConfig).theme.colors
// Sounds
import armSound from "./assets/sounds/armed.mp3"
import disarmSound from "./assets/sounds/disarmed.mp3"
import { AlertCategory, AlertSeverity } from "./components/dashboard/alert"
import { useAlerts } from "./components/dashboard/alertProvider"
import { useSettings } from "./helpers/settings"

export default function Dashboard() {
// Local Storage
Expand Down Expand Up @@ -148,9 +151,43 @@ export default function Dashboard() {
defaultValue: defaultDataMessages,
})

const { getSetting } = useSettings()

// Alerts
const { dispatchAlert, dismissAlert } = useAlerts()
const highestAltitudeRef = useRef(0)

function updateAltitudeAlert(msg) {
Comment thread
bensgilbert marked this conversation as resolved.
if (msg.alt > highestAltitudeRef.current)
return (highestAltitudeRef.current = msg.alt)
const altitudes = getSetting("Dashboard.altitudeAlerts")
altitudes.sort((a1, a2) => a1 - a2)

for (const [i, altitude] of altitudes.entries()) {
if (highestAltitudeRef.current > altitude && msg.alt < altitude) {
dispatchAlert({
category: AlertCategory.Altitude,
severity:
i == 0
? AlertSeverity.Red
: i == altitudes.length - 1
? AlertSeverity.Yellow
: AlertSeverity.Orange,
jsx: <>Caution! You've fallen below {altitude}m</>,
})
return
}
}

dismissAlert(AlertCategory.Altitude)
}

const incomingMessageHandler = useCallback(
() => ({
VFR_HUD: (msg) => setTelemetryData(msg),
VFR_HUD: (msg) => {
setTelemetryData(msg)
updateAltitudeAlert(msg)
},
BATTERY_STATUS: (msg) => {
const battery = localBatteryData.filter(
(battery) => battery.id == msg.id,
Expand Down
2 changes: 1 addition & 1 deletion gcs/src/missions.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
The missions screen.
The missions screen.
*/

// Base imports
Expand Down
Loading