Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions PyAres/Analyzing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from .analysis_service import AresAnalyzerService
from .analyzer_models import Analysis, AnalysisRequest, InfoResponse
from .analyzer_models import AnalysisResponse, AnalysisRequest, InfoResponse

__all__ = [
"Analysis",
"AnalysisResponse",
"AnalysisRequest",
"InfoResponse",
"AresAnalyzerService",
Expand Down
49 changes: 39 additions & 10 deletions PyAres/Analyzing/analysis_service.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Standard Imports
import grpc
from concurrent import futures
from typing import Callable, Awaitable, Union, Mapping, Dict, Optional
from typing import Callable, Awaitable, Union, Mapping, Dict, Optional, Any

# Import generated protobuf and gRPC stubs
from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2 as analyzer_service
Expand All @@ -11,22 +11,21 @@
from ares_datamodel.connection import connection_state_pb2
from ares_datamodel.connection import connection_status_pb2
from ares_datamodel.connection import connection_info_pb2
from ares_datamodel import ares_data_type_pb2
from ares_datamodel import ares_data_schema_pb2
from ares_datamodel import ares_outcome_enum_pb2

# Import Utilities
from ..Utils import ares_struct_utils
from ..Utils import ares_data_schema_utils
from ..Utils import ares_outcome_utils
from ..Utils import ares_value_utils

# Import python models
from ..Models import ares_data_models, RequestMetadata
from ..Models import AresSchemaEntry
from .analyzer_models import AnalysisRequest, Analysis, InfoResponse
from ..Models import ares_data_models, RequestMetadata, Limits, AresSchemaEntry
from .analyzer_models import AnalysisRequest, AnalysisResponse, InfoResponse

# Type hints for the user's custom logic
AnalyzeLogicFunction = Callable[[AnalysisRequest], Union[Analysis, Awaitable[Analysis]]]
AnalyzeLogicFunction = Callable[[AnalysisRequest], Union[AnalysisResponse, Awaitable[AnalysisResponse]]]

class AresAnalyzerServiceWrapper(analyzer_service_grpc.AresRemoteAnalyzerServiceServicer):
"""
Expand Down Expand Up @@ -73,7 +72,7 @@ def Analyze(self, request: analyzer_service.AnalysisRequest, context) -> analysi
if isinstance(python_response, Awaitable):
python_response = python_response.__await__()

if not isinstance(python_response, Analysis):
if not isinstance(python_response, AnalysisResponse):
print("Analysis response was an invalid type, ")
proto_analysis.analysis_outcome = ares_outcome_enum_pb2.FAILURE
proto_analysis.error_string = "The user's custom analysis logic returned an invalid type, analysis cannot be processed"
Expand Down Expand Up @@ -200,7 +199,15 @@ def __init__(self,
else:
self._server.add_insecure_port(f'[::]:{self._port}')

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):
def add_setting(self,
setting_name: str,
setting_type: ares_data_models.AresDataType,
default_value: Any = None,
optional: bool = True,
constraints: Union[list[int], list[str], list[float]] = [],
struct_schema: Optional[Dict[str, AresSchemaEntry]] = None,
limits: Optional[Limits] = None,
description: Optional[str] = None):
"""
Adds an analyzer setting to be reported to ARES when capabilities are requested.
While most `PyAres.Models.AresDataType` options are supported, bool arrays and byte arrays
Expand All @@ -212,8 +219,30 @@ def add_setting(self, setting_name: str, setting_type: ares_data_models.AresData
optional (bool): Whether the setting is optional.
constraints: An optional list of values to constrain the available setting choices. Can be integers, strings, or floats.
struct_schema: An optional dictionary defining the fields of a STRUCT type setting, using AresSchemaEntry objects.
limits: An optional limits object used to specify minimum and maximum values for a setting
description: An optional string to describe your setting in more detail. Appears in ARES as a tooltip in the settings menu.
"""
self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, struct_schema)
try:
if default_value is not None:
default_ares_value = ares_value_utils.create_ares_value(default_value)
self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type=setting_type,
optional=optional,
choices=constraints,
struct_schema=struct_schema,
limits=limits,
default_value=default_ares_value,
description=description)

else:
self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type=setting_type,
optional=optional,
choices=constraints,
struct_schema=struct_schema,
limits=limits,
description=description)

except Exception as e:
print(f"Encountered an exception while adding setting {setting_name}: {e}")

def add_analysis_parameter(self, parameter_name: str, parameter_type: ares_data_models.AresDataType, optional: bool = False, struct_schema: Optional[Dict[str, AresSchemaEntry]] = None):
"""
Expand Down Expand Up @@ -247,7 +276,7 @@ def start(self, wait_for_termination: bool = True):
Setting this value to false will allow you to continue execution after starting your service, however this should ONLY be done if you have
another mechanism for keeping your process alive (such as a GUI, or a loop). Defaults to true.
"""
print(f"Starting Ares Device Service on port {self._port}...")
print(f"Starting Ares Analysis Service on port {self._port}...")
self._server.start()

if wait_for_termination:
Expand Down
2 changes: 1 addition & 1 deletion PyAres/Analyzing/analyzer_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def __str__(self) -> str:
def __repr__(self) -> str:
return self.__str__()

class Analysis:
class AnalysisResponse:
""" Represents the result of an analysis process. """

def __init__(self, result: float, outcome: Outcome = Outcome.SUCCESS, error_string: str = ""):
Expand Down
20 changes: 20 additions & 0 deletions PyAres/Demo/Analyzers/airship_analyzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from PyAres import AresAnalyzerService, AnalysisRequest, Analysis, AresDataType, Outcome

def analysis_logic(request: AnalysisRequest):
result = int(request.inputs["ShotOutcome"])
print(f"Analyzing, result is: {result}")
response = Analysis(result=result, outcome=Outcome.SUCCESS)
return response

if __name__ == "__main__":
print("Welcome to the Airship Analyzer")

analyzer = AresAnalyzerService(
custom_analysis_logic=analysis_logic,
description="An analyzer for use with the Airship Device",
name="Airship Analyzer",
version="1.0.0")

analyzer.add_analysis_parameter("ShotOutcome", AresDataType.NUMBER)

analyzer.start()
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
from PyAres import AresAnalyzerService, AnalysisRequest, Analysis, AresDataType, Outcome
from PyAres import AresAnalyzerService, AnalysisRequest, AnalysisResponse, AresDataType, Outcome, Limits

def analyze(request: AnalysisRequest) -> Analysis:
def analyze(request: AnalysisRequest) -> AnalysisResponse:
#Custom Analysis Logic
temperature = request.inputs.get("Temperature")

if not isinstance(temperature, float):
print("Temperature was not a float")
temperature = 0.0

print(f"Temperature: {temperature}")

analysis = Analysis(result=temperature)
temp_one = request.inputs.get("Temperature One")
print("Processing Analysis Request")

if not isinstance(temp_one, float):
print("Temperature One was not a float")
print(temp_one)
temp_one = 0.0

else:
print(f"Temperature One: {temp_one}")

analysis = AnalysisResponse(result=temp_one)
return analysis


Expand All @@ -23,7 +26,5 @@ def analyze(request: AnalysisRequest) -> Analysis:

#Add Analysis Parameters
pythonDemoAnalyzer.add_analysis_parameter("Temperature", AresDataType.NUMBER)
pythonDemoAnalyzer.add_setting(setting_name="", setting_type=AresDataType.NULL, optional=True, constraints=[])
pythonDemoAnalyzer.start(wait_for_termination=True)

pythonDemoAnalyzer.add_setting(setting_name="", setting_type=AresDataType.NULL, optional=True, constraints=[], limits=Limits(0, 10000))
pythonDemoAnalyzer.start()
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
from PyAres import AresAnalyzerService, AnalysisRequest, Analysis, AresDataType, Outcome
from PyAres import AresAnalyzerService, AnalysisRequest, AnalysisResponse, AresDataType, Outcome, Limits

def analyze_sample(request: AnalysisRequest) -> Analysis:
def analyze_sample(request: AnalysisRequest) -> AnalysisResponse:
# 1. Extract inputs
# 'Growth_Metric' would come from a sensor or previous step
raw_value = request.inputs.get("Growth_Metric")

if raw_value is None:
return Analysis(result=0.0, outcome=Outcome.FAILURE)
return AnalysisResponse(result=0.0, outcome=Outcome.FAILURE)

# 2. Perform Logic
print(f"Analyzing sample with value: {raw_value}")

calculated_score = raw_value * 1.5 # Placeholder logic
is_success = calculated_score > 10.0 # Define success criteria
calculated_score = raw_value * 1.5

# 3. Return Result
return Analysis(result=calculated_score, outcome=Outcome.SUCCESS)
return AnalysisResponse(result=calculated_score, outcome=Outcome.SUCCESS)

if __name__ == "__main__":
service = AresAnalyzerService(
Expand All @@ -27,5 +26,10 @@ def analyze_sample(request: AnalysisRequest) -> Analysis:

# Define what data we need from ARES
service.add_analysis_parameter("Growth_Metric", AresDataType.NUMBER)

service.add_setting("Random Setting", AresDataType.NUMBER,
default_value=250,
limits=Limits(1, 500),
description="This is a random setting, it is purely for demonstration purposes")

service.start()
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ def __init__(self):
def set_temperature(self, temperature: float):
self.temperature = temperature
time.sleep(5)
return {}
return DeviceCommandResponse(None, status_code=StatusCode.COMMAND_SUCCESS)

def get_temperature(self):
return { "temperature": self.temperature }
return DeviceCommandResponse(self.temperature, status_code=StatusCode.COMMAND_SUCCESS)

def get_device_state(self) -> Dict:
state_dict = { "temperature": self.temperature }
Expand Down Expand Up @@ -45,6 +45,6 @@ def enter_safe_mode(self):
device_service.add_new_command(get_temp_desc, device.get_temperature)

#Add Settings
device_service.add_setting("Allow Negative Values", True)
device_service.add_setting("Allow Negative Values", True, description="A boolean value that determines whether the test device allows negative values")

device_service.start()
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from PyAres import AresDeviceService, AresDataType, DeviceCommandDescriptor, DeviceSchemaEntry


class TestDevice:
class FailureTestDevice:
def fail(self):
"""Intentionally fails so command failure handling can be tested."""
print("[Test Device] Running intentionally failing command...")
Expand All @@ -26,7 +26,7 @@ def safe_mode(self):


if __name__ == "__main__":
test_device = TestDevice()
test_device = FailureTestDevice()

service = AresDeviceService(
test_device.safe_mode,
Expand Down
8 changes: 5 additions & 3 deletions PyAres/Demo/hotplate.py → PyAres/Demo/Devices/hotplate.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from PyAres import AresDeviceService, AresDataType, DeviceSchemaEntry, DeviceCommandDescriptor
from PyAres import *

# --- PART 1: The Simulated Hardware ---
class VirtualHotplate:
Expand All @@ -8,14 +8,16 @@ def __init__(self):
def set_temperature(self, temp: float):
"""Simulates setting the heater."""
print(f"[Hardware] Heating to {temp}°C...")
response = DeviceCommandResponse(None, status_code=StatusCode.COMMAND_SUCCESS)
self.target_temp = temp
return {} # Return empty dict if no data needs to be sent back
return response

def get_temperature(self):
"""Simulates reading the sensor."""
response = DeviceCommandResponse({ "current_temp": self.target_temp }, status_code=StatusCode.COMMAND_SUCCESS)
# In a real device, you'd read a serial port here.
print("[Hardware] Retrieving the current temperature...")
return { "current_temp": self.target_temp }
return response

def get_state(self):
"""Required: Tells ARES the current status for logging."""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import random

from PyAres import AresDeviceService, AresDataType, DeviceSchemaEntry, DeviceCommandDescriptor
from PyAres import AresDeviceService, AresDataType, DeviceSchemaEntry, DeviceCommandDescriptor, DeviceCommandResponse, StatusCode


# --- PART 1: The Simulated Hardware ---
Expand All @@ -12,7 +12,8 @@ def generate_number(self):
"""Simulates reading a random value from hardware."""
self.last_number = random.randint(1, 100)
print(f"[Hardware] Generated random number: {self.last_number}")
return {"random_number": self.last_number}
response = DeviceCommandResponse({"random_number": self.last_number}, status_code=StatusCode.COMMAND_SUCCESS)
return response

def get_state(self):
"""Required: Tells ARES the current status for logging."""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from PyAres import AresDeviceService, AresDataType, DeviceSchemaEntry, DeviceCommandDescriptor
from PyAres import *

class RotaryMixer:
def __init__(self, speed):
Expand All @@ -11,12 +11,11 @@ def __init__(self, speed):
def set_speed(rpm: float):
print(f"Setting motor speed to {rpm}")
mixer.speed = rpm
# Hardware communication goes here...
return {} # Return empty dict if no data needs to be sent back
return DeviceCommandResponse(None, status_code=StatusCode.COMMAND_SUCCESS) # Return empty dict if no data needs to be sent back

def get_speed():
print("Hey I got speed")
return { "rpm": mixer.speed }
return DeviceCommandResponse(mixer.speed, status_code=StatusCode.COMMAND_SUCCESS)

def get_status():
# Return a dictionary matching your state schema
Expand All @@ -37,14 +36,10 @@ def safe_mode():

# 3. Define the 'Set Speed' Command
# Input: One number (Speed)
input_schema = {
"rpm": DeviceSchemaEntry(AresDataType.NUMBER, "Speed in RPM", "RPM")
}
input_schema = { "rpm": DeviceSchemaEntry(AresDataType.NUMBER, "Speed in RPM", "RPM") }
cmd_descriptor = DeviceCommandDescriptor("Set Speed", "Sets mixer speed", input_schema, {})

output_schema = {
"rpm": DeviceSchemaEntry(AresDataType.NUMBER, "Speed in RPM", "RPM")
}
output_schema = { "rpm": DeviceSchemaEntry(AresDataType.NUMBER, "Speed in RPM", "RPM") }
get_speed_descriptor = DeviceCommandDescriptor("Get Speed", "Gets mixer speed", {}, output_schema)

# 4. Register the command
Expand Down
Loading