From c2dfe689aa623e2ac9f543f23cfe0ccadb1e33ae Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Sat, 19 Jul 2025 04:19:57 +0800 Subject: [PATCH 1/3] fix: Protocol node resource run (#65) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * stir和adjustph的中的bug修不好 * fix sub-resource query in protocol node compiling * add resource placeholder to vessels * add the rest yaml * Update work_station.yaml --------- Co-authored-by: KCFeng425 <2100011801@stu.pku.edu.cn> --- unilabos/compile/adjustph_protocol.py | 17 +++- unilabos/compile/evaporate_protocol.py | 18 ++-- unilabos/compile/recrystallize_protocol.py | 77 ++++++++++++++-- unilabos/compile/stir_protocol.py | 2 +- unilabos/compile/wash_solid_protocol.py | 2 +- .../devices/virtual/virtual_multiway_valve.py | 25 ++++- unilabos/devices/virtual/virtual_rotavap.py | 16 +++- unilabos/registry/devices/virtual_device.yaml | 84 ++--------------- unilabos/registry/devices/work_station.yaml | 92 ++++++++++++++++++- unilabos/resources/graphio.py | 2 +- unilabos/ros/nodes/presets/protocol_node.py | 65 +++++-------- 11 files changed, 252 insertions(+), 148 deletions(-) diff --git a/unilabos/compile/adjustph_protocol.py b/unilabos/compile/adjustph_protocol.py index 4d39e93..374268a 100644 --- a/unilabos/compile/adjustph_protocol.py +++ b/unilabos/compile/adjustph_protocol.py @@ -1,6 +1,6 @@ import networkx as nx import logging -from typing import List, Dict, Any +from typing import List, Dict, Any, Union from .pump_protocol import generate_pump_protocol_with_rinsing logger = logging.getLogger(__name__) @@ -216,7 +216,7 @@ def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume def generate_adjust_ph_protocol( G: nx.DiGraph, - vessel: dict, # 🔧 修改:从字符串改为字典类型 + vessel:Union[dict,str], # 🔧 修改:从字符串改为字典类型 ph_value: float, reagent: str, **kwargs @@ -235,8 +235,17 @@ def generate_adjust_ph_protocol( List[Dict[str, Any]]: 动作序列 """ - # 🔧 核心修改:从字典中提取容器ID - vessel_id = vessel["id"] + # 统一处理vessel参数 + if isinstance(vessel, dict): + vessel_id = list(vessel.values())[0].get("id", "") + vessel_data = vessel.get("data", {}) + else: + vessel_id = str(vessel) + vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {} + + if not vessel_id: + debug_print(f"❌ vessel 参数无效,必须包含id字段或直接提供容器ID. vessel: {vessel}") + raise ValueError("vessel 参数无效,必须包含id字段或直接提供容器ID") debug_print("=" * 60) debug_print("🧪 开始生成pH调节协议") diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py index 7f93192..56228c2 100644 --- a/unilabos/compile/evaporate_protocol.py +++ b/unilabos/compile/evaporate_protocol.py @@ -22,7 +22,7 @@ def parse_time_input(time_input: Union[str, float]) -> float: """ if isinstance(time_input, (int, float)): debug_print(f"⏱️ 时间输入为数字: {time_input}s ✨") - return float(time_input) + return float(time_input) # 🔧 确保返回float if not time_input or not str(time_input).strip(): debug_print(f"⚠️ 时间输入为空,使用默认值: 180s (3分钟) 🕐") @@ -48,7 +48,7 @@ def parse_time_input(time_input: Union[str, float]) -> float: try: value = float(time_str) debug_print(f"✅ 时间解析成功: {time_str} → {value}s(无单位,默认秒)⏰") - return value + return float(value) # 🔧 确保返回float except ValueError: debug_print(f"❌ 无法解析时间: '{time_str}',使用默认值180s (3分钟) 😅") return 180.0 @@ -70,7 +70,7 @@ def parse_time_input(time_input: Union[str, float]) -> float: time_sec = value # 已经是s debug_print(f"🕐 时间转换: {value}s → {time_sec}s (已是秒) ⏰") - return time_sec + return float(time_sec) # 🔧 确保返回float def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]: """ @@ -389,12 +389,12 @@ def generate_evaporate_protocol( "device_id": rotavap_device, "action_name": "evaporate", "action_kwargs": { - "vessel": target_vessel, # 使用 target_vessel - "pressure": pressure, - "temp": temp, - "time": final_time, - "stir_speed": stir_speed, - "solvent": solvent + "vessel": target_vessel, + "pressure": float(pressure), + "temp": float(temp), + "time": float(final_time), # 🔧 强制转换为float类型 + "stir_speed": float(stir_speed), + "solvent": str(solvent) } } action_sequence.append(evaporate_action) diff --git a/unilabos/compile/recrystallize_protocol.py b/unilabos/compile/recrystallize_protocol.py index b6f4aff..7c15310 100644 --- a/unilabos/compile/recrystallize_protocol.py +++ b/unilabos/compile/recrystallize_protocol.py @@ -170,33 +170,94 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name} ✨") return vessel_name - # 第二步:通过模糊匹配 + # 第二步:通过模糊匹配(节点ID和名称) debug_print(" 🔍 步骤2: 模糊名称匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': node_name = G.nodes[node_id].get('name', '').lower() if solvent.lower() in node_id.lower() or solvent.lower() in node_name: - debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} ✨") + debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} (名称: {node_name}) ✨") return node_id - # 第三步:通过液体类型匹配 - debug_print(" 🧪 步骤3: 液体类型匹配...") + # 第三步:通过配置中的试剂信息匹配 + debug_print(" 🧪 步骤3: 配置试剂信息匹配...") + for node_id in G.nodes(): + if G.nodes[node_id].get('type') == 'container': + # 检查 config 中的 reagent 字段 + node_config = G.nodes[node_id].get('config', {}) + config_reagent = node_config.get('reagent', '').lower() + + if config_reagent and solvent.lower() == config_reagent: + debug_print(f" 🎉 通过config.reagent匹配找到容器: {node_id} (试剂: {config_reagent}) ✨") + return node_id + + # 第四步:通过数据中的试剂信息匹配 + debug_print(" 🧪 步骤4: 数据试剂信息匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': vessel_data = G.nodes[node_id].get('data', {}) - liquids = vessel_data.get('liquid', []) + # 检查 data 中的 reagent_name 字段 + reagent_name = vessel_data.get('reagent_name', '').lower() + if reagent_name and solvent.lower() == reagent_name: + debug_print(f" 🎉 通过data.reagent_name匹配找到容器: {node_id} (试剂: {reagent_name}) ✨") + return node_id + + # 检查 data 中的液体信息 + liquids = vessel_data.get('liquid', []) for liquid in liquids: if isinstance(liquid, dict): liquid_type = (liquid.get('liquid_type') or liquid.get('name', '')).lower() - reagent_name = vessel_data.get('reagent_name', '').lower() - if solvent.lower() in liquid_type or solvent.lower() in reagent_name: - debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} ✨") + if solvent.lower() in liquid_type: + debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} (液体类型: {liquid_type}) ✨") return node_id + # 第五步:部分匹配(如果前面都没找到) + debug_print(" 🔍 步骤5: 部分匹配...") + for node_id in G.nodes(): + if G.nodes[node_id].get('type') == 'container': + node_config = G.nodes[node_id].get('config', {}) + node_data = G.nodes[node_id].get('data', {}) + node_name = G.nodes[node_id].get('name', '').lower() + + config_reagent = node_config.get('reagent', '').lower() + data_reagent = node_data.get('reagent_name', '').lower() + + # 检查是否包含溶剂名称 + if (solvent.lower() in config_reagent or + solvent.lower() in data_reagent or + solvent.lower() in node_name or + solvent.lower() in node_id.lower()): + debug_print(f" 🎉 通过部分匹配找到容器: {node_id} ✨") + debug_print(f" - 节点名称: {node_name}") + debug_print(f" - 配置试剂: {config_reagent}") + debug_print(f" - 数据试剂: {data_reagent}") + return node_id + + # 调试信息:列出所有容器 + debug_print(" 🔎 调试信息:列出所有容器...") + container_list = [] + for node_id in G.nodes(): + if G.nodes[node_id].get('type') == 'container': + node_config = G.nodes[node_id].get('config', {}) + node_data = G.nodes[node_id].get('data', {}) + node_name = G.nodes[node_id].get('name', '') + + container_info = { + 'id': node_id, + 'name': node_name, + 'config_reagent': node_config.get('reagent', ''), + 'data_reagent': node_data.get('reagent_name', '') + } + container_list.append(container_info) + debug_print(f" - 容器: {node_id}, 名称: {node_name}, config试剂: {node_config.get('reagent', '')}, data试剂: {node_data.get('reagent_name', '')}") + debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭") + debug_print(f"🔍 查找的溶剂: '{solvent}' (小写: '{solvent.lower()}')") + debug_print(f"📊 总共发现 {len(container_list)} 个容器") + raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器") diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py index 70405ff..e13c1f8 100644 --- a/unilabos/compile/stir_protocol.py +++ b/unilabos/compile/stir_protocol.py @@ -149,7 +149,7 @@ def extract_vessel_id(vessel: Union[str, dict]) -> str: str: vessel_id """ if isinstance(vessel, dict): - vessel_id = vessel.get("id", "") + vessel_id = list(vessel.values())[0].get("id", "") debug_print(f"🔧 从vessel字典提取ID: {vessel_id}") return vessel_id elif isinstance(vessel, str): diff --git a/unilabos/compile/wash_solid_protocol.py b/unilabos/compile/wash_solid_protocol.py index e19d768..b167c85 100644 --- a/unilabos/compile/wash_solid_protocol.py +++ b/unilabos/compile/wash_solid_protocol.py @@ -165,7 +165,7 @@ def extract_vessel_id(vessel: Union[str, dict]) -> str: str: vessel_id """ if isinstance(vessel, dict): - vessel_id = vessel.get("id", "") + vessel_id = list(vessel.values())[0].get("id", "") debug_print(f"🔧 从vessel字典提取ID: {vessel_id}") return vessel_id elif isinstance(vessel, str): diff --git a/unilabos/devices/virtual/virtual_multiway_valve.py b/unilabos/devices/virtual/virtual_multiway_valve.py index 468175c..3cd68a1 100644 --- a/unilabos/devices/virtual/virtual_multiway_valve.py +++ b/unilabos/devices/virtual/virtual_multiway_valve.py @@ -70,11 +70,32 @@ class VirtualMultiwayValve: command: 目标位置 (0-8) 或位置字符串 0: transfer pump位置 1-8: 其他设备位置 + 'default': 默认位置(0号位) """ try: - # 如果是字符串形式的位置,先转换为数字 + # 🔧 处理特殊字符串命令 if isinstance(command, str): - pos = int(command) + command_lower = command.lower().strip() + + # 处理特殊命令 + if command_lower in ['default', 'pump', 'transfer_pump', 'home']: + pos = 0 # 默认位置为0号位(transfer pump) + self.logger.info(f"🔧 特殊命令 '{command}' 映射到位置 {pos}") + elif command_lower in ['open']: + pos = 0 # open命令也映射到0号位 + self.logger.info(f"🔧 OPEN命令映射到位置 {pos}") + elif command_lower in ['close', 'closed']: + # 关闭命令保持当前位置 + pos = self._current_position + self.logger.info(f"🔧 CLOSE命令保持当前位置 {pos}") + else: + # 尝试转换为数字 + try: + pos = int(command) + except ValueError: + error_msg = f"无法识别的命令: '{command}'" + self.logger.error(f"❌ {error_msg}") + raise ValueError(error_msg) else: pos = int(command) diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py index dd1cca4..61e66a3 100644 --- a/unilabos/devices/virtual/virtual_rotavap.py +++ b/unilabos/devices/virtual/virtual_rotavap.py @@ -88,6 +88,20 @@ class VirtualRotavap: ) -> bool: """Execute evaporate action - 简化版 🌪️""" + # 🔧 新增:确保time参数是数值类型 + if isinstance(time, str): + try: + time = float(time) + except ValueError: + self.logger.error(f"❌ 无法转换时间参数 '{time}' 为数值,使用默认值180.0秒") + time = 180.0 + elif not isinstance(time, (int, float)): + self.logger.error(f"❌ 时间参数类型无效: {type(time)},使用默认值180.0秒") + time = 180.0 + + # 确保time是float类型 + time = float(time) + # 🔧 简化处理:如果vessel就是设备自己,直接操作 if vessel == self.device_id: debug_print(f"🎯 在设备 {self.device_id} 上直接执行蒸发操作") @@ -158,7 +172,7 @@ class VirtualRotavap: }) return False - # 开始蒸发 + # 开始蒸发 - 🔧 现在time已经确保是float类型 self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️") self.data.update({ diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index 572456a..dca2dad 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -4580,7 +4580,6 @@ virtual_solid_dispenser: feedback: {} goal: properties: {} - required: [] type: object result: {} required: @@ -4588,30 +4587,6 @@ virtual_solid_dispenser: title: cleanup参数 type: object type: UniLabJsonCommandAsync - auto-find_solid_reagent_bottle: - feedback: {} - goal: {} - goal_default: - reagent_name: null - handles: [] - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - reagent_name: - type: string - required: - - reagent_name - type: object - result: {} - required: - - goal - title: find_solid_reagent_bottle参数 - type: object - type: UniLabJsonCommand auto-initialize: feedback: {} goal: {} @@ -4624,7 +4599,6 @@ virtual_solid_dispenser: feedback: {} goal: properties: {} - required: [] type: object result: {} required: @@ -4632,58 +4606,9 @@ virtual_solid_dispenser: title: initialize参数 type: object type: UniLabJsonCommandAsync - auto-parse_mass_string: - feedback: {} - goal: {} - goal_default: - mass_str: null - handles: [] - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - mass_str: - type: string - required: - - mass_str - type: object - result: {} - required: - - goal - title: parse_mass_string参数 - type: object - type: UniLabJsonCommand - auto-parse_mol_string: - feedback: {} - goal: {} - goal_default: - mol_str: null - handles: [] - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - mol_str: - type: string - required: - - mol_str - type: object - result: {} - required: - - goal - title: parse_mol_string参数 - type: object - type: UniLabJsonCommand module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser status_types: current_reagent: str - device_info: dict dispensed_amount: float status: str total_operations: int @@ -4716,14 +4641,18 @@ virtual_solid_dispenser: type: object device_id: type: string + max_capacity: + default: 100.0 + type: number + precision: + default: 0.001 + type: number required: [] type: object data: properties: current_reagent: type: string - device_info: - type: object dispensed_amount: type: number status: @@ -4735,7 +4664,6 @@ virtual_solid_dispenser: - current_reagent - dispensed_amount - total_operations - - device_info type: object version: 1.0.0 virtual_stirrer: diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index 7e861f5..3d906dc 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -245,8 +245,13 @@ workstation: feedback: {} goal: amount: amount + equiv: equiv + event: event mass: mass + mol: mol purpose: purpose + rate_spec: rate_spec + ratio: ratio reagent: reagent stir: stir stir_speed: stir_speed @@ -470,6 +475,11 @@ workstation: ph_value: ph_value reagent: reagent vessel: vessel + volume: volume + stir: stir + stir_speed: stir_speed + stir_time: stir_time + settling_time: settling_time goal_default: ph_value: 0.0 reagent: '' @@ -493,6 +503,11 @@ workstation: z: 0.0 sample_id: '' type: '' + volume: 0.0 + stir: false + stir_speed: 300.0 + stir_time: 60.0 + settling_time: 30.0 handles: input: - data_key: vessel @@ -511,6 +526,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -605,6 +622,21 @@ workstation: - data title: Resource type: object + volume: + type: number + description: 'Volume of the solution to adjust pH' + stir: + type: boolean + description: "是否启用搅拌" + stir_speed: + type: number + description: "搅拌速度(RPM)" + stir_time: + type: number + description: "搅拌时间(秒)" + settling_time: + type: number + description: "pH平衡时间(秒)" required: - vessel - ph_value @@ -674,6 +706,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -853,6 +887,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -1060,6 +1096,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -1191,6 +1229,10 @@ workstation: feedback: {} goal: amount: amount + event: event + mass: mass + mol: mol + reagent: reagent solvent: solvent stir_speed: stir_speed temp: temp @@ -1246,6 +1288,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -1429,6 +1473,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -1585,6 +1631,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -1778,6 +1826,8 @@ workstation: data_type: resource handler_key: vessel_out label: Evaporation Vessel + placeholder_keys: + vessel: unilabos_nodes result: {} schema: description: '' @@ -2014,6 +2064,9 @@ workstation: data_type: resource handler_key: filtrate_out label: Filtrate Vessel + placeholder_keys: + filtrate_vessel: unilabos_resources + vessel: unilabos_nodes result: {} schema: description: '' @@ -2195,7 +2248,7 @@ workstation: type: number required: - vessel - - filtrate_vessel + - #filtrate_vessel - stir - stir_speed - temp @@ -2325,6 +2378,9 @@ workstation: data_type: resource handler_key: ToVesselOut label: To Vessel + placeholder_keys: + from_vessel: unilabos_resources + to_vessel: unilabos_resources result: {} schema: description: '' @@ -2656,6 +2712,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -2835,6 +2893,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -2986,6 +3046,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -3135,6 +3197,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -3354,6 +3418,9 @@ workstation: data_type: resource handler_key: ToVesselOut label: To Vessel + placeholder_keys: + from_vessel: unilabos_nodes + to_vessel: unilabos_nodes result: {} schema: description: '' @@ -3667,6 +3734,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -4019,6 +4088,10 @@ workstation: data_type: resource handler_key: ToVesselOut label: To Vessel + placeholder_keys: + column: unilabos_devices + from_vessel: unilabos_resources + to_vessel: unilabos_resources result: {} schema: description: '' @@ -4422,6 +4495,11 @@ workstation: data_type: resource handler_key: ToVesselOut label: To Vessel + placeholder_keys: + from_vessel: unilabos_resources + to_vessel: unilabos_resources + waste_phase_to_vessel: unilabos_resources + waste_vessel: unilabos_resources result: {} schema: description: '' @@ -5053,6 +5131,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -5225,6 +5305,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -5391,6 +5473,8 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel + placeholder_keys: + vessel: unilabos_resources result: {} schema: description: '' @@ -5556,6 +5640,9 @@ workstation: data_type: resource handler_key: ToVesselOut label: To Vessel + placeholder_keys: + from_vessel: unilabos_nodes + to_vessel: unilabos_nodes result: {} schema: description: '' @@ -5722,6 +5809,9 @@ workstation: data_type: resource handler_key: filtrate_vessel_out label: Filtrate Vessel + placeholder_keys: + filtrate_vessel: unilabos_resources + vessel: unilabos_resources result: {} schema: description: '' diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index a392438..3e3ece3 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -248,7 +248,7 @@ def dict_to_nested_dict(nodes: dict, devices_only: bool = False) -> dict: root_nodes = { node["id"]: node for node in nodes_list - if node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan] + if node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan] or len(nodes_list) == 1 } # 如果存在多个根节点,返回所有根节点 diff --git a/unilabos/ros/nodes/presets/protocol_node.py b/unilabos/ros/nodes/presets/protocol_node.py index a17d4e9..72e3d65 100644 --- a/unilabos/ros/nodes/presets/protocol_node.py +++ b/unilabos/ros/nodes/presets/protocol_node.py @@ -189,45 +189,26 @@ class ROS2ProtocolNode(BaseROS2DeviceNode): # # 🔧 完全禁用Host查询,直接使用转换后的数据 # print(f"🔧 跳过Host查询,直接使用转换后的数据") - - # 🔧 额外验证:确保vessel数据完整 - if 'vessel' in protocol_kwargs: - vessel_data = protocol_kwargs['vessel'] - #print(f"🔍 验证vessel数据: {vessel_data}") - - # 如果vessel是空字典,尝试重新构建 - if not vessel_data or (isinstance(vessel_data, dict) and not vessel_data): - # print(f"⚠️ vessel数据为空,尝试从原始goal重新提取...") - - # 直接从原始goal提取vessel - if hasattr(goal, 'vessel') and goal.vessel: - # print(f"🔍 原始goal.vessel: {goal.vessel}") - # 手动转换vessel - vessel_data = { - 'id': goal.vessel.id, - 'name': goal.vessel.name, - 'type': goal.vessel.type, - 'category': goal.vessel.category, - 'config': goal.vessel.config, - 'data': goal.vessel.data - } - protocol_kwargs['vessel'] = vessel_data - # print(f"✅ 手动重建vessel数据: {vessel_data}") - else: - # print(f"❌ 无法从原始goal提取vessel数据") - # 创建一个基本的vessel - vessel_data = {'id': 'default_vessel'} - protocol_kwargs['vessel'] = vessel_data - # print(f"🔧 创建默认vessel: {vessel_data}") + # 向Host查询物料当前状态 + for k, v in goal.get_fields_and_field_types().items(): + if v in ["unilabos_msgs/Resource", "sequence"]: + r = ResourceGet.Request() + resource_id = ( + protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"] + ) + r.id = resource_id + r.with_children = True + response = await self._resource_clients["resource_get"].call_async(r) + protocol_kwargs[k] = list_to_nested_dict( + [convert_from_ros_msg(rs) for rs in response.resources] + ) - #print(f"🔍 最终传递给协议的 protocol_kwargs: {protocol_kwargs}") - #print(f"🔍 最终的 vessel: {protocol_kwargs.get('vessel', 'NOT_FOUND')}") + self.lab_logger().info(f"🔍 最终传递给协议的 protocol_kwargs: {protocol_kwargs}") + self.lab_logger().info(f"🔍 最终的 vessel: {protocol_kwargs.get('vessel', 'NOT_FOUND')}") from unilabos.resources.graphio import physical_setup_graph self.lab_logger().info(f"Working on physical setup: {physical_setup_graph}") - self.lab_logger().info(f"Protocol kwargs: {goal}") - self.lab_logger().info(f"Protocol kwargs: {action_value_mapping}") protocol_steps = protocol_steps_generator(G=physical_setup_graph, **protocol_kwargs) self.lab_logger().info(f"Goal received: {protocol_kwargs}, running steps: \n{protocol_steps}") @@ -263,14 +244,14 @@ class ROS2ProtocolNode(BaseROS2DeviceNode): } ) - # # 向Host更新物料当前状态 - # for k, v in goal.get_fields_and_field_types().items(): - # if v in ["unilabos_msgs/Resource", "sequence"]: - # r = ResourceUpdate.Request() - # r.resources = [ - # convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k]) - # ] - # response = await self._resource_clients["resource_update"].call_async(r) + # 向Host更新物料当前状态 + for k, v in goal.get_fields_and_field_types().items(): + if v in ["unilabos_msgs/Resource", "sequence"]: + r = ResourceUpdate.Request() + r.resources = [ + convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k]) + ] + response = await self._resource_clients["resource_update"].call_async(r) # 设置成功状态和返回值 execution_success = True From fd18b211478a103c9359c59d0dc3818c2776c57f Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Sat, 19 Jul 2025 10:38:58 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E9=87=87=E7=94=A8http=E6=8A=A5=E9=80=81res?= =?UTF-8?q?ource?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unilabos/app/register.py | 24 ++++-- unilabos/app/web/client.py | 23 ++++- unilabos/registry/devices/virtual_device.yaml | 84 +++++++++++++++++-- unilabos/registry/devices/work_station.yaml | 28 +------ unilabos/registry/registry.py | 2 + 5 files changed, 123 insertions(+), 38 deletions(-) diff --git a/unilabos/app/register.py b/unilabos/app/register.py index 40e4107..7a78cf9 100644 --- a/unilabos/app/register.py +++ b/unilabos/app/register.py @@ -18,10 +18,22 @@ def register_devices_and_resources(mqtt_client, lab_registry): mqtt_client.publish_registry(device_info["id"], device_info, False) logger.debug(f"[UniLab Register] 注册设备: {device_info['id']}") - # 注册资源信息 + # 注册资源信息 - 使用HTTP方式 + from unilabos.app.web.client import http_client + + resources_to_register = {} for resource_info in lab_registry.obtain_registry_resource_info(): - mqtt_client.publish_registry(resource_info["id"], resource_info, False) - logger.debug(f"[UniLab Register] 注册资源: {resource_info['id']}") + resources_to_register[resource_info["id"]] = resource_info + logger.debug(f"[UniLab Register] 准备注册资源: {resource_info['id']}") + + if resources_to_register: + start_time = time.time() + response = http_client.resource_registry(resources_to_register) + cost_time = time.time() - start_time + if response.status_code in [200, 201]: + logger.info(f"[UniLab Register] 成功通过HTTP注册 {len(resources_to_register)} 个资源 {cost_time}ms") + else: + logger.error(f"[UniLab Register] HTTP注册资源失败: {response.status_code}, {response.text} {cost_time}ms") time.sleep(10) @@ -53,11 +65,9 @@ def main(): help="是否补全注册表", ) args = parser.parse_args() - + load_config_from_file(args.config) # 构建注册表 build_registry(args.registry, args.complete_registry) - load_config_from_file(args.config) - from unilabos.app.mq import mqtt_client # 连接mqtt @@ -70,4 +80,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index 09b6ede..923e904 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -40,8 +40,9 @@ class HTTPClient: Returns: Response: API响应对象 """ + database_param = 1 if database_process_later else 0 response = requests.post( - f"{self.remote_addr}/lab/resource/edge/batch_create/?database_process_later={1 if database_process_later else 0}", + f"{self.remote_addr}/lab/resource/edge/batch_create/?database_process_later={database_param}", json=resources, headers={"Authorization": f"lab {self.auth}"}, timeout=100, @@ -149,6 +150,26 @@ class HTTPClient: ) return response + def resource_registry(self, registry_data: Dict[str, Any]) -> requests.Response: + """ + 注册资源到服务器 + + Args: + registry_data: 注册表数据,格式为 {resource_id: resource_info} + + Returns: + Response: API响应对象 + """ + response = requests.post( + f"{self.remote_addr}/lab/registry/", + json=registry_data, + headers={"Authorization": f"lab {self.auth}"}, + timeout=30, + ) + if response.status_code not in [200, 201]: + logger.error(f"注册资源失败: {response.status_code}, {response.text}") + return response + # 创建默认客户端实例 http_client = HTTPClient() diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index dca2dad..572456a 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -4580,6 +4580,7 @@ virtual_solid_dispenser: feedback: {} goal: properties: {} + required: [] type: object result: {} required: @@ -4587,6 +4588,30 @@ virtual_solid_dispenser: title: cleanup参数 type: object type: UniLabJsonCommandAsync + auto-find_solid_reagent_bottle: + feedback: {} + goal: {} + goal_default: + reagent_name: null + handles: [] + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + reagent_name: + type: string + required: + - reagent_name + type: object + result: {} + required: + - goal + title: find_solid_reagent_bottle参数 + type: object + type: UniLabJsonCommand auto-initialize: feedback: {} goal: {} @@ -4599,6 +4624,7 @@ virtual_solid_dispenser: feedback: {} goal: properties: {} + required: [] type: object result: {} required: @@ -4606,9 +4632,58 @@ virtual_solid_dispenser: title: initialize参数 type: object type: UniLabJsonCommandAsync + auto-parse_mass_string: + feedback: {} + goal: {} + goal_default: + mass_str: null + handles: [] + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + mass_str: + type: string + required: + - mass_str + type: object + result: {} + required: + - goal + title: parse_mass_string参数 + type: object + type: UniLabJsonCommand + auto-parse_mol_string: + feedback: {} + goal: {} + goal_default: + mol_str: null + handles: [] + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + mol_str: + type: string + required: + - mol_str + type: object + result: {} + required: + - goal + title: parse_mol_string参数 + type: object + type: UniLabJsonCommand module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser status_types: current_reagent: str + device_info: dict dispensed_amount: float status: str total_operations: int @@ -4641,18 +4716,14 @@ virtual_solid_dispenser: type: object device_id: type: string - max_capacity: - default: 100.0 - type: number - precision: - default: 0.001 - type: number required: [] type: object data: properties: current_reagent: type: string + device_info: + type: object dispensed_amount: type: number status: @@ -4664,6 +4735,7 @@ virtual_solid_dispenser: - current_reagent - dispensed_amount - total_operations + - device_info type: object version: 1.0.0 virtual_stirrer: diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index 3d906dc..c1cc5aa 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -474,12 +474,12 @@ workstation: goal: ph_value: ph_value reagent: reagent - vessel: vessel - volume: volume + settling_time: settling_time stir: stir stir_speed: stir_speed stir_time: stir_time - settling_time: settling_time + vessel: vessel + volume: volume goal_default: ph_value: 0.0 reagent: '' @@ -503,11 +503,6 @@ workstation: z: 0.0 sample_id: '' type: '' - volume: 0.0 - stir: false - stir_speed: 300.0 - stir_time: 60.0 - settling_time: 30.0 handles: input: - data_key: vessel @@ -622,21 +617,6 @@ workstation: - data title: Resource type: object - volume: - type: number - description: 'Volume of the solution to adjust pH' - stir: - type: boolean - description: "是否启用搅拌" - stir_speed: - type: number - description: "搅拌速度(RPM)" - stir_time: - type: number - description: "搅拌时间(秒)" - settling_time: - type: number - description: "pH平衡时间(秒)" required: - vessel - ph_value @@ -2248,7 +2228,7 @@ workstation: type: number required: - vessel - - #filtrate_vessel + - filtrate_vessel - stir - stir_speed - temp diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index a7583e3..780ba90 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -127,6 +127,8 @@ class Registry: }, }, "version": "1.0.0", + "category": [], + "config_info": [], "icon": "icon_device.webp", "registry_type": "device", "handles": [], From 3c9cca88ea501323c86a6ae327bc2204914d88ab Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Sat, 19 Jul 2025 11:09:24 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E9=87=87=E7=94=A8http=E6=8A=A5=E9=80=81res?= =?UTF-8?q?ource?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- README.md | 2 +- README_zh.md | 2 +- recipes/ros-humble-unilabos-msgs/recipe.yaml | 2 +- recipes/unilabos/recipe.yaml | 2 +- setup.py | 2 +- test/experiments/prcxi.json | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 5591f09..51d4de2 100644 --- a/.gitignore +++ b/.gitignore @@ -241,4 +241,5 @@ unilabos/device_mesh/view_robot.rviz # Certs **/.certs local_test2.py -ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2 +*.tar.bz2 +*.tar.gz diff --git a/README.md b/README.md index ab6320c..011db12 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name # Currently, you need to install the `unilabos_msgs` package # You can download the system-specific package from the Release page -conda install ros-humble-unilabos-msgs-0.9.13-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.14-xxxxx.tar.bz2 # Install PyLabRobot and other prerequisites git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/README_zh.md b/README_zh.md index 50084af..91e9b8d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名 # 现阶段,需要安装 `unilabos_msgs` 包 # 可以前往 Release 页面下载系统对应的包进行安装 -conda install ros-humble-unilabos-msgs-0.9.13-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.14-xxxxx.tar.bz2 # 安装PyLabRobot等前置 git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/recipes/ros-humble-unilabos-msgs/recipe.yaml b/recipes/ros-humble-unilabos-msgs/recipe.yaml index 89738d0..9f4d631 100644 --- a/recipes/ros-humble-unilabos-msgs/recipe.yaml +++ b/recipes/ros-humble-unilabos-msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.9.13 + version: 0.9.14 source: path: ../../unilabos_msgs folder: ros-humble-unilabos-msgs/src/work diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index 2463cc5..1a1b400 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.9.13" + version: "0.9.14" source: path: ../.. diff --git a/setup.py b/setup.py index 7a0f97f..97e21ef 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ package_name = 'unilabos' setup( name=package_name, - version='0.9.13', + version='0.9.14', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/test/experiments/prcxi.json b/test/experiments/prcxi.json index d72768f..a6c93a3 100644 --- a/test/experiments/prcxi.json +++ b/test/experiments/prcxi.json @@ -21,7 +21,7 @@ "timeout": 10.0, "axis": "Right", "channel_num": 1, - "setup": false, + "setup": true, "debug": false, "simulator": false, "matrix_id": "fd383e6d-2d0e-40b5-9c01-1b2870b1f1b1"