From 5ffef619c9fe592878d5df2773d03d5acdb14a19 Mon Sep 17 00:00:00 2001 From: Nick Kleiner Date: Mon, 8 Jun 2026 09:06:09 -0400 Subject: [PATCH 01/12] Added status code functionality to PyAres --- PyAres/Demo/Analyzers/airship_analyzer.py | 20 ++ PyAres/Demo/{ => Analyzers}/analyzer_test.py | 32 ++- .../analyzer_test_tools_demo.py | 0 PyAres/Demo/{ => Analyzers}/analyzer_wiki.py | 0 PyAres/Demo/{ => Devices}/device_test.py | 4 +- .../failure_test_device.py} | 4 +- PyAres/Demo/{ => Devices}/hotplate.py | 8 +- .../{ => Devices}/random_number_device.py | 5 +- PyAres/Demo/{ => Devices}/rotary_mixer.py | 15 +- PyAres/Demo/Planners/airship_planner.py | 187 ++++++++++++++++++ PyAres/Demo/{ => Planners}/planner_test.py | 0 .../{ => Planners}/planner_test_tools_demo.py | 0 PyAres/Demo/{ => Planners}/planner_wiki.py | 0 PyAres/Device/__init__.py | 2 + PyAres/Device/device_models.py | 26 ++- PyAres/Device/device_service.py | 42 +++- PyAres/Planning/planner_models.py | 2 +- PyAres/Utils/device_status_code_utils.py | 19 ++ PyAres/__init__.py | 2 + 19 files changed, 329 insertions(+), 39 deletions(-) create mode 100644 PyAres/Demo/Analyzers/airship_analyzer.py rename PyAres/Demo/{ => Analyzers}/analyzer_test.py (50%) rename PyAres/Demo/{ => Analyzers}/analyzer_test_tools_demo.py (100%) rename PyAres/Demo/{ => Analyzers}/analyzer_wiki.py (100%) rename PyAres/Demo/{ => Devices}/device_test.py (91%) rename PyAres/Demo/{test_device.py => Devices/failure_test_device.py} (96%) rename PyAres/Demo/{ => Devices}/hotplate.py (90%) rename PyAres/Demo/{ => Devices}/random_number_device.py (91%) rename PyAres/Demo/{ => Devices}/rotary_mixer.py (70%) create mode 100644 PyAres/Demo/Planners/airship_planner.py rename PyAres/Demo/{ => Planners}/planner_test.py (100%) rename PyAres/Demo/{ => Planners}/planner_test_tools_demo.py (100%) rename PyAres/Demo/{ => Planners}/planner_wiki.py (100%) create mode 100644 PyAres/Utils/device_status_code_utils.py diff --git a/PyAres/Demo/Analyzers/airship_analyzer.py b/PyAres/Demo/Analyzers/airship_analyzer.py new file mode 100644 index 0000000..8e9be73 --- /dev/null +++ b/PyAres/Demo/Analyzers/airship_analyzer.py @@ -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() diff --git a/PyAres/Demo/analyzer_test.py b/PyAres/Demo/Analyzers/analyzer_test.py similarity index 50% rename from PyAres/Demo/analyzer_test.py rename to PyAres/Demo/Analyzers/analyzer_test.py index 019be1b..4eafe1d 100644 --- a/PyAres/Demo/analyzer_test.py +++ b/PyAres/Demo/Analyzers/analyzer_test.py @@ -2,15 +2,26 @@ def analyze(request: AnalysisRequest) -> Analysis: #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}") + + # if not isinstance(temp_two, float): + # print("Temperature Two was not a float") + # print(temp_two) + # temp_two = 0.0 + + # else: + # print(f"Temperature Two: {temp_two}") + + analysis = Analysis(result=temp_one) return analysis @@ -22,7 +33,8 @@ def analyze(request: AnalysisRequest) -> Analysis: pythonDemoAnalyzer = AresAnalyzerService(analyze, name, version, description) #Add Analysis Parameters - pythonDemoAnalyzer.add_analysis_parameter("Temperature", AresDataType.NUMBER) + # pythonDemoAnalyzer.add_analysis_parameter("Temperature Two", AresDataType.NUMBER) + pythonDemoAnalyzer.add_analysis_parameter("Temperature One", AresDataType.NUMBER) pythonDemoAnalyzer.add_setting(setting_name="", setting_type=AresDataType.NULL, optional=True, constraints=[]) pythonDemoAnalyzer.start(wait_for_termination=True) diff --git a/PyAres/Demo/analyzer_test_tools_demo.py b/PyAres/Demo/Analyzers/analyzer_test_tools_demo.py similarity index 100% rename from PyAres/Demo/analyzer_test_tools_demo.py rename to PyAres/Demo/Analyzers/analyzer_test_tools_demo.py diff --git a/PyAres/Demo/analyzer_wiki.py b/PyAres/Demo/Analyzers/analyzer_wiki.py similarity index 100% rename from PyAres/Demo/analyzer_wiki.py rename to PyAres/Demo/Analyzers/analyzer_wiki.py diff --git a/PyAres/Demo/device_test.py b/PyAres/Demo/Devices/device_test.py similarity index 91% rename from PyAres/Demo/device_test.py rename to PyAres/Demo/Devices/device_test.py index 1cd1b1f..e1b8206 100644 --- a/PyAres/Demo/device_test.py +++ b/PyAres/Demo/Devices/device_test.py @@ -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 } diff --git a/PyAres/Demo/test_device.py b/PyAres/Demo/Devices/failure_test_device.py similarity index 96% rename from PyAres/Demo/test_device.py rename to PyAres/Demo/Devices/failure_test_device.py index 4a6b4e5..d369ec0 100644 --- a/PyAres/Demo/test_device.py +++ b/PyAres/Demo/Devices/failure_test_device.py @@ -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...") @@ -26,7 +26,7 @@ def safe_mode(self): if __name__ == "__main__": - test_device = TestDevice() + test_device = FailureTestDevice() service = AresDeviceService( test_device.safe_mode, diff --git a/PyAres/Demo/hotplate.py b/PyAres/Demo/Devices/hotplate.py similarity index 90% rename from PyAres/Demo/hotplate.py rename to PyAres/Demo/Devices/hotplate.py index 42901d2..3e662c0 100644 --- a/PyAres/Demo/hotplate.py +++ b/PyAres/Demo/Devices/hotplate.py @@ -1,4 +1,4 @@ -from PyAres import AresDeviceService, AresDataType, DeviceSchemaEntry, DeviceCommandDescriptor +from PyAres import * # --- PART 1: The Simulated Hardware --- class VirtualHotplate: @@ -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.""" diff --git a/PyAres/Demo/random_number_device.py b/PyAres/Demo/Devices/random_number_device.py similarity index 91% rename from PyAres/Demo/random_number_device.py rename to PyAres/Demo/Devices/random_number_device.py index d460026..7bbc53a 100644 --- a/PyAres/Demo/random_number_device.py +++ b/PyAres/Demo/Devices/random_number_device.py @@ -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 --- @@ -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.""" diff --git a/PyAres/Demo/rotary_mixer.py b/PyAres/Demo/Devices/rotary_mixer.py similarity index 70% rename from PyAres/Demo/rotary_mixer.py rename to PyAres/Demo/Devices/rotary_mixer.py index 4f15dea..1d41342 100644 --- a/PyAres/Demo/rotary_mixer.py +++ b/PyAres/Demo/Devices/rotary_mixer.py @@ -1,4 +1,4 @@ -from PyAres import AresDeviceService, AresDataType, DeviceSchemaEntry, DeviceCommandDescriptor +from PyAres import * class RotaryMixer: def __init__(self, speed): @@ -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 @@ -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 diff --git a/PyAres/Demo/Planners/airship_planner.py b/PyAres/Demo/Planners/airship_planner.py new file mode 100644 index 0000000..f4016cb --- /dev/null +++ b/PyAres/Demo/Planners/airship_planner.py @@ -0,0 +1,187 @@ +from PyAres import AresPlannerService, PlanRequest, PlanResponse, AresDataType +import random +import time +from enum import Enum + +class Planners(Enum): + RANDOM_PLANNER = 1 + TRADITIONAL_PLANNER = 2 + SEARCH_AND_DESTROY_PLANNER = 3 + +def coord_to_tuple(coord: str): + """Converts 'B3' to (1, 2).""" + return (ord(coord[0].upper()) - ord('A'), int(coord[1:]) - 1) + +def tuple_to_coord(tup: tuple): + """Converts (1, 2) to 'B3'.""" + return f"{chr(ord('A') + tup[0])}{tup[1] + 1}" + +def generate_all_coords(board_size=10): + """Generates all coordinates from 'A1' to 'J10'.""" + return [tuple_to_coord((r, c)) for r in range(board_size) for c in range(board_size)] + +def convert_param_history(param_history: list): + converted_history = [] + for value in param_history: + letter = value.planned_value[0] + converted_value = ord(letter) + 1 - ord('A') + converted_history.append((converted_value, int(value.planned_value[1:]))) + return converted_history + +def get_random_shot(param_history: list): + """ Chooses a random, un-shot-at coordinate. """ + converted_param_history = convert_param_history(param_history) + all_possible_shots = [(c, r) for r in range(1,11) for c in range(1, 11)] + available_shots = list(set(all_possible_shots) - set(converted_param_history)) + print(f"{len(available_shots)}/{len(all_possible_shots)}") + + if not available_shots: + return None + + return random.choice(available_shots) + +def get_traditional_search_shot(param_history: list): + """ Uses an extremely basic traditional search algorithm, moving across the board """ + shot_number = len(param_history) + all_possible_shots = [(c, r) for r in range(1,11) for c in range(1, 11)] + return all_possible_shots[shot_number] + +def get_search_and_destroy_shot(request: PlanRequest): + """A 'Hunt/Target' planner for Airship.""" + param = request.parameters[0] + shot_history = [p.planned_value for p in param.param_history] + shot_results = request.analysis_results # 0.0=Miss, 1.0=Hit, 2.0=Sunk + active_hits = [] + + # --- Identify All Active Hits --- + for i in range(len(shot_history)): + coord = shot_history[i] + result = shot_results[i] + + is_sunk = False + for j in range(i, len(shot_history)): + if shot_results[j] == 2.0 and shot_history[j] == coord: + is_sunk = True + active_hits = [] + break + + if result == 1.0 and not is_sunk: + active_hits.append(coord) + + # Get Unique active hits and sort them for consistency + active_hits = sorted(list(set(active_hits))) + next_shot = None + + # --- TARGET MODE --- + if active_hits: + print(f"Target Mode -> Active Hits: {active_hits}") + + is_horizontal = False + is_vertical = False + + if len(active_hits) >= 2: + #Convert the first two active hits to (row, col) tuples + r1, c1 = coord_to_tuple(active_hits[0]) + r2, c2 = coord_to_tuple(active_hits[1]) + + if r1 == r2: + is_horizontal = True + + elif c1 == c2: + is_vertical = True + + potential_targets = [] + + if is_horizontal or is_vertical: + print(f"Orientation Known! {'Horizontal' if is_horizontal else 'Vertical'}") + hit_tuples = [coord_to_tuple(c) for c in active_hits] + + if is_horizontal: + min_col = min(c for r, c in hit_tuples) + max_col = max(c for r, c in hit_tuples) + row = hit_tuples[0][0] + + potential_targets.append((row, min_col - 1)) + potential_targets.append((row, max_col + 1)) + + elif is_vertical: + min_row = min(r for r, c in hit_tuples) + max_row = max(r for r, c in hit_tuples) + col = hit_tuples[0][1] + + potential_targets.append((min_row - 1, col)) + potential_targets.append((max_row + 1, col)) + + else: + active_hit_coord = active_hits[0] + row, col = coord_to_tuple(active_hit_coord) + potential_targets.extend([(row - 1, col), (row + 1, col), (row, col - 1), (row, col + 1)]) + + valid_targets = [] + for r, c in potential_targets: + if 0 <= r < 10 and 0 <= c < 10: + coord = tuple_to_coord((r,c)) + if coord not in shot_history: + valid_targets.append(coord) + + if valid_targets: + next_shot = random.choice(valid_targets) + print(f"Targeting next logical square: {next_shot}") + + # --- HUNT MODE --- + if next_shot is None: + if active_hits: + print("Target mode exhausted (likely cornered the ship). Returning to Hunt Mode... ") + else: + print("Hunt Mode -> Searching for a new target.... ") + + available_shots = list(set(generate_all_coords()) - set(shot_history)) + hunt_candidates = [c for c in available_shots if (coord_to_tuple(c)[0] + coord_to_tuple(c)[1] % 2 == 0)] + + if hunt_candidates: + next_shot = random.choice(hunt_candidates) + + elif available_shots: + next_shot = random.choice(available_shots) + + else: + #Game Over + next_shot = "A1" + + print(f"Planner requesting fire at: {next_shot}") + time.sleep(0.25) + return PlanResponse(parameter_names=[param.name], parameter_values=[next_shot]) + +def plan(request: PlanRequest) -> PlanResponse: + #For an "Airship" game we should only ever have one parameter, which is our coordinate + param = request.parameters[0] + #Get next shot + if(param.planner_name == Planners.RANDOM_PLANNER.name): + shot = get_random_shot(param.param_history) + elif(param.planner_name == Planners.TRADITIONAL_PLANNER.name): + shot = get_traditional_search_shot(param.param_history) + else: + response = get_search_and_destroy_shot(request) + return response + + time.sleep(0.25) + letter = chr(ord('A') - 1 + shot[0]) + shot_string = f"{letter}{shot[1]}" + print(f"Requesting Fire at {shot_string}") + + response = PlanResponse(parameter_names=[param.name], parameter_values=[shot_string]) + return response + +if __name__ == "__main__": + name = "Airship Planner Service" + description = "A planner service that provides some basic algorithms for playing Airship." + planner = AresPlannerService(plan, name, description, "1.0.0", port=8003) + + #Add Supported Types + planner.add_supported_type(AresDataType.STRING) + + planner.add_planner_option(Planners.RANDOM_PLANNER.name, "Randomly shoots at an un-shot-at coordinate", "1.0.0") + planner.add_planner_option(Planners.TRADITIONAL_PLANNER.name, "Follows a very basic traditional search pattern", "1.0.0") + planner.add_planner_option(Planners.SEARCH_AND_DESTROY_PLANNER.name, "Searches for ships with random shots, destroys ships upon locating them.", "1.0.0") + + planner.start() \ No newline at end of file diff --git a/PyAres/Demo/planner_test.py b/PyAres/Demo/Planners/planner_test.py similarity index 100% rename from PyAres/Demo/planner_test.py rename to PyAres/Demo/Planners/planner_test.py diff --git a/PyAres/Demo/planner_test_tools_demo.py b/PyAres/Demo/Planners/planner_test_tools_demo.py similarity index 100% rename from PyAres/Demo/planner_test_tools_demo.py rename to PyAres/Demo/Planners/planner_test_tools_demo.py diff --git a/PyAres/Demo/planner_wiki.py b/PyAres/Demo/Planners/planner_wiki.py similarity index 100% rename from PyAres/Demo/planner_wiki.py rename to PyAres/Demo/Planners/planner_wiki.py diff --git a/PyAres/Device/__init__.py b/PyAres/Device/__init__.py index 707f209..dc4fd30 100644 --- a/PyAres/Device/__init__.py +++ b/PyAres/Device/__init__.py @@ -1,3 +1,5 @@ from .device_models import DeviceCommandDescriptor from .device_models import DeviceSchemaEntry +from .device_models import StatusCode +from .device_models import DeviceCommandResponse from .device_service import AresDeviceService \ No newline at end of file diff --git a/PyAres/Device/device_models.py b/PyAres/Device/device_models.py index 370b788..b1ee00a 100644 --- a/PyAres/Device/device_models.py +++ b/PyAres/Device/device_models.py @@ -1,10 +1,15 @@ -from typing import Dict, Union, Optional +from enum import Enum +from dataclasses import dataclass, field +from typing import Dict, Union, Optional, Any from ..Models import ares_data_models class DeviceSchemaEntry: """ A class that describes an input or output parameter for a device command """ - def __init__(self, type: ares_data_models.AresDataType, description: str = "", unit: str = "", optional: bool = False, + def __init__(self, type: ares_data_models.AresDataType, + description: str = "", + unit: str = "", + optional: bool = False, constraints: Union[list[int], list[float], list[str]] = [], quantity_schema: Optional[ares_data_models.QuantitySchema] = None, struct_schema: Optional[Dict[str, 'DeviceSchemaEntry']] = None, @@ -54,3 +59,20 @@ def __init__(self, name: str, description: str, input_schema: Dict[str, DeviceSc self.description = description self.input_schema = input_schema self.output_schema = output_schema + +class StatusCode(Enum): + STATUS_UNSPECIFIED = 0 + COMMAND_SUCCESS = 1 + SUCCESS_WITH_WARNINGS = 2 + COMMAND_FAILED = 3 + INVALID_COMMAND = 4 + HARDWARE_FAULT = 5 + EMERGENCY_STOP = 6 + OUT_OF_RANGE = 7 + PARAMETERS_UNACHIEVEABLE = 8 + +@dataclass +class DeviceCommandResponse: + response: Union[Dict[str, Any], Any] + error_string: str = "" + status_code: StatusCode = StatusCode.STATUS_UNSPECIFIED diff --git a/PyAres/Device/device_service.py b/PyAres/Device/device_service.py index ac6ace1..a6bb873 100644 --- a/PyAres/Device/device_service.py +++ b/PyAres/Device/device_service.py @@ -14,16 +14,18 @@ from ares_datamodel import ares_struct_pb2 from google.protobuf import empty_pb2 -from .device_models import DeviceCommandDescriptor +from .device_models import DeviceCommandDescriptor, DeviceCommandResponse, StatusCode from ..Utils import ares_device_command_utils from ..Utils import ares_data_schema_utils from ..Utils import ares_struct_utils from ..Utils import ares_value_utils from ..Utils import ares_data_type_utils +from ..Utils import device_status_code_utils # Type hint for the user's custom methods EnterSafeModeMethod = Callable[[], None] -DeviceCommandMethod = Callable[..., Dict[str, Any]] +AllowedReturns = Union[DeviceCommandResponse, Dict[str, Any], Any] +DeviceCommandMethod = Callable[..., AllowedReturns] DeviceStateMethod = Callable[[], Dict[str, Any]] class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServicer): @@ -85,18 +87,44 @@ def ExecuteCommand(self, request: device_service.ExecuteCommandRequest, context) provided_param_dict = ares_struct_utils.ares_struct_to_dict(request.arguments) try: result : Dict[str, Any] = method(**provided_param_dict) + except Exception as e: response.success = False response.error = f"Command '{request.command_name}' failed: {e}" return response - if isinstance(result, dict): - for key, value in result.items(): + # Modern devices should respond with a device command response + if isinstance(result, DeviceCommandResponse): + response.status_code = device_status_code_utils.python_status_code_to_proto_status_code(result.status_code) + response.success = device_status_code_utils.determine_success(result.status_code) + + if isinstance(result.response, dict): + for key, value in result.response.items(): ares_struct_utils.add_value_to_struct(response.result.struct_value, key, ares_value_utils.create_ares_value(value)) - else: - response.result.CopyFrom(ares_value_utils.create_ares_value(result)) - response.success = True + else: + response.result.CopyFrom(ares_value_utils.create_ares_value(result)) + + # Legacy device responses will only send back the value as the response, ensure backwards compatability + else: + # Keep a backup of the original formatting function + formatwarning_orig = warnings.formatwarning + + # Override it to force the source code line to be empty + warnings.formatwarning = lambda message, category, filename, lineno, line=None: \ + formatwarning_orig(message, category, filename, lineno, line='') + + warnings.warn("Returning raw values or dictionaries directly for device commands is deprecated. The new standard is to return a DeviceCommandResponse object instead. Please consider updating your device to use this standard.", FutureWarning) + + if isinstance(result, dict): + for key, value in result.items(): + ares_struct_utils.add_value_to_struct(response.result.struct_value, key, ares_value_utils.create_ares_value(value)) + + else: + response.result.CopyFrom(ares_value_utils.create_ares_value(result)) + + response.success = True + return response else: diff --git a/PyAres/Planning/planner_models.py b/PyAres/Planning/planner_models.py index e94f143..0dc13f5 100644 --- a/PyAres/Planning/planner_models.py +++ b/PyAres/Planning/planner_models.py @@ -16,7 +16,7 @@ def __init__(self, planned_value: Any, achieved_value: Any): def __str__(self): return (f"ParameterHistoryItem object with:\n" f" planned_value: {self.planned_value}\n" - f" acheived_value: {self.achieved_value}\n") + f" achieved_value: {self.achieved_value}\n") def __repr__(self) -> str: return self.__str__() diff --git a/PyAres/Utils/device_status_code_utils.py b/PyAres/Utils/device_status_code_utils.py new file mode 100644 index 0000000..1d268e9 --- /dev/null +++ b/PyAres/Utils/device_status_code_utils.py @@ -0,0 +1,19 @@ +from ..Device import StatusCode +from ares_datamodel import command_status_code_pb2 +from typing import cast + +def python_status_code_to_proto_status_code(py_value: StatusCode) -> command_status_code_pb2.CommandStatusCode: + """ A method to convert from the python AresDataType class to the protobuf version """ + val = cast( command_status_code_pb2.CommandStatusCode, py_value.value) + return val + +def proto_status_code_to_python_status_code(proto_value: command_status_code_pb2.CommandStatusCode) -> StatusCode: + """ A method to convert from the protobuf AresDataType class to the python version """ + return StatusCode(proto_value) + +def determine_success(code: StatusCode) -> bool: + if code == StatusCode.COMMAND_SUCCESS or code == StatusCode.SUCCESS_WITH_WARNGINGS: + return True + + else: + return False \ No newline at end of file diff --git a/PyAres/__init__.py b/PyAres/__init__.py index 588c9e0..eea2b6d 100644 --- a/PyAres/__init__.py +++ b/PyAres/__init__.py @@ -10,6 +10,8 @@ from .Device import AresDeviceService from .Device import DeviceCommandDescriptor from .Device import DeviceSchemaEntry +from .Device import DeviceCommandResponse +from .Device import StatusCode from .Models import AresDataType from .Models import Outcome from .Models import AresSchemaEntry From ca159aa24539e3fb85a5beee1d71f96f40654c5f Mon Sep 17 00:00:00 2001 From: Nick Kleiner Date: Wed, 10 Jun 2026 14:21:43 -0400 Subject: [PATCH 02/12] Fixed a an error where we provided the full device response instead of the value to the ares value util method --- PyAres/Device/device_service.py | 2 +- PyAres/Utils/device_status_code_utils.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PyAres/Device/device_service.py b/PyAres/Device/device_service.py index a6bb873..a0ed106 100644 --- a/PyAres/Device/device_service.py +++ b/PyAres/Device/device_service.py @@ -103,7 +103,7 @@ def ExecuteCommand(self, request: device_service.ExecuteCommandRequest, context) ares_struct_utils.add_value_to_struct(response.result.struct_value, key, ares_value_utils.create_ares_value(value)) else: - response.result.CopyFrom(ares_value_utils.create_ares_value(result)) + response.result.CopyFrom(ares_value_utils.create_ares_value(result.response)) # Legacy device responses will only send back the value as the response, ensure backwards compatability else: diff --git a/PyAres/Utils/device_status_code_utils.py b/PyAres/Utils/device_status_code_utils.py index 1d268e9..fbcde35 100644 --- a/PyAres/Utils/device_status_code_utils.py +++ b/PyAres/Utils/device_status_code_utils.py @@ -3,12 +3,12 @@ from typing import cast def python_status_code_to_proto_status_code(py_value: StatusCode) -> command_status_code_pb2.CommandStatusCode: - """ A method to convert from the python AresDataType class to the protobuf version """ + """ A method to convert from the python StatusCode enum class to the protobuf version """ val = cast( command_status_code_pb2.CommandStatusCode, py_value.value) return val def proto_status_code_to_python_status_code(proto_value: command_status_code_pb2.CommandStatusCode) -> StatusCode: - """ A method to convert from the protobuf AresDataType class to the python version """ + """ A method to convert from the protobuf StatusCode enum class to the python version """ return StatusCode(proto_value) def determine_success(code: StatusCode) -> bool: From 6b35c59602dae2c20a494775c0f6ec0e3a03c0ff Mon Sep 17 00:00:00 2001 From: Nick Kleiner Date: Wed, 10 Jun 2026 19:20:16 -0400 Subject: [PATCH 03/12] Updated a few names to make things more consistent across the PyAres library --- PyAres/Analyzing/__init__.py | 4 ++-- PyAres/Analyzing/analysis_service.py | 6 +++--- PyAres/Analyzing/analyzer_models.py | 2 +- PyAres/Demo/analyzer_test.py | 6 +++--- PyAres/Demo/analyzer_wiki.py | 8 ++++---- PyAres/Planning/planner_models.py | 4 ++-- tests/test_analyzer.py | 6 +++--- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/PyAres/Analyzing/__init__.py b/PyAres/Analyzing/__init__.py index 1d934f2..3a485f4 100644 --- a/PyAres/Analyzing/__init__.py +++ b/PyAres/Analyzing/__init__.py @@ -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", diff --git a/PyAres/Analyzing/analysis_service.py b/PyAres/Analyzing/analysis_service.py index 96ba532..5c9646c 100644 --- a/PyAres/Analyzing/analysis_service.py +++ b/PyAres/Analyzing/analysis_service.py @@ -23,10 +23,10 @@ # Import python models from ..Models import ares_data_models, RequestMetadata from ..Models import AresSchemaEntry -from .analyzer_models import AnalysisRequest, Analysis, InfoResponse +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): """ @@ -73,7 +73,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" diff --git a/PyAres/Analyzing/analyzer_models.py b/PyAres/Analyzing/analyzer_models.py index 1fe64f6..2fd90aa 100644 --- a/PyAres/Analyzing/analyzer_models.py +++ b/PyAres/Analyzing/analyzer_models.py @@ -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 = ""): diff --git a/PyAres/Demo/analyzer_test.py b/PyAres/Demo/analyzer_test.py index 019be1b..38ef71d 100644 --- a/PyAres/Demo/analyzer_test.py +++ b/PyAres/Demo/analyzer_test.py @@ -1,6 +1,6 @@ -from PyAres import AresAnalyzerService, AnalysisRequest, Analysis, AresDataType, Outcome +from PyAres import AresAnalyzerService, AnalysisRequest, AnalysisResponse, AresDataType, Outcome -def analyze(request: AnalysisRequest) -> Analysis: +def analyze(request: AnalysisRequest) -> AnalysisResponse: #Custom Analysis Logic temperature = request.inputs.get("Temperature") @@ -10,7 +10,7 @@ def analyze(request: AnalysisRequest) -> Analysis: print(f"Temperature: {temperature}") - analysis = Analysis(result=temperature) + analysis = AnalysisResponse(result=temperature) return analysis diff --git a/PyAres/Demo/analyzer_wiki.py b/PyAres/Demo/analyzer_wiki.py index d3f8077..57cab67 100644 --- a/PyAres/Demo/analyzer_wiki.py +++ b/PyAres/Demo/analyzer_wiki.py @@ -1,12 +1,12 @@ -from PyAres import AresAnalyzerService, AnalysisRequest, Analysis, AresDataType, Outcome +from PyAres import AresAnalyzerService, AnalysisRequest, AnalysisResponse, AresDataType, Outcome -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}") @@ -15,7 +15,7 @@ def analyze_sample(request: AnalysisRequest) -> Analysis: is_success = calculated_score > 10.0 # Define success criteria # 3. Return Result - return Analysis(result=calculated_score, outcome=Outcome.SUCCESS) + return AnalysisResponse(result=calculated_score, outcome=Outcome.SUCCESS) if __name__ == "__main__": service = AresAnalyzerService( diff --git a/PyAres/Planning/planner_models.py b/PyAres/Planning/planner_models.py index e94f143..a0e1619 100644 --- a/PyAres/Planning/planner_models.py +++ b/PyAres/Planning/planner_models.py @@ -166,7 +166,7 @@ def __init__(self, parameter_names: Optional[list[str]] = None, parameter_values: Optional[list] = None, parameter_data: Optional[dict[str,Any]] = None, - planning_outcome: Outcome = Outcome.SUCCESS, + outcome: Outcome = Outcome.SUCCESS, error_string: str = ""): """ Initializes a PlanResponse. Using either lists of names and values or a python dictonary of name:value pairs @@ -189,7 +189,7 @@ def __init__(self, else: raise ValueError("No values to assign!") - self.outcome = planning_outcome + self.outcome = outcome self.error_string = error_string def __str__(self): diff --git a/tests/test_analyzer.py b/tests/test_analyzer.py index b2824d9..b27916c 100644 --- a/tests/test_analyzer.py +++ b/tests/test_analyzer.py @@ -1,7 +1,7 @@ import unittest from PyAres import AresAnalyzerService, Outcome from PyAres.Models import ares_data_models -from PyAres.Analyzing.analyzer_models import AnalysisRequest, Analysis +from PyAres.Analyzing.analyzer_models import AnalysisRequest, AnalysisResponse from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2 as analyzer_service from ares_datamodel.analyzing import analysis_pb2 from ares_datamodel import ares_data_type_pb2, ares_outcome_enum_pb2, ares_data_schema_pb2 @@ -28,11 +28,11 @@ class TestAresAnalyzerService(unittest.TestCase): def setUp(self): self.captured_request: AnalysisRequest | None = None - def dummy_analyze(request: AnalysisRequest) -> Analysis: + def dummy_analyze(request: AnalysisRequest) -> AnalysisResponse: self.captured_request = request # Return a valid Analysis object - return Analysis( + return AnalysisResponse( result=100.0, outcome=Outcome.SUCCESS ) From 325404e834ca297120b555e9f46fe2dfcb3a03d4 Mon Sep 17 00:00:00 2001 From: Nick Kleiner Date: Wed, 10 Jun 2026 19:24:15 -0400 Subject: [PATCH 04/12] Forgot this init update --- PyAres/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PyAres/__init__.py b/PyAres/__init__.py index 588c9e0..4fa5827 100644 --- a/PyAres/__init__.py +++ b/PyAres/__init__.py @@ -4,7 +4,7 @@ from .Planning import PlanningParameter from .Planning import ParameterHistoryItem from .Analyzing import AresAnalyzerService -from .Analyzing import Analysis +from .Analyzing import AnalysisResponse from .Analyzing import AnalysisRequest from .Analyzing import InfoResponse from .Device import AresDeviceService From c14590385cc63ae050ee6836a6698af644aa2317 Mon Sep 17 00:00:00 2001 From: Nick Kleiner Date: Wed, 10 Jun 2026 19:49:06 -0400 Subject: [PATCH 05/12] Added limits capabilities into PyAres --- PyAres/Analyzing/analysis_service.py | 8 ++++---- PyAres/Demo/analyzer_test.py | 4 ++-- PyAres/Device/device_service.py | 8 +++++--- PyAres/Models/__init__.py | 5 +++-- PyAres/Models/ares_data_models.py | 5 +++++ PyAres/Planning/planning_service.py | 7 ++++--- PyAres/Utils/ares_data_schema_utils.py | 9 +++++++-- PyAres/__init__.py | 3 ++- 8 files changed, 32 insertions(+), 17 deletions(-) diff --git a/PyAres/Analyzing/analysis_service.py b/PyAres/Analyzing/analysis_service.py index 96ba532..65e4dcf 100644 --- a/PyAres/Analyzing/analysis_service.py +++ b/PyAres/Analyzing/analysis_service.py @@ -21,8 +21,7 @@ from ..Utils import ares_outcome_utils # Import python models -from ..Models import ares_data_models, RequestMetadata -from ..Models import AresSchemaEntry +from ..Models import ares_data_models, RequestMetadata, Limits, AresSchemaEntry from .analyzer_models import AnalysisRequest, Analysis, InfoResponse # Type hints for the user's custom logic @@ -200,7 +199,7 @@ 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, optional: bool = True, constraints: Union[list[int], list[str], list[float]] = [], struct_schema: Optional[Dict[str, AresSchemaEntry]] = None, limits: Optional[Limits] = 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 @@ -212,8 +211,9 @@ 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 """ - self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, struct_schema) + self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, struct_schema, limits) def add_analysis_parameter(self, parameter_name: str, parameter_type: ares_data_models.AresDataType, optional: bool = False, struct_schema: Optional[Dict[str, AresSchemaEntry]] = None): """ diff --git a/PyAres/Demo/analyzer_test.py b/PyAres/Demo/analyzer_test.py index 019be1b..77246dd 100644 --- a/PyAres/Demo/analyzer_test.py +++ b/PyAres/Demo/analyzer_test.py @@ -1,4 +1,4 @@ -from PyAres import AresAnalyzerService, AnalysisRequest, Analysis, AresDataType, Outcome +from PyAres import AresAnalyzerService, AnalysisRequest, Analysis, AresDataType, Outcome, Limits def analyze(request: AnalysisRequest) -> Analysis: #Custom Analysis Logic @@ -23,7 +23,7 @@ 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.add_setting(setting_name="", setting_type=AresDataType.NULL, optional=True, constraints=[], limits=Limits(0, 10000)) pythonDemoAnalyzer.start(wait_for_termination=True) pythonDemoAnalyzer.start() \ No newline at end of file diff --git a/PyAres/Device/device_service.py b/PyAres/Device/device_service.py index ac6ace1..ca3ab3e 100644 --- a/PyAres/Device/device_service.py +++ b/PyAres/Device/device_service.py @@ -3,7 +3,7 @@ import time import warnings from concurrent import futures -from typing import Dict, Callable, Awaitable, Union, Any +from typing import Dict, Callable, Awaitable, Union, Any, Optional from ares_datamodel.device.remote import ares_remote_device_service_pb2 as device_service from ares_datamodel.device.remote import ares_remote_device_service_pb2_grpc as device_service_grpc @@ -15,6 +15,7 @@ from google.protobuf import empty_pb2 from .device_models import DeviceCommandDescriptor +from ..Models import Limits from ..Utils import ares_device_command_utils from ..Utils import ares_data_schema_utils from ..Utils import ares_struct_utils @@ -274,7 +275,7 @@ def add_new_command(self, cmd_descriptor: DeviceCommandDescriptor, method): self._service_wrapper._command_methods[cmd_descriptor.name] = method self._service_wrapper._commands.append(cmd_descriptor) - def add_setting(self, setting_name: str, setting_value: Any, optional: bool = True, constraints: Union[list[int], list[str], list[float]] = []): + def add_setting(self, setting_name: str, setting_value: Any, optional: Optional[bool] = True, constraints: Optional[Union[list[int], list[str], list[float]]] = [], limits: Optional[Limits] = None): """ Adds a new device setting to be reported to ARES when your devices capabilities are requested. @@ -283,9 +284,10 @@ def add_setting(self, setting_name: str, setting_value: Any, optional: bool = Tr setting_value (Any): The default value of the setting optional (bool): Whether the setting is optional constraints: An optional list of values to constrain the available setting choices. Can be integers, floats, or strings. + limits: An optional Limits object for specifying minimum and maximum values """ setting_type = ares_data_type_utils.determine_python_ares_data_type(setting_value) - self._service_wrapper._setting_schema[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints) + self._service_wrapper._setting_schema[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, limits=limits) new_ares_value = ares_value_utils.create_ares_value(setting_value) self._service_wrapper._current_settings[setting_name] = new_ares_value diff --git a/PyAres/Models/__init__.py b/PyAres/Models/__init__.py index 525bb74..342beba 100644 --- a/PyAres/Models/__init__.py +++ b/PyAres/Models/__init__.py @@ -1,4 +1,4 @@ -from .ares_data_models import AresDataType, Outcome, RequestMetadata, AresSchemaEntry, Quantity, QuantitySchema +from .ares_data_models import AresDataType, Outcome, RequestMetadata, AresSchemaEntry, Quantity, QuantitySchema, Limits __all__ = [ "AresDataType", @@ -6,5 +6,6 @@ "RequestMetadata", "AresSchemaEntry", "Quantity", - "QuantitySchema" + "QuantitySchema", + "Limits" ] \ No newline at end of file diff --git a/PyAres/Models/ares_data_models.py b/PyAres/Models/ares_data_models.py index 0d34a51..ac6e320 100644 --- a/PyAres/Models/ares_data_models.py +++ b/PyAres/Models/ares_data_models.py @@ -56,6 +56,11 @@ def from_default_values(cls): default = request_metadata_pb2.RequestMetadata(system_name="TEST SYSTEM", campaign_name="TEST CAMPAIGN", campaign_id="TEST ID", experiment_id="TEST EXPERIMENT ID") return cls(default) +@dataclass +class Limits: + minimum: float + maximum: float + @dataclass class Quantity: scalar: float diff --git a/PyAres/Planning/planning_service.py b/PyAres/Planning/planning_service.py index 34cbef8..746d8dd 100644 --- a/PyAres/Planning/planning_service.py +++ b/PyAres/Planning/planning_service.py @@ -22,7 +22,7 @@ from ..Utils import ares_outcome_utils # Import python models -from ..Models import ares_data_models +from ..Models import ares_data_models, Limits from .planner_models import * # Type hint for the user's custom planning logic @@ -210,7 +210,7 @@ def add_planner_option(self, planner_name: str, planner_description: str, planne """ self._service_wrapper._planner_options.append(planner_pb2.Planner(planner_name=planner_name, description=planner_description, version=planner_version)) - def add_setting(self, setting_name: str, setting_type: ares_data_models.AresDataType, optional: bool = True, constraints: Union[list[int], list[str], list[float]] = []): + def add_setting(self, setting_name: str, setting_type: ares_data_models.AresDataType, optional: bool = True, constraints: Optional[Union[list[int], list[str], list[float]]] = [], limits: Optional[Limits] = None): """ Adds a planner setting to be reported to ARES when your services capabilities are requested. @@ -219,8 +219,9 @@ def add_setting(self, setting_name: str, setting_type: ares_data_models.AresData setting_type (AresDataType): The type of this settings value. 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. + limits: An optional Limits object for specifying minimum and maximum values """ - self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints) + self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, limits=limits) def add_supported_type(self, type: ares_data_models.AresDataType): """ diff --git a/PyAres/Utils/ares_data_schema_utils.py b/PyAres/Utils/ares_data_schema_utils.py index f08a5bd..2d9461f 100644 --- a/PyAres/Utils/ares_data_schema_utils.py +++ b/PyAres/Utils/ares_data_schema_utils.py @@ -1,7 +1,7 @@ from typing import Union, Dict, Optional, List from ares_datamodel import ares_data_schema_pb2 from ..Models import ares_data_models -from ..Models.ares_data_models import AresSchemaEntry +from ..Models.ares_data_models import AresSchemaEntry, Limits def convert_ares_schema_entry_to_proto(entry: AresSchemaEntry) -> ares_data_schema_pb2.AresValueSchema: proto_entry = create_settings_schema_entry(entry.type, entry.optional, entry.choices, entry.struct_schema) @@ -29,7 +29,8 @@ def create_settings_schema_entry( setting_type: ares_data_models.AresDataType, optional: bool, choices: Union[list[str], list[int], list[float]], - struct_schema: Optional[Dict[str, AresSchemaEntry]] = None) -> ares_data_schema_pb2.AresValueSchema: + struct_schema: Optional[Dict[str, AresSchemaEntry]] = None, + limits: Optional[Limits] = None) -> ares_data_schema_pb2.AresValueSchema: """ Creates a protobuf AresValueSchema message from the provided setting details. @@ -56,4 +57,8 @@ def create_settings_schema_entry( for key, value in struct_schema.items(): schema_entry.struct_schema.fields[key].CopyFrom(convert_ares_schema_entry_to_proto(value)) + if limits is not None: + schema_entry.limits.minimum = limits.minimum + schema_entry.limits.maximum = limits.maximum + return schema_entry diff --git a/PyAres/__init__.py b/PyAres/__init__.py index 588c9e0..1a40536 100644 --- a/PyAres/__init__.py +++ b/PyAres/__init__.py @@ -14,4 +14,5 @@ from .Models import Outcome from .Models import AresSchemaEntry from .Models import Quantity -from .Models import QuantitySchema \ No newline at end of file +from .Models import QuantitySchema +from .Models import Limits \ No newline at end of file From df1bf5fbca4ee7d3a69870883595a1ab1bd4b1c4 Mon Sep 17 00:00:00 2001 From: Nick Kleiner Date: Wed, 10 Jun 2026 19:49:28 -0400 Subject: [PATCH 06/12] Updated README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 296ce11..ca943f0 100644 --- a/README.md +++ b/README.md @@ -59,10 +59,10 @@ Analyzers can be initialized using the AresAnalyzerService class. Below is a bas ```Python from PyAres import AresAnalyzerService from PyAres import AnalysisRequest -from PyAres import Analysis +from PyAres import AnalysisResponse from PyAres import AresDataType -def analyze(request: AnalysisRequest) -> Analysis: +def analyze(request: AnalysisRequest) -> AnalysisResponse: #Custom Analysis Logic growth = request.inputs.get("Growth") temperature = request.inputs.get("Temperature") @@ -70,7 +70,7 @@ def analyze(request: AnalysisRequest) -> Analysis: print(f"Growth: {growth}") print(f"Temperature: {temperature}") - analysis = Analysis(result=growth, success=True) + analysis = AnalysisResponse(result=growth, success=True) return analysis From 5c5d11946a8ac098431403f044a090eddd3caf80 Mon Sep 17 00:00:00 2001 From: Nick Kleiner Date: Wed, 10 Jun 2026 20:03:50 -0400 Subject: [PATCH 07/12] Added an example for limits. Added a constructor for easier creation. --- PyAres/Demo/analyzer_wiki.py | 3 ++- PyAres/Models/ares_data_models.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/PyAres/Demo/analyzer_wiki.py b/PyAres/Demo/analyzer_wiki.py index d3f8077..c90b223 100644 --- a/PyAres/Demo/analyzer_wiki.py +++ b/PyAres/Demo/analyzer_wiki.py @@ -1,4 +1,4 @@ -from PyAres import AresAnalyzerService, AnalysisRequest, Analysis, AresDataType, Outcome +from PyAres import AresAnalyzerService, AnalysisRequest, Analysis, AresDataType, Outcome, Limits def analyze_sample(request: AnalysisRequest) -> Analysis: # 1. Extract inputs @@ -27,5 +27,6 @@ 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.FLOAT, False, None, None, Limits(1, 500)) service.start() \ No newline at end of file diff --git a/PyAres/Models/ares_data_models.py b/PyAres/Models/ares_data_models.py index ac6e320..6a06019 100644 --- a/PyAres/Models/ares_data_models.py +++ b/PyAres/Models/ares_data_models.py @@ -56,10 +56,10 @@ def from_default_values(cls): default = request_metadata_pb2.RequestMetadata(system_name="TEST SYSTEM", campaign_name="TEST CAMPAIGN", campaign_id="TEST ID", experiment_id="TEST EXPERIMENT ID") return cls(default) -@dataclass class Limits: - minimum: float - maximum: float + def __init__(self, minimum: float, maximum: float): + self.minimum = minimum + self.maximum = maximum @dataclass class Quantity: From 6e6e798537f8ee92709b7064bcbf4d4cdccd71f6 Mon Sep 17 00:00:00 2001 From: Nick Kleiner Date: Thu, 11 Jun 2026 09:28:02 -0400 Subject: [PATCH 08/12] Added ability to handle default values in both analyzers and planners for their settings --- PyAres/Analyzing/analysis_service.py | 28 +++++++++++++++++++++----- PyAres/Demo/analyzer_wiki.py | 2 +- PyAres/Demo/planner_test.py | 14 ++++++------- PyAres/Device/device_service.py | 4 +++- PyAres/Planning/planning_service.py | 17 ++++++++++++++-- PyAres/Utils/ares_data_schema_utils.py | 10 ++++++--- 6 files changed, 56 insertions(+), 19 deletions(-) diff --git a/PyAres/Analyzing/analysis_service.py b/PyAres/Analyzing/analysis_service.py index 65e4dcf..f68d0fd 100644 --- a/PyAres/Analyzing/analysis_service.py +++ b/PyAres/Analyzing/analysis_service.py @@ -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 @@ -11,7 +11,6 @@ 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 @@ -19,6 +18,7 @@ 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, Limits, AresSchemaEntry @@ -199,7 +199,13 @@ 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, limits: Optional[Limits] = 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): """ 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 @@ -213,7 +219,19 @@ def add_setting(self, setting_name: str, setting_type: ares_data_models.AresData 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 """ - self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, struct_schema, limits) + + 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) + + else: + self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, struct_schema, limits) + def add_analysis_parameter(self, parameter_name: str, parameter_type: ares_data_models.AresDataType, optional: bool = False, struct_schema: Optional[Dict[str, AresSchemaEntry]] = None): """ @@ -247,7 +265,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 Analyzer Service on port {self._port}...") self._server.start() if wait_for_termination: diff --git a/PyAres/Demo/analyzer_wiki.py b/PyAres/Demo/analyzer_wiki.py index c90b223..27f8bac 100644 --- a/PyAres/Demo/analyzer_wiki.py +++ b/PyAres/Demo/analyzer_wiki.py @@ -27,6 +27,6 @@ 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.FLOAT, False, None, None, Limits(1, 500)) + service.add_setting("Random Setting", AresDataType.NUMBER, 250, False, None, None, Limits(1, 500)) service.start() \ No newline at end of file diff --git a/PyAres/Demo/planner_test.py b/PyAres/Demo/planner_test.py index 9e642cd..8c9b78d 100644 --- a/PyAres/Demo/planner_test.py +++ b/PyAres/Demo/planner_test.py @@ -70,13 +70,13 @@ def gradual_planner(param: PlanningParameter) -> float: pythonDemoPlanner.add_planner_option("Gradual Planner", "A planner that gradually increases a value based on the values history", "1.0.0") #Add Planner Settings - pythonDemoPlanner.add_setting("String Setting", AresDataType.STRING) - pythonDemoPlanner.add_setting("Number Setting", AresDataType.NUMBER) - pythonDemoPlanner.add_setting("Boolean Setting", AresDataType.BOOLEAN) - pythonDemoPlanner.add_setting("String Array Setting", AresDataType.STRING_ARRAY) - pythonDemoPlanner.add_setting("Constrained Strings", AresDataType.STRING_ARRAY, True, ["One", "Two", "Three"]) - pythonDemoPlanner.add_setting("Number Array Setting", AresDataType.NUMBER_ARRAY) - pythonDemoPlanner.add_setting("Constrained Numbers", AresDataType.NUMBER_ARRAY, True, [1, 2, 3]) + pythonDemoPlanner.add_setting("String Setting", AresDataType.STRING, default_value="Default dood") + pythonDemoPlanner.add_setting("Number Setting", AresDataType.NUMBER, default_value=100.0) + pythonDemoPlanner.add_setting("Boolean Setting", AresDataType.BOOLEAN, default_value=False) + pythonDemoPlanner.add_setting("String Array Setting", AresDataType.STRING_ARRAY, default_value=["One", "Two", "Three"]) + pythonDemoPlanner.add_setting("Constrained Strings", AresDataType.STRING_ARRAY, optional=True, constraints=["One", "Two", "Three"]) + pythonDemoPlanner.add_setting("Number Array Setting", AresDataType.NUMBER_ARRAY, default_value=[1, 2, 3, 4, 5]) + pythonDemoPlanner.add_setting("Constrained Numbers", AresDataType.NUMBER_ARRAY, optional=True, constraints=[1, 2, 3]) #Set Planner Timeout pythonDemoPlanner.set_timeout(60) diff --git a/PyAres/Device/device_service.py b/PyAres/Device/device_service.py index ca3ab3e..b3a6107 100644 --- a/PyAres/Device/device_service.py +++ b/PyAres/Device/device_service.py @@ -275,7 +275,7 @@ def add_new_command(self, cmd_descriptor: DeviceCommandDescriptor, method): self._service_wrapper._command_methods[cmd_descriptor.name] = method self._service_wrapper._commands.append(cmd_descriptor) - def add_setting(self, setting_name: str, setting_value: Any, optional: Optional[bool] = True, constraints: Optional[Union[list[int], list[str], list[float]]] = [], limits: Optional[Limits] = None): + def add_setting(self, setting_name: str, setting_value: Optional[Any] = None, optional: bool = True, constraints: Union[list[int], list[str], list[float]] = [], limits: Optional[Limits] = None): """ Adds a new device setting to be reported to ARES when your devices capabilities are requested. @@ -288,6 +288,7 @@ def add_setting(self, setting_name: str, setting_value: Any, optional: Optional[ """ setting_type = ares_data_type_utils.determine_python_ares_data_type(setting_value) self._service_wrapper._setting_schema[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, limits=limits) + new_ares_value = ares_value_utils.create_ares_value(setting_value) self._service_wrapper._current_settings[setting_name] = new_ares_value @@ -301,6 +302,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}...") self._server.start() diff --git a/PyAres/Planning/planning_service.py b/PyAres/Planning/planning_service.py index 746d8dd..11e0d20 100644 --- a/PyAres/Planning/planning_service.py +++ b/PyAres/Planning/planning_service.py @@ -13,6 +13,7 @@ from ares_datamodel import ares_outcome_enum_pb2 from ares_datamodel.connection import connection_state_pb2 from ares_datamodel.connection import connection_info_pb2 +from ares_datamodel import ares_struct_pb2 # Import Utilities from ..Utils import ares_value_utils @@ -38,6 +39,7 @@ def __init__(self, service_name: str, description: str, version: str, timeout: i self._description: str = description self._version: str = version self._settings: Dict[str, ares_data_schema_pb2.AresValueSchema] = {} + self._current_settings: Dict[str, ares_struct_pb2.AresValue] = {} self._planner_options: list[planner_pb2.Planner] = [] self._supported_types: list[ares_data_type_pb2.AresDataType] = [] self._timeout: int = timeout @@ -210,18 +212,29 @@ def add_planner_option(self, planner_name: str, planner_description: str, planne """ self._service_wrapper._planner_options.append(planner_pb2.Planner(planner_name=planner_name, description=planner_description, version=planner_version)) - def add_setting(self, setting_name: str, setting_type: ares_data_models.AresDataType, optional: bool = True, constraints: Optional[Union[list[int], list[str], list[float]]] = [], limits: Optional[Limits] = None): + def add_setting(self, setting_name: str, + setting_type: ares_data_models.AresDataType, + default_value: Optional[Any] = None, + optional: Optional[bool] = True, + constraints: Optional[Union[list[int], list[str], list[float]]] = [], + limits: Optional[Limits] = None): """ Adds a planner setting to be reported to ARES when your services capabilities are requested. Args: setting_name (str): The name of the setting. setting_type (AresDataType): The type of this settings value. + default_value: The default value you want to be associated with this setting. Value must match the provided data type, and will be overwritten by ARES if it has another value stored. 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. limits: An optional Limits object for specifying minimum and maximum values """ - self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, limits=limits) + 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, optional, choices=constraints, limits=limits, default_value=default_ares_value) + + else: + self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, choices=constraints, limits=limits) def add_supported_type(self, type: ares_data_models.AresDataType): """ diff --git a/PyAres/Utils/ares_data_schema_utils.py b/PyAres/Utils/ares_data_schema_utils.py index 2d9461f..29461f1 100644 --- a/PyAres/Utils/ares_data_schema_utils.py +++ b/PyAres/Utils/ares_data_schema_utils.py @@ -1,5 +1,5 @@ -from typing import Union, Dict, Optional, List -from ares_datamodel import ares_data_schema_pb2 +from typing import Union, Dict, Optional, List, Any +from ares_datamodel import ares_data_schema_pb2, ares_struct_pb2 from ..Models import ares_data_models from ..Models.ares_data_models import AresSchemaEntry, Limits @@ -28,7 +28,8 @@ def convert_ares_schema_entry_to_proto(entry: AresSchemaEntry) -> ares_data_sche def create_settings_schema_entry( setting_type: ares_data_models.AresDataType, optional: bool, - choices: Union[list[str], list[int], list[float]], + default_value: Any = None, + choices: Union[list[str], list[int], list[float]] = None, struct_schema: Optional[Dict[str, AresSchemaEntry]] = None, limits: Optional[Limits] = None) -> ares_data_schema_pb2.AresValueSchema: """ @@ -61,4 +62,7 @@ def create_settings_schema_entry( schema_entry.limits.minimum = limits.minimum schema_entry.limits.maximum = limits.maximum + if isinstance(default_value, ares_struct_pb2.AresValue): + schema_entry.default_value.CopyFrom(default_value) + return schema_entry From 33cce16ab0d49c66880e58101ae7b6c3030b32e2 Mon Sep 17 00:00:00 2001 From: Nick Kleiner Date: Thu, 11 Jun 2026 09:38:03 -0400 Subject: [PATCH 09/12] Added some try and except logic to prevent crashes when bad settings are processed --- PyAres/Analyzing/analysis_service.py | 28 +++++++++++++++------------- PyAres/Device/device_service.py | 16 ++++++++++------ PyAres/Planning/planning_service.py | 14 +++++++++----- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/PyAres/Analyzing/analysis_service.py b/PyAres/Analyzing/analysis_service.py index f68d0fd..1b6a500 100644 --- a/PyAres/Analyzing/analysis_service.py +++ b/PyAres/Analyzing/analysis_service.py @@ -219,19 +219,21 @@ def add_setting(self, setting_name: str, 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 """ - - 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) + 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) - else: - self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, struct_schema, limits) - + else: + self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, struct_schema, limits) + + 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): """ @@ -265,7 +267,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 Analyzer Service on port {self._port}...") + print(f"Starting Ares Analysis Service on port {self._port}...") self._server.start() if wait_for_termination: diff --git a/PyAres/Device/device_service.py b/PyAres/Device/device_service.py index b3a6107..9d7ca64 100644 --- a/PyAres/Device/device_service.py +++ b/PyAres/Device/device_service.py @@ -286,12 +286,16 @@ def add_setting(self, setting_name: str, setting_value: Optional[Any] = None, op constraints: An optional list of values to constrain the available setting choices. Can be integers, floats, or strings. limits: An optional Limits object for specifying minimum and maximum values """ - setting_type = ares_data_type_utils.determine_python_ares_data_type(setting_value) - self._service_wrapper._setting_schema[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, limits=limits) + try: + setting_type = ares_data_type_utils.determine_python_ares_data_type(setting_value) + new_ares_value = ares_value_utils.create_ares_value(setting_value) - new_ares_value = ares_value_utils.create_ares_value(setting_value) - self._service_wrapper._current_settings[setting_name] = new_ares_value - + self._service_wrapper._setting_schema[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, choices=constraints, limits=limits, default_value=new_ares_value) + self._service_wrapper._current_settings[setting_name] = new_ares_value + + except Exception as e: + print(f"Exception when trying to create setting {setting_name}: {e}") + def start(self, wait_for_termination: bool = True): """ Starts the service on the specified port, and waits for termination. @@ -302,7 +306,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}...") self._server.start() diff --git a/PyAres/Planning/planning_service.py b/PyAres/Planning/planning_service.py index 11e0d20..fb2aeaf 100644 --- a/PyAres/Planning/planning_service.py +++ b/PyAres/Planning/planning_service.py @@ -229,12 +229,16 @@ def add_setting(self, setting_name: str, constraints: An optional list of values to constrain the available setting choices. Can be integers, strings, or floats. limits: An optional Limits object for specifying minimum and maximum values """ - 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, optional, choices=constraints, limits=limits, default_value=default_ares_value) + 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, optional, choices=constraints, limits=limits, default_value=default_ares_value) - else: - self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, choices=constraints, limits=limits) + else: + self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, choices=constraints, limits=limits) + + except Exception as e: + print(f"Encountered an exception while adding setting {setting_name}: {e}") def add_supported_type(self, type: ares_data_models.AresDataType): """ From 9ab3929b9e1b96f0cc7c8e86c46ccf9f542f73a7 Mon Sep 17 00:00:00 2001 From: Nick Kleiner Date: Thu, 11 Jun 2026 10:19:07 -0400 Subject: [PATCH 10/12] Added descriptions to settings to allow for tooltips to be displayed in ARES --- PyAres/Analyzing/analysis_service.py | 17 +++++++++++++---- PyAres/Demo/analyzer_wiki.py | 6 +++++- PyAres/Demo/planner_test.py | 10 +++++----- PyAres/Planning/planning_service.py | 8 +++++--- PyAres/Utils/ares_data_schema_utils.py | 6 +++++- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/PyAres/Analyzing/analysis_service.py b/PyAres/Analyzing/analysis_service.py index 1b6a500..d5bacae 100644 --- a/PyAres/Analyzing/analysis_service.py +++ b/PyAres/Analyzing/analysis_service.py @@ -199,13 +199,15 @@ def __init__(self, else: self._server.add_insecure_port(f'[::]:{self._port}') - def add_setting(self, setting_name: str, + 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): + 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 @@ -218,6 +220,7 @@ def add_setting(self, setting_name: str, 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. """ try: if default_value is not None: @@ -227,10 +230,16 @@ def add_setting(self, setting_name: str, choices=constraints, struct_schema=struct_schema, limits=limits, - default_value=default_ares_value) + default_value=default_ares_value, + description=description) else: - self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, struct_schema, limits) + 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}") diff --git a/PyAres/Demo/analyzer_wiki.py b/PyAres/Demo/analyzer_wiki.py index 27f8bac..10419f4 100644 --- a/PyAres/Demo/analyzer_wiki.py +++ b/PyAres/Demo/analyzer_wiki.py @@ -27,6 +27,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, 250, False, None, None, Limits(1, 500)) + + 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() \ No newline at end of file diff --git a/PyAres/Demo/planner_test.py b/PyAres/Demo/planner_test.py index 8c9b78d..6c0a838 100644 --- a/PyAres/Demo/planner_test.py +++ b/PyAres/Demo/planner_test.py @@ -70,12 +70,12 @@ def gradual_planner(param: PlanningParameter) -> float: pythonDemoPlanner.add_planner_option("Gradual Planner", "A planner that gradually increases a value based on the values history", "1.0.0") #Add Planner Settings - pythonDemoPlanner.add_setting("String Setting", AresDataType.STRING, default_value="Default dood") - pythonDemoPlanner.add_setting("Number Setting", AresDataType.NUMBER, default_value=100.0) - pythonDemoPlanner.add_setting("Boolean Setting", AresDataType.BOOLEAN, default_value=False) - pythonDemoPlanner.add_setting("String Array Setting", AresDataType.STRING_ARRAY, default_value=["One", "Two", "Three"]) + pythonDemoPlanner.add_setting("String Setting", AresDataType.STRING, default_value="Default String", description="This is a string setting") + pythonDemoPlanner.add_setting("Number Setting", AresDataType.NUMBER, default_value=100.0, description="This is a number setting") + pythonDemoPlanner.add_setting("Boolean Setting", AresDataType.BOOLEAN, description="This is a boolean setting") + pythonDemoPlanner.add_setting("String Array Setting", AresDataType.STRING_ARRAY, description="This is a string array setting") pythonDemoPlanner.add_setting("Constrained Strings", AresDataType.STRING_ARRAY, optional=True, constraints=["One", "Two", "Three"]) - pythonDemoPlanner.add_setting("Number Array Setting", AresDataType.NUMBER_ARRAY, default_value=[1, 2, 3, 4, 5]) + pythonDemoPlanner.add_setting("Number Array Setting", AresDataType.NUMBER_ARRAY, description="This is a number array setting") pythonDemoPlanner.add_setting("Constrained Numbers", AresDataType.NUMBER_ARRAY, optional=True, constraints=[1, 2, 3]) #Set Planner Timeout diff --git a/PyAres/Planning/planning_service.py b/PyAres/Planning/planning_service.py index fb2aeaf..11e938d 100644 --- a/PyAres/Planning/planning_service.py +++ b/PyAres/Planning/planning_service.py @@ -217,7 +217,8 @@ def add_setting(self, setting_name: str, default_value: Optional[Any] = None, optional: Optional[bool] = True, constraints: Optional[Union[list[int], list[str], list[float]]] = [], - limits: Optional[Limits] = None): + limits: Optional[Limits] = None, + description: Optional[str] = None): """ Adds a planner setting to be reported to ARES when your services capabilities are requested. @@ -228,14 +229,15 @@ def add_setting(self, setting_name: str, 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. limits: An optional Limits object for specifying minimum and maximum values + description: An optional string to describe your setting in more detail. Appears in ARES as a tooltip in the settings menu. """ 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, optional, choices=constraints, limits=limits, default_value=default_ares_value) + self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, choices=constraints, 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, optional, choices=constraints, limits=limits) + self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, choices=constraints, limits=limits, description=description) except Exception as e: print(f"Encountered an exception while adding setting {setting_name}: {e}") diff --git a/PyAres/Utils/ares_data_schema_utils.py b/PyAres/Utils/ares_data_schema_utils.py index 29461f1..7575217 100644 --- a/PyAres/Utils/ares_data_schema_utils.py +++ b/PyAres/Utils/ares_data_schema_utils.py @@ -31,7 +31,8 @@ def create_settings_schema_entry( default_value: Any = None, choices: Union[list[str], list[int], list[float]] = None, struct_schema: Optional[Dict[str, AresSchemaEntry]] = None, - limits: Optional[Limits] = None) -> ares_data_schema_pb2.AresValueSchema: + limits: Optional[Limits] = None, + description: Optional[str] = None) -> ares_data_schema_pb2.AresValueSchema: """ Creates a protobuf AresValueSchema message from the provided setting details. @@ -62,6 +63,9 @@ def create_settings_schema_entry( schema_entry.limits.minimum = limits.minimum schema_entry.limits.maximum = limits.maximum + if description is not None: + schema_entry.description = description + if isinstance(default_value, ares_struct_pb2.AresValue): schema_entry.default_value.CopyFrom(default_value) From 2e98f61bfdfdb19735a19ce0cad978da74104ccd Mon Sep 17 00:00:00 2001 From: Nick Kleiner Date: Thu, 11 Jun 2026 10:22:46 -0400 Subject: [PATCH 11/12] Added descriptions to device settings, though the device settings don't function properly currently --- PyAres/Device/device_service.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/PyAres/Device/device_service.py b/PyAres/Device/device_service.py index 9d7ca64..77b0651 100644 --- a/PyAres/Device/device_service.py +++ b/PyAres/Device/device_service.py @@ -275,7 +275,13 @@ def add_new_command(self, cmd_descriptor: DeviceCommandDescriptor, method): self._service_wrapper._command_methods[cmd_descriptor.name] = method self._service_wrapper._commands.append(cmd_descriptor) - def add_setting(self, setting_name: str, setting_value: Optional[Any] = None, optional: bool = True, constraints: Union[list[int], list[str], list[float]] = [], limits: Optional[Limits] = None): + def add_setting(self, + setting_name: str, + setting_value: Optional[Any] = None, + optional: bool = True, + constraints: Union[list[int], list[str], list[float]] = [], + limits: Optional[Limits] = None, + description: Optional[str] = None): """ Adds a new device setting to be reported to ARES when your devices capabilities are requested. @@ -285,17 +291,18 @@ def add_setting(self, setting_name: str, setting_value: Optional[Any] = None, op optional (bool): Whether the setting is optional constraints: An optional list of values to constrain the available setting choices. Can be integers, floats, or strings. limits: An optional Limits object for specifying minimum and maximum values + description: An optional string to describe your setting in more detail. Appears in ARES as a tooltip in the settings menu. """ try: setting_type = ares_data_type_utils.determine_python_ares_data_type(setting_value) new_ares_value = ares_value_utils.create_ares_value(setting_value) - self._service_wrapper._setting_schema[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, choices=constraints, limits=limits, default_value=new_ares_value) + self._service_wrapper._setting_schema[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, choices=constraints, limits=limits, default_value=new_ares_value, description=description) self._service_wrapper._current_settings[setting_name] = new_ares_value except Exception as e: print(f"Exception when trying to create setting {setting_name}: {e}") - + def start(self, wait_for_termination: bool = True): """ Starts the service on the specified port, and waits for termination. From 211e1f5bf242b7c8382bae429d34a1d0becdcf74 Mon Sep 17 00:00:00 2001 From: Nick Kleiner Date: Wed, 17 Jun 2026 09:51:25 -0400 Subject: [PATCH 12/12] Fixed device descriptions not appearing in tooltips on ARES --- PyAres/Demo/Devices/device_test.py | 2 +- PyAres/Device/device_service.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/PyAres/Demo/Devices/device_test.py b/PyAres/Demo/Devices/device_test.py index e1b8206..1c531be 100644 --- a/PyAres/Demo/Devices/device_test.py +++ b/PyAres/Demo/Devices/device_test.py @@ -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() \ No newline at end of file diff --git a/PyAres/Device/device_service.py b/PyAres/Device/device_service.py index 45c5c30..e7e8503 100644 --- a/PyAres/Device/device_service.py +++ b/PyAres/Device/device_service.py @@ -152,6 +152,7 @@ def GetSettingsSchema(self, request, context) -> device_service.SettingsSchemaRe settings_entry = response.schema.fields[key] settings_entry.type = value.type settings_entry.optional = value.optional + settings_entry.description = value.description if len(value.string_choices.strings) != 0: settings_entry.string_choices.strings.extend(value.string_choices.strings)