Compare commits

..

2 Commits

Author SHA1 Message Date
zhangshixiang
cfe64b023b 添加抓取后物料上传 2025-12-28 01:44:34 +08:00
zhangshixiang
ad1312cf26 修改传到网页的物料坐标 2025-12-28 01:40:20 +08:00
4 changed files with 174 additions and 60 deletions

View File

@@ -4,6 +4,7 @@ HTTP客户端模块
提供与远程服务器通信的客户端功能只有host需要用
"""
from copy import deepcopy
import json
import os
import time
@@ -75,6 +76,27 @@ class HTTPClient:
Returns:
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
"""
# 遍历 resources 及其所有子节点,将 pose.position.y 全部变为 -y
def invert_y_position(resource_instance, size_y: float = 0):
# 处理当前节点
pose = getattr(resource_instance.res_content, "pose", None)
if pose and hasattr(pose, "position"):
position = getattr(pose, "position", None)
pose_size = getattr(pose, "size", None)
if position and hasattr(position, "y") and pose_size and hasattr(pose_size, "height"):
position.y = size_y - position.y - pose_size.height
# 递归处理子节点
for child in getattr(resource_instance, "children", []):
_size_y = 0
if pose and hasattr(pose, "size"):
_size_y = pose.size.height
invert_y_position(child, _size_y)
# 处理所有树的所有节点,从树的根节点递归
resources_reversed = deepcopy(resources)
for tree in getattr(resources_reversed, "trees", []):
root_node = getattr(tree, "root_node", tree)
invert_y_position(root_node, root_node.res_content.pose.size.height if root_node.res_content.pose.size else 0)
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_add.json"), "w", encoding="utf-8") as f:
payload = {"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}
f.write(json.dumps(payload, indent=4))
@@ -85,14 +107,14 @@ class HTTPClient:
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
response = requests.post(
f"{self.remote_addr}/edge/material",
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
json={"nodes": [x for xs in resources_reversed.dump() for x in xs], "mount_uuid": mount_uuid},
headers={"Authorization": f"Lab {self.auth}"},
timeout=60,
)
else:
response = requests.put(
f"{self.remote_addr}/edge/material",
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
json={"nodes": [x for xs in resources_reversed.dump() for x in xs], "mount_uuid": mount_uuid},
headers={"Authorization": f"Lab {self.auth}"},
timeout=10,
)

View File

@@ -30,10 +30,11 @@ from pylabrobot.liquid_handling.standard import (
ResourceMove,
ResourceDrop,
)
from pylabrobot.resources import ResourceHolder, ResourceStack, Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash, PlateAdapter, TubeRack
from pylabrobot.resources import ResourceHolder, ResourceStack, Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash, PlateAdapter, TubeRack, create_homogeneous_resources, create_ordered_items_2d
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract, SimpleReturn
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
from unilabos.resources.itemized_carrier import ItemizedCarrier
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode
class PRCXIError(RuntimeError):
@@ -86,19 +87,81 @@ class PRCXI9300Container(Plate):
category: str,
ordering: collections.OrderedDict,
model: Optional[str] = None,
material_info: Optional[Dict[str, Any]] = None,
ordering_layout: str = "col-major",
**kwargs,
):
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
self._unilabos_state = {}
self.sites = kwargs.get("sites", [])
self.sites = create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(0, 0, 0)],
resource_size_x=size_x,
resource_size_y=size_y,
resource_size_z=size_z,
name_prefix=name,
)[0]
# 为 ItemizedCarrier 添加 _unilabos_state 属性,以便与其他 PRCXI 组件兼容
sites_resource = ItemizedCarrier(
name=name+"_sites",
sites={name: self.sites},
size_x=size_x,
size_y=size_y,
size_z=size_z,
category="warehouse",
model=model,
)
sites_resource._unilabos_state = {} # 添加 _unilabos_state 属性
if material_info:
sites_resource._unilabos_state["Material"] = material_info
self.assign_child_resource(sites_resource, location=self.sites.location)
# 保存排序方式供graphio.py的坐标映射使用
# 使用独立属性避免与父类的layout冲突
self.ordering_layout = ordering_layout
def serialize(self) -> dict:
"""序列化时保存 ordering_layout 属性"""
data = super().serialize()
data['ordering_layout'] = self.ordering_layout
return data
def load_state(self, state: Dict[str, Any]) -> None:
"""从给定的状态加载工作台信息。"""
super().load_state(state)
self._unilabos_state = state
def get_site(self) -> ResourceHolder:
"""获取容器的站点"""
return self.sites
def add_resource_to_site(self, resource) -> None:
"""向站点添加资源"""
self.sites.assign_child_resource(resource)
def get_resource_at_site(self):
"""获取站点上的资源"""
return self.sites.children[0] if self.sites.children else None
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
data = super().serialize_state()
data.update(self._unilabos_state)
# 避免序列化 ResourceHolder 对象
if hasattr(self, 'sites') and self.sites:
# 创建 sites 的可序列化版本
if hasattr(self.sites, '__class__') and 'pylabrobot' in str(self.sites.__class__.__module__):
data['sites'] = {
"__pylabrobot_object__": True,
"class": self.sites.__class__.__name__,
"module": self.sites.__class__.__module__,
"name": getattr(self.sites, 'name', str(self.sites))
}
else:
data['sites'] = self.sites
return data
class PRCXI9300Plate(Plate):
"""
@@ -210,9 +273,16 @@ class PRCXI9300TipRack(TipRack):
# 使用 ordering 参数,只包含位置信息(键)
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
else:
# ordering 的值已经是对象,可以直接使用
items = ordering
ordering_param = None
# ordering 的值已经是对象,需要过滤掉 None 值
# 只保留有效的对象,用于 ordered_items 参数
valid_items = {k: v for k, v in ordering.items() if v is not None}
if valid_items:
items = valid_items
ordering_param = None
else:
# 如果没有有效对象,使用 ordering 参数
items = None
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
else:
items = None
ordering_param = None
@@ -348,9 +418,16 @@ class PRCXI9300TubeRack(TubeRack):
# 使用 ordering 参数,只包含位置信息(键)
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
else:
# ordering 的值已经是对象,可以直接使用
items_to_pass = ordering
ordering_param = None
# ordering 的值已经是对象,需要过滤掉 None 值
# 只保留有效的对象,用于 ordered_items 参数
valid_items = {k: v for k, v in ordering.items() if v is not None}
if valid_items:
items_to_pass = valid_items
ordering_param = None
else:
# 如果没有有效对象,使用 ordering 参数
items_to_pass = None
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
elif items is not None:
# 兼容旧的 items 参数
items_to_pass = items
@@ -804,7 +881,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
**backend_kwargs,
):
return await super().move_plate(
res = await super().move_plate(
plate,
to,
intermediate_locations,
@@ -816,6 +893,12 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
target_plate_number = to,
**backend_kwargs,
)
plate.unassign()
to.assign_child_resource(plate, location=Coordinate(0, 0, 0))
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.deck]
})
return res
class PRCXI9300Backend(LiquidHandlerBackend):
"""PRCXI 9300 的后端实现,继承自 LiquidHandlerBackend。

