diff --git a/unilabos/device_comms/opcua_client/client.py b/unilabos/device_comms/opcua_client/client.py index 27a401ff..9c8eb506 100644 --- a/unilabos/device_comms/opcua_client/client.py +++ b/unilabos/device_comms/opcua_client/client.py @@ -5,6 +5,7 @@ from pydantic import BaseModel from opcua import Client, ua +from opcua.ua import NodeClass import pandas as pd import os @@ -12,7 +13,7 @@ from unilabos.device_comms.opcua_client.node.uniopcua import Variable, Method, NodeType, DataType from unilabos.device_comms.universal_driver import UniversalDriver from unilabos.utils.log import logger -from unilabos.devices.workstation.post_process.decks import post_process_deck + class OpcUaNode(BaseModel): name: str @@ -54,6 +55,8 @@ class OpcUaWorkflowModel(BaseModel): """ 前后端Json解析用 """ + + class NodeFunctionJson(BaseModel): func_name: str node_name: str @@ -116,8 +119,6 @@ class BaseClient(UniversalDriver): _variables_to_find: Dict[str, Dict[str, Any]] = {} _name_mapping: Dict[str, str] = {} # 英文名到中文名的映射 _reverse_mapping: Dict[str, str] = {} # 中文名到英文名的映射 - # 直接缓存已找到的 ua.Node 对象,避免因字符串 NodeId 格式导致订阅失败 - _found_node_objects: Dict[str, Any] = {} def __init__(self): super().__init__() @@ -126,9 +127,6 @@ def __init__(self): # 初始化名称映射字典 self._name_mapping = {} self._reverse_mapping = {} - # 初始化线程锁(在子类中会被重新创建,这里提供默认实现) - import threading - self._client_lock = threading.RLock() def _set_client(self, client: Optional[Client]) -> None: if client is None: @@ -156,24 +154,15 @@ def _find_nodes(self) -> None: if not self.client: raise ValueError('client is not connected') - logger.info(f'开始查找 {len(self._variables_to_find)} 个节点...') + logger.info('开始查找节点...') try: # 获取根节点 root = self.client.get_root_node() objects = root.get_child(["0:Objects"]) - # 记录查找前的状态 - before_count = len(self._node_registry) - # 查找节点 self._find_nodes_recursive(objects) - # 记录查找后的状态 - after_count = len(self._node_registry) - newly_found = after_count - before_count - - logger.info(f"本次查找新增 {newly_found} 个节点,当前共 {after_count} 个") - # 检查是否所有节点都已找到 not_found = [] for var_name, var_info in self._variables_to_find.items(): @@ -181,13 +170,9 @@ def _find_nodes(self) -> None: not_found.append(var_name) if not_found: - logger.warning(f"⚠ 以下 {len(not_found)} 个节点未找到: {', '.join(not_found[:10])}{'...' if len(not_found) > 10 else ''}") - logger.warning(f"提示:请检查这些节点名称是否与服务器的 BrowseName 完全匹配(包括大小写、空格等)") - # 提供一个示例来帮助调试 - if not_found: - logger.info(f"尝试在服务器中查找第一个未找到的节点 '{not_found[0]}' 的相似节点...") + logger.warning(f"以下节点未找到: {', '.join(not_found)}") else: - logger.info(f"✓ 所有 {len(self._variables_to_find)} 个节点均已找到并注册") + logger.info("所有节点均已找到") except Exception as e: logger.error(f"查找节点失败: {e}") @@ -205,20 +190,18 @@ def _find_nodes_recursive(self, node) -> None: var_info = self._variables_to_find[node_name] node_type = var_info.get("node_type") data_type = var_info.get("data_type") - node_id_str = str(node.nodeid) # 根据节点类型创建相应的对象 if node_type == NodeType.VARIABLE: - self._node_registry[node_name] = Variable(self.client, node_name, node_id_str, data_type) - logger.info(f"✓ 找到变量节点: '{node_name}', NodeId: {node_id_str}, DataType: {data_type}") - # 缓存真实的 ua.Node 对象用于订阅 - self._found_node_objects[node_name] = node + self._node_registry[node_name] = Variable(self.client, node_name, str(node.nodeid), data_type) + logger.info(f"找到变量节点: {node_name}") elif node_type == NodeType.METHOD: # 对于方法节点,需要获取父节点ID parent_node = node.get_parent() parent_node_id = str(parent_node.nodeid) - self._node_registry[node_name] = Method(self.client, node_name, node_id_str, parent_node_id, data_type) - logger.info(f"✓ 找到方法节点: '{node_name}', NodeId: {node_id_str}, ParentId: {parent_node_id}") + self._node_registry[node_name] = Method(self.client, node_name, str(node.nodeid), parent_node_id, + data_type) + logger.info(f"找到方法节点: {node_name}") # 递归处理子节点 for child in node.get_children(): @@ -316,17 +299,13 @@ def use_node(self, name: str) -> OpcUaNodeBase: if name in self._name_mapping: chinese_name = self._name_mapping[name] if chinese_name in self._node_registry: - node = self._node_registry[chinese_name] - logger.debug(f"使用节点: '{name}' -> '{chinese_name}', NodeId: {node.node_id}") - return node + return self._node_registry[chinese_name] elif chinese_name in self._variables_to_find: logger.warning(f"节点 {chinese_name} (英文名: {name}) 尚未找到,尝试重新查找") if self.client: self._find_nodes() if chinese_name in self._node_registry: - node = self._node_registry[chinese_name] - logger.info(f"重新查找成功: '{chinese_name}', NodeId: {node.node_id}") - return node + return self._node_registry[chinese_name] raise ValueError(f'节点 {chinese_name} (英文名: {name}) 未注册或未找到') # 直接使用原始名称查找 @@ -336,14 +315,9 @@ def use_node(self, name: str) -> OpcUaNodeBase: if self.client: self._find_nodes() if name in self._node_registry: - node = self._node_registry[name] - logger.info(f"重新查找成功: '{name}', NodeId: {node.node_id}") - return node - logger.error(f"❌ 节点 '{name}' 未注册或未找到。已注册节点: {list(self._node_registry.keys())[:5]}...") + return self._node_registry[name] raise ValueError(f'节点 {name} 未注册或未找到') - node = self._node_registry[name] - logger.debug(f"使用节点: '{name}', NodeId: {node.node_id}") - return node + return self._node_registry[name] def get_node_registry(self) -> Dict[str, OpcUaNodeBase]: return self._node_registry @@ -364,13 +338,12 @@ def register_node_list(self, node_list: List[OpcUaNode]) -> "BaseClient": return self logger.info(f'开始注册 {len(node_list)} 个节点...') - new_nodes_count = 0 for node in node_list: if node is None: continue if node.name in self._node_registry: - logger.debug(f'节点 "{node.name}" 已存在于注册表') + logger.info(f'节点 {node.name} 已存在') exist = self._node_registry[node.name] if exist.type != node.node_type: raise ValueError(f'节点 {node.name} 类型 {node.node_type} 与已存在的类型 {exist.type} 不一致') @@ -381,10 +354,9 @@ def register_node_list(self, node_list: List[OpcUaNode]) -> "BaseClient": "node_type": node.node_type, "data_type": node.data_type } - new_nodes_count += 1 - logger.debug(f'添加节点 "{node.name}" ({node.node_type}) 到待查找列表') + logger.info(f'添加节点 {node.name} 到待查找列表') - logger.info(f'节点注册完成:新增 {new_nodes_count} 个待查找节点,总计 {len(self._variables_to_find)} 个') + logger.info('节点注册完成') # 如果客户端已连接,立即开始查找 if self.client: @@ -477,7 +449,8 @@ def run_opcua_workflow_model(self, workflow: OpcUaWorkflowModel) -> bool: function_name: Dict[str, Callable[[Callable[[str], OpcUaNodeBase]], bool]] = {} - def create_node_function(self, func_name: str = None, node_name: str = None, mode: str = None, value: Any = None, **kwargs) -> Callable[[Callable[[str], OpcUaNodeBase]], bool]: + def create_node_function(self, func_name: str = None, node_name: str = None, mode: str = None, value: Any = None, + **kwargs) -> Callable[[Callable[[str], OpcUaNodeBase]], bool]: def execute_node_function(use_node: Callable[[str], OpcUaNodeBase]) -> Union[bool, Tuple[Any, bool]]: target_node = use_node(node_name) @@ -502,8 +475,6 @@ def execute_node_function(use_node: Callable[[str], OpcUaNodeBase]) -> Union[boo err = result_dict.get("error") print(f"读取 {node_name} 返回值 = {val} (类型: {type(val).__name__}), 错误 = {err}") - - print(f"读取 {node_name} 返回值 = {val} (类型: {type(val).__name__}, 错误 = {err}") return val, err except Exception as e: print(f"解析读取结果失败: {e}, 原始结果: {result_str}") @@ -551,25 +522,15 @@ def create_init_function(self, func_name: str = None, write_nodes: Union[Dict[st raise ValueError("必须提供write_nodes参数") def execute_init_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: - """根据 _workflow_params 为各节点写入真实数值。 - - 约定: - - write_nodes 为 list 时: 节点名 == 参数名,从 _workflow_params[node_name] 取值; - - write_nodes 为 dict 时: - * value 为字符串且在 _workflow_params 中: 当作参数名去取值; - * 否则 value 视为常量直接写入。 - """ - - params = getattr(self, "_workflow_params", {}) or {} - if isinstance(write_nodes, list): - # 节点列表形式: 节点名与参数名一致 + # 处理节点列表 for node_name in write_nodes: - if node_name not in params: - print(f"初始化函数: 参数中未找到 {node_name}, 跳过写入") - continue + # 尝试从参数中获取同名参数的值 + current_value = True # 默认值 + if hasattr(self, '_workflow_params') and node_name in self._workflow_params: + current_value = self._workflow_params[node_name] + print(f"初始化函数: 从参数获取值 {node_name} = {current_value}") - current_value = params[node_name] print(f"初始化函数: 写入节点 {node_name} = {current_value}") input_json = json.dumps({"node_name": node_name, "value": current_value}) result_str = self.write_node(input_json) @@ -581,14 +542,14 @@ def execute_init_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: except Exception as e: print(f"初始化函数: 解析写入结果失败: {e}, 原始结果: {result_str}") elif isinstance(write_nodes, dict): - # 映射形式: 节点名 -> 参数名或常量 + # 处理节点字典,使用指定的值 for node_name, node_value in write_nodes.items(): - if isinstance(node_value, str) and node_value in params: - current_value = params[node_value] + # 检查值是否是字符串类型的参数名 + current_value = node_value + if isinstance(node_value, str) and hasattr(self, + '_workflow_params') and node_value in self._workflow_params: + current_value = self._workflow_params[node_value] print(f"初始化函数: 从参数获取值 {node_value} = {current_value}") - else: - current_value = node_value - print(f"初始化函数: 使用常量值 写入 {node_name} = {current_value}") print(f"初始化函数: 写入节点 {node_name} = {current_value}") input_json = json.dumps({"node_name": node_name, "value": current_value}) @@ -705,7 +666,9 @@ def execute_cleanup_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: self.function_name[func_name] = execute_cleanup_function return execute_cleanup_function - def create_start_function(self, func_name: str, stop_condition_expression: str = "True", write_nodes: Union[Dict[str, Any], List[str]] = None, condition_nodes: Union[Dict[str, str], List[str]] = None): + def create_start_function(self, func_name: str, stop_condition_expression: str = "True", + write_nodes: Union[Dict[str, Any], List[str]] = None, + condition_nodes: Union[Dict[str, str], List[str]] = None): """ 创建开始函数 @@ -715,21 +678,22 @@ def create_start_function(self, func_name: str, stop_condition_expression: str = write_nodes: 写节点配置,可以是节点名列表[节点1,节点2]或节点值映射{节点1:值1,节点2:值2} condition_nodes: 条件节点列表 [节点名1, 节点名2] """ - def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: - """开始函数: 写入触发节点, 然后轮询条件节点直到满足停止条件。""" - - params = getattr(self, "_workflow_params", {}) or {} - # 先处理写入节点(触发位等) + def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: + # 直接处理写入节点 if write_nodes: if isinstance(write_nodes, list): - # 列表形式: 节点名与参数名一致, 若无参数则直接写 True - for node_name in write_nodes: - if node_name in params: - current_value = params[node_name] - else: - current_value = True + # 处理节点列表,默认值都是True + for i, node_name in enumerate(write_nodes): + # 尝试获取与节点对应的参数值 + param_name = f"write_{i}" + + # 获取参数值(如果有) + current_value = True # 默认值 + if hasattr(self, '_workflow_params') and param_name in self._workflow_params: + current_value = self._workflow_params[param_name] + # 直接写入节点 print(f"直接写入节点 {node_name} = {current_value}") input_json = json.dumps({"node_name": node_name, "value": current_value}) result_str = self.write_node(input_json) @@ -741,13 +705,14 @@ def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: except Exception as e: print(f"解析直接写入结果失败: {e}, 原始结果: {result_str}") elif isinstance(write_nodes, dict): - # 字典形式: 节点名 -> 常量值(如 True/False) + # 处理节点字典,值是指定的 for node_name, node_value in write_nodes.items(): - if node_name in params: - current_value = params[node_name] - else: - current_value = node_value + # 尝试获取参数值(如果节点名与参数名匹配) + current_value = node_value # 使用指定的默认值 + if hasattr(self, '_workflow_params') and node_name in self._workflow_params: + current_value = self._workflow_params[node_name] + # 直接写入节点 print(f"直接写入节点 {node_name} = {current_value}") input_json = json.dumps({"node_name": node_name, "value": current_value}) result_str = self.write_node(input_json) @@ -775,7 +740,6 @@ def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: # 直接读取节点 result_str = self.read_node(node_name) try: - time.sleep(1) result_str = result_str.replace("'", '"') result_dict = json.loads(result_str) read_res = result_dict.get("value") @@ -1079,33 +1043,31 @@ def read_node(self, node_name: str) -> Dict[str, Any]: 读取节点值的便捷方法 返回包含result字段的字典 """ - # 使用锁保护客户端访问 - with self._client_lock: - try: - node = self.use_node(node_name) - value, error = node.read() - - # 创建结果字典 - result = { - "value": value, - "error": error, - "node_name": node_name, - "timestamp": time.time() - } - - # 返回JSON字符串 - return json.dumps(result) - except Exception as e: - logger.error(f"读取节点 {node_name} 失败: {e}") - # 创建错误结果字典 - result = { - "value": None, - "error": True, - "node_name": node_name, - "error_message": str(e), - "timestamp": time.time() - } - return json.dumps(result) + try: + node = self.use_node(node_name) + value, error = node.read() + + # 创建结果字典 + result = { + "value": value, + "error": error, + "node_name": node_name, + "timestamp": time.time() + } + + # 返回JSON字符串 + return json.dumps(result) + except Exception as e: + logger.error(f"读取节点 {node_name} 失败: {e}") + # 创建错误结果字典 + result = { + "value": None, + "error": True, + "node_name": node_name, + "error_message": str(e), + "timestamp": time.time() + } + return json.dumps(result) def write_node(self, json_input: str) -> str: """ @@ -1114,49 +1076,48 @@ def write_node(self, json_input: str) -> str: eg:'{\"node_name\":\"反应罐号码\",\"value\":\"2\"}' 返回JSON格式的字符串,包含操作结果 """ - # 使用锁保护客户端访问 - with self._client_lock: + try: + # 解析JSON格式的输入 + if not isinstance(json_input, str): + json_input = str(json_input) + try: - # 解析JSON格式的输入 - if not isinstance(json_input, str): - json_input = str(json_input) + input_data = json.loads(json_input) + if not isinstance(input_data, dict): + return json.dumps( + {"error": True, "error_message": "输入必须是包含node_name和value的JSON对象", "success": False}) - try: - input_data = json.loads(json_input) - if not isinstance(input_data, dict): - return json.dumps({"error": True, "error_message": "输入必须是包含node_name和value的JSON对象", "success": False}) - - # 从JSON中提取节点名称和值 - node_name = input_data.get("node_name") - value = input_data.get("value") - - if node_name is None: - return json.dumps({"error": True, "error_message": "JSON中缺少node_name字段", "success": False}) - except json.JSONDecodeError as e: - return json.dumps({"error": True, "error_message": f"JSON解析错误: {str(e)}", "success": False}) - - node = self.use_node(node_name) - error = node.write(value) - - # 创建结果字典 - result = { - "value": value, - "error": error, - "node_name": node_name, - "timestamp": time.time(), - "success": not error - } - - return json.dumps(result) - except Exception as e: - logger.error(f"写入节点失败: {e}") - result = { - "error": True, - "error_message": str(e), - "timestamp": time.time(), - "success": False - } - return json.dumps(result) + # 从JSON中提取节点名称和值 + node_name = input_data.get("node_name") + value = input_data.get("value") + + if node_name is None: + return json.dumps({"error": True, "error_message": "JSON中缺少node_name字段", "success": False}) + except json.JSONDecodeError as e: + return json.dumps({"error": True, "error_message": f"JSON解析错误: {str(e)}", "success": False}) + + node = self.use_node(node_name) + error = node.write(value) + + # 创建结果字典 + result = { + "value": value, + "error": error, + "node_name": node_name, + "timestamp": time.time(), + "success": not error + } + + return json.dumps(result) + except Exception as e: + logger.error(f"写入节点失败: {e}") + result = { + "error": True, + "error_message": str(e), + "timestamp": time.time(), + "success": False + } + return json.dumps(result) def call_method(self, node_name: str, *args) -> Tuple[Any, bool]: """ @@ -1176,51 +1137,14 @@ def call_method(self, node_name: str, *args) -> Tuple[Any, bool]: class OpcUaClient(BaseClient): - def __init__( - self, - url: str, - deck: Optional[Union[post_process_deck, Dict[str, Any]]] = None, - config_path: str = None, - username: str = None, - password: str = None, - use_subscription: bool = True, - cache_timeout: float = 5.0, - subscription_interval: int = 500, - *args, - **kwargs, - ): + def __init__(self, url: str, config_path: str = None, username: str = None, password: str = None, + refresh_interval: float = 1.0): # 降低OPCUA库的日志级别 import logging logging.getLogger("opcua").setLevel(logging.WARNING) super().__init__() - - # ===== 关键修改:参照 BioyondWorkstation 处理 deck ===== - - super().__init__() - - # 处理 deck 参数 - if deck is None: - self.deck = post_process_deck(setup=True) - elif isinstance(deck, dict): - self.deck = post_process_deck(setup=True) - elif hasattr(deck, 'children'): - self.deck = deck - else: - raise ValueError(f"deck 参数类型不支持: {type(deck)}") - - if self.deck is None: - raise ValueError("Deck 配置不能为空") - - # 统计仓库信息 - warehouse_count = 0 - if hasattr(self.deck, 'children'): - warehouse_count = len(self.deck.children) - logger.info(f"Deck 初始化完成,加载 {warehouse_count} 个资源") - - - # OPC UA 客户端初始化 client = Client(url) if username and password: @@ -1228,152 +1152,75 @@ def __init__( client.set_password(password) self._set_client(client) - - # 订阅相关属性 - self._use_subscription = use_subscription - self._subscription = None - self._subscription_handles = {} - self._subscription_interval = subscription_interval - - # 缓存相关属性 - self._node_values = {} # 修改为支持时间戳的缓存结构 - self._cache_timeout = cache_timeout - - # 连接状态监控 - self._connection_check_interval = 30.0 # 连接检查间隔(秒) - self._connection_monitor_running = False - self._connection_monitor_thread = None - - # 添加线程锁,保护OPC UA客户端的并发访问 - import threading - self._client_lock = threading.RLock() - - # 连接到服务器 self._connect() + # 节点值缓存和刷新相关属性 + self._node_values = {} # 缓存节点值 + self._refresh_interval = refresh_interval # 刷新间隔(秒) + self._refresh_running = False + self._refresh_thread = None + # 如果提供了配置文件路径,则加载配置并注册工作流 if config_path: self.load_config(config_path) - # 启动连接监控 - self._start_connection_monitor() - - - def _connect(self) -> None: - """连接到OPC UA服务器""" - logger.info('尝试连接到 OPC UA 服务器...') - if self.client: - try: - self.client.connect() - logger.info('✓ 客户端已连接!') - - # 连接后开始查找节点 - if self._variables_to_find: - self._find_nodes() - - # 如果启用订阅模式,设置订阅 - if self._use_subscription: - self._setup_subscriptions() - else: - logger.info("订阅模式已禁用,将使用按需读取模式") + # 启动节点值刷新线程 + self.start_node_refresh() - except Exception as e: - logger.error(f'客户端连接失败: {e}') - raise - else: - raise ValueError('客户端未初始化') + def _register_nodes_as_attributes(self): + """将所有节点注册为实例属性,可以通过self.node_name访问""" + for node_name, node in self._node_registry.items(): + # 检查是否有对应的英文名称 + eng_name = self._reverse_mapping.get(node_name) + if eng_name: + # 如果有对应的英文名称,使用英文名称作为属性名 + attr_name = eng_name + else: + # 如果没有对应的英文名称,使用原始名称,但替换空格和特殊字符 + attr_name = node_name.replace(' ', '_').replace('-', '_') - class SubscriptionHandler: - """freeopcua订阅处理器:必须实现 datachange_notification 方法""" - def __init__(self, outer): - self.outer = outer + # 创建获取节点值的属性方法,使用中文名称获取节点值 + def create_property_getter(node_key): + def getter(self): + # 优先从缓存获取值 + if node_key in self._node_values: + return self._node_values[node_key] + # 缓存中没有则直接读取 + value, _ = self.use_node(node_key).read() + return value - def datachange_notification(self, node, val, data): - # 委托给外层类的处理函数 - try: - self.outer._on_subscription_datachange(node, val, data) - except Exception as e: - logger.error(f"订阅数据回调处理失败: {e}") + return getter - # 可选:事件通知占位,避免库调用时报缺失 - def event_notification(self, event): - pass + # 使用property装饰器将方法注册为类属性 + setattr(OpcUaClient, attr_name, property(create_property_getter(node_name))) + logger.info(f"已注册节点 '{node_name}' 为属性 '{attr_name}'") - def _setup_subscriptions(self): - """设置 OPC UA 订阅""" - if not self.client or not self._use_subscription: + def refresh_node_values(self): + """刷新所有节点的值到缓存""" + if not self.client: + logger.warning("客户端未初始化,无法刷新节点值") return - with self._client_lock: - try: - logger.info(f"开始设置订阅 (发布间隔: {self._subscription_interval}ms)...") - - # 创建订阅 - handler = OpcUaClient.SubscriptionHandler(self) - self._subscription = self.client.create_subscription( - self._subscription_interval, - handler - ) - - # 为所有变量节点创建监控项 - subscribed_count = 0 - skipped_count = 0 - - for node_name, node in self._node_registry.items(): - # 只为变量节点创建订阅 - if node.type == NodeType.VARIABLE and node.node_id: - try: - # 优先使用在查找阶段缓存的真实 ua.Node 对象 - ua_node = self._found_node_objects.get(node_name) - if ua_node is None: - ua_node = self.client.get_node(node.node_id) - handle = self._subscription.subscribe_data_change(ua_node) - self._subscription_handles[node_name] = handle - subscribed_count += 1 - logger.debug(f"✓ 已订阅节点: {node_name}") - except Exception as e: - skipped_count += 1 - logger.warning(f"✗ 订阅节点 {node_name} 失败: {e}") - else: - skipped_count += 1 - - logger.info(f"订阅设置完成: 成功 {subscribed_count} 个, 跳过 {skipped_count} 个") - - except Exception as e: - logger.error(f"设置订阅失败: {e}") - traceback.print_exc() - # 订阅失败时回退到按需读取模式 - self._use_subscription = False - logger.warning("订阅模式设置失败,已自动切换到按需读取模式") - - def _on_subscription_datachange(self, node, val, data): - """订阅数据变化处理器(供内部 SubscriptionHandler 调用)""" try: - node_id = str(node.nodeid) - current_time = time.time() - # 查找对应的节点名称 - for node_name, node_obj in self._node_registry.items(): - if node_obj.node_id == node_id: - self._node_values[node_name] = { - 'value': val, - 'timestamp': current_time, - 'source': 'subscription' - } - logger.debug(f"订阅更新: {node_name} = {val}") - break + # 简单检查连接状态,如果不连接会抛出异常 + self.client.get_namespace_array() except Exception as e: - logger.error(f"处理订阅数据失败: {e}") + logger.warning(f"客户端连接异常,无法刷新节点值: {e}") + return - def get_node_value(self, name, use_cache=True, force_read=False): - """ - 获取节点值(智能缓存版本) + for node_name, node in self._node_registry.items(): + try: + if hasattr(node, 'read'): + value, error = node.read() + if not error: + self._node_values[node_name] = value + # logger.debug(f"已刷新节点 '{node_name}' 的值: {value}") + except Exception as e: + logger.error(f"刷新节点 '{node_name}' 失败: {e}") - 参数: - name: 节点名称(支持中文名或英文名) - use_cache: 是否使用缓存 - force_read: 是否强制从服务器读取(忽略缓存) - """ - # 处理名称映射 + def get_node_value(self, name): + """获取节点值,支持中文名和英文名""" + # 如果提供的是英文名,转换为中文名 if name in self._name_mapping: chinese_name = self._name_mapping[name] # 优先从缓存获取值 @@ -1393,63 +1240,15 @@ def get_node_value(self, name, use_cache=True, force_read=False): else: raise ValueError(f"未找到名称为 '{name}' 的节点") - elif name in self._node_registry: - chinese_name = name - else: - raise ValueError(f"未找到名称为 '{name}' 的节点") - - # 如果强制读取,直接从服务器读取 - if force_read: - with self._client_lock: - value, _ = self.use_node(chinese_name).read() - # 更新缓存 - self._node_values[chinese_name] = { - 'value': value, - 'timestamp': time.time(), - 'source': 'forced_read' - } - return value - - # 检查缓存 - if use_cache and chinese_name in self._node_values: - cache_entry = self._node_values[chinese_name] - cache_age = time.time() - cache_entry['timestamp'] - - # 如果是订阅模式,缓存永久有效(由订阅更新) - # 如果是按需读取模式,检查缓存超时 - if cache_entry.get('source') == 'subscription' or cache_age < self._cache_timeout: - logger.debug(f"从缓存读取: {chinese_name} = {cache_entry['value']} (age: {cache_age:.2f}s, source: {cache_entry.get('source', 'unknown')})") - return cache_entry['value'] - - # 缓存过期或不存在,从服务器读取 - with self._client_lock: - try: - value, error = self.use_node(chinese_name).read() - if not error: - # 更新缓存 - self._node_values[chinese_name] = { - 'value': value, - 'timestamp': time.time(), - 'source': 'on_demand_read' - } - return value - else: - logger.warning(f"读取节点 {chinese_name} 失败") - return None - except Exception as e: - logger.error(f"读取节点 {chinese_name} 出错: {e}") - return None - def set_node_value(self, name, value): - """ - 设置节点值 - 写入成功后会立即更新本地缓存 - """ - # 处理名称映射 + """设置节点值,支持中文名和英文名""" + # 如果提供的是英文名,转换为中文名 if name in self._name_mapping: chinese_name = self._name_mapping[name] + node = self.use_node(chinese_name) + # 如果提供的是中文名,直接使用 elif name in self._node_registry: - chinese_name = name + node = self.use_node(name) else: raise ValueError(f"未找到名称为 '{name}' 的节点") @@ -1468,50 +1267,6 @@ def _refresh_worker(self): logger.info(f"节点值刷新线程已启动,刷新间隔: {self._refresh_interval}秒") while self._refresh_running: - - with self._client_lock: - try: - node = self.use_node(chinese_name) - error = node.write(value) - - if not error: - # 写入成功,立即更新缓存 - self._node_values[chinese_name] = { - 'value': value, - 'timestamp': time.time(), - 'source': 'write' - } - logger.debug(f"写入成功: {chinese_name} = {value}") - return True - else: - logger.warning(f"写入节点 {chinese_name} 失败") - return False - except Exception as e: - logger.error(f"写入节点 {chinese_name} 出错: {e}") - return False - - def _check_connection(self) -> bool: - """检查连接状态""" - try: - with self._client_lock: - if self.client: - # 尝试获取命名空间数组来验证连接 - self.client.get_namespace_array() - return True - except Exception as e: - logger.warning(f"连接检查失败: {e}") - return False - return False - - def _connection_monitor_worker(self): - """连接监控线程工作函数""" - self._connection_monitor_running = True - logger.info(f"连接监控线程已启动 (检查间隔: {self._connection_check_interval}秒)") - - reconnect_attempts = 0 - max_reconnect_attempts = 5 - - while self._connection_monitor_running: try: self.refresh_node_values() except Exception as e: @@ -1524,49 +1279,6 @@ def start_node_refresh(self): """启动节点值刷新线程""" if self._refresh_thread is not None and self._refresh_thread.is_alive(): logger.warning("节点值刷新线程已在运行") - # 检查连接状态 - if not self._check_connection(): - logger.warning("检测到连接断开,尝试重新连接...") - reconnect_attempts += 1 - - if reconnect_attempts <= max_reconnect_attempts: - try: - # 尝试重新连接 - with self._client_lock: - if self.client: - try: - self.client.disconnect() - except: - pass - - self.client.connect() - logger.info("✓ 重新连接成功") - - # 重新设置订阅 - if self._use_subscription: - self._setup_subscriptions() - - reconnect_attempts = 0 - except Exception as e: - logger.error(f"重新连接失败 (尝试 {reconnect_attempts}/{max_reconnect_attempts}): {e}") - time.sleep(5) # 重连失败后等待5秒 - else: - logger.error(f"达到最大重连次数 ({max_reconnect_attempts}),停止重连") - self._connection_monitor_running = False - else: - # 连接正常,重置重连计数 - reconnect_attempts = 0 - - except Exception as e: - logger.error(f"连接监控出错: {e}") - - # 等待下次检查 - time.sleep(self._connection_check_interval) - - def _start_connection_monitor(self): - """启动连接监控线程""" - if self._connection_monitor_thread is not None and self._connection_monitor_thread.is_alive(): - logger.warning("连接监控线程已在运行") return import threading @@ -1580,94 +1292,6 @@ def stop_node_refresh(self): self._refresh_thread.join(timeout=2.0) logger.info("节点值刷新线程已停止") - self._connection_monitor_thread = threading.Thread( - target=self._connection_monitor_worker, - daemon=True, - name="OpcUaConnectionMonitor" - ) - self._connection_monitor_thread.start() - - def _stop_connection_monitor(self): - """停止连接监控线程""" - self._connection_monitor_running = False - if self._connection_monitor_thread and self._connection_monitor_thread.is_alive(): - self._connection_monitor_thread.join(timeout=2.0) - logger.info("连接监控线程已停止") - - def read_node(self, node_name: str) -> str: - """ - 读取节点值的便捷方法(使用缓存) - 返回JSON格式字符串 - """ - try: - # 使用get_node_value方法,自动处理缓存 - value = self.get_node_value(node_name, use_cache=True) - - # 获取缓存信息 - chinese_name = self._name_mapping.get(node_name, node_name) - cache_info = self._node_values.get(chinese_name, {}) - - result = { - "value": value, - "error": False, - "node_name": node_name, - "timestamp": time.time(), - "cache_age": time.time() - cache_info.get('timestamp', time.time()), - "source": cache_info.get('source', 'unknown') - } - - return json.dumps(result) - except Exception as e: - logger.error(f"读取节点 {node_name} 失败: {e}") - result = { - "value": None, - "error": True, - "node_name": node_name, - "error_message": str(e), - "timestamp": time.time() - } - return json.dumps(result) - - def get_cache_stats(self) -> Dict[str, Any]: - """获取缓存统计信息""" - current_time = time.time() - stats = { - 'total_cached_nodes': len(self._node_values), - 'subscription_nodes': 0, - 'on_demand_nodes': 0, - 'expired_nodes': 0, - 'cache_timeout': self._cache_timeout, - 'using_subscription': self._use_subscription - } - - for node_name, cache_entry in self._node_values.items(): - source = cache_entry.get('source', 'unknown') - cache_age = current_time - cache_entry['timestamp'] - - if source == 'subscription': - stats['subscription_nodes'] += 1 - elif source in ['on_demand_read', 'forced_read', 'write']: - stats['on_demand_nodes'] += 1 - - if cache_age > self._cache_timeout: - stats['expired_nodes'] += 1 - - return stats - - def print_cache_stats(self): - """打印缓存统计信息""" - stats = self.get_cache_stats() - print("\n" + "="*80) - print("缓存统计信息") - print("="*80) - print(f"总缓存节点数: {stats['total_cached_nodes']}") - print(f"订阅模式: {'启用' if stats['using_subscription'] else '禁用'}") - print(f" - 订阅更新节点: {stats['subscription_nodes']}") - print(f" - 按需读取节点: {stats['on_demand_nodes']}") - print(f" - 已过期节点: {stats['expired_nodes']}") - print(f"缓存超时时间: {stats['cache_timeout']}秒") - print("="*80 + "\n") - def load_config(self, config_path: str) -> None: """从JSON配置文件加载并注册工作流""" try: @@ -1676,206 +1300,42 @@ def load_config(self, config_path: str) -> None: # 处理节点注册 if "register_node_list_from_csv_path" in config_data: + # 获取配置文件所在目录 config_dir = os.path.dirname(os.path.abspath(config_path)) # 处理CSV路径,如果是相对路径,则相对于配置文件所在目录 - if "path" in config_data["register_node_list_from_csv_path"]: csv_path = config_data["register_node_list_from_csv_path"]["path"] if not os.path.isabs(csv_path): + # 转换为绝对路径 csv_path = os.path.join(config_dir, csv_path) config_data["register_node_list_from_csv_path"]["path"] = csv_path # 直接使用字典 - self.register_node_list_from_csv_path(**config_data["register_node_list_from_csv_path"]) - if self.client and self._variables_to_find: - logger.info("CSV加载完成,开始查找服务器节点...") - self._find_nodes() - # 处理工作流创建 if "create_flow" in config_data: + # 直接传递字典列表 self.create_workflow_from_json(config_data["create_flow"]) + # 将工作流注册为实例方法 self.register_workflows_as_methods() - # 将所有节点注册为属性(只注册已找到的节点) + # 将所有节点注册为属性 self._register_nodes_as_attributes() - # 打印统计信息 - found_count = len(self._node_registry) - total_count = len(self._variables_to_find) - if found_count < total_count: - logger.warning(f"节点查找完成:找到 {found_count}/{total_count} 个节点") - else: - logger.info(f"✓ 节点查找完成:所有 {found_count} 个节点均已找到") - - # 如果使用订阅模式,重新设置订阅(确保新节点被订阅) - if self._use_subscription and found_count > 0: - self._setup_subscriptions() - logger.info(f"成功从 {config_path} 加载配置") except Exception as e: logger.error(f"加载配置文件 {config_path} 失败: {e}") traceback.print_exc() - def print_node_registry_status(self): - """打印节点注册状态,用于调试""" - print("\n" + "="*80) - print("节点注册状态诊断报告") - print("="*80) - print(f"\n待查找节点总数: {len(self._variables_to_find)}") - print(f"已找到节点总数: {len(self._node_registry)}") - print(f"未找到节点总数: {len(self._variables_to_find) - len(self._node_registry)}") - - # 显示已找到的节点(前10个) - if self._node_registry: - print(f"\n✓ 已找到的节点 (显示前10个):") - for i, (name, node) in enumerate(list(self._node_registry.items())[:10]): - eng_name = self._reverse_mapping.get(name, "") - eng_info = f" ({eng_name})" if eng_name else "" - print(f" {i+1}. '{name}'{eng_info}") - print(f" NodeId: {node.node_id}") - print(f" Type: {node.type}") - - # 显示未找到的节点 - not_found = [name for name in self._variables_to_find if name not in self._node_registry] - if not_found: - print(f"\n✗ 未找到的节点 (显示前20个):") - for i, name in enumerate(not_found[:20]): - eng_name = self._reverse_mapping.get(name, "") - eng_info = f" ({eng_name})" if eng_name else "" - node_info = self._variables_to_find[name] - print(f" {i+1}. '{name}'{eng_info} - {node_info['node_type']}") - - print("\n" + "="*80) - print("提示:") - print("1. 如果大量节点未找到,请检查CSV中的节点名称是否与服务器BrowseName完全匹配") - print("2. 可以使用 client.browse_server_nodes() 查看服务器的实际节点结构") - print("3. 节点名称区分大小写,且包括所有空格和特殊字符") - print("="*80 + "\n") - - def browse_server_nodes(self, max_depth=3, start_path=["0:Objects"]): - """浏览服务器节点树,用于调试和对比""" - if not self.client: - print("客户端未连接") - return - - print("\n" + "="*80) - print(f"服务器节点浏览 (最大深度: {max_depth})") - print("="*80 + "\n") - - try: - root = self.client.get_root_node() - start_node = root.get_child(start_path) - self._browse_node_recursive(start_node, depth=0, max_depth=max_depth) - except Exception as e: - print(f"浏览失败: {e}") - traceback.print_exc() - - def _browse_node_recursive(self, node, depth=0, max_depth=3): - """递归浏览节点""" - if depth > max_depth: - return - - try: - browse_name = node.get_browse_name() - node_class = node.get_node_class() - indent = " " * depth - - # 显示节点信息 - print(f"{indent}├─ {browse_name.Name}") - print(f"{indent}│ NodeId: {str(node.nodeid)}") - print(f"{indent}│ NodeClass: {node_class}") - - # 如果是变量,显示数据类型 - if node_class == NodeClass.Variable: - try: - data_type = node.get_data_type() - print(f"{indent}│ DataType: {data_type}") - except: - pass - - # 递归处理子节点(限制数量避免输出过多) - if depth < max_depth: - children = node.get_children() - for i, child in enumerate(children[:20]): # 每层最多显示20个子节点 - self._browse_node_recursive(child, depth + 1, max_depth) - if len(children) > 20: - print(f"{indent} ... ({len(children) - 20} more children)") - except Exception as e: - # 忽略单个节点的错误 - pass - def disconnect(self): # 停止刷新线程 self.stop_node_refresh() - """断开连接并清理资源""" - logger.info("正在断开连接...") - - # 停止连接监控 - self._stop_connection_monitor() - - # 删除订阅 - if self._subscription: - try: - with self._client_lock: - self._subscription.delete() - logger.info("订阅已删除") - except Exception as e: - logger.warning(f"删除订阅失败: {e}") - - # 断开客户端连接 if self.client: - try: - with self._client_lock: - self.client.disconnect() - logger.info("✓ OPC UA 客户端已断开连接") - except Exception as e: - logger.error(f"断开连接失败: {e}") - - def _register_nodes_as_attributes(self): - """将所有节点注册为实例属性""" - for node_name, node in self._node_registry.items(): - if not node.node_id or node.node_id == "": - logger.warning(f"⚠ 节点 '{node_name}' 的 node_id 为空,跳过注册为属性") - continue - - eng_name = self._reverse_mapping.get(node_name) - attr_name = eng_name if eng_name else node_name.replace(' ', '_').replace('-', '_') - - def create_property_getter(node_key): - def getter(self): - return self.get_node_value(node_key, use_cache=True) - return getter - - setattr(OpcUaClient, attr_name, property(create_property_getter(node_name))) - logger.debug(f"已注册节点 '{node_name}' 为属性 '{attr_name}'") - - def post_init(self, ros_node): - """ROS2 节点就绪后的初始化""" - if not (hasattr(self, 'deck') and self.deck): - return - - if not (hasattr(ros_node, 'resource_tracker') and ros_node.resource_tracker): - logger.warning("resource_tracker 不存在,无法注册 deck") - return - - # 1. 本地注册(必需) - ros_node.resource_tracker.add_resource(self.deck) - - # 2. 上传云端 - try: - from unilabos.ros.nodes.base_device_node import ROS2DeviceNode - ROS2DeviceNode.run_async_func( - ros_node.update_resource, - True, - resources=[self.deck] - ) - logger.info("Deck 已上传到云端") - except Exception as e: - logger.error(f"上传失败: {e}") + self.client.disconnect() + logger.info("OPC UA client disconnected") if __name__ == '__main__': @@ -1883,14 +1343,15 @@ def post_init(self, ros_node): # 使用配置文件创建客户端并自动注册工作流 import os + current_dir = os.path.dirname(os.path.abspath(__file__)) config_path = os.path.join(current_dir, "opcua_huairou.json") # 创建OPC UA客户端并加载配置 try: client = OpcUaClient( - url="opc.tcp://192.168.1.88:4840/freeopcua/server/", # 替换为实际的OPC UA服务器地址 - config_path="D:\\Uni-Lab-OS\\unilabos\\device_comms\\opcua_client\\opcua_huairou.json" # 传入配置文件路径 + url="opc.tcp://localhost:4840/freeopcua/server/", # 替换为实际的OPC UA服务器地址 + config_path=config_path # 传入配置文件路径 ) # 列出所有已注册的工作流 @@ -1900,9 +1361,7 @@ def post_init(self, ros_node): # 测试trigger_grab_action工作流 - 使用英文参数名 print("\n测试trigger_grab_action工作流 - 使用英文参数名:") - client.trigger_grab_action(reaction_tank_number=2, raw_tank_number=2) - # client.set_node_value("reaction_tank_number", 2) - + client.trigger_grab_action(reaction_tank_number=2, raw_tank_number=3) # 读取节点值 - 使用英文节点名 grab_complete = client.get_node_value("grab_complete") diff --git a/unilabos/registry/devices/bioyond.yaml b/unilabos/registry/devices/bioyond.yaml deleted file mode 100644 index 3325a260..00000000 --- a/unilabos/registry/devices/bioyond.yaml +++ /dev/null @@ -1,589 +0,0 @@ -workstation.bioyond_dispensing_station: - category: - - workstation - - bioyond - class: - action_value_mappings: - auto-batch_create_90_10_vial_feeding_tasks: - feedback: {} - goal: {} - goal_default: - delay_time: null - hold_m_name: null - liquid_material_name: NMP - speed: null - temperature: null - titration: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - delay_time: - type: string - hold_m_name: - type: string - liquid_material_name: - default: NMP - type: string - speed: - type: string - temperature: - type: string - titration: - type: string - required: - - titration - type: object - result: {} - required: - - goal - title: batch_create_90_10_vial_feeding_tasks参数 - type: object - type: UniLabJsonCommand - auto-batch_create_diamine_solution_tasks: - feedback: {} - goal: {} - goal_default: - delay_time: null - liquid_material_name: NMP - solutions: null - speed: null - temperature: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - delay_time: - type: string - liquid_material_name: - default: NMP - type: string - solutions: - type: string - speed: - type: string - temperature: - type: string - required: - - solutions - type: object - result: {} - required: - - goal - title: batch_create_diamine_solution_tasks参数 - type: object - type: UniLabJsonCommand - auto-brief_step_parameters: - feedback: {} - goal: {} - goal_default: - data: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - data: - type: object - required: - - data - type: object - result: {} - required: - - goal - title: brief_step_parameters参数 - type: object - type: UniLabJsonCommand - auto-compute_experiment_design: - feedback: {} - goal: {} - goal_default: - m_tot: '70' - ratio: null - titration_percent: '0.03' - wt_percent: '0.25' - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - m_tot: - default: '70' - type: string - ratio: - type: object - titration_percent: - default: '0.03' - type: string - wt_percent: - default: '0.25' - type: string - required: - - ratio - type: object - result: - properties: - feeding_order: - items: {} - title: Feeding Order - type: array - return_info: - title: Return Info - type: string - solutions: - items: {} - title: Solutions - type: array - solvents: - additionalProperties: true - title: Solvents - type: object - titration: - additionalProperties: true - title: Titration - type: object - required: - - solutions - - titration - - solvents - - feeding_order - - return_info - title: ComputeExperimentDesignReturn - type: object - required: - - goal - title: compute_experiment_design参数 - type: object - type: UniLabJsonCommand - auto-process_order_finish_report: - feedback: {} - goal: {} - goal_default: - report_request: null - used_materials: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - report_request: - type: string - used_materials: - type: string - required: - - report_request - - used_materials - type: object - result: {} - required: - - goal - title: process_order_finish_report参数 - type: object - type: UniLabJsonCommand - auto-project_order_report: - feedback: {} - goal: {} - goal_default: - order_id: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - order_id: - type: string - required: - - order_id - type: object - result: {} - required: - - goal - title: project_order_report参数 - type: object - type: UniLabJsonCommand - auto-query_resource_by_name: - feedback: {} - goal: {} - goal_default: - material_name: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - material_name: - type: string - required: - - material_name - type: object - result: {} - required: - - goal - title: query_resource_by_name参数 - type: object - type: UniLabJsonCommand - auto-transfer_materials_to_reaction_station: - feedback: {} - goal: {} - goal_default: - target_device_id: null - transfer_groups: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - target_device_id: - type: string - transfer_groups: - type: array - required: - - target_device_id - - transfer_groups - type: object - result: {} - required: - - goal - title: transfer_materials_to_reaction_station参数 - type: object - type: UniLabJsonCommand - auto-wait_for_multiple_orders_and_get_reports: - feedback: {} - goal: {} - goal_default: - batch_create_result: null - check_interval: 10 - timeout: 7200 - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - batch_create_result: - type: string - check_interval: - default: 10 - type: integer - timeout: - default: 7200 - type: integer - required: [] - type: object - result: {} - required: - - goal - title: wait_for_multiple_orders_and_get_reports参数 - type: object - type: UniLabJsonCommand - auto-workflow_sample_locations: - feedback: {} - goal: {} - goal_default: - workflow_id: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - workflow_id: - type: string - required: - - workflow_id - type: object - result: {} - required: - - goal - title: workflow_sample_locations参数 - type: object - type: UniLabJsonCommand - create_90_10_vial_feeding_task: - feedback: {} - goal: - delay_time: delay_time - hold_m_name: hold_m_name - order_name: order_name - percent_10_1_assign_material_name: percent_10_1_assign_material_name - percent_10_1_liquid_material_name: percent_10_1_liquid_material_name - percent_10_1_target_weigh: percent_10_1_target_weigh - percent_10_1_volume: percent_10_1_volume - percent_10_2_assign_material_name: percent_10_2_assign_material_name - percent_10_2_liquid_material_name: percent_10_2_liquid_material_name - percent_10_2_target_weigh: percent_10_2_target_weigh - percent_10_2_volume: percent_10_2_volume - percent_10_3_assign_material_name: percent_10_3_assign_material_name - percent_10_3_liquid_material_name: percent_10_3_liquid_material_name - percent_10_3_target_weigh: percent_10_3_target_weigh - percent_10_3_volume: percent_10_3_volume - percent_90_1_assign_material_name: percent_90_1_assign_material_name - percent_90_1_target_weigh: percent_90_1_target_weigh - percent_90_2_assign_material_name: percent_90_2_assign_material_name - percent_90_2_target_weigh: percent_90_2_target_weigh - percent_90_3_assign_material_name: percent_90_3_assign_material_name - percent_90_3_target_weigh: percent_90_3_target_weigh - speed: speed - temperature: temperature - goal_default: - delay_time: '' - hold_m_name: '' - order_name: '' - percent_10_1_assign_material_name: '' - percent_10_1_liquid_material_name: '' - percent_10_1_target_weigh: '' - percent_10_1_volume: '' - percent_10_2_assign_material_name: '' - percent_10_2_liquid_material_name: '' - percent_10_2_target_weigh: '' - percent_10_2_volume: '' - percent_10_3_assign_material_name: '' - percent_10_3_liquid_material_name: '' - percent_10_3_target_weigh: '' - percent_10_3_volume: '' - percent_90_1_assign_material_name: '' - percent_90_1_target_weigh: '' - percent_90_2_assign_material_name: '' - percent_90_2_target_weigh: '' - percent_90_3_assign_material_name: '' - percent_90_3_target_weigh: '' - speed: '' - temperature: '' - handles: {} - result: - return_info: return_info - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: DispenStationVialFeed_Feedback - type: object - goal: - properties: - delay_time: - type: string - hold_m_name: - type: string - order_name: - type: string - percent_10_1_assign_material_name: - type: string - percent_10_1_liquid_material_name: - type: string - percent_10_1_target_weigh: - type: string - percent_10_1_volume: - type: string - percent_10_2_assign_material_name: - type: string - percent_10_2_liquid_material_name: - type: string - percent_10_2_target_weigh: - type: string - percent_10_2_volume: - type: string - percent_10_3_assign_material_name: - type: string - percent_10_3_liquid_material_name: - type: string - percent_10_3_target_weigh: - type: string - percent_10_3_volume: - type: string - percent_90_1_assign_material_name: - type: string - percent_90_1_target_weigh: - type: string - percent_90_2_assign_material_name: - type: string - percent_90_2_target_weigh: - type: string - percent_90_3_assign_material_name: - type: string - percent_90_3_target_weigh: - type: string - speed: - type: string - temperature: - type: string - required: - - order_name - - percent_90_1_assign_material_name - - percent_90_1_target_weigh - - percent_90_2_assign_material_name - - percent_90_2_target_weigh - - percent_90_3_assign_material_name - - percent_90_3_target_weigh - - percent_10_1_assign_material_name - - percent_10_1_target_weigh - - percent_10_1_volume - - percent_10_1_liquid_material_name - - percent_10_2_assign_material_name - - percent_10_2_target_weigh - - percent_10_2_volume - - percent_10_2_liquid_material_name - - percent_10_3_assign_material_name - - percent_10_3_target_weigh - - percent_10_3_volume - - percent_10_3_liquid_material_name - - speed - - temperature - - delay_time - - hold_m_name - title: DispenStationVialFeed_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: DispenStationVialFeed_Result - type: object - required: - - goal - title: DispenStationVialFeed - type: object - type: DispenStationVialFeed - create_diamine_solution_task: - feedback: {} - goal: - delay_time: delay_time - hold_m_name: hold_m_name - liquid_material_name: liquid_material_name - material_name: material_name - order_name: order_name - speed: speed - target_weigh: target_weigh - temperature: temperature - volume: volume - goal_default: - delay_time: '' - hold_m_name: '' - liquid_material_name: '' - material_name: '' - order_name: '' - speed: '' - target_weigh: '' - temperature: '' - volume: '' - handles: {} - result: - return_info: return_info - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: DispenStationSolnPrep_Feedback - type: object - goal: - properties: - delay_time: - type: string - hold_m_name: - type: string - liquid_material_name: - type: string - material_name: - type: string - order_name: - type: string - speed: - type: string - target_weigh: - type: string - temperature: - type: string - volume: - type: string - required: - - order_name - - material_name - - target_weigh - - volume - - liquid_material_name - - speed - - temperature - - delay_time - - hold_m_name - title: DispenStationSolnPrep_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: DispenStationSolnPrep_Result - type: object - required: - - goal - title: DispenStationSolnPrep - type: object - type: DispenStationSolnPrep - module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation - status_types: {} - type: python - config_info: [] - description: '' - handles: [] - icon: '' - init_param_schema: - config: - properties: - config: - type: string - deck: - type: string - required: - - config - - deck - type: object - data: - properties: {} - required: [] - type: object - version: 1.0.0 diff --git a/unilabos/registry/devices/bioyond_cell.yaml b/unilabos/registry/devices/bioyond_cell.yaml index 9243e21e..fc4b75cb 100644 --- a/unilabos/registry/devices/bioyond_cell.yaml +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -32,7 +32,112 @@ bioyond_cell: feedback: {} goal: {} goal_default: - xlsx_path: D:/UniLab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx + WH3_x1_y1_z3_1_materialId: '' + WH3_x1_y1_z3_1_materialType: '' + WH3_x1_y1_z3_1_quantity: 0 + WH3_x1_y2_z3_4_materialId: '' + WH3_x1_y2_z3_4_materialType: '' + WH3_x1_y2_z3_4_quantity: 0 + WH3_x1_y3_z3_7_materialId: '' + WH3_x1_y3_z3_7_materialType: '' + WH3_x1_y3_z3_7_quantity: 0 + WH3_x1_y4_z3_10_materialId: '' + WH3_x1_y4_z3_10_materialType: '' + WH3_x1_y4_z3_10_quantity: 0 + WH3_x1_y5_z3_13_materialId: '' + WH3_x1_y5_z3_13_materialType: '' + WH3_x1_y5_z3_13_quantity: 0 + WH3_x2_y1_z3_2_materialId: '' + WH3_x2_y1_z3_2_materialType: '' + WH3_x2_y1_z3_2_quantity: 0 + WH3_x2_y2_z3_5_materialId: '' + WH3_x2_y2_z3_5_materialType: '' + WH3_x2_y2_z3_5_quantity: 0 + WH3_x2_y3_z3_8_materialId: '' + WH3_x2_y3_z3_8_materialType: '' + WH3_x2_y3_z3_8_quantity: 0 + WH3_x2_y4_z3_11_materialId: '' + WH3_x2_y4_z3_11_materialType: '' + WH3_x2_y4_z3_11_quantity: 0 + WH3_x2_y5_z3_14_materialId: '' + WH3_x2_y5_z3_14_materialType: '' + WH3_x2_y5_z3_14_quantity: 0 + WH3_x3_y1_z3_3_materialId: '' + WH3_x3_y1_z3_3_materialType: '' + WH3_x3_y1_z3_3_quantity: 0 + WH3_x3_y2_z3_6_materialId: '' + WH3_x3_y2_z3_6_materialType: '' + WH3_x3_y2_z3_6_quantity: 0 + WH3_x3_y3_z3_9_materialId: '' + WH3_x3_y3_z3_9_materialType: '' + WH3_x3_y3_z3_9_quantity: 0 + WH3_x3_y4_z3_12_materialId: '' + WH3_x3_y4_z3_12_materialType: '' + WH3_x3_y4_z3_12_quantity: 0 + WH3_x3_y5_z3_15_materialId: '' + WH3_x3_y5_z3_15_materialType: '' + WH3_x3_y5_z3_15_quantity: 0 + WH4_x1_y1_z1_1_materialName: '' + WH4_x1_y1_z1_1_quantity: 0.0 + WH4_x1_y1_z2_1_materialName: '' + WH4_x1_y1_z2_1_materialType: '' + WH4_x1_y1_z2_1_quantity: 0.0 + WH4_x1_y1_z2_1_targetWH: '' + WH4_x1_y2_z1_6_materialName: '' + WH4_x1_y2_z1_6_quantity: 0.0 + WH4_x1_y2_z2_4_materialName: '' + WH4_x1_y2_z2_4_materialType: '' + WH4_x1_y2_z2_4_quantity: 0.0 + WH4_x1_y2_z2_4_targetWH: '' + WH4_x1_y3_z1_11_materialName: '' + WH4_x1_y3_z1_11_quantity: 0.0 + WH4_x1_y3_z2_7_materialName: '' + WH4_x1_y3_z2_7_materialType: '' + WH4_x1_y3_z2_7_quantity: 0.0 + WH4_x1_y3_z2_7_targetWH: '' + WH4_x2_y1_z1_2_materialName: '' + WH4_x2_y1_z1_2_quantity: 0.0 + WH4_x2_y1_z2_2_materialName: '' + WH4_x2_y1_z2_2_materialType: '' + WH4_x2_y1_z2_2_quantity: 0.0 + WH4_x2_y1_z2_2_targetWH: '' + WH4_x2_y2_z1_7_materialName: '' + WH4_x2_y2_z1_7_quantity: 0.0 + WH4_x2_y2_z2_5_materialName: '' + WH4_x2_y2_z2_5_materialType: '' + WH4_x2_y2_z2_5_quantity: 0.0 + WH4_x2_y2_z2_5_targetWH: '' + WH4_x2_y3_z1_12_materialName: '' + WH4_x2_y3_z1_12_quantity: 0.0 + WH4_x2_y3_z2_8_materialName: '' + WH4_x2_y3_z2_8_materialType: '' + WH4_x2_y3_z2_8_quantity: 0.0 + WH4_x2_y3_z2_8_targetWH: '' + WH4_x3_y1_z1_3_materialName: '' + WH4_x3_y1_z1_3_quantity: 0.0 + WH4_x3_y1_z2_3_materialName: '' + WH4_x3_y1_z2_3_materialType: '' + WH4_x3_y1_z2_3_quantity: 0.0 + WH4_x3_y1_z2_3_targetWH: '' + WH4_x3_y2_z1_8_materialName: '' + WH4_x3_y2_z1_8_quantity: 0.0 + WH4_x3_y2_z2_6_materialName: '' + WH4_x3_y2_z2_6_materialType: '' + WH4_x3_y2_z2_6_quantity: 0.0 + WH4_x3_y2_z2_6_targetWH: '' + WH4_x3_y3_z2_9_materialName: '' + WH4_x3_y3_z2_9_materialType: '' + WH4_x3_y3_z2_9_quantity: 0.0 + WH4_x3_y3_z2_9_targetWH: '' + WH4_x4_y1_z1_4_materialName: '' + WH4_x4_y1_z1_4_quantity: 0.0 + WH4_x4_y2_z1_9_materialName: '' + WH4_x4_y2_z1_9_quantity: 0.0 + WH4_x5_y1_z1_5_materialName: '' + WH4_x5_y1_z1_5_quantity: 0.0 + WH4_x5_y2_z1_10_materialName: '' + WH4_x5_y2_z1_10_quantity: 0.0 + xlsx_path: D:\UniLab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx handles: {} placeholder_keys: {} result: {} @@ -42,8 +147,323 @@ bioyond_cell: feedback: {} goal: properties: + WH3_x1_y1_z3_1_materialId: + default: '' + type: string + WH3_x1_y1_z3_1_materialType: + default: '' + type: string + WH3_x1_y1_z3_1_quantity: + default: 0 + type: number + WH3_x1_y2_z3_4_materialId: + default: '' + type: string + WH3_x1_y2_z3_4_materialType: + default: '' + type: string + WH3_x1_y2_z3_4_quantity: + default: 0 + type: number + WH3_x1_y3_z3_7_materialId: + default: '' + type: string + WH3_x1_y3_z3_7_materialType: + default: '' + type: string + WH3_x1_y3_z3_7_quantity: + default: 0 + type: number + WH3_x1_y4_z3_10_materialId: + default: '' + type: string + WH3_x1_y4_z3_10_materialType: + default: '' + type: string + WH3_x1_y4_z3_10_quantity: + default: 0 + type: number + WH3_x1_y5_z3_13_materialId: + default: '' + type: string + WH3_x1_y5_z3_13_materialType: + default: '' + type: string + WH3_x1_y5_z3_13_quantity: + default: 0 + type: number + WH3_x2_y1_z3_2_materialId: + default: '' + type: string + WH3_x2_y1_z3_2_materialType: + default: '' + type: string + WH3_x2_y1_z3_2_quantity: + default: 0 + type: number + WH3_x2_y2_z3_5_materialId: + default: '' + type: string + WH3_x2_y2_z3_5_materialType: + default: '' + type: string + WH3_x2_y2_z3_5_quantity: + default: 0 + type: number + WH3_x2_y3_z3_8_materialId: + default: '' + type: string + WH3_x2_y3_z3_8_materialType: + default: '' + type: string + WH3_x2_y3_z3_8_quantity: + default: 0 + type: number + WH3_x2_y4_z3_11_materialId: + default: '' + type: string + WH3_x2_y4_z3_11_materialType: + default: '' + type: string + WH3_x2_y4_z3_11_quantity: + default: 0 + type: number + WH3_x2_y5_z3_14_materialId: + default: '' + type: string + WH3_x2_y5_z3_14_materialType: + default: '' + type: string + WH3_x2_y5_z3_14_quantity: + default: 0 + type: number + WH3_x3_y1_z3_3_materialId: + default: '' + type: string + WH3_x3_y1_z3_3_materialType: + default: '' + type: string + WH3_x3_y1_z3_3_quantity: + default: 0 + type: number + WH3_x3_y2_z3_6_materialId: + default: '' + type: string + WH3_x3_y2_z3_6_materialType: + default: '' + type: string + WH3_x3_y2_z3_6_quantity: + default: 0 + type: number + WH3_x3_y3_z3_9_materialId: + default: '' + type: string + WH3_x3_y3_z3_9_materialType: + default: '' + type: string + WH3_x3_y3_z3_9_quantity: + default: 0 + type: number + WH3_x3_y4_z3_12_materialId: + default: '' + type: string + WH3_x3_y4_z3_12_materialType: + default: '' + type: string + WH3_x3_y4_z3_12_quantity: + default: 0 + type: number + WH3_x3_y5_z3_15_materialId: + default: '' + type: string + WH3_x3_y5_z3_15_materialType: + default: '' + type: string + WH3_x3_y5_z3_15_quantity: + default: 0 + type: number + WH4_x1_y1_z1_1_materialName: + default: '' + type: string + WH4_x1_y1_z1_1_quantity: + default: 0.0 + type: number + WH4_x1_y1_z2_1_materialName: + default: '' + type: string + WH4_x1_y1_z2_1_materialType: + default: '' + type: string + WH4_x1_y1_z2_1_quantity: + default: 0.0 + type: number + WH4_x1_y1_z2_1_targetWH: + default: '' + type: string + WH4_x1_y2_z1_6_materialName: + default: '' + type: string + WH4_x1_y2_z1_6_quantity: + default: 0.0 + type: number + WH4_x1_y2_z2_4_materialName: + default: '' + type: string + WH4_x1_y2_z2_4_materialType: + default: '' + type: string + WH4_x1_y2_z2_4_quantity: + default: 0.0 + type: number + WH4_x1_y2_z2_4_targetWH: + default: '' + type: string + WH4_x1_y3_z1_11_materialName: + default: '' + type: string + WH4_x1_y3_z1_11_quantity: + default: 0.0 + type: number + WH4_x1_y3_z2_7_materialName: + default: '' + type: string + WH4_x1_y3_z2_7_materialType: + default: '' + type: string + WH4_x1_y3_z2_7_quantity: + default: 0.0 + type: number + WH4_x1_y3_z2_7_targetWH: + default: '' + type: string + WH4_x2_y1_z1_2_materialName: + default: '' + type: string + WH4_x2_y1_z1_2_quantity: + default: 0.0 + type: number + WH4_x2_y1_z2_2_materialName: + default: '' + type: string + WH4_x2_y1_z2_2_materialType: + default: '' + type: string + WH4_x2_y1_z2_2_quantity: + default: 0.0 + type: number + WH4_x2_y1_z2_2_targetWH: + default: '' + type: string + WH4_x2_y2_z1_7_materialName: + default: '' + type: string + WH4_x2_y2_z1_7_quantity: + default: 0.0 + type: number + WH4_x2_y2_z2_5_materialName: + default: '' + type: string + WH4_x2_y2_z2_5_materialType: + default: '' + type: string + WH4_x2_y2_z2_5_quantity: + default: 0.0 + type: number + WH4_x2_y2_z2_5_targetWH: + default: '' + type: string + WH4_x2_y3_z1_12_materialName: + default: '' + type: string + WH4_x2_y3_z1_12_quantity: + default: 0.0 + type: number + WH4_x2_y3_z2_8_materialName: + default: '' + type: string + WH4_x2_y3_z2_8_materialType: + default: '' + type: string + WH4_x2_y3_z2_8_quantity: + default: 0.0 + type: number + WH4_x2_y3_z2_8_targetWH: + default: '' + type: string + WH4_x3_y1_z1_3_materialName: + default: '' + type: string + WH4_x3_y1_z1_3_quantity: + default: 0.0 + type: number + WH4_x3_y1_z2_3_materialName: + default: '' + type: string + WH4_x3_y1_z2_3_materialType: + default: '' + type: string + WH4_x3_y1_z2_3_quantity: + default: 0.0 + type: number + WH4_x3_y1_z2_3_targetWH: + default: '' + type: string + WH4_x3_y2_z1_8_materialName: + default: '' + type: string + WH4_x3_y2_z1_8_quantity: + default: 0.0 + type: number + WH4_x3_y2_z2_6_materialName: + default: '' + type: string + WH4_x3_y2_z2_6_materialType: + default: '' + type: string + WH4_x3_y2_z2_6_quantity: + default: 0.0 + type: number + WH4_x3_y2_z2_6_targetWH: + default: '' + type: string + WH4_x3_y3_z2_9_materialName: + default: '' + type: string + WH4_x3_y3_z2_9_materialType: + default: '' + type: string + WH4_x3_y3_z2_9_quantity: + default: 0.0 + type: number + WH4_x3_y3_z2_9_targetWH: + default: '' + type: string + WH4_x4_y1_z1_4_materialName: + default: '' + type: string + WH4_x4_y1_z1_4_quantity: + default: 0.0 + type: number + WH4_x4_y2_z1_9_materialName: + default: '' + type: string + WH4_x4_y2_z1_9_quantity: + default: 0.0 + type: number + WH4_x5_y1_z1_5_materialName: + default: '' + type: string + WH4_x5_y1_z1_5_quantity: + default: 0.0 + type: number + WH4_x5_y2_z1_10_materialName: + default: '' + type: string + WH4_x5_y2_z1_10_quantity: + default: 0.0 + type: number xlsx_path: - default: D:/UniLab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/2025122301.xlsx + default: D:\UniLab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx type: string required: [] type: object @@ -507,7 +927,112 @@ bioyond_cell: feedback: {} goal: {} goal_default: - xlsx_path: D:/UniLab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx + WH3_x1_y1_z3_1_materialId: '' + WH3_x1_y1_z3_1_materialType: '' + WH3_x1_y1_z3_1_quantity: 0 + WH3_x1_y2_z3_4_materialId: '' + WH3_x1_y2_z3_4_materialType: '' + WH3_x1_y2_z3_4_quantity: 0 + WH3_x1_y3_z3_7_materialId: '' + WH3_x1_y3_z3_7_materialType: '' + WH3_x1_y3_z3_7_quantity: 0 + WH3_x1_y4_z3_10_materialId: '' + WH3_x1_y4_z3_10_materialType: '' + WH3_x1_y4_z3_10_quantity: 0 + WH3_x1_y5_z3_13_materialId: '' + WH3_x1_y5_z3_13_materialType: '' + WH3_x1_y5_z3_13_quantity: 0 + WH3_x2_y1_z3_2_materialId: '' + WH3_x2_y1_z3_2_materialType: '' + WH3_x2_y1_z3_2_quantity: 0 + WH3_x2_y2_z3_5_materialId: '' + WH3_x2_y2_z3_5_materialType: '' + WH3_x2_y2_z3_5_quantity: 0 + WH3_x2_y3_z3_8_materialId: '' + WH3_x2_y3_z3_8_materialType: '' + WH3_x2_y3_z3_8_quantity: 0 + WH3_x2_y4_z3_11_materialId: '' + WH3_x2_y4_z3_11_materialType: '' + WH3_x2_y4_z3_11_quantity: 0 + WH3_x2_y5_z3_14_materialId: '' + WH3_x2_y5_z3_14_materialType: '' + WH3_x2_y5_z3_14_quantity: 0 + WH3_x3_y1_z3_3_materialId: '' + WH3_x3_y1_z3_3_materialType: '' + WH3_x3_y1_z3_3_quantity: 0 + WH3_x3_y2_z3_6_materialId: '' + WH3_x3_y2_z3_6_materialType: '' + WH3_x3_y2_z3_6_quantity: 0 + WH3_x3_y3_z3_9_materialId: '' + WH3_x3_y3_z3_9_materialType: '' + WH3_x3_y3_z3_9_quantity: 0 + WH3_x3_y4_z3_12_materialId: '' + WH3_x3_y4_z3_12_materialType: '' + WH3_x3_y4_z3_12_quantity: 0 + WH3_x3_y5_z3_15_materialId: '' + WH3_x3_y5_z3_15_materialType: '' + WH3_x3_y5_z3_15_quantity: 0 + WH4_x1_y1_z1_1_materialName: '' + WH4_x1_y1_z1_1_quantity: 0.0 + WH4_x1_y1_z2_1_materialName: '' + WH4_x1_y1_z2_1_materialType: '' + WH4_x1_y1_z2_1_quantity: 0.0 + WH4_x1_y1_z2_1_targetWH: '' + WH4_x1_y2_z1_6_materialName: '' + WH4_x1_y2_z1_6_quantity: 0.0 + WH4_x1_y2_z2_4_materialName: '' + WH4_x1_y2_z2_4_materialType: '' + WH4_x1_y2_z2_4_quantity: 0.0 + WH4_x1_y2_z2_4_targetWH: '' + WH4_x1_y3_z1_11_materialName: '' + WH4_x1_y3_z1_11_quantity: 0.0 + WH4_x1_y3_z2_7_materialName: '' + WH4_x1_y3_z2_7_materialType: '' + WH4_x1_y3_z2_7_quantity: 0.0 + WH4_x1_y3_z2_7_targetWH: '' + WH4_x2_y1_z1_2_materialName: '' + WH4_x2_y1_z1_2_quantity: 0.0 + WH4_x2_y1_z2_2_materialName: '' + WH4_x2_y1_z2_2_materialType: '' + WH4_x2_y1_z2_2_quantity: 0.0 + WH4_x2_y1_z2_2_targetWH: '' + WH4_x2_y2_z1_7_materialName: '' + WH4_x2_y2_z1_7_quantity: 0.0 + WH4_x2_y2_z2_5_materialName: '' + WH4_x2_y2_z2_5_materialType: '' + WH4_x2_y2_z2_5_quantity: 0.0 + WH4_x2_y2_z2_5_targetWH: '' + WH4_x2_y3_z1_12_materialName: '' + WH4_x2_y3_z1_12_quantity: 0.0 + WH4_x2_y3_z2_8_materialName: '' + WH4_x2_y3_z2_8_materialType: '' + WH4_x2_y3_z2_8_quantity: 0.0 + WH4_x2_y3_z2_8_targetWH: '' + WH4_x3_y1_z1_3_materialName: '' + WH4_x3_y1_z1_3_quantity: 0.0 + WH4_x3_y1_z2_3_materialName: '' + WH4_x3_y1_z2_3_materialType: '' + WH4_x3_y1_z2_3_quantity: 0.0 + WH4_x3_y1_z2_3_targetWH: '' + WH4_x3_y2_z1_8_materialName: '' + WH4_x3_y2_z1_8_quantity: 0.0 + WH4_x3_y2_z2_6_materialName: '' + WH4_x3_y2_z2_6_materialType: '' + WH4_x3_y2_z2_6_quantity: 0.0 + WH4_x3_y2_z2_6_targetWH: '' + WH4_x3_y3_z2_9_materialName: '' + WH4_x3_y3_z2_9_materialType: '' + WH4_x3_y3_z2_9_quantity: 0.0 + WH4_x3_y3_z2_9_targetWH: '' + WH4_x4_y1_z1_4_materialName: '' + WH4_x4_y1_z1_4_quantity: 0.0 + WH4_x4_y2_z1_9_materialName: '' + WH4_x4_y2_z1_9_quantity: 0.0 + WH4_x5_y1_z1_5_materialName: '' + WH4_x5_y1_z1_5_quantity: 0.0 + WH4_x5_y2_z1_10_materialName: '' + WH4_x5_y2_z1_10_quantity: 0.0 + xlsx_path: D:\UniLab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx handles: {} placeholder_keys: {} result: {} @@ -517,8 +1042,323 @@ bioyond_cell: feedback: {} goal: properties: + WH3_x1_y1_z3_1_materialId: + default: '' + type: string + WH3_x1_y1_z3_1_materialType: + default: '' + type: string + WH3_x1_y1_z3_1_quantity: + default: 0 + type: number + WH3_x1_y2_z3_4_materialId: + default: '' + type: string + WH3_x1_y2_z3_4_materialType: + default: '' + type: string + WH3_x1_y2_z3_4_quantity: + default: 0 + type: number + WH3_x1_y3_z3_7_materialId: + default: '' + type: string + WH3_x1_y3_z3_7_materialType: + default: '' + type: string + WH3_x1_y3_z3_7_quantity: + default: 0 + type: number + WH3_x1_y4_z3_10_materialId: + default: '' + type: string + WH3_x1_y4_z3_10_materialType: + default: '' + type: string + WH3_x1_y4_z3_10_quantity: + default: 0 + type: number + WH3_x1_y5_z3_13_materialId: + default: '' + type: string + WH3_x1_y5_z3_13_materialType: + default: '' + type: string + WH3_x1_y5_z3_13_quantity: + default: 0 + type: number + WH3_x2_y1_z3_2_materialId: + default: '' + type: string + WH3_x2_y1_z3_2_materialType: + default: '' + type: string + WH3_x2_y1_z3_2_quantity: + default: 0 + type: number + WH3_x2_y2_z3_5_materialId: + default: '' + type: string + WH3_x2_y2_z3_5_materialType: + default: '' + type: string + WH3_x2_y2_z3_5_quantity: + default: 0 + type: number + WH3_x2_y3_z3_8_materialId: + default: '' + type: string + WH3_x2_y3_z3_8_materialType: + default: '' + type: string + WH3_x2_y3_z3_8_quantity: + default: 0 + type: number + WH3_x2_y4_z3_11_materialId: + default: '' + type: string + WH3_x2_y4_z3_11_materialType: + default: '' + type: string + WH3_x2_y4_z3_11_quantity: + default: 0 + type: number + WH3_x2_y5_z3_14_materialId: + default: '' + type: string + WH3_x2_y5_z3_14_materialType: + default: '' + type: string + WH3_x2_y5_z3_14_quantity: + default: 0 + type: number + WH3_x3_y1_z3_3_materialId: + default: '' + type: string + WH3_x3_y1_z3_3_materialType: + default: '' + type: string + WH3_x3_y1_z3_3_quantity: + default: 0 + type: number + WH3_x3_y2_z3_6_materialId: + default: '' + type: string + WH3_x3_y2_z3_6_materialType: + default: '' + type: string + WH3_x3_y2_z3_6_quantity: + default: 0 + type: number + WH3_x3_y3_z3_9_materialId: + default: '' + type: string + WH3_x3_y3_z3_9_materialType: + default: '' + type: string + WH3_x3_y3_z3_9_quantity: + default: 0 + type: number + WH3_x3_y4_z3_12_materialId: + default: '' + type: string + WH3_x3_y4_z3_12_materialType: + default: '' + type: string + WH3_x3_y4_z3_12_quantity: + default: 0 + type: number + WH3_x3_y5_z3_15_materialId: + default: '' + type: string + WH3_x3_y5_z3_15_materialType: + default: '' + type: string + WH3_x3_y5_z3_15_quantity: + default: 0 + type: number + WH4_x1_y1_z1_1_materialName: + default: '' + type: string + WH4_x1_y1_z1_1_quantity: + default: 0.0 + type: number + WH4_x1_y1_z2_1_materialName: + default: '' + type: string + WH4_x1_y1_z2_1_materialType: + default: '' + type: string + WH4_x1_y1_z2_1_quantity: + default: 0.0 + type: number + WH4_x1_y1_z2_1_targetWH: + default: '' + type: string + WH4_x1_y2_z1_6_materialName: + default: '' + type: string + WH4_x1_y2_z1_6_quantity: + default: 0.0 + type: number + WH4_x1_y2_z2_4_materialName: + default: '' + type: string + WH4_x1_y2_z2_4_materialType: + default: '' + type: string + WH4_x1_y2_z2_4_quantity: + default: 0.0 + type: number + WH4_x1_y2_z2_4_targetWH: + default: '' + type: string + WH4_x1_y3_z1_11_materialName: + default: '' + type: string + WH4_x1_y3_z1_11_quantity: + default: 0.0 + type: number + WH4_x1_y3_z2_7_materialName: + default: '' + type: string + WH4_x1_y3_z2_7_materialType: + default: '' + type: string + WH4_x1_y3_z2_7_quantity: + default: 0.0 + type: number + WH4_x1_y3_z2_7_targetWH: + default: '' + type: string + WH4_x2_y1_z1_2_materialName: + default: '' + type: string + WH4_x2_y1_z1_2_quantity: + default: 0.0 + type: number + WH4_x2_y1_z2_2_materialName: + default: '' + type: string + WH4_x2_y1_z2_2_materialType: + default: '' + type: string + WH4_x2_y1_z2_2_quantity: + default: 0.0 + type: number + WH4_x2_y1_z2_2_targetWH: + default: '' + type: string + WH4_x2_y2_z1_7_materialName: + default: '' + type: string + WH4_x2_y2_z1_7_quantity: + default: 0.0 + type: number + WH4_x2_y2_z2_5_materialName: + default: '' + type: string + WH4_x2_y2_z2_5_materialType: + default: '' + type: string + WH4_x2_y2_z2_5_quantity: + default: 0.0 + type: number + WH4_x2_y2_z2_5_targetWH: + default: '' + type: string + WH4_x2_y3_z1_12_materialName: + default: '' + type: string + WH4_x2_y3_z1_12_quantity: + default: 0.0 + type: number + WH4_x2_y3_z2_8_materialName: + default: '' + type: string + WH4_x2_y3_z2_8_materialType: + default: '' + type: string + WH4_x2_y3_z2_8_quantity: + default: 0.0 + type: number + WH4_x2_y3_z2_8_targetWH: + default: '' + type: string + WH4_x3_y1_z1_3_materialName: + default: '' + type: string + WH4_x3_y1_z1_3_quantity: + default: 0.0 + type: number + WH4_x3_y1_z2_3_materialName: + default: '' + type: string + WH4_x3_y1_z2_3_materialType: + default: '' + type: string + WH4_x3_y1_z2_3_quantity: + default: 0.0 + type: number + WH4_x3_y1_z2_3_targetWH: + default: '' + type: string + WH4_x3_y2_z1_8_materialName: + default: '' + type: string + WH4_x3_y2_z1_8_quantity: + default: 0.0 + type: number + WH4_x3_y2_z2_6_materialName: + default: '' + type: string + WH4_x3_y2_z2_6_materialType: + default: '' + type: string + WH4_x3_y2_z2_6_quantity: + default: 0.0 + type: number + WH4_x3_y2_z2_6_targetWH: + default: '' + type: string + WH4_x3_y3_z2_9_materialName: + default: '' + type: string + WH4_x3_y3_z2_9_materialType: + default: '' + type: string + WH4_x3_y3_z2_9_quantity: + default: 0.0 + type: number + WH4_x3_y3_z2_9_targetWH: + default: '' + type: string + WH4_x4_y1_z1_4_materialName: + default: '' + type: string + WH4_x4_y1_z1_4_quantity: + default: 0.0 + type: number + WH4_x4_y2_z1_9_materialName: + default: '' + type: string + WH4_x4_y2_z1_9_quantity: + default: 0.0 + type: number + WH4_x5_y1_z1_5_materialName: + default: '' + type: string + WH4_x5_y1_z1_5_quantity: + default: 0.0 + type: number + WH4_x5_y2_z1_10_materialName: + default: '' + type: string + WH4_x5_y2_z1_10_quantity: + default: 0.0 + type: number xlsx_path: - default: D:/UniLab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx + default: D:\UniLab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx type: string required: [] type: object @@ -532,7 +1372,112 @@ bioyond_cell: feedback: {} goal: {} goal_default: - xlsx_path: D:/UniLab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx + WH3_x1_y1_z3_1_materialId: '' + WH3_x1_y1_z3_1_materialType: '' + WH3_x1_y1_z3_1_quantity: 0 + WH3_x1_y2_z3_4_materialId: '' + WH3_x1_y2_z3_4_materialType: '' + WH3_x1_y2_z3_4_quantity: 0 + WH3_x1_y3_z3_7_materialId: '' + WH3_x1_y3_z3_7_materialType: '' + WH3_x1_y3_z3_7_quantity: 0 + WH3_x1_y4_z3_10_materialId: '' + WH3_x1_y4_z3_10_materialType: '' + WH3_x1_y4_z3_10_quantity: 0 + WH3_x1_y5_z3_13_materialId: '' + WH3_x1_y5_z3_13_materialType: '' + WH3_x1_y5_z3_13_quantity: 0 + WH3_x2_y1_z3_2_materialId: '' + WH3_x2_y1_z3_2_materialType: '' + WH3_x2_y1_z3_2_quantity: 0 + WH3_x2_y2_z3_5_materialId: '' + WH3_x2_y2_z3_5_materialType: '' + WH3_x2_y2_z3_5_quantity: 0 + WH3_x2_y3_z3_8_materialId: '' + WH3_x2_y3_z3_8_materialType: '' + WH3_x2_y3_z3_8_quantity: 0 + WH3_x2_y4_z3_11_materialId: '' + WH3_x2_y4_z3_11_materialType: '' + WH3_x2_y4_z3_11_quantity: 0 + WH3_x2_y5_z3_14_materialId: '' + WH3_x2_y5_z3_14_materialType: '' + WH3_x2_y5_z3_14_quantity: 0 + WH3_x3_y1_z3_3_materialId: '' + WH3_x3_y1_z3_3_materialType: '' + WH3_x3_y1_z3_3_quantity: 0 + WH3_x3_y2_z3_6_materialId: '' + WH3_x3_y2_z3_6_materialType: '' + WH3_x3_y2_z3_6_quantity: 0 + WH3_x3_y3_z3_9_materialId: '' + WH3_x3_y3_z3_9_materialType: '' + WH3_x3_y3_z3_9_quantity: 0 + WH3_x3_y4_z3_12_materialId: '' + WH3_x3_y4_z3_12_materialType: '' + WH3_x3_y4_z3_12_quantity: 0 + WH3_x3_y5_z3_15_materialId: '' + WH3_x3_y5_z3_15_materialType: '' + WH3_x3_y5_z3_15_quantity: 0 + WH4_x1_y1_z1_1_materialName: '' + WH4_x1_y1_z1_1_quantity: 0.0 + WH4_x1_y1_z2_1_materialName: '' + WH4_x1_y1_z2_1_materialType: '' + WH4_x1_y1_z2_1_quantity: 0.0 + WH4_x1_y1_z2_1_targetWH: '' + WH4_x1_y2_z1_6_materialName: '' + WH4_x1_y2_z1_6_quantity: 0.0 + WH4_x1_y2_z2_4_materialName: '' + WH4_x1_y2_z2_4_materialType: '' + WH4_x1_y2_z2_4_quantity: 0.0 + WH4_x1_y2_z2_4_targetWH: '' + WH4_x1_y3_z1_11_materialName: '' + WH4_x1_y3_z1_11_quantity: 0.0 + WH4_x1_y3_z2_7_materialName: '' + WH4_x1_y3_z2_7_materialType: '' + WH4_x1_y3_z2_7_quantity: 0.0 + WH4_x1_y3_z2_7_targetWH: '' + WH4_x2_y1_z1_2_materialName: '' + WH4_x2_y1_z1_2_quantity: 0.0 + WH4_x2_y1_z2_2_materialName: '' + WH4_x2_y1_z2_2_materialType: '' + WH4_x2_y1_z2_2_quantity: 0.0 + WH4_x2_y1_z2_2_targetWH: '' + WH4_x2_y2_z1_7_materialName: '' + WH4_x2_y2_z1_7_quantity: 0.0 + WH4_x2_y2_z2_5_materialName: '' + WH4_x2_y2_z2_5_materialType: '' + WH4_x2_y2_z2_5_quantity: 0.0 + WH4_x2_y2_z2_5_targetWH: '' + WH4_x2_y3_z1_12_materialName: '' + WH4_x2_y3_z1_12_quantity: 0.0 + WH4_x2_y3_z2_8_materialName: '' + WH4_x2_y3_z2_8_materialType: '' + WH4_x2_y3_z2_8_quantity: 0.0 + WH4_x2_y3_z2_8_targetWH: '' + WH4_x3_y1_z1_3_materialName: '' + WH4_x3_y1_z1_3_quantity: 0.0 + WH4_x3_y1_z2_3_materialName: '' + WH4_x3_y1_z2_3_materialType: '' + WH4_x3_y1_z2_3_quantity: 0.0 + WH4_x3_y1_z2_3_targetWH: '' + WH4_x3_y2_z1_8_materialName: '' + WH4_x3_y2_z1_8_quantity: 0.0 + WH4_x3_y2_z2_6_materialName: '' + WH4_x3_y2_z2_6_materialType: '' + WH4_x3_y2_z2_6_quantity: 0.0 + WH4_x3_y2_z2_6_targetWH: '' + WH4_x3_y3_z2_9_materialName: '' + WH4_x3_y3_z2_9_materialType: '' + WH4_x3_y3_z2_9_quantity: 0.0 + WH4_x3_y3_z2_9_targetWH: '' + WH4_x4_y1_z1_4_materialName: '' + WH4_x4_y1_z1_4_quantity: 0.0 + WH4_x4_y2_z1_9_materialName: '' + WH4_x4_y2_z1_9_quantity: 0.0 + WH4_x5_y1_z1_5_materialName: '' + WH4_x5_y1_z1_5_quantity: 0.0 + WH4_x5_y2_z1_10_materialName: '' + WH4_x5_y2_z1_10_quantity: 0.0 + xlsx_path: D:\UniLab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx handles: {} placeholder_keys: {} result: {} @@ -542,8 +1487,323 @@ bioyond_cell: feedback: {} goal: properties: + WH3_x1_y1_z3_1_materialId: + default: '' + type: string + WH3_x1_y1_z3_1_materialType: + default: '' + type: string + WH3_x1_y1_z3_1_quantity: + default: 0 + type: number + WH3_x1_y2_z3_4_materialId: + default: '' + type: string + WH3_x1_y2_z3_4_materialType: + default: '' + type: string + WH3_x1_y2_z3_4_quantity: + default: 0 + type: number + WH3_x1_y3_z3_7_materialId: + default: '' + type: string + WH3_x1_y3_z3_7_materialType: + default: '' + type: string + WH3_x1_y3_z3_7_quantity: + default: 0 + type: number + WH3_x1_y4_z3_10_materialId: + default: '' + type: string + WH3_x1_y4_z3_10_materialType: + default: '' + type: string + WH3_x1_y4_z3_10_quantity: + default: 0 + type: number + WH3_x1_y5_z3_13_materialId: + default: '' + type: string + WH3_x1_y5_z3_13_materialType: + default: '' + type: string + WH3_x1_y5_z3_13_quantity: + default: 0 + type: number + WH3_x2_y1_z3_2_materialId: + default: '' + type: string + WH3_x2_y1_z3_2_materialType: + default: '' + type: string + WH3_x2_y1_z3_2_quantity: + default: 0 + type: number + WH3_x2_y2_z3_5_materialId: + default: '' + type: string + WH3_x2_y2_z3_5_materialType: + default: '' + type: string + WH3_x2_y2_z3_5_quantity: + default: 0 + type: number + WH3_x2_y3_z3_8_materialId: + default: '' + type: string + WH3_x2_y3_z3_8_materialType: + default: '' + type: string + WH3_x2_y3_z3_8_quantity: + default: 0 + type: number + WH3_x2_y4_z3_11_materialId: + default: '' + type: string + WH3_x2_y4_z3_11_materialType: + default: '' + type: string + WH3_x2_y4_z3_11_quantity: + default: 0 + type: number + WH3_x2_y5_z3_14_materialId: + default: '' + type: string + WH3_x2_y5_z3_14_materialType: + default: '' + type: string + WH3_x2_y5_z3_14_quantity: + default: 0 + type: number + WH3_x3_y1_z3_3_materialId: + default: '' + type: string + WH3_x3_y1_z3_3_materialType: + default: '' + type: string + WH3_x3_y1_z3_3_quantity: + default: 0 + type: number + WH3_x3_y2_z3_6_materialId: + default: '' + type: string + WH3_x3_y2_z3_6_materialType: + default: '' + type: string + WH3_x3_y2_z3_6_quantity: + default: 0 + type: number + WH3_x3_y3_z3_9_materialId: + default: '' + type: string + WH3_x3_y3_z3_9_materialType: + default: '' + type: string + WH3_x3_y3_z3_9_quantity: + default: 0 + type: number + WH3_x3_y4_z3_12_materialId: + default: '' + type: string + WH3_x3_y4_z3_12_materialType: + default: '' + type: string + WH3_x3_y4_z3_12_quantity: + default: 0 + type: number + WH3_x3_y5_z3_15_materialId: + default: '' + type: string + WH3_x3_y5_z3_15_materialType: + default: '' + type: string + WH3_x3_y5_z3_15_quantity: + default: 0 + type: number + WH4_x1_y1_z1_1_materialName: + default: '' + type: string + WH4_x1_y1_z1_1_quantity: + default: 0.0 + type: number + WH4_x1_y1_z2_1_materialName: + default: '' + type: string + WH4_x1_y1_z2_1_materialType: + default: '' + type: string + WH4_x1_y1_z2_1_quantity: + default: 0.0 + type: number + WH4_x1_y1_z2_1_targetWH: + default: '' + type: string + WH4_x1_y2_z1_6_materialName: + default: '' + type: string + WH4_x1_y2_z1_6_quantity: + default: 0.0 + type: number + WH4_x1_y2_z2_4_materialName: + default: '' + type: string + WH4_x1_y2_z2_4_materialType: + default: '' + type: string + WH4_x1_y2_z2_4_quantity: + default: 0.0 + type: number + WH4_x1_y2_z2_4_targetWH: + default: '' + type: string + WH4_x1_y3_z1_11_materialName: + default: '' + type: string + WH4_x1_y3_z1_11_quantity: + default: 0.0 + type: number + WH4_x1_y3_z2_7_materialName: + default: '' + type: string + WH4_x1_y3_z2_7_materialType: + default: '' + type: string + WH4_x1_y3_z2_7_quantity: + default: 0.0 + type: number + WH4_x1_y3_z2_7_targetWH: + default: '' + type: string + WH4_x2_y1_z1_2_materialName: + default: '' + type: string + WH4_x2_y1_z1_2_quantity: + default: 0.0 + type: number + WH4_x2_y1_z2_2_materialName: + default: '' + type: string + WH4_x2_y1_z2_2_materialType: + default: '' + type: string + WH4_x2_y1_z2_2_quantity: + default: 0.0 + type: number + WH4_x2_y1_z2_2_targetWH: + default: '' + type: string + WH4_x2_y2_z1_7_materialName: + default: '' + type: string + WH4_x2_y2_z1_7_quantity: + default: 0.0 + type: number + WH4_x2_y2_z2_5_materialName: + default: '' + type: string + WH4_x2_y2_z2_5_materialType: + default: '' + type: string + WH4_x2_y2_z2_5_quantity: + default: 0.0 + type: number + WH4_x2_y2_z2_5_targetWH: + default: '' + type: string + WH4_x2_y3_z1_12_materialName: + default: '' + type: string + WH4_x2_y3_z1_12_quantity: + default: 0.0 + type: number + WH4_x2_y3_z2_8_materialName: + default: '' + type: string + WH4_x2_y3_z2_8_materialType: + default: '' + type: string + WH4_x2_y3_z2_8_quantity: + default: 0.0 + type: number + WH4_x2_y3_z2_8_targetWH: + default: '' + type: string + WH4_x3_y1_z1_3_materialName: + default: '' + type: string + WH4_x3_y1_z1_3_quantity: + default: 0.0 + type: number + WH4_x3_y1_z2_3_materialName: + default: '' + type: string + WH4_x3_y1_z2_3_materialType: + default: '' + type: string + WH4_x3_y1_z2_3_quantity: + default: 0.0 + type: number + WH4_x3_y1_z2_3_targetWH: + default: '' + type: string + WH4_x3_y2_z1_8_materialName: + default: '' + type: string + WH4_x3_y2_z1_8_quantity: + default: 0.0 + type: number + WH4_x3_y2_z2_6_materialName: + default: '' + type: string + WH4_x3_y2_z2_6_materialType: + default: '' + type: string + WH4_x3_y2_z2_6_quantity: + default: 0.0 + type: number + WH4_x3_y2_z2_6_targetWH: + default: '' + type: string + WH4_x3_y3_z2_9_materialName: + default: '' + type: string + WH4_x3_y3_z2_9_materialType: + default: '' + type: string + WH4_x3_y3_z2_9_quantity: + default: 0.0 + type: number + WH4_x3_y3_z2_9_targetWH: + default: '' + type: string + WH4_x4_y1_z1_4_materialName: + default: '' + type: string + WH4_x4_y1_z1_4_quantity: + default: 0.0 + type: number + WH4_x4_y2_z1_9_materialName: + default: '' + type: string + WH4_x4_y2_z1_9_quantity: + default: 0.0 + type: number + WH4_x5_y1_z1_5_materialName: + default: '' + type: string + WH4_x5_y1_z1_5_quantity: + default: 0.0 + type: number + WH4_x5_y2_z1_10_materialName: + default: '' + type: string + WH4_x5_y2_z1_10_quantity: + default: 0.0 + type: number xlsx_path: - default: D:/UniLab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx + default: D:\UniLab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\bioyond_cell\material_template.xlsx type: string required: [] type: object @@ -785,6 +2045,39 @@ bioyond_cell: title: wait_for_order_finish参数 type: object type: UniLabJsonCommand + auto-wait_for_order_finish_polling: + feedback: {} + goal: {} + goal_default: + order_code: null + poll_interval: 0.5 + timeout: 36000 + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + order_code: + type: string + poll_interval: + default: 0.5 + type: number + timeout: + default: 36000 + type: integer + required: + - order_code + type: object + result: {} + required: + - goal + title: wait_for_order_finish_polling参数 + type: object + type: UniLabJsonCommand auto-wait_for_transfer_task: feedback: {} goal: {} @@ -820,6 +2113,7 @@ bioyond_cell: module: unilabos.devices.workstation.bioyond_studio.bioyond_cell.bioyond_cell_workstation:BioyondCellWorkstation status_types: device_id: String + material_info: dict type: python config_info: [] description: '' @@ -828,7 +2122,7 @@ bioyond_cell: init_param_schema: config: properties: - config: + bioyond_config: type: object deck: type: string @@ -840,8 +2134,11 @@ bioyond_cell: properties: device_id: type: string + material_info: + type: object required: - device_id + - material_info type: object registry_type: device version: 1.0.0 diff --git a/unilabos/registry/devices/bioyond_dispensing_station.yaml b/unilabos/registry/devices/bioyond_dispensing_station.yaml index 97b55cc1..7b9ebc90 100644 --- a/unilabos/registry/devices/bioyond_dispensing_station.yaml +++ b/unilabos/registry/devices/bioyond_dispensing_station.yaml @@ -5,6 +5,135 @@ bioyond_dispensing_station: - bioyond_dispensing_station class: action_value_mappings: + auto-brief_step_parameters: + feedback: {} + goal: {} + goal_default: + data: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + data: + type: object + required: + - data + type: object + result: {} + required: + - goal + title: brief_step_parameters参数 + type: object + type: UniLabJsonCommand + auto-process_order_finish_report: + feedback: {} + goal: {} + goal_default: + report_request: null + used_materials: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + report_request: + type: string + used_materials: + type: string + required: + - report_request + - used_materials + type: object + result: {} + required: + - goal + title: process_order_finish_report参数 + type: object + type: UniLabJsonCommand + auto-project_order_report: + feedback: {} + goal: {} + goal_default: + order_id: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + order_id: + type: string + required: + - order_id + type: object + result: {} + required: + - goal + title: project_order_report参数 + type: object + type: UniLabJsonCommand + auto-query_resource_by_name: + feedback: {} + goal: {} + goal_default: + material_name: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + material_name: + type: string + required: + - material_name + type: object + result: {} + required: + - goal + title: query_resource_by_name参数 + type: object + type: UniLabJsonCommand + auto-workflow_sample_locations: + feedback: {} + goal: {} + goal_default: + workflow_id: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + workflow_id: + type: string + required: + - workflow_id + type: object + result: {} + required: + - goal + title: workflow_sample_locations参数 + type: object + type: UniLabJsonCommand batch_create_90_10_vial_feeding_tasks: feedback: {} goal: diff --git a/unilabos/registry/devices/coin_cell_workstation.yaml b/unilabos/registry/devices/coin_cell_workstation.yaml index c8a671a7..2e9f6073 100644 --- a/unilabos/registry/devices/coin_cell_workstation.yaml +++ b/unilabos/registry/devices/coin_cell_workstation.yaml @@ -405,7 +405,7 @@ coincellassemblyworkstation_device: goal: properties: bottle_num: - type: integer + type: string required: - bottle_num type: object diff --git a/unilabos/registry/devices/opcua_example.yaml b/unilabos/registry/devices/opcua_example.yaml index 0f500cfa..a7e6b4e3 100644 --- a/unilabos/registry/devices/opcua_example.yaml +++ b/unilabos/registry/devices/opcua_example.yaml @@ -49,11 +49,10 @@ opcua_example: title: load_config参数 type: object type: UniLabJsonCommand - auto-post_init: + auto-refresh_node_values: feedback: {} goal: {} - goal_default: - ros_node: null + goal_default: {} handles: {} placeholder_keys: {} result: {} @@ -62,22 +61,21 @@ opcua_example: properties: feedback: {} goal: - properties: - ros_node: - type: string - required: - - ros_node + properties: {} + required: [] type: object result: {} required: - goal - title: post_init参数 + title: refresh_node_values参数 type: object type: UniLabJsonCommand - auto-print_cache_stats: + auto-set_node_value: feedback: {} goal: {} - goal_default: {} + goal_default: + name: null + value: null handles: {} placeholder_keys: {} result: {} @@ -86,20 +84,25 @@ opcua_example: properties: feedback: {} goal: - properties: {} - required: [] + properties: + name: + type: string + value: + type: string + required: + - name + - value type: object result: {} required: - goal - title: print_cache_stats参数 + title: set_node_value参数 type: object type: UniLabJsonCommand - auto-read_node: + auto-start_node_refresh: feedback: {} goal: {} - goal_default: - node_name: null + goal_default: {} handles: {} placeholder_keys: {} result: {} @@ -108,24 +111,19 @@ opcua_example: properties: feedback: {} goal: - properties: - node_name: - type: string - required: - - node_name + properties: {} + required: [] type: object result: {} required: - goal - title: read_node参数 + title: start_node_refresh参数 type: object type: UniLabJsonCommand - auto-set_node_value: + auto-stop_node_refresh: feedback: {} goal: {} - goal_default: - name: null - value: null + goal_default: {} handles: {} placeholder_keys: {} result: {} @@ -134,24 +132,17 @@ opcua_example: properties: feedback: {} goal: - properties: - name: - type: string - value: - type: string - required: - - name - - value + properties: {} + required: [] type: object result: {} required: - goal - title: set_node_value参数 + title: stop_node_refresh参数 type: object type: UniLabJsonCommand module: unilabos.device_comms.opcua_client.client:OpcUaClient status_types: - cache_stats: dict node_value: String type: python config_info: [] @@ -161,23 +152,15 @@ opcua_example: init_param_schema: config: properties: - cache_timeout: - default: 5.0 - type: number config_path: type: string - deck: - type: string password: type: string - subscription_interval: - default: 500 - type: integer + refresh_interval: + default: 1.0 + type: number url: type: string - use_subscription: - default: true - type: boolean username: type: string required: @@ -185,12 +168,9 @@ opcua_example: type: object data: properties: - cache_stats: - type: object node_value: type: string required: - node_value - - cache_stats type: object version: 1.0.0 diff --git a/unilabos/registry/devices/reaction_station_bioyond.yaml b/unilabos/registry/devices/reaction_station_bioyond.yaml index cf46d7ef..8b4622dc 100644 --- a/unilabos/registry/devices/reaction_station_bioyond.yaml +++ b/unilabos/registry/devices/reaction_station_bioyond.yaml @@ -58,6 +58,313 @@ reaction_station.bioyond: title: add_time_constraint参数 type: object type: UniLabJsonCommand + auto-clear_workflows: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: clear_workflows参数 + type: object + type: UniLabJsonCommand + auto-create_order: + feedback: {} + goal: {} + goal_default: + json_str: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + json_str: + type: string + required: + - json_str + type: object + result: {} + required: + - goal + title: create_order参数 + type: object + type: UniLabJsonCommand + auto-hard_delete_merged_workflows: + feedback: {} + goal: {} + goal_default: + workflow_ids: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + workflow_ids: + items: + type: string + type: array + required: + - workflow_ids + type: object + result: {} + required: + - goal + title: hard_delete_merged_workflows参数 + type: object + type: UniLabJsonCommand + auto-merge_workflow_with_parameters: + feedback: {} + goal: {} + goal_default: + json_str: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + json_str: + type: string + required: + - json_str + type: object + result: {} + required: + - goal + title: merge_workflow_with_parameters参数 + type: object + type: UniLabJsonCommand + auto-process_temperature_cutoff_report: + feedback: {} + goal: {} + goal_default: + report_request: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + report_request: + type: string + required: + - report_request + type: object + result: {} + required: + - goal + title: process_temperature_cutoff_report参数 + type: object + type: UniLabJsonCommand + auto-process_web_workflows: + feedback: {} + goal: {} + goal_default: + web_workflow_json: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + web_workflow_json: + type: string + required: + - web_workflow_json + type: object + result: {} + required: + - goal + title: process_web_workflows参数 + type: object + type: UniLabJsonCommand + auto-set_reactor_temperature: + feedback: {} + goal: {} + goal_default: + reactor_id: null + temperature: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + reactor_id: + type: integer + temperature: + type: number + required: + - reactor_id + - temperature + type: object + result: {} + required: + - goal + title: set_reactor_temperature参数 + type: object + type: UniLabJsonCommand + auto-skip_titration_steps: + feedback: {} + goal: {} + goal_default: + preintake_id: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + preintake_id: + type: string + required: + - preintake_id + type: object + result: {} + required: + - goal + title: skip_titration_steps参数 + type: object + type: UniLabJsonCommand + auto-sync_workflow_sequence_from_bioyond: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: sync_workflow_sequence_from_bioyond参数 + type: object + type: UniLabJsonCommand + auto-wait_for_multiple_orders_and_get_reports: + feedback: {} + goal: {} + goal_default: + batch_create_result: null + check_interval: 10 + timeout: 7200 + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + batch_create_result: + type: string + check_interval: + default: 10 + type: integer + timeout: + default: 7200 + type: integer + required: [] + type: object + result: {} + required: + - goal + title: wait_for_multiple_orders_and_get_reports参数 + type: object + type: UniLabJsonCommand + auto-workflow_sequence: + feedback: {} + goal: {} + goal_default: + value: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + value: + items: + type: string + type: array + required: + - value + type: object + result: {} + required: + - goal + title: workflow_sequence参数 + type: object + type: UniLabJsonCommand + auto-workflow_step_query: + feedback: {} + goal: {} + goal_default: + workflow_id: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + workflow_id: + type: string + required: + - workflow_id + type: object + result: {} + required: + - goal + title: workflow_step_query参数 + type: object + type: UniLabJsonCommand clean_all_server_workflows: feedback: {} goal: {} @@ -674,17 +981,7 @@ reaction_station.bioyond: module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactionStation protocol_type: [] status_types: - average_viscosity: Float64 - force: Float64 - in_temperature: Float64 - out_temperature: Float64 - pt100_temperature: Float64 - sensor_average_temperature: Float64 - setting_temperature: Float64 - speed: Float64 - target_temperature: Float64 - viscosity: Float64 - workflow_sequence: String + workflow_sequence: str type: python config_info: [] description: Bioyond反应站 @@ -704,9 +1001,7 @@ reaction_station.bioyond: data: properties: workflow_sequence: - items: - type: string - type: array + type: string required: - workflow_sequence type: object @@ -716,19 +1011,34 @@ reaction_station.reactor: - reactor - reaction_station_bioyond class: - action_value_mappings: {} + action_value_mappings: + auto-update_metrics: + feedback: {} + goal: {} + goal_default: + payload: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + payload: + type: object + required: + - payload + type: object + result: {} + required: + - goal + title: update_metrics参数 + type: object + type: UniLabJsonCommand module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactor - status_types: - average_viscosity: Float64 - force: Float64 - in_temperature: Float64 - out_temperature: Float64 - pt100_temperature: Float64 - sensor_average_temperature: Float64 - setting_temperature: Float64 - speed: Float64 - target_temperature: Float64 - viscosity: Float64 + status_types: {} type: python config_info: [] description: 反应站子设备-反应器 diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index 64aba3c8..f09b79c1 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -479,7 +479,11 @@ def _generate_status_types_schema(self, status_types: Dict[str, Any]) -> Dict[st return status_schema def _generate_unilab_json_command_schema( - self, method_args: List[Dict[str, Any]], method_name: str, return_annotation: Any = None + self, + method_args: List[Dict[str, Any]], + method_name: str, + return_annotation: Any = None, + previous_schema: Dict[str, Any] | None = None, ) -> Dict[str, Any]: """ 根据UniLabJsonCommand方法信息生成JSON Schema,暂不支持嵌套类型 @@ -488,6 +492,7 @@ def _generate_unilab_json_command_schema( method_args: 方法信息字典,包含args等 method_name: 方法名称 return_annotation: 返回类型注解,用于生成result schema(仅支持TypedDict) + previous_schema: 之前的 schema,用于保留 goal/feedback/result 下一级字段的 description Returns: JSON Schema格式的参数schema @@ -521,7 +526,7 @@ def _generate_unilab_json_command_schema( if return_annotation is not None and self._is_typed_dict(return_annotation): result_schema = self._generate_typed_dict_result_schema(return_annotation) - return { + final_schema = { "title": f"{method_name}参数", "description": f"", "type": "object", @@ -529,6 +534,39 @@ def _generate_unilab_json_command_schema( "required": ["goal"], } + # 保留之前 schema 中 goal/feedback/result 下一级字段的 description + if previous_schema: + self._preserve_field_descriptions(final_schema, previous_schema) + + return final_schema + + def _preserve_field_descriptions( + self, new_schema: Dict[str, Any], previous_schema: Dict[str, Any] + ) -> None: + """ + 保留之前 schema 中 goal/feedback/result 下一级字段的 description + + Args: + new_schema: 新生成的 schema(会被修改) + previous_schema: 之前的 schema + """ + for section in ["goal", "feedback", "result"]: + new_section = new_schema.get("properties", {}).get(section, {}) + prev_section = previous_schema.get("properties", {}).get(section, {}) + + if not new_section or not prev_section: + continue + + new_props = new_section.get("properties", {}) + prev_props = prev_section.get("properties", {}) + + for field_name, field_schema in new_props.items(): + if field_name in prev_props: + prev_field = prev_props[field_name] + # 保留字段的 description + if "description" in prev_field and prev_field["description"]: + field_schema["description"] = prev_field["description"] + def _is_typed_dict(self, annotation: Any) -> bool: """ 检查类型注解是否是TypedDict @@ -697,13 +735,10 @@ def load_device_types(self, path: os.PathLike, complete_registry: bool): sorted(device_config["class"]["status_types"].items()) ) if complete_registry: - # 保存原有的description信息 - old_descriptions = {} + # 保存原有的 action 配置(用于保留 schema 的 description 和 handles 等) + old_action_configs = {} for action_name, action_config in device_config["class"]["action_value_mappings"].items(): - if "description" in action_config.get("schema", {}): - description = action_config["schema"]["description"] - if len(description): - old_descriptions[action_name] = action_config["schema"]["description"] + old_action_configs[action_name] = action_config device_config["class"]["action_value_mappings"] = { k: v @@ -719,10 +754,15 @@ def load_device_types(self, path: os.PathLike, complete_registry: bool): "feedback": {}, "result": {}, "schema": self._generate_unilab_json_command_schema( - v["args"], k, v.get("return_annotation") + v["args"], + k, + v.get("return_annotation"), + # 传入旧的 schema 以保留字段 description + old_action_configs.get(f"auto-{k}", {}).get("schema"), ), "goal_default": {i["name"]: i["default"] for i in v["args"]}, - "handles": [], + # 保留原有的 handles 配置 + "handles": old_action_configs.get(f"auto-{k}", {}).get("handles", []), "placeholder_keys": { i["name"]: ( "unilabos_resources" @@ -746,12 +786,14 @@ def load_device_types(self, path: os.PathLike, complete_registry: bool): if k not in device_config["class"]["action_value_mappings"] } ) - # 恢复原有的description信息(auto开头的不修改) - for action_name, description in old_descriptions.items(): + # 恢复原有的 description 信息(非 auto- 开头的动作) + for action_name, old_config in old_action_configs.items(): if action_name in device_config["class"]["action_value_mappings"]: # 有一些会被删除 - device_config["class"]["action_value_mappings"][action_name]["schema"][ - "description" - ] = description + old_schema = old_config.get("schema", {}) + if "description" in old_schema and old_schema["description"]: + device_config["class"]["action_value_mappings"][action_name]["schema"][ + "description" + ] = old_schema["description"] device_config["init_param_schema"] = {} device_config["init_param_schema"]["config"] = self._generate_unilab_json_command_schema( enhanced_info["init_params"], "__init__" diff --git a/yibin_electrolyte_config_example.json b/unilabos/test/experiments/yibin_electrolyte_config_example.json similarity index 100% rename from yibin_electrolyte_config_example.json rename to unilabos/test/experiments/yibin_electrolyte_config_example.json diff --git a/unilabos/utils/log.py b/unilabos/utils/log.py index f10bd518..cee3269b 100644 --- a/unilabos/utils/log.py +++ b/unilabos/utils/log.py @@ -191,21 +191,6 @@ def configure_logger(loglevel=None, working_dir=None): # 添加处理器到根日志记录器 root_logger.addHandler(console_handler) - - # 降低第三方库的日志级别,避免过多输出 - # pymodbus 库的日志太详细,设置为 WARNING - logging.getLogger('pymodbus').setLevel(logging.WARNING) - logging.getLogger('pymodbus.logging').setLevel(logging.WARNING) - logging.getLogger('pymodbus.logging.base').setLevel(logging.WARNING) - logging.getLogger('pymodbus.logging.decoders').setLevel(logging.WARNING) - - # websockets 库的日志输出较多,设置为 WARNING - logging.getLogger('websockets').setLevel(logging.WARNING) - logging.getLogger('websockets.client').setLevel(logging.WARNING) - logging.getLogger('websockets.server').setLevel(logging.WARNING) - - # ROS 节点的状态更新日志过于频繁,设置为 INFO - logging.getLogger('unilabos.ros.nodes.presets.host_node').setLevel(logging.INFO) # 如果指定了工作目录,添加文件处理器 if working_dir is not None: