From e113878bbc215405ec9d919962c1d107dd93f5ce Mon Sep 17 00:00:00 2001 From: Trishna Saeharaseelan <trishna.saeharaseelan@noc.ac.uk> Date: Tue, 3 Jan 2023 22:10:15 +0800 Subject: [PATCH] refactor: all messages from flask model to json schema definitions --- README.md | 2 +- __init__.py | 1 - docs/__init__.py | 8 - docs/generate_swagger.py | 183 ----------- examples/all/observation.json | 2 +- formats/__init__.py | 190 ++++++------ formats/acknowledgement.py | 59 ++-- formats/message_wrapper.py | 58 ++++ formats/mission_plan.py | 212 ++++++++----- formats/observation.py | 106 ++++--- formats/planning_configuration.py | 308 ++++++++++++------ formats/platform_status.py | 497 ++++++++++++++++++++---------- generate_swagger.py | 134 ++++++++ 13 files changed, 1086 insertions(+), 674 deletions(-) delete mode 100644 docs/__init__.py delete mode 100644 docs/generate_swagger.py create mode 100644 formats/message_wrapper.py create mode 100644 generate_swagger.py diff --git a/README.md b/README.md index 04df03d..56bbe31 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This project repository is a collaborative workspace. It consists of all message # Message Types -Each message below will be treated as the `payload` that are wrapped in a `full_message_format` that includes a `message_header`: +Each message below will be wrapped in a `message_wrapper`: * `mission_plan`: these would be two message types, i. encoded (platform-specific serialized message) and ii. parsed, human-readable message. * `platform_status`: these would be two message types, i. encoded (platform-specific serialized message) and ii. parsed, human-readable message. * `observation`: this would be desired scientific data sent by the platform diff --git a/__init__.py b/__init__.py index 7a18f10..1aa4a33 100644 --- a/__init__.py +++ b/__init__.py @@ -11,4 +11,3 @@ __all__ = [ ] app = Flask(__name__) api = Api(app) -# api = Marshmallow(app) diff --git a/docs/__init__.py b/docs/__init__.py deleted file mode 100644 index 2253a07..0000000 --- a/docs/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -import os - - -__all__ = [ - os.path.splitext(os.path.basename(x))[0] - for x in os.listdir(os.path.dirname(__file__)) - if x.endswith(".py") and x != "__init__.py" -] diff --git a/docs/generate_swagger.py b/docs/generate_swagger.py deleted file mode 100644 index f5ae472..0000000 --- a/docs/generate_swagger.py +++ /dev/null @@ -1,183 +0,0 @@ -from flask import Flask -from flasgger import Swagger - -app = Flask(__name__) - -swagger_config = { - "headers": [], - "openapi": "3.0.2", - "swagger_ui": True, - "specs_route": "/soardocs/", - "info": { - "title": "Backbone Message Formats", - "version": "0.1", - "description": "SoAR message schemas (i.e. formats)", - }, - "specs": [ - { - "endpoint": "swagger", - "route": "/swagger.json", - "rule_filter": lambda rule: True, - "model_filter": lambda tag: True, - } - ], - "components": { - "schemas": { - "Message": { - "type": "object", - "required": ["message_type"], - "properties": { - "message_type": { - "type": "string", - }, - "source": { - "type": "string", - "description": "The sender.", - "example": "autonomy-engine", - }, - "destination": { - "type": "string", - "description": "Publisher topic.", - "example": "soar.noc.autosub.ah1.status", - }, - "delivery_type": { - "type": "string", - "description": "Published or broadcast", - "enum": ["broadcast", "publish"], - "example": "2.0.0", - }, - "message_ID": { - "type": "string", - "description": "An identifier for the type of " - + "message received.", - "example": "PlatformStatus", - }, - }, - "discriminator": { - "propertyName": "message_type", - }, - }, - "Coordinate": { - "allOf": [{"$ref": "#/components/schemas/Message"}], - "type": "object", - "properties": { - "latitude": { - "type": "integer", - "description": "Latitude in decimal degrees.", - "example": 54.234, - }, - "longitude": { - "type": "integer", - "description": "Longitude in decimal degrees.", - "example": -1.432, - }, - "depth": { - "type": "integer", - "description": "Target depth", - "default": 0, - "example": 50, - }, - "projection": { - "type": "integer", - "description": "EPSG Projection Code", - "example": 4326, - "default": 4326, - }, - }, - }, - "PlatformStatus": { - "allOf": [ - {"$ref": "#/components/schemas/Message"}, - ], - "type": "object", - "properties": { - "partner_ID": { - "type": "string", - "description": "An identifier for the partner " - + "owning/operating the platform.", - "example": "noc", - }, - "platform_ID": { - "type": "string", - "description": "An identifier for the platform.", - "example": "noc_ah1", - }, - "state": { - "type": "string", - "description": "Status of platform.", - "example": "idle", - }, - }, - }, - }, - }, - "paths": { - "/messages": { - "get": { - "description": "Returns all messages from the system.", - "responses": { - "200": { - "description": "A list of messages.", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/" - + "schemas/Coordinate" - }, - { - "$ref": "#/components/" - + "schemas/PlatformStatus" - }, - ], - "discriminator": { - "propertyName": "message_type", - }, - } - } - }, - } - }, - } - }, - "/platformstatus": { - "get": { - "description": "Returns platform status message", - "responses": { - "200": { - "description": "Platform status message.", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas" - + "/PlatformStatus" - }, - ], - "discriminator": { - "propertyName": "message_type", - }, - } - } - }, - } - }, - } - }, - }, - "produces": ["application/json"], - "consumes": ["application/json"], -} - -swag = Swagger(app, config=swagger_config, merge=True) - -# app.add_url_rule( -# '/coordinates', -# view_func=Coordinates.as_view('coordinates'), -# methods=['GET'] -# ) - -if __name__ == "__main__": - app.run(debug=True) diff --git a/examples/all/observation.json b/examples/all/observation.json index a4a8574..65049a9 100644 --- a/examples/all/observation.json +++ b/examples/all/observation.json @@ -5,7 +5,7 @@ "source": "ecosub_c2", "destination": "autonomy_engine", "encoded": false, - "type": "platform_status", + "type": "observation", "payload": { "platform_serial":"ecosub-3", "points_of_interest": [], diff --git a/formats/__init__.py b/formats/__init__.py index fda3a30..446108f 100644 --- a/formats/__init__.py +++ b/formats/__init__.py @@ -1,5 +1,5 @@ -from flask_restx import fields -from . import api +# from flask_restx import fields +# from . import api import os @@ -11,99 +11,99 @@ __all__ = [ # TODO: Define units for all schemas -message_types = [ - "platform_status", - "mission_plan_ecosub", - "mission_plan_reav", - "mission_plan_autosub", -] # TODO: Add full range of message types once scoped out +# message_types = [ +# "platform_status", +# "mission_plan_ecosub", +# "mission_plan_reav", +# "mission_plan_autosub", +# ] # TODO: Add full range of message types once scoped out -full_message_schema = api.model( - "FullMessageSchema", - { - "message_ID": fields.String( - required=True, - description="UUID assigned to this message", - example="b427003c-7bc8-11ed-a1eb-0242ac120002", - ), - "timestamp": fields.DateTime( - required=True, - description="Timestamp of message", - example="2022-11-16T00:00:00Z", - ), - "version": fields.Float( - required=True, - description="Version of comms bacbone messaging format protocol", - example=2.0, - ), - "source": fields.String( - required=True, - description="Where is this message from", - example="autonomy_engine", - ), - "destination": fields.String( - required=True, - description="What is the destination of this message", - example="ah-1", - ), - "encoded": fields.Boolean( - required=True, - description="Indicate that message raw (encoded) or decoded. " - + "Options: encoded=True, decoded=False", - example=False, - ), - "type": fields.String( - required=True, - description="Type of message", - example="platform_status", - ), - "payload": fields.Raw( - required=True, - description="Content of Message", - # example="{}", - ), - }, -) +# full_message_schema = api.model( +# "FullMessageSchema", +# { +# "message_ID": fields.String( +# required=True, +# description="UUID assigned to this message", +# example="b427003c-7bc8-11ed-a1eb-0242ac120002", +# ), +# "timestamp": fields.DateTime( +# required=True, +# description="Timestamp of message", +# example="2022-11-16T00:00:00Z", +# ), +# "version": fields.Float( +# required=True, +# description="Version of comms bacbone messaging format protocol", +# example=2.0, +# ), +# "source": fields.String( +# required=True, +# description="Where is this message from", +# example="autonomy_engine", +# ), +# "destination": fields.String( +# required=True, +# description="What is the destination of this message", +# example="ah-1", +# ), +# "encoded": fields.Boolean( +# required=True, +# description="Indicate that message raw (encoded) or decoded. " +# + "Options: encoded=True, decoded=False", +# example=False, +# ), +# "type": fields.String( +# required=True, +# description="Type of message", +# example="platform_status", +# ), +# "payload": fields.Raw( +# required=True, +# description="Content of Message", +# # example="{}", +# ), +# }, +# ) -constraints_schema = api.model( - "ConstraintsSchema", - { - "min_altitude": fields.Float( - required=True, - description="Minimum altitude set for squad.", - example=15.2, - ), - "min_velocity": fields.Float( - required=True, - description="Minimum velocity set for squad.", - example=0.1, - ), - "max_velocity": fields.Float( - required=True, - description="Maximum altitude set for squad.", - example=0.9, - ), - }, -) +# constraints_schema = api.model( +# "ConstraintsSchema", +# { +# "min_altitude": fields.Float( +# required=True, +# description="Minimum altitude set for squad.", +# example=15.2, +# ), +# "min_velocity": fields.Float( +# required=True, +# description="Minimum velocity set for squad.", +# example=0.1, +# ), +# "max_velocity": fields.Float( +# required=True, +# description="Maximum altitude set for squad.", +# example=0.9, +# ), +# }, +# ) -platform_schema = api.model( - "PlatformSchema", - { - "platform_ID": fields.Integer( - required=True, - description="unique identifier for platform", - example=1, - ), - "serial": fields.String( - required=True, - description="platform serial number", - example="reav-60", - ), - "model": fields.String( - required=True, - description="platform serial number", - example="reav", - ), - "constraints": fields.Nested(constraints_schema), - }, -) +# platform_schema = api.model( +# "PlatformSchema", +# { +# "platform_ID": fields.Integer( +# required=True, +# description="unique identifier for platform", +# example=1, +# ), +# "serial": fields.String( +# required=True, +# description="platform serial number", +# example="reav-60", +# ), +# "model": fields.String( +# required=True, +# description="platform serial number", +# example="reav", +# ), +# "constraints": fields.Nested(constraints_schema), +# }, +# ) diff --git a/formats/acknowledgement.py b/formats/acknowledgement.py index af0cff0..6c6d965 100644 --- a/formats/acknowledgement.py +++ b/formats/acknowledgement.py @@ -2,29 +2,44 @@ schemas: Acknowledgement status sent by the surface platform to report receipt of message. """ -from . import api, full_message_schema -from flask_restx import fields - - -acknowledgement_schema = api.model( - "Acknowledgement", - { - "message": fields.Nested( - full_message_schema, - required=True, - description="Message header", - ), - "message_ID": fields.Integer( - required=True, - description="Identifier of message received and executed with " +acknowledgement_schema = { + "properties": { + "message_ID": { + "type": "string", + "description": "Identifier of message received and executed with " + "success for mission plans sent by the Autonomy Engine.", - example=202, - ), - "status": fields.String( - required=True, - description="Highest level of acknowledgement. I.e. `c2_received`:" + "example": "b427003c-7bc8-11ed-a1eb-0242ac999999", + }, + "status": { + "type": "string", + "description": "Highest level of acknowledgement. I.e. `c2_received`:" + " Received by C2, `c2_sent`: Sent from C2->Platform, `executed`:" + " Executed by platform", - ), + "example": "executed by platform", + }, }, -) + "required": ["message_ID", "status"], +} + +# acknowledgement_schema = api.model( +# "Acknowledgement", +# { +# "message": fields.Nested( +# full_message_schema, +# required=True, +# description="Message header", +# ), +# "message_ID": fields.Integer( +# required=True, +# description="Identifier of message received and executed with " +# + "success for mission plans sent by the Autonomy Engine.", +# example=202, +# ), +# "status": fields.String( +# required=True, +# description="Highest level of acknowledgement. I.e. `c2_received`:" +# + " Received by C2, `c2_sent`: Sent from C2->Platform, `executed`:" +# + " Executed by platform", +# ), +# }, +# ) diff --git a/formats/message_wrapper.py b/formats/message_wrapper.py new file mode 100644 index 0000000..b9bebfa --- /dev/null +++ b/formats/message_wrapper.py @@ -0,0 +1,58 @@ +""" + schemas: Message Wrapper is used to wrap all message types that contain + details of where the message is coming from, which end client is its destination + and the type of message. +""" + +message_wrapper_schema = { + "type": "object", + "discriminator": { + "propertyName": "message_type", + }, + "properties": { + "message_ID": { + "type": "string", + "description": "An identifier for the type of " + "message received.", + "example": "PlatformStatus", + }, + "timestamp": { + "type": "date-time", + "description": "Timestamp of message", + "example": "2022-11-16T00:00:00Z", + }, + "message_type": { + "type": "Type of message", + "description": "Type of message", + "example": "platform_status", + }, + "version": { + "type": "string", + "description": "Version of comms bacbone messaging format protocol", + "example": 2.0, + }, + "source": { + "type": "string", + "description": "The sender; Where is this message from", + "example": "autonomy_engine", + }, + "destination": { + "type": "string", + "description": "Publisher topic; What is the destination of this message", + "example": "ah1", + }, + "encoded": { + "type": "boolean", + "description": "Indicate that message raw (encoded) or decoded. " + + "Options: encoded=True, decoded=False", + "example": False, + }, + "delivery_type": { + "type": "string", + "description": "To publish or broadcast this message.", + "enum": ["broadcast", "publish"], + "example": "publish", + "default": "publish", + }, + }, + "required": ["message_type"], +} diff --git a/formats/mission_plan.py b/formats/mission_plan.py index c6af384..46243ca 100644 --- a/formats/mission_plan.py +++ b/formats/mission_plan.py @@ -3,83 +3,151 @@ sent to the respective platform's C2 to compile into a platform-specific mission plan. """ -from . import api, full_message_schema -from flask_restx import fields +# from . import api, full_message_schema +# from flask_restx import fields - -action_schema = api.model( - "AutonomyEngineAction", - { - "action": fields.String( - required=True, - description="Autonomy Engine's action from `move`, `payload`," +action_schema = { + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Autonomy Engine's action from `move`, `payload`," + " `dive`, `send_hits`, `scanline`, `scanpoint`.", - example="move", - ), - "flight_style": fields.String( - required=False, - description="Platform-specific modes/flight styles to perform" + "example": "move", + }, + "flight_style": { + "type": "string", + "description": "Platform-specific modes/flight styles to perform" + " next action", - example="orbit", - ), - "latitude_waypoint": fields.Float( - required=True, - description="Next waypoint, x-coordinate", - example=-4.187143188645706, - ), - "longitude_waypoint": fields.Float( - required=True, - description="Next waypoint, y-coordinate", - example=50.37072283932642, - ), - "altitude": fields.Float( - required=False, - description="Altitude of next action", - example=15.0, - ), - "depth": fields.Float( - required=False, - description="Depth of next action", - example=15.0, - ), - "activate_payload": fields.Boolean( - required=False, - description="To activate/deactivate sensor for Autosub " + "example": "orbit", + }, + "latitude_waypoint": { + "type": "number", + "description": "Next waypoint, x-coordinate", + "example": -4.187143188645706, + }, + "longitude_waypoint": { + "type": "number", + "description": "Next waypoint, y-coordinate", + "example": 50.37072283932642, + }, + "altitude": { + "type": "number", + "description": "Altitude of next action", + "example": 15.0, + }, + "depth": { + "type": "number", + "description": "Depth of next action", + "example": 15.0, + }, + "activate_payload": { + "type": "boolean", + "description": "To activate/deactivate sensor for Autosub " + "Hover-1 --> `MBES` sensor and for EcoSUB --> `Sidescan`", - example=True, - ), - "send_environmental_data": fields.Boolean( - required=False, - description="To trigger the platform to send list of observations" + "example": True, + }, + "send_environmental_data": { + "type": "boolean", + "description": "To trigger the platform to send list of observations" + " if any found", - example=False, - ), + "example": False, + }, }, -) + "required": [ + "action", + "latitude_waypoint", + "longitude_waypoint", + ], +} -mission_plan_schema = api.model( - "MissionPlan", - { - "message": fields.Nested( - full_message_schema, - required=True, - description="Message header", - ), - "plan_ID": fields.Integer( - required=True, - description="Identifier of given mission sequence planned", - example=1, - ), - "platform_serial": fields.String( - required=True, - description="Serial of target platform to send mission to", - example="reav-60", - ), - "plan": fields.List( - fields.Nested(action_schema), - required=True, - description="Sequence of actions/instructions generated by the " - + " Autonomy Engine that should be compiled by the respective C2.", - ), +mission_plan_schema = { + "allOf": [{"$ref": "#/components/schemas/Message"}], + "type": "object", + "properties": { + "plan_ID": {"type": "integer"}, + "platform_serial": {"type": "string"}, + "plan": { + "type": "array", + "items": action_schema, + }, }, -) + "required": ["plan_ID", "platform_serial", "plan"], +} + +# action_schema = api.model( +# "AutonomyEngineAction", +# { +# "action": fields.String( +# required=True, +# description="Autonomy Engine's action from `move`, `payload`," +# + " `dive`, `send_hits`, `scanline`, `scanpoint`.", +# example="move", +# ), +# "flight_style": fields.String( +# required=False, +# description="Platform-specific modes/flight styles to perform" +# + " next action", +# example="orbit", +# ), +# "latitude_waypoint": fields.Float( +# required=True, +# description="Next waypoint, x-coordinate", +# example=-4.187143188645706, +# ), +# "longitude_waypoint": fields.Float( +# required=True, +# description="Next waypoint, y-coordinate", +# example=50.37072283932642, +# ), +# "altitude": fields.Float( +# required=False, +# description="Altitude of next action", +# example=15.0, +# ), +# "depth": fields.Float( +# required=False, +# description="Depth of next action", +# example=15.0, +# ), +# "activate_payload": fields.Boolean( +# required=False, +# description="To activate/deactivate sensor for Autosub " +# + "Hover-1 --> `MBES` sensor and for EcoSUB --> `Sidescan`", +# example=True, +# ), +# "send_environmental_data": fields.Boolean( +# required=False, +# description="To trigger the platform to send list of observations" +# + " if any found", +# example=False, +# ), +# }, +# ) + +# mission_plan_schema = api.model( +# "MissionPlan", +# { +# "message": fields.Nested( +# full_message_schema, +# required=True, +# description="Message header", +# ), +# "plan_ID": fields.Integer( +# required=True, +# description="Identifier of given mission sequence planned", +# example=1, +# ), +# "platform_serial": fields.String( +# required=True, +# description="Serial of target platform to send mission to", +# example="reav-60", +# ), +# "plan": fields.List( +# fields.Nested(action_schema), +# required=True, +# description="Sequence of actions/instructions generated by the " +# + " Autonomy Engine that should be compiled by the respective C2.", +# ), +# }, +# ) diff --git a/formats/observation.py b/formats/observation.py index 6889b15..8cfa412 100644 --- a/formats/observation.py +++ b/formats/observation.py @@ -1,47 +1,79 @@ """ schema: platform-specific decoded status message (DRAFT) """ -from . import full_message_schema, api -from flask_restx import fields +# from . import full_message_schema, api +# from flask_restx import fields - -observation_schema = api.model( - "Observation", - { - "message": fields.Nested( - full_message_schema, - required=True, - description="Message header", - ), - "platform_serial": fields.String( - required=True, - description="Serial of platform to sendign observations", - example="ecosub-3", - ), +observation_schema = { + "allOf": [{"$ref": "#/components/schemas/Message"}], + "type": "object", + "properties": { + "platform_serial": { + "description": "Serial of platform to sendign observations", + "example": "ecosub-3", + }, # "observation_type" ==> payloads tied to different types maybe? # properties of each observation? - "points_of_interest": fields.Float( - required=False, - description="Points from features of interest identified by" + "points_of_interest": { + "description": "Points from features of interest identified by" + " platform if any found. DEFINE FORMAT.", - example="", - ), - "region_surveyed": fields.Float( - required=False, - description="Region surveyed by given platform. DEFINE FORMAT." + "example": "", + }, + "region_surveyed": { + "description": "Region surveyed by given platform. DEFINE FORMAT." + " GEOJSON?", - example="", - ), - "quality_of_points": fields.Float( - required=False, - description="Quality/strength of points from features of interest" + "example": "", + }, + "quality_of_points": { + "description": "Quality/strength of points from features of interest" + " identified by platform. DEFINE FORMAT.", - example=0.98, - ), - "additional_data": fields.Raw( - required=False, - description="Placeholder field for any additional data", - example={"sensor_payload": False}, - ), + "example": 0.98, + }, + "additional_data": { + "description": "Placeholder field for any additional data", + "example": {"sensor_payload": False}, + }, }, -) + "required": ["platform_serial"], +} + +# observation_schema = api.model( +# "Observation", +# { +# "message": fields.Nested( +# full_message_schema, +# required=True, +# description="Message header", +# ), +# "platform_serial": fields.String( +# required=True, +# description="Serial of platform to sendign observations", +# example="ecosub-3", +# ), +# # "observation_type" ==> payloads tied to different types maybe? +# # properties of each observation? +# "points_of_interest": fields.Float( +# required=False, +# description="Points from features of interest identified by" +# + " platform if any found. DEFINE FORMAT.", +# example="", +# ), +# "region_surveyed": fields.Float( +# required=False, +# description="Region surveyed by given platform. DEFINE FORMAT." +# + " GEOJSON?", +# example="", +# ), +# "quality_of_points": fields.Float( +# required=False, +# description="Quality/strength of points from features of interest" +# + " identified by platform. DEFINE FORMAT.", +# example=0.98, +# ), +# "additional_data": fields.Raw( +# required=False, +# description="Placeholder field for any additional data", +# example={"sensor_payload": False}, +# ), +# }, +# ) diff --git a/formats/planning_configuration.py b/formats/planning_configuration.py index b46cc32..7f1d84e 100644 --- a/formats/planning_configuration.py +++ b/formats/planning_configuration.py @@ -2,102 +2,230 @@ schemas: configuration sent to Autonomy Engine (i.e. during an emergency, if a platform needs to be removed from the mission planning) """ -from . import api, full_message_schema, platform_schema -from flask_restx import fields +# from . import api, full_message_schema, platform_schema +# from flask_restx import fields -region_schema = api.model( - "RegionSchema", - { - "region": fields.Raw( - required=True, - description="Using GEOJSON, exact region of interest in rectangle" - + " format polygon", - example={ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "properties": {}, - "geometry": { - "coordinates": [ - [ - [-4.187143188645706, 50.37072283932642], - [-4.202697005964865, 50.368816892405874], - [-4.203156724702808, 50.365640144076906], - [-4.19449868846155, 50.362267670845654], - ] - ], - "type": "Polygon", - }, - } - ], - }, - ), +constraints_schema = { + "type": "object", + "properties": { + "min_altitude": { + "type": "number", + "description": "Minimum altitude set for squad.", + "example": 15.2, + }, + "min_velocity": { + "type": "number", + "description": "Minimum velocity set for squad.", + "example": 0.1, + }, + "max_velocity": { + "type": "number", + "description": "Maximum altitude set for squad.", + "example": 0.9, + }, }, -) + "required": ["min_altitude", "min_velocity", "max_velocity"], +} -squad_metadata_schema = api.model( - "SquadMetadataSchema", - { - "squad_ID": fields.Integer( - required=True, - description="Identifier of given squad", - example=23, - ), - "no_of_platforms": fields.Integer( - required=True, - description="Number of platforms", - example=3, - ), - "platforms": fields.List( - fields.Nested(platform_schema), - required=True, - description="Squad consists of these platforms", - ), - "squad_mission_type": fields.String( - required=True, - description="Mission of given squad: `tracking`, `survey`" - + ", `inspection`", - example="survey", - ), - "squad_state": fields.Boolean( - required=True, - description="In execution, Waiting.. <define further>", - example=False, - ), - "region_of_interest": fields.List( - fields.Nested(region_schema), - required=False, - description="Region of interest and exclusion zones per squad.", - ), - "exclusion_zones": fields.List( - fields.Nested(region_schema), - required=True, - description="Exclusion zones exclusion zones per squad.", - ), +platform_schema = { + "type": "object", + "properties": { + "platform_ID": { + "type": "integer", + "description": "Identifier for platform", + "example": 23, + }, + "serial": { + "type": "string", + "description": "platform serial number", + "example": "reav-60", + }, + "model": { + "type": "string", + "example": "reav", + }, + "constraints": constraints_schema, + }, + "required": ["platform_ID", "serial", "model", "constraints"], +} + +region_schema = { + "type": "object", + "properties": { + "geometry_coordinates": { + "type": "array", # TODO: Check if config defn is right. + "example": [ + [ + [-4.187143188645706, 50.37072283932642], + [-4.202697005964865, 50.368816892405874], + [-4.203156724702808, 50.365640144076906], + [-4.19449868846155, 50.362267670845654], + ] + ], + }, }, -) + "description": "Using GEOJSON, exact 4-point region (rectangle shaped)", + "required": ["geometry_coordinates"], +} +squad_metadata_schema = { + "type": "object", + "properties": { + "squad_ID": { + "type": "integer", + "description": "Identifier of given squad", + "example": 23, + }, + "no_of_platforms": { + "type": "integer", + "description": "Number of platforms", + "example": 3, + }, + "platforms": { + "type": "array", + "items": platform_schema, + "description": "Squad consists of these platforms", + }, + "squad_mission_type": { + "type": "string", + "enum": ["tracking", "survey", "inspection"], + "description": "Mission of given squad: `tracking`, `survey`" + + ", `inspection`", + "example": "survey", + }, + "squad_state": { + "type": "string", + "description": "In execution, Waiting.. <define further>", + "example": False, + }, + "region_of_interest": region_schema, + "exclusion_zones": { + "type": "array", + "items": region_schema, + "description": "Exclusion zones per squad.", + }, + }, + "required": [ + "squad_ID", + "no_of_platforms", + "platforms", + "squad_mission_type", + "squad_state", + "exclusion_zones", + ], +} -planning_configuration_schema = api.model( - "PlanningConfigurationSchema", - { - "message": fields.Nested( - full_message_schema, - required=True, - description="Message header", - ), - "ID": fields.Integer( - required=True, - description="Unique identifier tagged to version of this" +planning_configuration_schema = { + "allOf": [{"$ref": "#/components/schemas/Message"}], + "type": "object", + "properties": { + "config_ID": { + "type": "integer", + "description": "Unique identifier tagged to version of this" + " configuration plan", - example=3, - ), - "squads": fields.Nested( - squad_metadata_schema, - required=False, - description="Details of each squad", - ), + "example": 3, + }, + "squads": { + "type": "array", + "items": squad_metadata_schema, + }, }, -) + "required": ["config_ID", "squads"], +} + +# region_schema = api.model( +# "RegionSchema", +# { +# "region": fields.Raw( +# required=True, +# description="Using GEOJSON, exact region of interest in rectangle" +# + " format polygon", +# example={ +# "type": "FeatureCollection", +# "features": [ +# { +# "type": "Feature", +# "properties": {}, +# "geometry": { +# "coordinates": [ +# [ +# [-4.187143188645706, 50.37072283932642], +# [-4.202697005964865, 50.368816892405874], +# [-4.203156724702808, 50.365640144076906], +# [-4.19449868846155, 50.362267670845654], +# ] +# ], +# "type": "Polygon", +# }, +# } +# ], +# }, +# ), +# }, +# ) + +# squad_metadata_schema = api.model( +# "SquadMetadataSchema", +# { +# "squad_ID": fields.Integer( +# required=True, +# description="Identifier of given squad", +# example=23, +# ), +# "no_of_platforms": fields.Integer( +# required=True, +# description="Number of platforms", +# example=3, +# ), +# "platforms": fields.List( +# fields.Nested(platform_schema), +# required=True, +# description="Squad consists of these platforms", +# ), +# "squad_mission_type": fields.String( +# required=True, +# description="Mission of given squad: `tracking`, `survey`" +# + ", `inspection`", +# example="survey", +# ), +# "squad_state": fields.Boolean( +# required=True, +# description="In execution, Waiting.. <define further>", +# example=False, +# ), +# "region_of_interest": fields.List( +# fields.Nested(region_schema), +# required=False, +# description="Region of interest and exclusion zones per squad.", +# ), +# "exclusion_zones": fields.List( +# fields.Nested(region_schema), +# required=True, +# description="Exclusion zones exclusion zones per squad.", +# ), +# }, +# ) + + +# planning_configuration_schema = api.model( +# "PlanningConfigurationSchema", +# { +# "message": fields.Nested( +# full_message_schema, +# required=True, +# description="Message header", +# ), +# "ID": fields.Integer( +# required=True, +# description="Unique identifier tagged to version of this" +# + " configuration plan", +# example=3, +# ), +# "squads": fields.Nested( +# squad_metadata_schema, +# required=False, +# description="Details of each squad", +# ), +# }, +# ) diff --git a/formats/platform_status.py b/formats/platform_status.py index 761b0fc..d3c8157 100644 --- a/formats/platform_status.py +++ b/formats/platform_status.py @@ -1,177 +1,346 @@ """ - schema: platform-specific decoded status message (DRAFT) + schema: platform-specific decoded status message """ -from . import full_message_schema, api -from flask_restx import fields +# from . import full_message_schema, api +# from flask_restx import fields -gps_schema = api.model( - "GPS", - { - "gps_source": fields.Float( # TODO: TBD with partners - required=False, - description=( - "Source of gps position. E.g. USBL (external)," - + "platform itself (internal)" - ), - example="internal", - ), - "latitude_type": fields.String( - required=False, - description="", - example="", - ), - "longitude_type": fields.String( - required=False, - description="", - example="", - ), - "latitude": fields.Float( - required=False, - description="Latitude in <DEFINE UNITS>", - example="", - ), - "longitude": fields.Float( - required=False, - description="Longitude in <DEFINE UNITS>", - example="", - ), - "depth": fields.Float( - required=False, - description="Depth in <DEFINE UNITS>", - example="", - ), - "altitude": fields.Float( - required=False, - description="Altitude in <DEFINE UNITS>", - example="", - ), - # "gps_fix_seconds_ago" - }, -) -sensor_schema = api.model( - "SensorSchema", - { - "sensor_ID": fields.Integer( - required=True, - description="unique identifier for platform", - example=2, - ), - "serial": fields.String( - required=False, - description="serial number of sensor", - example="mbes-001", - ), - "sensor_status": fields.Boolean( - required=False, - description="Sensor switched on (True) or off (False)", - example=True, - ), - "additional_data": fields.Raw( - required=False, - description="Any addition fields/data to be added here", - ), +gps_schema = { + "type": "object", + "properties": { + "gps_source": { + "type": "string", + "description": "Source of gps position. E.g. USBL (external)," + + "platform itself (internal)", + "example": "internal", + }, + "latitude_type": { + "type": "string", + "description": "TODO: Add description", + }, + "longitude_type": { + "type": "string", + "description": "TODO: Add description", + }, + "latitude": { + "type": "number", + "description": "Latitude in decimal degrees.", + "example": 178.2, + }, + "longitude": { + "type": "number", + "description": "Longitude in decimal degrees.", + "example": -10.122, + }, + "depth": { + "type": "number", + "description": "Target depth in metres", + "example": 50, + "default": 0, + }, + "altitude": { + "type": "number", + "description": "Target altitude in metres", + "example": 20, + }, }, -) + "required": [ + "gps_source", + "latitude", + "longitude", + ], +} +sensor_schema = { + "type": "object", + "description": "Scanning sensor on platform available to be controlled by the Autonomy Engine", + "properties": { + "sensor_serial": { + "type": "string", + "description": "serial number of sensor", + "example": "mbes-002a", + }, + "sensor_status": { + "type": "boolean", + "description": "Sensor switched on (True) or off (False)", + "example": True, + }, + "additional_data": { + "type": "null", + "description": "Any addition fields/data to be added here", + "example": {"payload": [1.2, 434]}, + }, + }, + "required": [], +} -platform_status_message_schema = api.model( - "platformStatusMessage", - { - "message": fields.Nested( - full_message_schema, - required=True, - description="Message header", - ), - "platform_ID": fields.Integer( - required=True, - description="unique identifier for platform", - example=1, - ), - "active": fields.Boolean( - required=False, - description="When a platform is in deployment (executing a" +platform_status_message_schema = { + "allOf": [{"$ref": "#/components/schemas/Message"}], + "type": "object", + "properties": { + "platform_ID": { + "type": "integer", + "description": "Identifier for platform", + "example": 1, + }, + "platform_timestamp": { + "type": "date-time", + "decription": "Timestamp for onboard platform status message", + "example": "2022-12-21T00:00:00Z", + }, + "active": { + "type": "boolean", + "description": "When a platform is in deployment (executing a" + " mission plan) this should be True", - example=True, - ), - "platform_state": fields.String( + "example": True, + }, + "platform_state": { # TODO: Define dictionary with potential STATES of each platform - required=False, - description="Current state executed by platform. E.g. " + "type": "string", + "description": "Current state executed by platform. E.g. " + "STOP, IDLE, ABORT.", - example="IDLE", - ), - "autonomy_plan_ID": fields.Integer( - required=False, - description="Last mission plan ID (according to Autonomy Engine's" + "example": "ABORT", + }, + "autonomy_plan_ID": { + "type": "integer", + "description": "Last mission plan ID (according to Autonomy Engine's" + " mission plan number) executed by platform", - example=1, - ), - "mission_track_ID": fields.Integer( - required=False, - description=( - "Track number - stage in mission (e.g. " - + "4 --> Waypoint 3 to Waypoint 4)" - ), - example=4, - ), - "mission_action_ID": fields.Integer( - required=False, - description="to add description", - example=1, - ), - "range_to_go": fields.Float( - required=False, - description="Estimated distance to reach next waypoint", - example=124.3, - ), - "speed_over_ground": fields.Float( - required=False, - description="", - example=124.3, - ), - "water_current_velocity": fields.Float( - required=False, - description="", - example=124.3, - ), - "thrust_applied": fields.Float( - required=False, - description="TODO: Needs further consideration", - example=124.3, - ), - "health_status": fields.String( - required=False, - description="Health status extracted by respective platform " - + "if any diagnosis available checks on sensors", - example="Warning", - ), - "gps_data": fields.List( - fields.Nested(gps_schema), # TODO: TBD Do we want a list of - # gps readings to allow > 1 reading i.e. platform + usbl - required=True, - description="Metadata pf each platform", - ), - "localisation_error": fields.Float( - required=False, - description="Localisation error at last USBL update.", - example="", - ), - "usbl_fix_seconds_ago": fields.Float( - required=False, - description="", - example="", - ), - "battery_remaining_capacity": fields.Float( - required=True, - description="Battery remaining capacity % provided by respective" - + " platform/C2.", - example=80.0, - ), - "sensor_config": fields.Nested( - sensor_schema - ), # TODO: TBD Do we want a list of sensors to allow > 1 sensor + "example": 1, + }, + "mission_track_ID": { + "type": "integer", + "description": "Track number - stage in mission (e.g. " + + "4 --> Waypoint 3 to Waypoint 4)", + "example": 4, + }, + "mission_action_ID": { + "type": "integer", + "description": "TODO: add description", + "example": 1, + }, + "range_to_go": { + "type": "number", + "description": "Estimated distance to reach next waypoint", + "example": 124.3, + }, + "speed_over_ground": { + "type": "number", + "description": "TODO: add description", + "example": 124.3, + }, + "water_current_velocity": { + "type": "number", + "description": "TODO: add description", + "example": 124.3, + }, + "thrust_applied": { + "type": "number", + "description": "TODO: Needs further consideration", + "example": 124.3, + }, + "health_status": { + "type": "string", + "description": "Health status extracted by respective platform " + + "if any diagnosis is available to check sensors", + "example": "Warning", + }, + "gps_data": { + "type": "array", + "description": "position of platform", + "items": gps_schema, + }, + "localisation_error": { + "type": "number", + "description": "Localisation error at last USBL update.", + "example": 0.000129, + }, + "usbl_fix_seconds_ago": { + "type": "number", + "description": "USBL Fix received x second ago.", + "example": 10.0, + }, + "battery_remaining_capacity": { + "type": "number", + "description": "Battery remaining capacity % provided by respective", + "example": 80.2, + }, + "sensor_config": sensor_schema, }, -) + "required": [ + "platform_ID", + "platform_timestamp", + "gps_data", + "battery_remaining_capacity", + ], +} + +# gps_schema = api.model( +# "GPS", +# { +# "gps_source": fields.Float( # TODO: TBD with partners +# required=False, +# description=( +# "Source of gps position. E.g. USBL (external)," +# + "platform itself (internal)" +# ), +# example="internal", +# ), +# "latitude_type": fields.String( +# required=False, +# description="", +# example="", +# ), +# "longitude_type": fields.String( +# required=False, +# description="", +# example="", +# ), +# "latitude": fields.Float( +# required=False, +# description="Latitude in <DEFINE UNITS>", +# example="", +# ), +# "longitude": fields.Float( +# required=False, +# description="Longitude in <DEFINE UNITS>", +# example="", +# ), +# "depth": fields.Float( +# required=False, +# description="Depth in <DEFINE UNITS>", +# example="", +# ), +# "altitude": fields.Float( +# required=False, +# description="Altitude in <DEFINE UNITS>", +# example="", +# ), +# # "gps_fix_seconds_ago" +# }, +# ) + +# sensor_schema = api.model( +# "SensorSchema", +# { +# "sensor_ID": fields.Integer( +# required=True, +# description="unique identifier for platform", +# example=2, +# ), +# "serial": fields.String( +# required=False, +# description="serial number of sensor", +# example="mbes-001", +# ), +# "sensor_status": fields.Boolean( +# required=False, +# description="Sensor switched on (True) or off (False)", +# example=True, +# ), +# "additional_data": fields.Raw( +# required=False, +# description="Any addition fields/data to be added here", +# ), +# }, +# ) + +# platform_status_message_schema = api.model( +# "platformStatusMessage", +# { +# "message": fields.Nested( +# full_message_schema, +# required=True, +# description="Message header", +# ), +# "platform_ID": fields.Integer( +# required=True, +# description="unique identifier for platform", +# example=1, +# ), +# "active": fields.Boolean( +# required=False, +# description="When a platform is in deployment (executing a" +# + " mission plan) this should be True", +# example=True, +# ), +# "platform_state": fields.String( +# # TODO: Define dictionary with potential STATES of each platform +# required=False, +# description="Current state executed by platform. E.g. " +# + "STOP, IDLE, ABORT.", +# example="IDLE", +# ), +# "autonomy_plan_ID": fields.Integer( +# required=False, +# description="Last mission plan ID (according to Autonomy Engine's" +# + " mission plan number) executed by platform", +# example=1, +# ), +# "mission_track_ID": fields.Integer( +# required=False, +# description=( +# "Track number - stage in mission (e.g. " +# + "4 --> Waypoint 3 to Waypoint 4)" +# ), +# example=4, +# ), +# "mission_action_ID": fields.Integer( +# required=False, +# description="to add description", +# example=1, +# ), +# "range_to_go": fields.Float( +# required=False, +# description="Estimated distance to reach next waypoint", +# example=124.3, +# ), +# "speed_over_ground": fields.Float( +# required=False, +# description="", +# example=124.3, +# ), +# "water_current_velocity": fields.Float( +# required=False, +# description="", +# example=124.3, +# ), +# "thrust_applied": fields.Float( +# required=False, +# description="TODO: Needs further consideration", +# example=124.3, +# ), +# "health_status": fields.String( +# required=False, +# description="Health status extracted by respective platform " +# + "if any diagnosis available checks on sensors", +# example="Warning", +# ), +# "gps_data": fields.List( +# fields.Nested(gps_schema), # TODO: TBD Do we want a list of +# # gps readings to allow > 1 reading i.e. platform + usbl +# required=True, +# description="Metadata pf each platform", +# ), +# "localisation_error": fields.Float( +# required=False, +# description="Localisation error at last USBL update.", +# example="", +# ), +# "usbl_fix_seconds_ago": fields.Float( +# required=False, +# description="", +# example="", +# ), +# "battery_remaining_capacity": fields.Float( +# required=True, +# description="Battery remaining capacity % provided by respective" +# + " platform/C2.", +# example=80.0, +# ), +# "sensor_config": fields.Nested( +# sensor_schema +# ), # TODO: TBD Do we want a list of sensors to allow > 1 sensor +# }, +# ) -# TBD: Do we append beacon positions with platform positions? +# # TBD: Do we append beacon positions with platform positions? diff --git a/generate_swagger.py b/generate_swagger.py new file mode 100644 index 0000000..678c1bb --- /dev/null +++ b/generate_swagger.py @@ -0,0 +1,134 @@ +from flask import Flask +from flasgger import Swagger + +# from . import properties +from formats.message_wrapper import message_wrapper_schema +from formats.mission_plan import mission_plan_schema +from formats.observation import observation_schema +from formats.planning_configuration import planning_configuration_schema +from formats.platform_status import platform_status_message_schema + +app = Flask(__name__) + +swagger_config = { + "headers": [], + "openapi": "3.0.2", + "swagger_ui": True, + "specs_route": "/soardocs/", + "info": { + "title": "Backbone Message Formats", + "version": "0.1", + "description": "SoAR message schemas (i.e. formats)", + }, + "specs": [ + { + "endpoint": "swagger", + "route": "/swagger.json", + "rule_filter": lambda rule: True, + "model_filter": lambda tag: True, + } + ], + "components": { + "schemas": { + "Message": message_wrapper_schema, + "MissionPlan": mission_plan_schema, + "Observation": observation_schema, + "PlanningConfiguration": planning_configuration_schema, + "PlatformStatus": platform_status_message_schema, + }, + }, + "paths": { + "/all_messages": { + "get": { + "description": "Returns all messages from the system.", + "responses": { + "200": { + "description": "A list of messages.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/" + + "schemas/Coordinate" + }, + { + "$ref": "#/components/" + + "schemas/PlatformStatus" + }, + { + "$ref": "#/components/" + + "schemas/MissionPlan" + }, + { + "$ref": "#/components/" + + "schemas/Observation" + }, + { + "$ref": "#/components/" + + "schemas/PlanningConfiguration" + }, + { + "$ref": "#/components/" + + "schemas/PlatformStatus" + }, + ], + "discriminator": { + "propertyName": "message_type", + }, + } + } + }, + } + }, + } + }, + }, + "produces": ["application/json"], + "consumes": ["application/json"], +} +message_types = [ + "Message", + "Acknowledgement", + "MissionPlan", + "Observation", + "PlanningConfiguration", + "PlatformStatus", +] +for item in message_types: + swagger_config["paths"]["/" + str(item.lower())] = { + "get": { + "description": "Returns message for " + item, + "responses": { + "200": { + "description": item + " message.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas" + + "/" + item, + }, + ], + "discriminator": { + "propertyName": "message_type", + }, + } + } + }, + } + }, + } + } + +swag = Swagger(app, config=swagger_config, merge=True) + +# app.add_url_rule( +# '/coordinates', +# view_func=Coordinates.as_view('coordinates'), +# methods=['GET'] +# ) + +if __name__ == "__main__": + app.run(debug=True) -- GitLab