diff --git a/unilabos/app/register.py b/unilabos/app/register.py index 633df98..5918b43 100644 --- a/unilabos/app/register.py +++ b/unilabos/app/register.py @@ -38,9 +38,9 @@ def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[ response = http_client.resource_registry({"resources": list(devices_to_register.values())}) cost_time = time.time() - start_time if response.status_code in [200, 201]: - logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}ms") + logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}s") else: - logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}ms") + logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}s") except Exception as e: logger.error(f"[UniLab Register] 设备注册异常: {e}") @@ -51,9 +51,9 @@ def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[ response = http_client.resource_registry({"resources": list(resources_to_register.values())}) cost_time = time.time() - start_time if response.status_code in [200, 201]: - logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}ms") + logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}s") else: - logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}ms") + logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}s") except Exception as e: logger.error(f"[UniLab Register] 资源注册异常: {e}") diff --git a/unilabos/registry/device_comms/communication_devices.yaml b/unilabos/registry/device_comms/communication_devices.yaml index ea3f1b6..782889d 100644 --- a/unilabos/registry/device_comms/communication_devices.yaml +++ b/unilabos/registry/device_comms/communication_devices.yaml @@ -96,10 +96,13 @@ serial: type: string port: type: string + registry_name: + type: string resource_tracker: type: object required: - device_id + - registry_name - port type: object data: diff --git a/unilabos/registry/devices/camera.yaml b/unilabos/registry/devices/camera.yaml index fe1aef2..c8b9d94 100644 --- a/unilabos/registry/devices/camera.yaml +++ b/unilabos/registry/devices/camera.yaml @@ -67,6 +67,9 @@ camera: period: default: 0.1 type: number + registry_name: + default: '' + type: string resource_tracker: type: object required: [] diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index df4758d..3d2c83e 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -5,6 +5,7 @@ import sys import inspect import importlib import threading +import traceback from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path from typing import Any, Dict, List, Union, Tuple @@ -944,6 +945,7 @@ class Registry: if is_valid: results.append((file, data, device_ids)) except Exception as e: + traceback.print_exc() logger.warning(f"[UniLab Registry] 处理设备文件异常: {file}, 错误: {e}") # 线程安全地更新注册表 diff --git a/unilabos/workflow/common.py b/unilabos/workflow/common.py index 381cc66..3a1fee2 100644 --- a/unilabos/workflow/common.py +++ b/unilabos/workflow/common.py @@ -362,14 +362,16 @@ def build_protocol_graph( protocol_steps: List[Dict[str, Any]], workstation_name: str, action_resource_mapping: Optional[Dict[str, str]] = None, + labware_defs: Optional[List[Dict[str, Any]]] = None, ) -> WorkflowGraph: """统一的协议图构建函数,根据设备类型自动选择构建逻辑 Args: - labware_info: labware 信息字典,格式为 {name: {slot, well, labware, ...}, ...} + labware_info: reagent 信息字典,格式为 {name: {slot, well}, ...},用于 set_liquid 和 well 查找 protocol_steps: 协议步骤列表 workstation_name: 工作站名称 action_resource_mapping: action 到 resource_name 的映射字典,可选 + labware_defs: labware 定义列表,格式为 [{"name": "...", "slot": "1", "type": "lab_xxx"}, ...] """ G = WorkflowGraph() resource_last_writer = {} # reagent_name -> "node_id:port" @@ -377,18 +379,7 @@ def build_protocol_graph( protocol_steps = refactor_data(protocol_steps, action_resource_mapping) - # ==================== 第一步:按 slot 去重创建 create_resource 节点 ==================== - # 收集所有唯一的 slot - slots_info = {} # slot -> {labware, res_id} - for labware_id, item in labware_info.items(): - slot = str(item.get("slot", "")) - if slot and slot not in slots_info: - res_id = f"plate_slot_{slot}" - slots_info[slot] = { - "labware": item.get("labware", ""), - "res_id": res_id, - } - + # ==================== 第一步:按 slot 创建 create_resource 节点 ==================== # 创建 Group 节点,包含所有 create_resource 节点 group_node_id = str(uuid.uuid4()) G.add_node( @@ -404,29 +395,35 @@ def build_protocol_graph( param=None, ) - # 为每个唯一的 slot 创建 create_resource 节点 + # 直接使用 JSON 中的 labware 定义,每个 slot 一条记录,type 即 class_name res_index = 0 - for slot, info in slots_info.items(): - node_id = str(uuid.uuid4()) - res_id = info["res_id"] + for lw in (labware_defs or []): + slot = str(lw.get("slot", "")) + if not slot or slot in slot_to_create_resource: + continue # 跳过空 slot 或已处理的 slot + + lw_name = lw.get("name", f"slot {slot}") + lw_type = lw.get("type", CREATE_RESOURCE_DEFAULTS["class_name"]) + res_id = f"plate_slot_{slot}" res_index += 1 + node_id = str(uuid.uuid4()) G.add_node( node_id, template_name="create_resource", resource_name="host_node", - name=f"Plate {res_index}", - description=f"Create plate on slot {slot}", + name=lw_name, + description=f"Create {lw_name}", lab_node_type="Labware", footer="create_resource-host_node", device_name=DEVICE_NAME_HOST, type=NODE_TYPE_DEFAULT, - parent_uuid=group_node_id, # 指向 Group 节点 - minimized=True, # 折叠显示 + parent_uuid=group_node_id, + minimized=True, param={ "res_id": res_id, "device_id": CREATE_RESOURCE_DEFAULTS["device_id"], - "class_name": CREATE_RESOURCE_DEFAULTS["class_name"], + "class_name": lw_type, "parent": CREATE_RESOURCE_DEFAULTS["parent_template"].format(slot=slot), "bind_locations": {"x": 0.0, "y": 0.0, "z": 0.0}, "slot_on_deck": slot, @@ -434,8 +431,6 @@ def build_protocol_graph( ) slot_to_create_resource[slot] = node_id - # create_resource 之间不需要 ready 连接 - # ==================== 第二步:为每个 reagent 创建 set_liquid_from_plate 节点 ==================== # 创建 Group 节点,包含所有 set_liquid_from_plate 节点 set_liquid_group_id = str(uuid.uuid4()) diff --git a/unilabos/workflow/convert_from_json.py b/unilabos/workflow/convert_from_json.py index ff749d7..acd0d71 100644 --- a/unilabos/workflow/convert_from_json.py +++ b/unilabos/workflow/convert_from_json.py @@ -1,16 +1,20 @@ """ JSON 工作流转换模块 -将 workflow/reagent 格式的 JSON 转换为统一工作流格式。 +将 workflow/reagent/labware 格式的 JSON 转换为统一工作流格式。 输入格式: { + "labware": [ + {"name": "...", "slot": "1", "type": "lab_xxx"}, + ... + ], "workflow": [ {"action": "...", "action_args": {...}}, ... ], "reagent": { - "reagent_name": {"slot": int, "well": [...], "labware": "..."}, + "reagent_name": {"slot": int, "well": [...]}, ... } } @@ -245,18 +249,18 @@ def convert_from_json( if "workflow" not in json_data or "reagent" not in json_data: raise ValueError( "不支持的 JSON 格式。请使用标准格式:\n" - '{"workflow": [{"action": "...", "action_args": {...}}, ...], ' - '"reagent": {"name": {"slot": int, "well": [...], "labware": "..."}, ...}}' + '{"labware": [...], "workflow": [...], "reagent": {...}}' ) # 提取数据 workflow = json_data["workflow"] reagent = json_data["reagent"] + labware_defs = json_data.get("labware", []) # 新的 labware 定义列表 # 规范化步骤数据 protocol_steps = normalize_workflow_steps(workflow) - # reagent 已经是字典格式,直接使用 + # reagent 已经是字典格式,用于 set_liquid 和 well 数量查找 labware_info = reagent # 构建工作流图 @@ -265,6 +269,7 @@ def convert_from_json( protocol_steps=protocol_steps, workstation_name=workstation_name, action_resource_mapping=ACTION_RESOURCE_MAPPING, + labware_defs=labware_defs, ) # 校验句柄配置