From ad9f00f60d2188e6c7df12574e29701615c6a8d6 Mon Sep 17 00:00:00 2001 From: Julian Jones Date: Thu, 6 Mar 2025 23:32:30 +0000 Subject: [PATCH 01/16] Added kush's stuff --- gcs/src/components/dashboard/map.jsx | 405 ++++++++++++++++----- radio/app/controllers/missionController.py | 272 ++++++++++++-- radio/app/controllers/navController.py | 47 +++ radio/app/utils.py | 36 +- radio/tests/test_mission.py | 18 + 5 files changed, 654 insertions(+), 124 deletions(-) diff --git a/gcs/src/components/dashboard/map.jsx b/gcs/src/components/dashboard/map.jsx index afd615882..0c1d14844 100644 --- a/gcs/src/components/dashboard/map.jsx +++ b/gcs/src/components/dashboard/map.jsx @@ -7,10 +7,10 @@ */ // Base imports -import React, { useEffect, useRef, useState } from "react" +import { useEffect, useRef, useState } from "react" // Maplibre and mantine imports -import { Button, Divider, Modal, NumberInput } from "@mantine/core" +import { Button, Divider, Modal, NumberInput, Tooltip } from "@mantine/core" import { useClipboard, useDisclosure, @@ -18,11 +18,13 @@ import { useSessionStorage, } from "@mantine/hooks" import "maplibre-gl/dist/maplibre-gl.css" -import Map from "react-map-gl/maplibre" +import Map, { Layer, Marker, Source } from "react-map-gl/maplibre" + +// Assets +import arrow from "../../assets/arrow.svg" // Helper scripts -import { intToCoord } from "../../helpers/dataFormatters" -import { filterMissionItems } from "../../helpers/filterMissions" +import { FILTER_MISSION_ITEM_COMMANDS_LIST } from "../../helpers/mavlinkConstants" import { showErrorNotification, showNotification, @@ -31,21 +33,71 @@ import { import { socket } from "../../helpers/socket" // Other dashboard imports -import DrawLineCoordinates from "../mapComponents/drawLineCoordinates" -import DroneMarker from "../mapComponents/droneMarker" -import HomeMarker from "../mapComponents/homeMarker" -import MarkerPin from "../mapComponents/markerPin" -import MissionItems from "../mapComponents/missionItems" import ContextMenuItem from "./contextMenuItem" +import MissionItems from "./missionItems" import useContextMenu from "./useContextMenu" // Tailwind styling import resolveConfig from "tailwindcss/resolveConfig" import tailwindConfig from "../../../tailwind.config" -import { useSettings } from "../../helpers/settings" const tailwindColors = resolveConfig(tailwindConfig).theme.colors -function MapSectionNonMemo({ +// Convert coordinates from mavlink into gps coordinates +export function intToCoord(val) { + return val * 1e-7 +} + +function degToRad(deg) { + return deg * (Math.PI / 180) +} + +function radToDeg(rad) { + return rad * (180 / Math.PI) +} + +function getPointAtDistance(lat1, lon1, distance, bearing) { + // https://stackoverflow.com/questions/7222382/get-lat-long-given-current-point-distance-and-bearing + + const R = 6378.14 // Radius of the Earth in km + const brng = degToRad(bearing) + const lat1Rad = degToRad(lat1) + const lon1Rad = degToRad(lon1) + const lat2 = Math.asin( + Math.sin(lat1Rad) * Math.cos(distance / R) + + Math.cos(lat1Rad) * Math.sin(distance / R) * Math.cos(brng), + ) + const lon2 = + lon1Rad + + Math.atan2( + Math.sin(brng) * Math.sin(distance / R) * Math.cos(lat1Rad), + Math.cos(distance / R) - Math.sin(lat1Rad) * Math.sin(lat2), + ) + + return [radToDeg(lat2), radToDeg(lon2)] +} + +export function filterMissionItems(missionItems) { + if (!missionItems || !missionItems.length) return [] + + // remove first mission item as it's a home point, for some reason the home + // position is denoted MAV_CMD_NAV_WAYPOINT command (why??) + if ( + missionItems[0].command == 16 && + missionItems[0].frame == 0 && + missionItems[0].seq == 0 + ) { + missionItems.shift() + } + + return missionItems.filter( + (missionItem) => + !Object.values(FILTER_MISSION_ITEM_COMMANDS_LIST).includes( + missionItem.command, + ), + ) +} + +export default function MapSection({ passedRef, data, heading, @@ -55,7 +107,7 @@ function MapSectionNonMemo({ onDragstart, getFlightMode, }) { - const [connected] = useSessionStorage({ + const [connected] = useLocalStorage({ key: "connectedToDrone", defaultValue: false, }) @@ -80,8 +132,6 @@ function MapSectionNonMemo({ const [opened, { open, close }] = useDisclosure(false) const clipboard = useClipboard({ timeout: 500 }) - const { getSetting } = useSettings() - const [repositionAltitude, setRepositionAltitude] = useLocalStorage({ key: "repositionAltitude", defaultValue: 30, @@ -91,8 +141,6 @@ function MapSectionNonMemo({ defaultValue: null, }) - const coordsFractionDigits = 7 - useEffect(() => { socket.on("nav_reposition_result", (msg) => { if (!msg.success) { @@ -172,7 +220,9 @@ function MapSectionNonMemo({
+ <> + + + + + + + + + + + )} {/* Show home position */} {homePosition !== null && ( - 0 && [ - intToCoord(filteredMissionItems[0].y), - intToCoord(filteredMissionItems[0].x), - ] - } - /> + <> + + + + + H + + + + {filteredMissionItems.length > 0 && ( + + + + )} + )} @@ -232,55 +404,118 @@ function MapSectionNonMemo({ {/* Show mission geo-fence MARKERS */} {missionItems.fence_items.map((item, index) => { return ( - + longitude={intToCoord(item.y)} + latitude={intToCoord(item.x)} + > + + + + ) })} {/* Show geo-fence outlines */} {missionItems.fence_items.length > 0 && ( - [ - intToCoord(item.y), - intToCoord(item.x), - ]), - [ - intToCoord(missionItems.fence_items[0].y), - intToCoord(missionItems.fence_items[0].x), - ], - ]} - colour={tailwindColors.blue[200]} - lineProps={{ "line-dasharray": [2, 2] }} - /> + [ + intToCoord(item.y), + intToCoord(item.x), + ]), + [ + intToCoord(missionItems.fence_items[0].y), + intToCoord(missionItems.fence_items[0].x), + ], + ], + }, + }} + > + + )} {/* Show mission rally point */} {missionItems.rally_items.map((item, index) => { return ( - + longitude={intToCoord(item.y)} + latitude={intToCoord(item.x)} + > + + + + + + ) })} {getFlightMode() === "Guided" && guidedModePinData !== null && ( - + + + + + + + )} @@ -313,44 +548,20 @@ function MapSectionNonMemo({ className="absolute bg-falcongrey-700 rounded-md p-1" style={{ top: points.y, left: points.x }} > - Fly to here + { clipboard.copy( `${clickedGpsCoords.lat}, ${clickedGpsCoords.lng}`, ) showNotification("Copied to clipboard") }} - > -
-

- {clickedGpsCoords.lat.toFixed(coordsFractionDigits)},{" "} - {clickedGpsCoords.lng.toFixed(coordsFractionDigits)} -

- - - -
-
+ />
)} ) -} -function propsAreEqual(prev, next) { - return JSON.stringify(prev) === JSON.stringify(next) -} -const MapSection = React.memo(MapSectionNonMemo, propsAreEqual) - -export default MapSection +} \ No newline at end of file diff --git a/radio/app/controllers/missionController.py b/radio/app/controllers/missionController.py index 1674b2001..b5be6be25 100644 --- a/radio/app/controllers/missionController.py +++ b/radio/app/controllers/missionController.py @@ -1,15 +1,21 @@ from __future__ import annotations +import os from typing import TYPE_CHECKING, Any, List import serial from app.customTypes import Response -from app.utils import commandAccepted -from pymavlink import mavutil +from app.utils import commandAccepted, wpToMissionItemInt +from pymavlink import mavutil, mavwp if TYPE_CHECKING: from app.drone import Drone +TYPE_MISSION = mavutil.mavlink.MAV_MISSION_TYPE_MISSION +TYPE_FENCE = mavutil.mavlink.MAV_MISSION_TYPE_FENCE +TYPE_RALLY = mavutil.mavlink.MAV_MISSION_TYPE_RALLY +MISSION_TYPES = [TYPE_MISSION, TYPE_FENCE, TYPE_RALLY] + class MissionController: def __init__(self, drone: Drone) -> None: @@ -25,40 +31,65 @@ def __init__(self, drone: Drone) -> None: self.mission_items: List[Any] = [] self.fence_items: List[Any] = [] self.rally_items: List[Any] = [] + self.missionLoader = mavwp.MAVWPLoader( + target_system=drone.target_system, target_component=drone.target_component + ) + self.fenceLoader = mavwp.MissionItemProtocol_Fence( + target_system=drone.target_system, target_component=drone.target_component + ) + self.rallyLoader = mavwp.MissionItemProtocol_Rally( + target_system=drone.target_system, target_component=drone.target_component + ) - mission_items = self.getMissionItems() + mission_items = self.getMissionItems(mission_type=TYPE_MISSION) if not mission_items.get("success"): self.drone.logger.warning(mission_items.get("message")) return else: self.mission_items = mission_items.get("data", []) - fence_items = self.getMissionItems(mission_type=1) + fence_items = self.getMissionItems(mission_type=TYPE_FENCE) if not fence_items.get("success"): self.drone.logger.warning(fence_items.get("message")) return else: self.fence_items = fence_items.get("data", []) - rally_items = self.getMissionItems(mission_type=2) + rally_items = self.getMissionItems(mission_type=TYPE_RALLY) if not rally_items.get("success"): self.drone.logger.warning(rally_items.get("message")) return else: self.rally_items = rally_items.get("data", []) - def getMissionItems(self, mission_type: int = 0) -> Response: + def _checkMissionType(self, mission_type: int) -> Response: + if mission_type not in MISSION_TYPES: + return { + "success": False, + "message": f"Invalid mission type {mission_type}. Must be one of {MISSION_TYPES}", + } + return {"success": True} + + def getMissionItems(self, mission_type: int) -> Response: """ Get all mission items of a specific type from the drone. Args: - mission_type (int, optional): The type of mission to get. 0=Mission,1=Fence,2=Rally. + mission_type (int): The type of mission to get. 0=Mission,1=Fence,2=Rally. """ + mission_type_check = self._checkMissionType(mission_type) + if not mission_type_check.get("success"): + return mission_type_check + failure_message = "Could not get current mission items" - items = [] + if mission_type == TYPE_MISSION: + loader = self.missionLoader + elif mission_type == TYPE_FENCE: + loader = self.fenceLoader + else: + loader = self.rallyLoader - # TODO: Try send custom mission request list command? try: self.drone.master.mav.mission_request_list_send( self.drone.target_system, @@ -90,21 +121,14 @@ def getMissionItems(self, mission_type: int = 0) -> Response: "message": item_response.get("message", failure_message), } - if ( - self.drone.autopilot - == mavutil.mavlink.MAV_AUTOPILOT_ARDUPILOTMEGA - and i == 0 - and mission_type == 0 - ): - item_response_data = item_response.get("data") - if item_response_data and item_response_data.frame == 0: - continue + item_response_data = item_response.get("data", None) - items.append(item_response.get("data")) + if item_response_data: + loader.add(item_response_data) return { "success": True, - "data": items, + "data": loader.wpoints, } else: @@ -119,17 +143,21 @@ def getMissionItems(self, mission_type: int = 0) -> Response: "message": f"{failure_message}, serial exception", } - def getItemDetails(self, item_number: int, mission_type: int = 0) -> Response: + def getItemDetails(self, item_number: int, mission_type: int) -> Response: """ Get the details of a specific mission item. Args: item_number (int): The number of the mission item to get - mission_type (int, optional): The type of mission to get. 0=Mission,1=Fence,2=Rally. + mission_type (int): The type of mission to get. 0=Mission,1=Fence,2=Rally. Returns: Dict: The details of the mission item """ + mission_type_check = self._checkMissionType(mission_type) + if not mission_type_check.get("success"): + return mission_type_check + failure_message = ( f"Failed to get mission item {item_number} for mission type {mission_type}" ) @@ -235,3 +263,203 @@ def restartMission(self) -> Response: "success": False, "message": "Failed to restart mission, serial exception", } + + def clearMission(self, mission_type: int) -> Response: + """ + Clears the specified mission type from the drone. + + Args: + mission_type (int): The type of mission to clear. 0=Mission,1=Fence,2=Rally. + """ + mission_type_check = self._checkMissionType(mission_type) + if not mission_type_check.get("success"): + return mission_type_check + + self.drone.is_listening = False + self.drone.master.mav.mission_clear_all_send( + self.drone.target_system, + self.drone.target_component, + mission_type=mission_type, + ) + try: + while True: + response = self.drone.master.recv_match( + type=[ + "MISSION_ACK", + ], + blocking=True, + timeout=2, + ) + if not response: + break + elif response.mission_type != mission_type: + continue + elif response.type == 0: + self.drone.is_listening = True + + return { + "success": True, + "message": "Mission cleared successfully", + } + else: + self.drone.logger.error( + f"Error clearing mission, mission ack response: {response.type}" + ) + break + + self.drone.is_listening = True + return { + "success": False, + "message": "Could not clear mission", + } + + except serial.serialutil.SerialException: + self.drone.is_listening = True + return { + "success": False, + "message": "Could not clear mission, serial exception", + } + + def loadWaypointFile(self, file_path: str, mission_type: int) -> Response: + """ + Loads waypoints from a file into the specified mission type. + + Args: + file_path (str): The path to the waypoint file + mission_type (int): The type of mission to load the waypoints into. 0=Mission,1=Fence,2=Rally. + """ + mission_type_check = self._checkMissionType(mission_type) + if not mission_type_check.get("success"): + return mission_type_check + + if not os.path.exists(file_path): + self.drone.logger.error(f"Waypoint file not found at {file_path}") + return { + "success": False, + "message": f"Waypoint file not found at {file_path}", + } + + if mission_type == TYPE_MISSION: + loader = self.missionLoader + elif mission_type == TYPE_FENCE: + loader = self.fenceLoader + else: + loader = self.rallyLoader + + loader.load(file_path) + + # Remove the first point if it's a command 16 as this is usually a home point or placeholder. + if mission_type in [TYPE_FENCE, TYPE_RALLY]: + first_wp = loader.item(0) + if first_wp.command == 16: + loader.remove(first_wp) + + self.drone.logger.info( + f"Loaded waypoint file with {loader.count()} points successfully" + ) + return { + "success": True, + "message": f"Waypoint file loaded {loader.count()} points successfully", + } + + def uploadMission(self, mission_type: int) -> Response: + """ + Uploads the current mission to the drone. + + Args: + mission_type (int): The type of mission to upload. 0=Mission,1=Fence,2=Rally. + """ + mission_type_check = self._checkMissionType(mission_type) + if not mission_type_check.get("success"): + return mission_type_check + + if mission_type == TYPE_MISSION: + loader = self.missionLoader + elif mission_type == TYPE_FENCE: + loader = self.fenceLoader + else: + loader = self.rallyLoader + + if loader.count() == 0: + return { + "success": False, + "message": f"No waypoints loaded for the mission type of {mission_type}", + } + + clear_mission_response = self.clearMission(mission_type) + if not clear_mission_response.get("success"): + return clear_mission_response + + self.drone.is_listening = False + + self.drone.master.mav.mission_count_send( + self.drone.target_system, + self.drone.target_component, + loader.count(), + mission_type=mission_type, + ) + + try: + while True: + response = self.drone.master.recv_match( + type=["MISSION_REQUEST", "MISSION_ACK"], + blocking=True, + timeout=2, + ) + if not response: + self.drone.is_listening = True + + return { + "success": False, + "message": "Could not upload mission, mission request not received", + } + elif response.msgname == "MISSION_ACK" and response.type != 0: + self.drone.logger.error( + f"Error uploading mission, mission ack response: {response.type}" + ) + return { + "success": False, + "message": "Could not upload mission, received mission acknowledgement error", + } + elif response.mission_type == mission_type: + self.drone.logger.debug( + f"Sending mission item {response.seq} out of {loader.count()}" + ) + self.drone.master.mav.send( + wpToMissionItemInt(loader.item(response.seq)) + ) + + if response.seq == loader.count() - 1: + mission_ack_response = self.drone.master.recv_match( + type=[ + "MISSION_ACK", + ], + blocking=True, + timeout=2, + ) + self.drone.is_listening = True + + if ( + mission_ack_response + and mission_ack_response.type == 0 + and mission_ack_response.mission_type == mission_type + ): + self.drone.logger.info("Uploaded mission successfully") + return { + "success": True, + "message": "Mission uploaded successfully", + } + else: + self.drone.logger.error( + f"Error uploading mission, mission ack response: {mission_ack_response.type}" + ) + return { + "success": False, + "message": "Could not upload mission, not received mission acknowledgement", + } + except serial.serialutil.SerialException: + self.drone.is_listening = True + return { + "success": False, + "message": "Could not upload mission, serial exception", + } diff --git a/radio/app/controllers/navController.py b/radio/app/controllers/navController.py index 2bdfddf43..b2f366026 100644 --- a/radio/app/controllers/navController.py +++ b/radio/app/controllers/navController.py @@ -22,6 +22,9 @@ def __init__(self, drone: Drone) -> None: self.drone = drone def getHomePosition(self) -> Response: + """ + Request the current home position from the drone. + """ self.drone.is_listening = False self.drone.sendCommand( mavutil.mavlink.MAV_CMD_REQUEST_MESSAGE, @@ -60,6 +63,50 @@ def getHomePosition(self) -> Response: "message": "Could not get home position, serial exception", } + def setHomePosition(self, lat: float, lon: float, alt: float) -> Response: + """ + Set the home point of the drone. + + Args: + lat (float): The latitude of the home point + lon (float): The longitude of the home point + alt (float): The altitude of the home point + """ + + self.drone.is_listening = False + self.drone.sendCommandInt( + mavutil.mavlink.MAV_CMD_DO_SET_HOME, x=lat, y=lon, z=alt + ) + + try: + response = self.drone.master.recv_match( + type=[ + "COMMAND_ACK", + ], + blocking=True, + timeout=2, + ) + self.drone.is_listening = True + + if commandAccepted(response, mavutil.mavlink.MAV_CMD_DO_SET_HOME): + return { + "success": True, + "message": "Home point set successfully", + } + + else: + return { + "success": False, + "message": "Could not set home point", + } + + except serial.serialutil.SerialException: + self.drone.is_listening = True + return { + "success": False, + "message": "Could not set home point, serial exception", + } + def takeoff(self, alt: float) -> Response: """ Tells the drone to takeoff to a specified altitude. diff --git a/radio/app/utils.py b/radio/app/utils.py index a9d7dc936..3fc9bd87a 100644 --- a/radio/app/utils.py +++ b/radio/app/utils.py @@ -1,5 +1,5 @@ import sys -from typing import Any, List, Optional, Union +from typing import Any, List from pymavlink import mavutil from serial.tools import list_ports @@ -138,12 +138,12 @@ def droneConnectStatusCb(msg: Any) -> None: socketio.emit("drone_connect_status", {"message": msg}) -def notConnectedError(action: Optional[str] = None) -> None: +def notConnectedError(action: str | None = None) -> None: """ Send error to the socket indicating that drone connection must be established to complete this action Args: - action Optional[str]: The action the that requires drone connection. Default `None`. + action (str | None): The action the that requires drone connection. Default `None`. """ socketio.emit( "connection_error", @@ -153,13 +153,13 @@ def notConnectedError(action: Optional[str] = None) -> None: ) -def missingParameterError(endpoint: str, params: Union[str, List[str]]) -> None: +def missingParameterError(endpoint: str, params: str | list[str]) -> None: """ " Send error to the socket indicating that a request made to the server was missing required parameters Args endpoint (str): The endpoint that is missing a parameter - params Union[str, List[str]]: The names of the parameter/s that are missing from the request + params (str | list[str]): The names of the parameter/s that are missing from the request """ socketio.emit( "drone_error", @@ -181,3 +181,29 @@ def sendMessage(msg: Any) -> None: data = msg.to_dict() data["timestamp"] = msg._timestamp socketio.emit("incoming_msg", data) + + +def wpToMissionItemInt( + wp: mavutil.mavlink.MAVLink_message, +) -> mavutil.mavlink.MAVLink_message: + if wp.get_type() == "MISSION_ITEM_INT": + return wp + + wp_int = mavutil.mavlink.MAVLink_mission_item_int_message( + wp.target_system, + wp.target_component, + wp.seq, + wp.frame, + wp.command, + wp.current, + wp.autocontinue, + wp.param1, + wp.param2, + wp.param3, + wp.param4, + int(wp.x * 1e7), + int(wp.y * 1e7), + wp.z, + wp.mission_type, + ) + return wp_int diff --git a/radio/tests/test_mission.py b/radio/tests/test_mission.py index 919726cb1..bd70a0686 100644 --- a/radio/tests/test_mission.py +++ b/radio/tests/test_mission.py @@ -29,6 +29,24 @@ def test_getCurrentMission_correctState( # pytest.skip(reason="Sending mission to simulator is currently bugged and fails sometimes") assert socketio_result["args"][0] == { "mission_items": [ + { + "autocontinue": 1, + "command": 16, + "current": 0, + "frame": 0, + "mavpackettype": "MISSION_ITEM_INT", + "mission_type": 0, + "param1": 0.0, + "param2": 0.0, + "param3": 0.0, + "param4": 0.0, + "seq": 0, + "target_component": 0, + "target_system": 255, + "x": 527805690, + "y": -7079236, + "z": 0.09999999403953552, + }, { "mavpackettype": "MISSION_ITEM_INT", "target_system": 255, From 5a1fb512a1f6f0bc3fb347c6adc338fee01c3323 Mon Sep 17 00:00:00 2001 From: Julian Jones <37962677+NexInfinite@users.noreply.github.com> Date: Sat, 8 Mar 2025 16:34:50 +0000 Subject: [PATCH 02/16] Fixing kush's branch (#539) * fixes * Fix map.jsx --------- Co-authored-by: Kush Makkapati --- gcs/src/components/dashboard/map.jsx | 405 +++++++-------------------- 1 file changed, 97 insertions(+), 308 deletions(-) diff --git a/gcs/src/components/dashboard/map.jsx b/gcs/src/components/dashboard/map.jsx index 0c1d14844..afd615882 100644 --- a/gcs/src/components/dashboard/map.jsx +++ b/gcs/src/components/dashboard/map.jsx @@ -7,10 +7,10 @@ */ // Base imports -import { useEffect, useRef, useState } from "react" +import React, { useEffect, useRef, useState } from "react" // Maplibre and mantine imports -import { Button, Divider, Modal, NumberInput, Tooltip } from "@mantine/core" +import { Button, Divider, Modal, NumberInput } from "@mantine/core" import { useClipboard, useDisclosure, @@ -18,13 +18,11 @@ import { useSessionStorage, } from "@mantine/hooks" import "maplibre-gl/dist/maplibre-gl.css" -import Map, { Layer, Marker, Source } from "react-map-gl/maplibre" - -// Assets -import arrow from "../../assets/arrow.svg" +import Map from "react-map-gl/maplibre" // Helper scripts -import { FILTER_MISSION_ITEM_COMMANDS_LIST } from "../../helpers/mavlinkConstants" +import { intToCoord } from "../../helpers/dataFormatters" +import { filterMissionItems } from "../../helpers/filterMissions" import { showErrorNotification, showNotification, @@ -33,71 +31,21 @@ import { import { socket } from "../../helpers/socket" // Other dashboard imports +import DrawLineCoordinates from "../mapComponents/drawLineCoordinates" +import DroneMarker from "../mapComponents/droneMarker" +import HomeMarker from "../mapComponents/homeMarker" +import MarkerPin from "../mapComponents/markerPin" +import MissionItems from "../mapComponents/missionItems" import ContextMenuItem from "./contextMenuItem" -import MissionItems from "./missionItems" import useContextMenu from "./useContextMenu" // Tailwind styling import resolveConfig from "tailwindcss/resolveConfig" import tailwindConfig from "../../../tailwind.config" +import { useSettings } from "../../helpers/settings" const tailwindColors = resolveConfig(tailwindConfig).theme.colors -// Convert coordinates from mavlink into gps coordinates -export function intToCoord(val) { - return val * 1e-7 -} - -function degToRad(deg) { - return deg * (Math.PI / 180) -} - -function radToDeg(rad) { - return rad * (180 / Math.PI) -} - -function getPointAtDistance(lat1, lon1, distance, bearing) { - // https://stackoverflow.com/questions/7222382/get-lat-long-given-current-point-distance-and-bearing - - const R = 6378.14 // Radius of the Earth in km - const brng = degToRad(bearing) - const lat1Rad = degToRad(lat1) - const lon1Rad = degToRad(lon1) - const lat2 = Math.asin( - Math.sin(lat1Rad) * Math.cos(distance / R) + - Math.cos(lat1Rad) * Math.sin(distance / R) * Math.cos(brng), - ) - const lon2 = - lon1Rad + - Math.atan2( - Math.sin(brng) * Math.sin(distance / R) * Math.cos(lat1Rad), - Math.cos(distance / R) - Math.sin(lat1Rad) * Math.sin(lat2), - ) - - return [radToDeg(lat2), radToDeg(lon2)] -} - -export function filterMissionItems(missionItems) { - if (!missionItems || !missionItems.length) return [] - - // remove first mission item as it's a home point, for some reason the home - // position is denoted MAV_CMD_NAV_WAYPOINT command (why??) - if ( - missionItems[0].command == 16 && - missionItems[0].frame == 0 && - missionItems[0].seq == 0 - ) { - missionItems.shift() - } - - return missionItems.filter( - (missionItem) => - !Object.values(FILTER_MISSION_ITEM_COMMANDS_LIST).includes( - missionItem.command, - ), - ) -} - -export default function MapSection({ +function MapSectionNonMemo({ passedRef, data, heading, @@ -107,7 +55,7 @@ export default function MapSection({ onDragstart, getFlightMode, }) { - const [connected] = useLocalStorage({ + const [connected] = useSessionStorage({ key: "connectedToDrone", defaultValue: false, }) @@ -132,6 +80,8 @@ export default function MapSection({ const [opened, { open, close }] = useDisclosure(false) const clipboard = useClipboard({ timeout: 500 }) + const { getSetting } = useSettings() + const [repositionAltitude, setRepositionAltitude] = useLocalStorage({ key: "repositionAltitude", defaultValue: 30, @@ -141,6 +91,8 @@ export default function MapSection({ defaultValue: null, }) + const coordsFractionDigits = 7 + useEffect(() => { socket.on("nav_reposition_result", (msg) => { if (!msg.success) { @@ -220,9 +172,7 @@ export default function MapSection({
- - - - - - - - - - - + )} {/* Show home position */} {homePosition !== null && ( - <> - - - - - H - - - - {filteredMissionItems.length > 0 && ( - - - - )} - + 0 && [ + intToCoord(filteredMissionItems[0].y), + intToCoord(filteredMissionItems[0].x), + ] + } + /> )} @@ -404,118 +232,55 @@ export default function MapSection({ {/* Show mission geo-fence MARKERS */} {missionItems.fence_items.map((item, index) => { return ( - - - - - + lat={intToCoord(item.x)} + lon={intToCoord(item.y)} + colour={tailwindColors.blue[400]} + /> ) })} {/* Show geo-fence outlines */} {missionItems.fence_items.length > 0 && ( - [ - intToCoord(item.y), - intToCoord(item.x), - ]), - [ - intToCoord(missionItems.fence_items[0].y), - intToCoord(missionItems.fence_items[0].x), - ], - ], - }, - }} - > - - + [ + intToCoord(item.y), + intToCoord(item.x), + ]), + [ + intToCoord(missionItems.fence_items[0].y), + intToCoord(missionItems.fence_items[0].x), + ], + ]} + colour={tailwindColors.blue[200]} + lineProps={{ "line-dasharray": [2, 2] }} + /> )} {/* Show mission rally point */} {missionItems.rally_items.map((item, index) => { return ( - - - - - - - + lat={intToCoord(item.x)} + lon={intToCoord(item.y)} + colour={tailwindColors.purple[400]} + tooltipText={item.z ? `Alt: ${item.z}` : null} + /> ) })} {getFlightMode() === "Guided" && guidedModePinData !== null && ( - - - - - - - + )} @@ -548,20 +313,44 @@ export default function MapSection({ className="absolute bg-falcongrey-700 rounded-md p-1" style={{ top: points.y, left: points.x }} > - + Fly to here { clipboard.copy( `${clickedGpsCoords.lat}, ${clickedGpsCoords.lng}`, ) showNotification("Copied to clipboard") }} - /> + > +
+

+ {clickedGpsCoords.lat.toFixed(coordsFractionDigits)},{" "} + {clickedGpsCoords.lng.toFixed(coordsFractionDigits)} +

+ + + +
+
)} ) -} \ No newline at end of file +} +function propsAreEqual(prev, next) { + return JSON.stringify(prev) === JSON.stringify(next) +} +const MapSection = React.memo(MapSectionNonMemo, propsAreEqual) + +export default MapSection From 1b5dd00879d975b9002f645d81e248dc572e39ca Mon Sep 17 00:00:00 2001 From: "Kwashie A." <104215256+Kwash67@users.noreply.github.com> Date: Sun, 16 Mar 2025 13:05:05 +0000 Subject: [PATCH 03/16] Alpha 0.1.9/523 new missions page (#545) * added map, resizable boxes, and added hotkeys to switch to page * display spotlight action for missions * allow user to decide whether maps should be in sync * removing usused vars and formatting * fixing lint errors * format * this better work * Separate missions map component, update general stuff * Fix linting issues --------- Co-authored-by: Kush Makkapati --- gcs/data/default_settings.json | 5 + gcs/src/components/dashboard/map.jsx | 35 +- .../contextMenuItem.jsx | 0 .../useContextMenu.jsx | 0 gcs/src/components/missions/missionsMap.jsx | 299 ++++++++++++++++++ gcs/src/components/navbar.jsx | 9 + gcs/src/components/spotlight/actions.jsx | 28 +- .../components/spotlight/commandHandler.js | 12 +- gcs/src/dashboard.jsx | 6 +- gcs/src/main.jsx | 2 + gcs/src/missions.jsx | 178 +++++++++++ radio/app/endpoints/states.py | 11 + 12 files changed, 559 insertions(+), 26 deletions(-) rename gcs/src/components/{dashboard => mapComponents}/contextMenuItem.jsx (100%) rename gcs/src/components/{dashboard => mapComponents}/useContextMenu.jsx (100%) create mode 100644 gcs/src/components/missions/missionsMap.jsx create mode 100644 gcs/src/missions.jsx diff --git a/gcs/data/default_settings.json b/gcs/data/default_settings.json index fc1ccbcca..850020dd5 100644 --- a/gcs/data/default_settings.json +++ b/gcs/data/default_settings.json @@ -25,6 +25,11 @@ "default": true, "type": "boolean", "display": "Automatically Check for Updates" + }, + "syncMapViews": { + "default": false, + "type": "boolean", + "display": "Sync Dashboard and Missions Map Viewstate" } }, "Params": {}, diff --git a/gcs/src/components/dashboard/map.jsx b/gcs/src/components/dashboard/map.jsx index afd615882..910cab32d 100644 --- a/gcs/src/components/dashboard/map.jsx +++ b/gcs/src/components/dashboard/map.jsx @@ -28,23 +28,25 @@ import { showNotification, showSuccessNotification, } from "../../helpers/notification" +import { useSettings } from "../../helpers/settings" import { socket } from "../../helpers/socket" // Other dashboard imports +import ContextMenuItem from "../mapComponents/contextMenuItem" import DrawLineCoordinates from "../mapComponents/drawLineCoordinates" import DroneMarker from "../mapComponents/droneMarker" import HomeMarker from "../mapComponents/homeMarker" import MarkerPin from "../mapComponents/markerPin" import MissionItems from "../mapComponents/missionItems" -import ContextMenuItem from "./contextMenuItem" -import useContextMenu from "./useContextMenu" +import useContextMenu from "../mapComponents/useContextMenu" // Tailwind styling import resolveConfig from "tailwindcss/resolveConfig" import tailwindConfig from "../../../tailwind.config" -import { useSettings } from "../../helpers/settings" const tailwindColors = resolveConfig(tailwindConfig).theme.colors +const coordsFractionDigits = 7 + function MapSectionNonMemo({ passedRef, data, @@ -54,6 +56,7 @@ function MapSectionNonMemo({ homePosition, onDragstart, getFlightMode, + mapId = "dashboard", }) { const [connected] = useSessionStorage({ key: "connectedToDrone", @@ -62,11 +65,27 @@ function MapSectionNonMemo({ const [position, setPosition] = useState(null) const [firstCenteredToDrone, setFirstCenteredToDrone] = useState(false) + const { getSetting } = useSettings() + + // Check if maps should be synchronized (from settings) + const syncMaps = getSetting("General.syncMapViews") || false + + // Use either a shared key or a unique key based on the setting + const viewStateKey = syncMaps + ? "initialViewState" + : `initialViewState_${mapId}` + const [initialViewState, setInitialViewState] = useLocalStorage({ - key: "initialViewState", + key: viewStateKey, defaultValue: { latitude: 53.381655, longitude: -1.481434, zoom: 17 }, getInitialValueInEffect: false, }) + + const [repositionAltitude, setRepositionAltitude] = useLocalStorage({ + key: "repositionAltitude", + defaultValue: 30, + }) + const [filteredMissionItems, setFilteredMissionItems] = useState([]) const contextMenuRef = useRef() @@ -80,19 +99,11 @@ function MapSectionNonMemo({ const [opened, { open, close }] = useDisclosure(false) const clipboard = useClipboard({ timeout: 500 }) - const { getSetting } = useSettings() - - const [repositionAltitude, setRepositionAltitude] = useLocalStorage({ - key: "repositionAltitude", - defaultValue: 30, - }) const [guidedModePinData, setGuidedModePinData] = useSessionStorage({ key: "guidedModePinData", defaultValue: null, }) - const coordsFractionDigits = 7 - useEffect(() => { socket.on("nav_reposition_result", (msg) => { if (!msg.success) { diff --git a/gcs/src/components/dashboard/contextMenuItem.jsx b/gcs/src/components/mapComponents/contextMenuItem.jsx similarity index 100% rename from gcs/src/components/dashboard/contextMenuItem.jsx rename to gcs/src/components/mapComponents/contextMenuItem.jsx diff --git a/gcs/src/components/dashboard/useContextMenu.jsx b/gcs/src/components/mapComponents/useContextMenu.jsx similarity index 100% rename from gcs/src/components/dashboard/useContextMenu.jsx rename to gcs/src/components/mapComponents/useContextMenu.jsx diff --git a/gcs/src/components/missions/missionsMap.jsx b/gcs/src/components/missions/missionsMap.jsx new file mode 100644 index 000000000..adf9725a4 --- /dev/null +++ b/gcs/src/components/missions/missionsMap.jsx @@ -0,0 +1,299 @@ +/* + The missions map. + + This uses maplibre to load the map, currently (as of 16/03/2025) this needs an internet + connection to load but this will be addressed in later versions of FGCS. Please check + docs/changelogs if this description has not been updated. +*/ + +// Base imports +import React, { useEffect, useRef, useState } from "react" + +// Maplibre and mantine imports +import { + useClipboard, + useLocalStorage, + useSessionStorage, +} from "@mantine/hooks" +import "maplibre-gl/dist/maplibre-gl.css" +import Map from "react-map-gl/maplibre" + +// Helper scripts +import { intToCoord } from "../../helpers/dataFormatters" +import { filterMissionItems } from "../../helpers/filterMissions" +import { showNotification } from "../../helpers/notification" +import { useSettings } from "../../helpers/settings" + +// Other dashboard imports +import ContextMenuItem from "../mapComponents/contextMenuItem" +import DrawLineCoordinates from "../mapComponents/drawLineCoordinates" +import DroneMarker from "../mapComponents/droneMarker" +import HomeMarker from "../mapComponents/homeMarker" +import MarkerPin from "../mapComponents/markerPin" +import MissionItems from "../mapComponents/missionItems" +import useContextMenu from "../mapComponents/useContextMenu" + +// Tailwind styling +import resolveConfig from "tailwindcss/resolveConfig" +import tailwindConfig from "../../../tailwind.config" +const tailwindColors = resolveConfig(tailwindConfig).theme.colors + +const coordsFractionDigits = 7 + +function MapSectionNonMemo({ + passedRef, + data, + heading, + desiredBearing, + missionItems, + homePosition, + onDragstart, + getFlightMode, + mapId = "dashboard", +}) { + const [connected] = useSessionStorage({ + key: "connectedToDrone", + defaultValue: false, + }) + const [guidedModePinData] = useSessionStorage({ + key: "guidedModePinData", + defaultValue: null, + }) + + const [position, setPosition] = useState(null) + const { getSetting } = useSettings() + + // Check if maps should be synchronized (from settings) + const syncMaps = getSetting("General.syncMapViews") || false + + // Use either a shared key or a unique key based on the setting + const viewStateKey = syncMaps + ? "initialViewState" + : `initialViewState_${mapId}` + + const [initialViewState, setInitialViewState] = useLocalStorage({ + key: viewStateKey, + defaultValue: { latitude: 53.381655, longitude: -1.481434, zoom: 17 }, + getInitialValueInEffect: false, + }) + + const [filteredMissionItems, setFilteredMissionItems] = useState([]) + + const contextMenuRef = useRef() + const { clicked, setClicked, points, setPoints } = useContextMenu() + const [ + contextMenuPositionCalculationInfo, + setContextMenuPositionCalculationInfo, + ] = useState() + const [clickedGpsCoords, setClickedGpsCoords] = useState({ lng: 0, lat: 0 }) + + const clipboard = useClipboard({ timeout: 500 }) + + useEffect(() => { + return () => {} + }, [connected]) + + useEffect(() => { + // Check latest data point is valid + if (isNaN(data.lat) || isNaN(data.lon) || data.lon === 0 || data.lat === 0) + return + + // Move drone icon on map + let lat = intToCoord(data.lat) + let lon = intToCoord(data.lon) + setPosition({ latitude: lat, longitude: lon }) + }, [data]) + + useEffect(() => { + setFilteredMissionItems(filterMissionItems(missionItems.mission_items)) + }, [missionItems]) + + useEffect(() => { + if (contextMenuRef.current) { + const contextMenuWidth = Math.round( + contextMenuRef.current.getBoundingClientRect().width, + ) + const contextMenuHeight = Math.round( + contextMenuRef.current.getBoundingClientRect().height, + ) + let x = contextMenuPositionCalculationInfo.clickedPoint.x + let y = contextMenuPositionCalculationInfo.clickedPoint.y + + if ( + contextMenuWidth + contextMenuPositionCalculationInfo.clickedPoint.x > + contextMenuPositionCalculationInfo.canvasSize.width + ) { + x = contextMenuPositionCalculationInfo.clickedPoint.x - contextMenuWidth + } + if ( + contextMenuHeight + contextMenuPositionCalculationInfo.clickedPoint.y > + contextMenuPositionCalculationInfo.canvasSize.height + ) { + y = + contextMenuPositionCalculationInfo.clickedPoint.y - contextMenuHeight + } + + setPoints({ x, y }) + } + }, [contextMenuPositionCalculationInfo]) + + return ( +
+ + setInitialViewState({ + latitude: newViewState.viewState.latitude, + longitude: newViewState.viewState.longitude, + zoom: newViewState.viewState.zoom, + }) + } + onDragStart={onDragstart} + onContextMenu={(e) => { + e.preventDefault() + setClicked(true) + setClickedGpsCoords(e.lngLat) + setContextMenuPositionCalculationInfo({ + clickedPoint: e.point, + canvasSize: { + height: e.originalEvent.target.clientHeight, + width: e.originalEvent.target.clientWidth, + }, + }) + }} + cursor="default" + > + {/* Show marker on map if the position is set */} + {position !== null && + !isNaN(position?.latitude) && + !isNaN(position?.longitude) && ( + + )} + + {/* Show home position */} + {homePosition !== null && ( + 0 && [ + intToCoord(filteredMissionItems[0].y), + intToCoord(filteredMissionItems[0].x), + ] + } + /> + )} + + + + {/* Show mission geo-fence MARKERS */} + {missionItems.fence_items.map((item, index) => { + return ( + + ) + })} + + {/* Show geo-fence outlines */} + {missionItems.fence_items.length > 0 && ( + [ + intToCoord(item.y), + intToCoord(item.x), + ]), + [ + intToCoord(missionItems.fence_items[0].y), + intToCoord(missionItems.fence_items[0].x), + ], + ]} + colour={tailwindColors.blue[200]} + lineProps={{ "line-dasharray": [2, 2] }} + /> + )} + + {/* Show mission rally point */} + {missionItems.rally_items.map((item, index) => { + return ( + + ) + })} + + {getFlightMode() === "Guided" && guidedModePinData !== null && ( + + )} + + {clicked && ( +
+ { + clipboard.copy( + `${clickedGpsCoords.lat}, ${clickedGpsCoords.lng}`, + ) + showNotification("Copied to clipboard") + }} + > +
+

+ {clickedGpsCoords.lat.toFixed(coordsFractionDigits)},{" "} + {clickedGpsCoords.lng.toFixed(coordsFractionDigits)} +

+ + + +
+
+
+ )} +
+
+ ) +} +function propsAreEqual(prev, next) { + return JSON.stringify(prev) === JSON.stringify(next) +} +const MissionsMapSection = React.memo(MapSectionNonMemo, propsAreEqual) + +export default MissionsMapSection diff --git a/gcs/src/components/navbar.jsx b/gcs/src/components/navbar.jsx index aaa6ca79c..08cacddde 100644 --- a/gcs/src/components/navbar.jsx +++ b/gcs/src/components/navbar.jsx @@ -407,6 +407,15 @@ export default function Navbar({ currentPage }) { > Dashboard + + Missions + { + RunCommand("goto_missions") + }, + kbdBadge("ALT + 2", badgeColor), + kbdBadge("⌘ + 2", badgeColor), +) AddSpotlightAction( "graphs", "Graphs", @@ -49,8 +59,8 @@ AddSpotlightAction( () => { RunCommand("goto_graphs") }, - kbdBadge("ALT + 2", badgeColor), - kbdBadge("⌘ + 2", badgeColor), + kbdBadge("ALT + 3", badgeColor), + kbdBadge("⌘ + 3", badgeColor), ) AddSpotlightAction( "params", @@ -59,8 +69,8 @@ AddSpotlightAction( () => { RunCommand("goto_params") }, - kbdBadge("ALT + 3", badgeColor), - kbdBadge("⌘ + 3", badgeColor), + kbdBadge("ALT + 4", badgeColor), + kbdBadge("⌘ + 4", badgeColor), ) AddSpotlightAction( "config", @@ -69,8 +79,8 @@ AddSpotlightAction( () => { RunCommand("goto_config") }, - kbdBadge("ALT + 4", badgeColor), - kbdBadge("⌘ + 4", badgeColor), + kbdBadge("ALT + 5", badgeColor), + kbdBadge("⌘ + 5", badgeColor), ) AddSpotlightAction( "fla", @@ -79,8 +89,8 @@ AddSpotlightAction( () => { RunCommand("goto_fla") }, - kbdBadge("ALT + 5", badgeColor), - kbdBadge("⌘ + 5", badgeColor), + kbdBadge("ALT + 6", badgeColor), + kbdBadge("⌘ + 6", badgeColor), ) // Commands diff --git a/gcs/src/components/spotlight/commandHandler.js b/gcs/src/components/spotlight/commandHandler.js index 4a62cd783..2291b2522 100644 --- a/gcs/src/components/spotlight/commandHandler.js +++ b/gcs/src/components/spotlight/commandHandler.js @@ -28,6 +28,9 @@ export function Commands() { AddCommand("goto_dashboard", () => { navigate("/") }) + AddCommand("goto_missions", () => { + navigate("/missions") + }) AddCommand("goto_graphs", () => { navigate("/graphs") }) @@ -56,10 +59,11 @@ export function Commands() { // Register hotkeys useHotkeys([ [isMac ? "meta+1" : "alt+1", () => RunCommand("goto_dashboard")], - [isMac ? "meta+2" : "alt+2", () => RunCommand("goto_graphs")], - [isMac ? "meta+3" : "alt+3", () => RunCommand("goto_params")], - [isMac ? "meta+4" : "alt+4", () => RunCommand("goto_config")], - [isMac ? "meta+5" : "alt+5", () => RunCommand("goto_fla")], + [isMac ? "meta+2" : "alt+2", () => RunCommand("goto_missions")], + [isMac ? "meta+3" : "alt+3", () => RunCommand("goto_graphs")], + [isMac ? "meta+4" : "alt+4", () => RunCommand("goto_params")], + [isMac ? "meta+5" : "alt+5", () => RunCommand("goto_config")], + [isMac ? "meta+6" : "alt+6", () => RunCommand("goto_fla")], ["mod+shift+r", () => RunCommand("force_refresh")], ["mod+,", () => RunCommand("open_settings")], ]) diff --git a/gcs/src/dashboard.jsx b/gcs/src/dashboard.jsx index 75616312f..c7391c2b6 100644 --- a/gcs/src/dashboard.jsx +++ b/gcs/src/dashboard.jsx @@ -130,7 +130,10 @@ export default function Dashboard() { const [homePosition, setHomePosition] = useState(null) // Following Drone - const [followDrone, setFollowDrone] = useState(false) + const [followDrone, setFollowDrone] = useSessionStorage({ + key: "followDroneBool", + defaultValue: false, + }) const [currentFlightModeNumber, setCurrentFlightModeNumber] = useState(null) // Map and messages @@ -365,6 +368,7 @@ export default function Dashboard() { setFollowDrone(false) }} getFlightMode={getFlightMode} + mapId="dashboard" /> diff --git a/gcs/src/main.jsx b/gcs/src/main.jsx index 98e40a222..64005a8c9 100644 --- a/gcs/src/main.jsx +++ b/gcs/src/main.jsx @@ -18,6 +18,7 @@ import { MantineProvider } from "@mantine/core" // Route imports import Config from "./config.jsx" import Dashboard from "./dashboard.jsx" +import Missions from "./missions.jsx" import FLA from "./fla.jsx" import Graphs from "./graphs.jsx" import Params from "./params.jsx" @@ -45,6 +46,7 @@ ReactDOM.createRoot(document.getElementById("root")).render( } /> + } /> } /> } /> } /> diff --git a/gcs/src/missions.jsx b/gcs/src/missions.jsx new file mode 100644 index 000000000..5a4bf690d --- /dev/null +++ b/gcs/src/missions.jsx @@ -0,0 +1,178 @@ +/* + The missions screen. +*/ + +// Base imports +import { useCallback, useEffect, useRef, useState } from "react" + +// 3rd Party Imports +import { useLocalStorage, useSessionStorage } from "@mantine/hooks" +import { ResizableBox } from "react-resizable" + +// Custom component and helpers +import Layout from "./components/layout" +import MissionsMapSection from "./components/missions/missionsMap" +import { + COPTER_MODES_FLIGHT_MODE_MAP, + MAV_AUTOPILOT_INVALID, + PLANE_MODES_FLIGHT_MODE_MAP, +} from "./helpers/mavlinkConstants" +import { showErrorNotification } from "./helpers/notification" +import { socket } from "./helpers/socket" + +export default function Missions() { + // Local Storage + const [connected] = useSessionStorage({ + key: "connectedToDrone", + defaultValue: false, + }) + const [aircraftType] = useLocalStorage({ + key: "aircraftType", + }) + + // Mission + const missionItems = { + mission_items: [], + fence_items: [], + rally_items: [], + } + const [homePosition, setHomePosition] = useState(null) + + // Heartbeat data + const [heartbeatData, setHeartbeatData] = useState({ system_status: 0 }) + + // GPS and Telemetry + const [gpsData, setGpsData] = useState({}) + + // Map and messages + const mapRef = useRef() + + // System data + const [navControllerOutputData, setNavControllerOutputData] = useState({}) + + const incomingMessageHandler = useCallback( + () => ({ + GLOBAL_POSITION_INT: (msg) => setGpsData(msg), + NAV_CONTROLLER_OUTPUT: (msg) => setNavControllerOutputData(msg), + HEARTBEAT: (msg) => { + if (msg.autopilot !== MAV_AUTOPILOT_INVALID) { + setHeartbeatData(msg) + } + }, + }), + [], + ) + + useEffect(() => { + if (!connected) { + return + } else { + socket.emit("set_state", { state: "missions" }) + socket.emit("get_home_position") + } + + socket.on("incoming_msg", (msg) => { + if (incomingMessageHandler()[msg.mavpackettype] !== undefined) { + incomingMessageHandler()[msg.mavpackettype](msg) + } + console.log(msg) + }) + + socket.on("home_position_result", (data) => { + if (data.success) { + setHomePosition(data.data) + } else { + showErrorNotification(data.message) + } + }) + + return () => { + socket.off("incoming_msg") + socket.off("home_position_result") + } + }, [connected]) + + function getFlightMode() { + if (aircraftType === 1) { + return PLANE_MODES_FLIGHT_MODE_MAP[heartbeatData.custom_mode] + } else if (aircraftType === 2) { + return COPTER_MODES_FLIGHT_MODE_MAP[heartbeatData.custom_mode] + } + + return "UNKNOWN" + } + + return ( + +
+
+ {/* Resizable Sidebar */} +
+ } + className="relative bg-falcongrey-800 overflow-y-auto" + > +
+

Mission Control

+
+

Current Mission

+

Mission Type: Idk man

+

Status: Random Stuff

+
+
+

Waypoints

+
    +
  • Waypoint 1: Takeoff
  • +
  • Waypoint 2: Navigate to target
  • +
  • Waypoint 3: Return home
  • +
+
+
+ + + {/* Main content area */} +
+ {/* Map area */} +
+ +
+ + {/* Resizable Bottom Bar */} +
+ } + className="relative bg-falcongrey-800 overflow-y-auto" + > +
+

Set Waypoints

+
+ +
+ + +
+ ) +} diff --git a/radio/app/endpoints/states.py b/radio/app/endpoints/states.py index 3e6f1a477..b94e1992d 100644 --- a/radio/app/endpoints/states.py +++ b/radio/app/endpoints/states.py @@ -49,6 +49,7 @@ def set_state(data: SetStateType) -> None: "ESC_TELEMETRY_5_TO_8", "MISSION_CURRENT", ], + "missions": ["GLOBAL_POSITION_INT", "NAV_CONTROLLER_OUTPUT", "HEARTBEAT"], "graphs": ["VFR_HUD", "ATTITUDE", "SYS_STATUS"], "config.flight_modes": ["RC_CHANNELS", "HEARTBEAT"], } @@ -57,6 +58,16 @@ def set_state(data: SetStateType) -> None: droneStatus.drone.setupDataStreams() for message in message_listeners["dashboard"]: droneStatus.drone.addMessageListener(message, sendMessage) + if droneStatus.state == "missions": + droneStatus.drone.stopAllDataStreams() + droneStatus.drone.setupSingleDataStream( + mavutil.mavlink.MAV_DATA_STREAM_EXTENDED_STATUS + ) + droneStatus.drone.setupSingleDataStream( + mavutil.mavlink.MAV_DATA_STREAM_POSITION + ) + for message in message_listeners["missions"]: + droneStatus.drone.addMessageListener(message, sendMessage) elif droneStatus.state == "graphs": droneStatus.drone.stopAllDataStreams() From 306382b131eae0b07c1151456a7df3c2736d0ba0 Mon Sep 17 00:00:00 2001 From: Julian Jones <37962677+NexInfinite@users.noreply.github.com> Date: Tue, 18 Mar 2025 20:51:01 +0000 Subject: [PATCH 04/16] 542 show error stack on error boundary (#543) * changed release version * Showed stack log * formatted * removed comments * styalised code block * updated bug message * improving readability * Update bug_report.md --------- Co-authored-by: Kwashie A. <104215256+Kwash67@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- gcs/package.json | 4 +- gcs/src/components/error/errorBoundary.jsx | 46 +++++++++++----------- gcs/src/main.jsx | 8 ++-- 4 files changed, 31 insertions(+), 29 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d5f128903..72f938988 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -31,7 +31,7 @@ If applicable, add screenshots to help explain your problem. - What version of ArduPilot is running on the flight controller? **Error Log** -Please share the error's found in the console tab of inspect element (found by doing `ctrl + shift + i` and clicking the `console` tab). Either a screenshot or a copy paste of the error would suffice. If there is no error found in the console tab you can leave this blank. If you are struggling to find the console tab click [here](../../help/how_to_find_error_console.png). +If you received an error message within FGCS, please click on 'Show stack log', then copy and paste it here using the button in the top right. If you don't see the stack log, please share the errors found in the console tab of inspect element (found by doing `ctrl + shift + i` and clicking the `console` tab). Either a screenshot or a copy paste of the error would suffice. If there is no error found in the console tab you can leave this blank. If you are struggling to find the console tab click [here](../../help/how_to_find_error_console.png). **Additional context** Add any other context about the problem here. diff --git a/gcs/package.json b/gcs/package.json index 76f94dfab..252c734c6 100644 --- a/gcs/package.json +++ b/gcs/package.json @@ -6,7 +6,7 @@ "name": "Avis-Drone-Labs" }, "private": true, - "version": "0.1.8-alpha", + "version": "0.1.9-alpha", "license": "GPL-3.0-only", "homepage": "https://fgcs.projectfalcon.uk", "bugs": { @@ -26,6 +26,7 @@ }, "dependencies": { "@headlessui/react": "2.1.4", + "@mantine/code-highlight": "^7.17.1", "@mantine/core": "^7.3.2", "@mantine/hooks": "^7.3.2", "@mantine/notifications": "^7.4.0", @@ -52,6 +53,7 @@ "react": "^18.2.0", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^5.0.0", "react-map-gl": "^7.1.6", "react-redux": "^9.1.2", "react-resizable": "^3.0.5", diff --git a/gcs/src/components/error/errorBoundary.jsx b/gcs/src/components/error/errorBoundary.jsx index 4f02985d0..ffffa8c33 100644 --- a/gcs/src/components/error/errorBoundary.jsx +++ b/gcs/src/components/error/errorBoundary.jsx @@ -7,8 +7,9 @@ import React from "react" // 3rd Party Imports import { Button } from "@mantine/core" +import { CodeHighlight } from '@mantine/code-highlight' -export function ErrorComponent() { +export default function ErrorBoundaryFallback({ error }) { return (

@@ -28,6 +29,26 @@ export function ErrorComponent() { so we can fix it as soon as possible!

To get back to what you were doing, refresh or click below.

+ +

{ + document.getElementById("stack-error").hidden = + !document.getElementById("stack-error").hidden + }} + > + Show stack log +

+

+ { + if (!isValidNumber(num, range)) return; + updateAltitude(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 +170,7 @@ function SettingsModal() { {settingTabs.map((t) => { return ( - + {Object.keys(DefaultSettings[t]).map((s) => { return ( Date: Sun, 25 May 2025 14:06:43 +0100 Subject: [PATCH 12/16] altitude alert system --- gcs/src/components/dashboard/alert.jsx | 19 +++------- .../components/dashboard/alertProvider.jsx | 32 +++++++++++----- gcs/src/dashboard.jsx | 38 +++++++++++++++++-- 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/gcs/src/components/dashboard/alert.jsx b/gcs/src/components/dashboard/alert.jsx index 8392306f4..1997288aa 100644 --- a/gcs/src/components/dashboard/alert.jsx +++ b/gcs/src/components/dashboard/alert.jsx @@ -23,30 +23,21 @@ const SeverityColor = { export default function AlertSection() { const { alerts, dismissAlert } = useAlerts(); - const highestSeverityAlerts = useMemo(() => { - const groupedByCategory = alerts.reduce((acc, alert) => { - (acc[alert.category] ??= []).push(alert); - return acc; - }, {}); - - return Object.values(groupedByCategory).map((alerts) => - alerts.reduce((severeAlert, alert) => - alert.severity > severeAlert.severity ? alert : severeAlert - ) - ); + const sortedAlerts = useMemo(() => { + return alerts.toSorted((a1, a2) => a2.severity - a1.severity); }, [alerts]); return (
- {highestSeverityAlerts.map((alert) => ( -
+ {sortedAlerts.map((alert) => ( +
} - onClose={() => dismissAlert(alert.id)} + onClose={() => dismissAlert(alert.category, true)} > {alert.jsx} diff --git a/gcs/src/components/dashboard/alertProvider.jsx b/gcs/src/components/dashboard/alertProvider.jsx index 8c60794f7..e27eae505 100644 --- a/gcs/src/components/dashboard/alertProvider.jsx +++ b/gcs/src/components/dashboard/alertProvider.jsx @@ -6,24 +6,38 @@ * } */ -import { createContext, useContext, useState } from "react"; +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) { - const id = Math.random().toString(16).slice(2); - setAlerts((prevAlerts) => [...prevAlerts, { - ...alert, - id - }]); - return id; + 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(id) { - setAlerts((prevAlerts) => prevAlerts.filter((alert) => alert.id != id)); + 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 ( diff --git a/gcs/src/dashboard.jsx b/gcs/src/dashboard.jsx index 443754b36..1d490aaca 100644 --- a/gcs/src/dashboard.jsx +++ b/gcs/src/dashboard.jsx @@ -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 @@ -148,9 +151,37 @@ export default function Dashboard() { defaultValue: defaultDataMessages, }) + const { getSetting } = useSettings() + + // Alerts + const { dispatchAlert, dismissAlert } = useAlerts() + const highestAltitudeRef = useRef(0) + + function updateAltitudeAlert(msg) { + if (msg.alt > highestAltitudeRef.current) return highestAltitudeRef.current = msg.alt + const altitudes = getSetting("Config.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)[0] if (battery) { @@ -431,9 +462,8 @@ export default function Dashboard() { /> } - value={`(${gpsData.lat !== undefined ? (gpsData.lat * 1e-7).toFixed(6) : 0}, ${ - gpsData.lon !== undefined ? (gpsData.lon * 1e-7).toFixed(6) : 0 - })`} + value={`(${gpsData.lat !== undefined ? (gpsData.lat * 1e-7).toFixed(6) : 0}, ${gpsData.lon !== undefined ? (gpsData.lon * 1e-7).toFixed(6) : 0 + })`} tooltip="GPS (lat, lon)" /> Date: Sun, 25 May 2025 14:08:13 +0100 Subject: [PATCH 13/16] formatting --- gcs/src/components/dashboard/alert.jsx | 76 ++++++++++--------- .../components/dashboard/alertProvider.jsx | 74 +++++++++--------- gcs/src/components/dashboard/telemetry.jsx | 11 ++- gcs/src/components/error/errorBoundary.jsx | 2 +- gcs/src/components/mainContent.jsx | 13 ++-- gcs/src/components/noDroneConnected.jsx | 2 +- gcs/src/components/settingsModal.jsx | 63 +++++++++------ gcs/src/dashboard.jsx | 21 +++-- gcs/src/main.jsx | 2 +- 9 files changed, 150 insertions(+), 114 deletions(-) diff --git a/gcs/src/components/dashboard/alert.jsx b/gcs/src/components/dashboard/alert.jsx index 1997288aa..d0d0571ad 100644 --- a/gcs/src/components/dashboard/alert.jsx +++ b/gcs/src/components/dashboard/alert.jsx @@ -1,48 +1,52 @@ -import { Alert } from "@mantine/core"; -import { IconAlertTriangle } from "@tabler/icons-react"; -import { useMemo } from "react"; -import { useAlerts } from "./alertProvider"; +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", -}; + Altitude: "Altitude", + Speed: "Speed", +} export const AlertSeverity = { - Yellow: 1, - Orange: 2, - Red: 3, -}; + Yellow: 1, + Orange: 2, + Red: 3, +} const SeverityColor = { - [AlertSeverity.Yellow]: "yellow", - [AlertSeverity.Orange]: "orange", - [AlertSeverity.Red]: "red", -}; + [AlertSeverity.Yellow]: "yellow", + [AlertSeverity.Orange]: "orange", + [AlertSeverity.Red]: "red", +} export default function AlertSection() { - const { alerts, dismissAlert } = useAlerts(); + const { alerts, dismissAlert } = useAlerts() - const sortedAlerts = useMemo(() => { - return alerts.toSorted((a1, a2) => a2.severity - a1.severity); - }, [alerts]); + const sortedAlerts = useMemo(() => { + return alerts.toSorted((a1, a2) => a2.severity - a1.severity) + }, [alerts]) - return ( -
- {sortedAlerts.map((alert) => ( -
- } - onClose={() => dismissAlert(alert.category, true)} - > - {alert.jsx} - -
- ))} + 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 index e27eae505..df0b50565 100644 --- a/gcs/src/components/dashboard/alertProvider.jsx +++ b/gcs/src/components/dashboard/alertProvider.jsx @@ -6,45 +6,47 @@ * } */ -import { createContext, useContext, useRef, useState } from "react"; +import { createContext, useContext, useRef, useState } from "react" -const AlertContext = createContext(); +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} - + 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); +export const useAlerts = () => useContext(AlertContext) diff --git a/gcs/src/components/dashboard/telemetry.jsx b/gcs/src/components/dashboard/telemetry.jsx index af7a30f65..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. */ @@ -164,14 +164,13 @@ export default function TelemetrySection({ - {batteryData.map(battery => ( + {batteryData.map((battery) => (
BATTERY{battery.id} - {(battery.voltages - ? battery.voltages[0] / 1000 - : 0 - ).toFixed(2)} + {(battery.voltages ? battery.voltages[0] / 1000 : 0).toFixed( + 2, + )} V diff --git a/gcs/src/components/error/errorBoundary.jsx b/gcs/src/components/error/errorBoundary.jsx index ffffa8c33..fdaa9c33d 100644 --- a/gcs/src/components/error/errorBoundary.jsx +++ b/gcs/src/components/error/errorBoundary.jsx @@ -7,7 +7,7 @@ import React from "react" // 3rd Party Imports import { Button } from "@mantine/core" -import { CodeHighlight } from '@mantine/code-highlight' +import { CodeHighlight } from "@mantine/code-highlight" export default function ErrorBoundaryFallback({ error }) { return ( diff --git a/gcs/src/components/mainContent.jsx b/gcs/src/components/mainContent.jsx index bd3fc13c9..484a5095f 100644 --- a/gcs/src/components/mainContent.jsx +++ b/gcs/src/components/mainContent.jsx @@ -35,11 +35,14 @@ export default function AppContent() { - - - - } /> + + + + } + /> } /> } /> } /> diff --git a/gcs/src/components/noDroneConnected.jsx b/gcs/src/components/noDroneConnected.jsx index ee82ea2f3..c836339fa 100644 --- a/gcs/src/components/noDroneConnected.jsx +++ b/gcs/src/components/noDroneConnected.jsx @@ -1,4 +1,4 @@ -export default function NoDroneConnected({tab}) { +export default function NoDroneConnected({ tab }) { return (

diff --git a/gcs/src/components/settingsModal.jsx b/gcs/src/components/settingsModal.jsx index a627c7a4a..7d6d786cb 100644 --- a/gcs/src/components/settingsModal.jsx +++ b/gcs/src/components/settingsModal.jsx @@ -1,4 +1,12 @@ -import { Button, Checkbox, Input, Modal, NativeSelect, NumberInput, 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" @@ -8,8 +16,8 @@ import DefaultSettings from "../../data/default_settings.json" const isValidNumber = (num, range) => { return ( num && - (parseInt(num) && - (range === null || (range[0] <= num && num <= range[1]))) + parseInt(num) && + (range === null || (range[0] <= num && num <= range[1])) ) } @@ -59,28 +67,29 @@ function NumberSetting({ settingName, range }) { ) } -const generateId = () => Math.random().toString(36).slice(8); +const generateId = () => Math.random().toString(36).slice(8) function ExtendableNumberSetting({ settingName, range }) { const { getSetting, setSetting } = useSettings() const [altitudes, setAltitudes] = useState( - getSetting(settingName).map(val => ({ id: generateId(), value: val })) - ); + getSetting(settingName).map((val) => ({ id: generateId(), value: val })), + ) useEffect(() => { - setSetting(settingName, altitudes.map(a => a.value)); - }, [altitudes]); + setSetting( + settingName, + altitudes.map((a) => a.value), + ) + }, [altitudes]) const updateAltitude = (id, value) => { - setAltitudes(prev => - prev.map(a => (a.id === id ? { ...a, value } : a)) - ); - }; + setAltitudes((prev) => prev.map((a) => (a.id === id ? { ...a, value } : a))) + } const removeAltitude = (id) => { - setAltitudes(prev => prev.filter(a => a.id !== id)); - }; + setAltitudes((prev) => prev.filter((a) => a.id !== id)) + } return (

@@ -95,29 +104,39 @@ function ExtendableNumberSetting({ settingName, range }) { { - if (!isValidNumber(num, range)) return; - updateAltitude(id, parseInt(num)); + if (!isValidNumber(num, range)) return + updateAltitude(id, parseInt(num)) }} suffix="m" />
))} - +
- ); + ) } function Setting({ settingName, df }) { return ( -
+
{df.display}:

{df.description}

{df.type == "extendableNumber" ? ( - + ) : df.type == "number" ? ( ) : df.type == "boolean" ? ( diff --git a/gcs/src/dashboard.jsx b/gcs/src/dashboard.jsx index 1d490aaca..8206994c3 100644 --- a/gcs/src/dashboard.jsx +++ b/gcs/src/dashboard.jsx @@ -158,7 +158,8 @@ export default function Dashboard() { const highestAltitudeRef = useRef(0) function updateAltitudeAlert(msg) { - if (msg.alt > highestAltitudeRef.current) return highestAltitudeRef.current = msg.alt + if (msg.alt > highestAltitudeRef.current) + return (highestAltitudeRef.current = msg.alt) const altitudes = getSetting("Config.altitudeAlerts") altitudes.sort((a1, a2) => a1 - a2) @@ -166,8 +167,13 @@ export default function Dashboard() { 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 + severity: + i == 0 + ? AlertSeverity.Red + : i == altitudes.length - 1 + ? AlertSeverity.Yellow + : AlertSeverity.Orange, + jsx: <>Caution! You've fallen below {altitude}m, }) return } @@ -183,7 +189,9 @@ export default function Dashboard() { updateAltitudeAlert(msg) }, BATTERY_STATUS: (msg) => { - const battery = localBatteryData.filter(battery => battery.id == msg.id)[0] + const battery = localBatteryData.filter( + (battery) => battery.id == msg.id, + )[0] if (battery) { Object.assign(battery, msg) } else { @@ -462,8 +470,9 @@ export default function Dashboard() { /> } - value={`(${gpsData.lat !== undefined ? (gpsData.lat * 1e-7).toFixed(6) : 0}, ${gpsData.lon !== undefined ? (gpsData.lon * 1e-7).toFixed(6) : 0 - })`} + value={`(${gpsData.lat !== undefined ? (gpsData.lat * 1e-7).toFixed(6) : 0}, ${ + gpsData.lon !== undefined ? (gpsData.lon * 1e-7).toFixed(6) : 0 + })`} tooltip="GPS (lat, lon)" /> Date: Sun, 25 May 2025 14:22:25 +0100 Subject: [PATCH 14/16] slight style change for new alert button --- gcs/src/components/settingsModal.jsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/gcs/src/components/settingsModal.jsx b/gcs/src/components/settingsModal.jsx index 7d6d786cb..43fed6f85 100644 --- a/gcs/src/components/settingsModal.jsx +++ b/gcs/src/components/settingsModal.jsx @@ -111,14 +111,16 @@ function ExtendableNumberSetting({ settingName, range }) { />
))} - +
+ +
) } From 5ff7d645f44564fc74e05b9ad26058ce7914a504 Mon Sep 17 00:00:00 2001 From: Ben Gilbert Date: Sun, 25 May 2025 14:46:17 +0100 Subject: [PATCH 15/16] update alert settings location --- gcs/data/default_settings.json | 5 +++-- gcs/src/dashboard.jsx | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gcs/data/default_settings.json b/gcs/data/default_settings.json index 55c3b286a..1c6c1af50 100644 --- a/gcs/data/default_settings.json +++ b/gcs/data/default_settings.json @@ -35,8 +35,7 @@ "display": "Sync Dashboard and Missions Map Viewstate" } }, - "Params": {}, - "Config": { + "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], @@ -48,5 +47,7 @@ "display": "Altitude Alerts" } }, + "Params": {}, + "Config": {}, "FGCS": {} } diff --git a/gcs/src/dashboard.jsx b/gcs/src/dashboard.jsx index 59eaeafab..9a6f408a3 100644 --- a/gcs/src/dashboard.jsx +++ b/gcs/src/dashboard.jsx @@ -160,7 +160,7 @@ export default function Dashboard() { function updateAltitudeAlert(msg) { if (msg.alt > highestAltitudeRef.current) return (highestAltitudeRef.current = msg.alt) - const altitudes = getSetting("Config.altitudeAlerts") + const altitudes = getSetting("Dashboard.altitudeAlerts") altitudes.sort((a1, a2) => a1 - a2) for (const [i, altitude] of altitudes.entries()) { From 0e8f8b22e8736400730281abd0a686df9a94e097 Mon Sep 17 00:00:00 2001 From: Ben Gilbert Date: Sat, 7 Jun 2025 15:05:20 +0100 Subject: [PATCH 16/16] minor improvements --- .../components/dashboard/alertProvider.jsx | 1 + gcs/src/components/settingsModal.jsx | 22 +++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/gcs/src/components/dashboard/alertProvider.jsx b/gcs/src/components/dashboard/alertProvider.jsx index df0b50565..2fa602d52 100644 --- a/gcs/src/components/dashboard/alertProvider.jsx +++ b/gcs/src/components/dashboard/alertProvider.jsx @@ -17,6 +17,7 @@ export default function AlertProvider({ children }) { 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, ) diff --git a/gcs/src/components/settingsModal.jsx b/gcs/src/components/settingsModal.jsx index 43fed6f85..35642fd1b 100644 --- a/gcs/src/components/settingsModal.jsx +++ b/gcs/src/components/settingsModal.jsx @@ -72,32 +72,32 @@ const generateId = () => Math.random().toString(36).slice(8) function ExtendableNumberSetting({ settingName, range }) { const { getSetting, setSetting } = useSettings() - const [altitudes, setAltitudes] = useState( + const [values, setValues] = useState( getSetting(settingName).map((val) => ({ id: generateId(), value: val })), ) useEffect(() => { setSetting( settingName, - altitudes.map((a) => a.value), + values.map((a) => a.value), ) - }, [altitudes]) + }, [values]) - const updateAltitude = (id, value) => { - setAltitudes((prev) => prev.map((a) => (a.id === id ? { ...a, value } : a))) + const updateValue = (id, value) => { + setValues((prev) => prev.map((a) => (a.id === id ? { ...a, value } : a))) } - const removeAltitude = (id) => { - setAltitudes((prev) => prev.filter((a) => a.id !== id)) + const removeValue = (id) => { + setValues((prev) => prev.filter((a) => a.id !== id)) } return (
- {altitudes.map(({ id, value }) => ( + {values.map(({ id, value }) => (
@@ -105,7 +105,7 @@ function ExtendableNumberSetting({ settingName, range }) { value={value} onChange={(num) => { if (!isValidNumber(num, range)) return - updateAltitude(id, parseInt(num)) + updateValue(id, parseInt(num)) }} suffix="m" /> @@ -115,7 +115,7 @@ function ExtendableNumberSetting({ settingName, range }) {