View File

@@ -4019,7 +4019,8 @@ liquid_handler:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times: 0
mix_times:
- 0
mix_vol: 0
none_keys:
- ''
@@ -4175,9 +4176,11 @@ liquid_handler:
mix_stage:
type: string
mix_times:
maximum: 2147483647
minimum: -2147483648
type: integer
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
mix_vol:
maximum: 2147483647
minimum: -2147483648
@@ -5040,7 +5043,8 @@ liquid_handler.biomek:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times: 0
mix_times:
- 0
mix_vol: 0
none_keys:
- ''
@@ -5183,9 +5187,11 @@ liquid_handler.biomek:
mix_stage:
type: string
mix_times:
maximum: 2147483647
minimum: -2147483648
type: integer
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
mix_vol:
maximum: 2147483647
minimum: -2147483648
@@ -9665,7 +9671,8 @@ liquid_handler.prcxi:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times: 0
mix_times:
- 0
mix_vol: 0
none_keys:
- ''
@@ -9821,9 +9828,11 @@ liquid_handler.prcxi:
mix_stage:
type: string
mix_times:
maximum: 2147483647
minimum: -2147483648
type: integer
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
mix_vol:
maximum: 2147483647
minimum: -2147483648

View File

@@ -8,8 +8,8 @@
"parent": "",
"pose": {
"size": {
"width": 562,
"height": 394,
"width": 542,
"height": 374,
"depth": 0
}
},
@@ -37,7 +37,7 @@
"model": null,
"position": {
"x": 0,
"y": 240,
"y": 700,
"z": 0
}
},
@@ -50,8 +50,8 @@
"type": "deck",
"class": "",
"position": {
"x": 10,
"y": 10,
"x": 0,
"y": 0,
"z": 0
},
"config": {
@@ -83,7 +83,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -96,7 +96,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T1",
@@ -128,7 +128,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -141,7 +141,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T2",
@@ -173,7 +173,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -186,7 +186,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T3",
@@ -218,7 +218,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -231,7 +231,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T4",
@@ -263,7 +263,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -276,7 +276,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T5",
@@ -308,7 +308,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -321,7 +321,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T6",
@@ -353,7 +353,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -366,7 +366,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T7",
@@ -398,7 +398,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -411,7 +411,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T8",
@@ -443,7 +443,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -456,7 +456,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T9",
@@ -488,7 +488,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -501,7 +501,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T10",
@@ -533,7 +533,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -546,7 +546,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T11",
@@ -578,7 +578,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -591,7 +591,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T12",
@@ -623,7 +623,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -636,7 +636,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T13",
@@ -668,7 +668,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -681,7 +681,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T14",
@@ -713,7 +713,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -726,7 +726,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T15",
@@ -758,7 +758,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300PlateAdapterSite",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
@@ -771,7 +771,7 @@
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T16",