PyAres 0.3.3__tar.gz → 1.0.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-0.3.3 → pyares-1.0.0}/.gitignore +6 -0
- {pyares-0.3.3 → pyares-1.0.0}/PKG-INFO +10 -10
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Analyzing/__init__.py +2 -2
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Analyzing/analysis_service.py +42 -13
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Analyzing/analyzer_models.py +21 -2
- pyares-1.0.0/PyAres/Demo/Analyzers/airship_analyzer.py +20 -0
- {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Analyzers}/analyzer_test.py +15 -14
- {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Analyzers}/analyzer_wiki.py +10 -6
- {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Devices}/device_test.py +9 -7
- pyares-1.0.0/PyAres/Demo/Devices/failure_test_device.py +66 -0
- {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Devices}/hotplate.py +18 -6
- pyares-1.0.0/PyAres/Demo/Devices/random_number_device.py +70 -0
- pyares-1.0.0/PyAres/Demo/Devices/rotary_mixer.py +50 -0
- pyares-1.0.0/PyAres/Demo/Planners/airship_planner.py +187 -0
- {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Planners}/planner_test.py +7 -7
- pyares-1.0.0/PyAres/Device/__init__.py +5 -0
- pyares-1.0.0/PyAres/Device/device_models.py +78 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Device/device_service.py +67 -14
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Models/__init__.py +5 -2
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Models/ares_data_models.py +41 -1
- pyares-1.0.0/PyAres/Planning/planner_models.py +203 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Planning/planning_service.py +25 -13
- pyares-1.0.0/PyAres/Utils/ares_data_schema_utils.py +73 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/ares_data_type_utils.py +12 -1
- pyares-1.0.0/PyAres/Utils/ares_device_command_utils.py +65 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/ares_struct_utils.py +59 -6
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/ares_value_utils.py +116 -18
- pyares-1.0.0/PyAres/Utils/device_status_code_utils.py +19 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/__init__.py +8 -2
- {pyares-0.3.3 → pyares-1.0.0}/README.md +8 -6
- {pyares-0.3.3 → pyares-1.0.0}/pyproject.toml +3 -5
- {pyares-0.3.3 → pyares-1.0.0}/tests/test_analyzer.py +5 -5
- {pyares-0.3.3 → pyares-1.0.0}/tests/test_ares_data_schema_utils.py +15 -4
- {pyares-0.3.3 → pyares-1.0.0}/tests/test_ares_data_type_utils.py +13 -1
- {pyares-0.3.3 → pyares-1.0.0}/tests/test_ares_device.py +18 -2
- {pyares-0.3.3 → pyares-1.0.0}/tests/test_ares_struct_utils.py +19 -1
- {pyares-0.3.3 → pyares-1.0.0}/tests/test_ares_value_type_conversions.py +28 -1
- pyares-0.3.3/PyAres/Demo/rotary_mixer.py +0 -37
- pyares-0.3.3/PyAres/Device/__init__.py +0 -3
- pyares-0.3.3/PyAres/Device/device_models.py +0 -40
- pyares-0.3.3/PyAres/Planning/planner_models.py +0 -101
- pyares-0.3.3/PyAres/Utils/ares_data_schema_utils.py +0 -49
- pyares-0.3.3/PyAres/Utils/ares_device_command_utils.py +0 -43
- {pyares-0.3.3 → pyares-1.0.0}/.github/dependabot.yml +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/.github/workflows/pypi.yaml +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/.github/workflows/python-tests.yaml +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/LICENSE +0 -0
- {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Analyzers}/analyzer_test_tools_demo.py +0 -0
- {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Planners}/planner_test_tools_demo.py +0 -0
- {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Planners}/planner_wiki.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Device/device_warnings.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Planning/__init__.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/ares_outcome_utils.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/param_history_info_utils.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/plan_request_utils.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/plan_response_utils.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/planning_param_utils.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/test_tools/__init__.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/test_tools/analyzer_tester.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/PyAres/test_tools/planner_tester.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/setup.cfg +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/setup.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/tests/__init__.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/tests/mock_grpc_context.py +0 -0
- {pyares-0.3.3 → pyares-1.0.0}/tests/test_planner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyAres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: The official Python library for ARES.
|
|
5
5
|
Project-URL: Homepage, https://github.com/AFRL-ARES/PyAres
|
|
6
6
|
Author-email: AFRL <AFRL.RX.AREScontact@us.af.mil>
|
|
@@ -9,13 +9,13 @@ License-File: LICENSE
|
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
|
-
Requires-Dist: ares-datamodel>=0.
|
|
13
|
-
Requires-Dist: grpcio-tools>=1.75.1
|
|
14
|
-
Requires-Dist: grpcio<2.0.0,>=1.71.0
|
|
12
|
+
Requires-Dist: ares-datamodel>=0.29.0
|
|
15
13
|
Description-Content-Type: text/markdown
|
|
16
14
|
|
|
17
15
|
# PyAres
|
|
18
|
-
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.
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
You can find additional documentation on PyAres as well as the rest of the ARES ecosystem by visting https://afrl-ares.github.io/ to browse our centralized documentation site.
|
|
19
19
|
|
|
20
20
|
### ✨ Features
|
|
21
21
|
* A Pythonic API built on the performance of Protobuf and gRPC to streamline the creation of self-driving lab components
|
|
@@ -73,10 +73,10 @@ Analyzers can be initialized using the AresAnalyzerService class. Below is a bas
|
|
|
73
73
|
```Python
|
|
74
74
|
from PyAres import AresAnalyzerService
|
|
75
75
|
from PyAres import AnalysisRequest
|
|
76
|
-
from PyAres import
|
|
76
|
+
from PyAres import AnalysisResponse
|
|
77
77
|
from PyAres import AresDataType
|
|
78
78
|
|
|
79
|
-
def analyze(request: AnalysisRequest) ->
|
|
79
|
+
def analyze(request: AnalysisRequest) -> AnalysisResponse:
|
|
80
80
|
#Custom Analysis Logic
|
|
81
81
|
growth = request.inputs.get("Growth")
|
|
82
82
|
temperature = request.inputs.get("Temperature")
|
|
@@ -84,7 +84,7 @@ def analyze(request: AnalysisRequest) -> Analysis:
|
|
|
84
84
|
print(f"Growth: {growth}")
|
|
85
85
|
print(f"Temperature: {temperature}")
|
|
86
86
|
|
|
87
|
-
analysis =
|
|
87
|
+
analysis = AnalysisResponse(result=growth, success=True)
|
|
88
88
|
return analysis
|
|
89
89
|
|
|
90
90
|
|
|
@@ -119,8 +119,8 @@ class DemoDevice:
|
|
|
119
119
|
return {}
|
|
120
120
|
|
|
121
121
|
def get_temperature(self):
|
|
122
|
-
#
|
|
123
|
-
return
|
|
122
|
+
# Data type should match the one specified in your output schema
|
|
123
|
+
return self.temperature
|
|
124
124
|
|
|
125
125
|
def get_device_state(self):
|
|
126
126
|
state_dictionary = { "temperature": self.temperature }
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from .analysis_service import AresAnalyzerService
|
|
2
|
-
from .analyzer_models import
|
|
2
|
+
from .analyzer_models import AnalysisResponse, AnalysisRequest, InfoResponse
|
|
3
3
|
|
|
4
4
|
__all__ = [
|
|
5
|
-
"
|
|
5
|
+
"AnalysisResponse",
|
|
6
6
|
"AnalysisRequest",
|
|
7
7
|
"InfoResponse",
|
|
8
8
|
"AresAnalyzerService",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Standard Imports
|
|
2
2
|
import grpc
|
|
3
3
|
from concurrent import futures
|
|
4
|
-
from typing import Callable, Awaitable, Union, Mapping, Dict, Optional
|
|
4
|
+
from typing import Callable, Awaitable, Union, Mapping, Dict, Optional, Any
|
|
5
5
|
|
|
6
6
|
# Import generated protobuf and gRPC stubs
|
|
7
7
|
from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2 as analyzer_service
|
|
@@ -11,7 +11,6 @@ from ares_datamodel.analyzing import analyzer_capabilities_pb2
|
|
|
11
11
|
from ares_datamodel.connection import connection_state_pb2
|
|
12
12
|
from ares_datamodel.connection import connection_status_pb2
|
|
13
13
|
from ares_datamodel.connection import connection_info_pb2
|
|
14
|
-
from ares_datamodel import ares_data_type_pb2
|
|
15
14
|
from ares_datamodel import ares_data_schema_pb2
|
|
16
15
|
from ares_datamodel import ares_outcome_enum_pb2
|
|
17
16
|
|
|
@@ -19,14 +18,14 @@ from ares_datamodel import ares_outcome_enum_pb2
|
|
|
19
18
|
from ..Utils import ares_struct_utils
|
|
20
19
|
from ..Utils import ares_data_schema_utils
|
|
21
20
|
from ..Utils import ares_outcome_utils
|
|
21
|
+
from ..Utils import ares_value_utils
|
|
22
22
|
|
|
23
23
|
# Import python models
|
|
24
|
-
from ..Models import ares_data_models, RequestMetadata
|
|
25
|
-
from
|
|
26
|
-
from .analyzer_models import AnalysisRequest, Analysis, InfoResponse
|
|
24
|
+
from ..Models import ares_data_models, RequestMetadata, Limits, AresSchemaEntry
|
|
25
|
+
from .analyzer_models import AnalysisRequest, AnalysisResponse, InfoResponse
|
|
27
26
|
|
|
28
27
|
# Type hints for the user's custom logic
|
|
29
|
-
AnalyzeLogicFunction = Callable[[AnalysisRequest], Union[
|
|
28
|
+
AnalyzeLogicFunction = Callable[[AnalysisRequest], Union[AnalysisResponse, Awaitable[AnalysisResponse]]]
|
|
30
29
|
|
|
31
30
|
class AresAnalyzerServiceWrapper(analyzer_service_grpc.AresRemoteAnalyzerServiceServicer):
|
|
32
31
|
"""
|
|
@@ -36,8 +35,8 @@ class AresAnalyzerServiceWrapper(analyzer_service_grpc.AresRemoteAnalyzerService
|
|
|
36
35
|
self._info = info
|
|
37
36
|
self._timeout = timeout
|
|
38
37
|
self._custom_analysis_logic = custom_analysis_logic
|
|
39
|
-
self._settings: Dict[str, ares_data_schema_pb2.
|
|
40
|
-
self._analysis_parameters: Dict[str, ares_data_schema_pb2.
|
|
38
|
+
self._settings: Dict[str, ares_data_schema_pb2.AresValueSchema] = {}
|
|
39
|
+
self._analysis_parameters: Dict[str, ares_data_schema_pb2.AresValueSchema] = {}
|
|
41
40
|
|
|
42
41
|
def GetInfo(self, request, context) -> connection_info_pb2.InfoResponse:
|
|
43
42
|
print("Info Requested!")
|
|
@@ -73,7 +72,7 @@ class AresAnalyzerServiceWrapper(analyzer_service_grpc.AresRemoteAnalyzerService
|
|
|
73
72
|
if isinstance(python_response, Awaitable):
|
|
74
73
|
python_response = python_response.__await__()
|
|
75
74
|
|
|
76
|
-
if not isinstance(python_response,
|
|
75
|
+
if not isinstance(python_response, AnalysisResponse):
|
|
77
76
|
print("Analysis response was an invalid type, ")
|
|
78
77
|
proto_analysis.analysis_outcome = ares_outcome_enum_pb2.FAILURE
|
|
79
78
|
proto_analysis.error_string = "The user's custom analysis logic returned an invalid type, analysis cannot be processed"
|
|
@@ -138,7 +137,7 @@ class AresAnalyzerServiceWrapper(analyzer_service_grpc.AresRemoteAnalyzerService
|
|
|
138
137
|
|
|
139
138
|
def ValidateInputs(self, request: analyzer_service.ParameterValidationRequest, context):
|
|
140
139
|
response = analyzer_service.ParameterValidationResult(success=True)
|
|
141
|
-
provided_params: Mapping[str, ares_data_schema_pb2.
|
|
140
|
+
provided_params: Mapping[str, ares_data_schema_pb2.AresValueSchema] = request.input_schema.fields
|
|
142
141
|
|
|
143
142
|
for stored_key, stored_schema in self._analysis_parameters.items():
|
|
144
143
|
if stored_key in provided_params:
|
|
@@ -200,7 +199,15 @@ class AresAnalyzerService:
|
|
|
200
199
|
else:
|
|
201
200
|
self._server.add_insecure_port(f'[::]:{self._port}')
|
|
202
201
|
|
|
203
|
-
def add_setting(self,
|
|
202
|
+
def add_setting(self,
|
|
203
|
+
setting_name: str,
|
|
204
|
+
setting_type: ares_data_models.AresDataType,
|
|
205
|
+
default_value: Any = None,
|
|
206
|
+
optional: bool = True,
|
|
207
|
+
constraints: Union[list[int], list[str], list[float]] = [],
|
|
208
|
+
struct_schema: Optional[Dict[str, AresSchemaEntry]] = None,
|
|
209
|
+
limits: Optional[Limits] = None,
|
|
210
|
+
description: Optional[str] = None):
|
|
204
211
|
"""
|
|
205
212
|
Adds an analyzer setting to be reported to ARES when capabilities are requested.
|
|
206
213
|
While most `PyAres.Models.AresDataType` options are supported, bool arrays and byte arrays
|
|
@@ -212,8 +219,30 @@ class AresAnalyzerService:
|
|
|
212
219
|
optional (bool): Whether the setting is optional.
|
|
213
220
|
constraints: An optional list of values to constrain the available setting choices. Can be integers, strings, or floats.
|
|
214
221
|
struct_schema: An optional dictionary defining the fields of a STRUCT type setting, using AresSchemaEntry objects.
|
|
222
|
+
limits: An optional limits object used to specify minimum and maximum values for a setting
|
|
223
|
+
description: An optional string to describe your setting in more detail. Appears in ARES as a tooltip in the settings menu.
|
|
215
224
|
"""
|
|
216
|
-
|
|
225
|
+
try:
|
|
226
|
+
if default_value is not None:
|
|
227
|
+
default_ares_value = ares_value_utils.create_ares_value(default_value)
|
|
228
|
+
self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type=setting_type,
|
|
229
|
+
optional=optional,
|
|
230
|
+
choices=constraints,
|
|
231
|
+
struct_schema=struct_schema,
|
|
232
|
+
limits=limits,
|
|
233
|
+
default_value=default_ares_value,
|
|
234
|
+
description=description)
|
|
235
|
+
|
|
236
|
+
else:
|
|
237
|
+
self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type=setting_type,
|
|
238
|
+
optional=optional,
|
|
239
|
+
choices=constraints,
|
|
240
|
+
struct_schema=struct_schema,
|
|
241
|
+
limits=limits,
|
|
242
|
+
description=description)
|
|
243
|
+
|
|
244
|
+
except Exception as e:
|
|
245
|
+
print(f"Encountered an exception while adding setting {setting_name}: {e}")
|
|
217
246
|
|
|
218
247
|
def add_analysis_parameter(self, parameter_name: str, parameter_type: ares_data_models.AresDataType, optional: bool = False, struct_schema: Optional[Dict[str, AresSchemaEntry]] = None):
|
|
219
248
|
"""
|
|
@@ -247,7 +276,7 @@ class AresAnalyzerService:
|
|
|
247
276
|
Setting this value to false will allow you to continue execution after starting your service, however this should ONLY be done if you have
|
|
248
277
|
another mechanism for keeping your process alive (such as a GUI, or a loop). Defaults to true.
|
|
249
278
|
"""
|
|
250
|
-
print(f"Starting Ares
|
|
279
|
+
print(f"Starting Ares Analysis Service on port {self._port}...")
|
|
251
280
|
self._server.start()
|
|
252
281
|
|
|
253
282
|
if wait_for_termination:
|
|
@@ -9,7 +9,18 @@ class AnalysisRequest:
|
|
|
9
9
|
self.settings = settings
|
|
10
10
|
self.request_metadata = metadata
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
def __str__(self) -> str:
|
|
13
|
+
"""Returns a string representation of the AnalysisRequest with all information organized."""
|
|
14
|
+
metadata_str = str(self.request_metadata).replace('\n', '\n\t')
|
|
15
|
+
return (f"AnalysisRequest object with:\n"
|
|
16
|
+
f" inputs: {self.inputs}\n"
|
|
17
|
+
f" settings: {self.settings}\n"
|
|
18
|
+
f" metadata: {metadata_str}")
|
|
19
|
+
|
|
20
|
+
def __repr__(self) -> str:
|
|
21
|
+
return self.__str__()
|
|
22
|
+
|
|
23
|
+
class AnalysisResponse:
|
|
13
24
|
""" Represents the result of an analysis process. """
|
|
14
25
|
|
|
15
26
|
def __init__(self, result: float, outcome: Outcome = Outcome.SUCCESS, error_string: str = ""):
|
|
@@ -24,7 +35,15 @@ class Analysis:
|
|
|
24
35
|
self.result = result
|
|
25
36
|
self.outcome = outcome
|
|
26
37
|
self.error_string = error_string
|
|
27
|
-
|
|
38
|
+
|
|
39
|
+
def __str__(self) -> str:
|
|
40
|
+
return (f"Analysis object with:\n"
|
|
41
|
+
f" result: {self.result}\n"
|
|
42
|
+
f" outcome: {self.outcome}\n"
|
|
43
|
+
f" error_string: {self.error_string}")
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
return self.__str__()
|
|
28
47
|
|
|
29
48
|
class InfoResponse:
|
|
30
49
|
""" A response message that provides basic information about your analyzer. """
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from PyAres import AresAnalyzerService, AnalysisRequest, Analysis, AresDataType, Outcome
|
|
2
|
+
|
|
3
|
+
def analysis_logic(request: AnalysisRequest):
|
|
4
|
+
result = int(request.inputs["ShotOutcome"])
|
|
5
|
+
print(f"Analyzing, result is: {result}")
|
|
6
|
+
response = Analysis(result=result, outcome=Outcome.SUCCESS)
|
|
7
|
+
return response
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
print("Welcome to the Airship Analyzer")
|
|
11
|
+
|
|
12
|
+
analyzer = AresAnalyzerService(
|
|
13
|
+
custom_analysis_logic=analysis_logic,
|
|
14
|
+
description="An analyzer for use with the Airship Device",
|
|
15
|
+
name="Airship Analyzer",
|
|
16
|
+
version="1.0.0")
|
|
17
|
+
|
|
18
|
+
analyzer.add_analysis_parameter("ShotOutcome", AresDataType.NUMBER)
|
|
19
|
+
|
|
20
|
+
analyzer.start()
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
from PyAres import AresAnalyzerService, AnalysisRequest,
|
|
1
|
+
from PyAres import AresAnalyzerService, AnalysisRequest, AnalysisResponse, AresDataType, Outcome, Limits
|
|
2
2
|
|
|
3
|
-
def analyze(request: AnalysisRequest) ->
|
|
3
|
+
def analyze(request: AnalysisRequest) -> AnalysisResponse:
|
|
4
4
|
#Custom Analysis Logic
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
temp_one = request.inputs.get("Temperature One")
|
|
6
|
+
print("Processing Analysis Request")
|
|
7
|
+
|
|
8
|
+
if not isinstance(temp_one, float):
|
|
9
|
+
print("Temperature One was not a float")
|
|
10
|
+
print(temp_one)
|
|
11
|
+
temp_one = 0.0
|
|
12
|
+
|
|
13
|
+
else:
|
|
14
|
+
print(f"Temperature One: {temp_one}")
|
|
15
|
+
|
|
16
|
+
analysis = AnalysisResponse(result=temp_one)
|
|
14
17
|
return analysis
|
|
15
18
|
|
|
16
19
|
|
|
@@ -23,7 +26,5 @@ if __name__ == "__main__":
|
|
|
23
26
|
|
|
24
27
|
#Add Analysis Parameters
|
|
25
28
|
pythonDemoAnalyzer.add_analysis_parameter("Temperature", AresDataType.NUMBER)
|
|
26
|
-
pythonDemoAnalyzer.add_setting(setting_name="", setting_type=AresDataType.NULL, optional=True, constraints=[])
|
|
27
|
-
pythonDemoAnalyzer.start(wait_for_termination=True)
|
|
28
|
-
|
|
29
|
+
pythonDemoAnalyzer.add_setting(setting_name="", setting_type=AresDataType.NULL, optional=True, constraints=[], limits=Limits(0, 10000))
|
|
29
30
|
pythonDemoAnalyzer.start()
|
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
from PyAres import AresAnalyzerService, AnalysisRequest,
|
|
1
|
+
from PyAres import AresAnalyzerService, AnalysisRequest, AnalysisResponse, AresDataType, Outcome, Limits
|
|
2
2
|
|
|
3
|
-
def analyze_sample(request: AnalysisRequest) ->
|
|
3
|
+
def analyze_sample(request: AnalysisRequest) -> AnalysisResponse:
|
|
4
4
|
# 1. Extract inputs
|
|
5
5
|
# 'Growth_Metric' would come from a sensor or previous step
|
|
6
6
|
raw_value = request.inputs.get("Growth_Metric")
|
|
7
7
|
|
|
8
8
|
if raw_value is None:
|
|
9
|
-
return
|
|
9
|
+
return AnalysisResponse(result=0.0, outcome=Outcome.FAILURE)
|
|
10
10
|
|
|
11
11
|
# 2. Perform Logic
|
|
12
12
|
print(f"Analyzing sample with value: {raw_value}")
|
|
13
13
|
|
|
14
|
-
calculated_score = raw_value * 1.5
|
|
15
|
-
is_success = calculated_score > 10.0 # Define success criteria
|
|
14
|
+
calculated_score = raw_value * 1.5
|
|
16
15
|
|
|
17
16
|
# 3. Return Result
|
|
18
|
-
return
|
|
17
|
+
return AnalysisResponse(result=calculated_score, outcome=Outcome.SUCCESS)
|
|
19
18
|
|
|
20
19
|
if __name__ == "__main__":
|
|
21
20
|
service = AresAnalyzerService(
|
|
@@ -27,5 +26,10 @@ if __name__ == "__main__":
|
|
|
27
26
|
|
|
28
27
|
# Define what data we need from ARES
|
|
29
28
|
service.add_analysis_parameter("Growth_Metric", AresDataType.NUMBER)
|
|
29
|
+
|
|
30
|
+
service.add_setting("Random Setting", AresDataType.NUMBER,
|
|
31
|
+
default_value=250,
|
|
32
|
+
limits=Limits(1, 500),
|
|
33
|
+
description="This is a random setting, it is purely for demonstration purposes")
|
|
30
34
|
|
|
31
35
|
service.start()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
from PyAres import
|
|
1
|
+
from PyAres import *
|
|
2
2
|
from typing import Dict
|
|
3
|
+
import pint
|
|
3
4
|
import time
|
|
4
5
|
|
|
5
6
|
class DemoDevice:
|
|
@@ -10,10 +11,10 @@ class DemoDevice:
|
|
|
10
11
|
def set_temperature(self, temperature: float):
|
|
11
12
|
self.temperature = temperature
|
|
12
13
|
time.sleep(5)
|
|
13
|
-
return
|
|
14
|
+
return DeviceCommandResponse(None, status_code=StatusCode.COMMAND_SUCCESS)
|
|
14
15
|
|
|
15
16
|
def get_temperature(self):
|
|
16
|
-
return
|
|
17
|
+
return DeviceCommandResponse(self.temperature, status_code=StatusCode.COMMAND_SUCCESS)
|
|
17
18
|
|
|
18
19
|
def get_device_state(self) -> Dict:
|
|
19
20
|
state_dict = { "temperature": self.temperature }
|
|
@@ -25,24 +26,25 @@ class DemoDevice:
|
|
|
25
26
|
device = DemoDevice()
|
|
26
27
|
|
|
27
28
|
if __name__ == "__main__":
|
|
28
|
-
# Basic information about my device
|
|
29
|
+
ureg = pint.UnitRegistry() # Basic information about my device
|
|
29
30
|
device_name = "Demo Device"
|
|
30
31
|
description = "A device to demonstrate the PyAres device capabilities"
|
|
31
32
|
version = "1.0.0"
|
|
32
33
|
device_service = AresDeviceService(device.enter_safe_mode, device.get_device_state, device_name, description, version)
|
|
33
34
|
|
|
34
35
|
# Create the "Set Temperature" Command
|
|
35
|
-
|
|
36
|
+
quantity_schema = QuantitySchema(ureg.degree_Celsius, 40.0, 125.0)
|
|
37
|
+
parameter_schema = DeviceSchemaEntry(AresDataType.QUANTITY, "A numeric temperature value", quantity_schema=quantity_schema)
|
|
36
38
|
input_schema = { "temperature": parameter_schema }
|
|
37
39
|
set_temp_descriptor = DeviceCommandDescriptor("Set Temperature", "Set's the temperature of the demo device to the provided value.", input_schema, {})
|
|
38
40
|
device_service.add_new_command(set_temp_descriptor, device.set_temperature)
|
|
39
41
|
|
|
40
42
|
# Create the "Get Temperature" Command
|
|
41
|
-
output_schema = {"temperature": DeviceSchemaEntry(AresDataType.
|
|
43
|
+
output_schema = {"temperature": DeviceSchemaEntry(AresDataType.QUANTITY, "The current temperature of the device", quantity_schema=quantity_schema)}
|
|
42
44
|
get_temp_desc = DeviceCommandDescriptor("Get Temperature", "Get's the current temperature of the demo device.", {}, output_schema)
|
|
43
45
|
device_service.add_new_command(get_temp_desc, device.get_temperature)
|
|
44
46
|
|
|
45
47
|
#Add Settings
|
|
46
|
-
device_service.add_setting("Allow Negative Values", True)
|
|
48
|
+
device_service.add_setting("Allow Negative Values", True, description="A boolean value that determines whether the test device allows negative values")
|
|
47
49
|
|
|
48
50
|
device_service.start()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
from PyAres import AresDeviceService, AresDataType, DeviceCommandDescriptor, DeviceSchemaEntry
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FailureTestDevice:
|
|
7
|
+
def fail(self):
|
|
8
|
+
"""Intentionally fails so command failure handling can be tested."""
|
|
9
|
+
print("[Test Device] Running intentionally failing command...")
|
|
10
|
+
raise RuntimeError("Intentional test command failure")
|
|
11
|
+
|
|
12
|
+
def maybe_fail(self):
|
|
13
|
+
"""Fails half the time and otherwise returns 20."""
|
|
14
|
+
print("[Test Device] Running command with a 50% failure chance...")
|
|
15
|
+
if random.random() < 0.5:
|
|
16
|
+
raise RuntimeError("Random test command failure")
|
|
17
|
+
|
|
18
|
+
print("[Test Device] Command succeeded and returned 20.")
|
|
19
|
+
return {"number": 20}
|
|
20
|
+
|
|
21
|
+
def get_state(self):
|
|
22
|
+
return {}
|
|
23
|
+
|
|
24
|
+
def safe_mode(self):
|
|
25
|
+
print("[Test Device] Safe mode triggered.")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
test_device = FailureTestDevice()
|
|
30
|
+
|
|
31
|
+
service = AresDeviceService(
|
|
32
|
+
test_device.safe_mode,
|
|
33
|
+
test_device.get_state,
|
|
34
|
+
"Test Device",
|
|
35
|
+
"A device with commands for testing ARES failure handling",
|
|
36
|
+
"1.0.0",
|
|
37
|
+
port=7102,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
fail_command = DeviceCommandDescriptor(
|
|
41
|
+
"Fail",
|
|
42
|
+
"Intentionally throws an exception",
|
|
43
|
+
{},
|
|
44
|
+
{},
|
|
45
|
+
)
|
|
46
|
+
service.add_new_command(fail_command, test_device.fail)
|
|
47
|
+
|
|
48
|
+
maybe_fail_output_schema = {
|
|
49
|
+
"output": DeviceSchemaEntry(
|
|
50
|
+
AresDataType.STRUCT,
|
|
51
|
+
"Successful command output",
|
|
52
|
+
struct_schema={
|
|
53
|
+
"number": DeviceSchemaEntry(AresDataType.NUMBER, "Returned Number")
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
maybe_fail_command = DeviceCommandDescriptor(
|
|
58
|
+
"Maybe Fail",
|
|
59
|
+
"Has a 50% chance of failing; otherwise returns 20",
|
|
60
|
+
{},
|
|
61
|
+
maybe_fail_output_schema,
|
|
62
|
+
)
|
|
63
|
+
service.add_new_command(maybe_fail_command, test_device.maybe_fail)
|
|
64
|
+
|
|
65
|
+
print("Test Device Service Running...")
|
|
66
|
+
service.start()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from PyAres import
|
|
1
|
+
from PyAres import *
|
|
2
2
|
|
|
3
3
|
# --- PART 1: The Simulated Hardware ---
|
|
4
4
|
class VirtualHotplate:
|
|
@@ -8,14 +8,16 @@ class VirtualHotplate:
|
|
|
8
8
|
def set_temperature(self, temp: float):
|
|
9
9
|
"""Simulates setting the heater."""
|
|
10
10
|
print(f"[Hardware] Heating to {temp}°C...")
|
|
11
|
+
response = DeviceCommandResponse(None, status_code=StatusCode.COMMAND_SUCCESS)
|
|
11
12
|
self.target_temp = temp
|
|
12
|
-
return
|
|
13
|
+
return response
|
|
13
14
|
|
|
14
15
|
def get_temperature(self):
|
|
15
16
|
"""Simulates reading the sensor."""
|
|
17
|
+
response = DeviceCommandResponse({ "current_temp": self.target_temp }, status_code=StatusCode.COMMAND_SUCCESS)
|
|
16
18
|
# In a real device, you'd read a serial port here.
|
|
17
19
|
print("[Hardware] Retrieving the current temperature...")
|
|
18
|
-
return
|
|
20
|
+
return response
|
|
19
21
|
|
|
20
22
|
def get_state(self):
|
|
21
23
|
"""Required: Tells ARES the current status for logging."""
|
|
@@ -54,9 +56,19 @@ if __name__ == "__main__":
|
|
|
54
56
|
service.add_new_command(set_cmd, my_hotplate.set_temperature)
|
|
55
57
|
|
|
56
58
|
# 4. Define Command: Get Temperature
|
|
57
|
-
# This schema tells ARES to expect a
|
|
59
|
+
# This schema tells ARES to expect a struct back
|
|
58
60
|
output_schema = {
|
|
59
|
-
"
|
|
61
|
+
"output": DeviceSchemaEntry(
|
|
62
|
+
AresDataType.STRUCT,
|
|
63
|
+
"Current temperature output",
|
|
64
|
+
struct_schema={
|
|
65
|
+
"current_temp": DeviceSchemaEntry(
|
|
66
|
+
AresDataType.NUMBER,
|
|
67
|
+
"Current Temperature",
|
|
68
|
+
"Celsius"
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
)
|
|
60
72
|
}
|
|
61
73
|
get_cmd = DeviceCommandDescriptor(
|
|
62
74
|
"Get Temp",
|
|
@@ -69,4 +81,4 @@ if __name__ == "__main__":
|
|
|
69
81
|
# 5. Start the Service
|
|
70
82
|
# This will block and listen for ARES connections
|
|
71
83
|
print("Virtual Hotplate Service Running...")
|
|
72
|
-
service.start()
|
|
84
|
+
service.start()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
from PyAres import AresDeviceService, AresDataType, DeviceSchemaEntry, DeviceCommandDescriptor, DeviceCommandResponse, StatusCode
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# --- PART 1: The Simulated Hardware ---
|
|
7
|
+
class VirtualRandomNumberDevice:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.last_number = 0
|
|
10
|
+
|
|
11
|
+
def generate_number(self):
|
|
12
|
+
"""Simulates reading a random value from hardware."""
|
|
13
|
+
self.last_number = random.randint(1, 100)
|
|
14
|
+
print(f"[Hardware] Generated random number: {self.last_number}")
|
|
15
|
+
response = DeviceCommandResponse({"random_number": self.last_number}, status_code=StatusCode.COMMAND_SUCCESS)
|
|
16
|
+
return response
|
|
17
|
+
|
|
18
|
+
def get_state(self):
|
|
19
|
+
"""Required: Tells ARES the current status for logging."""
|
|
20
|
+
return {"last_number": self.last_number}
|
|
21
|
+
|
|
22
|
+
def safe_mode(self):
|
|
23
|
+
"""Required: A safety fallback."""
|
|
24
|
+
print("[Hardware] SAFE MODE TRIGGERED: Random number device idle.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# --- PART 2: The Ares Service Wrapper ---
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
# 1. Initialize the hardware
|
|
30
|
+
my_random_device = VirtualRandomNumberDevice()
|
|
31
|
+
|
|
32
|
+
# 2. Define the Service Info
|
|
33
|
+
service = AresDeviceService(
|
|
34
|
+
my_random_device.safe_mode,
|
|
35
|
+
my_random_device.get_state,
|
|
36
|
+
"My Virtual Random Number Device",
|
|
37
|
+
"A simulated device that generates random numbers",
|
|
38
|
+
"1.0.0",
|
|
39
|
+
port=7101
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# 3. Define Command: Generate Number
|
|
43
|
+
# This schema tells ARES to expect a struct back
|
|
44
|
+
output_schema = {
|
|
45
|
+
"output": DeviceSchemaEntry(
|
|
46
|
+
AresDataType.STRUCT,
|
|
47
|
+
"Generated random number output",
|
|
48
|
+
struct_schema={
|
|
49
|
+
"random_number": DeviceSchemaEntry(
|
|
50
|
+
AresDataType.NUMBER,
|
|
51
|
+
"Random Number",
|
|
52
|
+
"1-100",
|
|
53
|
+
min_number_value=1,
|
|
54
|
+
max_number_value=100,
|
|
55
|
+
)
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
generate_cmd = DeviceCommandDescriptor(
|
|
60
|
+
"Generate Number",
|
|
61
|
+
"Generates a random number from 1 to 100",
|
|
62
|
+
{},
|
|
63
|
+
output_schema,
|
|
64
|
+
)
|
|
65
|
+
service.add_new_command(generate_cmd, my_random_device.generate_number)
|
|
66
|
+
|
|
67
|
+
# 4. Start the Service
|
|
68
|
+
# This will block and listen for ARES connections
|
|
69
|
+
print("Virtual Random Number Device Service Running...")
|
|
70
|
+
service.start()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from PyAres import *
|
|
2
|
+
|
|
3
|
+
class RotaryMixer:
|
|
4
|
+
def __init__(self, speed):
|
|
5
|
+
self.speed = speed
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
mixer = RotaryMixer(1200)
|
|
9
|
+
|
|
10
|
+
# 1. Define your hardware logic
|
|
11
|
+
def set_speed(rpm: float):
|
|
12
|
+
print(f"Setting motor speed to {rpm}")
|
|
13
|
+
mixer.speed = rpm
|
|
14
|
+
return DeviceCommandResponse(None, status_code=StatusCode.COMMAND_SUCCESS) # Return empty dict if no data needs to be sent back
|
|
15
|
+
|
|
16
|
+
def get_speed():
|
|
17
|
+
print("Hey I got speed")
|
|
18
|
+
return DeviceCommandResponse(mixer.speed, status_code=StatusCode.COMMAND_SUCCESS)
|
|
19
|
+
|
|
20
|
+
def get_status():
|
|
21
|
+
# Return a dictionary matching your state schema
|
|
22
|
+
return { "rpm": mixer.speed }
|
|
23
|
+
|
|
24
|
+
def safe_mode():
|
|
25
|
+
print("Stopping motor immediately!")
|
|
26
|
+
|
|
27
|
+
# 2. Initialize Service
|
|
28
|
+
service = AresDeviceService(
|
|
29
|
+
safe_mode,
|
|
30
|
+
get_status,
|
|
31
|
+
"Rotary Mixer",
|
|
32
|
+
"High-speed mixer control",
|
|
33
|
+
"1.0.0",
|
|
34
|
+
port=7101
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# 3. Define the 'Set Speed' Command
|
|
38
|
+
# Input: One number (Speed)
|
|
39
|
+
input_schema = { "rpm": DeviceSchemaEntry(AresDataType.NUMBER, "Speed in RPM", "RPM") }
|
|
40
|
+
cmd_descriptor = DeviceCommandDescriptor("Set Speed", "Sets mixer speed", input_schema, {})
|
|
41
|
+
|
|
42
|
+
output_schema = { "rpm": DeviceSchemaEntry(AresDataType.NUMBER, "Speed in RPM", "RPM") }
|
|
43
|
+
get_speed_descriptor = DeviceCommandDescriptor("Get Speed", "Gets mixer speed", {}, output_schema)
|
|
44
|
+
|
|
45
|
+
# 4. Register the command
|
|
46
|
+
service.add_new_command(cmd_descriptor, set_speed)
|
|
47
|
+
service.add_new_command(get_speed_descriptor, get_speed)
|
|
48
|
+
|
|
49
|
+
# 5. Start
|
|
50
|
+
service.start()
|