mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-27 22:13:07 +00:00
Compare commits
18 Commits
prcix9320
...
e0394bf414
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0394bf414 | ||
|
|
975a56415a | ||
|
|
cadbe87e3f | ||
|
|
b993c1f590 | ||
|
|
e0fae94c10 | ||
|
|
b5cd181ac1 | ||
|
|
5c047beb83 | ||
|
|
b40c087143 | ||
|
|
7f1cc3b2a5 | ||
|
|
3f160c2049 | ||
|
|
a54e7c0f23 | ||
|
|
e5015cd5e0 | ||
|
|
514373c164 | ||
|
|
fcea02585a | ||
|
|
07cf690897 | ||
|
|
cfea27460a | ||
|
|
b7d3e980a9 | ||
|
|
f9ed6cb3fb |
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
package:
|
package:
|
||||||
name: unilabos
|
name: unilabos
|
||||||
version: 0.10.17
|
version: 0.10.18
|
||||||
|
|
||||||
source:
|
source:
|
||||||
path: ../../unilabos
|
path: ../../unilabos
|
||||||
@@ -46,13 +46,15 @@ requirements:
|
|||||||
- jinja2
|
- jinja2
|
||||||
- requests
|
- requests
|
||||||
- uvicorn
|
- uvicorn
|
||||||
- opcua # [not osx]
|
- if: not osx
|
||||||
|
then:
|
||||||
|
- opcua
|
||||||
- pyserial
|
- pyserial
|
||||||
- pandas
|
- pandas
|
||||||
- pymodbus
|
- pymodbus
|
||||||
- matplotlib
|
- matplotlib
|
||||||
- pylibftdi
|
- pylibftdi
|
||||||
- uni-lab::unilabos-env ==0.10.17
|
- uni-lab::unilabos-env ==0.10.18
|
||||||
|
|
||||||
about:
|
about:
|
||||||
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
package:
|
package:
|
||||||
name: unilabos-env
|
name: unilabos-env
|
||||||
version: 0.10.17
|
version: 0.10.18
|
||||||
|
|
||||||
build:
|
build:
|
||||||
noarch: generic
|
noarch: generic
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
package:
|
package:
|
||||||
name: unilabos-full
|
name: unilabos-full
|
||||||
version: 0.10.17
|
version: 0.10.18
|
||||||
|
|
||||||
build:
|
build:
|
||||||
noarch: generic
|
noarch: generic
|
||||||
@@ -11,7 +11,7 @@ build:
|
|||||||
requirements:
|
requirements:
|
||||||
run:
|
run:
|
||||||
# Base unilabos package (includes unilabos-env)
|
# Base unilabos package (includes unilabos-env)
|
||||||
- uni-lab::unilabos ==0.10.17
|
- uni-lab::unilabos ==0.10.18
|
||||||
# Documentation tools
|
# Documentation tools
|
||||||
- sphinx
|
- sphinx
|
||||||
- sphinx_rtd_theme
|
- sphinx_rtd_theme
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: ros-humble-unilabos-msgs
|
name: ros-humble-unilabos-msgs
|
||||||
version: 0.10.17
|
version: 0.10.18
|
||||||
source:
|
source:
|
||||||
path: ../../unilabos_msgs
|
path: ../../unilabos_msgs
|
||||||
target_directory: src
|
target_directory: src
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: unilabos
|
name: unilabos
|
||||||
version: "0.10.17"
|
version: "0.10.18"
|
||||||
|
|
||||||
source:
|
source:
|
||||||
path: ../..
|
path: ../..
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name=package_name,
|
name=package_name,
|
||||||
version='0.10.17',
|
version='0.10.18',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=['setuptools'],
|
install_requires=['setuptools'],
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.10.17"
|
__version__ = "0.10.18"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
@@ -358,7 +359,7 @@ def main():
|
|||||||
if BasicConfig.test_mode:
|
if BasicConfig.test_mode:
|
||||||
print_status("启用测试模式:所有动作将模拟执行,不调用真实硬件", "warning")
|
print_status("启用测试模式:所有动作将模拟执行,不调用真实硬件", "warning")
|
||||||
BasicConfig.communication_protocol = "websocket"
|
BasicConfig.communication_protocol = "websocket"
|
||||||
machine_name = os.popen("hostname").read().strip()
|
machine_name = platform.node()
|
||||||
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
|
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
|
||||||
BasicConfig.machine_name = machine_name
|
BasicConfig.machine_name = machine_name
|
||||||
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
|
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ HTTP客户端模块
|
|||||||
|
|
||||||
提供与远程服务器通信的客户端功能,只有host需要用
|
提供与远程服务器通信的客户端功能,只有host需要用
|
||||||
"""
|
"""
|
||||||
|
import gzip
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
@@ -290,10 +290,17 @@ class HTTPClient:
|
|||||||
Returns:
|
Returns:
|
||||||
Response: API响应对象
|
Response: API响应对象
|
||||||
"""
|
"""
|
||||||
|
compressed_body = gzip.compress(
|
||||||
|
json.dumps(registry_data, ensure_ascii=False, default=str).encode("utf-8")
|
||||||
|
)
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.remote_addr}/lab/resource",
|
f"{self.remote_addr}/lab/resource",
|
||||||
json=registry_data,
|
data=compressed_body,
|
||||||
headers={"Authorization": f"Lab {self.auth}"},
|
headers={
|
||||||
|
"Authorization": f"Lab {self.auth}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Content-Encoding": "gzip",
|
||||||
|
},
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
if response.status_code not in [200, 201]:
|
if response.status_code not in [200, 201]:
|
||||||
|
|||||||
@@ -1184,6 +1184,11 @@ class QueueProcessor:
|
|||||||
logger.debug(f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs")
|
logger.debug(f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs")
|
||||||
|
|
||||||
for job_info in queued_jobs:
|
for job_info in queued_jobs:
|
||||||
|
# 快照可能已过期:在遍历过程中 end_job() 可能已将此 job 移至 READY,
|
||||||
|
# 此时不应再发送 busy/need_more,否则会覆盖已发出的 free=True 通知
|
||||||
|
if job_info.status != JobStatus.QUEUE:
|
||||||
|
continue
|
||||||
|
|
||||||
message = {
|
message = {
|
||||||
"action": "report_action_state",
|
"action": "report_action_state",
|
||||||
"data": {
|
"data": {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from pylabrobot.resources import (
|
|||||||
ResourceHolder,
|
ResourceHolder,
|
||||||
Lid,
|
Lid,
|
||||||
Trash,
|
Trash,
|
||||||
Tip,
|
Tip, TubeRack,
|
||||||
)
|
)
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
@@ -696,10 +696,13 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
|
|
||||||
如果 liquid_names 和 volumes 为空,但 plate 和 well_names 不为空,直接返回 plate 和 wells。
|
如果 liquid_names 和 volumes 为空,但 plate 和 well_names 不为空,直接返回 plate 和 wells。
|
||||||
"""
|
"""
|
||||||
assert issubclass(plate.__class__, Plate), "plate must be a Plate"
|
assert issubclass(plate.__class__, Plate) or issubclass(plate.__class__, TubeRack) , f"plate must be a Plate, now: {type(plate)}"
|
||||||
plate: Plate = cast(Plate, cast(Resource, plate))
|
plate: Union[Plate, TubeRack]
|
||||||
# 根据 well_names 获取对应的 Well 对象
|
# 根据 well_names 获取对应的 Well 对象
|
||||||
wells = [plate.get_well(name) for name in well_names]
|
if issubclass(plate.__class__, Plate):
|
||||||
|
wells = [plate.get_well(name) for name in well_names]
|
||||||
|
elif issubclass(plate.__class__, TubeRack):
|
||||||
|
wells = [plate.get_tube(name) for name in well_names]
|
||||||
res_volumes = []
|
res_volumes = []
|
||||||
|
|
||||||
# 如果 liquid_names 和 volumes 都为空,直接返回
|
# 如果 liquid_names 和 volumes 都为空,直接返回
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ from unilabos.devices.liquid_handling.liquid_handler_abstract import (
|
|||||||
TransferLiquidReturn,
|
TransferLiquidReturn,
|
||||||
)
|
)
|
||||||
from unilabos.registry.placeholder_type import ResourceSlot
|
from unilabos.registry.placeholder_type import ResourceSlot
|
||||||
|
from unilabos.resources.resource_tracker import ResourceTreeSet
|
||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
@@ -90,20 +91,103 @@ class PRCXI9300Deck(Deck):
|
|||||||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float, **kwargs):
|
# T1-T16 默认位置 (4列×4行)
|
||||||
super().__init__(name, size_x, size_y, size_z)
|
_DEFAULT_SITE_POSITIONS = [
|
||||||
self.slots = [None] * 16 # PRCXI 9300/9320 最大有 16 个槽位
|
(0, 0, 0), (138, 0, 0), (276, 0, 0), (414, 0, 0), # T1-T4
|
||||||
self.slot_locations = [Coordinate(0, 0, 0)] * 16
|
(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):
|
||||||
|
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)
|
||||||
|
|
||||||
def assign_child_at_slot(self, resource: Resource, slot: int, reassign: bool = False) -> None:
|
def assign_child_at_slot(self, resource: Resource, slot: int, reassign: bool = False) -> None:
|
||||||
if self.slots[slot - 1] is not None and not reassign:
|
self.assign_child_resource(resource, spot=slot - 1, reassign=reassign)
|
||||||
raise ValueError(f"Spot {slot} is already occupied")
|
|
||||||
|
|
||||||
self.slots[slot - 1] = resource
|
def serialize(self) -> dict:
|
||||||
super().assign_child_resource(resource, location=self.slot_locations[slot - 1])
|
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
|
||||||
|
|
||||||
|
|
||||||
class PRCXI9300Container(Plate):
|
class PRCXI9300Container(Container):
|
||||||
"""PRCXI 9300 的专用 Container 类,继承自 Plate,用于槽位定位和未知模块。
|
"""PRCXI 9300 的专用 Container 类,继承自 Plate,用于槽位定位和未知模块。
|
||||||
|
|
||||||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||||||
@@ -116,11 +200,10 @@ class PRCXI9300Container(Plate):
|
|||||||
size_y: float,
|
size_y: float,
|
||||||
size_z: float,
|
size_z: float,
|
||||||
category: str,
|
category: str,
|
||||||
ordering: collections.OrderedDict,
|
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
|
super().__init__(name, size_x, size_y, size_z, category=category, model=model)
|
||||||
self._unilabos_state = {}
|
self._unilabos_state = {}
|
||||||
|
|
||||||
def load_state(self, state: Dict[str, Any]) -> None:
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
@@ -248,14 +331,15 @@ class PRCXI9300TipRack(TipRack):
|
|||||||
if ordered_items is not None:
|
if ordered_items is not None:
|
||||||
items = ordered_items
|
items = ordered_items
|
||||||
elif ordering is not None:
|
elif ordering is not None:
|
||||||
# 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况)
|
# 检查 ordering 中的值类型来决定如何处理:
|
||||||
# 如果是字符串,说明这是位置名称,需要让 TipRack 自己创建 Tip 对象
|
# - 字符串值(从 JSON 反序列化): 只用键创建 ordering_param
|
||||||
# 我们只传递位置信息(键),不传递值,使用 ordering 参数
|
# - None 值(从第二次往返序列化): 同样只用键创建 ordering_param
|
||||||
if ordering and isinstance(next(iter(ordering.values()), None), str):
|
# - 对象值(已经是实际的 Resource 对象): 直接作为 ordered_items 使用
|
||||||
# ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict
|
first_val = next(iter(ordering.values()), None) if ordering else None
|
||||||
|
if not ordering or first_val is None or isinstance(first_val, str):
|
||||||
|
# ordering 的值是字符串或 None,只使用键(位置信息)创建新的 OrderedDict
|
||||||
# 传递 ordering 参数而不是 ordered_items,让 TipRack 自己创建 Tip 对象
|
# 传递 ordering 参数而不是 ordered_items,让 TipRack 自己创建 Tip 对象
|
||||||
items = None
|
items = None
|
||||||
# 使用 ordering 参数,只包含位置信息(键)
|
|
||||||
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
||||||
else:
|
else:
|
||||||
# ordering 的值已经是对象,可以直接使用
|
# ordering 的值已经是对象,可以直接使用
|
||||||
@@ -397,14 +481,15 @@ class PRCXI9300TubeRack(TubeRack):
|
|||||||
items_to_pass = ordered_items
|
items_to_pass = ordered_items
|
||||||
ordering_param = None
|
ordering_param = None
|
||||||
elif ordering is not None:
|
elif ordering is not None:
|
||||||
# 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况)
|
# 检查 ordering 中的值类型来决定如何处理:
|
||||||
# 如果是字符串,说明这是位置名称,需要让 TubeRack 自己创建 Tube 对象
|
# - 字符串值(从 JSON 反序列化): 只用键创建 ordering_param
|
||||||
# 我们只传递位置信息(键),不传递值,使用 ordering 参数
|
# - None 值(从第二次往返序列化): 同样只用键创建 ordering_param
|
||||||
if ordering and isinstance(next(iter(ordering.values()), None), str):
|
# - 对象值(已经是实际的 Resource 对象): 直接作为 ordered_items 使用
|
||||||
# ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict
|
first_val = next(iter(ordering.values()), None) if ordering else None
|
||||||
|
if not ordering or first_val is None or isinstance(first_val, str):
|
||||||
|
# ordering 的值是字符串或 None,只使用键(位置信息)创建新的 OrderedDict
|
||||||
# 传递 ordering 参数而不是 ordered_items,让 TubeRack 自己创建 Tube 对象
|
# 传递 ordering 参数而不是 ordered_items,让 TubeRack 自己创建 Tube 对象
|
||||||
items_to_pass = None
|
items_to_pass = None
|
||||||
# 使用 ordering 参数,只包含位置信息(键)
|
|
||||||
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
||||||
else:
|
else:
|
||||||
# ordering 的值已经是对象,可以直接使用
|
# ordering 的值已经是对象,可以直接使用
|
||||||
@@ -565,14 +650,14 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
tablets_info = []
|
tablets_info = []
|
||||||
count = 0
|
count = 0
|
||||||
for child in deck.children:
|
for child in deck.children:
|
||||||
if child.children:
|
# 如果放其他类型的物料,是不可以的
|
||||||
if "Material" in child.children[0]._unilabos_state:
|
if hasattr(child, "_unilabos_state") and "Material" in child._unilabos_state:
|
||||||
number = int(child.name.replace("T", ""))
|
number = int(child.name.replace("T", ""))
|
||||||
tablets_info.append(
|
tablets_info.append(
|
||||||
WorkTablets(
|
WorkTablets(
|
||||||
Number=number, Code=f"T{number}", Material=child.children[0]._unilabos_state["Material"]
|
Number=number, Code=f"T{number}", Material=child._unilabos_state["Material"]
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
if is_9320:
|
if is_9320:
|
||||||
print("当前设备是9320")
|
print("当前设备是9320")
|
||||||
# 始终初始化 step_mode 属性
|
# 始终初始化 step_mode 属性
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class Registry:
|
|||||||
test_resource_method_info = host_node_enhanced_info.get("action_methods", {}).get("test_resource", {})
|
test_resource_method_info = host_node_enhanced_info.get("action_methods", {}).get("test_resource", {})
|
||||||
test_resource_schema = self._generate_unilab_json_command_schema(
|
test_resource_schema = self._generate_unilab_json_command_schema(
|
||||||
test_resource_method_info.get("args", []),
|
test_resource_method_info.get("args", []),
|
||||||
"auto-test_resource",
|
"test_resource",
|
||||||
test_resource_method_info.get("return_annotation"),
|
test_resource_method_info.get("return_annotation"),
|
||||||
)
|
)
|
||||||
test_resource_schema["description"] = "用于测试物料、设备和样本。"
|
test_resource_schema["description"] = "用于测试物料、设备和样本。"
|
||||||
@@ -175,7 +175,8 @@ class Registry:
|
|||||||
"res_id": "unilabos_resources", # 将当前实验室的全部物料id作为下拉框可选择
|
"res_id": "unilabos_resources", # 将当前实验室的全部物料id作为下拉框可选择
|
||||||
"device_id": "unilabos_devices", # 将当前实验室的全部设备id作为下拉框可选择
|
"device_id": "unilabos_devices", # 将当前实验室的全部设备id作为下拉框可选择
|
||||||
"parent": "unilabos_nodes", # 将当前实验室的设备/物料作为下拉框可选择
|
"parent": "unilabos_nodes", # 将当前实验室的设备/物料作为下拉框可选择
|
||||||
"class_name": "unilabos_class",
|
"class_name": "unilabos_class", # 当前实验室物料的class name
|
||||||
|
"slot_on_deck": "unilabos_resource_slot:parent", # 勾选的parent的config中的sites的name,展示name,参数对应slot(index)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"test_latency": {
|
"test_latency": {
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import json
|
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
from pylabrobot.resources import Container
|
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):
|
class RegularContainer(Container):
|
||||||
@@ -16,12 +12,12 @@ class RegularContainer(Container):
|
|||||||
kwargs["size_y"] = 0
|
kwargs["size_y"] = 0
|
||||||
if "size_z" not in kwargs:
|
if "size_z" not in kwargs:
|
||||||
kwargs["size_z"] = 0
|
kwargs["size_z"] = 0
|
||||||
|
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
self.state = {}
|
|
||||||
super().__init__(*args, category="container", **kwargs)
|
super().__init__(*args, category="container", **kwargs)
|
||||||
|
|
||||||
def load_state(self, state: Dict[str, Any]):
|
def load_state(self, state: Dict[str, Any]):
|
||||||
self.state = state
|
super().load_state(state)
|
||||||
|
|
||||||
|
|
||||||
def get_regular_container(name="container"):
|
def get_regular_container(name="container"):
|
||||||
@@ -29,7 +25,6 @@ def get_regular_container(name="container"):
|
|||||||
r.category = "container"
|
r.category = "container"
|
||||||
return r
|
return r
|
||||||
|
|
||||||
#
|
|
||||||
# class RegularContainer(object):
|
# class RegularContainer(object):
|
||||||
# # 第一个参数必须是id传入
|
# # 第一个参数必须是id传入
|
||||||
# # noinspection PyShadowingBuiltins
|
# # noinspection PyShadowingBuiltins
|
||||||
@@ -89,4 +84,4 @@ def get_regular_container(name="container"):
|
|||||||
# return to_dict
|
# return to_dict
|
||||||
#
|
#
|
||||||
# def __str__(self):
|
# def __str__(self):
|
||||||
# return f"{self.id}"
|
# return f"{self.id}"
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ def canonicalize_nodes_data(
|
|||||||
if sample_id:
|
if sample_id:
|
||||||
logger.error(f"{node}的sample_id参数已弃用,sample_id: {sample_id}")
|
logger.error(f"{node}的sample_id参数已弃用,sample_id: {sample_id}")
|
||||||
for k in list(node.keys()):
|
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"]:
|
if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children", "pose", "extra"]:
|
||||||
v = node.pop(k)
|
v = node.pop(k)
|
||||||
node["config"][k] = v
|
node["config"][k] = v
|
||||||
if outer_host_node_id is not None:
|
if outer_host_node_id is not None:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
EXTRA_CLASS = "unilabos_resource_class"
|
EXTRA_CLASS = "unilabos_resource_class"
|
||||||
|
FRONTEND_POSE_EXTRA = "unilabos_frontend_pose_extra"
|
||||||
EXTRA_SAMPLE_UUID = "sample_uuid"
|
EXTRA_SAMPLE_UUID = "sample_uuid"
|
||||||
EXTRA_UNILABOS_SAMPLE_UUID = "unilabos_sample_uuid"
|
EXTRA_UNILABOS_SAMPLE_UUID = "unilabos_sample_uuid"
|
||||||
|
|
||||||
@@ -38,24 +39,52 @@ class LabSample(TypedDict):
|
|||||||
extra: Dict[str, Any]
|
extra: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictPositionSizeType(TypedDict):
|
||||||
|
depth: float
|
||||||
|
width: float
|
||||||
|
height: float
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictPositionSize(BaseModel):
|
class ResourceDictPositionSize(BaseModel):
|
||||||
depth: float = Field(description="Depth", default=0.0) # z
|
depth: float = Field(description="Depth", default=0.0) # z
|
||||||
width: float = Field(description="Width", default=0.0) # x
|
width: float = Field(description="Width", default=0.0) # x
|
||||||
height: float = Field(description="Height", default=0.0) # y
|
height: float = Field(description="Height", default=0.0) # y
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictPositionScaleType(TypedDict):
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
z: float
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictPositionScale(BaseModel):
|
class ResourceDictPositionScale(BaseModel):
|
||||||
x: float = Field(description="x scale", default=0.0)
|
x: float = Field(description="x scale", default=0.0)
|
||||||
y: float = Field(description="y scale", default=0.0)
|
y: float = Field(description="y scale", default=0.0)
|
||||||
z: float = Field(description="z scale", default=0.0)
|
z: float = Field(description="z scale", default=0.0)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictPositionObjectType(TypedDict):
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
z: float
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictPositionObject(BaseModel):
|
class ResourceDictPositionObject(BaseModel):
|
||||||
x: float = Field(description="X coordinate", default=0.0)
|
x: float = Field(description="X coordinate", default=0.0)
|
||||||
y: float = Field(description="Y coordinate", default=0.0)
|
y: float = Field(description="Y coordinate", default=0.0)
|
||||||
z: float = Field(description="Z coordinate", default=0.0)
|
z: float = Field(description="Z coordinate", default=0.0)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictPositionType(TypedDict):
|
||||||
|
size: ResourceDictPositionSizeType
|
||||||
|
scale: ResourceDictPositionScaleType
|
||||||
|
layout: Literal["2d", "x-y", "z-y", "x-z"]
|
||||||
|
position: ResourceDictPositionObjectType
|
||||||
|
position3d: ResourceDictPositionObjectType
|
||||||
|
rotation: ResourceDictPositionObjectType
|
||||||
|
cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"]
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictPosition(BaseModel):
|
class ResourceDictPosition(BaseModel):
|
||||||
size: ResourceDictPositionSize = Field(description="Resource size", default_factory=ResourceDictPositionSize)
|
size: ResourceDictPositionSize = Field(description="Resource size", default_factory=ResourceDictPositionSize)
|
||||||
scale: ResourceDictPositionScale = Field(description="Resource scale", default_factory=ResourceDictPositionScale)
|
scale: ResourceDictPositionScale = Field(description="Resource scale", default_factory=ResourceDictPositionScale)
|
||||||
@@ -72,6 +101,25 @@ class ResourceDictPosition(BaseModel):
|
|||||||
cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field(
|
cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field(
|
||||||
description="Cross section type", default="rectangle"
|
description="Cross section type", default="rectangle"
|
||||||
)
|
)
|
||||||
|
extra: Optional[Dict[str, Any]] = Field(description="Extra data", default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDictType(TypedDict):
|
||||||
|
id: str
|
||||||
|
uuid: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
resource_schema: Dict[str, Any]
|
||||||
|
model: Dict[str, Any]
|
||||||
|
icon: str
|
||||||
|
parent_uuid: Optional[str]
|
||||||
|
parent: Optional["ResourceDictType"]
|
||||||
|
type: Union[Literal["device"], str]
|
||||||
|
klass: str
|
||||||
|
pose: ResourceDictPositionType
|
||||||
|
config: Dict[str, Any]
|
||||||
|
data: Dict[str, Any]
|
||||||
|
extra: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
# 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化
|
# 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化
|
||||||
@@ -365,6 +413,15 @@ class ResourceTreeSet(object):
|
|||||||
"tip_spot": "tip_spot",
|
"tip_spot": "tip_spot",
|
||||||
"tube": "tube",
|
"tube": "tube",
|
||||||
"bottle_carrier": "bottle_carrier",
|
"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:
|
if source in replace_info:
|
||||||
return replace_info[source]
|
return replace_info[source]
|
||||||
@@ -408,6 +465,7 @@ class ResourceTreeSet(object):
|
|||||||
"position3d": raw_pos,
|
"position3d": raw_pos,
|
||||||
"rotation": d["rotation"],
|
"rotation": d["rotation"],
|
||||||
"cross_section_type": d.get("cross_section_type", "rectangle"),
|
"cross_section_type": d.get("cross_section_type", "rectangle"),
|
||||||
|
"extra": extra.get(FRONTEND_POSE_EXTRA)
|
||||||
}
|
}
|
||||||
|
|
||||||
# 先构建当前节点的字典(不包含children)
|
# 先构建当前节点的字典(不包含children)
|
||||||
@@ -493,6 +551,7 @@ class ResourceTreeSet(object):
|
|||||||
name_to_uuid[node.res_content.name] = node.res_content.uuid
|
name_to_uuid[node.res_content.name] = node.res_content.uuid
|
||||||
all_states[node.res_content.name] = node.res_content.data
|
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] = 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
|
name_to_extra[node.res_content.name][EXTRA_CLASS] = node.res_content.klass
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
collect_node_data(child, name_to_uuid, all_states, name_to_extra)
|
collect_node_data(child, name_to_uuid, all_states, name_to_extra)
|
||||||
@@ -561,7 +620,7 @@ class ResourceTreeSet(object):
|
|||||||
plr_resources.append(plr_resource)
|
plr_resources.append(plr_resource)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"转换 PLR 资源失败: {e}")
|
logger.error(f"转换 PLR 资源失败: {e} {str(plr_dict)[:1000]}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(f"堆栈: {traceback.format_exc()}")
|
logger.error(f"堆栈: {traceback.format_exc()}")
|
||||||
@@ -781,14 +840,27 @@ class ResourceTreeSet(object):
|
|||||||
f"从远端同步了 {added_count} 个物料子树"
|
f"从远端同步了 {added_count} 个物料子树"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 情况2: 二级是物料(不是 device)
|
# 二级物料已存在,比较三级子节点是否缺失
|
||||||
if remote_child_name not in local_children_map:
|
local_material = local_children_map[remote_child_name]
|
||||||
# 引入整个子树
|
local_material_children_map = {child.res_content.name: child for child in
|
||||||
remote_child.res_content.parent = local_device.res_content
|
local_material.children}
|
||||||
local_device.children.append(remote_child)
|
added_count = 0
|
||||||
logger.info(f"Device '{remote_root_id}': 从远端同步物料子树 '{remote_child_name}'")
|
for remote_sub in remote_child.children:
|
||||||
else:
|
remote_sub_name = remote_sub.res_content.name
|
||||||
logger.info(f"物料 '{remote_root_id}/{remote_child_name}' 已存在,跳过")
|
if remote_sub_name not in local_material_children_map:
|
||||||
|
remote_sub.res_content.parent = local_material.res_content
|
||||||
|
local_material.children.append(remote_sub)
|
||||||
|
added_count += 1
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"物料 '{remote_root_id}/{remote_child_name}/{remote_sub_name}' "
|
||||||
|
f"已存在,跳过"
|
||||||
|
)
|
||||||
|
if added_count > 0:
|
||||||
|
logger.info(
|
||||||
|
f"物料 '{remote_root_id}/{remote_child_name}': "
|
||||||
|
f"从远端同步了 {added_count} 个子物料"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# 情况1: 一级节点是物料(不是 device)
|
# 情况1: 一级节点是物料(不是 device)
|
||||||
# 检查是否已存在
|
# 检查是否已存在
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
}
|
}
|
||||||
res.response = json.dumps(final_response)
|
res.response = json.dumps(final_response)
|
||||||
# 如果driver自己就有assign的方法,那就使用driver自己的assign方法
|
# 如果driver自己就有assign的方法,那就使用driver自己的assign方法
|
||||||
if hasattr(self.driver_instance, "create_resource"):
|
if hasattr(self.driver_instance, "create_resource") and self.node_name != "host_node":
|
||||||
create_resource_func = getattr(self.driver_instance, "create_resource")
|
create_resource_func = getattr(self.driver_instance, "create_resource")
|
||||||
try:
|
try:
|
||||||
ret = create_resource_func(
|
ret = create_resource_func(
|
||||||
@@ -915,8 +915,24 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
if target_site is not None and sites is not None and site_names is not None:
|
if target_site is not None and sites is not None and site_names is not None:
|
||||||
site_index = sites.index(original_instance)
|
site_index = None
|
||||||
site_name = site_names[site_index]
|
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]
|
||||||
if site_name != target_site:
|
if site_name != target_site:
|
||||||
parent = self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
parent = self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
@@ -924,6 +940,14 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
parent_appended = True
|
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.location = plr_resource.location
|
||||||
original_instance.rotation = plr_resource.rotation
|
original_instance.rotation = plr_resource.rotation
|
||||||
original_instance.barcode = plr_resource.barcode
|
original_instance.barcode = plr_resource.barcode
|
||||||
@@ -984,7 +1008,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
].call_async(
|
].call_async(
|
||||||
r
|
r
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
self.lab_logger().info(f"确认资源云端 Add 结果: {response.response}")
|
self.lab_logger().trace(f"确认资源云端 Add 结果: {response.response}")
|
||||||
results.append(result)
|
results.append(result)
|
||||||
elif action == "update":
|
elif action == "update":
|
||||||
if tree_set is None:
|
if tree_set is None:
|
||||||
@@ -1010,7 +1034,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
].call_async(
|
].call_async(
|
||||||
r
|
r
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
self.lab_logger().info(f"确认资源云端 Update 结果: {response.response}")
|
self.lab_logger().trace(f"确认资源云端 Update 结果: {response.response}")
|
||||||
results.append(result)
|
results.append(result)
|
||||||
elif action == "remove":
|
elif action == "remove":
|
||||||
result = _handle_remove(resources_uuid)
|
result = _handle_remove(resources_uuid)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from unilabos.resources.resource_tracker import (
|
|||||||
ResourceTreeInstance,
|
ResourceTreeInstance,
|
||||||
RETURN_UNILABOS_SAMPLES,
|
RETURN_UNILABOS_SAMPLES,
|
||||||
JSON_UNILABOS_PARAM,
|
JSON_UNILABOS_PARAM,
|
||||||
PARAM_SAMPLE_UUIDS,
|
PARAM_SAMPLE_UUIDS, SampleUUIDsType, LabSample,
|
||||||
)
|
)
|
||||||
from unilabos.ros.initialize_device import initialize_device_from_dict
|
from unilabos.ros.initialize_device import initialize_device_from_dict
|
||||||
from unilabos.ros.msgs.message_converter import (
|
from unilabos.ros.msgs.message_converter import (
|
||||||
@@ -65,6 +65,7 @@ class DeviceActionStatus:
|
|||||||
class TestResourceReturn(TypedDict):
|
class TestResourceReturn(TypedDict):
|
||||||
resources: List[List[ResourceDict]]
|
resources: List[List[ResourceDict]]
|
||||||
devices: List[Dict[str, Any]]
|
devices: List[Dict[str, Any]]
|
||||||
|
unilabos_samples: List[LabSample]
|
||||||
|
|
||||||
|
|
||||||
class TestLatencyReturn(TypedDict):
|
class TestLatencyReturn(TypedDict):
|
||||||
@@ -1194,7 +1195,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点")
|
self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点")
|
||||||
# 还需要加入到资源图中,暂不实现,考虑资源图新的获取方式
|
# 还需要加入到资源图中,暂不实现,考虑资源图新的获取方式
|
||||||
response.response = json.dumps(uuid_mapping)
|
response.response = json.dumps(uuid_mapping)
|
||||||
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
|
self.lab_logger().info(f"[Host Node-Resource] Resource tree update completed, success: {success}")
|
||||||
|
|
||||||
async def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response):
|
async def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response):
|
||||||
"""
|
"""
|
||||||
@@ -1582,6 +1583,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
|
|
||||||
def test_resource(
|
def test_resource(
|
||||||
self,
|
self,
|
||||||
|
sample_uuids: SampleUUIDsType,
|
||||||
resource: ResourceSlot = None,
|
resource: ResourceSlot = None,
|
||||||
resources: List[ResourceSlot] = None,
|
resources: List[ResourceSlot] = None,
|
||||||
device: DeviceSlot = None,
|
device: DeviceSlot = None,
|
||||||
@@ -1596,6 +1598,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
return {
|
return {
|
||||||
"resources": ResourceTreeSet.from_plr_resources([resource, *resources], known_newly_created=True).dump(),
|
"resources": ResourceTreeSet.from_plr_resources([resource, *resources], known_newly_created=True).dump(),
|
||||||
"devices": [device, *devices],
|
"devices": [device, *devices],
|
||||||
|
"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 handle_pong_response(self, pong_data: dict):
|
def handle_pong_response(self, pong_data: dict):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@
|
|||||||
res_id: plate_slot_{slot}
|
res_id: plate_slot_{slot}
|
||||||
device_id: /PRCXI
|
device_id: /PRCXI
|
||||||
class_name: PRCXI_BioER_96_wellplate
|
class_name: PRCXI_BioER_96_wellplate
|
||||||
parent: /PRCXI/PRCXI_Deck/T{slot}
|
parent: /PRCXI/PRCXI_Deck
|
||||||
slot_on_deck: "{slot}"
|
slot_on_deck: "{slot}"
|
||||||
- 输出端口: labware(用于连接 set_liquid_from_plate)
|
- 输出端口: labware(用于连接 set_liquid_from_plate)
|
||||||
- 控制流: create_resource 之间通过 ready 端口串联
|
- 控制流: create_resource 之间通过 ready 端口串联
|
||||||
@@ -122,7 +122,7 @@ NODE_TYPE_DEFAULT = "ILab" # 所有节点的默认类型
|
|||||||
# create_resource 节点默认参数
|
# create_resource 节点默认参数
|
||||||
CREATE_RESOURCE_DEFAULTS = {
|
CREATE_RESOURCE_DEFAULTS = {
|
||||||
"device_id": "/PRCXI",
|
"device_id": "/PRCXI",
|
||||||
"parent_template": "/PRCXI/PRCXI_Deck/T{slot}", # {slot} 会被替换为实际的 slot 值
|
"parent_template": "/PRCXI/PRCXI_Deck",
|
||||||
"class_name": "PRCXI_BioER_96_wellplate",
|
"class_name": "PRCXI_BioER_96_wellplate",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,7 +424,7 @@ def build_protocol_graph(
|
|||||||
"res_id": res_id,
|
"res_id": res_id,
|
||||||
"device_id": CREATE_RESOURCE_DEFAULTS["device_id"],
|
"device_id": CREATE_RESOURCE_DEFAULTS["device_id"],
|
||||||
"class_name": lw_type,
|
"class_name": lw_type,
|
||||||
"parent": CREATE_RESOURCE_DEFAULTS["parent_template"].format(slot=slot),
|
"parent": CREATE_RESOURCE_DEFAULTS["parent_template"],
|
||||||
"bind_locations": {"x": 0.0, "y": 0.0, "z": 0.0},
|
"bind_locations": {"x": 0.0, "y": 0.0, "z": 0.0},
|
||||||
"slot_on_deck": slot,
|
"slot_on_deck": slot,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||||
<package format="3">
|
<package format="3">
|
||||||
<name>unilabos_msgs</name>
|
<name>unilabos_msgs</name>
|
||||||
<version>0.10.17</version>
|
<version>0.10.18</version>
|
||||||
<description>ROS2 Messages package for unilabos devices</description>
|
<description>ROS2 Messages package for unilabos devices</description>
|
||||||
<maintainer email="changjh@pku.edu.cn">Junhan Chang</maintainer>
|
<maintainer email="changjh@pku.edu.cn">Junhan Chang</maintainer>
|
||||||
<maintainer email="18435084+Xuwznln@users.noreply.github.com">Xuwznln</maintainer>
|
<maintainer email="18435084+Xuwznln@users.noreply.github.com">Xuwznln</maintainer>
|
||||||
|
|||||||
Reference in New Issue
Block a user