PyAres 1.0.1__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyares-1.0.1 → pyares-1.2.0}/.gitignore +1 -0
- pyares-1.2.0/PKG-INFO +27 -0
- pyares-1.2.0/PyAres/Analyzing/analysis_service.py +147 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Devices/device_test.py +2 -2
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Planners/planner_test.py +8 -0
- pyares-1.2.0/PyAres/Demo/Planners/planner_wiki.py +33 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Device/device_service.py +37 -21
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Models/__init__.py +3 -2
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Models/ares_data_models.py +6 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Planning/__init__.py +6 -2
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Planning/planner_models.py +86 -12
- pyares-1.2.0/PyAres/Planning/planning_service.py +166 -0
- pyares-1.2.0/PyAres/Utils/ares_objective_status_utils.py +10 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/ares_outcome_utils.py +2 -4
- pyares-1.2.0/PyAres/Utils/ares_plan_status_code_utils.py +9 -0
- pyares-1.2.0/PyAres/Utils/ares_service_base.py +137 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/ares_value_utils.py +1 -1
- pyares-1.2.0/PyAres/Utils/datamodel_version_utils.py +29 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/device_status_code_utils.py +2 -3
- pyares-1.2.0/PyAres/Utils/grpc_base.py +59 -0
- pyares-1.2.0/PyAres/Utils/logging_utils.py +17 -0
- pyares-1.2.0/PyAres/Utils/metadata_interceptors.py +69 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/plan_response_utils.py +10 -2
- pyares-1.2.0/PyAres/Utils/planning_param_utils.py +44 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/__init__.py +5 -1
- pyares-1.2.0/README.md +13 -0
- {pyares-1.0.1 → pyares-1.2.0}/tests/test_analyzer.py +55 -10
- pyares-1.2.0/tests/test_analyzer_integration.py +86 -0
- pyares-1.2.0/tests/test_batch_planning_regressions.py +64 -0
- {pyares-1.0.1 → pyares-1.2.0}/tests/test_planner.py +4 -3
- pyares-1.0.1/PKG-INFO +0 -168
- pyares-1.0.1/PyAres/Analyzing/analysis_service.py +0 -290
- pyares-1.0.1/PyAres/Demo/Planners/planner_wiki.py +0 -29
- pyares-1.0.1/PyAres/Planning/planning_service.py +0 -284
- pyares-1.0.1/PyAres/Utils/planning_param_utils.py +0 -20
- pyares-1.0.1/README.md +0 -154
- {pyares-1.0.1 → pyares-1.2.0}/.github/dependabot.yml +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/.github/workflows/pypi.yaml +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/.github/workflows/python-tests.yaml +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/LICENSE +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Analyzing/__init__.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Analyzing/analyzer_models.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Analyzers/airship_analyzer.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Analyzers/analyzer_test.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Analyzers/analyzer_test_tools_demo.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Analyzers/analyzer_wiki.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Devices/failure_test_device.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Devices/hotplate.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Devices/random_number_device.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Devices/rotary_mixer.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Planners/airship_planner.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Planners/planner_test_tools_demo.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Device/__init__.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Device/device_models.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Device/device_warnings.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/ares_data_schema_utils.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/ares_data_type_utils.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/ares_device_command_utils.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/ares_struct_utils.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/param_history_info_utils.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/plan_request_utils.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/test_tools/__init__.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/test_tools/analyzer_tester.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/PyAres/test_tools/planner_tester.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/pyproject.toml +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/setup.cfg +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/setup.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/tests/__init__.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/tests/mock_grpc_context.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/tests/test_ares_data_schema_utils.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/tests/test_ares_data_type_utils.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/tests/test_ares_device.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/tests/test_ares_struct_utils.py +0 -0
- {pyares-1.0.1 → pyares-1.2.0}/tests/test_ares_value_type_conversions.py +0 -0
pyares-1.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyAres
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: The official Python library for ARES.
|
|
5
|
+
Project-URL: Homepage, https://github.com/AFRL-ARES/PyAres
|
|
6
|
+
Author-email: AFRL <AFRL.RX.AREScontact@us.af.mil>
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: ares-datamodel>=0.31.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# PyAres
|
|
16
|
+
|
|
17
|
+
The PyAres library is designed to provide support for building planners, analyzers and devices as part of your ARES self driving labratory. PyAres leverages the power of protobuf and gRPC to communicate with your ARES system while providing a simple Pythonic API.
|
|
18
|
+
|
|
19
|
+
You can find the main documentation on PyAres [here](https://afrl-ares.github.io/docs/pyares/intro) as well as the rest of the ARES ecosystem by visting https://afrl-ares.github.io/ to browse our centralized documentation site.
|
|
20
|
+
|
|
21
|
+
### 📄 License
|
|
22
|
+
|
|
23
|
+
The PyAres project is licensed under the MIT License - see details in [LICENSE.txt]([https://github.com/AFRL-ARES/PyAres/blob/Develop/LICENSE.txt](https://github.com/AFRL-ARES/PyAres/blob/Develop/LICENSE)) <br></br>
|
|
24
|
+
|
|
25
|
+
### CLEARANCE
|
|
26
|
+
|
|
27
|
+
Distribution A. Approved for public release: distribution unlimited. AFRL-2025-5332.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Standard Imports
|
|
2
|
+
import grpc
|
|
3
|
+
from typing import Callable, Awaitable, Union, Mapping, Dict, Optional
|
|
4
|
+
|
|
5
|
+
# Import generated protobuf and gRPC stubs
|
|
6
|
+
from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2 as analyzer_service
|
|
7
|
+
from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2_grpc as analyzer_service_grpc
|
|
8
|
+
from ares_datamodel.analyzing import analysis_pb2
|
|
9
|
+
from ares_datamodel.analyzing import analyzer_capabilities_pb2
|
|
10
|
+
from ares_datamodel import ares_data_schema_pb2
|
|
11
|
+
from ares_datamodel import ares_outcome_enum_pb2
|
|
12
|
+
|
|
13
|
+
# Import Utilities
|
|
14
|
+
from ..Utils import ares_struct_utils
|
|
15
|
+
from ..Utils import ares_data_schema_utils
|
|
16
|
+
from ..Utils import ares_outcome_utils
|
|
17
|
+
from ..Utils.ares_service_base import AresServiceWrapperBase, AresBaseService
|
|
18
|
+
|
|
19
|
+
# Import python models
|
|
20
|
+
from ..Models import ares_data_models, RequestMetadata, AresSchemaEntry
|
|
21
|
+
from .analyzer_models import AnalysisRequest, AnalysisResponse
|
|
22
|
+
|
|
23
|
+
# Type hints for the user's custom logic
|
|
24
|
+
AnalyzeLogicFunction = Callable[[AnalysisRequest], Union[AnalysisResponse, Awaitable[AnalysisResponse]]]
|
|
25
|
+
|
|
26
|
+
class AresAnalyzerServiceWrapper(AresServiceWrapperBase, analyzer_service_grpc.AresRemoteAnalyzerServiceServicer):
|
|
27
|
+
"""
|
|
28
|
+
A wrapper around the gRPC service to expose native Python objects for analysis.
|
|
29
|
+
"""
|
|
30
|
+
def __init__(self, name: str, version: str, description: str, timeout: int, custom_analysis_logic: AnalyzeLogicFunction):
|
|
31
|
+
super().__init__(name, version, description, timeout)
|
|
32
|
+
self._custom_analysis_logic = custom_analysis_logic
|
|
33
|
+
self._analysis_parameters: Dict[str, ares_data_schema_pb2.AresValueSchema] = {}
|
|
34
|
+
|
|
35
|
+
def Analyze(self, request: analyzer_service.AnalysisRequest, context) -> analysis_pb2.Analysis:
|
|
36
|
+
print("Received an analysis request!")
|
|
37
|
+
try:
|
|
38
|
+
python_request = AnalysisRequest(
|
|
39
|
+
inputs=ares_struct_utils.ares_struct_to_dict(request.inputs),
|
|
40
|
+
settings=ares_struct_utils.ares_struct_to_dict(request.settings),
|
|
41
|
+
metadata=RequestMetadata(request.metadata))
|
|
42
|
+
|
|
43
|
+
python_response = self._custom_analysis_logic(python_request)
|
|
44
|
+
python_response = self._resolve_awaitable(python_response)
|
|
45
|
+
|
|
46
|
+
if not isinstance(python_response, AnalysisResponse):
|
|
47
|
+
print("Analysis response was an invalid type, ")
|
|
48
|
+
proto_analysis = analysis_pb2.Analysis()
|
|
49
|
+
proto_analysis.analysis_outcome = ares_outcome_enum_pb2.FAILURE
|
|
50
|
+
proto_analysis.error_string = "The user's custom analysis logic returned an invalid type, analysis cannot be processed"
|
|
51
|
+
return proto_analysis
|
|
52
|
+
|
|
53
|
+
print("Sending Analysis Response.....")
|
|
54
|
+
return analysis_pb2.Analysis(
|
|
55
|
+
result=python_response.result,
|
|
56
|
+
analysis_outcome=ares_outcome_utils.python_ares_outcome_to_proto_ares_outcome(python_response.outcome),
|
|
57
|
+
error_string=python_response.error_string
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
context.set_code(grpc.StatusCode.INTERNAL)
|
|
62
|
+
context.set_details(f"Error in custom analysis logic: {e}")
|
|
63
|
+
return analysis_pb2.Analysis(analysis_outcome=ares_outcome_enum_pb2.FAILURE, error_string=str(e))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def GetAnalysisParameters(self, request, context):
|
|
67
|
+
print("Analysis Parameters Requested")
|
|
68
|
+
try:
|
|
69
|
+
analysisParamResponse = analyzer_service.AnalysisParametersResponse()
|
|
70
|
+
|
|
71
|
+
for key, value in self._analysis_parameters.items():
|
|
72
|
+
map_entry = analysisParamResponse.parameter_schema.fields[key]
|
|
73
|
+
map_entry.CopyFrom(value)
|
|
74
|
+
|
|
75
|
+
return analysisParamResponse
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"Exception while trying to respond to ARES with analysis parameters! {e}")
|
|
79
|
+
|
|
80
|
+
def GetAnalyzerCapabilities(self, request, context) -> analyzer_capabilities_pb2.AnalyzerCapabilities:
|
|
81
|
+
print("Capabilities Requested!")
|
|
82
|
+
capabilities = analyzer_capabilities_pb2.AnalyzerCapabilities(timeout_seconds=self._timeout)
|
|
83
|
+
try:
|
|
84
|
+
for(key, value) in self._settings.items():
|
|
85
|
+
settings_entry = capabilities.settings_schema.fields[key]
|
|
86
|
+
settings_entry.CopyFrom(value)
|
|
87
|
+
|
|
88
|
+
return capabilities
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f"Exception while trying to respond to ARES capabilities request! {e}")
|
|
92
|
+
return capabilities
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def ValidateInputs(self, request: analyzer_service.ParameterValidationRequest, context):
|
|
96
|
+
response = analyzer_service.ParameterValidationResult(success=True)
|
|
97
|
+
provided_params: Mapping[str, ares_data_schema_pb2.AresValueSchema] = request.input_schema.fields
|
|
98
|
+
|
|
99
|
+
for stored_key, stored_schema in self._analysis_parameters.items():
|
|
100
|
+
if stored_key in provided_params:
|
|
101
|
+
matching_schema = provided_params[stored_key]
|
|
102
|
+
if stored_schema.type != matching_schema.type:
|
|
103
|
+
message = f"Schema Mismatch! {stored_key} was provided with the value type {stored_schema.type}, but the value type {matching_schema} was expected!"
|
|
104
|
+
response.messages.append(message)
|
|
105
|
+
else:
|
|
106
|
+
if not stored_schema.optional:
|
|
107
|
+
message = f"Schema Missing! {stored_key} is marked as a required piece of data for analysis, but no assignment was found in the provided schema!"
|
|
108
|
+
response.messages.append(message)
|
|
109
|
+
|
|
110
|
+
if len(response.messages) != 0:
|
|
111
|
+
response.success = False
|
|
112
|
+
|
|
113
|
+
return response
|
|
114
|
+
|
|
115
|
+
class AresAnalyzerService(AresBaseService):
|
|
116
|
+
"""
|
|
117
|
+
Manages the gRPC server for the AresAnalyzerService.
|
|
118
|
+
"""
|
|
119
|
+
def __init__(self,
|
|
120
|
+
custom_analysis_logic: AnalyzeLogicFunction,
|
|
121
|
+
name: str,
|
|
122
|
+
version: str,
|
|
123
|
+
description: str = "",
|
|
124
|
+
timeout: int = 30,
|
|
125
|
+
use_localhost: bool = True,
|
|
126
|
+
port: int = 7083,
|
|
127
|
+
max_message_size: int = -1):
|
|
128
|
+
"""
|
|
129
|
+
Initializes the AresAnalyzerService.
|
|
130
|
+
"""
|
|
131
|
+
super().__init__(
|
|
132
|
+
service_name=name,
|
|
133
|
+
description=description,
|
|
134
|
+
version=version,
|
|
135
|
+
port=port,
|
|
136
|
+
use_localhost=use_localhost,
|
|
137
|
+
max_message_size=max_message_size
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
self._service_wrapper = AresAnalyzerServiceWrapper(name=name, version=version, description=description, timeout=timeout, custom_analysis_logic=custom_analysis_logic)
|
|
141
|
+
analyzer_service_grpc.add_AresRemoteAnalyzerServiceServicer_to_server(self._service_wrapper, self.get_server())
|
|
142
|
+
|
|
143
|
+
def add_analysis_parameter(self, parameter_name: str, parameter_type: ares_data_models.AresDataType, optional: bool = False, struct_schema: Optional[Dict[str, AresSchemaEntry]] = None):
|
|
144
|
+
"""
|
|
145
|
+
Adds an analysis parameter that will be reported to ARES.
|
|
146
|
+
"""
|
|
147
|
+
self._service_wrapper._analysis_parameters[parameter_name] = ares_data_schema_utils.create_settings_schema_entry(parameter_type, optional, [], struct_schema)
|
|
@@ -30,7 +30,7 @@ if __name__ == "__main__":
|
|
|
30
30
|
device_name = "Demo Device"
|
|
31
31
|
description = "A device to demonstrate the PyAres device capabilities"
|
|
32
32
|
version = "1.0.0"
|
|
33
|
-
device_service = AresDeviceService(device.enter_safe_mode, device.get_device_state, device_name, description, version)
|
|
33
|
+
device_service = AresDeviceService(device.enter_safe_mode, device.get_device_state, device_name, description, version, False)
|
|
34
34
|
|
|
35
35
|
# Create the "Set Temperature" Command
|
|
36
36
|
quantity_schema = QuantitySchema(ureg.degree_Celsius, 40.0, 125.0)
|
|
@@ -40,7 +40,7 @@ if __name__ == "__main__":
|
|
|
40
40
|
device_service.add_new_command(set_temp_descriptor, device.set_temperature)
|
|
41
41
|
|
|
42
42
|
# Create the "Get Temperature" Command
|
|
43
|
-
output_schema = {"temperature": DeviceSchemaEntry(AresDataType.
|
|
43
|
+
output_schema = {"temperature": DeviceSchemaEntry(AresDataType.NUMBER, "The current temperature of the device")}
|
|
44
44
|
get_temp_desc = DeviceCommandDescriptor("Get Temperature", "Get's the current temperature of the demo device.", {}, output_schema)
|
|
45
45
|
device_service.add_new_command(get_temp_desc, device.get_temperature)
|
|
46
46
|
|
|
@@ -8,6 +8,14 @@ def plan(request: PlanRequest) -> PlanResponse:
|
|
|
8
8
|
gpdoods = []
|
|
9
9
|
names = []
|
|
10
10
|
|
|
11
|
+
for i in range(len(request.analysis_results)):
|
|
12
|
+
currentAnalysis = request.analysis_results[i]
|
|
13
|
+
|
|
14
|
+
for j in range(len(currentAnalysis.objectives)):
|
|
15
|
+
currentObjective : Objective = currentAnalysis.objectives[j]
|
|
16
|
+
print(f"Analysis Result {i}: Objective number {j} is named {currentObjective.objective_name} and has a value of {currentObjective.objective_value}")
|
|
17
|
+
|
|
18
|
+
|
|
11
19
|
for param in request.parameters:
|
|
12
20
|
if param.planner_name == "GPRDood":
|
|
13
21
|
gpdoods.append(param)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from PyAres import *
|
|
2
|
+
from typing import List
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
def generate_plans(request: PlanRequest) -> List[Plan]:
|
|
6
|
+
plans = []
|
|
7
|
+
for i in request.previous_plan_status_codes:
|
|
8
|
+
print(f"Status Code: {i}")
|
|
9
|
+
|
|
10
|
+
# Make as many plans as was requested
|
|
11
|
+
for i in range(request.batch_size):
|
|
12
|
+
current_params = []
|
|
13
|
+
for param in request.parameters:
|
|
14
|
+
# Simple Logic: Pick a random value within the allowed range
|
|
15
|
+
val = random.uniform(param.minimum_value, param.maximum_value)
|
|
16
|
+
current_params.append(PlannedParameter(param.name, val))
|
|
17
|
+
|
|
18
|
+
plans.append(Plan(current_params, Outcome.SUCCESS))
|
|
19
|
+
|
|
20
|
+
return plans
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
service = AresPlannerService(
|
|
24
|
+
generate_plans,
|
|
25
|
+
"Random Search Planner",
|
|
26
|
+
"This planner picks random values within bounds.",
|
|
27
|
+
"1.0.0"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Tell ARES we can plan for Numeric values
|
|
31
|
+
service.add_supported_type(AresDataType.NUMBER)
|
|
32
|
+
|
|
33
|
+
service.start()
|
|
@@ -2,7 +2,6 @@ import grpc
|
|
|
2
2
|
import inspect
|
|
3
3
|
import time
|
|
4
4
|
import warnings
|
|
5
|
-
from concurrent import futures
|
|
6
5
|
from typing import Dict, Callable, Awaitable, Union, Any, Optional
|
|
7
6
|
|
|
8
7
|
from ares_datamodel.device.remote import ares_remote_device_service_pb2 as device_service
|
|
@@ -16,13 +15,14 @@ from google.protobuf import empty_pb2
|
|
|
16
15
|
|
|
17
16
|
from .device_models import DeviceCommandDescriptor
|
|
18
17
|
from ..Models import Limits
|
|
19
|
-
from .device_models import DeviceCommandDescriptor, DeviceCommandResponse
|
|
18
|
+
from .device_models import DeviceCommandDescriptor, DeviceCommandResponse
|
|
20
19
|
from ..Utils import ares_device_command_utils
|
|
21
20
|
from ..Utils import ares_data_schema_utils
|
|
22
21
|
from ..Utils import ares_struct_utils
|
|
23
22
|
from ..Utils import ares_value_utils
|
|
24
23
|
from ..Utils import ares_data_type_utils
|
|
25
24
|
from ..Utils import device_status_code_utils
|
|
25
|
+
from ..Utils.ares_service_base import AresServiceWrapperBase, AresBaseService
|
|
26
26
|
|
|
27
27
|
# Type hint for the user's custom methods
|
|
28
28
|
EnterSafeModeMethod = Callable[[], None]
|
|
@@ -30,15 +30,19 @@ AllowedReturns = Union[DeviceCommandResponse, Dict[str, Any], Any]
|
|
|
30
30
|
DeviceCommandMethod = Callable[..., AllowedReturns]
|
|
31
31
|
DeviceStateMethod = Callable[[], Dict[str, Any]]
|
|
32
32
|
|
|
33
|
-
class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServicer):
|
|
33
|
+
class AresDeviceServiceWrapper(AresServiceWrapperBase, device_service_grpc.AresRemoteDeviceServiceServicer):
|
|
34
34
|
"""
|
|
35
35
|
A wrapper around the gRPC service to expose native Python objects for devices
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
-
def __init__(self, device_name: str, description: str, version: str, enter_safe_mode: EnterSafeModeMethod, update_device_state: DeviceStateMethod):
|
|
38
|
+
def __init__(self, device_name: str, description: str, version: str, timeout: int, enter_safe_mode: EnterSafeModeMethod, update_device_state: DeviceStateMethod):
|
|
39
|
+
super().__init__(service_name=device_name, version=version, description=description, timeout=timeout)
|
|
40
|
+
|
|
41
|
+
# Preserve public attributes for backwards compatibility
|
|
39
42
|
self.device_name = device_name
|
|
40
43
|
self.description = description
|
|
41
44
|
self.version = version
|
|
45
|
+
|
|
42
46
|
self._enter_safe_mode = enter_safe_mode
|
|
43
47
|
self._update_device_state = update_device_state
|
|
44
48
|
self._setting_schema: Dict[str, ares_data_schema_pb2.AresValueSchema] = {}
|
|
@@ -48,13 +52,16 @@ class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServic
|
|
|
48
52
|
self._command_methods: Dict[str, Callable] = {}
|
|
49
53
|
|
|
50
54
|
def GetOperationalStatus(self, request, context) -> device_status_pb2.DeviceOperationalStatus:
|
|
51
|
-
return device_status_pb2.DeviceOperationalStatus(
|
|
55
|
+
return device_status_pb2.DeviceOperationalStatus(
|
|
56
|
+
operational_state=device_status_pb2.OperationalState.ACTIVE,
|
|
57
|
+
message=f"{self._service_name} is active!"
|
|
58
|
+
)
|
|
52
59
|
|
|
53
60
|
def GetInfo(self, request, context) -> device_service.DeviceInfoResponse:
|
|
54
61
|
info = device_service.DeviceInfoResponse()
|
|
55
|
-
info.name = self.
|
|
56
|
-
info.description = self.
|
|
57
|
-
info.version = self.
|
|
62
|
+
info.name = self._service_name
|
|
63
|
+
info.description = self._description
|
|
64
|
+
info.version = self._version
|
|
58
65
|
return info
|
|
59
66
|
|
|
60
67
|
def GetCommands(self, request, context) -> device_service.CommandsResponse:
|
|
@@ -249,7 +256,7 @@ class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServic
|
|
|
249
256
|
except grpc.RpcError as e:
|
|
250
257
|
print(f"gRPC error occured in device state stream")
|
|
251
258
|
|
|
252
|
-
class AresDeviceService:
|
|
259
|
+
class AresDeviceService(AresBaseService):
|
|
253
260
|
""" Manages the gRPC service for the AresDeviceSerivce """
|
|
254
261
|
def __init__(self,
|
|
255
262
|
enter_safe_mode_logic: EnterSafeModeMethod,
|
|
@@ -257,8 +264,10 @@ class AresDeviceService:
|
|
|
257
264
|
device_name: str,
|
|
258
265
|
description: str,
|
|
259
266
|
version: str,
|
|
267
|
+
timeout: int = 30,
|
|
260
268
|
use_localhost: bool = True,
|
|
261
|
-
port: int = 7100
|
|
269
|
+
port: int = 7100,
|
|
270
|
+
max_message_size: int = -1):
|
|
262
271
|
"""
|
|
263
272
|
Initializes the AresDeviceService
|
|
264
273
|
|
|
@@ -270,22 +279,28 @@ class AresDeviceService:
|
|
|
270
279
|
device_name (str): The name description of your device.
|
|
271
280
|
description (str): A brief description of your device.
|
|
272
281
|
version (str): The version associated with your device implementation.
|
|
282
|
+
timeout (int): Timeout in seconds for service calls reported to ARES.
|
|
273
283
|
use_localhost (bool): An optional value that allows the user to specify whether to host the service on the local network. Defaults to True.
|
|
274
284
|
port (int): The port that your device service will serve on. Defaults to port 7100.
|
|
285
|
+
max_message_size (int): Maximum message size for gRPC. Defaults to -1 (use library default).
|
|
275
286
|
"""
|
|
276
287
|
|
|
288
|
+
super().__init__(
|
|
289
|
+
service_name=device_name,
|
|
290
|
+
description=description,
|
|
291
|
+
version=version,
|
|
292
|
+
port=port,
|
|
293
|
+
use_localhost=use_localhost,
|
|
294
|
+
max_message_size=max_message_size
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Preserve these public attributes for backwards compatibility
|
|
277
298
|
self.device_name = device_name
|
|
278
299
|
self.description = description
|
|
279
300
|
self.version = version
|
|
280
301
|
|
|
281
|
-
self.
|
|
282
|
-
self.
|
|
283
|
-
self._service_wrapper = AresDeviceServiceWrapper(device_name, description, version, enter_safe_mode_logic, get_device_state_logic)
|
|
284
|
-
device_service_grpc.add_AresRemoteDeviceServiceServicer_to_server(self._service_wrapper, self._server)
|
|
285
|
-
if(use_localhost):
|
|
286
|
-
self._server.add_insecure_port(f'localhost:{self._port}')
|
|
287
|
-
else:
|
|
288
|
-
self._server.add_insecure_port(f'[::]:{self._port}')
|
|
302
|
+
self._service_wrapper = AresDeviceServiceWrapper(device_name, description, version, timeout, enter_safe_mode_logic, get_device_state_logic)
|
|
303
|
+
device_service_grpc.add_AresRemoteDeviceServiceServicer_to_server(self._service_wrapper, self.get_server())
|
|
289
304
|
|
|
290
305
|
def add_new_command(self, cmd_descriptor: DeviceCommandDescriptor, method):
|
|
291
306
|
"""
|
|
@@ -345,12 +360,13 @@ class AresDeviceService:
|
|
|
345
360
|
"""
|
|
346
361
|
|
|
347
362
|
print(f"Starting Ares Device Service on port {self._port}...")
|
|
348
|
-
self.
|
|
363
|
+
server = self.get_server()
|
|
364
|
+
server.start()
|
|
349
365
|
|
|
350
366
|
if wait_for_termination:
|
|
351
|
-
|
|
367
|
+
server.wait_for_termination()
|
|
352
368
|
|
|
353
369
|
def stop(self):
|
|
354
370
|
""" Stops the service, terminating the connection. """
|
|
355
371
|
print("Stopping Ares Device Service...")
|
|
356
|
-
self.
|
|
372
|
+
self.get_server().stop(0).wait()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .ares_data_models import AresDataType, Outcome, RequestMetadata, AresSchemaEntry, Quantity, QuantitySchema, Limits
|
|
1
|
+
from .ares_data_models import AresDataType, Outcome, RequestMetadata, AresSchemaEntry, Quantity, QuantitySchema, Limits, PlanStatusCode
|
|
2
2
|
|
|
3
3
|
__all__ = [
|
|
4
4
|
"AresDataType",
|
|
@@ -7,5 +7,6 @@ __all__ = [
|
|
|
7
7
|
"AresSchemaEntry",
|
|
8
8
|
"Quantity",
|
|
9
9
|
"QuantitySchema",
|
|
10
|
-
"Limits"
|
|
10
|
+
"Limits",
|
|
11
|
+
"PlanStatusCode"
|
|
11
12
|
]
|
|
@@ -30,6 +30,12 @@ class Outcome(Enum):
|
|
|
30
30
|
WARNING = 3
|
|
31
31
|
CANCELED = 4
|
|
32
32
|
|
|
33
|
+
class PlanStatusCode(Enum):
|
|
34
|
+
PLAN_STATUS_UNSPECIFIED = 0
|
|
35
|
+
PLAN_ACCEPTED = 1
|
|
36
|
+
PLAN_UNACHIEVABLE = 2
|
|
37
|
+
PLAN_FAILED = 3
|
|
38
|
+
|
|
33
39
|
class RequestMetadata():
|
|
34
40
|
def __init__(self, proto_metadata: request_metadata_pb2.RequestMetadata):
|
|
35
41
|
self.system_name = proto_metadata.system_name
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
from .planner_models import PlanningParameter, PlanRequest, PlanResponse, ParameterHistoryItem
|
|
1
|
+
from .planner_models import PlanningParameter, PlanRequest, PlanResponse, ParameterHistoryItem, Plan, PlannedParameter, ObjectiveStatus
|
|
2
2
|
from .planning_service import AresPlannerService
|
|
3
3
|
|
|
4
4
|
__all__ = [
|
|
5
5
|
"PlanningParameter",
|
|
6
6
|
"PlanRequest",
|
|
7
7
|
"PlanResponse",
|
|
8
|
-
"AresPlannerService"
|
|
8
|
+
"AresPlannerService",
|
|
9
|
+
"Plan",
|
|
10
|
+
"PlannedParameter",
|
|
11
|
+
"ParameterHistoryItem",
|
|
12
|
+
"ObjectiveStatus"
|
|
9
13
|
]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Dict, Any, List, Sequence, Optional
|
|
2
|
-
from ..Models import Outcome, AresDataType, RequestMetadata
|
|
2
|
+
from ..Models import Outcome, AresDataType, RequestMetadata, PlanStatusCode
|
|
3
|
+
from enum import Enum
|
|
3
4
|
|
|
4
5
|
class ParameterHistoryItem:
|
|
5
6
|
""" Represents a single historical parameter item """
|
|
@@ -20,6 +21,13 @@ class ParameterHistoryItem:
|
|
|
20
21
|
|
|
21
22
|
def __repr__(self) -> str:
|
|
22
23
|
return self.__str__()
|
|
24
|
+
|
|
25
|
+
class ObjectiveStatus(Enum):
|
|
26
|
+
""" An enum representing the current status of the objective the planner is trying to achieve (if any) """
|
|
27
|
+
OBJECTIVE_STATUS_UNSPECIFIED = 0
|
|
28
|
+
OBJECTIVE_UNACHIEVED = 1
|
|
29
|
+
OBJECTIVE_ACHIEVED = 2
|
|
30
|
+
OBJECTIVE_FAILED = 3
|
|
23
31
|
|
|
24
32
|
class PlanningParameter:
|
|
25
33
|
"""
|
|
@@ -28,9 +36,15 @@ class PlanningParameter:
|
|
|
28
36
|
Designed to provide a more user-friendly abstraction for the user to interact
|
|
29
37
|
with planning parameters through.
|
|
30
38
|
"""
|
|
31
|
-
def __init__(self, name: str,
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
def __init__(self, name: str,
|
|
40
|
+
data_type: AresDataType,
|
|
41
|
+
minimum_value: float = None,
|
|
42
|
+
maximum_value: float = None,
|
|
43
|
+
param_history: list[ParameterHistoryItem] = [],
|
|
44
|
+
is_planned: bool = False,
|
|
45
|
+
is_result: bool = False,
|
|
46
|
+
planner_name: str = "",
|
|
47
|
+
initial_value = None):
|
|
34
48
|
"""
|
|
35
49
|
Initializes a PlanningParameter.
|
|
36
50
|
|
|
@@ -107,7 +121,13 @@ class PlanRequest:
|
|
|
107
121
|
|
|
108
122
|
Designed to provide a more user-friendly abstraction for interacting with a plan request message.
|
|
109
123
|
"""
|
|
110
|
-
def __init__(self,
|
|
124
|
+
def __init__(self,
|
|
125
|
+
parameters: list[PlanningParameter],
|
|
126
|
+
settings: Dict[str, Any],
|
|
127
|
+
analysis_results: Sequence[float],
|
|
128
|
+
metadata: RequestMetadata = RequestMetadata.from_default_values(),
|
|
129
|
+
batch_size: int = 1,
|
|
130
|
+
previous_plan_status_codes: List[PlanStatusCode] = None):
|
|
111
131
|
"""
|
|
112
132
|
Initializes a PlanRequest.
|
|
113
133
|
|
|
@@ -117,8 +137,14 @@ class PlanRequest:
|
|
|
117
137
|
self.parameters = parameters
|
|
118
138
|
self.settings = settings
|
|
119
139
|
self.analysis_results = analysis_results
|
|
140
|
+
self.batch_size = batch_size
|
|
120
141
|
self.request_metadata = metadata
|
|
121
142
|
|
|
143
|
+
if previous_plan_status_codes is None:
|
|
144
|
+
self.previous_plan_status_codes = []
|
|
145
|
+
else:
|
|
146
|
+
self.previous_plan_status_codes = previous_plan_status_codes
|
|
147
|
+
|
|
122
148
|
def __str__(self) -> str:
|
|
123
149
|
param_str = "\n ".join(self.parameter_names)
|
|
124
150
|
settings_str = "\n ".join([f"{k}: {v}" for k, v in self.settings.items()])
|
|
@@ -127,13 +153,15 @@ class PlanRequest:
|
|
|
127
153
|
metadata_str = str(self.request_metadata).replace('\n', '\n ')
|
|
128
154
|
return (f"PlanRequest object with:\n"
|
|
129
155
|
f"parameters:\n"
|
|
130
|
-
f"
|
|
156
|
+
f"{param_str}\n"
|
|
131
157
|
f"settings:\n"
|
|
132
|
-
f"
|
|
158
|
+
f"{settings_str}\n"
|
|
133
159
|
f"analysis_results:\n"
|
|
134
|
-
f"
|
|
160
|
+
f"{analysis_str}\n"
|
|
135
161
|
f"request_metadata:\n"
|
|
136
|
-
f"{metadata_str}"
|
|
162
|
+
f"{metadata_str}"
|
|
163
|
+
f"batch_size:\n"
|
|
164
|
+
f"{self.batch_size}")
|
|
137
165
|
|
|
138
166
|
def __repr__(self) -> str:
|
|
139
167
|
return self.__str__()
|
|
@@ -167,7 +195,8 @@ class PlanResponse:
|
|
|
167
195
|
parameter_values: Optional[list] = None,
|
|
168
196
|
parameter_data: Optional[dict[str,Any]] = None,
|
|
169
197
|
outcome: Outcome = Outcome.SUCCESS,
|
|
170
|
-
error_string: str = ""
|
|
198
|
+
error_string: str = "",
|
|
199
|
+
objective_status: ObjectiveStatus = ObjectiveStatus.OBJECTIVE_STATUS_UNSPECIFIED):
|
|
171
200
|
"""
|
|
172
201
|
Initializes a PlanResponse. Using either lists of names and values or a python dictonary of name:value pairs
|
|
173
202
|
|
|
@@ -175,6 +204,9 @@ class PlanResponse:
|
|
|
175
204
|
parameter_names: A list of names associated with planned parameters.
|
|
176
205
|
parameter_values: A list of values associated with planned parameters.
|
|
177
206
|
parameter_data: A python dictionary of key:value pairs of planned parameters and planned values
|
|
207
|
+
outcome: An enum of type Outcome that determines whether the planning process succeeded or not, defaults to SUCCESS
|
|
208
|
+
error_string: An optional string for specifying planning failure reasons to be relayed to ARES
|
|
209
|
+
objective_status: An optional value that specifies the status of the objective your planner is trying to achieve (if any)
|
|
178
210
|
"""
|
|
179
211
|
if parameter_data is not None:
|
|
180
212
|
self.parameter_names = list(parameter_data.keys())
|
|
@@ -191,13 +223,55 @@ class PlanResponse:
|
|
|
191
223
|
|
|
192
224
|
self.outcome = outcome
|
|
193
225
|
self.error_string = error_string
|
|
226
|
+
self.objective_status = objective_status
|
|
194
227
|
|
|
195
228
|
def __str__(self):
|
|
196
229
|
return (f"PlanResponse object with:\n"
|
|
197
230
|
f" outcome: {self.outcome}\n"
|
|
198
231
|
f" parameter_names: {self.parameter_names}\n"
|
|
199
232
|
f" parameter_values: {self.parameter_values}\n"
|
|
200
|
-
f" error_string: {self.error_string}\n"
|
|
233
|
+
f" error_string: {self.error_string}\n"
|
|
234
|
+
f" objective_status: {self.objective_status}\n")
|
|
201
235
|
|
|
202
236
|
def __repr__(self) -> str:
|
|
203
|
-
return self.__str__()
|
|
237
|
+
return self.__str__()
|
|
238
|
+
|
|
239
|
+
class PlannedParameter:
|
|
240
|
+
def __init__(self, parameter_name: str, parameter_value: Any):
|
|
241
|
+
self.parameter_name = parameter_name
|
|
242
|
+
self.parameter_value = parameter_value
|
|
243
|
+
|
|
244
|
+
def __str__(self):
|
|
245
|
+
# A clean, readable key-value output
|
|
246
|
+
return f"{self.parameter_name}: {self.parameter_value}"
|
|
247
|
+
|
|
248
|
+
def __repr__(self):
|
|
249
|
+
# The !r formatting flag automatically wraps strings in quotes and calls __repr__ on the values
|
|
250
|
+
return f"PlannedParameters(parameter_name={self.parameter_name!r}, parameter_value={self.parameter_value!r})"
|
|
251
|
+
|
|
252
|
+
class Plan:
|
|
253
|
+
def __init__(self,
|
|
254
|
+
planned_parameters: List[PlannedParameter],
|
|
255
|
+
outcome: Outcome,
|
|
256
|
+
error_string: str = "",
|
|
257
|
+
objective_status: ObjectiveStatus = ObjectiveStatus.OBJECTIVE_STATUS_UNSPECIFIED):
|
|
258
|
+
self.planned_parameters = planned_parameters
|
|
259
|
+
self.outcome = outcome
|
|
260
|
+
self.error_string = error_string
|
|
261
|
+
self.objective_status = objective_status
|
|
262
|
+
|
|
263
|
+
def __str__(self):
|
|
264
|
+
base_str = f"Plan (Outcome: {self.outcome}) \n (Objective Status: {self.objective_status})"
|
|
265
|
+
|
|
266
|
+
# Format the list of parameters into a readable string
|
|
267
|
+
if self.planned_parameters:
|
|
268
|
+
params_str = ", ".join(str(p) for p in self.planned_parameters)
|
|
269
|
+
base_str += f" | Parameters: [{params_str}]"
|
|
270
|
+
|
|
271
|
+
if self.error_string:
|
|
272
|
+
base_str += f" - Error: '{self.error_string}'"
|
|
273
|
+
|
|
274
|
+
return base_str
|
|
275
|
+
|
|
276
|
+
def __repr__(self):
|
|
277
|
+
return f"Plan(planned_parameters={self.planned_parameters!r}, outcome={self.outcome!r}, error_string={self.error_string!r})"
|