diff --git a/core/frontend/src/components/autopilot/EndpointCard.vue b/core/frontend/src/components/autopilot/EndpointCard.vue
index dd2671a35e..47bbf8d98c 100644
--- a/core/frontend/src/components/autopilot/EndpointCard.vue
+++ b/core/frontend/src/components/autopilot/EndpointCard.vue
@@ -56,16 +56,17 @@
{
diff --git a/core/frontend/src/components/autopilot/EndpointCreationDialog.vue b/core/frontend/src/components/autopilot/EndpointCreationDialog.vue
index 277d2f1083..7b190a32a0 100644
--- a/core/frontend/src/components/autopilot/EndpointCreationDialog.vue
+++ b/core/frontend/src/components/autopilot/EndpointCreationDialog.vue
@@ -159,7 +159,7 @@ export default Vue.extend({
available_ips(): string[] {
return [...new Set(beacon.available_domains.map((domain) => domain.ip))]
},
- connection_type(): EndpointType {
+ connection_type(): string {
return this.edited_endpoint.connection_type
},
},
diff --git a/core/frontend/src/types/autopilot.ts b/core/frontend/src/types/autopilot.ts
index feca849ab7..2468427d0d 100644
--- a/core/frontend/src/types/autopilot.ts
+++ b/core/frontend/src/types/autopilot.ts
@@ -47,7 +47,7 @@ export function vehicleTypeFromString(vehicle_type: string): Vehicle {
}
}
-export function userFriendlyEndpointType(type: EndpointType): string {
+export function userFriendlyEndpointType(type: string): string {
switch (type) {
case EndpointType.udpin: return 'UDP Server'
case EndpointType.udpout: return 'UDP Client'
@@ -55,13 +55,18 @@ export function userFriendlyEndpointType(type: EndpointType): string {
case EndpointType.tcpout: return 'TCP Client'
case EndpointType.serial: return 'Serial'
case EndpointType.zenoh: return 'Zenoh'
- default: return 'Undefined type'
+ default: return type
}
}
+
+export function isKnownEndpointType(type: string): boolean {
+ return (Object.values(EndpointType) as string[]).includes(type)
+}
+
export interface AutopilotEndpoint {
name: string
owner: string
- connection_type: EndpointType
+ connection_type: string
place: string
argument: number
persistent: boolean
diff --git a/core/services/ardupilot_manager/api/v1/routers/endpoints.py b/core/services/ardupilot_manager/api/v1/routers/endpoints.py
index 7b5d6f6635..778ad8e7a9 100644
--- a/core/services/ardupilot_manager/api/v1/routers/endpoints.py
+++ b/core/services/ardupilot_manager/api/v1/routers/endpoints.py
@@ -1,10 +1,10 @@
from typing import Any, Dict, List, Set
-from fastapi import APIRouter, Body, status
+from fastapi import APIRouter, Body, HTTPException, status
from fastapi_versioning import versioned_api_route
from autopilot_manager import AutoPilotManager
-from mavlink_proxy.Endpoint import Endpoint
+from mavlink_proxy.Endpoint import Endpoint, EndpointType
endpoints_router_v1 = APIRouter(
prefix="/endpoints",
@@ -16,13 +16,26 @@
autopilot = AutoPilotManager()
+def reject_unsupported_endpoints(endpoints: Set[Endpoint]) -> None:
+ unsupported = [endpoint.name for endpoint in endpoints if not endpoint.is_supported()]
+ if unsupported:
+ raise HTTPException(
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+ detail=(
+ f"Unsupported connection_type for endpoints {unsupported}. "
+ f"Valid types are: {[endpoint_type.value for endpoint_type in EndpointType]}."
+ ),
+ )
+
+
@endpoints_router_v1.get("/", response_model=List[Dict[str, Any]])
def get_available_endpoints() -> Any:
- return list(map(Endpoint.as_dict, autopilot.get_endpoints()))
+ return list(map(Endpoint.as_api_dict, autopilot.get_endpoints()))
@endpoints_router_v1.post("/", status_code=status.HTTP_201_CREATED)
async def create_endpoints(endpoints: Set[Endpoint] = Body(...)) -> Any:
+ reject_unsupported_endpoints(endpoints)
await autopilot.add_new_endpoints(endpoints)
@@ -33,4 +46,5 @@ async def remove_endpoints(endpoints: Set[Endpoint] = Body(...)) -> Any:
@endpoints_router_v1.put("/", status_code=status.HTTP_200_OK)
async def update_endpoints(endpoints: Set[Endpoint] = Body(...)) -> Any:
+ reject_unsupported_endpoints(endpoints)
await autopilot.update_endpoints(endpoints)
diff --git a/core/services/ardupilot_manager/autopilot_manager.py b/core/services/ardupilot_manager/autopilot_manager.py
index 2168c6125f..ec0bf8da00 100644
--- a/core/services/ardupilot_manager/autopilot_manager.py
+++ b/core/services/ardupilot_manager/autopilot_manager.py
@@ -672,7 +672,14 @@ async def restart_ardupilot(self) -> None:
raise
def _get_configuration_endpoints(self) -> Set[Endpoint]:
- return {Endpoint(**endpoint) for endpoint in self.configuration.get("endpoints") or []}
+ endpoints: Set[Endpoint] = set()
+ for raw in self.configuration.get("endpoints") or []:
+ endpoint = Endpoint.from_raw(raw)
+ if endpoint is None:
+ logger.warning(f"Ignoring invalid endpoint record {raw}")
+ continue
+ endpoints.add(endpoint)
+ return endpoints
def _save_endpoints_to_configuration(self, endpoints: Set[Endpoint]) -> None:
self.configuration["endpoints"] = list(map(Endpoint.as_dict, endpoints))
diff --git a/core/services/ardupilot_manager/mavlink_proxy/AbstractRouter.py b/core/services/ardupilot_manager/mavlink_proxy/AbstractRouter.py
index 9c86086462..0afe32cca6 100644
--- a/core/services/ardupilot_manager/mavlink_proxy/AbstractRouter.py
+++ b/core/services/ardupilot_manager/mavlink_proxy/AbstractRouter.py
@@ -183,7 +183,8 @@ def set_logdir(self, directory: pathlib.Path) -> None:
self._logdir = directory
def add_endpoint(self, endpoint: Endpoint) -> None:
- self._validate_endpoint(endpoint)
+ if endpoint.is_supported():
+ self._validate_endpoint(endpoint)
for current_endpoint in self._endpoints:
if endpoint == current_endpoint:
diff --git a/core/services/ardupilot_manager/mavlink_proxy/Endpoint.py b/core/services/ardupilot_manager/mavlink_proxy/Endpoint.py
index 76b3818588..a52936e4c0 100644
--- a/core/services/ardupilot_manager/mavlink_proxy/Endpoint.py
+++ b/core/services/ardupilot_manager/mavlink_proxy/Endpoint.py
@@ -58,13 +58,28 @@ def is_mavlink_endpoint(cls: Type["Endpoint"], values: Any) -> Any:
raise ValueError(f"Invalid serial baudrate: {argument}. Valid option are {VALID_SERIAL_BAUDRATES}.")
return values
- raise ValueError(
- f"Invalid connection_type: {connection_type}. Valid types are: {[e.value for e in EndpointType]}."
- )
+ return values
+
+ def is_supported(self) -> bool:
+ return self.connection_type in {endpoint_type.value for endpoint_type in EndpointType}
@staticmethod
def filter_enabled(endpoints: Iterable["Endpoint"]) -> Iterable["Endpoint"]:
- return [endpoint for endpoint in endpoints if endpoint.enabled is True]
+ return [endpoint for endpoint in endpoints if endpoint.enabled is True and endpoint.is_supported()]
+
+ @staticmethod
+ def from_raw(raw_endpoint: Any) -> Optional["Endpoint"]:
+ """Build an endpoint from a saved record, returning None if the record is not an endpoint."""
+ try:
+ return Endpoint(**raw_endpoint)
+ except Exception:
+ return None
+
+ def as_api_dict(self) -> Dict[str, Any]:
+ data = self.as_dict()
+ if not self.is_supported():
+ data["enabled"] = False
+ return data
def __str__(self) -> str:
return ":".join([self.connection_type, self.place, str(self.argument)])
diff --git a/core/services/ardupilot_manager/mavlink_proxy/MAVLinkRouter.py b/core/services/ardupilot_manager/mavlink_proxy/MAVLinkRouter.py
index 475ed1dcc9..65bce964be 100644
--- a/core/services/ardupilot_manager/mavlink_proxy/MAVLinkRouter.py
+++ b/core/services/ardupilot_manager/mavlink_proxy/MAVLinkRouter.py
@@ -47,10 +47,10 @@ def convert_endpoint(endpoint: Endpoint) -> str:
EndpointType.TCPServer: 2,
EndpointType.TCPClient: 3,
}
- sorted_endpoints = sorted(
- self.endpoints(), key=lambda endpoint: types_order[endpoint.connection_type] # type: ignore
+ filtered_endpoints = sorted(
+ Endpoint.filter_enabled(self.endpoints()),
+ key=lambda endpoint: types_order[endpoint.connection_type], # type: ignore
)
- filtered_endpoints = Endpoint.filter_enabled(sorted_endpoints)
endpoints = " ".join([convert_endpoint(endpoint) for endpoint in filtered_endpoints])
if master_endpoint.connection_type not in [
diff --git a/core/services/ardupilot_manager/mavlink_proxy/Manager.py b/core/services/ardupilot_manager/mavlink_proxy/Manager.py
index 3a0de47336..881cbd9914 100644
--- a/core/services/ardupilot_manager/mavlink_proxy/Manager.py
+++ b/core/services/ardupilot_manager/mavlink_proxy/Manager.py
@@ -66,7 +66,9 @@ def add_endpoints(self, new_endpoints: Set[Endpoint]) -> None:
) from error
def remove_endpoints(self, endpoints_to_remove: Set[Endpoint]) -> None:
- protected_endpoints = set(filter(lambda endpoint: endpoint.protected, endpoints_to_remove))
+ protected_endpoints = {
+ endpoint for endpoint in endpoints_to_remove if endpoint.protected and endpoint.is_supported()
+ }
if protected_endpoints:
raise ValueError(f"Endpoints {[e.name for e in protected_endpoints]} are protected. Aborting operation.")
diff --git a/core/services/ardupilot_manager/mavlink_proxy/test_all.py b/core/services/ardupilot_manager/mavlink_proxy/test_all.py
index d55c53dde3..d0ae874e4e 100644
--- a/core/services/ardupilot_manager/mavlink_proxy/test_all.py
+++ b/core/services/ardupilot_manager/mavlink_proxy/test_all.py
@@ -144,8 +144,38 @@ def test_endpoint_validators() -> None:
Endpoint.is_mavlink_endpoint(
{"connection_type": EndpointType.Serial, "place": serial_port_name, "argument": 100000}
)
- with pytest.raises(ValueError):
- Endpoint.is_mavlink_endpoint({"connection_type": "potato", "place": serial_port_name, "argument": 100})
+
+
+def test_unsupported_endpoints_are_stored_but_not_routed() -> None:
+ known = {
+ "name": "GCS Client Link",
+ "owner": "ardupilot-manager",
+ "connection_type": "udpout",
+ "place": "192.168.2.1",
+ "argument": 14550,
+ }
+ unknown = {
+ "name": "Future endpoint",
+ "owner": "ardupilot-manager",
+ "connection_type": "notatype",
+ "place": "0.0.0.0",
+ "argument": 7117,
+ "persistent": True,
+ "protected": True,
+ "enabled": True,
+ }
+
+ parsed_known = Endpoint.from_raw(known)
+ parsed_unknown = Endpoint.from_raw(unknown)
+ assert parsed_known is not None
+ assert parsed_unknown is not None
+ assert parsed_known.is_supported() is True
+ assert parsed_unknown.is_supported() is False
+ assert parsed_unknown.enabled is True
+ assert parsed_unknown.as_api_dict()["enabled"] is False
+ assert parsed_unknown.as_dict()["enabled"] is True
+ assert list(Endpoint.filter_enabled([parsed_known, parsed_unknown])) == [parsed_known]
+ assert Endpoint.from_raw("not a mapping") is None
@pytest.mark.skip(