diff --git a/gcs/data/default_settings.json b/gcs/data/default_settings.json
index 9078b4453..1c6c1af50 100644
--- a/gcs/data/default_settings.json
+++ b/gcs/data/default_settings.json
@@ -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": {}
diff --git a/gcs/src/components/dashboard/alert.jsx b/gcs/src/components/dashboard/alert.jsx
new file mode 100644
index 000000000..d0d0571ad
--- /dev/null
+++ b/gcs/src/components/dashboard/alert.jsx
@@ -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 = {
+ Altitude: "Altitude",
+ Speed: "Speed",
+}
+
+export const AlertSeverity = {
+ 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 (
+
+ {sortedAlerts.map((alert) => (
+
+
}
+ onClose={() => dismissAlert(alert.category, true)}
+ >
+ {alert.jsx}
+
+
+ ))}
+
+ )
+}
diff --git a/gcs/src/components/dashboard/alertProvider.jsx b/gcs/src/components/dashboard/alertProvider.jsx
new file mode 100644
index 000000000..2fa602d52
--- /dev/null
+++ b/gcs/src/components/dashboard/alertProvider.jsx
@@ -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(
+ (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 (
+
+ {children}
+
+ )
+}
+
+export const useAlerts = () => useContext(AlertContext)
diff --git a/gcs/src/components/dashboard/statusBar.jsx b/gcs/src/components/dashboard/statusBar.jsx
index 990327bd0..df5cb9c1a 100644
--- a/gcs/src/components/dashboard/statusBar.jsx
+++ b/gcs/src/components/dashboard/statusBar.jsx
@@ -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 (
@@ -66,6 +67,9 @@ export default function StatusBar(props) {
Current heading
Desired heading
+
)
}
diff --git a/gcs/src/components/dashboard/telemetry.jsx b/gcs/src/components/dashboard/telemetry.jsx
index 3fc86de24..b9bb9c647 100644
--- a/gcs/src/components/dashboard/telemetry.jsx
+++ b/gcs/src/components/dashboard/telemetry.jsx
@@ -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.
*/
diff --git a/gcs/src/components/mainContent.jsx b/gcs/src/components/mainContent.jsx
index 4551c13ef..484a5095f 100644
--- a/gcs/src/components/mainContent.jsx
+++ b/gcs/src/components/mainContent.jsx
@@ -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() {
@@ -34,7 +35,14 @@ export default function AppContent() {
- } />
+
+
+
+ }
+ />
} />
} />
} />
@@ -48,7 +56,6 @@ export default function AppContent() {
}
/>
- } />
{renderUI && }
diff --git a/gcs/src/components/settingsModal.jsx b/gcs/src/components/settingsModal.jsx
index 6e4977f71..35642fd1b 100644
--- a/gcs/src/components/settingsModal.jsx
+++ b/gcs/src/components/settingsModal.jsx
@@ -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()
@@ -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 (
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 (
+
+ {values.map(({ id, value }) => (
+
+
+ {
+ if (!isValidNumber(num, range)) return
+ updateValue(id, parseInt(num))
+ }}
+ suffix="m"
+ />
+
+ ))}
+
+
+
+
+ )
+}
+
function Setting({ settingName, df }) {
return (
-
-
{df.display}:
- {df.type == "number" ? (
+
+
+
{df.display}:
+
{df.description}
+
+ {df.type == "extendableNumber" ? (
+
+ ) : df.type == "number" ? (
) : df.type == "boolean" ? (
-
+
) : df.type == "option" ? (
) : (
@@ -111,7 +191,7 @@ function SettingsModal() {
{settingTabs.map((t) => {
return (
-
+
{Object.keys(DefaultSettings[t]).map((s) => {
return (
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,
diff --git a/gcs/src/missions.jsx b/gcs/src/missions.jsx
index 052bb6be5..9970027ea 100644
--- a/gcs/src/missions.jsx
+++ b/gcs/src/missions.jsx
@@ -1,5 +1,5 @@
/*
- The missions screen.
+ The missions screen.
*/
// Base imports