From ee2334bb75738c48b6c30492f38543f3d7a51828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Fri, 14 Aug 2026 20:01:56 -0300 Subject: [PATCH 1/6] core: services: ardupilot_manager: mavlink_proxy: Keep unknown endpoint types Unknown connection types construct as records. They are omitted from routing and reported disabled by as_api_dict. --- .../mavlink_proxy/Endpoint.py | 23 ++++++++++--- .../mavlink_proxy/test_all.py | 34 +++++++++++++++++-- 2 files changed, 51 insertions(+), 6 deletions(-) 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/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( From 5f97aabee0916e72dc91a90347c4e01b513f3511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Fri, 14 Aug 2026 20:01:56 -0300 Subject: [PATCH 2/6] core: services: ardupilot_manager: mavlink_proxy: Skip unknown types when routing --- .../ardupilot_manager/mavlink_proxy/AbstractRouter.py | 3 ++- .../ardupilot_manager/mavlink_proxy/MAVLinkRouter.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) 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/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 [ From d927688d5c3541404ee821efa700d1fcd239e8b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Fri, 14 Aug 2026 20:01:56 -0300 Subject: [PATCH 3/6] core: services: ardupilot_manager: mavlink_proxy: Manager: Allow deleting unknown types --- core/services/ardupilot_manager/mavlink_proxy/Manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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.") From 49d22c67c83080262f9962cefdbc8a565d483071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Fri, 14 Aug 2026 20:01:57 -0300 Subject: [PATCH 4/6] core: services: ardupilot_manager: Load unknown endpoints from settings --- core/services/ardupilot_manager/autopilot_manager.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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)) From 73c86cb9e32a656613ce10295c7d1a4f10977e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Fri, 14 Aug 2026 20:01:57 -0300 Subject: [PATCH 5/6] core: services: ardupilot_manager: api: Show unknown endpoints as disabled POST and PUT of unknown types return 422. --- .../api/v1/routers/endpoints.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) 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) From 9355fccc30b9db556592d673fa577c97180de8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Fri, 14 Aug 2026 20:01:57 -0300 Subject: [PATCH 6/6] core: frontend: src: Show unknown MAVLink endpoint types --- .../src/components/autopilot/EndpointCard.vue | 18 ++++++++++++++---- .../autopilot/EndpointCreationDialog.vue | 2 +- core/frontend/src/types/autopilot.ts | 11 ++++++++--- 3 files changed, 23 insertions(+), 8 deletions(-) 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