Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Open sidebar
Communications Backbone System
backbone-message-format
Commits
e113878b
Commit
e113878b
authored
2 years ago
by
Trishna Saeharaseelan
Browse files
Options
Download
Email Patches
Plain Diff
refactor: all messages from flask model to json schema definitions
parent
3a41f4a2
Changes
12
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
1086 additions
and
491 deletions
+1086
-491
README.md
README.md
+1
-1
__init__.py
__init__.py
+0
-1
docs/__init__.py
docs/__init__.py
+0
-8
examples/all/observation.json
examples/all/observation.json
+1
-1
formats/__init__.py
formats/__init__.py
+95
-95
formats/acknowledgement.py
formats/acknowledgement.py
+37
-22
formats/message_wrapper.py
formats/message_wrapper.py
+58
-0
formats/mission_plan.py
formats/mission_plan.py
+140
-72
formats/observation.py
formats/observation.py
+69
-37
formats/planning_configuration.py
formats/planning_configuration.py
+218
-90
formats/platform_status.py
formats/platform_status.py
+333
-164
generate_swagger.py
generate_swagger.py
+134
-0
No files found.
README.md
View file @
e113878b
...
...
@@ -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_head
er`
:
Each message below will be wrapped in a
`message_
wrapp
er`
:
*
`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
...
...
This diff is collapsed.
Click to expand it.
__init__.py
View file @
e113878b
...
...
@@ -11,4 +11,3 @@ __all__ = [
]
app
=
Flask
(
__name__
)
api
=
Api
(
app
)
# api = Marshmallow(app)
This diff is collapsed.
Click to expand it.
docs/__init__.py
deleted
100644 → 0
View file @
3a41f4a2
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"
]
This diff is collapsed.
Click to expand it.
examples/all/observation.json
View file @
e113878b
...
...
@@ -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"
:
[],
...
...
This diff is collapsed.
Click to expand it.
formats/__init__.py
View file @
e113878b
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),
#
},
#
)
This diff is collapsed.
Click to expand it.
formats/acknowledgement.py
View file @
e113878b
...
...
@@ -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",
# ),
# },
# )
This diff is collapsed.
Click to expand it.
formats/message_wrapper.py
0 → 100644
View file @
e113878b
"""
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"
],
}
This diff is collapsed.
Click to expand it.
formats/mission_plan.py
View file @
e113878b
...
...
@@ -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.",
# ),
# },
# )
This diff is collapsed.
Click to expand it.
formats/observation.py
View file @
e113878b
"""
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},
# ),
# },
# )
This diff is collapsed.
Click to expand it.
formats/planning_configuration.py
View file @
e113878b
...
...
@@ -2,25 +2,60 @@
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"
:
[
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"
],
}
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
],
...
...
@@ -28,76 +63,169 @@ region_schema = api.model(
[
-
4.19449868846155
,
50.362267670845654
],
]
],
"type"
:
"Polygon"
,
},
}
},
"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
=
{
"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"
:
{
"type"
:
"array"
,
"items"
:
squad_metadata_schema
,
},
),
},
)
"required"
:
[
"config_ID"
,
"squads"
],
}
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."
,
),
},
)
# 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"
,
),
},
)
# 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",
# ),
# },
# )
This diff is collapsed.
Click to expand it.
formats/platform_status.py
View file @
e113878b
"""
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?
This diff is collapsed.
Click to expand it.
docs/
generate_swagger.py
→
generate_swagger.py
View file @
e113878b
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
=
{
...
...
@@ -23,96 +30,15 @@ swagger_config = {
],
"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"
,
},
},
},
"Message"
:
message_wrapper_schema
,
"MissionPlan"
:
mission_plan_schema
,
"Observation"
:
observation_schema
,
"PlanningConfiguration"
:
planning_configuration_schema
,
"PlatformStatus"
:
platform_status_message_schema
,
},
},
"paths"
:
{
"/messages"
:
{
"/
all_
messages"
:
{
"get"
:
{
"description"
:
"Returns all messages from the system."
,
"responses"
:
{
...
...
@@ -130,6 +56,22 @@ swagger_config = {
"$ref"
:
"#/components/"
+
"schemas/PlatformStatus"
},
{
"$ref"
:
"#/components/"
+
"schemas/MissionPlan"
},
{
"$ref"
:
"#/components/"
+
"schemas/Observation"
},
{
"$ref"
:
"#/components/"
+
"schemas/PlanningConfiguration"
},
{
"$ref"
:
"#/components/"
+
"schemas/PlatformStatus"
},
],
"discriminator"
:
{
"propertyName"
:
"message_type"
,
...
...
@@ -141,19 +83,32 @@ swagger_config = {
},
}
},
"/platformstatus"
:
{
},
"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
platform status message"
,
"description"
:
"Returns
message for "
+
item
,
"responses"
:
{
"200"
:
{
"description"
:
"Platform status
message."
,
"description"
:
item
+
"
message."
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"allOf"
:
[
{
"$ref"
:
"#/components/schemas"
+
"/PlatformStatus"
+
"/"
+
item
,
},
],
"discriminator"
:
{
...
...
@@ -165,11 +120,7 @@ swagger_config = {
}
},
}
},
},
"produces"
:
[
"application/json"
],
"consumes"
:
[
"application/json"
],
}
}
swag
=
Swagger
(
app
,
config
=
swagger_config
,
merge
=
True
)
...
...
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment