mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-24 13:19:14 +00:00
Compare commits
1 Commits
5c047beb83
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7340e33652 |
2
.github/workflows/conda-pack-build.yml
vendored
2
.github/workflows/conda-pack-build.yml
vendored
@@ -312,7 +312,7 @@ jobs:
|
||||
|
||||
- name: Upload distribution package
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}
|
||||
path: dist-package/
|
||||
|
||||
2
.github/workflows/multi-platform-build.yml
vendored
2
.github/workflows/multi-platform-build.yml
vendored
@@ -149,7 +149,7 @@ jobs:
|
||||
|
||||
- name: Upload conda package artifacts
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: conda-package-${{ matrix.platform }}
|
||||
path: conda-packages-temp
|
||||
|
||||
2
.github/workflows/unilabos-conda-build.yml
vendored
2
.github/workflows/unilabos-conda-build.yml
vendored
@@ -195,7 +195,7 @@ jobs:
|
||||
|
||||
- name: Upload conda package artifacts
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: conda-package-unilabos-${{ matrix.platform }}
|
||||
path: conda-packages-temp
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import signal
|
||||
import sys
|
||||
@@ -359,7 +358,7 @@ def main():
|
||||
if BasicConfig.test_mode:
|
||||
print_status("启用测试模式:所有动作将模拟执行,不调用真实硬件", "warning")
|
||||
BasicConfig.communication_protocol = "websocket"
|
||||
machine_name = platform.node()
|
||||
machine_name = os.popen("hostname").read().strip()
|
||||
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
|
||||
BasicConfig.machine_name = machine_name
|
||||
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
|
||||
|
||||
@@ -55,7 +55,6 @@ from unilabos.devices.liquid_handling.liquid_handler_abstract import (
|
||||
TransferLiquidReturn,
|
||||
)
|
||||
from unilabos.registry.placeholder_type import ResourceSlot
|
||||
from unilabos.resources.resource_tracker import ResourceTreeSet
|
||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||
|
||||
|
||||
@@ -91,103 +90,20 @@ class PRCXI9300Deck(Deck):
|
||||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||||
"""
|
||||
|
||||
# T1-T16 默认位置 (4列×4行)
|
||||
_DEFAULT_SITE_POSITIONS = [
|
||||
(0, 0, 0), (138, 0, 0), (276, 0, 0), (414, 0, 0), # T1-T4
|
||||
(0, 96, 0), (138, 96, 0), (276, 96, 0), (414, 96, 0), # T5-T8
|
||||
(0, 192, 0), (138, 192, 0), (276, 192, 0), (414, 192, 0), # T9-T12
|
||||
(0, 288, 0), (138, 288, 0), (276, 288, 0), (414, 288, 0), # T13-T16
|
||||
]
|
||||
_DEFAULT_SITE_SIZE = {"width": 128.0, "height": 86, "depth": 0}
|
||||
_DEFAULT_CONTENT_TYPE = ["plate", "tip_rack", "plates", "tip_racks", "tube_rack", "adaptor"]
|
||||
|
||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
|
||||
sites: Optional[List[Dict[str, Any]]] = None, **kwargs):
|
||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float, **kwargs):
|
||||
super().__init__(size_x, size_y, size_z, name)
|
||||
if sites is not None:
|
||||
self.sites: List[Dict[str, Any]] = [dict(s) for s in sites]
|
||||
else:
|
||||
self.sites = []
|
||||
for i, (x, y, z) in enumerate(self._DEFAULT_SITE_POSITIONS):
|
||||
self.sites.append({
|
||||
"label": f"T{i + 1}",
|
||||
"visible": True,
|
||||
"position": {"x": x, "y": y, "z": z},
|
||||
"size": dict(self._DEFAULT_SITE_SIZE),
|
||||
"content_type": list(self._DEFAULT_CONTENT_TYPE),
|
||||
})
|
||||
# _ordering: label -> None, 用于外部通过 list(keys()).index(site) 将 Tn 转换为 spot index
|
||||
self._ordering = collections.OrderedDict(
|
||||
(site["label"], None) for site in self.sites
|
||||
)
|
||||
|
||||
def _get_site_location(self, idx: int) -> Coordinate:
|
||||
pos = self.sites[idx]["position"]
|
||||
return Coordinate(pos["x"], pos["y"], pos["z"])
|
||||
|
||||
def _get_site_resource(self, idx: int) -> Optional[Resource]:
|
||||
site_loc = self._get_site_location(idx)
|
||||
for child in self.children:
|
||||
if child.location == site_loc:
|
||||
return child
|
||||
return None
|
||||
|
||||
def assign_child_resource(
|
||||
self,
|
||||
resource: Resource,
|
||||
location: Optional[Coordinate] = None,
|
||||
reassign: bool = True,
|
||||
spot: Optional[int] = None,
|
||||
):
|
||||
idx = spot
|
||||
if spot is not None:
|
||||
idx = spot
|
||||
else:
|
||||
for i, site in enumerate(self.sites):
|
||||
site_loc = self._get_site_location(i)
|
||||
if site.get("label") == resource.name:
|
||||
idx = i
|
||||
break
|
||||
if location is not None and site_loc == location:
|
||||
idx = i
|
||||
break
|
||||
|
||||
if idx is None:
|
||||
for i in range(len(self.sites)):
|
||||
if self._get_site_resource(i) is None:
|
||||
idx = i
|
||||
break
|
||||
|
||||
if idx is None:
|
||||
raise ValueError(f"No available site on deck '{self.name}' for resource '{resource.name}'")
|
||||
|
||||
if not reassign and self._get_site_resource(idx) is not None:
|
||||
raise ValueError(f"Site {idx} ('{self.sites[idx]['label']}') is already occupied")
|
||||
|
||||
loc = self._get_site_location(idx)
|
||||
super().assign_child_resource(resource, location=loc, reassign=reassign)
|
||||
self.slots = [None] * 16 # PRCXI 9300/9320 最大有 16 个槽位
|
||||
self.slot_locations = [Coordinate(0, 0, 0)] * 16
|
||||
|
||||
def assign_child_at_slot(self, resource: Resource, slot: int, reassign: bool = False) -> None:
|
||||
self.assign_child_resource(resource, spot=slot - 1, reassign=reassign)
|
||||
if self.slots[slot - 1] is not None and not reassign:
|
||||
raise ValueError(f"Spot {slot} is already occupied")
|
||||
|
||||
def serialize(self) -> dict:
|
||||
data = super().serialize()
|
||||
sites_out = []
|
||||
for i, site in enumerate(self.sites):
|
||||
occupied = self._get_site_resource(i)
|
||||
sites_out.append({
|
||||
"label": site["label"],
|
||||
"visible": site.get("visible", True),
|
||||
"occupied_by": occupied.name if occupied is not None else None,
|
||||
"position": site["position"],
|
||||
"size": site["size"],
|
||||
"content_type": site["content_type"],
|
||||
})
|
||||
data["sites"] = sites_out
|
||||
return data
|
||||
self.slots[slot - 1] = resource
|
||||
super().assign_child_resource(resource, location=self.slot_locations[slot - 1])
|
||||
|
||||
|
||||
class PRCXI9300Container(Container):
|
||||
class PRCXI9300Container(Plate):
|
||||
"""PRCXI 9300 的专用 Container 类,继承自 Plate,用于槽位定位和未知模块。
|
||||
|
||||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||||
@@ -200,10 +116,11 @@ class PRCXI9300Container(Container):
|
||||
size_y: float,
|
||||
size_z: float,
|
||||
category: str,
|
||||
ordering: collections.OrderedDict,
|
||||
model: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(name, size_x, size_y, size_z, category=category, model=model)
|
||||
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
|
||||
self._unilabos_state = {}
|
||||
|
||||
def load_state(self, state: Dict[str, Any]) -> None:
|
||||
@@ -650,14 +567,14 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
||||
tablets_info = []
|
||||
count = 0
|
||||
for child in deck.children:
|
||||
# 如果放其他类型的物料,是不可以的
|
||||
if hasattr(child, "_unilabos_state") and "Material" in child._unilabos_state:
|
||||
number = int(child.name.replace("T", ""))
|
||||
tablets_info.append(
|
||||
WorkTablets(
|
||||
Number=number, Code=f"T{number}", Material=child._unilabos_state["Material"]
|
||||
if child.children:
|
||||
if "Material" in child.children[0]._unilabos_state:
|
||||
number = int(child.name.replace("T", ""))
|
||||
tablets_info.append(
|
||||
WorkTablets(
|
||||
Number=number, Code=f"T{number}", Material=child.children[0]._unilabos_state["Material"]
|
||||
)
|
||||
)
|
||||
)
|
||||
if is_9320:
|
||||
print("当前设备是9320")
|
||||
# 始终初始化 step_mode 属性
|
||||
|
||||
@@ -175,8 +175,7 @@ class Registry:
|
||||
"res_id": "unilabos_resources", # 将当前实验室的全部物料id作为下拉框可选择
|
||||
"device_id": "unilabos_devices", # 将当前实验室的全部设备id作为下拉框可选择
|
||||
"parent": "unilabos_nodes", # 将当前实验室的设备/物料作为下拉框可选择
|
||||
"class_name": "unilabos_class", # 当前实验室物料的class name
|
||||
"slot_on_deck": "unilabos_resource_slot:parent", # 勾选的parent的config中的sites的name,展示name,参数对应slot(index)
|
||||
"class_name": "unilabos_class",
|
||||
},
|
||||
},
|
||||
"test_latency": {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
|
||||
from pylabrobot.resources import Container
|
||||
from unilabos_msgs.msg import Resource
|
||||
|
||||
from unilabos.ros.msgs.message_converter import convert_from_ros_msg
|
||||
|
||||
|
||||
class RegularContainer(Container):
|
||||
@@ -12,12 +16,12 @@ class RegularContainer(Container):
|
||||
kwargs["size_y"] = 0
|
||||
if "size_z" not in kwargs:
|
||||
kwargs["size_z"] = 0
|
||||
|
||||
self.kwargs = kwargs
|
||||
self.state = {}
|
||||
super().__init__(*args, category="container", **kwargs)
|
||||
|
||||
def load_state(self, state: Dict[str, Any]):
|
||||
super().load_state(state)
|
||||
self.state = state
|
||||
|
||||
|
||||
def get_regular_container(name="container"):
|
||||
@@ -25,6 +29,7 @@ def get_regular_container(name="container"):
|
||||
r.category = "container"
|
||||
return r
|
||||
|
||||
#
|
||||
# class RegularContainer(object):
|
||||
# # 第一个参数必须是id传入
|
||||
# # noinspection PyShadowingBuiltins
|
||||
@@ -84,4 +89,4 @@ def get_regular_container(name="container"):
|
||||
# return to_dict
|
||||
#
|
||||
# def __str__(self):
|
||||
# return f"{self.id}"
|
||||
# return f"{self.id}"
|
||||
@@ -76,7 +76,7 @@ def canonicalize_nodes_data(
|
||||
if sample_id:
|
||||
logger.error(f"{node}的sample_id参数已弃用,sample_id: {sample_id}")
|
||||
for k in list(node.keys()):
|
||||
if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children", "pose", "extra"]:
|
||||
if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children", "pose"]:
|
||||
v = node.pop(k)
|
||||
node["config"][k] = v
|
||||
if outer_host_node_id is not None:
|
||||
|
||||
@@ -16,7 +16,6 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
EXTRA_CLASS = "unilabos_resource_class"
|
||||
FRONTEND_POSE_EXTRA = "unilabos_frontend_pose_extra"
|
||||
EXTRA_SAMPLE_UUID = "sample_uuid"
|
||||
EXTRA_UNILABOS_SAMPLE_UUID = "unilabos_sample_uuid"
|
||||
|
||||
@@ -75,14 +74,6 @@ class ResourceDictPositionObject(BaseModel):
|
||||
z: float = Field(description="Z coordinate", default=0.0)
|
||||
|
||||
|
||||
class ResourceDictPoseExtraObjectType(BaseModel):
|
||||
z_index: int
|
||||
|
||||
|
||||
class ResourceDictPoseExtraObject(BaseModel):
|
||||
z_index: Optional[int] = Field(alias="zIndex", default=None)
|
||||
|
||||
|
||||
class ResourceDictPositionType(TypedDict):
|
||||
size: ResourceDictPositionSizeType
|
||||
scale: ResourceDictPositionScaleType
|
||||
@@ -109,7 +100,6 @@ class ResourceDictPosition(BaseModel):
|
||||
cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field(
|
||||
description="Cross section type", default="rectangle"
|
||||
)
|
||||
extra: Optional[ResourceDictPoseExtraObject] = Field(description="Extra data", default=None)
|
||||
|
||||
|
||||
class ResourceDictType(TypedDict):
|
||||
@@ -421,15 +411,6 @@ class ResourceTreeSet(object):
|
||||
"tip_spot": "tip_spot",
|
||||
"tube": "tube",
|
||||
"bottle_carrier": "bottle_carrier",
|
||||
"material_hole": "material_hole",
|
||||
"container": "container",
|
||||
"material_plate": "material_plate",
|
||||
"electrode_sheet": "electrode_sheet",
|
||||
"warehouse": "warehouse",
|
||||
"magazine_holder": "magazine_holder",
|
||||
"resource_group": "resource_group",
|
||||
"trash": "trash",
|
||||
"plate_adapter": "plate_adapter",
|
||||
}
|
||||
if source in replace_info:
|
||||
return replace_info[source]
|
||||
@@ -473,7 +454,6 @@ class ResourceTreeSet(object):
|
||||
"position3d": raw_pos,
|
||||
"rotation": d["rotation"],
|
||||
"cross_section_type": d.get("cross_section_type", "rectangle"),
|
||||
"extra": extra.get(FRONTEND_POSE_EXTRA)
|
||||
}
|
||||
|
||||
# 先构建当前节点的字典(不包含children)
|
||||
@@ -559,7 +539,6 @@ class ResourceTreeSet(object):
|
||||
name_to_uuid[node.res_content.name] = node.res_content.uuid
|
||||
all_states[node.res_content.name] = node.res_content.data
|
||||
name_to_extra[node.res_content.name] = node.res_content.extra
|
||||
name_to_extra[node.res_content.name][FRONTEND_POSE_EXTRA] = node.res_content.pose.extra
|
||||
name_to_extra[node.res_content.name][EXTRA_CLASS] = node.res_content.klass
|
||||
for child in node.children:
|
||||
collect_node_data(child, name_to_uuid, all_states, name_to_extra)
|
||||
@@ -628,7 +607,7 @@ class ResourceTreeSet(object):
|
||||
plr_resources.append(plr_resource)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"转换 PLR 资源失败: {e} {str(plr_dict)[:1000]}")
|
||||
logger.error(f"转换 PLR 资源失败: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(f"堆栈: {traceback.format_exc()}")
|
||||
|
||||
@@ -915,24 +915,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
else []
|
||||
)
|
||||
if target_site is not None and sites is not None and site_names is not None:
|
||||
site_index = None
|
||||
try:
|
||||
# sites 可能是 Resource 列表或 dict 列表 (如 PRCXI9300Deck)
|
||||
# 只有itemized_carrier在使用,准备弃用
|
||||
site_index = sites.index(original_instance)
|
||||
except ValueError:
|
||||
# dict 类型的 sites: 通过name匹配
|
||||
for idx, site in enumerate(sites):
|
||||
if original_instance.name == site["occupied_by"]:
|
||||
site_index = idx
|
||||
break
|
||||
elif (original_instance.location.x == site["position"]["x"] and original_instance.location.y == site["position"]["y"] and original_instance.location.z == site["position"]["z"]):
|
||||
site_index = idx
|
||||
break
|
||||
if site_index is None:
|
||||
site_name = None
|
||||
else:
|
||||
site_name = site_names[site_index]
|
||||
site_index = sites.index(original_instance)
|
||||
site_name = site_names[site_index]
|
||||
if site_name != target_site:
|
||||
parent = self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
||||
if parent is not None:
|
||||
@@ -940,14 +924,6 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
parent_appended = True
|
||||
|
||||
# 加载状态
|
||||
# noinspection PyProtectedMember
|
||||
original_instance._size_x = plr_resource._size_x
|
||||
# noinspection PyProtectedMember
|
||||
original_instance._size_y = plr_resource._size_y
|
||||
# noinspection PyProtectedMember
|
||||
original_instance._size_z = plr_resource._size_z
|
||||
# noinspection PyProtectedMember
|
||||
original_instance._local_size_z = plr_resource._local_size_z
|
||||
original_instance.location = plr_resource.location
|
||||
original_instance.rotation = plr_resource.rotation
|
||||
original_instance.barcode = plr_resource.barcode
|
||||
@@ -1008,7 +984,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
].call_async(
|
||||
r
|
||||
) # type: ignore
|
||||
self.lab_logger().trace(f"确认资源云端 Add 结果: {response.response}")
|
||||
self.lab_logger().info(f"确认资源云端 Add 结果: {response.response}")
|
||||
results.append(result)
|
||||
elif action == "update":
|
||||
if tree_set is None:
|
||||
@@ -1034,7 +1010,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
].call_async(
|
||||
r
|
||||
) # type: ignore
|
||||
self.lab_logger().trace(f"确认资源云端 Update 结果: {response.response}")
|
||||
self.lab_logger().info(f"确认资源云端 Update 结果: {response.response}")
|
||||
results.append(result)
|
||||
elif action == "remove":
|
||||
result = _handle_remove(resources_uuid)
|
||||
|
||||
@@ -1195,7 +1195,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点")
|
||||
# 还需要加入到资源图中,暂不实现,考虑资源图新的获取方式
|
||||
response.response = json.dumps(uuid_mapping)
|
||||
self.lab_logger().info(f"[Host Node-Resource] Resource tree update completed, success: {success}")
|
||||
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
|
||||
|
||||
async def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response):
|
||||
"""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@
|
||||
res_id: plate_slot_{slot}
|
||||
device_id: /PRCXI
|
||||
class_name: PRCXI_BioER_96_wellplate
|
||||
parent: /PRCXI/PRCXI_Deck
|
||||
parent: /PRCXI/PRCXI_Deck/T{slot}
|
||||
slot_on_deck: "{slot}"
|
||||
- 输出端口: labware(用于连接 set_liquid_from_plate)
|
||||
- 控制流: create_resource 之间通过 ready 端口串联
|
||||
@@ -122,7 +122,7 @@ NODE_TYPE_DEFAULT = "ILab" # 所有节点的默认类型
|
||||
# create_resource 节点默认参数
|
||||
CREATE_RESOURCE_DEFAULTS = {
|
||||
"device_id": "/PRCXI",
|
||||
"parent_template": "/PRCXI/PRCXI_Deck",
|
||||
"parent_template": "/PRCXI/PRCXI_Deck/T{slot}", # {slot} 会被替换为实际的 slot 值
|
||||
"class_name": "PRCXI_BioER_96_wellplate",
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@ def build_protocol_graph(
|
||||
"res_id": res_id,
|
||||
"device_id": CREATE_RESOURCE_DEFAULTS["device_id"],
|
||||
"class_name": lw_type,
|
||||
"parent": CREATE_RESOURCE_DEFAULTS["parent_template"],
|
||||
"parent": CREATE_RESOURCE_DEFAULTS["parent_template"].format(slot=slot),
|
||||
"bind_locations": {"x": 0.0, "y": 0.0, "z": 0.0},
|
||||
"slot_on_deck": slot,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user