Skip to content

Commit

Permalink
Use flight planning client
Browse files Browse the repository at this point in the history
  • Loading branch information
BenjaminPelletier committed Oct 10, 2023
1 parent 0e290f3 commit a40fb64
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 108 deletions.
25 changes: 14 additions & 11 deletions monitoring/monitorlib/clients/flight_planning/client_scd.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,6 @@ def _inject(
raise NotImplementedError(
f"Unsupported operator AirspaceUsageState '{usage_state}' with UasState '{uas_state}'"
)
volumes = [
v.to_scd_automated_testing_api()
for v in flight_info.basic_information.area
]
off_nominal_volumes = []
elif usage_state == AirspaceUsageState.InUse:
if uas_state == UasState.OffNominal:
state = scd_api.OperationalIntentState.Nonconforming
Expand All @@ -75,15 +70,23 @@ def _inject(
raise NotImplementedError(
f"Unsupported operator UasState '{uas_state}' with AirspaceUsageState '{usage_state}'"
)
else:
raise NotImplementedError(
f"Unsupported combination of operator AirspaceUsageState '{usage_state}' and UasState '{uas_state}'"
)

if uas_state == UasState.Nominal:
volumes = [
v.to_interuss_scd_api()
for v in flight_info.basic_information.area
]
off_nominal_volumes = []
else:
volumes = []
off_nominal_volumes = [
v.to_scd_automated_testing_api()
for v in flight_info.basic_information.area
]
else:
raise NotImplementedError(
f"Unsupported combination of operator AirspaceUsageState '{usage_state}' and UasState '{uas_state}'"
)

if "astm_f3548_21" in flight_info and flight_info.astm_f3548_21:
priority = flight_info.astm_f3548_21.priority
Expand Down Expand Up @@ -242,7 +245,7 @@ def report_readiness(self) -> TestPreparationActivityResponse:

def clear_area(self, area: Volume4D) -> TestPreparationActivityResponse:
req = scd_api.ClearAreaRequest(
request_id=str(uuid.uuid4()), extent=area.to_scd_automated_testing_api()
request_id=str(uuid.uuid4()), extent=area.to_interuss_scd_api()
)

op = scd_api.OPERATIONS[scd_api.OperationID.ClearArea]
Expand All @@ -264,7 +267,7 @@ def clear_area(self, area: Volume4D) -> TestPreparationActivityResponse:
)

if resp.outcome.success:
errors = []
errors = None
else:
errors = [f"[{resp.outcome.timestamp}]: {resp.outcome.message}"]

Expand Down
23 changes: 23 additions & 0 deletions monitoring/monitorlib/geotemporal.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,26 @@ def resolve_volume4d(template: Volume4DTemplate, start_of_test: datetime) -> Vol
class Volume4DCollection(ImplicitDict):
volumes: List[Volume4D]

def __add__(self, other):
if isinstance(other, Volume4D):
return Volume4DCollection(volumes=self.volumes + [other])
elif isinstance(other, Volume4DCollection):
return Volume4DCollection(volumes=self.volumes + other.volumes)
else:
raise NotImplementedError(
f"Cannot add {type(other).__name__} to {type(self).__name__}"
)

def __iadd__(self, other):
if isinstance(other, Volume4D):
self.volumes.append(other)
elif isinstance(other, Volume4DCollection):
self.volumes.extend(other.volumes)
else:
raise NotImplementedError(
f"Cannot iadd {type(other).__name__} to {type(self).__name__}"
)

@property
def time_start(self) -> Optional[Time]:
return (
Expand Down Expand Up @@ -547,3 +567,6 @@ def from_interuss_scd_api(

def to_f3548v21(self) -> List[f3548v21.Volume4D]:
return [v.to_f3548v21() for v in self.volumes]

def to_interuss_scd_api(self) -> List[interuss_scd_api.Volume4D]:
return [v.to_interuss_scd_api() for v in self.volumes]
229 changes: 134 additions & 95 deletions monitoring/uss_qualifier/resources/flight_planning/flight_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,25 @@
from implicitdict import ImplicitDict

from monitoring.monitorlib import infrastructure, fetch
from monitoring.monitorlib.clients.flight_planning.client import PlanningActivityError
from monitoring.monitorlib.clients.flight_planning.client_scd import (
SCDFlightPlannerClient,
)
from monitoring.monitorlib.clients.flight_planning.flight_info import (
ExecutionStyle,
FlightInfo,
BasicFlightPlanInformation,
ASTMF354821OpIntentInformation,
FlightAuthorisationData,
AirspaceUsageState,
UasState,
)
from monitoring.monitorlib.clients.flight_planning.planning import (
PlanningActivityResult,
FlightPlanStatus,
)
from monitoring.monitorlib.fetch import QueryError, Query
from monitoring.monitorlib.geotemporal import Volume4D
from monitoring.monitorlib.geotemporal import Volume4D, Volume4DCollection
from uas_standards.interuss.automated_testing.scd.v1.api import (
InjectFlightResponseResult,
DeleteFlightResponseResult,
Expand All @@ -15,6 +32,8 @@
InjectFlightRequest,
ClearAreaResponse,
ClearAreaRequest,
OperationalIntentState,
ClearAreaOutcome,
)
from monitoring.monitorlib.scd_automated_testing.scd_injection_api import (
SCOPE_SCD_QUALIFIER_INJECT,
Expand Down Expand Up @@ -45,17 +64,20 @@ def __init__(self, *args, **kwargs):


class FlightPlanner:
"""Manages the state and the interactions with flight planner USS"""
"""Manages the state and the interactions with flight planner USS.
Note: this class will be deprecated in favor of FlightPlannerClient."""

def __init__(
self,
config: FlightPlannerConfiguration,
auth_adapter: infrastructure.AuthAdapter,
):
self.config = config
self.client = infrastructure.UTMClientSession(
session = infrastructure.UTMClientSession(
self.config.injection_base_url, auth_adapter, config.timeout_seconds
)
self.scd_client = SCDFlightPlannerClient(session)

# Flights injected by this target.
self.created_flight_ids: Set[str] = set()
Expand All @@ -78,114 +100,131 @@ def request_flight(
request: InjectFlightRequest,
flight_id: Optional[str] = None,
) -> Tuple[InjectFlightResponse, fetch.Query, str]:
usage_states = {
OperationalIntentState.Accepted: AirspaceUsageState.Planned,
OperationalIntentState.Activated: AirspaceUsageState.InUse,
OperationalIntentState.Nonconforming: AirspaceUsageState.InUse,
OperationalIntentState.Contingent: AirspaceUsageState.InUse,
}
uas_states = {
OperationalIntentState.Accepted: UasState.Nominal,
OperationalIntentState.Activated: UasState.Nominal,
OperationalIntentState.Nonconforming: UasState.OffNominal,
OperationalIntentState.Contingent: UasState.Contingent,
}
if request.operational_intent.state in (OperationalIntentState.Accepted, OperationalIntentState.Activated) and request.operational_intent.off_nominal_volumes:
# This invalid request can no longer be represented with a standard flight planning request; reject it at the client level instead
raise ValueError(f"Request for nominal {request.operational_intent.state} operational intent is invalid because it contains off-nominal volumes")
v4c = Volume4DCollection.from_interuss_scd_api(
request.operational_intent.volumes
) + Volume4DCollection.from_interuss_scd_api(
request.operational_intent.off_nominal_volumes
)
basic_information = BasicFlightPlanInformation(
usage_state=usage_states[request.operational_intent.state],
uas_state=uas_states[request.operational_intent.state],
area=v4c.volumes,
)
astm_f3548v21 = ASTMF354821OpIntentInformation(
priority=request.operational_intent.priority
)
uspace_flight_authorisation = ImplicitDict.parse(
request.flight_authorisation, FlightAuthorisationData
)
flight_info = FlightInfo(
basic_information=basic_information,
astm_f3548_21=astm_f3548v21,
uspace_flight_authorisation=uspace_flight_authorisation,
)

if not flight_id:
flight_id = str(uuid.uuid4())
url = "{}/v1/flights/{}".format(self.config.injection_base_url, flight_id)

query = fetch.query_and_describe(
self.client,
"PUT",
url,
json=request,
scope=SCOPE_SCD_QUALIFIER_INJECT,
server_id=self.config.participant_id,
)
if query.status_code != 200:
raise QueryError(
f"Inject flight query to {url} returned {query.status_code}", [query]
)
try:
result = ImplicitDict.parse(
query.response.get("json", {}), InjectFlightResponse
)
except ValueError as e:
raise QueryError(
f"Inject flight response from {url} could not be decoded: {str(e)}",
[query],
try:
resp = self.scd_client.try_plan_flight(
flight_info, ExecutionStyle.IfAllowed
)
except PlanningActivityError as e:
raise QueryError(str(e), e.queries)
else:
try:
resp = self.scd_client.try_update_flight(
flight_id, flight_info, ExecutionStyle.IfAllowed
)
except PlanningActivityError as e:
raise QueryError(str(e), e.queries)

if resp.activity_result == PlanningActivityResult.Failed:
result = InjectFlightResponseResult.Failed
elif resp.activity_result == PlanningActivityResult.NotSupported:
result = InjectFlightResponseResult.NotSupported
elif resp.activity_result == PlanningActivityResult.Rejected:
result = InjectFlightResponseResult.Rejected
elif resp.activity_result == PlanningActivityResult.Completed:
if resp.flight_plan_status == FlightPlanStatus.Planned:
result = InjectFlightResponseResult.Planned
elif resp.flight_plan_status == FlightPlanStatus.OkToFly:
result = InjectFlightResponseResult.ReadyToFly
elif resp.flight_plan_status == FlightPlanStatus.OffNominal:
result = InjectFlightResponseResult.ReadyToFly
else:
raise NotImplementedError(
f"Unable to handle '{resp.flight_plan_status}' FlightPlanStatus with {resp.activity_result} PlanningActivityResult"
)
self.created_flight_ids.add(flight_id)
else:
raise NotImplementedError(
f"Unable to handle '{resp.activity_result}' PlanningActivityResult"
)

if result.result == InjectFlightResponseResult.Planned:
self.created_flight_ids.add(flight_id)
response = InjectFlightResponse(
result=result,
operational_intent_id="<not provided>",
)

return result, query, flight_id
return response, resp.queries[0], flight_id

def cleanup_flight(
self, flight_id: str
) -> Tuple[DeleteFlightResponse, fetch.Query]:
url = "{}/v1/flights/{}".format(self.config.injection_base_url, flight_id)
query = fetch.query_and_describe(
self.client,
"DELETE",
url,
scope=SCOPE_SCD_QUALIFIER_INJECT,
server_id=self.config.participant_id,
)
if query.status_code != 200:
raise QueryError(
f"Delete flight query to {url} returned {query.status_code}", [query]
)
try:
result = ImplicitDict.parse(
query.response.get("json", {}), DeleteFlightResponse
)
except ValueError as e:
raise QueryError(
f"Delete flight response from {url} could not be decoded: {str(e)}",
[query],
)

if result.result == DeleteFlightResponseResult.Closed:
resp = self.scd_client.try_end_flight(flight_id, ExecutionStyle.IfAllowed)
except PlanningActivityError as e:
raise QueryError(str(e), e.queries)

if (
resp.activity_result == PlanningActivityResult.Completed
and resp.flight_plan_status == FlightPlanStatus.Closed
):
self.created_flight_ids.remove(flight_id)
return result, query

def get_readiness(self) -> Tuple[Optional[str], Query]:
url_status = "{}/v1/status".format(self.config.injection_base_url)
version_query = fetch.query_and_describe(
self.client,
"GET",
url_status,
scope=SCOPE_SCD_QUALIFIER_INJECT,
server_id=self.config.participant_id,
)
if version_query.status_code != 200:
return (
f"Status query to {url_status} returned {version_query.status_code}",
version_query,
DeleteFlightResponse(result=DeleteFlightResponseResult.Closed),
resp.queries[0],
)
try:
ImplicitDict.parse(version_query.response.get("json", {}), StatusResponse)
except ValueError as e:
else:
return (
f"Status response from {url_status} could not be decoded: {str(e)}",
version_query,
DeleteFlightResponse(result=DeleteFlightResponseResult.Failed),
resp.queries[0],
)

return None, version_query
def get_readiness(self) -> Tuple[Optional[str], Query]:
try:
resp = self.scd_client.report_readiness()
except PlanningActivityError as e:
return str(e), e.queries[0]
return None, resp.queries[0]

def clear_area(self, extent: Volume4D) -> Tuple[ClearAreaResponse, fetch.Query]:
req = ClearAreaRequest(
request_id=str(uuid.uuid4()), extent=extent.to_f3548v21()
)
url = f"{self.config.injection_base_url}/v1/clear_area_requests"
query = fetch.query_and_describe(
self.client,
"POST",
url,
scope=SCOPE_SCD_QUALIFIER_INJECT,
json=req,
server_id=self.config.participant_id,
)
if query.status_code != 200:
raise QueryError(
f"Clear area query to {url} returned {query.status_code}", [query]
)
try:
result = ImplicitDict.parse(
query.response.get("json", {}), ClearAreaResponse
)
except ValueError as e:
raise QueryError(
f"Clear area response from {url} could not be decoded: {str(e)}",
[query],
)
return result, query
resp = self.scd_client.clear_area(extent)
except PlanningActivityError as e:
raise QueryError(str(e), e.queries)
success = False if resp.errors else True
return (
ClearAreaResponse(
outcome=ClearAreaOutcome(
success=success,
timestamp=resp.queries[0].response.reported,
)
),
resp.queries[0],
)
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def clear_area(
check.record_failed(
summary="Area could not be cleared",
severity=Severity.High,
details=f'Participant indicated "{resp.outcome.message}"',
details=f'Participant indicated "{resp.outcome.message}"' if "message" in resp.outcome else "See query",
query_timestamps=[query.request.timestamp],
)

Expand Down Expand Up @@ -385,7 +385,7 @@ def cleanup_flights(
else:
check.record_failed(
summary="Failed to delete flight",
details=f"USS indicated: {resp.notes}",
details=f"USS indicated: {resp.notes}" if "notes" in resp else "See query",
severity=Severity.Medium,
query_timestamps=[query.request.timestamp],
)

0 comments on commit a40fb64

Please sign in to comment.