Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions core/frontend/src/components/autopilot/EndpointCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,17 @@
</v-icon>
<v-switch
v-model="updated_endpoint.enabled"
v-tooltip="updated_endpoint.enabled ? 'Disable endpoint' : 'Enable endpoint'"
v-tooltip="enable_tooltip"
color="primary"
class="my-1 ml-2"
hide-details
dense
:disabled="!is_known_type"
@change="toggleEndpointEnabled"
/>
</div>
<v-btn
v-if="!endpoint.protected"
v-if="!endpoint.protected && is_known_type"
color="primary"
class="endpoint-edit-btn"
dark
Expand All @@ -79,7 +80,7 @@
</v-icon>
</v-btn>
<v-btn
v-if="!endpoint.protected"
v-if="!endpoint.protected || !is_known_type"
color="error"
class="endpoint-remove-btn"
dark
Expand Down Expand Up @@ -107,7 +108,7 @@ import Vue, { PropType } from 'vue'

import Notifier from '@/libs/notifier'
import autopilot from '@/store/autopilot_manager'
import { AutopilotEndpoint, userFriendlyEndpointType } from '@/types/autopilot'
import { AutopilotEndpoint, isKnownEndpointType, userFriendlyEndpointType } from '@/types/autopilot'
import { autopilot_service } from '@/types/frontend_services'
import back_axios from '@/utils/api'
import { sleep } from '@/utils/helper_functions'
Expand Down Expand Up @@ -147,6 +148,15 @@ export default Vue.extend({
}
return { icon: 'mdi-lock-off', tooltip: 'Not protected' }
},
is_known_type(): boolean {
return isKnownEndpointType(this.endpoint.connection_type)
},
enable_tooltip(): string {
if (!this.is_known_type) {
return 'This endpoint type is not supported'
}
return this.updated_endpoint.enabled ? 'Disable endpoint' : 'Enable endpoint'
},
},
methods: {
async removeEndpoint(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
},
Expand Down
11 changes: 8 additions & 3 deletions core/frontend/src/types/autopilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,26 @@ 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'
case EndpointType.tcpin: return 'TCP Server'
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
Expand Down
20 changes: 17 additions & 3 deletions core/services/ardupilot_manager/api/v1/routers/endpoints.py
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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)


Expand All @@ -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)
9 changes: 8 additions & 1 deletion core/services/ardupilot_manager/autopilot_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
23 changes: 19 additions & 4 deletions core/services/ardupilot_manager/mavlink_proxy/Endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand Down
4 changes: 3 additions & 1 deletion core/services/ardupilot_manager/mavlink_proxy/Manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")

Expand Down
34 changes: 32 additions & 2 deletions core/services/ardupilot_manager/mavlink_proxy/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading