Skip to content

Commit

Permalink
[uss_qualifier/rid] NET0470 OperatorID, UAS ID, Operator Location, Op…
Browse files Browse the repository at this point in the history
…erator Altitude, Operational Status, Current Position, Height, Timestamp, Track, Speed checks (#150)

* [rid] NET0470 Operator ID

* [rid] NET0470 UAS ID, Operator Location, Operator Altitude and Operational Status checks

* Fix misused argument

* format

* Reorganize version checks in common_dictionary

* Add todo for location altitude to address it separately

* Format and fix type of observed_details

* Capture TODO

* Implement new observation api fields for mock details responses

* Fix unit tests and format

* Return from mock riddp the operational_status

* Replace monitorlib observation_api by uas_standards

* Rewording

* Format

* Add check for astm.f3411.v22a.NET0470,Table1,5

* Add check for astm.f3411.v22a.NET0470,Table1,20

* Add check for astm.f3411.v22a.NET0470,Table1,20 resolution

* Add check for astm.f3411.v22a.NET0470,Table1,19 track and use injected speed

* Add check for astm.f3411.v22a.NET0470,Table1,14-15 height

* Add check for astm.f3411.v22a.NET0470,Table1,10-11 current position

* Fix import

* format

* Rename to _evaluate_arbitrary_uas_id and skip some tests if not v22a

* Clean up

* format

* Update doc

* Update participants

* Prevent exception when number is not round

* updates

* various fixes

* fixes

* align

* fixes

* cleanup

* remove resolution limit for RID DP

* remove resolution checks

* fix python test

---------

Co-authored-by: Mickaël Misbach <[email protected]>
  • Loading branch information
barroco and mickmis authored Sep 20, 2023
1 parent 3938023 commit eed69cb
Show file tree
Hide file tree
Showing 19 changed files with 1,056 additions and 215 deletions.
2 changes: 1 addition & 1 deletion interfaces/rid/v1
Submodule v1 updated 1 files
+1 −1 remoteid/augmented.yaml
4 changes: 3 additions & 1 deletion monitoring/mock_uss/riddp/clustering.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
from implicitdict import ImplicitDict

from monitoring.monitorlib.rid import RIDVersion
from monitoring.monitorlib.rid_automated_testing import observation_api
from uas_standards.interuss.automated_testing.rid.v1 import (
observation as observation_api,
)


class Point(object):
Expand Down
61 changes: 53 additions & 8 deletions monitoring/mock_uss/riddp/routes_observation.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
from typing import Dict, List, Optional, Tuple

import arrow
import flask
from loguru import logger
import s2sphere
from uas_standards.astm.f3411.v19.api import ErrorResponse
from uas_standards.astm.f3411.v19.constants import Scope

from uas_standards.astm.f3411.v22a.constants import (
MinHeightResolution,
MinTrackDirectionResolution,
MinSpeedResolution,
)
from monitoring.monitorlib import geo
from monitoring.monitorlib.fetch import rid as fetch
from monitoring.monitorlib.fetch.rid import Flight, FetchedISAs
from monitoring.monitorlib.rid import RIDVersion
from monitoring.monitorlib.rid_automated_testing import observation_api
from uas_standards.interuss.automated_testing.rid.v1 import (
observation as observation_api,
)
from monitoring.mock_uss import webapp
from monitoring.mock_uss.auth import requires_scope
from . import clustering, database, utm_client
from .behavior import DisplayProviderBehavior
from .config import KEY_RID_VERSION
from .database import db
from monitoring.monitorlib.formatting import limit_resolution


def _make_flight_observation(
Expand Down Expand Up @@ -55,10 +61,22 @@ def _make_flight_observation(
paths.append(current_path)

p = flight.most_recent_position
current_state = observation_api.CurrentState(
timestamp=p.time.isoformat(),
operational_status=flight.operational_status,
track=limit_resolution(flight.track, MinTrackDirectionResolution),
speed=limit_resolution(flight.speed, MinSpeedResolution),
)
h = p.get("height")
if h:
h.distance = limit_resolution(h.distance, MinHeightResolution)
return observation_api.Flight(
id=flight.id,
most_recent_position=observation_api.Position(lat=p.lat, lng=p.lng, alt=p.alt),
most_recent_position=observation_api.Position(
lat=p.lat, lng=p.lng, alt=p.alt, height=h
),
recent_paths=[observation_api.Path(positions=path) for path in paths],
current_state=current_state,
)


Expand Down Expand Up @@ -159,9 +177,36 @@ def riddp_display_data() -> Tuple[str, int]:
@requires_scope([Scope.Read])
def riddp_flight_details(flight_id: str) -> Tuple[str, int]:
"""Implements get flight details endpoint per automated testing API."""

tx = db.value
if flight_id not in tx.flights:
return 'Flight "{}" not found'.format(flight_id), 404
flight_info = tx.flights.get(flight_id)
if not flight_info:
return f'Flight "{flight_id}" not found', 404

return flask.jsonify(observation_api.GetDetailsResponse())
rid_version: RIDVersion = webapp.config[KEY_RID_VERSION]
flight_details = fetch.flight_details(
flight_info.flights_url, flight_id, True, rid_version, utm_client
)
details = flight_details.details

result = observation_api.GetDetailsResponse(
operator=observation_api.Operator(
id=details.operator_id,
location=None,
altitude=observation_api.OperatorAltitude(),
),
uas=observation_api.UAS(
id=details.arbitrary_uas_id,
),
)
if details.operator_location is not None:
result.operator.location = observation_api.LatLngPoint(
lat=details.operator_location.lat,
lng=details.operator_location.lng,
)
if details.operator_altitude is not None:
result.operator.altitude.altitude = details.operator_altitude.value
if details.operator_altitude_type is not None:
result.operator.altitude.altitude_type = (
observation_api.OperatorAltitudeAltitudeType(details.operator_altitude_type)
)
return flask.jsonify(result)
191 changes: 186 additions & 5 deletions monitoring/monitorlib/fetch/rid.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
import datetime
from typing import Dict, List, Optional, Any, Union

from implicitdict import ImplicitDict
from implicitdict import ImplicitDict, StringBasedDateTime
import s2sphere
from uas_standards.astm.f3411 import v19, v22a
import uas_standards.astm.f3411.v19.api
import uas_standards.astm.f3411.v19.constants
import uas_standards.astm.f3411.v22a.api
import uas_standards.astm.f3411.v22a.constants
import yaml
from uas_standards.astm.f3411.v22a.api import RIDHeight
from yaml.representer import Representer

from monitoring.monitorlib import fetch, rid_v1, rid_v2, geo
Expand Down Expand Up @@ -149,17 +150,21 @@ class Position(ImplicitDict):
time: datetime.datetime
"""Timestamp for the position."""

height: Optional[RIDHeight]

@staticmethod
def from_v19_rid_aircraft_position(
p: v19.api.RIDAircraftPosition, t: v19.api.StringBasedDateTime
) -> Position:
return Position(lat=p.lat, lng=p.lng, alt=p.alt, time=t.datetime)
return Position(lat=p.lat, lng=p.lng, alt=p.alt, time=t.datetime, height=None)

@staticmethod
def from_v22a_rid_aircraft_position(
p: v22a.api.RIDAircraftPosition, t: v22a.api.StringBasedDateTime
) -> Position:
return Position(lat=p.lat, lng=p.lng, alt=p.alt, time=t.datetime)
return Position(
lat=p.lat, lng=p.lng, alt=p.alt, time=t.datetime, height=p.get("height")
)


class Flight(ImplicitDict):
Expand Down Expand Up @@ -211,7 +216,7 @@ def most_recent_position(
)
else:
raise NotImplementedError(
f"Cannot retrieve most recent position using RID version {self.rid_version}"
f"Cannot retrieve most_recent_position using RID version {self.rid_version}"
)
else:
return None
Expand All @@ -230,7 +235,83 @@ def recent_positions(self) -> List[Position]:
]
else:
raise NotImplementedError(
f"Cannot retrieve recent positions using RID version {self.rid_version}"
f"Cannot retrieve recent_positions using RID version {self.rid_version}"
)

@property
def operational_status(self) -> Optional[str]:
if self.rid_version == RIDVersion.f3411_19:
if not self.v19_value.has_field_with_value(
"current_state"
) or not self.v19_value.current_state.has_field_with_value(
"operational_status"
):
return None
return self.v19_value.current_state.operational_status
elif self.rid_version == RIDVersion.f3411_22a:
if not self.v22a_value.has_field_with_value(
"current_state"
) or not self.v22a_value.current_state.has_field_with_value(
"operational_status"
):
return None
return self.v22a_value.current_state.operational_status
else:
raise NotImplementedError(
f"Cannot retrieve operational_status using RID version {self.rid_version}"
)

@property
def track(self) -> Optional[float]:
if self.rid_version == RIDVersion.f3411_19:
if not self.v19_value.has_field_with_value(
"current_state"
) or not self.v19_value.current_state.has_field_with_value("track"):
return None
return self.v19_value.current_state.track
elif self.rid_version == RIDVersion.f3411_22a:
if not self.v22a_value.has_field_with_value(
"current_state"
) or not self.v22a_value.current_state.has_field_with_value("track"):
return None
return self.v22a_value.current_state.track
else:
raise NotImplementedError(
f"Cannot retrieve track using RID version {self.rid_version}"
)

@property
def speed(self) -> Optional[float]:
if self.rid_version == RIDVersion.f3411_19:
if not self.v19_value.has_field_with_value(
"current_state"
) or not self.v19_value.current_state.has_field_with_value("speed"):
return None
return self.v19_value.current_state.speed
elif self.rid_version == RIDVersion.f3411_22a:
if not self.v22a_value.has_field_with_value(
"current_state"
) or not self.v22a_value.current_state.has_field_with_value("speed"):
return None
return self.v22a_value.current_state.speed
else:
raise NotImplementedError(
f"Cannot retrieve speed using RID version {self.rid_version}"
)

@property
def timestamp(self) -> Optional[StringBasedDateTime]:
if self.rid_version == RIDVersion.f3411_19:
if not self.v19_value.has_field_with_value("current_state"):
return None
return self.v19_value.current_state.timestamp
elif self.rid_version == RIDVersion.f3411_22a:
if not self.v22a_value.has_field_with_value("current_state"):
return None
return self.v22a_value.current_state.timestamp.value
else:
raise NotImplementedError(
f"Cannot retrieve speed using RID version {self.rid_version}"
)

def errors(self) -> List[str]:
Expand Down Expand Up @@ -321,6 +402,106 @@ def raw(
def id(self) -> str:
return self.raw.id

@property
def operator_id(self) -> str:
if self.rid_version == RIDVersion.f3411_19:
return self.v19_value.operator_id
elif self.rid_version == RIDVersion.f3411_22a:
return self.v22a_value.operator_id
else:
raise NotImplementedError(
f"Cannot retrieve operator_id using RID version {self.rid_version}"
)

@property
def arbitrary_uas_id(self) -> Optional[str]:
"""Returns a UAS id as a plain string without type hint.
If multiple are provided:
For v19, registration_number is returned if set, else it falls back to the serial_number.
For v22a, the order of ASTM F3411-v22a Table 1 is used.
If no match, it returns None.
"""
if self.rid_version == RIDVersion.f3411_19:
registration_number = self.v19_value.registration_number
if registration_number:
return registration_number
else:
return self.v19_value.serial_number
elif self.rid_version == RIDVersion.f3411_22a:
uas_id = self.v22a_value.uas_id
if uas_id.serial_number:
return uas_id.serial_number
elif uas_id.registration_id:
return uas_id.registration_id
elif uas_id.utm_id:
return uas_id.utm_id
elif uas_id.specific_session_id:
return uas_id.specific_session_id
else:
raise NotImplementedError(
f"Cannot retrieve plain_uas_id using RID version {self.rid_version}"
)

@property
def operator_location(
self,
) -> Optional[geo.LatLngPoint]:
if self.rid_version == RIDVersion.f3411_19:
if not self.v19_value.has_field_with_value("operator_location"):
return None
return geo.LatLngPoint(
lat=self.v19_value.operator_location.lat,
lng=self.v19_value.operator_location.lng,
)
elif self.rid_version == RIDVersion.f3411_22a:
if not self.v22a_value.has_field_with_value("operator_location"):
return None
pos = self.v22a_value.operator_location.position
return geo.LatLngPoint(lat=pos.lat, lng=pos.lng)
else:
raise NotImplementedError(
f"Cannot retrieve operator_position using RID version {self.rid_version}"
)

@property
def operator_altitude(
self,
) -> Optional[geo.Altitude]:
if self.rid_version == RIDVersion.f3411_19:
return None
elif self.rid_version == RIDVersion.f3411_22a:
if not self.v22a_value.has_field_with_value(
"operator_location"
) or not self.v22a_value.operator_location.has_field_with_value("altitude"):
return None
alt = self.v22a_value.operator_location.altitude
return geo.Altitude(
value=alt.value, reference=alt.reference, units=alt.units
)
else:
raise NotImplementedError(
f"Cannot retrieve operator_altitude using RID version {self.rid_version}"
)

@property
def operator_altitude_type(
self,
) -> Optional[str]:
if self.rid_version == RIDVersion.f3411_19:
return None
elif self.rid_version == RIDVersion.f3411_22a:
if not self.v22a_value.has_field_with_value(
"operator_location"
) or not self.v22a_value.operator_location.has_field_with_value(
"altitude_type"
):
return None
return self.v22a_value.operator_location.altitude_type
else:
raise NotImplementedError(
f"Cannot retrieve operator_altitude_type using RID version {self.rid_version}"
)


class Subscription(ImplicitDict):
"""Version-independent representation of a F3411 subscription."""
Expand Down
5 changes: 5 additions & 0 deletions monitoring/monitorlib/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,8 @@ def make_datetime(t) -> datetime.datetime:
return arrow.get(t).datetime
else:
raise ValueError("Could not convert {} to datetime".format(str(type(t))))


def limit_resolution(value: float, resolution: float) -> float:
"""Change resolution of a value"""
return round(value / resolution) * resolution
10 changes: 8 additions & 2 deletions monitoring/monitorlib/geo.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ class Altitude(ImplicitDict):
reference: AltitudeDatum
units: DistanceUnits

@staticmethod
def w84m(value: Optional[float]):
if not value:
return None
return Altitude(value=value, reference=AltitudeDatum.W84, units=DistanceUnits.M)


class Volume3D(ImplicitDict):
outline_circle: Optional[Circle] = None
Expand Down Expand Up @@ -151,14 +157,14 @@ def make_latlng_rect(area) -> s2sphere.LatLngRect:
)


def validate_lat(lat: str) -> float:
def validate_lat(lat: Union[str, float]) -> float:
lat = float(lat)
if lat < -90 or lat > 90:
raise ValueError("Latitude must be in [-90, 90] range")
return lat


def validate_lng(lng: str) -> float:
def validate_lng(lng: Union[str, float]) -> float:
lng = float(lng)
if lng < -180 or lng > 180:
raise ValueError("Longitude must be in [-180, 180] range")
Expand Down
Loading

0 comments on commit eed69cb

Please sign in to comment.