From eed69cb1d1c858127f63ed911585246a31be3015 Mon Sep 17 00:00:00 2001 From: Michael Barroco Date: Wed, 20 Sep 2023 18:20:31 -0400 Subject: [PATCH] [uss_qualifier/rid] NET0470 OperatorID, UAS ID, Operator Location, Operator Altitude, Operational Status, Current Position, Height, Timestamp, Track, Speed checks (#150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [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 --- interfaces/automated_testing | 2 +- interfaces/rid/v1 | 2 +- monitoring/mock_uss/riddp/clustering.py | 4 +- .../mock_uss/riddp/routes_observation.py | 61 ++- monitoring/monitorlib/fetch/rid.py | 191 ++++++++- monitoring/monitorlib/formatting.py | 5 + monitoring/monitorlib/geo.py | 10 +- .../rid_automated_testing/observation_api.py | 38 +- .../resources/netrid/observers.py | 7 +- .../simulation/operator_flight_details.py | 1 + .../netrid/common_dictionary_evaluator.py | 387 ++++++++++++++---- .../common_dictionary_evaluator_test.py | 227 +++++++--- .../astm/netrid/display_data_evaluator.py | 51 ++- .../astm/netrid/v19/nominal_behavior.md | 8 + .../astm/netrid/v22a/nominal_behavior.md | 59 ++- .../suites/astm/netrid/f3411_22a.md | 72 +++- .../suites/uspace/network_identification.md | 72 +++- .../suites/uspace/required_services.md | 72 +++- requirements.txt | 2 +- 19 files changed, 1056 insertions(+), 215 deletions(-) diff --git a/interfaces/automated_testing b/interfaces/automated_testing index 8c83e2735c..e5e5ff2a3f 160000 --- a/interfaces/automated_testing +++ b/interfaces/automated_testing @@ -1 +1 @@ -Subproject commit 8c83e2735c762f6fee8d6ca62ee1c1c0d479512c +Subproject commit e5e5ff2a3f1ae6a381402e9ce5f9efb37d3b9876 diff --git a/interfaces/rid/v1 b/interfaces/rid/v1 index 935f3b64cf..228b301be1 160000 --- a/interfaces/rid/v1 +++ b/interfaces/rid/v1 @@ -1 +1 @@ -Subproject commit 935f3b64cf252e82777a74037001e17420bc3803 +Subproject commit 228b301be1f4466df9b014f20b515665861a8e67 diff --git a/monitoring/mock_uss/riddp/clustering.py b/monitoring/mock_uss/riddp/clustering.py index e0f6ce8332..110c1c0a77 100644 --- a/monitoring/mock_uss/riddp/clustering.py +++ b/monitoring/mock_uss/riddp/clustering.py @@ -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): diff --git a/monitoring/mock_uss/riddp/routes_observation.py b/monitoring/mock_uss/riddp/routes_observation.py index d4a1f3b5e6..ec2ef5a840 100644 --- a/monitoring/mock_uss/riddp/routes_observation.py +++ b/monitoring/mock_uss/riddp/routes_observation.py @@ -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( @@ -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, ) @@ -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) diff --git a/monitoring/monitorlib/fetch/rid.py b/monitoring/monitorlib/fetch/rid.py index bb96bf2d24..3f52335bfc 100644 --- a/monitoring/monitorlib/fetch/rid.py +++ b/monitoring/monitorlib/fetch/rid.py @@ -2,7 +2,7 @@ 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 @@ -10,6 +10,7 @@ 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 @@ -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): @@ -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 @@ -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]: @@ -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.""" diff --git a/monitoring/monitorlib/formatting.py b/monitoring/monitorlib/formatting.py index e428281840..ca6663b661 100644 --- a/monitoring/monitorlib/formatting.py +++ b/monitoring/monitorlib/formatting.py @@ -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 diff --git a/monitoring/monitorlib/geo.py b/monitoring/monitorlib/geo.py index dc191d5a6a..e58c18fa74 100644 --- a/monitoring/monitorlib/geo.py +++ b/monitoring/monitorlib/geo.py @@ -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 @@ -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") diff --git a/monitoring/monitorlib/rid_automated_testing/observation_api.py b/monitoring/monitorlib/rid_automated_testing/observation_api.py index e0998dc5d2..7efb31144a 100644 --- a/monitoring/monitorlib/rid_automated_testing/observation_api.py +++ b/monitoring/monitorlib/rid_automated_testing/observation_api.py @@ -1,37 +1 @@ -from typing import List, Optional - -from implicitdict import ImplicitDict - - -# Mirrors of types defined in remote ID automated testing observation API - - -class Position(ImplicitDict): - lat: float - lng: float - alt: Optional[float] - - -class Path(ImplicitDict): - positions: List[Position] - - -class Cluster(ImplicitDict): - corners: List[Position] - area_sqm: float - number_of_flights: int - - -class Flight(ImplicitDict): - id: str - most_recent_position: Optional[Position] - recent_paths: Optional[List[Path]] - - -class GetDetailsResponse(ImplicitDict): - pass - - -class GetDisplayDataResponse(ImplicitDict): - flights: List[Flight] = [] - clusters: List[Cluster] = [] +# Replaced by uas_standards package diff --git a/monitoring/uss_qualifier/resources/netrid/observers.py b/monitoring/uss_qualifier/resources/netrid/observers.py index e29d5ddf76..2448df2c79 100644 --- a/monitoring/uss_qualifier/resources/netrid/observers.py +++ b/monitoring/uss_qualifier/resources/netrid/observers.py @@ -8,7 +8,9 @@ from monitoring.monitorlib.fetch import QueryType from monitoring.monitorlib.infrastructure import UTMClientSession 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.uss_qualifier.resources.resource import Resource from monitoring.uss_qualifier.resources.communications import AuthAdapterResource @@ -85,7 +87,8 @@ def observe_flight_details( if query.status_code == 200 else None ) - except ValueError: + except ValueError as e: + logger.error("Error parsing observation details response: {}", e) result = None return result, query diff --git a/monitoring/uss_qualifier/resources/netrid/simulation/operator_flight_details.py b/monitoring/uss_qualifier/resources/netrid/simulation/operator_flight_details.py index a07a95aef8..ba56001204 100644 --- a/monitoring/uss_qualifier/resources/netrid/simulation/operator_flight_details.py +++ b/monitoring/uss_qualifier/resources/netrid/simulation/operator_flight_details.py @@ -37,6 +37,7 @@ def generate_operation_description(self): return self.random.choice(operation_description) def generate_operator_location(self, centroid): + # TODO: Inject operator location altitude operator_location = LatLngPoint(lat=centroid.y, lng=centroid.x) return operator_location diff --git a/monitoring/uss_qualifier/scenarios/astm/netrid/common_dictionary_evaluator.py b/monitoring/uss_qualifier/scenarios/astm/netrid/common_dictionary_evaluator.py index 4af3b7cd5a..d5a2eab313 100644 --- a/monitoring/uss_qualifier/scenarios/astm/netrid/common_dictionary_evaluator.py +++ b/monitoring/uss_qualifier/scenarios/astm/netrid/common_dictionary_evaluator.py @@ -1,18 +1,38 @@ import datetime -import s2sphere +import math + +from arrow import ParserError +from implicitdict import StringBasedDateTime from typing import List, Optional +import s2sphere +from uas_standards.astm.f3411.v22a.api import UASID + +from uas_standards.interuss.automated_testing.rid.v1 import ( + observation as observation_api, +) + +from uas_standards.ansi_cta_2063_a import SerialNumber +from uas_standards.astm.f3411 import v22a + +from uas_standards.astm.f3411.v22a.constants import ( + SpecialSpeed, + MaxSpeed, + SpecialTrackDirection, + MinTrackDirection, + MaxTrackDirection, +) + from monitoring.monitorlib.fetch.rid import ( FetchedFlights, FlightDetails, ) +from monitoring.monitorlib.geo import validate_lat, validate_lng, Altitude, LatLngPoint +from monitoring.monitorlib.rid import RIDVersion from monitoring.uss_qualifier.common_data_definitions import Severity from monitoring.uss_qualifier.resources.netrid.evaluation import EvaluationConfiguration from monitoring.uss_qualifier.scenarios.scenario import TestScenarioType, PendingCheck -from monitoring.monitorlib.rid import RIDVersion -from monitoring.monitorlib.geo import validate_lat, validate_lng from monitoring.monitorlib.fetch.rid import Flight, Position -from uas_standards.ansi_cta_2063_a import SerialNumber -from uas_standards.astm.f3411 import v22a + # SP responses to /flights endpoint's p99 should be below this: SP_FLIGHTS_RESPONSE_TIME_TOLERANCE_SEC = 3 @@ -53,14 +73,37 @@ def evaluate_sp_flights( requested_area, f, participants ) - if self._rid_version == RIDVersion.f3411_22a: - for f in observed_flights.flights: - # Evaluate on all flights regardless of where they came from - self.evaluate_operational_status( - f.v22a_value.get("current_state", {}).get("operational_status"), - participants, + for f in observed_flights.flights: + # Evaluate on all flights regardless of where they came from + self._evaluate_operational_status( + f.operational_status, + participants, + ) + + def evaluate_dp_flight( + self, + observed_flight: observation_api.Flight, + participants: List[str], + ): + with self._test_scenario.check("Current state present", participants) as check: + if not observed_flight.has_field_with_value("current_state"): + check.record_failed( + f"Current state for flight {observed_flight.id}", + details=f"The current state must be specified.", + severity=Severity.High, ) + self._evaluate_speed(observed_flight.current_state.speed, participants) + self._evaluate_track(observed_flight.current_state.track, participants) + self._evaluate_timestamp(observed_flight.current_state.timestamp, participants) + self._evaluate_operational_status( + observed_flight.current_state.operational_status, participants + ) + self._evaluate_position(observed_flight.most_recent_position, participants) + self._evaluate_height( + observed_flight.most_recent_position.get("height"), participants + ) + def _evaluate_recent_position_time( self, p: Position, query_time: datetime.datetime, check: PendingCheck ): @@ -151,16 +194,41 @@ def fail_check(): fail_check() def evaluate_sp_details(self, details: FlightDetails, participants: List[str]): - if self._rid_version == RIDVersion.f3411_22a: - self.evaluate_uas_id(details.v22a_value.get("uas_id"), participants) - self.evaluate_operator_id( - details.v22a_value.get("operator_id"), participants - ) - self.evaluate_operator_location( - details.v22a_value.get("operator_location"), participants - ) + self._evaluate_uas_id(details.raw.get("uas_id"), participants) + self._evaluate_operator_id(details.operator_id, participants) + self._evaluate_operator_location( + details.operator_location, + details.operator_altitude, + details.operator_altitude_type, + participants, + ) - def evaluate_uas_id(self, value: Optional[v22a.api.UASID], participants: List[str]): + def evaluate_dp_details( + self, + observed_details: Optional[observation_api.GetDetailsResponse], + participants: List[str], + ): + if not observed_details: + return + + self._evaluate_arbitrary_uas_id( + observed_details.get("uas", {}).get("id"), participants + ) + + operator = observed_details.get("operator", {}) + self._evaluate_operator_id(operator.get("id"), participants) + + operator_location = operator.get("location", {}) + operator_altitude = operator.get("altitude", {}) + operator_altitude_value = operator_altitude.get("altitude") + self._evaluate_operator_location( + operator_location, + Altitude.w84m(value=operator_altitude_value), + operator_altitude.get("altitude_type"), + participants, + ) + + def _evaluate_uas_id(self, value: Optional[UASID], participants: List[str]): if self._rid_version == RIDVersion.f3411_22a: formats_keys = [ "serial_number", @@ -178,7 +246,7 @@ def evaluate_uas_id(self, value: Optional[v22a.api.UASID], participants: List[st ) as check: if formats_count == 0: check.record_failed( - "UAS ID not present as required by the Common Dictionary definition", + f"UAS ID not present as required by the Common Dictionary definition: {value}", severity=Severity.Medium, ) return @@ -207,7 +275,66 @@ def evaluate_uas_id(self, value: Optional[v22a.api.UASID], participants: List[st message=f"Unsupported version {self._rid_version}: skipping UAS ID evaluation", ) - def evaluate_operator_id(self, value: Optional[str], participants: List[str]): + def _evaluate_arbitrary_uas_id(self, value: str, participants: List[str]): + if self._rid_version == RIDVersion.f3411_22a: + with self._test_scenario.check( + "UAS ID presence in flight details", participants + ) as check: + if not value: + check.record_failed( + f"UAS ID not present as required by the Common Dictionary definition: {value}", + severity=Severity.Medium, + ) + return + + if SerialNumber(value).valid: + self._test_scenario.check( + "UAS ID (Serial Number format) consistency with Common Dictionary", + participants, + ).record_passed(participants) + + # TODO: Add registration id format check + # TODO: Add utm id format check + # TODO: Add specific session id format check + # TODO: Add a check to validate at least one format is correct + else: + self._test_scenario.record_note( + key="skip_reason", + message=f"Unsupported version {self._rid_version}: skipping arbitrary uas id evaluation", + ) + + def _evaluate_timestamp(self, timestamp: Optional[str], participants: List[str]): + if self._rid_version == RIDVersion.f3411_22a: + with self._test_scenario.check( + "Timestamp consistency with Common Dictionary", participants + ) as check: + if timestamp is None: + check.record_failed( + f"Timestamp not present", + details=f"The timestamp must be specified.", + severity=Severity.High, + ) + + try: + t = StringBasedDateTime(timestamp) + if t.datetime.utcoffset().seconds != 0: + check.record_failed( + f"Timestamp must be relative to UTC: {t}", + severity=Severity.Medium, + ) + except ParserError as e: + check.record_failed( + f"Unable to parse timestamp: {timestamp}", + details=f"Reason: {e}", + severity=Severity.Medium, + ) + else: + self._test_scenario.record_note( + key="skip_reason", + message=f"Unsupported version {self._rid_version}: skipping timestamp evaluation", + ) + + def _evaluate_operator_id(self, value: Optional[str], participants: List[str]): if self._rid_version == RIDVersion.f3411_22a: if value: with self._test_scenario.check( @@ -222,77 +349,187 @@ def evaluate_operator_id(self, value: Optional[str], participants: List[str]): else: self._test_scenario.record_note( key="skip_reason", - message=f"Unsupported version {self._rid_version}: skipping Operator ID evaluation", + message=f"Unsupported version {self._rid_version}: skipping operator id evaluation", + ) + + def _evaluate_speed(self, speed: Optional[float], participants: List[str]): + if self._rid_version == RIDVersion.f3411_22a: + with self._test_scenario.check( + "Speed consistency with Common Dictionary", participants + ) as check: + if speed is None: + check.record_failed( + f"Speed not present", + details=f"The speed must be specified.", + severity=Severity.High, + ) + + if not (0 <= speed <= MaxSpeed or math.isclose(speed, SpecialSpeed)): + check.record_failed( + f"Invalid speed: {speed}", + details=f"The speed shall be greater than 0 and less than {MaxSpeed}. The Special Value {SpecialSpeed} is allowed.", + severity=Severity.Medium, + ) + else: + self._test_scenario.record_note( + key="skip_reason", + message=f"Unsupported version {self._rid_version}: skipping speed evaluation", + ) + + def _evaluate_track(self, track: Optional[float], participants: List[str]): + if self._rid_version == RIDVersion.f3411_22a: + with self._test_scenario.check( + "Track Direction consistency with Common Dictionary", participants + ) as check: + if track is None: + check.record_failed( + f"Track direction not present", + details=f"The track direction must be specified.", + severity=Severity.High, + ) + + if not ( + MinTrackDirection <= track <= MaxTrackDirection + or round(track) == SpecialTrackDirection + ): + check.record_failed( + f"Invalid track direction: {track}", + details=f"The track direction shall be greater than -360 and less than {MaxSpeed}. The Special Value {SpecialSpeed} is allowed.", + severity=Severity.Medium, + ) + else: + self._test_scenario.record_note( + key="skip_reason", + message=f"Unsupported version {self._rid_version}: skipping track direction evaluation", + ) + + def _evaluate_position(self, position: Position, participants: List[str]): + if self._rid_version == RIDVersion.f3411_22a: + with self._test_scenario.check( + "Current Position consistency with Common Dictionary", participants + ) as check: + lat = position.lat + try: + lat = validate_lat(lat) + except ValueError: + check.record_failed( + "Current Position contains an invalid latitude", + details=f"Invalid latitude: {lat}", + severity=Severity.Medium, + ) + lng = position.lng + try: + lng = validate_lng(lng) + except ValueError: + check.record_failed( + "Current Position contains an invalid longitude", + details=f"Invalid longitude: {lng}", + severity=Severity.Medium, + ) + else: + self._test_scenario.record_note( + key="skip_reason", + message=f"Unsupported version {self._rid_version}: skipping position evaluation", + ) + + def _evaluate_height( + self, height: Optional[observation_api.RIDHeight], participants: List[str] + ): + if self._rid_version == RIDVersion.f3411_22a: + if height: + with self._test_scenario.check( + "Height Type consistency with Common Dictionary", participants + ) as check: + if ( + height.reference + != observation_api.RIDHeightReference.TakeoffLocation + and height.reference + != observation_api.RIDHeightReference.GroundLevel + ): + check.record_failed( + f"Invalid height type: {height.reference}", + details=f"The height type reference shall be either {observation_api.RIDHeightReference.TakeoffLocation} or {observation_api.RIDHeightReference.GroundLevel}", + severity=Severity.Medium, + ) + else: + self._test_scenario.record_note( + key="skip_reason", + message=f"Unsupported version {self._rid_version}: skipping Height evaluation", ) - def evaluate_operator_location( - self, value: Optional[v22a.api.OperatorLocation], participants: List[str] + def _evaluate_operator_location( + self, + position: Optional[LatLngPoint], + altitude: Optional[Altitude], + altitude_type: Optional[observation_api.OperatorAltitudeAltitudeType], + participants: List[str], ): if self._rid_version == RIDVersion.f3411_22a: - if value: + with self._test_scenario.check( + "Operator Location consistency with Common Dictionary", participants + ) as check: + if not position: + check.record_failed( + "Missing Operator Location position", + details=f"Invalid position: {position}", + severity=Severity.Medium, + ) + return + + lat = position.lat + try: + lat = validate_lat(lat) + except ValueError: + check.record_failed( + "Operator Location contains an invalid latitude", + details=f"Invalid latitude: {lat}", + severity=Severity.Medium, + ) + lng = position.lng + try: + lng = validate_lng(lng) + except ValueError: + check.record_failed( + "Operator Location contains an invalid longitude", + details=f"Invalid longitude: {lng}", + severity=Severity.Medium, + ) + + alt = altitude + if alt: with self._test_scenario.check( - "Operator Location consistency with Common Dictionary", participants + "Operator Altitude consistency with Common Dictionary", + participants, ) as check: - lat = value.position.lat - try: - lat = validate_lat(lat) - except ValueError: + if alt.reference != v22a.api.AltitudeReference.W84: check.record_failed( - "Operator Location contains an invalid latitude", - details=f"Invalid latitude: {lat}", + "Operator Altitude shall be based on WGS-84 height above ellipsoid (HAE)", + details=f"Invalid Operator Altitude reference: {alt.reference}", severity=Severity.Medium, ) - lng = value.position.lng - try: - lng = validate_lng(lng) - except ValueError: + if alt.units != v22a.api.AltitudeUnits.M: check.record_failed( - "Operator Location contains an invalid longitude", - details=f"Invalid longitude: {lng}", + "Operator Altitude units shall be provided in meters", + details=f"Invalid Operator Altitude units: {alt.units}", severity=Severity.Medium, ) - alt = value.get("altitude") - if alt: + alt_type = altitude_type + if alt_type: with self._test_scenario.check( - "Operator Altitude consistency with Common Dictionary", + "Operator Altitude Type consistency with Common Dictionary", participants, ) as check: - if alt.reference != v22a.api.AltitudeReference.W84: - check.record_failed( - "Operator Altitude shall be based on WGS-84 height above ellipsoid (HAE)", - details=f"Invalid Operator Altitude reference: {alt.reference}", - severity=Severity.Medium, - ) - if alt.units != v22a.api.AltitudeUnits.M: + try: + v22a.api.OperatorLocationAltitudeType( + alt_type + ) # raise ValueError if alt_type is invalid + except ValueError: check.record_failed( - "Operator Altitude units shall be provided in meters", - details=f"Invalid Operator Altitude units: {alt.units}", + "Operator Location contains an altitude type which is invalid", + details=f"Invalid altitude type: {alt_type}", severity=Severity.Medium, ) - if alt.value != round(alt.value): - check.record_failed( - "Operator Altitude must have a minimum resolution of 1 m.", - details=f"Invalid Operator Altitude: {alt.value}", - severity=Severity.Medium, - ) - - alt_type = value.get("altitude_type") - if alt_type: - with self._test_scenario.check( - "Operator Altitude Type consistency with Common Dictionary", - participants, - ) as check: - try: - v22a.api.OperatorLocationAltitudeType( - alt_type - ) # raise ValueError if alt_type is invalid - except ValueError: - check.record_failed( - "Operator Location contains an altitude type which is invalid", - details=f"Invalid altitude type: {alt_type}", - severity=Severity.Medium, - ) else: self._test_scenario.record_note( @@ -300,7 +537,7 @@ def evaluate_operator_location( message=f"Unsupported version {self._rid_version}: skipping Operator Location evaluation", ) - def evaluate_operational_status( + def _evaluate_operational_status( self, value: Optional[str], participants: List[str] ): if self._rid_version == RIDVersion.f3411_22a: diff --git a/monitoring/uss_qualifier/scenarios/astm/netrid/common_dictionary_evaluator_test.py b/monitoring/uss_qualifier/scenarios/astm/netrid/common_dictionary_evaluator_test.py index ffdde3cdd0..36fd354db1 100644 --- a/monitoring/uss_qualifier/scenarios/astm/netrid/common_dictionary_evaluator_test.py +++ b/monitoring/uss_qualifier/scenarios/astm/netrid/common_dictionary_evaluator_test.py @@ -1,6 +1,15 @@ from datetime import datetime, timedelta, timezone import s2sphere -from typing import List, Tuple +from typing import List, Tuple, Optional + +from implicitdict import StringBasedDateTime +from uas_standards.interuss.automated_testing.rid.v1.observation import ( + OperatorAltitudeAltitudeType, + RIDHeight, + RIDHeightReference, +) + +from uas_standards.astm.f3411.v22a.constants import SpecialTrackDirection from monitoring.monitorlib.rid import RIDVersion from monitoring.monitorlib.fetch.rid import Flight from monitoring.uss_qualifier.scenarios.astm.netrid.common_dictionary_evaluator import ( @@ -11,7 +20,6 @@ from uas_standards.astm.f3411.v22a.api import ( Altitude, LatLngPoint, - OperatorLocation, UAType, ) from uas_standards.astm.f3411 import v22a @@ -24,7 +32,7 @@ def step_under_test(self: UnitTestScenario): test_scenario=self, rid_version=RIDVersion.f3411_22a, ) - evaluator.evaluate_operator_id(value, RIDVersion.f3411_22a) + evaluator._evaluate_operator_id(value, []) unit_test_scenario = UnitTestScenario(step_under_test).execute_unit_test() assert unit_test_scenario.get_report().successful == outcome @@ -39,7 +47,7 @@ def test_operator_id_ascii(): def _assert_operator_location( - value: OperatorLocation, expected_passed_checks, expected_failed_checks + position, altitude, altitude_type, expected_passed_checks, expected_failed_checks ): def step_under_test(self: UnitTestScenario): evaluator = RIDCommonDictionaryEvaluator( @@ -47,7 +55,7 @@ def step_under_test(self: UnitTestScenario): test_scenario=self, rid_version=RIDVersion.f3411_22a, ) - evaluator.evaluate_operator_location(value, RIDVersion.f3411_22a) + evaluator._evaluate_operator_location(position, altitude, altitude_type, []) unit_test_scenario = UnitTestScenario(step_under_test).execute_unit_test() assert ( @@ -61,108 +69,105 @@ def step_under_test(self: UnitTestScenario): def test_operator_location(): - valid_locations: List[Tuple[OperatorLocation, int]] = [ + valid_locations: List[ + Tuple[ + Optional[LatLngPoint], + Optional[Altitude], + Optional[OperatorAltitudeAltitudeType], + int, + ] + ] = [ ( - OperatorLocation( - position=LatLngPoint(lat=1.0, lng=1.0), - ), + LatLngPoint(lat=1.0, lng=1.0), + None, + None, 1, ), ( - OperatorLocation( - position=LatLngPoint(lat=-90.0, lng=180.0), - ), + LatLngPoint(lat=-90.0, lng=180.0), + None, + None, 1, ), ( - OperatorLocation( - position=LatLngPoint( - lat=46.2, - lng=6.1, - ), - altitude=Altitude(value=1), - altitude_type="Takeoff", + LatLngPoint( + lat=46.2, + lng=6.1, ), + Altitude(value=1), + OperatorAltitudeAltitudeType("Takeoff"), 3, ), ] for valid_location in valid_locations: _assert_operator_location(*valid_location, 0) - invalid_locations: List[Tuple[OperatorLocation, int, int]] = [ + invalid_locations: List[ + Tuple[ + Optional[LatLngPoint], + Optional[Altitude], + Optional[OperatorAltitudeAltitudeType], + int, + int, + ] + ] = [ ( - OperatorLocation( - position=LatLngPoint(lat=-90.001, lng=0), # out of range and valid - ), + LatLngPoint(lat=-90.001, lng=0), # out of range and valid + None, + None, 0, 1, ), ( - OperatorLocation( - position=LatLngPoint( - lat=0, # valid - lng=180.001, # out of range - ), + LatLngPoint( + lat=0, # valid + lng=180.001, # out of range ), + None, + None, 0, 1, ), ( - OperatorLocation( - position=LatLngPoint(lat=-90.001, lng=180.001), # both out of range - ), + LatLngPoint(lat=-90.001, lng=180.001), # both out of range + None, + None, 0, 2, ), ( - OperatorLocation( - position=LatLngPoint( - lat="46°12'7.99 N", # Float required - lng="6°08'44.48 E", # Float required - ), + LatLngPoint( + lat="46°12'7.99 N", # Float required + lng="6°08'44.48 E", # Float required ), + None, + None, 0, 2, ), ( - OperatorLocation( - position=LatLngPoint( - lat=46.2, - lng=6.1, - ), - altitude=Altitude(value=1), - altitude_type="invalid", # Invalid value + LatLngPoint( + lat=46.2, + lng=6.1, ), + Altitude(value=1), + "invalid", # Invalid value 2, 1, ), ( - OperatorLocation( - position=LatLngPoint( - lat=46.2, - lng=6.1, - ), - altitude=Altitude(value=1000.9), # Invalid value - altitude_type="Takeoff", + LatLngPoint( + lat=46.2, + lng=6.1, ), - 2, - 1, - ), - ( - OperatorLocation( - position=LatLngPoint( - lat=46.2, - lng=6.1, - ), - altitude=Altitude( - value=1000.9, # Invalid value - units="FT", # Invalid value - reference="UNKNOWN", # Invalid value - ), - altitude_type="Takeoff", + Altitude( + value=1000.9, + units="FT", # Invalid value + reference="UNKNOWN", # Invalid value ), + "Takeoff", + 2, 2, - 3, ), ] for invalid_location in invalid_locations: @@ -177,7 +182,7 @@ def step_under_test(self: UnitTestScenario): rid_version=RIDVersion.f3411_22a, ) - evaluator.evaluate_operational_status(value, RIDVersion.f3411_22a) + evaluator._evaluate_operational_status(value, []) unit_test_scenario = UnitTestScenario(step_under_test).execute_unit_test() assert unit_test_scenario.get_report().successful == outcome @@ -189,6 +194,94 @@ def test_operational_status(): _assert_operational_status("Invalid", False) # Invalid +def _assert_timestamp(value: str, outcome: bool): + def step_under_test(self: UnitTestScenario): + evaluator = RIDCommonDictionaryEvaluator( + config=EvaluationConfiguration(), + test_scenario=self, + rid_version=RIDVersion.f3411_22a, + ) + + evaluator._evaluate_timestamp(StringBasedDateTime(value), []) + + unit_test_scenario = UnitTestScenario(step_under_test).execute_unit_test() + assert unit_test_scenario.get_report().successful == outcome + + +def test_timestamp(): + _assert_timestamp("2023-09-13T04:43:00.1Z", True) # Ok + _assert_timestamp("2023-09-13T04:43:00Z", True) # Ok + _assert_timestamp("2023-09-13T04:43:00.501Z", True) # Ok + _assert_timestamp("2023-09-13T04:43:00.1+07:00", False) # Wrong timezone + + +def _assert_speed(value: float, outcome: bool): + def step_under_test(self: UnitTestScenario): + evaluator = RIDCommonDictionaryEvaluator( + config=EvaluationConfiguration(), + test_scenario=self, + rid_version=RIDVersion.f3411_22a, + ) + + evaluator._evaluate_speed(value, []) + + unit_test_scenario = UnitTestScenario(step_under_test).execute_unit_test() + assert unit_test_scenario.get_report().successful == outcome + + +def test_speed(): + _assert_speed(1, True) # Ok + _assert_speed(20.75, True) # Ok + _assert_speed(400, False) # Fail, above MaxSpeed + _assert_speed(23.3, True) # Ok + + +def _assert_track(value: float, outcome: bool): + def step_under_test(self: UnitTestScenario): + evaluator = RIDCommonDictionaryEvaluator( + config=EvaluationConfiguration(), + test_scenario=self, + rid_version=RIDVersion.f3411_22a, + ) + + evaluator._evaluate_track(value, []) + + unit_test_scenario = UnitTestScenario(step_under_test).execute_unit_test() + assert unit_test_scenario.get_report().successful == outcome + + +def test_track(): + _assert_track(1, True) # Ok + _assert_track(-359, True) # Ok + _assert_track(400, False) # Fail, above MaxTrackDirection + _assert_track(-360, False) # Fail, below MinTrackDirection + _assert_track(23.3, True) # Wrong resolution + _assert_track(SpecialTrackDirection, True) + + +def _assert_height(value: RIDHeight, outcome: bool): + def step_under_test(self: UnitTestScenario): + evaluator = RIDCommonDictionaryEvaluator( + config=EvaluationConfiguration(), + test_scenario=self, + rid_version=RIDVersion.f3411_22a, + ) + + evaluator._evaluate_height(value, []) + + unit_test_scenario = UnitTestScenario(step_under_test).execute_unit_test() + assert unit_test_scenario.get_report().successful == outcome + + +def test_height(): + _assert_height(None, True) # Ok + _assert_height(RIDHeight(distance=10, reference="TakeoffLocation"), True) # Ok + _assert_height(RIDHeight(distance=10.101, reference="TakeoffLocation"), True) # Ok + _assert_height( + RIDHeight(distance=10.101, reference="Moon"), False + ) # Wrong reference + + def _assert_evaluate_sp_flight_recent_positions( f: Flight, query_time: datetime, outcome: bool ): diff --git a/monitoring/uss_qualifier/scenarios/astm/netrid/display_data_evaluator.py b/monitoring/uss_qualifier/scenarios/astm/netrid/display_data_evaluator.py index 815dcb5ab4..a0556956e2 100644 --- a/monitoring/uss_qualifier/scenarios/astm/netrid/display_data_evaluator.py +++ b/monitoring/uss_qualifier/scenarios/astm/netrid/display_data_evaluator.py @@ -38,7 +38,6 @@ ) from monitoring.uss_qualifier.scenarios.scenario import ( TestScenarioType, - PendingCheck, TestScenario, ) from monitoring.uss_qualifier.scenarios.astm.netrid.injection import InjectedFlight @@ -219,6 +218,11 @@ def __init__( raise ValueError( f"Cannot evaluate a system using RID version {rid_version} with a DSS using RID version {dss.rid_version}" ) + self._retrieved_flight_details: Set[ + str + ] = ( + set() + ) # Contains the observed IDs of the flights whose details were retrieved. def evaluate_system_instantaneously( self, @@ -266,16 +270,6 @@ def evaluate_system_instantaneously( query, verified_sps, ) - # We also issue queries to the flight details endpoint in order to collect - # performance statistics, which are computed and checked at a later stage. - if query.status_code == 200: - # If there are multiple flights, we only issue a single details query for the first returned one, - # as we don't want to slow down the test we are piggy-backing on. - if len(observation.flights) > 0: - (_, detailQuery) = observer.observe_flight_details( - observation.flights[0].id, self._rid_version - ) - self._test_scenario.record_query(detailQuery) # TODO: If bounding rect is smaller than cluster threshold, expand slightly above cluster threshold and re-observe # TODO: If bounding rect is smaller than area-too-large threshold, expand slightly above area-too-large threshold and re-observe @@ -393,6 +387,10 @@ def _evaluate_normal_observation( details=f"{mapping.injected_flight.uss_participant_id}'s flight with injection ID {mapping.injected_flight.flight.injection_id} in test {mapping.injected_flight.test_id} had telemetry index {mapping.telemetry_index} at {injected_telemetry.timestamp} with lat={injected_telemetry.position.lat}, lng={injected_telemetry.position.lng}, alt={injected_telemetry.position.alt}, but {observer.participant_id} observed lat={observed_position.lat}, lng={observed_position.lng}, alt={observed_position.alt} at {query.request.initiated_at}", ) + self._common_dictionary_evaluator.evaluate_dp_flight( + mapping.observed_flight, [observer.participant_id] + ) + # Check that flights using telemetry are not using extrapolated position data for mapping in mapping_by_injection_id.values(): injected_telemetry = mapping.injected_flight.flight.telemetry[ @@ -431,6 +429,37 @@ def _evaluate_normal_observation( ), ) + # Check details of flights (once per flight) + for mapping in mapping_by_injection_id.values(): + with self._test_scenario.check( + "Successful details observation", + [mapping.injected_flight.uss_participant_id], + ) as check: + # query for flight details only once per flight + if mapping.observed_flight.id in self._retrieved_flight_details: + continue + + details, query = observer.observe_flight_details( + mapping.observed_flight.id, self._rid_version + ) + self._test_scenario.record_query(query) + + if query.status_code != 200: + check.record_failed( + summary=f"Observation of details failed for {mapping.observed_flight.id}", + details=f"When queried for details of observation (ID {mapping.observed_flight.id}), {observer.participant_id} returned code {query.status_code}", + severity=Severity.Medium, + query_timestamps=[query.request.timestamp], + ) + else: + self._retrieved_flight_details.add(mapping.observed_flight.id) + self._common_dictionary_evaluator.evaluate_dp_details( + details, + participants=[ + observer.participant_id, + ], + ) + def _evaluate_flight_presence( self, observer_participant_id: str, diff --git a/monitoring/uss_qualifier/scenarios/astm/netrid/v19/nominal_behavior.md b/monitoring/uss_qualifier/scenarios/astm/netrid/v19/nominal_behavior.md index a28ac4f879..a71c94ea9e 100644 --- a/monitoring/uss_qualifier/scenarios/astm/netrid/v19/nominal_behavior.md +++ b/monitoring/uss_qualifier/scenarios/astm/netrid/v19/nominal_behavior.md @@ -168,6 +168,14 @@ Taking into account the propagation time of the injected flights, if the total n For a display area with a diagonal greather than *NetDetailsMaxDisplayAreaDiagonal* and less than *NetMaxDisplayAreaDiagonal*, **[astm.f3411.v19.NET0480](../../../../requirements/astm/f3411/v19.md)** requires that a Display provider shall cluster UAs in close proximity to each other using a circular or polygonal area covering no less than *NetMinClusterSize* percent of the display area size. This check validates that the display area of a cluster, measured and provided in square meters by the test harness, is no less than *NetMinClusterSize* percent of the display area. +#### Successful details observation check + +Per **[interuss.automated_testing.rid.observation.ObservationSuccess](../../../../requirements/interuss/automated_testing/rid/observation.md)**, the call for flight details is expected to succeed since a valid ID was provided by uss_qualifier. + +#### Current state present check + +**[astm.f3411.v19.NET0470](../../../../requirements/astm/f3411/v19.md)** requires that Net-RID Display Provider shall provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that the current state is present. If it is not, this check will fail. + ## Cleanup The cleanup phase of this test scenario attempts to remove injected data from all SPs. diff --git a/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/nominal_behavior.md b/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/nominal_behavior.md index 25cf47626f..faec11994b 100644 --- a/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/nominal_behavior.md +++ b/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/nominal_behavior.md @@ -112,7 +112,7 @@ The timestamps of the injected telemetry usually start in the future. If a flig #### Operator Altitude consistency with Common Dictionary check -**[astm.f3411.v22a.NET0260](../../../../requirements/astm/f3411/v22a.md)** requires that relevant Remote ID data, consistent with the common data dictionary, be reported by the Service Provider. This check validates that if the Operator Altitude is based on WGS-84 height above ellipsoid (HAE), is provided in meters and must have a minimum resolution of 1 m. (**[astm.f3411.v22a.NET0260,Table1,25](../../../../requirements/astm/f3411/v22a.md)**) +**[astm.f3411.v22a.NET0260](../../../../requirements/astm/f3411/v22a.md)** requires that relevant Remote ID data, consistent with the common data dictionary, be reported by the Service Provider. This check validates that if the Operator Altitude is based on WGS-84 height above ellipsoid (HAE) and is provided in meters. (**[astm.f3411.v22a.NET0260,Table1,25](../../../../requirements/astm/f3411/v22a.md)**) #### Operator Altitude Type consistency with Common Dictionary check @@ -196,6 +196,63 @@ Taking into account the propagation time of the injected flights, if the total n For a display area with a diagonal greather than *NetDetailsMaxDisplayAreaDiagonal* and less than *NetMaxDisplayAreaDiagonal*, **[astm.f3411.v22a.NET0480](../../../../requirements/astm/f3411/v22a.md)** requires that a Display provider shall cluster UAs in close proximity to each other using a circular or polygonal area covering no less than *NetMinClusterSize* percent of the display area size. This check validates that the display area of a cluster, measured and provided in square meters by the test harness, is no less than *NetMinClusterSize* percent of the display area. +#### Successful details observation check + +Per **[interuss.automated_testing.rid.observation.ObservationSuccess](../../../../requirements/interuss/automated_testing/rid/observation.md)**, the call for flight details is expected to succeed since a valid ID was provided by uss_qualifier. + +#### Current state present check + +**[astm.f3411.v22a.NET0470](../../../../requirements/astm/f3411/v22a.md)** requires that Net-RID Display Provider shall provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that the current state is present. If it is not, this check will fail. + +#### UAS ID presence in flight details check + +**[astm.f3411.v22a.NET0470](../../../../requirements/astm/f3411/v22a.md)** requires that Net-RID Display Provider shall provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that the UAS ID is present in the information sent by the Display Provider. (**[astm.f3411.v22a.NET0470,Table1,1](../../../../requirements/astm/f3411/v22a.md)**) + +#### UAS ID (Serial Number format) consistency with Common Dictionary check + +**[astm.f3411.v22a.NET0470](../../../../requirements/astm/f3411/v22a.md)** requires that Net-RID Display Provider shall provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that if the UAS ID is in serial number format, its format is valid. (**[astm.f3411.v22a.NET0470,Table1,1a](../../../../requirements/astm/f3411/v22a.md)**) + +#### Timestamp consistency with Common Dictionary check + +**[astm.f3411.v22a.NET0470](../../../../requirements/astm/f3411/v22a.md)** requires that Net-RID Display Provider shall provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that timestamps are relative to UTC. (**[astm.f3411.v22a.NET0470,Table1,5](../../../../requirements/astm/f3411/v22a.md)**) + +#### Operational Status consistency with Common Dictionary check + +**[astm.f3411.v22a.NET0470](../../../../requirements/astm/f3411/v22a.md)** requires that Net-RID Display Provider shall provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that the Operational Status, if present, is valid. (**[astm.f3411.v22a.NET0470,Table1,7](../../../../requirements/astm/f3411/v22a.md)**) + +#### Operator ID consistency with Common Dictionary check + +**[astm.f3411.v22a.NET0470](../../../../requirements/astm/f3411/v22a.md)** requires that Net-RID Display Provider shall (NET0470) provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that the Operator ID, if present, is valid. (**[astm.f3411.v22a.NET0470,Table1,9](../../../../requirements/astm/f3411/v22a.md)**) + +#### Current Position consistency with Common Dictionary check + +**[astm.f3411.v22a.NET0470](../../../../requirements/astm/f3411/v22a.md)** requires that Net-RID Display Provider shall provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that the Current Position provided is valid. (**[astm.f3411.v22a.NET0470,Table1,10](../../../../requirements/astm/f3411/v22a.md)** and **[astm.f3411.v22a.NET0470,Table1,11](../../../../requirements/astm/f3411/v22a.md)**). If the observed Current Position do not contain valid latitude and longitude, this check will fail. +TODO: If the resolution is greater than 7 number digits, this check will fail. + +#### Height Type consistency with Common Dictionary check + +**[astm.f3411.v22a.NET0470](../../../../requirements/astm/f3411/v22a.md)** requires that Net-RID Display Provider shall provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that the Height Type (**[astm.f3411.v22a.NET0470,Table1,15](../../../../requirements/astm/f3411/v22a.md)**), if present, is valid. If the observed Height Type indicates a value different than Takeoff Location or Ground Level, this check will fail. + +#### Track Direction consistency with Common Dictionary check + +**[astm.f3411.v22a.NET0470](../../../../requirements/astm/f3411/v22a.md)** requires that Net-RID Display Provider shall provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that the Track Direction (**[astm.f3411.v22a.NET0470,Table1,19](../../../../requirements/astm/f3411/v22a.md)**) is valid. If the observed Track Direction is less than -359 or is greater than 359, except for the special value 361, this check will fail. + +#### Speed consistency with Common Dictionary check + +**[astm.f3411.v22a.NET0470](../../../../requirements/astm/f3411/v22a.md)** requires that Net-RID Display Provider shall provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that the Speed (**[astm.f3411.v22a.NET0470,Table1,20](../../../../requirements/astm/f3411/v22a.md)**) is valid. If the observed Speed is negative or greater than 254.25, except for the special value 255, this check will fail. + +#### Operator Location consistency with Common Dictionary check + +**[astm.f3411.v22a.NET0470](../../../../requirements/astm/f3411/v22a.md)** requires that Net-RID Display Provider shall provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that the Operator Latitude (**[astm.f3411.v22a.NET0470,Table1,23](../../../../requirements/astm/f3411/v22a.md)**) and Longitude (**[astm.f3411.v22a.NET0470,Table1,24](../../../../requirements/astm/f3411/v22a.md)**), if present, are valid. + +#### Operator Altitude consistency with Common Dictionary check + +**[astm.f3411.v22a.NET0470](../../../../requirements/astm/f3411/v22a.md)** requires that Net-RID Display Provider shall provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that, if present, the Operator Altitude is based on WGS-84 height above ellipsoid (HAE) and is provided in meters. (**[astm.f3411.v22a.NET0470,Table1,25](../../../../requirements/astm/f3411/v22a.md)**) + +#### Operator Altitude Type consistency with Common Dictionary check + +**[astm.f3411.v22a.NET0470](../../../../requirements/astm/f3411/v22a.md)** requires that Net-RID Display Provider shall provide access to required and optional fields to Remote ID Display Applications according to the Common Dictionary. This check validates that the Operator Altitude Type is valid, if present. (**[astm.f3411.v22a.NET0470,Table1,26](../../../../requirements/astm/f3411/v22a.md)**) + ## Cleanup The cleanup phase of this test scenario attempts to remove injected data from all SPs. diff --git a/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md b/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md index a7644ef814..4958f65391 100644 --- a/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md +++ b/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md @@ -23,7 +23,7 @@ Checked in - astm
.f3411
.v22a
+ astm
.f3411
.v22a
A2-6-1,1a Implemented ASTM F3411-22a NetRID DSS interoperability @@ -285,6 +285,76 @@ NET0470 + Implemented + TODO + ASTM NetRID nominal behavior + + + NET0470,Table1,1 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,10 + TODO + ASTM NetRID nominal behavior + + + NET0470,Table1,11 + TODO + ASTM NetRID nominal behavior + + + NET0470,Table1,15 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,19 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,1a + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,20 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,23 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,24 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,25 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,26 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,5 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,7 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,9 Implemented ASTM NetRID nominal behavior diff --git a/monitoring/uss_qualifier/suites/uspace/network_identification.md b/monitoring/uss_qualifier/suites/uspace/network_identification.md index cab3803e7d..3f5d846080 100644 --- a/monitoring/uss_qualifier/suites/uspace/network_identification.md +++ b/monitoring/uss_qualifier/suites/uspace/network_identification.md @@ -16,7 +16,7 @@ Checked in - astm
.f3411
.v22a
+ astm
.f3411
.v22a
A2-6-1,1a Implemented ASTM F3411-22a NetRID DSS interoperability @@ -278,6 +278,76 @@ NET0470 + Implemented + TODO + ASTM NetRID nominal behavior + + + NET0470,Table1,1 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,10 + TODO + ASTM NetRID nominal behavior + + + NET0470,Table1,11 + TODO + ASTM NetRID nominal behavior + + + NET0470,Table1,15 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,19 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,1a + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,20 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,23 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,24 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,25 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,26 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,5 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,7 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,9 Implemented ASTM NetRID nominal behavior diff --git a/monitoring/uss_qualifier/suites/uspace/required_services.md b/monitoring/uss_qualifier/suites/uspace/required_services.md index f75201301e..765f47ff7f 100644 --- a/monitoring/uss_qualifier/suites/uspace/required_services.md +++ b/monitoring/uss_qualifier/suites/uspace/required_services.md @@ -17,7 +17,7 @@ Checked in - astm
.f3411
.v22a
+ astm
.f3411
.v22a
A2-6-1,1a Implemented ASTM F3411-22a NetRID DSS interoperability @@ -279,6 +279,76 @@ NET0470 + Implemented + TODO + ASTM NetRID nominal behavior + + + NET0470,Table1,1 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,10 + TODO + ASTM NetRID nominal behavior + + + NET0470,Table1,11 + TODO + ASTM NetRID nominal behavior + + + NET0470,Table1,15 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,19 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,1a + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,20 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,23 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,24 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,25 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,26 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,5 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,7 + Implemented + ASTM NetRID nominal behavior + + + NET0470,Table1,9 Implemented ASTM NetRID nominal behavior diff --git a/requirements.txt b/requirements.txt index a4c773ff81..cee4d53f0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,5 +39,5 @@ s2sphere==0.2.5 shapely==1.7.1 structlog==21.5.0 # deployment_manager termcolor==1.1.0 -uas_standards==2.0.0 +uas_standards==2.1.0 Werkzeug==2.0.3 # See https://github.com/interuss/dss/issues/753