diff --git a/gcs/src/components/missions/missionStatistics.jsx b/gcs/src/components/missions/missionStatistics.jsx
index 959628372..a04b48b25 100644
--- a/gcs/src/components/missions/missionStatistics.jsx
+++ b/gcs/src/components/missions/missionStatistics.jsx
@@ -288,8 +288,8 @@ export default function MissionStatistics() {
label="Max distance between waypoints"
value={maxDistanceBetweenWaypoints.maxDistance}
tooltip={
- maxDistanceBetweenWaypoints.points &&
- `Between ${maxDistanceBetweenWaypoints.points[0].seq} and ${maxDistanceBetweenWaypoints.points[1].seq}`
+ maxDistanceBetweenWaypoints?.points?.length > 1 &&
+ `Between ${maxDistanceBetweenWaypoints.points[0]?.seq} and ${maxDistanceBetweenWaypoints.points[1]?.seq}`
}
units="m"
/>
@@ -298,7 +298,7 @@ export default function MissionStatistics() {
label="Max slope gradient"
value={maxSlopeGradient.maxGradient}
tooltip={
- maxSlopeGradient.points &&
+ maxSlopeGradient?.points?.length > 1 &&
`Between ${maxSlopeGradient.points[0]?.seq} and ${maxSlopeGradient.points[1]?.seq}`
}
units="%"
diff --git a/gcs/src/missions.jsx b/gcs/src/missions.jsx
index 97adabb49..132f138e8 100644
--- a/gcs/src/missions.jsx
+++ b/gcs/src/missions.jsx
@@ -6,29 +6,27 @@
import { useEffect, useRef, useState } from "react"
// 3rd Party Imports
-import { useSessionStorage } from "@mantine/hooks"
import { ResizableBox } from "react-resizable"
import { v4 as uuidv4 } from "uuid"
// Custom component and helpers
import {
- ActionIcon,
Button,
Divider,
Modal,
NumberInput,
Progress,
+ Select,
Tabs,
Tooltip,
} from "@mantine/core"
-import { IconInfoCircle, IconX } from "@tabler/icons-react"
+import { IconInfoCircle } from "@tabler/icons-react"
import Layout from "./components/layout"
import FenceItemsTable from "./components/missions/fenceItemsTable"
import MissionItemsTable from "./components/missions/missionItemsTable"
import MissionStatistics from "./components/missions/missionStatistics"
import MissionsMapSection from "./components/missions/missionsMap"
import RallyItemsTable from "./components/missions/rallyItemsTable"
-import NoDroneConnected from "./components/noDroneConnected"
import { coordToInt, intToCoord } from "./helpers/dataFormatters"
// Redux
@@ -43,6 +41,10 @@ import resolveConfig from "tailwindcss/resolveConfig"
import tailwindConfig from "../tailwind.config"
import UpdatePlannedHomePositionModal from "./components/missions/updatePlannedHomePositionModal"
import { showErrorNotification } from "./helpers/notification"
+import {
+ selectAircraftType,
+ setDroneAircraftType,
+} from "./redux/slices/droneInfoSlice"
import {
emitExportMissionToFile,
emitGetCurrentMission,
@@ -96,6 +98,7 @@ export default function Missions() {
const targetInfo = useSelector(selectTargetInfo)
const plannedHomePosition = useSelector(selectPlannedHomePosition)
const activeTab = useSelector(selectActiveTab)
+ const aircraftType = useSelector(selectAircraftType)
// Mission items
const missionItems = useSelector(selectDrawingMissionItems)
@@ -105,12 +108,6 @@ export default function Missions() {
const missionProgressModalOpened = useSelector(selectMissionProgressModal)
const missionProgressModalData = useSelector(selectMissionProgressData)
- // Other states
- const [showWarningBanner, setShowWarningBanner] = useSessionStorage({
- key: "showWarningBanner",
- defaultValue: true,
- })
-
// Need to keep a reference to the active tab to avoid stale closures
const activeTabRef = useRef(activeTab)
const tabsListRef = useRef(null)
@@ -345,249 +342,239 @@ export default function Missions() {
- {/* Banner to let people know that things are still under development */}
- {showWarningBanner && (
-
-
- Missions is still under development so some features are still
- missing. If you find any bugs please report them to us.
-
-
setShowWarningBanner(false)}
- variant="transparent"
- className="mr-2"
+
+
+ {/* Resizable Sidebar */}
+
+ }
+ className="relative bg-falcongrey-800 overflow-y-auto"
>
-
-
-
- )}
-
- {connected ? (
-
-
- {/* Resizable Sidebar */}
-
- }
- className="relative bg-falcongrey-800 overflow-y-auto"
- >
-
-
-
-
- {
- readMissionFromDrone()
- }}
- disabled={!connected}
- className="grow"
- >
- Read {activeTab}
-
- {
- writeMissionToDrone()
- }}
- disabled={!connected}
- className="grow"
- >
- Write {activeTab}
-
-
-
-
-
-
-
- Import from file
-
- {
- saveMissionToFile()
- }}
- className="grow"
- >
- Save to file
-
-
-
-
-
-
-
- Planned home{" "}
-
-
-
- The planned home location is used to approximate
- the starting location of the mission. The
- dashboard displays the actual home location
- used by the drone.
-
- >
- }
- >
-
-
-
-
-
setPlannedHomeLatInput(val)}
- onBlur={() => {
- if (isInvalidInputNumber(plannedHomeLatInput)) {
- setPlannedHomeLatInput(
- intToCoord(plannedHomePosition?.lat).toFixed(
- coordsFractionDigits,
- ),
- )
- }
- }}
- min={-90}
- max={90}
- step={0.000001}
- hideControls
- />
- setPlannedHomeLonInput(val)}
- onBlur={() => {
- if (isInvalidInputNumber(plannedHomeLonInput)) {
- setPlannedHomeLonInput(
- intToCoord(plannedHomePosition?.lon).toFixed(
- coordsFractionDigits,
- ),
- )
- }
+
+
+ {connected ? (
+ <>
+
+ {
+ readMissionFromDrone()
+ }}
+ disabled={!connected}
+ className="grow"
+ >
+ Read {activeTab}
+
+ {
+ writeMissionToDrone()
+ }}
+ disabled={!connected}
+ className="grow"
+ >
+ Write {activeTab}
+
+ >
+ ) : (
+ {
+ dispatch(setDroneAircraftType(Number(val)))
}}
- min={-180}
- max={180}
- step={0.000001}
- hideControls
+ data={[
+ { value: "1", label: "Plane" },
+ { value: "2", label: "Copter" },
+ ]}
+ allowDeselect={false}
/>
- setPlannedHomeAltInput(val)}
- onBlur={() => {
- if (isInvalidInputNumber(plannedHomeAltInput)) {
- setPlannedHomeAltInput(plannedHomePosition?.alt)
+ )}
+
+
+
+
+
+
+ Import from file
+
+ {
+ saveMissionToFile()
+ }}
+ className="grow"
+ >
+ Save to file
+
+
+
+
+
+
+
+ Planned home{" "}
+
+
+
+ The planned home location is used to approximate the
+ starting location of the mission. The dashboard
+ displays the actual home location used by the
+ drone.
+
+ >
}
- }}
- min={0.1}
- allowNegative={false}
- hideControls
- />
-
- dispatch(
- setPlannedHomePosition({
- lat: coordToInt(plannedHomeLatInput),
- lon: coordToInt(plannedHomeLonInput),
- alt: plannedHomeAltInput,
- }),
+ >
+
+
+
+
+
setPlannedHomeLatInput(val)}
+ onBlur={() => {
+ if (isInvalidInputNumber(plannedHomeLatInput)) {
+ setPlannedHomeLatInput(
+ intToCoord(plannedHomePosition?.lat).toFixed(
+ coordsFractionDigits,
+ ),
)
}
- >
- Save planned home
-
-
-
-
-
-
+ }}
+ min={-90}
+ max={90}
+ step={0.000001}
+ hideControls
+ />
+
setPlannedHomeLonInput(val)}
+ onBlur={() => {
+ if (isInvalidInputNumber(plannedHomeLonInput)) {
+ setPlannedHomeLonInput(
+ intToCoord(plannedHomePosition?.lon).toFixed(
+ coordsFractionDigits,
+ ),
+ )
+ }
+ }}
+ min={-180}
+ max={180}
+ step={0.000001}
+ hideControls
+ />
+ setPlannedHomeAltInput(val)}
+ onBlur={() => {
+ if (isInvalidInputNumber(plannedHomeAltInput)) {
+ setPlannedHomeAltInput(plannedHomePosition?.alt)
+ }
+ }}
+ min={0.1}
+ allowNegative={false}
+ hideControls
+ />
+
+ dispatch(
+ setPlannedHomePosition({
+ lat: coordToInt(plannedHomeLatInput),
+ lon: coordToInt(plannedHomeLonInput),
+ alt: plannedHomeAltInput,
+ }),
+ )
+ }
+ >
+ Save planned home
+
-
- {/* Main content area */}
-
- {/* Map area */}
-
+
+
+ {/* Main content area */}
+
+ {/* Map area */}
+
+
+
- {/* Resizable Bottom Bar */}
-
- }
- className="relative bg-falcongrey-800 overflow-y-auto"
- onResizeStop={(_, { size }) => {
- setTableSectionHeight(
- size.height -
- tabsListRef.current.clientHeight -
- resizeTableHeightPadding,
- )
- }}
+ {/* Resizable Bottom Bar */}
+
+ }
+ className="relative bg-falcongrey-800 overflow-y-auto"
+ onResizeStop={(_, { size }) => {
+ setTableSectionHeight(
+ size.height -
+ tabsListRef.current.clientHeight -
+ resizeTableHeightPadding,
+ )
+ }}
+ >
+ dispatch(setActiveTab(value))}
+ className="mt-2"
>
- dispatch(setActiveTab(value))}
- className="mt-2"
- >
-
-
- Mission
-
-
- Fence
-
-
- Rally
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Mission
+
+
+ Fence
+
+
+ Rally
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- ) : (
-
- )}
+
)
}
diff --git a/gcs/src/redux/middleware/emitters.js b/gcs/src/redux/middleware/emitters.js
index b523f520e..f40cf51a4 100644
--- a/gcs/src/redux/middleware/emitters.js
+++ b/gcs/src/redux/middleware/emitters.js
@@ -37,12 +37,6 @@ import {
setCurrentPage,
setIsForwarding,
} from "../slices/droneConnectionSlice"
-import {
- setSimulationStatus,
- SimulationStatus,
- emitStartSimulation,
- emitStopSimulation,
-} from "../slices/simulationParamsSlice"
import {
emitListFiles,
emitListLogFiles,
@@ -68,6 +62,12 @@ import {
emitSetMultipleParams,
setParamsWriteProgressModalOpen,
} from "../slices/paramsSlice"
+import {
+ emitStartSimulation,
+ emitStopSimulation,
+ setSimulationStatus,
+ SimulationStatus,
+} from "../slices/simulationParamsSlice"
import { resetMessages } from "../slices/statusTextSlice"
export function handleEmitters(socket, store, action) {
@@ -146,11 +146,7 @@ export function handleEmitters(socket, store, action) {
emitter: emitSetState,
callback: () => {
store.dispatch(setCurrentPage(action.payload))
- const storeState = store.getState()
- const isDroneConnected = storeState.droneConnection.connected
- if (isDroneConnected) {
- socket.socket.emit("set_state", { state: action.payload })
- }
+ socket.socket.emit("set_state", { state: action.payload })
},
},
{
diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js
index cf41c5a92..1f3b6bd77 100644
--- a/gcs/src/redux/middleware/socketMiddleware.js
+++ b/gcs/src/redux/middleware/socketMiddleware.js
@@ -634,6 +634,98 @@ const socketMiddleware = (store) => {
)
}
})
+
+ socket.socket.on(
+ MissionSpecificSocketEvents.onImportMissionResult,
+ (msg) => {
+ if (msg.success) {
+ const storeState = store.getState()
+
+ if (msg.mission_type === "mission") {
+ const missionItemsWithIds = []
+ for (let missionItem of msg.items) {
+ missionItemsWithIds.push(addIdToItem(missionItem))
+ }
+
+ // Check if first item is a home location, then open modal to
+ // select whether to update planned home position
+ if (missionItemsWithIds.length > 0) {
+ const potentialHomeLocation = missionItemsWithIds[0]
+ const currentPlannedHomeLocation =
+ storeState.missionInfo.plannedHomePosition
+
+ // Check if the potential home location is different from the current planned home location
+ if (
+ isGlobalFrameHomeCommand(potentialHomeLocation) &&
+ (potentialHomeLocation.x !==
+ currentPlannedHomeLocation.lat ||
+ potentialHomeLocation.y !==
+ currentPlannedHomeLocation.lon ||
+ potentialHomeLocation.z !==
+ currentPlannedHomeLocation.alt)
+ ) {
+ store.dispatch(
+ setUpdatePlannedHomePositionFromLoadData({
+ lat: potentialHomeLocation.x,
+ lon: potentialHomeLocation.y,
+ alt: potentialHomeLocation.z,
+ from: "file",
+ }),
+ )
+ store.dispatch(
+ setUpdatePlannedHomePositionFromLoadModal(true),
+ )
+ }
+ }
+ store.dispatch(setDrawingMissionItems(missionItemsWithIds))
+ store.dispatch(
+ setUnwrittenChanges({
+ ...storeState.missionInfo.unwrittenChanges,
+ mission: true,
+ }),
+ )
+ } else if (msg.mission_type === "fence") {
+ const fenceItemsWithIds = []
+ for (let fence of msg.items) {
+ fenceItemsWithIds.push(addIdToItem(fence))
+ }
+ store.dispatch(setDrawingFenceItems(fenceItemsWithIds))
+ store.dispatch(
+ setUnwrittenChanges({
+ ...storeState.missionInfo.unwrittenChanges,
+ fence: true,
+ }),
+ )
+ } else if (msg.mission_type === "rally") {
+ const rallyItemsWithIds = []
+ for (let rallyItem of msg.items) {
+ rallyItemsWithIds.push(addIdToItem(rallyItem))
+ }
+
+ store.dispatch(setDrawingRallyItems(rallyItemsWithIds))
+ store.dispatch(
+ setUnwrittenChanges({
+ ...storeState.missionInfo.unwrittenChanges,
+ rally: true,
+ }),
+ )
+ }
+
+ showSuccessNotification(msg.message)
+ } else {
+ showErrorNotification(msg.message)
+ }
+ },
+ )
+
+ socket.socket.on(
+ MissionSpecificSocketEvents.onExportMissionResult,
+ (msg) => {
+ msg.success
+ ? showSuccessNotification(msg.message)
+ : showErrorNotification(msg.message)
+ },
+ )
}
}
@@ -957,89 +1049,6 @@ const socketMiddleware = (store) => {
},
)
- socket.socket.on(
- MissionSpecificSocketEvents.onImportMissionResult,
- (msg) => {
- if (msg.success) {
- const storeState = store.getState()
-
- if (msg.mission_type === "mission") {
- const missionItemsWithIds = []
- for (let missionItem of msg.items) {
- missionItemsWithIds.push(addIdToItem(missionItem))
- }
-
- // Check if first item is a home location, then open modal to
- // select whether to update planned home position
- if (missionItemsWithIds.length > 0) {
- const potentialHomeLocation = missionItemsWithIds[0]
- const currentPlannedHomeLocation =
- storeState.missionInfo.plannedHomePosition
-
- // Check if the potential home location is different from the current planned home location
- if (
- isGlobalFrameHomeCommand(potentialHomeLocation) &&
- (potentialHomeLocation.x !==
- currentPlannedHomeLocation.lat ||
- potentialHomeLocation.y !==
- currentPlannedHomeLocation.lon ||
- potentialHomeLocation.z !==
- currentPlannedHomeLocation.alt)
- ) {
- store.dispatch(
- setUpdatePlannedHomePositionFromLoadData({
- lat: potentialHomeLocation.x,
- lon: potentialHomeLocation.y,
- alt: potentialHomeLocation.z,
- from: "file",
- }),
- )
- store.dispatch(
- setUpdatePlannedHomePositionFromLoadModal(true),
- )
- }
- }
- store.dispatch(setDrawingMissionItems(missionItemsWithIds))
- store.dispatch(
- setUnwrittenChanges({
- ...storeState.missionInfo.unwrittenChanges,
- mission: true,
- }),
- )
- } else if (msg.mission_type === "fence") {
- const fenceItemsWithIds = []
- for (let fence of msg.items) {
- fenceItemsWithIds.push(addIdToItem(fence))
- }
- store.dispatch(setDrawingFenceItems(fenceItemsWithIds))
- store.dispatch(
- setUnwrittenChanges({
- ...storeState.missionInfo.unwrittenChanges,
- fence: true,
- }),
- )
- } else if (msg.mission_type === "rally") {
- const rallyItemsWithIds = []
- for (let rallyItem of msg.items) {
- rallyItemsWithIds.push(addIdToItem(rallyItem))
- }
-
- store.dispatch(setDrawingRallyItems(rallyItemsWithIds))
- store.dispatch(
- setUnwrittenChanges({
- ...storeState.missionInfo.unwrittenChanges,
- rally: true,
- }),
- )
- }
-
- showSuccessNotification(msg.message)
- } else {
- showErrorNotification(msg.message)
- }
- },
- )
-
socket.socket.on(
MissionSpecificSocketEvents.onMissionControlResult,
(msg) => {
@@ -1053,15 +1062,6 @@ const socketMiddleware = (store) => {
store.dispatch(setTargetInfo(msg))
})
- socket.socket.on(
- MissionSpecificSocketEvents.onExportMissionResult,
- (msg) => {
- msg.success
- ? showSuccessNotification(msg.message)
- : showErrorNotification(msg.message)
- },
- )
-
socket.socket.on(
MissionSpecificSocketEvents.onCurrentMissionProgress,
(msg) => {
@@ -1316,9 +1316,13 @@ const socketMiddleware = (store) => {
Object.values(ParamSpecificSocketEvents).map((event) =>
socket.socket.off(event),
)
- Object.values(MissionSpecificSocketEvents).map((event) =>
- socket.socket.off(event),
- )
+ Object.values(MissionSpecificSocketEvents)
+ .filter(
+ (event) =>
+ event !== MissionSpecificSocketEvents.onImportMissionResult &&
+ event !== MissionSpecificSocketEvents.onExportMissionResult,
+ )
+ .map((event) => socket.socket.off(event))
Object.values(ConfigSpecificSocketEvents).map((event) =>
socket.socket.off(event),
)
diff --git a/gcs/src/redux/slices/droneConnectionSlice.js b/gcs/src/redux/slices/droneConnectionSlice.js
index fca96c5e4..26eabbfc8 100644
--- a/gcs/src/redux/slices/droneConnectionSlice.js
+++ b/gcs/src/redux/slices/droneConnectionSlice.js
@@ -17,7 +17,7 @@ const initialState = {
connected_to_simulator: false,
// aircraft type
- aircraft_type: 0,
+ aircraft_type: 2, // default to copter
// drone connection parameters
baudrate: "9600", // local
diff --git a/gcs/src/redux/slices/droneInfoSlice.js b/gcs/src/redux/slices/droneInfoSlice.js
index 020190c9a..b7be7ced7 100644
--- a/gcs/src/redux/slices/droneInfoSlice.js
+++ b/gcs/src/redux/slices/droneInfoSlice.js
@@ -74,7 +74,7 @@ const droneInfoSlice = createSlice({
hasEverHadGpsFix: false,
rssi: 0.0,
notificationSound: "",
- aircraftType: 0, // TODO: This should be in local storage but I have no idea how :D,
+ aircraftType: 2, // Default to copter, will be updated on heartbeat
batteryData: [],
extraDroneData: [
...defaultDataMessages, // TODO: Should also be stored in local storage, values set to 0 on launch but actual messages stored
diff --git a/radio/app/controllers/missionController.py b/radio/app/controllers/missionController.py
index 5bbb633b0..e155f7b29 100644
--- a/radio/app/controllers/missionController.py
+++ b/radio/app/controllers/missionController.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import os
+from logging import getLogger
from threading import current_thread
from typing import TYPE_CHECKING, Any, Callable, List, Optional
@@ -17,6 +18,352 @@
TYPE_RALLY = mavutil.mavlink.MAV_MISSION_TYPE_RALLY
MISSION_TYPES = [TYPE_MISSION, TYPE_FENCE, TYPE_RALLY]
+logger = getLogger("fgcs")
+
+
+def _checkMissionType(mission_type: int) -> Response:
+ """
+ Check if the mission type is valid.
+
+ Args:
+ mission_type (int): The type of mission to check.
+
+ Returns:
+ Response: A response dict with success status.
+ """
+ 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 _convertCoordinate(coordinate) -> Number:
+ """
+ Convert a coordinate between int and float formats.
+
+ Args:
+ coordinate: The coordinate to convert (int or float).
+
+ Returns:
+ Number: The converted coordinate.
+ """
+ gps_coordinate_scale = 1e7
+
+ if isinstance(coordinate, float):
+ return int(coordinate * gps_coordinate_scale)
+ elif isinstance(coordinate, int):
+ return coordinate / gps_coordinate_scale
+
+ raise ValueError(
+ f"Invalid coordinate type {type(coordinate)}. Must be int or float."
+ )
+
+
+def _getMissionName(mission_type: int) -> str:
+ """
+ Get the name of the mission type.
+
+ Args:
+ mission_type (int): The type of mission to get the name for.
+
+ Returns:
+ str: The name of the mission type.
+ """
+ if mission_type == TYPE_MISSION:
+ return "mission"
+ elif mission_type == TYPE_FENCE:
+ return "fence"
+ elif mission_type == TYPE_RALLY:
+ return "rally"
+ else:
+ logger.error(f"Invalid mission type {mission_type}")
+ return "unknown"
+
+
+def _getCommandName(command: int) -> str:
+ """
+ Get the name of the command type.
+
+ Args:
+ command (int): The command to get the name for.
+
+ Returns:
+ str: The name of the command.
+ """
+ try:
+ return mavutil.mavlink.enums["MAV_CMD"][command].name
+ except KeyError:
+ return f"Unknown command {command}"
+
+
+def _parseWaypointsListIntoLoader(
+ waypoints: List[dict],
+ mission_type: int,
+ target_system: int = 1,
+ target_component: int = 1,
+) -> mavwp.MAVWPLoader:
+ """
+ Parses a list of waypoints into a MAVWPLoader object.
+
+ Args:
+ waypoints (List[dict]): The list of waypoints to parse.
+ mission_type (int): The type of mission. 0=Mission,1=Fence,2=Rally.
+ target_system (int): The target system ID (default: 1).
+ target_component (int): The target component ID (default: 1).
+
+ Returns:
+ mavwp.MAVWPLoader: The loader object with parsed waypoints.
+ """
+ if mission_type == TYPE_MISSION:
+ loader = mavwp.MAVWPLoader(
+ target_system=target_system, target_component=target_component
+ )
+ elif mission_type == TYPE_FENCE:
+ loader = mavwp.MissionItemProtocol_Fence(
+ target_system=target_system, target_component=target_component
+ )
+ elif mission_type == TYPE_RALLY:
+ loader = mavwp.MissionItemProtocol_Rally(
+ target_system=target_system, target_component=target_component
+ )
+ else:
+ logger.error(f"Invalid mission type {mission_type}")
+ raise ValueError(f"Invalid mission type {mission_type}")
+
+ for wp in waypoints:
+ if isinstance(wp, mavutil.mavlink.MAVLink_mission_item_int_message):
+ loader.add(wp)
+ elif isinstance(wp, dict):
+ # Convert dict to MAVLink mission item int message
+ p = mavutil.mavlink.MAVLink_mission_item_int_message(
+ target_system,
+ target_component,
+ wp["seq"],
+ wp["frame"],
+ wp["command"],
+ wp["current"],
+ wp["autocontinue"],
+ wp["param1"],
+ wp["param2"],
+ wp["param3"],
+ wp["param4"],
+ int(wp["x"]),
+ int(wp["y"]),
+ wp["z"],
+ wp["mission_type"],
+ )
+ loader.add(p)
+ else:
+ logger.error(f"Invalid waypoint type {type(wp)} in waypoints list")
+ raise ValueError(f"Invalid waypoint type {type(wp)} in waypoints list")
+
+ logger.debug(
+ f"Parsed {loader.count()} waypoints into loader for mission type {mission_type}"
+ )
+ return loader
+
+
+def importMissionFromFile(
+ mission_type: int, file_path: str, target_system: int = 1, target_component: int = 1
+) -> Response:
+ """
+ Imports a mission from a file, return the waypoints loaded.
+
+ Args:
+ mission_type (int): The type of mission to import. 0=Mission,1=Fence,2=Rally.
+ file_path (str): The path to the waypoint file to import.
+ target_system (int): The target system ID (default: 1).
+ target_component (int): The target component ID (default: 1).
+
+ Returns:
+ Response: A response dict with success status and waypoint data.
+ """
+ mission_type_check = _checkMissionType(mission_type)
+ if not mission_type_check.get("success"):
+ return mission_type_check
+
+ if not file_path or not os.path.exists(file_path):
+ logger.error(f"Waypoint file not found at {file_path}")
+ return {
+ "success": False,
+ "message": f"Waypoint file not found at {file_path}",
+ }
+
+ logger.debug(
+ f"Importing waypoint file from {file_path} for mission type {mission_type}"
+ )
+
+ if mission_type == TYPE_MISSION:
+ loader = mavwp.MAVWPLoader(
+ target_system=target_system, target_component=target_component
+ )
+ elif mission_type == TYPE_FENCE:
+ loader = mavwp.MissionItemProtocol_Fence(
+ target_system=target_system, target_component=target_component
+ )
+ else:
+ loader = mavwp.MissionItemProtocol_Rally(
+ target_system=target_system, target_component=target_component
+ )
+
+ try:
+ loader.load(file_path)
+ except Exception as e:
+ logger.error(f"Failed to load waypoint file: {e}")
+ return {
+ "success": False,
+ "message": f"Failed to load waypoint file: {e}",
+ }
+
+ # 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]:
+ if loader.count() > 0:
+ first_wp = loader.item(0)
+ if first_wp.command == 16:
+ loader.remove(first_wp)
+ else:
+ logger.error("Loader is empty; no waypoints to process.")
+ return {
+ "success": False,
+ "message": "Loader is empty; no waypoints to process.",
+ }
+
+ for wp in loader.wpoints:
+ # Check if mission type correlates to correct command
+ if (
+ (
+ mission_type == TYPE_RALLY
+ and wp.command != mavutil.mavlink.MAV_CMD_NAV_RALLY_POINT
+ )
+ or (
+ mission_type == TYPE_FENCE
+ and wp.command
+ not in [
+ mavutil.mavlink.MAV_CMD_NAV_FENCE_RETURN_POINT,
+ mavutil.mavlink.MAV_CMD_NAV_FENCE_POLYGON_VERTEX_INCLUSION,
+ mavutil.mavlink.MAV_CMD_NAV_FENCE_POLYGON_VERTEX_EXCLUSION,
+ mavutil.mavlink.MAV_CMD_NAV_FENCE_CIRCLE_INCLUSION,
+ mavutil.mavlink.MAV_CMD_NAV_FENCE_CIRCLE_EXCLUSION,
+ ]
+ )
+ or (
+ mission_type == TYPE_MISSION
+ and wp.command
+ in [
+ mavutil.mavlink.MAV_CMD_NAV_RALLY_POINT,
+ mavutil.mavlink.MAV_CMD_NAV_FENCE_RETURN_POINT,
+ mavutil.mavlink.MAV_CMD_NAV_FENCE_POLYGON_VERTEX_INCLUSION,
+ mavutil.mavlink.MAV_CMD_NAV_FENCE_POLYGON_VERTEX_EXCLUSION,
+ mavutil.mavlink.MAV_CMD_NAV_FENCE_CIRCLE_INCLUSION,
+ mavutil.mavlink.MAV_CMD_NAV_FENCE_CIRCLE_EXCLUSION,
+ ]
+ )
+ ):
+ logger.error(
+ f"Waypoint command {_getCommandName(wp.command)} does not match mission type {_getMissionName(mission_type)}"
+ )
+ return {
+ "success": False,
+ "message": f"Could not load the waypoint file. Waypoint command {_getCommandName(wp.command)} does not match mission type {_getMissionName(mission_type)}",
+ }
+
+ # Convert coordinates to the correct format
+ if hasattr(wp, "x") and hasattr(wp, "y"):
+ try:
+ wp.x = _convertCoordinate(wp.x)
+ wp.y = _convertCoordinate(wp.y)
+ except ValueError as e:
+ logger.error(f"Error converting coordinates for waypoint {wp}: {e}")
+ return {
+ "success": False,
+ "message": f"Error converting coordinates for waypoint {wp}: {e}",
+ }
+
+ logger.info(f"Loaded waypoint file with {loader.count()} points successfully")
+ return {
+ "success": True,
+ "message": f"Waypoint file loaded {loader.count()} points successfully",
+ "data": [wp.to_dict() for wp in loader.wpoints],
+ }
+
+
+def exportMissionToFile(
+ mission_type: int,
+ file_path: str,
+ waypoints: List[dict],
+ target_system: int = 1,
+ target_component: int = 1,
+) -> Response:
+ """
+ Exports a mission to a file from a given list of waypoints.
+
+ Args:
+ mission_type (int): The type of mission to export. 0=Mission,1=Fence,2=Rally.
+ file_path (str): The path to the waypoint file to export.
+ waypoints (List[dict]): The list of waypoints to upload. Each waypoint should be a dict with the required fields.
+ target_system (int): The target system ID (default: 1).
+ target_component (int): The target component ID (default: 1).
+
+ Returns:
+ Response: A response dict with success status.
+ """
+ mission_type_check = _checkMissionType(mission_type)
+ if not mission_type_check.get("success"):
+ return mission_type_check
+
+ try:
+ loader = _parseWaypointsListIntoLoader(
+ waypoints, mission_type, target_system, target_component
+ )
+ except ValueError as e:
+ logger.error(f"Error parsing waypoints: {e}")
+ return {
+ "success": False,
+ "message": f"Error parsing waypoints: {e}",
+ }
+
+ # Convert coordinates to the correct format for saving
+ for wp in loader.wpoints:
+ if hasattr(wp, "x") and hasattr(wp, "y"):
+ try:
+ wp.x = _convertCoordinate(wp.x)
+ wp.y = _convertCoordinate(wp.y)
+ except ValueError as e:
+ logger.error(f"Error converting coordinates for waypoint {wp}: {e}")
+ return {
+ "success": False,
+ "message": f"Error converting coordinates for waypoint {wp}: {e}",
+ }
+
+ if loader.count() == 0:
+ return {
+ "success": False,
+ "message": f"No waypoints loaded for the mission type of {_getMissionName(mission_type)}",
+ }
+
+ logger.debug(
+ f"Exporting waypoint file to {file_path} for mission type {_getMissionName(mission_type)}"
+ )
+
+ try:
+ loader.save(file_path)
+ except Exception as e:
+ logger.error(f"Failed to save waypoint file: {e}")
+ return {
+ "success": False,
+ "message": f"Failed to save waypoint file: {e}",
+ }
+
+ logger.info(
+ f"Saved waypoint file with {loader.count()} points successfully to {file_path}"
+ )
+ return {
+ "success": True,
+ "message": f"Waypoint file saved {loader.count()} points successfully to {file_path}",
+ }
+
class MissionController:
def __init__(self, drone: Drone) -> None:
@@ -43,54 +390,6 @@ def __init__(self, drone: Drone) -> None:
target_system=drone.target_system, target_component=drone.target_component
)
- 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 _convertCoordinate(self, coordinate) -> Number:
- gps_coordinate_scale = 1e7
-
- if isinstance(coordinate, float):
- return int(coordinate * gps_coordinate_scale)
- elif isinstance(coordinate, int):
- return coordinate / gps_coordinate_scale
-
- raise ValueError(
- f"Invalid coordinate type {type(coordinate)}. Must be int or float."
- )
-
- def _getMissionName(self, mission_type: int) -> str:
- """
- Get the name of the mission type.
-
- Args:
- mission_type (int): The type of mission to get the name for.
- """
- if mission_type == TYPE_MISSION:
- return "mission"
- elif mission_type == TYPE_FENCE:
- return "fence"
- elif mission_type == TYPE_RALLY:
- return "rally"
- else:
- raise ValueError(f"Invalid mission type {mission_type}")
-
- def _getCommandName(self, command: int) -> str:
- """
- Get the name of the command type.
-
- Args:
- command (int): The command to get the name for.
- """
- try:
- return mavutil.mavlink.enums["MAV_CMD"][command].name
- except KeyError:
- return f"Unknown command {command}"
-
def getCurrentMission(
self,
mission_type: int,
@@ -104,7 +403,7 @@ def getCurrentMission(
progressUpdateCallback (Optional[Callable]): A callback function to update the progress of the mission fetch.
The callback should accept a string message and a float progress value.
"""
- mission_type_check = self._checkMissionType(mission_type)
+ mission_type_check = _checkMissionType(mission_type)
if not mission_type_check.get("success"):
return mission_type_check
@@ -177,7 +476,7 @@ def getMissionItems(
progressUpdateCallback (Optional[Callable]): A callback function to update the progress of the mission fetch.
The callback should accept a string message and a float progress value.
"""
- mission_type_check = self._checkMissionType(mission_type)
+ mission_type_check = _checkMissionType(mission_type)
if not mission_type_check.get("success"):
return mission_type_check
@@ -309,7 +608,7 @@ def getItemDetails(
Returns:
Dict: The details of the mission item
"""
- mission_type_check = self._checkMissionType(mission_type)
+ mission_type_check = _checkMissionType(mission_type)
if not mission_type_check.get("success"):
return mission_type_check
@@ -465,7 +764,7 @@ def clearMission(self, mission_type: int) -> Response:
Args:
mission_type (int): The type of mission to clear. 0=Mission,1=Fence,2=Rally.
"""
- mission_type_check = self._checkMissionType(mission_type)
+ mission_type_check = _checkMissionType(mission_type)
if not mission_type_check.get("success"):
return mission_type_check
@@ -522,57 +821,16 @@ def _parseWaypointsListIntoLoader(
) -> mavwp.MAVWPLoader:
"""
Parses a list of waypoints into a MAVWPLoader object.
- """
- if mission_type == TYPE_MISSION:
- loader = mavwp.MAVWPLoader(
- target_system=self.drone.target_system,
- target_component=self.drone.target_component,
- )
- elif mission_type == TYPE_FENCE:
- loader = mavwp.MissionItemProtocol_Fence(
- target_system=self.drone.target_system,
- target_component=self.drone.target_component,
- )
- elif mission_type == TYPE_RALLY:
- loader = mavwp.MissionItemProtocol_Rally(
- target_system=self.drone.target_system,
- target_component=self.drone.target_component,
- )
-
- for wp in waypoints:
- if isinstance(wp, mavutil.mavlink.MAVLink_mission_item_int_message):
- loader.add(wp)
- elif isinstance(wp, dict):
- # Convert dict to MAVLink mission item int message
- p = mavutil.mavlink.MAVLink_mission_item_int_message(
- self.drone.target_system,
- self.drone.target_component,
- wp["seq"],
- wp["frame"],
- wp["command"],
- wp["current"],
- wp["autocontinue"],
- wp["param1"],
- wp["param2"],
- wp["param3"],
- wp["param4"],
- int(wp["x"]),
- int(wp["y"]),
- wp["z"],
- wp["mission_type"],
- )
- loader.add(p)
- else:
- self.drone.logger.error(
- f"Invalid waypoint type {type(wp)} in waypoints list"
- )
- raise ValueError(f"Invalid waypoint type {type(wp)} in waypoints list")
-
- self.drone.logger.debug(
- f"Parsed {loader.count()} waypoints into loader for mission type {mission_type}"
+ This is a wrapper method that calls the standalone _parseWaypointsListIntoLoader function
+ with the drone's target_system and target_component.
+ """
+ return _parseWaypointsListIntoLoader(
+ waypoints,
+ mission_type,
+ target_system=self.drone.target_system,
+ target_component=self.drone.target_component,
)
- return loader
def uploadMission(
self,
@@ -589,11 +847,18 @@ def uploadMission(
progressUpdateCallback (Optional[Callable]): A callback function to update the progress of the mission writing.
The callback should accept a string message and a float progress value.
"""
- mission_type_check = self._checkMissionType(mission_type)
+ mission_type_check = _checkMissionType(mission_type)
if not mission_type_check.get("success"):
return mission_type_check
- new_loader = self._parseWaypointsListIntoLoader(waypoints, mission_type)
+ try:
+ new_loader = self._parseWaypointsListIntoLoader(waypoints, mission_type)
+ except ValueError as e:
+ self.drone.logger.error(f"Error parsing waypoints: {e}")
+ return {
+ "success": False,
+ "message": f"Error parsing waypoints: {e}",
+ }
clear_mission_response = self.clearMission(mission_type)
if not clear_mission_response.get("success"):
@@ -722,161 +987,38 @@ def importMissionFromFile(self, mission_type: int, file_path: str) -> Response:
"""
Imports a mission from a file, return the waypoints loaded.
+ This is a wrapper method that calls the standalone importMissionFromFile function
+ with the drone's target_system and target_component.
+
Args:
mission_type (int): The type of mission to import. 0=Mission,1=Fence,2=Rally.
file_path (str): The path to the waypoint file to import.
"""
- mission_type_check = self._checkMissionType(mission_type)
- if not mission_type_check.get("success"):
- return mission_type_check
-
- if not file_path or 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}",
- }
-
- self.drone.logger.debug(
- f"Importing waypoint file from {file_path} for mission type {mission_type}"
+ return importMissionFromFile(
+ mission_type,
+ file_path,
+ target_system=self.drone.target_system,
+ target_component=self.drone.target_component,
)
- if mission_type == TYPE_MISSION:
- loader = mavwp.MAVWPLoader(
- target_system=self.drone.target_system,
- target_component=self.drone.target_component,
- )
- elif mission_type == TYPE_FENCE:
- loader = mavwp.MissionItemProtocol_Fence(
- target_system=self.drone.target_system,
- target_component=self.drone.target_component,
- )
- else:
- loader = mavwp.MissionItemProtocol_Rally(
- target_system=self.drone.target_system,
- target_component=self.drone.target_component,
- )
-
- try:
- loader.load(file_path)
- except Exception as e:
- self.drone.logger.error(f"Failed to load waypoint file: {e}")
- return {
- "success": False,
- "message": f"Failed to load waypoint file: {e}",
- }
-
- # 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]:
- if loader.count() > 0:
- first_wp = loader.item(0)
- if first_wp.command == 16:
- loader.remove(first_wp)
- else:
- self.drone.logger.error("Loader is empty; no waypoints to process.")
- return {
- "success": False,
- "message": "Loader is empty; no waypoints to process.",
- }
-
- for wp in loader.wpoints:
- # Check if mission type correlates to correct command
- if (
- (
- mission_type == TYPE_RALLY
- and wp.command != mavutil.mavlink.MAV_CMD_NAV_RALLY_POINT
- )
- or (
- mission_type == TYPE_FENCE
- and wp.command
- not in [
- mavutil.mavlink.MAV_CMD_NAV_FENCE_RETURN_POINT,
- mavutil.mavlink.MAV_CMD_NAV_FENCE_POLYGON_VERTEX_INCLUSION,
- mavutil.mavlink.MAV_CMD_NAV_FENCE_POLYGON_VERTEX_EXCLUSION,
- mavutil.mavlink.MAV_CMD_NAV_FENCE_CIRCLE_INCLUSION,
- mavutil.mavlink.MAV_CMD_NAV_FENCE_CIRCLE_EXCLUSION,
- ]
- )
- or (
- mission_type == TYPE_MISSION
- and wp.command
- in [
- mavutil.mavlink.MAV_CMD_NAV_RALLY_POINT,
- mavutil.mavlink.MAV_CMD_NAV_FENCE_RETURN_POINT,
- mavutil.mavlink.MAV_CMD_NAV_FENCE_POLYGON_VERTEX_INCLUSION,
- mavutil.mavlink.MAV_CMD_NAV_FENCE_POLYGON_VERTEX_EXCLUSION,
- mavutil.mavlink.MAV_CMD_NAV_FENCE_CIRCLE_INCLUSION,
- mavutil.mavlink.MAV_CMD_NAV_FENCE_CIRCLE_EXCLUSION,
- ]
- )
- ):
- self.drone.logger.error(
- f"Waypoint command {self._getCommandName(wp.command)} does not match mission type {self._getMissionName(mission_type)}"
- )
- return {
- "success": False,
- "message": f"Could not load the waypoint file. Waypoint command {self._getCommandName(wp.command)} does not match mission type {self._getMissionName(mission_type)}",
- }
-
- # Convert coordinates to the correct format
- if hasattr(wp, "x") and hasattr(wp, "y"):
- wp.x = self._convertCoordinate(wp.x)
- wp.y = self._convertCoordinate(wp.y)
-
- 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",
- "data": [wp.to_dict() for wp in loader.wpoints],
- }
-
def exportMissionToFile(
self, mission_type: int, file_path: str, waypoints: List[dict]
) -> Response:
"""
- Exports a mission to a file from a given list of waypoints.
+ Exports a mission to a file.
+
+ This is a wrapper method that calls the standalone exportMissionToFile function
+ with the drone's target_system and target_component.
Args:
mission_type (int): The type of mission to export. 0=Mission,1=Fence,2=Rally.
file_path (str): The path to the waypoint file to export.
- waypoints (List[dict]): The list of waypoints to upload. Each waypoint should be a dict with the required fields.
+ waypoints (list): The list of waypoints to export.
"""
- mission_type_check = self._checkMissionType(mission_type)
- if not mission_type_check.get("success"):
- return mission_type_check
-
- loader = self._parseWaypointsListIntoLoader(waypoints, mission_type)
-
- for wp in loader.wpoints:
- if hasattr(wp, "x") and hasattr(wp, "y"):
- wp.x = self._convertCoordinate(wp.x)
- wp.y = self._convertCoordinate(wp.y)
-
- if loader.count() == 0:
- return {
- "success": False,
- "message": f"No waypoints loaded for the mission type of {self._getMissionName(mission_type)}",
- }
-
- self.drone.logger.debug(
- f"Exporting waypoint file to {file_path} for mission type {self._getMissionName(mission_type)}"
+ return exportMissionToFile(
+ mission_type,
+ file_path,
+ waypoints,
+ target_system=self.drone.target_system,
+ target_component=self.drone.target_component,
)
-
- try:
- loader.save(file_path)
- except Exception as e:
- self.drone.logger.error(f"Failed to save waypoint file: {e}")
- return {
- "success": False,
- "message": f"Failed to save waypoint file: {e}",
- }
-
- self.drone.logger.info(
- f"Saved waypoint file with {loader.count()} points successfully to {file_path}"
- )
- return {
- "success": True,
- "message": f"Waypoint file saved {loader.count()} points successfully to {file_path}",
- }
diff --git a/radio/app/endpoints/mission.py b/radio/app/endpoints/mission.py
index ef549d099..845ccdfe0 100644
--- a/radio/app/endpoints/mission.py
+++ b/radio/app/endpoints/mission.py
@@ -4,6 +4,12 @@
import app.droneStatus as droneStatus
from app import logger, socketio
+from app.controllers.missionController import (
+ exportMissionToFile as exportMissionToFileNotConnected,
+)
+from app.controllers.missionController import (
+ importMissionFromFile as importMissionFromFileNotConnected,
+)
from app.utils import notConnectedError
@@ -183,9 +189,6 @@ def importMissionFromFile(data: ImportMissionFileType) -> None:
logger.debug(f"Current state: {droneStatus.state}")
return
- if not droneStatus.drone:
- return notConnectedError(action="import mission from file")
-
mission_type = data.get("type")
mission_type_array = ["mission", "fence", "rally"]
@@ -204,9 +207,14 @@ def importMissionFromFile(data: ImportMissionFileType) -> None:
file_path = data.get("file_path", "")
- result = droneStatus.drone.missionController.importMissionFromFile(
- mission_type_array.index(mission_type), file_path
- )
+ if droneStatus.drone is not None:
+ result = droneStatus.drone.missionController.importMissionFromFile(
+ mission_type_array.index(mission_type), file_path
+ )
+ else:
+ result = importMissionFromFileNotConnected(
+ mission_type_array.index(mission_type), file_path
+ )
if not result.get("success"):
logger.error(result.get("message"))
@@ -235,9 +243,6 @@ def exportMissionToFile(data: ExportMissionFileType) -> None:
logger.debug(f"Current state: {droneStatus.state}")
return
- if not droneStatus.drone:
- return notConnectedError(action="export mission to file")
-
mission_type = data.get("type")
mission_type_array = ["mission", "fence", "rally"]
@@ -257,9 +262,14 @@ def exportMissionToFile(data: ExportMissionFileType) -> None:
file_path = data.get("file_path", "")
items = data.get("items", [])
- result = droneStatus.drone.missionController.exportMissionToFile(
- mission_type_array.index(mission_type), file_path, items
- )
+ if droneStatus.drone is not None:
+ result = droneStatus.drone.missionController.exportMissionToFile(
+ mission_type_array.index(mission_type), file_path, items
+ )
+ else:
+ result = exportMissionToFileNotConnected(
+ mission_type_array.index(mission_type), file_path, items
+ )
if not result.get("success"):
logger.error(result.get("message"))
diff --git a/radio/app/endpoints/states.py b/radio/app/endpoints/states.py
index 892892878..19aac577d 100644
--- a/radio/app/endpoints/states.py
+++ b/radio/app/endpoints/states.py
@@ -2,10 +2,9 @@
from typing_extensions import TypedDict
import app.droneStatus as droneStatus
-from app import socketio
+from app import logger, socketio
from app.utils import (
missingParameterError,
- notConnectedError,
sendMessage,
)
@@ -50,16 +49,16 @@ def set_state(data: SetStateType) -> None:
Args:
data: The form data passed in from the frontend, this contains the state we wish to change to
"""
- if not droneStatus.drone:
- return notConnectedError(action="set the drone state")
-
# Ensure that a state was actually sent
if (newState := data.get("state", None)) is None:
return missingParameterError("set_state", "state")
+ logger.info(f"Changing state to {newState}")
+
droneStatus.state = newState
- droneStatus.drone.logger.info(f"Changing state to {droneStatus.state}")
+ if not droneStatus.drone:
+ return
# Reset all data streams
droneStatus.drone.stopAllDataStreams()
diff --git a/radio/tests/test_MissionController.py b/radio/tests/test_MissionController.py
index 259ff53f2..5b81ed17d 100644
--- a/radio/tests/test_MissionController.py
+++ b/radio/tests/test_MissionController.py
@@ -1,52 +1,47 @@
import pytest
-from app.controllers.missionController import MissionController
+from app.controllers.missionController import (
+ _checkMissionType,
+ _convertCoordinate,
+ _getCommandName,
+ _getMissionName,
+)
-class DummyDrone:
- target_system = 1
- target_component = 1
-
-
-@pytest.fixture
-def controller():
- return MissionController(DummyDrone())
-
-
-def test_checkMissionType_valid_mission(controller):
- resp = controller._checkMissionType(0)
+def test_checkMissionType_valid_mission():
+ resp = _checkMissionType(0)
assert resp["success"] is True
-def test_checkMissionType_valid_fence(controller):
- resp = controller._checkMissionType(1)
+def test_checkMissionType_valid_fence():
+ resp = _checkMissionType(1)
assert resp["success"] is True
-def test_checkMissionType_valid_rally(controller):
- resp = controller._checkMissionType(2)
+def test_checkMissionType_valid_rally():
+ resp = _checkMissionType(2)
assert resp["success"] is True
-def test_checkMissionType_invalid_type(controller):
- resp = controller._checkMissionType(9999)
+def test_checkMissionType_invalid_type():
+ resp = _checkMissionType(9999)
assert resp["success"] is False
-def test_convertCoordinate_float_to_int(controller):
- result = controller._convertCoordinate(52.7814618)
+def test_convertCoordinate_float_to_int():
+ result = _convertCoordinate(52.7814618)
assert isinstance(result, (int, float))
assert result == 527814618
-def test_convertCoordinate_int_to_float(controller):
- result = controller._convertCoordinate(527814618)
+def test_convertCoordinate_int_to_float():
+ result = _convertCoordinate(527814618)
assert isinstance(result, (int, float))
assert result == 52.7814618
-def test_convertCoordinate_invalid_type_raises(controller):
+def test_convertCoordinate_invalid_type_raises():
with pytest.raises(ValueError) as excinfo:
- controller._convertCoordinate("not_a_number")
+ _convertCoordinate("not_a_number")
assert (
str(excinfo.value)
@@ -54,34 +49,32 @@ def test_convertCoordinate_invalid_type_raises(controller):
)
-def test_getMissionName_mission(controller):
- name = controller._getMissionName(0)
+def test_getMissionName_mission():
+ name = _getMissionName(0)
assert name == "mission"
-def test_getMissionName_fence(controller):
- name = controller._getMissionName(1)
+def test_getMissionName_fence():
+ name = _getMissionName(1)
assert name == "fence"
-def test_getMissionName_rally(controller):
- name = controller._getMissionName(2)
+def test_getMissionName_rally():
+ name = _getMissionName(2)
assert name == "rally"
-def test_getMissionName_invalid_type_raises(controller):
- with pytest.raises(ValueError) as excinfo:
- controller._getMissionName(9999)
-
- assert str(excinfo.value) == "Invalid mission type 9999"
+def test_getMissionName_invalid_type():
+ name = _getMissionName(9999)
+ assert name == "unknown"
-def test_getCommandName_known_command(controller):
- name = controller._getCommandName(16) # Example known command
+def test_getCommandName_known_command():
+ name = _getCommandName(16) # Example known command
assert isinstance(name, str)
assert name == "MAV_CMD_NAV_WAYPOINT"
-def test_getCommandName_unknown_command(controller):
- name = controller._getCommandName(9999999) # Example unknown command
+def test_getCommandName_unknown_command():
+ name = _getCommandName(9999999) # Example unknown command
assert name == "Unknown command 9999999"
diff --git a/radio/tests/test_states.py b/radio/tests/test_states.py
index 7e803c15b..7c9652457 100644
--- a/radio/tests/test_states.py
+++ b/radio/tests/test_states.py
@@ -8,11 +8,11 @@
def test_setState_no_drone_connection(
socketio_client: SocketIOTestClient, droneStatus
) -> None:
- """Test that setState fails when no drone is connected"""
+ """Test that setState succeeds when no drone is connected"""
with NoDrone():
- assert send_and_receive("set_state", "dashboard") == {
- "message": "Must be connected to the drone to set the drone state."
- }
+ socketio_client.emit("set_state", {"state": "dashboard"})
+ assert len(socketio_client.get_received()) == 0
+ assert droneStatus.state == "dashboard"
@falcon_test(pass_drone_status=True)
@@ -69,16 +69,3 @@ def test_setState_config_rc_state(
socketio_client.emit("set_state", {"state": "config.rc"})
assert len(socketio_client.get_received()) == 0
assert len(droneStatus.drone.message_listeners) == 5
-
-
-# TODO: Sort this out
-# @falcon_test(pass_drone_status=True)
-# def test_setState_params_state(socketio_client: SocketIOTestClient, droneStatus) -> None:
-# """Test setting state to params"""
-# # pytest.skip(reason="Issues with parameterController to be fixed in alpha 0.1.8")
-# droneStatus.drone.message_listeners = {}
-#
-# socketio_client.emit("set_state", {"state": "params"})
-# time.sleep(15)
-# assert len(socketio_client.get_received()[-1]["args"][0]) == 1400
-# assert len(droneStatus.drone.message_listeners) == 1