diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index 2b1f837229..d8d8937c98 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -2023,22 +2023,6 @@ "lineCount": 1 } }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 12, - "endColumn": 44, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 12, - "endColumn": 44, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { diff --git a/monitoring/monitorlib/clients/flight_planning/flight_info.py b/monitoring/monitorlib/clients/flight_planning/flight_info.py index 5b3ce24a5c..1dbce0839a 100644 --- a/monitoring/monitorlib/clients/flight_planning/flight_info.py +++ b/monitoring/monitorlib/clients/flight_planning/flight_info.py @@ -9,6 +9,8 @@ from uas_standards.interuss.automated_testing.flight_planning.v1 import api as fp_api from uas_standards.interuss.automated_testing.scd.v1 import api as scd_api +from monitoring.monitorlib.clients.flight_planning.telemetry import FlightTelemetry +from monitoring.monitorlib.geo import Altitude, LatLngPoint from monitoring.monitorlib.geotemporal import Volume4D, Volume4DCollection # ===== ASTM F3548-21 ===== @@ -250,11 +252,107 @@ def f3548v21_op_intent_state(self) -> f3548v21.OperationalIntentState: return state +class UAType(str, Enum): + """The UA Type can help infer performance, speed, and duration of flights, for example, a + "fixed wing" can generally fly in a forward direction only (as compared to a multi-rotor). + + `HybridLift` is a fixed wing aircraft that can take off vertically. `Helicopter` includes multirotor. + + `VTOL` is equivalent to HybridLift. + """ + + NotDeclared = "NotDeclared" + Aeroplane = "Aeroplane" + Helicopter = "Helicopter" + Gyroplane = "Gyroplane" + VTOL = "VTOL" + HybridLift = "HybridLift" + Ornithopter = "Ornithopter" + Glider = "Glider" + Kite = "Kite" + FreeBalloon = "FreeBalloon" + CaptiveBalloon = "CaptiveBalloon" + Airship = "Airship" + FreeFallOrParachute = "FreeFallOrParachute" + Rocket = "Rocket" + TetheredPoweredAircraft = "TetheredPoweredAircraft" + GroundObstacle = "GroundObstacle" + Other = "Other" + + +class UASRegistrationNumber(ImplicitDict): + """Number provided by CAA or authorized representative for registering and/or identifying UAS.""" + + authority: str | None = "" + """Authority providing this registration number. If authority represents a country, the ICAO nationality + mark is recommended. + """ + + identifier: str + """Authority-assigned number or identifier.""" + + +class UAClassificationEUCategory(str, Enum): + EUCategoryUndefined = "EUCategoryUndefined" + Open = "Open" + Specific = "Specific" + Certified = "Certified" + + +class UAClassificationEUClass(str, Enum): + EUClassUndefined = "EUClassUndefined" + Class0 = "Class0" + Class1 = "Class1" + Class2 = "Class2" + Class3 = "Class3" + Class4 = "Class4" + Class5 = "Class5" + Class6 = "Class6" + + +UAClassificationEU = dict[UAClassificationEUCategory, UAClassificationEUClass] + + +class UASInformation(ImplicitDict): + """Information about a UAS that may be provided in flight planning scenarios.""" + + aircraft_type: UAType | None + """Aircraft type of the injected test flight.""" + + serial_number: str | None = "" + """This is generally expressed in the CTA-2063-A Serial Number format.""" + + registration_numbers: list[UASRegistrationNumber] | None = [] + """For each relevant authority with which this UAS is registered, the number/identifier assigned to this UAS.""" + + eu_classification: UAClassificationEU | None + """EU classification of aircraft.""" + + +class OperatorInformation(ImplicitDict): + """Information about the operator that may be provided in flight planning scenarios.""" + + registration_numbers: list[OperatorRegistrationNumber] | None + """Registration numbers for the remote pilot or operator.""" + + location: LatLngPoint | None + """Location of operator.""" + + altitude: Altitude | None + """Altitude of operator.""" + + class FlightInfo(ImplicitDict): """Details of user's intent to create or modify a flight plan.""" basic_information: BasicFlightPlanInformation + uas: UASInformation | None + + operator: OperatorInformation | None + + telemetry: FlightTelemetry | None + astm_f3548_21: Optional[ASTMF354821OpIntentInformation] uspace_flight_authorisation: Optional[FlightAuthorisationData] @@ -266,44 +364,64 @@ class FlightInfo(ImplicitDict): @staticmethod def from_flight_plan(plan: fp_api.FlightPlan) -> FlightInfo: - kwargs = { - "basic_information": BasicFlightPlanInformation.from_flight_planning_api( + result = FlightInfo( + basic_information=BasicFlightPlanInformation.from_flight_planning_api( plan.basic_information ) - } + ) + if "uas" in plan and plan.uas: + result.uas = ImplicitDict.parse(plan.uas, UASInformation) + if "operator" in plan and plan.operator: + result.operator = ImplicitDict.parse(plan.operator, OperatorInformation) + if "telemetry" in plan and plan.telemetry: + result.telemetry = ImplicitDict.parse(plan.telemetry, FlightTelemetry) if "astm_f3548_21" in plan and plan.astm_f3548_21: - kwargs["astm_f3548_21"] = ImplicitDict.parse( + result.astm_f3548_21 = ImplicitDict.parse( plan.astm_f3548_21, ASTMF354821OpIntentInformation ) if "uspace_flight_authorisation" in plan and plan.uspace_flight_authorisation: - kwargs["uspace_flight_authorisation"] = ImplicitDict.parse( + result.uspace_flight_authorisation = ImplicitDict.parse( plan.uspace_flight_authorisation, FlightAuthorisationData ) if "rpas_operating_rules_2_6" in plan and plan.rpas_operating_rules_2_6: - kwargs["rpas_operating_rules_2_6"] = ImplicitDict.parse( + result.rpas_operating_rules_2_6 = ImplicitDict.parse( plan.rpas_operating_rules_2_6, RPAS26FlightDetails ) if "additional_information" in plan and plan.additional_information: - kwargs["additional_information"] = plan.additional_information - return FlightInfo(**kwargs) + result.additional_information = plan.additional_information + return result def to_flight_plan(self) -> fp_api.FlightPlan: - kwargs = {"basic_information": self.basic_information.to_flight_planning_api()} + result = fp_api.FlightPlan( + basic_information=self.basic_information.to_flight_planning_api() + ) + if "uas" in self and self.uas: + result.uas = ImplicitDict.parse(self.uas, fp_api.UASInformation) + if "operator" in self and self.operator: + result.operator = ImplicitDict.parse( + self.operator, fp_api.OperatorInformation + ) + if "telemetry" in self and self.telemetry: + result.telemetry = ImplicitDict.parse( + self.telemetry, fp_api.FlightTelemetry + ) if "astm_f3548_21" in self and self.astm_f3548_21: - kwargs["astm_f3548_21"] = ImplicitDict.parse( + result.astm_f3548_21 = ImplicitDict.parse( self.astm_f3548_21, fp_api.ASTMF354821OpIntentInformation ) if "uspace_flight_authorisation" in self and self.uspace_flight_authorisation: - kwargs["uspace_flight_authorisation"] = ImplicitDict.parse( + result.uspace_flight_authorisation = ImplicitDict.parse( self.uspace_flight_authorisation, fp_api.FlightAuthorisationData ) if "rpas_operating_rules_2_6" in self and self.rpas_operating_rules_2_6: - kwargs["rpas_operating_rules_2_6"] = ImplicitDict.parse( + result.rpas_operating_rules_2_6 = ImplicitDict.parse( self.rpas_operating_rules_2_6, fp_api.RPAS26FlightDetails ) if "additional_information" in self and self.additional_information: - kwargs["additional_information"] = self.additional_information - return fp_api.FlightPlan(**kwargs) + result.additional_information = fp_api.FlightPlanAdditionalInformation() + for k, v in self.additional_information.items(): + result.additional_information[k] = v + return result @staticmethod def from_scd_inject_flight_request( diff --git a/monitoring/monitorlib/clients/flight_planning/telemetry.py b/monitoring/monitorlib/clients/flight_planning/telemetry.py new file mode 100644 index 0000000000..a0e995d968 --- /dev/null +++ b/monitoring/monitorlib/clients/flight_planning/telemetry.py @@ -0,0 +1,148 @@ +from enum import Enum + +from implicitdict import ImplicitDict + +from monitoring.monitorlib.geo import Altitude, LatLngPoint +from monitoring.monitorlib.temporal import Time + + +class HorizontalAccuracy(str, Enum): + """This is the NACp enumeration from ADS-B, plus 1m for a more complete range for UAs.""" + + HAUnknown = "HAUnknown" + HA10NMPlus = "HA10NMPlus" + HA10NM = "HA10NM" + HA4NM = "HA4NM" + HA2NM = "HA2NM" + HA1NM = "HA1NM" + HA05NM = "HA05NM" + HA03NM = "HA03NM" + HA01NM = "HA01NM" + HA005NM = "HA005NM" + HA30m = "HA30m" + HA10m = "HA10m" + HA3m = "HA3m" + HA1m = "HA1m" + + +class VerticalAccuracy(str, Enum): + """This is the GVA enumeration from ADS-B, plus some finer values for UAs.""" + + VAUnknown = "VAUnknown" + VA150mPlus = "VA150mPlus" + VA150m = "VA150m" + VA45m = "VA45m" + VA25m = "VA25m" + VA10m = "VA10m" + VA3m = "VA3m" + VA1m = "VA1m" + + +class SpeedAccuracy(str, Enum): + """This is the same enumeration scale and values from ADS-B NACv.""" + + SAUnknown = "SAUnknown" + SA10mpsPlus = "SA10mpsPlus" + SA10mps = "SA10mps" + SA3mps = "SA3mps" + SA1mps = "SA1mps" + SA03mps = "SA03mps" + + +class OperationalStatus(str, Enum): + """Indicates operational status of associated aircraft. + * `Undeclared`: The system does not support acquisition of knowledge about the status of the aircraft. + * `Ground`: The aircraft is reporting status but is not airborne. + * `Airborne`: The aircraft is, or should be considered as, being airborne. + * `Emergency`: The aircraft is reporting an emergency. + * `SystemFailure`: Some aspect of the reporting/telemetry system has failed, but the aircraft is not in emergency. + * `Unknown`: The system supports acquisition of knowledge about the status of the aircraft, but the status cannot currently be determined. + """ + + Undeclared = "Undeclared" + Ground = "Ground" + Airborne = "Airborne" + Emergency = "Emergency" + SystemFailure = "SystemFailure" + Unknown = "Unknown" + + +class FunctionalState(str, Enum): + """Functional state of the user's UAS associated with this flight plan. + + - `Nominal`: The user or UAS reports or implies that it is performing nominally, or has not indicated + `OffNominal` or `Contingent`. + + - `OffNominal`: The user or UAS reports or implies that it is temporarily not performing nominally, but + may expect to be able to recover to normal operation. + + - `Contingent`: The user or UAS reports or implies that it is not performing nominally and may be unable + to recover to normal operation. + + - `NotSpecified`: The UAS status is not currently available or known (for instance, if the flight is + planned in the future and the UAS that will be flying has not yet connected to the system). + """ + + Nominal = "Nominal" + OffNominal = "OffNominal" + Contingent = "Contingent" + NotSpecified = "NotSpecified" + + +class AircraftPosition(ImplicitDict): + """Reported or actual position of an aircraft at a particular time.""" + + location: LatLngPoint | None + + altitudes: list[Altitude] | None + """The single vertical location of the aircraft, potentially reported relative to multiple datums.""" + + accuracy_h: HorizontalAccuracy | None + """Horizontal error that may be be present in this reported position.""" + + accuracy_v: VerticalAccuracy | None + """Vertical error that may be present in this reported position.""" + + pressure_altitude: float | None = 0.0 + """The uncorrected altitude (based on reference standard 29.92 inHg, 1013.25 mb) provides a reference for algorithms that utilize "altitude deltas" between aircraft. This value is provided in meters.""" + + +class AircraftState(ImplicitDict): + """State of an aircraft for the purposes of simulating the execution of a flight plan.""" + + id: str + """Unique identifier for this aircraft state/telemetry. The content of an AircraftState with a given ID must not change over time. Therefore, if a USS has already queued an aircraft state with this ID as telemetry to be delivered, this state may be ignored.""" + + timestamp: Time + """Time at which this state is valid. This is equivalent to the time of sensor measurement, so the USS's primary system under test should not be aware of this state until after this time.""" + + timestamp_accuracy: float | None = 0.0 + """Declaration of timestamp accuracy, which is the one-sided width of the 95%-confidence interval around `timestamp` for the true time of applicability for any of the data fields.""" + + position: AircraftPosition | None + + operational_status: OperationalStatus | None + + uas_state: FunctionalState | None + """State of the user's UAS associated with this flight plan as reported by the operator. If the UAS reports its own state, that report will be reflected here. If the operator reports the UAS state, that report will be reflected in the BasicFlightPlanInformation. + If a system accepts UAS state reports from both the operator and UAS, it is possible the reported state may differ between those two sources. + """ + + track: float | None = 0.0 + """Direction of flight expressed as a "True North-based" ground track angle. This value is provided in degrees East of North. A value of 360 indicates that the true value is unavailable.""" + + speed: float | None = 0.0 + """Ground speed of flight in meters per second.""" + + speed_accuracy: SpeedAccuracy | None + """Accuracy of horizontal ground speed.""" + + vertical_speed: float | None = 0.0 + """Geodetic vertical speed upward. Units of meters per second.""" + + +class FlightTelemetry(ImplicitDict): + """When this information is present and the USS supports telemetry ingestion for a flight, the USS receiving this information should enqueue these telemetry reports to be delivered to the system, as if they were coming from the user, at (or soon after) the specified times.""" + + states: list[AircraftState] | None = [] + """The set of telemetry data that should be injected into the system (as if reported by the user or the user's system) at the appropriate times (and not before) for this flight."""