Creating Devices
Devices are the hands of your laboratory. In PyAres, a Device is a service that translates ARES commands (like "Set Temperature") into actual hardware instructions (like serial commands or USB signals).
The Core Concept
A Device Service in PyAres has three main responsibilities:
- Execute Commands: Perform actions when asked by ARES.
- Report State: Tell ARES what the hardware is currently doing (for logging).
- Ensure Safety: Provide a
safe_modethat ARES can trigger in emergencies.
⚠️ IMPORTANT Variable Naming Rule: When defining a command, your Python callback function must use argument names that exactly match the keys defined in your
input_schema.
- Incorrect: Schema key is
"voltage", but function isdef set_volts(v):- Correct: Schema key is
"voltage", and function isdef set_volts(voltage):PyAres maps the incoming data directly to your function arguments by name (using
method(**provided_param_dict)). If they do not match, or if the number of arguments in your function does not match the number of schema entries, the command will fail.
Key Classes
AresDeviceService
Initialization (__init__)
Arguments required to create an instance of the service:
enter_safe_mode_logic: A callable function that will be executed when the device is instructed to enter safe mode. This logic should put your device in a stable state, for instance telling a furnace to return to ambient temperature.get_device_state_logic: A callable function that handles gathering device state information for logging purposes. This function should return a dictionary containing the keys and values that define your device's state. This function may be synchronous or async (awaitable).device_name(str): The name of your device.description(str): A brief description of your device.version(str): The version associated with your device implementation.timeout(int): Timeout in seconds for service calls as reported to ARES. Defaults to 30.use_localhost(bool): An optional value that allows the user to specify whether to host the service on the local network or remote (if remote, it will listen to all addresses). Defaults to True.port(int): The port that your device service will serve on. Defaults to port 7100.max_message_size(int): Maximum message size for gRPC in megabytes. Defaults to -1, which uses the library default.
This service is the main wrapper for your device, giving you the bridge to connect your hardware to ARES.
Methods
add_new_command(descriptor, callback): Registers a specific action (likeset_voltage) and links it to a Python function.- The
descriptoris aDeviceCommandDescriptorthat defines the input and output schema for the command. - The
callbackis a Python function whose parameter names must exactly match the keys in the descriptor'sinput_schemaand whose parameter count should match the number of input schema entries. PyAres will call it ascallback(**arguments). - The preferred return type is a
DeviceCommandResponse. Returning raw values or dictionaries is still supported for backward compatibility but is deprecated and will emit a warning.
- The
add_setting(setting_name, setting_value=None, optional=True, constraints=[], limits=None, description=None): Adds a new device setting to be reported to ARES when your device's capabilities are requested. The setting type is inferred fromsetting_value;constraintscan be used to limit allowed values (for numbers or strings), andlimits(aLimitsobject) can define minimum and maximum numeric values.start(wait_for_termination): Starts your device service and begins listening for requests.- If
True(Default): The call blocks the main thread, keeping your program running indefinitely. This is necessary for standalone scripts; without it the background gRPC threads would die as soon as the script finishes. - If
False: The call returns immediately. This allows you to run other code, but it becomes your responsibility to keep the program alive (for example via a GUI loop orwhileloop).
- If
stop(): Stops your device service, terminating the connection.
Command Contracts and Responses
DeviceSchemaEntry
Describes an input or output parameter for a device command.
Key fields:
type(AresDataType): The data type for this parameter (for example, NUMBER, STRING, BOOLEAN).description(str): Human-readable description of the parameter.unit(str): Optional unit string (for example,"RPM","C").optional(bool): Whether this parameter is optional.constraints(list[int|float|str]): Optional list of allowed values.quantity_schema: Optional metadata for quantity-style types.struct_schema(Dict[str, DeviceSchemaEntry]): Optional nested schema for struct-like parameters.list_element_schema(DeviceSchemaEntry): Optional schema describing the elements of list-type parameters.min_number_value/max_number_value(float): Optional numeric bounds.
DeviceCommandDescriptor
Defines the "contract" for a command. You must define:
- Name & Description: What the ARES user sees.
- Input Schema: A dictionary mapping parameter names to
DeviceSchemaEntryobjects (for example, a number for voltage). - Output Schema: A dictionary mapping output names to
DeviceSchemaEntryobjects (for example, a measured current).
StatusCode and DeviceCommandResponse
DeviceCommandResponse is the preferred way for your command callbacks to report results back to ARES:
DeviceCommandResponse(response, error_string="", status_code=StatusCode.STATUS_UNSPECIFIED)response: Either a single value or a dictionary of key/value pairs representing the command result.error_string(str): Optional text describing what went wrong if the command failed.status_code(StatusCode): An enum that describes the outcome. Common values include:StatusCode.COMMAND_SUCCESSStatusCode.SUCCESS_WITH_WARNINGSStatusCode.COMMAND_FAILEDStatusCode.HARDWARE_FAULTStatusCode.EMERGENCY_STOP
When you return a DeviceCommandResponse, ARES determines success or failure from status_code and encodes response into the protobuf result (either as a struct for dictionaries or a scalar value).
Legacy behavior:
- If your callback returns a raw dictionary or scalar value instead of a
DeviceCommandResponse, PyAres will still encode that value into the result and mark the command as successful, but it will emit aFutureWarningindicating that this pattern is deprecated. New device implementations should always returnDeviceCommandResponse.
Example Implementation
from PyAres import AresDeviceService, AresDataType, DeviceSchemaEntry, DeviceCommandDescriptor, DeviceCommandResponse, StatusCode
# 1. Define your hardware logic
def set_speed(rpm: float):
print(f"Setting motor speed to {rpm}")
# Hardware communication goes here...
# Standard practice is to return a DeviceCommandResponse
return DeviceCommandResponse(None, status_code=StatusCode.COMMAND_SUCCESS)
def get_status():
# Return a dictionary matching your state schema
return { "rpm": 1200 }
def safe_mode():
print("Stopping motor immediately!")
# 2. Initialize Service
service = AresDeviceService(
safe_mode,
get_status,
"Rotary Mixer",
"High-speed mixer control",
"1.0.0",
port=7101
)
# 3. Define the 'Set Speed' Command
# Input: One number (Speed)
input_schema = {
"rpm": DeviceSchemaEntry(AresDataType.NUMBER, "Speed in RPM", "RPM")
}
cmd_descriptor = DeviceCommandDescriptor("Set Speed", "Sets mixer speed", input_schema, {})
# 4. Register the command
service.add_new_command(cmd_descriptor, set_speed)
# 5. Start
service.start()