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.
Files changed (65) hide show
  1. {pyares-0.3.3 → pyares-1.0.0}/.gitignore +6 -0
  2. {pyares-0.3.3 → pyares-1.0.0}/PKG-INFO +10 -10
  3. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Analyzing/__init__.py +2 -2
  4. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Analyzing/analysis_service.py +42 -13
  5. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Analyzing/analyzer_models.py +21 -2
  6. pyares-1.0.0/PyAres/Demo/Analyzers/airship_analyzer.py +20 -0
  7. {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Analyzers}/analyzer_test.py +15 -14
  8. {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Analyzers}/analyzer_wiki.py +10 -6
  9. {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Devices}/device_test.py +9 -7
  10. pyares-1.0.0/PyAres/Demo/Devices/failure_test_device.py +66 -0
  11. {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Devices}/hotplate.py +18 -6
  12. pyares-1.0.0/PyAres/Demo/Devices/random_number_device.py +70 -0
  13. pyares-1.0.0/PyAres/Demo/Devices/rotary_mixer.py +50 -0
  14. pyares-1.0.0/PyAres/Demo/Planners/airship_planner.py +187 -0
  15. {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Planners}/planner_test.py +7 -7
  16. pyares-1.0.0/PyAres/Device/__init__.py +5 -0
  17. pyares-1.0.0/PyAres/Device/device_models.py +78 -0
  18. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Device/device_service.py +67 -14
  19. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Models/__init__.py +5 -2
  20. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Models/ares_data_models.py +41 -1
  21. pyares-1.0.0/PyAres/Planning/planner_models.py +203 -0
  22. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Planning/planning_service.py +25 -13
  23. pyares-1.0.0/PyAres/Utils/ares_data_schema_utils.py +73 -0
  24. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/ares_data_type_utils.py +12 -1
  25. pyares-1.0.0/PyAres/Utils/ares_device_command_utils.py +65 -0
  26. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/ares_struct_utils.py +59 -6
  27. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/ares_value_utils.py +116 -18
  28. pyares-1.0.0/PyAres/Utils/device_status_code_utils.py +19 -0
  29. {pyares-0.3.3 → pyares-1.0.0}/PyAres/__init__.py +8 -2
  30. {pyares-0.3.3 → pyares-1.0.0}/README.md +8 -6
  31. {pyares-0.3.3 → pyares-1.0.0}/pyproject.toml +3 -5
  32. {pyares-0.3.3 → pyares-1.0.0}/tests/test_analyzer.py +5 -5
  33. {pyares-0.3.3 → pyares-1.0.0}/tests/test_ares_data_schema_utils.py +15 -4
  34. {pyares-0.3.3 → pyares-1.0.0}/tests/test_ares_data_type_utils.py +13 -1
  35. {pyares-0.3.3 → pyares-1.0.0}/tests/test_ares_device.py +18 -2
  36. {pyares-0.3.3 → pyares-1.0.0}/tests/test_ares_struct_utils.py +19 -1
  37. {pyares-0.3.3 → pyares-1.0.0}/tests/test_ares_value_type_conversions.py +28 -1
  38. pyares-0.3.3/PyAres/Demo/rotary_mixer.py +0 -37
  39. pyares-0.3.3/PyAres/Device/__init__.py +0 -3
  40. pyares-0.3.3/PyAres/Device/device_models.py +0 -40
  41. pyares-0.3.3/PyAres/Planning/planner_models.py +0 -101
  42. pyares-0.3.3/PyAres/Utils/ares_data_schema_utils.py +0 -49
  43. pyares-0.3.3/PyAres/Utils/ares_device_command_utils.py +0 -43
  44. {pyares-0.3.3 → pyares-1.0.0}/.github/dependabot.yml +0 -0
  45. {pyares-0.3.3 → pyares-1.0.0}/.github/workflows/pypi.yaml +0 -0
  46. {pyares-0.3.3 → pyares-1.0.0}/.github/workflows/python-tests.yaml +0 -0
  47. {pyares-0.3.3 → pyares-1.0.0}/LICENSE +0 -0
  48. {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Analyzers}/analyzer_test_tools_demo.py +0 -0
  49. {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Planners}/planner_test_tools_demo.py +0 -0
  50. {pyares-0.3.3/PyAres/Demo → pyares-1.0.0/PyAres/Demo/Planners}/planner_wiki.py +0 -0
  51. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Device/device_warnings.py +0 -0
  52. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Planning/__init__.py +0 -0
  53. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/ares_outcome_utils.py +0 -0
  54. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/param_history_info_utils.py +0 -0
  55. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/plan_request_utils.py +0 -0
  56. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/plan_response_utils.py +0 -0
  57. {pyares-0.3.3 → pyares-1.0.0}/PyAres/Utils/planning_param_utils.py +0 -0
  58. {pyares-0.3.3 → pyares-1.0.0}/PyAres/test_tools/__init__.py +0 -0
  59. {pyares-0.3.3 → pyares-1.0.0}/PyAres/test_tools/analyzer_tester.py +0 -0
  60. {pyares-0.3.3 → pyares-1.0.0}/PyAres/test_tools/planner_tester.py +0 -0
  61. {pyares-0.3.3 → pyares-1.0.0}/setup.cfg +0 -0
  62. {pyares-0.3.3 → pyares-1.0.0}/setup.py +0 -0
  63. {pyares-0.3.3 → pyares-1.0.0}/tests/__init__.py +0 -0
  64. {pyares-0.3.3 → pyares-1.0.0}/tests/mock_grpc_context.py +0 -0
  65. {pyares-0.3.3 → pyares-1.0.0}/tests/test_planner.py +0 -0
