From b9d6f719707e7de79ab592ab00b3bd4638931b46 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Fri, 6 Feb 2026 00:48:27 +0800 Subject: [PATCH] Adapt to new scheduler. --- unilabos/app/model.py | 2 +- unilabos/app/ws_client.py | 2 + .../liquid_handler_abstract.py | 6 +- .../devices/liquid_handling/prcxi/prcxi.py | 2 +- unilabos/devices/virtual/workbench.py | 42 ++- unilabos/registry/devices/liquid_handler.yaml | 341 ++++++++++++++---- unilabos/registry/devices/virtual_device.yaml | 100 +++++ unilabos/resources/resource_tracker.py | 8 + unilabos/ros/nodes/presets/host_node.py | 2 +- 9 files changed, 419 insertions(+), 86 deletions(-) diff --git a/unilabos/app/model.py b/unilabos/app/model.py index 678fe32..f80ce35 100644 --- a/unilabos/app/model.py +++ b/unilabos/app/model.py @@ -54,7 +54,7 @@ class JobAddReq(BaseModel): action_type: str = Field( examples=["unilabos_msgs.action._str_single_input.StrSingleInput"], description="action type", default="" ) - sample_material: dict = Field(examples=[{"string": "string"}], description="sample uuid to material uuid", default_factory=dict) + sample_material: dict = Field(examples=[{"string": "string"}], description="sample uuid to material uuid") action_args: dict = Field(examples=[{"string": "string"}], description="action arguments", default_factory=dict) task_id: str = Field(examples=["task_id"], description="task uuid (auto-generated if empty)", default="") job_id: str = Field(examples=["job_id"], description="goal uuid (auto-generated if empty)", default="") diff --git a/unilabos/app/ws_client.py b/unilabos/app/ws_client.py index 7949aaa..8dd3ec0 100644 --- a/unilabos/app/ws_client.py +++ b/unilabos/app/ws_client.py @@ -657,6 +657,8 @@ class MessageProcessor: async def _handle_job_start(self, data: Dict[str, Any]): """处理job_start消息""" try: + if not data.get("sample_material"): + data["sample_material"] = {} req = JobAddReq(**data) job_log = format_job_log(req.job_id, req.task_id, req.device_id, req.action) diff --git a/unilabos/devices/liquid_handling/liquid_handler_abstract.py b/unilabos/devices/liquid_handling/liquid_handler_abstract.py index 07abb87..3540e5e 100644 --- a/unilabos/devices/liquid_handling/liquid_handler_abstract.py +++ b/unilabos/devices/liquid_handling/liquid_handler_abstract.py @@ -690,16 +690,14 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware): ) def set_liquid_from_plate( - self, plate: List[ResourceSlot], well_names: list[str], liquid_names: list[str], volumes: list[float] + self, plate: ResourceSlot, well_names: list[str], liquid_names: list[str], volumes: list[float] ) -> SetLiquidFromPlateReturn: """Set the liquid in wells of a plate by well names (e.g., A1, A2, B3). 如果 liquid_names 和 volumes 为空,但 plate 和 well_names 不为空,直接返回 plate 和 wells。 """ - if isinstance(plate, list): # 未来移除 - plate = plate[0] assert issubclass(plate.__class__, Plate), "plate must be a Plate" - plate: Plate = cast(Plate, plate) + plate: Plate = cast(Plate, cast(Resource, plate)) # 根据 well_names 获取对应的 Well 对象 wells = [plate.get_well(name) for name in well_names] res_volumes = [] diff --git a/unilabos/devices/liquid_handling/prcxi/prcxi.py b/unilabos/devices/liquid_handling/prcxi/prcxi.py index f337529..5e21ad9 100644 --- a/unilabos/devices/liquid_handling/prcxi/prcxi.py +++ b/unilabos/devices/liquid_handling/prcxi/prcxi.py @@ -595,7 +595,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract): return super().set_liquid(wells, liquid_names, volumes) def set_liquid_from_plate( - self, plate: List[ResourceSlot], well_names: list[str], liquid_names: list[str], volumes: list[float] + self, plate: ResourceSlot, well_names: list[str], liquid_names: list[str], volumes: list[float] ) -> SetLiquidFromPlateReturn: return super().set_liquid_from_plate(plate, well_names, liquid_names, volumes) diff --git a/unilabos/devices/virtual/workbench.py b/unilabos/devices/virtual/workbench.py index f3ad912..d20885f 100644 --- a/unilabos/devices/virtual/workbench.py +++ b/unilabos/devices/virtual/workbench.py @@ -14,7 +14,7 @@ Virtual Workbench Device - 模拟工作台设备 import logging import time -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, List from dataclasses import dataclass from enum import Enum from threading import Lock, RLock @@ -23,7 +23,7 @@ from typing_extensions import TypedDict from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode from unilabos.utils.decorator import not_action -from unilabos.resources.resource_tracker import SampleUUIDsType +from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, RETURN_UNILABOS_SAMPLES # ============ TypedDict 返回类型定义 ============ @@ -37,6 +37,7 @@ class MoveToHeatingStationResult(TypedDict): material_id: str material_number: int message: str + unilabos_samples: List[LabSample] class StartHeatingResult(TypedDict): @@ -47,6 +48,7 @@ class StartHeatingResult(TypedDict): material_id: str material_number: int message: str + unilabos_samples: List[LabSample] class MoveToOutputResult(TypedDict): @@ -55,6 +57,7 @@ class MoveToOutputResult(TypedDict): success: bool station_id: int material_id: str + unilabos_samples: List[LabSample] class PrepareMaterialsResult(TypedDict): @@ -68,6 +71,7 @@ class PrepareMaterialsResult(TypedDict): material_4: int # 物料编号4 material_5: int # 物料编号5 message: str + unilabos_samples: List[LabSample] # ============ 状态枚举 ============ @@ -325,6 +329,7 @@ class VirtualWorkbench: "material_4": materials[3] if len(materials) > 3 else 0, "material_5": materials[4] if len(materials) > 4 else 0, "message": f"已准备 {count} 个物料: A1-A{count}", + "unilabos_samples": [LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for sample_uuid, content in sample_uuids.items()] } def move_to_heating_station( @@ -406,6 +411,9 @@ class VirtualWorkbench: "material_id": material_id, "material_number": material_number, "message": f"{material_id}已成功移动到加热台{station_id}", + "unilabos_samples": [ + LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for + sample_uuid, content in sample_uuids.items()] } except Exception as e: @@ -418,6 +426,9 @@ class VirtualWorkbench: "material_id": material_id, "material_number": material_number, "message": f"移动失败: {str(e)}", + "unilabos_samples": [ + LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for + sample_uuid, content in sample_uuids.items()] } def start_heating( @@ -445,6 +456,9 @@ class VirtualWorkbench: "material_id": "", "material_number": material_number, "message": f"无效的加热台ID: {station_id}", + "unilabos_samples": [ + LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for + sample_uuid, content in sample_uuids.items()] } with self._stations_lock: @@ -457,6 +471,9 @@ class VirtualWorkbench: "material_id": "", "material_number": material_number, "message": f"加热台{station_id}上没有物料", + "unilabos_samples": [ + LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for + sample_uuid, content in sample_uuids.items()] } if station.state == HeatingStationState.HEATING: @@ -466,6 +483,9 @@ class VirtualWorkbench: "material_id": station.current_material, "material_number": material_number, "message": f"加热台{station_id}已经在加热中", + "unilabos_samples": [ + LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for + sample_uuid, content in sample_uuids.items()] } material_id = station.current_material @@ -515,6 +535,9 @@ class VirtualWorkbench: "material_id": material_id, "material_number": material_number, "message": f"加热台{station_id}加热完成", + "unilabos_samples": [ + LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for + sample_uuid, content in sample_uuids.items()] } def move_to_output( @@ -542,6 +565,9 @@ class VirtualWorkbench: "material_id": "", "output_position": f"C{output_number}", "message": f"无效的加热台ID: {station_id}", + "unilabos_samples": [ + LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for + sample_uuid, content in sample_uuids.items()] } with self._stations_lock: @@ -555,6 +581,9 @@ class VirtualWorkbench: "material_id": "", "output_position": f"C{output_number}", "message": f"加热台{station_id}上没有物料", + "unilabos_samples": [ + LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for + sample_uuid, content in sample_uuids.items()] } if station.state != HeatingStationState.COMPLETED: @@ -564,6 +593,9 @@ class VirtualWorkbench: "material_id": material_id, "output_position": f"C{output_number}", "message": f"加热台{station_id}尚未完成加热 (当前状态: {station.state.value})", + "unilabos_samples": [ + LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for + sample_uuid, content in sample_uuids.items()] } output_position = f"C{output_number}" @@ -612,6 +644,9 @@ class VirtualWorkbench: "material_id": material_id, "output_position": output_position, "message": f"{material_id}已成功移动到{output_position}", + "unilabos_samples": [ + LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for + sample_uuid, content in sample_uuids.items()] } except Exception as e: @@ -624,6 +659,9 @@ class VirtualWorkbench: "material_id": "", "output_position": output_position, "message": f"移动失败: {str(e)}", + "unilabos_samples": [ + LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for + sample_uuid, content in sample_uuids.items()] } # ============ 状态属性 ============ diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index 319d033..b04d631 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -9468,7 +9468,7 @@ liquid_handler.prcxi: well_names: null handles: input: - - data_key: plate + - data_key: '@this.0@@@plate' data_source: handle data_type: resource handler_key: input_plate @@ -9503,81 +9503,78 @@ liquid_handler.prcxi: type: string type: array plate: - items: - properties: - category: + properties: + category: + type: string + children: + items: type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: plate - type: object + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data title: plate - type: array + type: object volumes: items: type: number @@ -9593,17 +9590,207 @@ liquid_handler.prcxi: - volumes type: object result: + $defs: + ResourceDict: + properties: + class: + description: Resource class name + title: Class + type: string + config: + additionalProperties: true + description: Resource configuration + title: Config + type: object + data: + additionalProperties: true + description: 'Resource data, eg: container liquid data' + title: Data + type: object + description: + default: '' + description: Resource description + title: Description + type: string + extra: + additionalProperties: true + description: 'Extra data, eg: slot index' + title: Extra + type: object + icon: + default: '' + description: Resource icon + title: Icon + type: string + id: + description: Resource ID + title: Id + type: string + model: + additionalProperties: true + description: Resource model + title: Model + type: object + name: + description: Resource name + title: Name + type: string + parent: + anyOf: + - $ref: '#/$defs/ResourceDict' + - type: 'null' + default: null + description: Parent resource object + parent_uuid: + anyOf: + - type: string + - type: 'null' + default: null + description: Parent resource uuid + title: Parent Uuid + pose: + $ref: '#/$defs/ResourceDictPosition' + description: Resource position + schema: + additionalProperties: true + description: Resource schema + title: Schema + type: object + type: + anyOf: + - const: device + type: string + - type: string + description: Resource type + title: Type + uuid: + description: Resource UUID + title: Uuid + type: string + required: + - id + - uuid + - name + - type + - class + - config + - data + - extra + title: ResourceDict + type: object + ResourceDictPosition: + properties: + cross_section_type: + default: rectangle + description: Cross section type + enum: + - rectangle + - circle + - rounded_rectangle + title: Cross Section Type + type: string + layout: + default: x-y + description: Resource layout + enum: + - 2d + - x-y + - z-y + - x-z + title: Layout + type: string + position: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource position + position3d: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource position in 3D space + rotation: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource rotation + scale: + $ref: '#/$defs/ResourceDictPositionScale' + description: Resource scale + size: + $ref: '#/$defs/ResourceDictPositionSize' + description: Resource size + title: ResourceDictPosition + type: object + ResourceDictPositionObject: + properties: + x: + default: 0.0 + description: X coordinate + title: X + type: number + y: + default: 0.0 + description: Y coordinate + title: Y + type: number + z: + default: 0.0 + description: Z coordinate + title: Z + type: number + title: ResourceDictPositionObject + type: object + ResourceDictPositionScale: + properties: + x: + default: 0.0 + description: x scale + title: X + type: number + y: + default: 0.0 + description: y scale + title: Y + type: number + z: + default: 0.0 + description: z scale + title: Z + type: number + title: ResourceDictPositionScale + type: object + ResourceDictPositionSize: + properties: + depth: + default: 0.0 + description: Depth + title: Depth + type: number + height: + default: 0.0 + description: Height + title: Height + type: number + width: + default: 0.0 + description: Width + title: Width + type: number + title: ResourceDictPositionSize + type: object properties: plate: - items: {} + items: + items: + $ref: '#/$defs/ResourceDict' + type: array title: Plate type: array volumes: - items: {} + items: + type: number title: Volumes type: array wells: - items: {} + items: + items: + $ref: '#/$defs/ResourceDict' + type: array title: Wells type: array required: diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index c38655c..e44b745 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -5835,6 +5835,25 @@ virtual_workbench: - material_number type: object result: + $defs: + LabSample: + properties: + extra: + additionalProperties: true + title: Extra + type: object + oss_path: + title: Oss Path + type: string + sample_uuid: + title: Sample Uuid + type: string + required: + - sample_uuid + - oss_path + - extra + title: LabSample + type: object description: move_to_heating_station 返回类型 properties: material_id: @@ -5853,12 +5872,18 @@ virtual_workbench: success: title: Success type: boolean + unilabos_samples: + items: + $ref: '#/$defs/LabSample' + title: Unilabos Samples + type: array required: - success - station_id - material_id - material_number - message + - unilabos_samples title: MoveToHeatingStationResult type: object required: @@ -5903,6 +5928,25 @@ virtual_workbench: - material_number type: object result: + $defs: + LabSample: + properties: + extra: + additionalProperties: true + title: Extra + type: object + oss_path: + title: Oss Path + type: string + sample_uuid: + title: Sample Uuid + type: string + required: + - sample_uuid + - oss_path + - extra + title: LabSample + type: object description: move_to_output 返回类型 properties: material_id: @@ -5914,10 +5958,16 @@ virtual_workbench: success: title: Success type: boolean + unilabos_samples: + items: + $ref: '#/$defs/LabSample' + title: Unilabos Samples + type: array required: - success - station_id - material_id + - unilabos_samples title: MoveToOutputResult type: object required: @@ -5972,6 +6022,25 @@ virtual_workbench: required: [] type: object result: + $defs: + LabSample: + properties: + extra: + additionalProperties: true + title: Extra + type: object + oss_path: + title: Oss Path + type: string + sample_uuid: + title: Sample Uuid + type: string + required: + - sample_uuid + - oss_path + - extra + title: LabSample + type: object description: prepare_materials 返回类型 - 批量准备物料 properties: count: @@ -5998,6 +6067,11 @@ virtual_workbench: success: title: Success type: boolean + unilabos_samples: + items: + $ref: '#/$defs/LabSample' + title: Unilabos Samples + type: array required: - success - count @@ -6007,6 +6081,7 @@ virtual_workbench: - material_4 - material_5 - message + - unilabos_samples title: PrepareMaterialsResult type: object required: @@ -6062,6 +6137,25 @@ virtual_workbench: - material_number type: object result: + $defs: + LabSample: + properties: + extra: + additionalProperties: true + title: Extra + type: object + oss_path: + title: Oss Path + type: string + sample_uuid: + title: Sample Uuid + type: string + required: + - sample_uuid + - oss_path + - extra + title: LabSample + type: object description: start_heating 返回类型 properties: material_id: @@ -6079,12 +6173,18 @@ virtual_workbench: success: title: Success type: boolean + unilabos_samples: + items: + $ref: '#/$defs/LabSample' + title: Unilabos Samples + type: array required: - success - station_id - material_id - material_number - message + - unilabos_samples title: StartHeatingResult type: object required: diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index 0b67fe3..2054e2a 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -5,6 +5,8 @@ from pydantic import BaseModel, field_serializer, field_validator, ValidationErr from pydantic import Field from typing import List, Tuple, Any, Dict, Literal, Optional, cast, TYPE_CHECKING, Union +from typing_extensions import TypedDict + from unilabos.resources.plr_additional_res_reg import register from unilabos.utils.log import logger @@ -30,6 +32,12 @@ RETURN_UNILABOS_SAMPLES = "unilabos_samples" SampleUUIDsType = Dict[str, Optional["PLRResource"]] +class LabSample(TypedDict): + sample_uuid: str + oss_path: str + extra: Dict[str, Any] + + class ResourceDictPositionSize(BaseModel): depth: float = Field(description="Depth", default=0.0) # z width: float = Field(description="Width", default=0.0) # x diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 0ac1de2..dde756a 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -863,7 +863,7 @@ class HostNode(BaseROS2DeviceNode): f"{[s.get('name', s.get('id', 'unknown')) if isinstance(s, dict) else str(s)[:20] for s in unilabos_samples[:5]]}" f"{'...' if len(unilabos_samples) > 5 else ''}" ) - return_info[RETURN_UNILABOS_SAMPLES] = unilabos_samples + return_info["samples"] = unilabos_samples suc = return_info.get("suc", False) if not suc: status = "failed"