@@ -165,3 +165,9 @@ cython_debug/
165
165
 
166
166
  # PyPI configuration file
167
167
  .pypirc
168
+
169
+ # VS Code
170
+ .vscode/
171
+
172
+ #macOS
173
+ .DS_Store
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAres
3
- Version: 0.3.3
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.16.1
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 Analysis
76
+ from PyAres import AnalysisResponse
77
77
  from PyAres import AresDataType
78
78
 
79
- def analyze(request: AnalysisRequest) -> Analysis:
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 = Analysis(result=growth, success=True)
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
- # Dictionary key should match what we defined in our schema earlier
123
- return { "temperature": self.temperature }
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 Analysis, AnalysisRequest, InfoResponse
2
+ from .analyzer_models import AnalysisResponse, AnalysisRequest, InfoResponse
3
3
 
4
4
  __all__ = [
5
- "Analysis",
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 ..Models import AresSchemaEntry
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[Analysis, Awaitable[Analysis]]]
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.SchemaEntry] = {}
40
- self._analysis_parameters: Dict[str, ares_data_schema_pb2.SchemaEntry] = {}
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, Analysis):
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.SchemaEntry] = request.input_schema.fields
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, setting_name: str, setting_type: ares_data_models.AresDataType, optional: bool = True, constraints: Union[list[int], list[str], list[float]] = [], struct_schema: Optional[Dict[str, AresSchemaEntry]] = None):
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
- self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, struct_schema)
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 Device Service on port {self._port}...")
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
- class Analysis:
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, Analysis, AresDataType, Outcome
1
+ from PyAres import AresAnalyzerService, AnalysisRequest, AnalysisResponse, AresDataType, Outcome, Limits
2
2
 
3
- def analyze(request: AnalysisRequest) -> Analysis:
3
+ def analyze(request: AnalysisRequest) -> AnalysisResponse:
4
4
  #Custom Analysis Logic
5
- temperature = request.inputs.get("Temperature")
6
-
7
- if not isinstance(temperature, float):
8
- print("Temperature was not a float")
9
- temperature = 0.0
10
-
11
- print(f"Temperature: {temperature}")
12
-
13
- analysis = Analysis(result=temperature)
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, Analysis, AresDataType, Outcome
1
+ from PyAres import AresAnalyzerService, AnalysisRequest, AnalysisResponse, AresDataType, Outcome, Limits
2
2
 
3
- def analyze_sample(request: AnalysisRequest) -> Analysis:
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 Analysis(result=0.0, outcome=Outcome.FAILURE)
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 # Placeholder logic
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 Analysis(result=calculated_score, outcome=Outcome.SUCCESS)
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 AresDeviceService, DeviceCommandDescriptor, DeviceSchemaEntry, AresDataType
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 { "temperature": self.temperature }
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
- parameter_schema = DeviceSchemaEntry(AresDataType.NUMBER, "A numeric temperature value", "Degree's Celsius")
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.NUMBER, "The current temperature of the device", "Degree's Celsius")}
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 AresDeviceService, AresDataType, DeviceSchemaEntry, DeviceCommandDescriptor
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 {} # Return empty dict if no data needs to be sent back
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 { "current_temp": self.target_temp }
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 number back
59
+ # This schema tells ARES to expect a struct back
58
60
  output_schema = {
59
- "current_temp": DeviceSchemaEntry(AresDataType.NUMBER, "Current Temperature", "Celsius")
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()