Compare commits

..

13 Commits

Author SHA1 Message Date
Junhan Chang
c2dfe689aa fix: Protocol node resource run (#65)
* stir和adjustph的中的bug修不好

* fix sub-resource query in protocol node compiling

* add resource placeholder to vessels

* add the rest yaml

* Update work_station.yaml

---------

Co-authored-by: KCFeng425 <2100011801@stu.pku.edu.cn>
2025-07-19 04:19:57 +08:00
Xuwznln
4cd40865b4 fix resource check serialize 2025-07-19 02:24:00 +08:00
Xuwznln
fd3dbcf1ff fix devices 2025-07-19 01:56:41 +08:00
Xuwznln
ebe9e1b1f8 yaml dump支持ordered dict,支持config_info 2025-07-19 01:54:53 +08:00
Xuwznln
862f250e49 update set tip rack 2025-07-19 01:31:33 +08:00
Xuwznln
73f33c82db update registry version & category 2025-07-19 01:29:59 +08:00
Xuwznln
58bf6496b6 bump version 2025-07-19 01:02:39 +08:00
Xuwznln
2b7da0e396 SET TIP RACK 2025-07-19 00:54:24 +08:00
Guangxin Zhang
dd89d00588 Update prcxi.py 2025-07-19 00:22:00 +08:00
Guangxin Zhang
9327d59915 Merge branch 'dev' of https://github.com/dptech-corp/Uni-Lab-OS into dev 2025-07-19 00:10:07 +08:00
Guangxin Zhang
736f55765b Update 2025-07-19 00:09:59 +08:00
Xuwznln
9eb1f9823e registry fix 2025-07-19 00:03:02 +08:00
Xuwznln
c61c4aae59 registry fix 2025-07-18 23:59:59 +08:00
50 changed files with 167996 additions and 1366 deletions

2
.gitignore vendored
View File

@@ -241,4 +241,4 @@ unilabos/device_mesh/view_robot.rviz
# Certs
**/.certs
local_test2.py
ros-humble-unilabos-msgs-0.9.12-h6403a04_5.tar.bz2
ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2

View File

@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name
# Currently, you need to install the `unilabos_msgs` package
# You can download the system-specific package from the Release page
conda install ros-humble-unilabos-msgs-0.9.12-xxxxx.tar.bz2
conda install ros-humble-unilabos-msgs-0.9.13-xxxxx.tar.bz2
# Install PyLabRobot and other prerequisites
git clone https://github.com/PyLabRobot/pylabrobot plr_repo

View File

@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名
# 现阶段,需要安装 `unilabos_msgs` 包
# 可以前往 Release 页面下载系统对应的包进行安装
conda install ros-humble-unilabos-msgs-0.9.12-xxxxx.tar.bz2
conda install ros-humble-unilabos-msgs-0.9.13-xxxxx.tar.bz2
# 安装PyLabRobot等前置
git clone https://github.com/PyLabRobot/pylabrobot plr_repo

13533
deck.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
package:
name: ros-humble-unilabos-msgs
version: 0.9.12
version: 0.9.13
source:
path: ../../unilabos_msgs
folder: ros-humble-unilabos-msgs/src/work

View File

@@ -1,6 +1,6 @@
package:
name: unilabos
version: "0.9.12"
version: "0.9.13"
source:
path: ../..

View File

@@ -4,7 +4,7 @@ package_name = 'unilabos'
setup(
name=package_name,
version='0.9.12',
version='0.9.13',
packages=find_packages(),
include_package_data=True,
install_requires=['setuptools'],

View File

@@ -1,6 +1,6 @@
import networkx as nx
import logging
from typing import List, Dict, Any
from typing import List, Dict, Any, Union
from .pump_protocol import generate_pump_protocol_with_rinsing
logger = logging.getLogger(__name__)
@@ -216,7 +216,7 @@ def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume
def generate_adjust_ph_protocol(
G: nx.DiGraph,
vessel: dict, # 🔧 修改:从字符串改为字典类型
vessel:Union[dict,str], # 🔧 修改:从字符串改为字典类型
ph_value: float,
reagent: str,
**kwargs
@@ -235,8 +235,17 @@ def generate_adjust_ph_protocol(
List[Dict[str, Any]]: 动作序列
"""
# 🔧 核心修改从字典中提取容器ID
vessel_id = vessel["id"]
# 统一处理vessel参数
if isinstance(vessel, dict):
vessel_id = list(vessel.values())[0].get("id", "")
vessel_data = vessel.get("data", {})
else:
vessel_id = str(vessel)
vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
if not vessel_id:
debug_print(f"❌ vessel 参数无效必须包含id字段或直接提供容器ID. vessel: {vessel}")
raise ValueError("vessel 参数无效必须包含id字段或直接提供容器ID")
debug_print("=" * 60)
debug_print("🧪 开始生成pH调节协议")

View File

@@ -22,7 +22,7 @@ def parse_time_input(time_input: Union[str, float]) -> float:
"""
if isinstance(time_input, (int, float)):
debug_print(f"⏱️ 时间输入为数字: {time_input}s ✨")
return float(time_input)
return float(time_input) # 🔧 确保返回float
if not time_input or not str(time_input).strip():
debug_print(f"⚠️ 时间输入为空,使用默认值: 180s (3分钟) 🕐")
@@ -48,7 +48,7 @@ def parse_time_input(time_input: Union[str, float]) -> float:
try:
value = float(time_str)
debug_print(f"✅ 时间解析成功: {time_str}{value}s无单位默认秒")
return value
return float(value) # 🔧 确保返回float
except ValueError:
debug_print(f"❌ 无法解析时间: '{time_str}'使用默认值180s (3分钟) 😅")
return 180.0
@@ -70,7 +70,7 @@ def parse_time_input(time_input: Union[str, float]) -> float:
time_sec = value # 已经是s
debug_print(f"🕐 时间转换: {value}s → {time_sec}s (已是秒) ⏰")
return time_sec
return float(time_sec) # 🔧 确保返回float
def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]:
"""
@@ -389,12 +389,12 @@ def generate_evaporate_protocol(
"device_id": rotavap_device,
"action_name": "evaporate",
"action_kwargs": {
"vessel": target_vessel, # 使用 target_vessel
"pressure": pressure,
"temp": temp,
"time": final_time,
"stir_speed": stir_speed,
"solvent": solvent
"vessel": target_vessel,
"pressure": float(pressure),
"temp": float(temp),
"time": float(final_time), # 🔧 强制转换为float类型
"stir_speed": float(stir_speed),
"solvent": str(solvent)
}
}
action_sequence.append(evaporate_action)

View File

@@ -170,33 +170,94 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name}")
return vessel_name
# 第二步:通过模糊匹配
# 第二步:通过模糊匹配节点ID和名称
debug_print(" 🔍 步骤2: 模糊名称匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
node_name = G.nodes[node_id].get('name', '').lower()
if solvent.lower() in node_id.lower() or solvent.lower() in node_name:
debug_print(f" 🎉 通过模糊匹配找到容器: {node_id}")
debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} (名称: {node_name})")
return node_id
# 第三步:通过液体类型匹配
debug_print(" 🧪 步骤3: 液体类型匹配...")
# 第三步:通过配置中的试剂信息匹配
debug_print(" 🧪 步骤3: 配置试剂信息匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
# 检查 config 中的 reagent 字段
node_config = G.nodes[node_id].get('config', {})
config_reagent = node_config.get('reagent', '').lower()
if config_reagent and solvent.lower() == config_reagent:
debug_print(f" 🎉 通过config.reagent匹配找到容器: {node_id} (试剂: {config_reagent}) ✨")
return node_id
# 第四步:通过数据中的试剂信息匹配
debug_print(" 🧪 步骤4: 数据试剂信息匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
vessel_data = G.nodes[node_id].get('data', {})
liquids = vessel_data.get('liquid', [])
# 检查 data 中的 reagent_name 字段
reagent_name = vessel_data.get('reagent_name', '').lower()
if reagent_name and solvent.lower() == reagent_name:
debug_print(f" 🎉 通过data.reagent_name匹配找到容器: {node_id} (试剂: {reagent_name}) ✨")
return node_id
# 检查 data 中的液体信息
liquids = vessel_data.get('liquid', [])
for liquid in liquids:
if isinstance(liquid, dict):
liquid_type = (liquid.get('liquid_type') or liquid.get('name', '')).lower()
reagent_name = vessel_data.get('reagent_name', '').lower()
if solvent.lower() in liquid_type or solvent.lower() in reagent_name:
debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id}")
if solvent.lower() in liquid_type:
debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} (液体类型: {liquid_type})")
return node_id
# 第五步:部分匹配(如果前面都没找到)
debug_print(" 🔍 步骤5: 部分匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
node_config = G.nodes[node_id].get('config', {})
node_data = G.nodes[node_id].get('data', {})
node_name = G.nodes[node_id].get('name', '').lower()
config_reagent = node_config.get('reagent', '').lower()
data_reagent = node_data.get('reagent_name', '').lower()
# 检查是否包含溶剂名称
if (solvent.lower() in config_reagent or
solvent.lower() in data_reagent or
solvent.lower() in node_name or
solvent.lower() in node_id.lower()):
debug_print(f" 🎉 通过部分匹配找到容器: {node_id}")
debug_print(f" - 节点名称: {node_name}")
debug_print(f" - 配置试剂: {config_reagent}")
debug_print(f" - 数据试剂: {data_reagent}")
return node_id
# 调试信息:列出所有容器
debug_print(" 🔎 调试信息:列出所有容器...")
container_list = []
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
node_config = G.nodes[node_id].get('config', {})
node_data = G.nodes[node_id].get('data', {})
node_name = G.nodes[node_id].get('name', '')
container_info = {
'id': node_id,
'name': node_name,
'config_reagent': node_config.get('reagent', ''),
'data_reagent': node_data.get('reagent_name', '')
}
container_list.append(container_info)
debug_print(f" - 容器: {node_id}, 名称: {node_name}, config试剂: {node_config.get('reagent', '')}, data试剂: {node_data.get('reagent_name', '')}")
debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭")
debug_print(f"🔍 查找的溶剂: '{solvent}' (小写: '{solvent.lower()}')")
debug_print(f"📊 总共发现 {len(container_list)} 个容器")
raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器")

View File

@@ -149,7 +149,7 @@ def extract_vessel_id(vessel: Union[str, dict]) -> str:
str: vessel_id
"""
if isinstance(vessel, dict):
vessel_id = vessel.get("id", "")
vessel_id = list(vessel.values())[0].get("id", "")
debug_print(f"🔧 从vessel字典提取ID: {vessel_id}")
return vessel_id
elif isinstance(vessel, str):

View File

@@ -165,7 +165,7 @@ def extract_vessel_id(vessel: Union[str, dict]) -> str:
str: vessel_id
"""
if isinstance(vessel, dict):
vessel_id = vessel.get("id", "")
vessel_id = list(vessel.values())[0].get("id", "")
debug_print(f"🔧 从vessel字典提取ID: {vessel_id}")
return vessel_id
elif isinstance(vessel, str):

View File

@@ -590,14 +590,14 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
else:
# 首先应该对任务分组然后每次1个/8个进行操作处理
if len(use_channels) == 1 and self.backend.num_channels == 1:
tip = []
for _ in range(len(use_channels)):
tip.extend(next(self.current_tip))
await self.pick_up_tips(tip)
for _ in range(len(waste_liquid)):
for _ in range(len(sources)):
tip = []
for __ in range(len(use_channels)):
tip.extend(next(self.current_tip))
await self.pick_up_tips(tip)
await self.aspirate(
resources=sources,
resources=[sources[_]],
vols=[vols[_]],
use_channels=use_channels,
flow_rates=[flow_rates[0]] if flow_rates else None,
@@ -608,8 +608,9 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
)
if delays is not None:
await self.custom_delay(seconds=delays[0])
await self.dispense(
resources=[waste_liquid[_]],
resources=[waste_liquid],
vols=[vols[_]],
use_channels=use_channels,
flow_rates=[flow_rates[1]] if flow_rates else None,
@@ -618,7 +619,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
liquid_height=[liquid_height[1]] if liquid_height else None,
spread=spread,
)
await self.discard_tips()
await self.discard_tips()
elif len(use_channels) == 8 and self.backend.num_channels == 8:
tip = []
for _ in range(len(use_channels)):
@@ -706,9 +707,9 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
mix_liquid_height: Optional[float] = None,
none_keys: List[str] = [],
):
"""A complete *add* (aspirate reagent → dispense into targets) operation."""
# """A complete *add* (aspirate reagent → dispense into targets) operation."""
try:
# # try:
if is_96_well:
pass # This mode is not verified.
else:
@@ -720,9 +721,9 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
tip = []
for _ in range(len(use_channels)):
tip.extend(next(self.current_tip))
await self.pick_up_tips(tip)
await self.pick_up_tips(tip)
for _ in range(len(targets)):
print(use_channels, reagent_sources)
await self.aspirate(
resources=reagent_sources,
vols=[asp_vols[_]],
@@ -733,6 +734,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
blow_out_air_volume=[blow_out_air_volume[0]] if blow_out_air_volume else None,
spread=spread,
)
if delays is not None:
await self.custom_delay(seconds=delays[0])
await self.dispense(
@@ -749,7 +751,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
if delays is not None:
await self.custom_delay(seconds=delays[1])
await self.mix(
targets=targets[_],
targets=[targets[_]],
mix_time=mix_time,
mix_vol=mix_vol,
offsets=offsets if offsets else None,
@@ -759,7 +761,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
if delays is not None:
await self.custom_delay(seconds=delays[1])
await self.touch_tip(targets[_])
await self.discard_tips()
elif len(use_channels) == 8:
# 对于8个的情况需要判断此时任务是不是能被8通道移液站来成功处理
@@ -825,9 +827,9 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
await self.discard_tips()
except Exception as e:
traceback.print_exc()
raise RuntimeError(f"Liquid addition failed: {e}") from e
# except Exception as e:
# traceback.print_exc()
# raise RuntimeError(f"Liquid addition failed: {e}") from e
# ---------------------------------------------------------------
# TRANSFER LIQUID ------------------------------------------------
@@ -872,126 +874,126 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
Set *True* to use the 96channel head.
"""
try:
if is_96_well:
pass # This mode is not verified.
else:
if len(asp_vols) != len(targets):
raise ValueError(f"Length of `asp_vols` {len(asp_vols)} must match `targets` {len(targets)}.")
# 首先应该对任务分组然后每次1个/8个进行操作处理
if len(use_channels) == 1:
if is_96_well:
pass # This mode is not verified.
else:
if len(asp_vols) != len(targets):
raise ValueError(f"Length of `asp_vols` {len(asp_vols)} must match `targets` {len(targets)}.")
# 首先应该对任务分组然后每次1个/8个进行操作处理
if len(use_channels) == 1:
for _ in range(len(targets)):
tip = []
for ___ in range(len(use_channels)):
tip.extend(next(self.current_tip))
await self.pick_up_tips(tip)
await self.aspirate(
resources=[sources[_]],
vols=[asp_vols[_]],
use_channels=use_channels,
flow_rates=[asp_flow_rates[0]] if asp_flow_rates else None,
offsets=[offsets[0]] if offsets else None,
liquid_height=[liquid_height[0]] if liquid_height else None,
blow_out_air_volume=[blow_out_air_volume[0]] if blow_out_air_volume else None,
spread=spread,
)
if delays is not None:
await self.custom_delay(seconds=delays[0])
await self.dispense(
resources=[targets[_]],
vols=[dis_vols[_]],
use_channels=use_channels,
flow_rates=[dis_flow_rates[1]] if dis_flow_rates else None,
offsets=[offsets[1]] if offsets else None,
blow_out_air_volume=[blow_out_air_volume[1]] if blow_out_air_volume else None,
liquid_height=[liquid_height[1]] if liquid_height else None,
spread=spread,
)
if delays is not None:
await self.custom_delay(seconds=delays[1])
await self.mix(
targets=[targets[_]],
mix_time=mix_times,
mix_vol=mix_vol,
offsets=offsets if offsets else None,
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
mix_rate=mix_rate if mix_rate else None,
)
if delays is not None:
await self.custom_delay(seconds=delays[1])
await self.touch_tip(targets[_])
await self.discard_tips()
elif len(use_channels) == 8:
# 对于8个的情况需要判断此时任务是不是能被8通道移液站来成功处理
if len(targets) % 8 != 0:
raise ValueError(f"Length of `targets` {len(targets)} must be a multiple of 8 for 8-channel mode.")
# 8个8个来取任务序列
for i in range(0, len(targets), 8):
# 取出8个任务
tip = []
for _ in range(len(use_channels)):
tip.extend(next(self.current_tip))
await self.pick_up_tips(tip)
current_targets = targets[i:i + 8]
current_reagent_sources = sources[i:i + 8]
current_asp_vols = asp_vols[i:i + 8]
current_dis_vols = dis_vols[i:i + 8]
current_asp_flow_rates = asp_flow_rates[i:i + 8]
current_asp_offset = offsets[i:i + 8] if offsets else [None] * 8
current_dis_offset = offsets[-i*8-8:len(offsets)-i*8] if offsets else [None] * 8
current_asp_liquid_height = liquid_height[i:i + 8] if liquid_height else [None] * 8
current_dis_liquid_height = liquid_height[-i*8-8:len(liquid_height)-i*8] if liquid_height else [None] * 8
current_asp_blow_out_air_volume = blow_out_air_volume[i:i + 8] if blow_out_air_volume else [None] * 8
current_dis_blow_out_air_volume = blow_out_air_volume[-i*8-8:len(blow_out_air_volume)-i*8] if blow_out_air_volume else [None] * 8
current_dis_flow_rates = dis_flow_rates[i:i + 8] if dis_flow_rates else [None] * 8
for _ in range(len(targets)):
await self.aspirate(
resources=sources,
vols=[asp_vols[_]],
use_channels=use_channels,
flow_rates=[asp_flow_rates[0]] if asp_flow_rates else None,
offsets=[offsets[0]] if offsets else None,
liquid_height=[liquid_height[0]] if liquid_height else None,
blow_out_air_volume=[blow_out_air_volume[0]] if blow_out_air_volume else None,
spread=spread,
)
if delays is not None:
await self.custom_delay(seconds=delays[0])
await self.dispense(
resources=[targets[_]],
vols=[dis_vols[_]],
use_channels=use_channels,
flow_rates=[dis_flow_rates[1]] if dis_flow_rates else None,
offsets=[offsets[1]] if offsets else None,
blow_out_air_volume=[blow_out_air_volume[1]] if blow_out_air_volume else None,
liquid_height=[liquid_height[1]] if liquid_height else None,
spread=spread,
)
if delays is not None:
await self.custom_delay(seconds=delays[1])
await self.mix(
targets=targets[_],
mix_time=mix_times,
mix_vol=mix_vol,
offsets=offsets if offsets else None,
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
mix_rate=mix_rate if mix_rate else None,
)
if delays is not None:
await self.custom_delay(seconds=delays[1])
await self.touch_tip(targets[_])
await self.aspirate(
resources=current_reagent_sources,
vols=current_asp_vols,
use_channels=use_channels,
flow_rates=current_asp_flow_rates,
offsets=current_asp_offset,
blow_out_air_volume=current_asp_blow_out_air_volume,
liquid_height=current_asp_liquid_height,
spread=spread,
)
if delays is not None:
await self.custom_delay(seconds=delays[0])
await self.dispense(
resources=current_targets,
vols=current_dis_vols,
use_channels=use_channels,
flow_rates=current_dis_flow_rates,
offsets=current_dis_offset,
blow_out_air_volume=current_dis_blow_out_air_volume,
liquid_height=current_dis_liquid_height,
spread=spread,
)
if delays is not None:
await self.custom_delay(seconds=delays[1])
await self.mix(
targets=current_targets,
mix_time=mix_times,
mix_vol=mix_vol,
offsets=offsets if offsets else None,
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
mix_rate=mix_rate if mix_rate else None,
)
if delays is not None:
await self.custom_delay(seconds=delays[1])
await self.touch_tip(current_targets)
await self.discard_tips()
elif len(use_channels) == 8:
# 对于8个的情况需要判断此时任务是不是能被8通道移液站来成功处理
if len(targets) % 8 != 0:
raise ValueError(f"Length of `targets` {len(targets)} must be a multiple of 8 for 8-channel mode.")
# 8个8个来取任务序列
for i in range(0, len(targets), 8):
# 取出8个任务
tip = []
for _ in range(len(use_channels)):
tip.extend(next(self.current_tip))
await self.pick_up_tips(tip)
current_targets = targets[i:i + 8]
current_reagent_sources = sources[i:i + 8]
current_asp_vols = asp_vols[i:i + 8]
current_dis_vols = dis_vols[i:i + 8]
current_asp_flow_rates = asp_flow_rates[i:i + 8]
current_asp_offset = offsets[i:i + 8] if offsets else [None] * 8
current_dis_offset = offsets[-i*8-8:len(offsets)-i*8] if offsets else [None] * 8
current_asp_liquid_height = liquid_height[i:i + 8] if liquid_height else [None] * 8
current_dis_liquid_height = liquid_height[-i*8-8:len(liquid_height)-i*8] if liquid_height else [None] * 8
current_asp_blow_out_air_volume = blow_out_air_volume[i:i + 8] if blow_out_air_volume else [None] * 8
current_dis_blow_out_air_volume = blow_out_air_volume[-i*8-8:len(blow_out_air_volume)-i*8] if blow_out_air_volume else [None] * 8
current_dis_flow_rates = dis_flow_rates[i:i + 8] if dis_flow_rates else [None] * 8
await self.aspirate(
resources=current_reagent_sources,
vols=current_asp_vols,
use_channels=use_channels,
flow_rates=current_asp_flow_rates,
offsets=current_asp_offset,
blow_out_air_volume=current_asp_blow_out_air_volume,
liquid_height=current_asp_liquid_height,
spread=spread,
)
if delays is not None:
await self.custom_delay(seconds=delays[0])
await self.dispense(
resources=current_targets,
vols=current_dis_vols,
use_channels=use_channels,
flow_rates=current_dis_flow_rates,
offsets=current_dis_offset,
blow_out_air_volume=current_dis_blow_out_air_volume,
liquid_height=current_dis_liquid_height,
spread=spread,
)
if delays is not None:
await self.custom_delay(seconds=delays[1])
await self.mix(
targets=current_targets,
mix_time=mix_times,
mix_vol=mix_vol,
offsets=offsets if offsets else None,
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
mix_rate=mix_rate if mix_rate else None,
)
if delays is not None:
await self.custom_delay(seconds=delays[1])
await self.touch_tip(current_targets)
await self.discard_tips()
except Exception as e:
traceback.print_exc()
raise RuntimeError(f"Liquid addition failed: {e}") from e
# except Exception as e:
# traceback.print_exc()
# raise RuntimeError(f"Liquid addition failed: {e}") from e
# ---------------------------------------------------------------

View File

@@ -433,10 +433,14 @@ class PRCXI9300Backend(LiquidHandlerBackend):
plate_indexes = []
for op in ops:
plate = op.resource.parent.parent
plate = op.resource.parent
deck = plate.parent
plate_index = deck.children.index(plate)
print(f"Plate index: {plate_index}, Plate name: {plate.name}")
print(f"Number of children in deck: {len(deck.children)}")
plate_indexes.append(plate_index)
if len(set(plate_indexes)) != 1:
raise ValueError("All pickups must be from the same plate. Found different plates: " + str(plate_indexes))
@@ -447,7 +451,8 @@ class PRCXI9300Backend(LiquidHandlerBackend):
tip_columns.append(tipspot_index // 8)
if len(set(tip_columns)) != 1:
raise ValueError("All pickups must be from the same tip column. Found different columns: " + str(tip_columns))
# print('111'*99)
# print(plate_indexes[0])
PlateNo = plate_indexes[0] + 1
hole_col = tip_columns[0] + 1
@@ -485,7 +490,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
plate_indexes = []
for op in ops:
plate = op.resource.parent.parent
plate = op.resource.parent
deck = plate.parent
plate_index = deck.children.index(plate)
plate_indexes.append(plate_index)
@@ -532,11 +537,10 @@ class PRCXI9300Backend(LiquidHandlerBackend):
"""Mix liquid in the specified resources."""
plate_indexes = []
for op in targets:
deck = op.parent.parent.parent
plate = op.parent.parent
deck = op.parent.parent
plate = op.parent
plate_index = deck.children.index(plate)
plate_indexes.append(plate_index)
@@ -577,7 +581,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
plate_indexes = []
for op in ops:
plate = op.resource.parent.parent
plate = op.resource.parent
deck = plate.parent
plate_index = deck.children.index(plate)
plate_indexes.append(plate_index)
@@ -617,7 +621,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
plate_indexes = []
for op in ops:
plate = op.resource.parent.parent
plate = op.resource.parent
deck = plate.parent
plate_index = deck.children.index(plate)
plate_indexes.append(plate_index)
@@ -1244,7 +1248,7 @@ if __name__ == "__main__":
handler = PRCXI9300Handler(deck=deck, host="10.181.102.13", port=9999,
timeout=10.0, setup=False, debug=False,
matrix_id="fd383e6d-2d0e-40b5-9c01-1b2870b1f1b1",
channel_num=1)
channel_num=1, axis="Right") # Initialize the handler with the deck and host settings
handler.set_tiprack([plate8]) # Set the tip rack for the handler
asyncio.run(handler.setup()) # Initialize the handler and setup the connection
@@ -1262,32 +1266,78 @@ if __name__ == "__main__":
print(plate11.get_well(0).tracker.get_used_volume())
asyncio.run(handler.create_protocol(protocol_name="Test Protocol")) # Initialize the backend and setup the connection
print(plate8.children[3])
asyncio.run(handler.pick_up_tips([plate8.children[3]],[0]))
asyncio.run(handler.aspirate([plate11.children[0]],[10], [0]))
asyncio.run(handler.dispense([plate1.children[3]],[10],[0]))
asyncio.run(handler.mix([plate1.children[3]], mix_time=3, mix_vol=5, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
asyncio.run(handler.discard_tips())
# asyncio.run(handler.pick_up_tips([plate8.children[8]],[0]))
# print(plate8.children[8])
# # asyncio.run(handler.run_protocol())
# asyncio.run(handler.aspirate([plate11.children[0]],[10], [0]))
# print(plate11.children[0])
# # asyncio.run(handler.run_protocol())
# asyncio.run(handler.dispense([plate1.children[0]],[10],[0]))
# print(plate1.children[0])
# # asyncio.run(handler.run_protocol())
# asyncio.run(handler.mix([plate1.children[0]], mix_time=3, mix_vol=5, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
# print(plate1.children[0])
# asyncio.run(handler.discard_tips())
# asyncio.run(handler.add_liquid(
# asp_vols=[10]*2,
# dis_vols=[10]*2,
# reagent_sources=[plate11.children[0]],
# targets=plate11.children[-2:],
# use_channels=[0],
# flow_rates=[None] * 4,
# offsets=[Coordinate(0, 0, 0)] * 4,
# liquid_height=[None] * 2,
# blow_out_air_volume=[None] * 2,
# delays=None,
# mix_time=3,
# mix_vol=5,
# spread="wide",
# ))
# asyncio.run(handler.transfer_liquid(
# asp_vols=[10]*2,
# dis_vols=[10]*2,
# sources=plate11.children[:2],
# targets=plate11.children[-2:],
# use_channels=[0],
# offsets=[Coordinate(0, 0, 0)] * 4,
# liquid_height=[None] * 2,
# blow_out_air_volume=[None] * 2,
# delays=None,
# mix_times=3,
# mix_vol=5,
# spread="wide",
# tip_racks=[plate8]
# ))
asyncio.run(handler.remove_liquid(
vols=[10]*2,
sources=plate11.children[:2],
waste_liquid=plate11.children[43],
use_channels=[0],
offsets=[Coordinate(0, 0, 0)] * 4,
liquid_height=[None] * 2,
blow_out_air_volume=[None] * 2,
delays=None,
spread="wide"
))
asyncio.run(handler.run_protocol())
# asyncio.run(handler.discard_tips())
# asyncio.run(handler.mix(well_containers.children[:8
# ], mix_time=3, mix_vol=50, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
#print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
# asyncio.run(handler.add_liquid(
# asp_vols=[100]*16,
# dis_vols=[100]*16,
# reagent_sources=final_plate_2.children[-16:],
# targets=final_plate_2.children[:16],
# use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
# flow_rates=[None] * 32,
# offsets=[Coordinate(0, 0, 0)] * 32,
# liquid_height=[None] * 16,
# blow_out_air_volume=[None] * 16,
# delays=None,
# mix_time=3,
# mix_vol=50,
# spread="wide",
# ))
# asyncio.run(handler.remove_liquid(
# vols=[100]*16,

View File

@@ -70,11 +70,32 @@ class VirtualMultiwayValve:
command: 目标位置 (0-8) 或位置字符串
0: transfer pump位置
1-8: 其他设备位置
'default': 默认位置0号位
"""
try:
# 如果是字符串形式的位置,先转换为数字
# 🔧 处理特殊字符串命令
if isinstance(command, str):
pos = int(command)
command_lower = command.lower().strip()
# 处理特殊命令
if command_lower in ['default', 'pump', 'transfer_pump', 'home']:
pos = 0 # 默认位置为0号位transfer pump
self.logger.info(f"🔧 特殊命令 '{command}' 映射到位置 {pos}")
elif command_lower in ['open']:
pos = 0 # open命令也映射到0号位
self.logger.info(f"🔧 OPEN命令映射到位置 {pos}")
elif command_lower in ['close', 'closed']:
# 关闭命令保持当前位置
pos = self._current_position
self.logger.info(f"🔧 CLOSE命令保持当前位置 {pos}")
else:
# 尝试转换为数字
try:
pos = int(command)
except ValueError:
error_msg = f"无法识别的命令: '{command}'"
self.logger.error(f"{error_msg}")
raise ValueError(error_msg)
else:
pos = int(command)

View File

@@ -88,6 +88,20 @@ class VirtualRotavap:
) -> bool:
"""Execute evaporate action - 简化版 🌪️"""
# 🔧 新增确保time参数是数值类型
if isinstance(time, str):
try:
time = float(time)
except ValueError:
self.logger.error(f"❌ 无法转换时间参数 '{time}' 为数值使用默认值180.0秒")
time = 180.0
elif not isinstance(time, (int, float)):
self.logger.error(f"❌ 时间参数类型无效: {type(time)}使用默认值180.0秒")
time = 180.0
# 确保time是float类型
time = float(time)
# 🔧 简化处理如果vessel就是设备自己直接操作
if vessel == self.device_id:
debug_print(f"🎯 在设备 {self.device_id} 上直接执行蒸发操作")
@@ -158,7 +172,7 @@ class VirtualRotavap:
})
return False
# 开始蒸发
# 开始蒸发 - 🔧 现在time已经确保是float类型
self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️")
self.data.update({

View File

@@ -1,4 +1,6 @@
serial:
category:
- serial
class:
action_value_mappings:
auto-handle_serial_request:
@@ -76,6 +78,7 @@ serial:
module: unilabos.ros.nodes.presets.serial_node:ROS2SerialNode
status_types: {}
type: ros2
config_info: []
description: Serial communication interface, used when sharing same serial port
for multiple devices
handles: []
@@ -100,4 +103,4 @@ serial:
properties: {}
required: []
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
camera:
category:
- camera
class:
action_value_mappings:
auto-destroy_node:
@@ -44,6 +46,7 @@ camera:
module: unilabos.ros.nodes.presets.camera:VideoPublisher
status_types: {}
type: ros2
config_info: []
description: VideoPublisher摄像头设备节点用于实时视频采集和流媒体发布。该设备通过OpenCV连接本地摄像头如USB摄像头、内置摄像头等定时采集视频帧并将其转换为ROS2的sensor_msgs/Image消息格式发布到视频话题。主要用于实验室自动化系统中的视觉监控、图像分析、实时观察等应用场景。支持可配置的摄像头索引、发布频率等参数。
handles: []
icon: ''
@@ -67,4 +70,4 @@ camera:
properties: {}
required: []
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
hplc.agilent:
category:
- characterization_optic
class:
action_value_mappings:
auto-check_status:
@@ -176,6 +178,7 @@ hplc.agilent:
status_text: str
success: bool
type: python
config_info: []
description: 安捷伦高效液相色谱HPLC分析设备用于复杂化合物的分离、检测和定量分析。该设备通过UI自动化技术控制安捷伦ChemStation软件实现全自动的样品分析流程。具备序列启动、设备状态监控、数据文件提取、结果处理等功能。支持多样品批量处理和实时状态反馈适用于药物分析、环境检测、食品安全、化学研究等需要高精度色谱分析的实验室应用。
handles: []
icon: ''
@@ -215,8 +218,10 @@ hplc.agilent:
- finish_status
- data_file
type: object
version: 0.0.1
version: 1.0.0
raman_home_made:
category:
- characterization_optic
class:
action_value_mappings:
auto-ccd_time:
@@ -378,6 +383,7 @@ raman_home_made:
module: unilabos.devices.raman_uv.home_made_raman:RamanObj
status_types: {}
type: python
config_info: []
description: 拉曼光谱分析设备用于物质的分子结构和化学成分表征。该设备集成激光器和CCD检测器通过串口通信控制激光功率和光谱采集。具备背景扣除、多次平均、自动数据处理等功能支持高精度的拉曼光谱测量。适用于材料表征、化学分析、质量控制、研究开发等需要分子指纹识别和结构分析的实验应用。
handles: []
icon: ''
@@ -402,4 +408,4 @@ raman_home_made:
properties: {}
required: []
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,10 +1,13 @@
hotel.thermo_orbitor_rs2_hotel:
category:
- hotel
class:
action_value_mappings: {}
module: unilabos.devices.resource_container.container:HotelContainer
status_types:
rotation: String
type: python
config_info: []
description: Thermo Orbitor RS2 Hotel容器设备用于实验室样品的存储和管理。该设备通过HotelContainer类实现容器的旋转控制和状态监控主要用于存储实验样品、试剂瓶或其他实验器具支持旋转功能以便于样品的自动化存取。适用于需要有序存储和快速访问大量样品的实验室自动化场景。
handles: []
icon: ''
@@ -29,4 +32,4 @@ hotel.thermo_orbitor_rs2_hotel:
model:
mesh: thermo_orbitor_rs2_hotel
type: device
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
laiyu_add_solid:
category:
- laiyu_add_solid
class:
action_value_mappings:
add_powder_tube:
@@ -355,6 +357,7 @@ laiyu_add_solid:
status_types:
status: str
type: python
config_info: []
description: 来渝固体粉末自动分装设备用于实验室化学试剂的精确称量和分装。该设备通过Modbus RTU协议与控制系统通信集成了精密天平、三轴运动平台、粉筒管理系统等组件。支持多种粉末试剂的自动拿取、精确称量、定点分装和归位操作。具备高精度称量、位置控制和批量处理能力适用于化学合成、药物研发、材料制备等需要精确固体试剂配制的实验室应用场景。
handles: []
icon: ''
@@ -379,4 +382,4 @@ laiyu_add_solid:
required:
- status
type: object
version: 0.0.1
version: 1.0.0

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,6 @@
mock_chiller:
category:
- mock_devices
class:
action_value_mappings:
emergency_stop:
@@ -325,6 +327,7 @@ mock_chiller:
target_temperature: float
vessel: str
type: python
config_info: []
description: Mock Chiller Device
handles: []
icon: ''
@@ -364,8 +367,10 @@ mock_chiller:
- purpose
- status_info
type: object
version: 0.0.1
version: 1.0.0
mock_filter:
category:
- mock_devices
class:
action_value_mappings:
auto-emergency_stop:
@@ -743,6 +748,7 @@ mock_filter:
temperature: float
vessel: str
type: python
config_info: []
description: Mock Filter Device
handles: []
icon: ''
@@ -803,8 +809,10 @@ mock_filter:
- target_volume
- status_info
type: object
version: 0.0.1
version: 1.0.0
mock_heater:
category:
- mock_devices
class:
action_value_mappings:
auto-set_heating_power:
@@ -1361,6 +1369,7 @@ mock_heater:
target_temperature: float
vessel: str
type: python
config_info: []
description: Mock Heater Device
handles: []
icon: ''
@@ -1409,8 +1418,10 @@ mock_heater:
- stir_speed
- status_info
type: object
version: 0.0.1
version: 1.0.0
mock_pump:
category:
- mock_devices
class:
action_value_mappings:
auto-emergency_stop:
@@ -1887,6 +1898,7 @@ mock_pump:
transfer_time: float
transfer_volume: float
type: python
config_info: []
description: Mock Pump Device
handles: []
icon: ''
@@ -1968,8 +1980,10 @@ mock_pump:
- time_remaining
- status_info
type: object
version: 0.0.1
version: 1.0.0
mock_rotavap:
category:
- mock_devices
class:
action_value_mappings:
auto-emergency_stop:
@@ -2296,6 +2310,7 @@ mock_rotavap:
temperature: float
vacuum_level: float
type: python
config_info: []
description: Mock Rotavap Device
handles: []
icon: ''
@@ -2341,8 +2356,10 @@ mock_rotavap:
- target_temperature
- status_info
type: object
version: 0.0.1
version: 1.0.0
mock_separator:
category:
- mock_devices
class:
action_value_mappings:
separate:
@@ -3247,6 +3264,7 @@ mock_separator:
valve_state: str
waste_phase_to_vessel: str
type: python
config_info: []
description: Simplified Mock Separator Device
handles: []
icon: ''
@@ -3325,8 +3343,10 @@ mock_separator:
- time_remaining
- status_info
type: object
version: 0.0.1
version: 1.0.0
mock_solenoid_valve:
category:
- mock_devices
class:
action_value_mappings:
auto-is_closed:
@@ -3481,6 +3501,7 @@ mock_solenoid_valve:
status: str
valve_status: str
type: python
config_info: []
description: Mock Solenoid Valve Device
handles: []
icon: ''
@@ -3502,8 +3523,10 @@ mock_solenoid_valve:
- status
- valve_status
type: object
version: 0.0.1
version: 1.0.0
mock_stirrer:
category:
- mock_devices
class:
action_value_mappings:
auto-emergency_stop:
@@ -3749,6 +3772,7 @@ mock_stirrer:
target_temperature: float
temperature: float
type: python
config_info: []
description: Mock Stirrer Device
handles: []
icon: ''
@@ -3797,8 +3821,10 @@ mock_stirrer:
- max_temperature
- status_info
type: object
version: 0.0.1
version: 1.0.0
mock_stirrer_new:
category:
- mock_devices
class:
action_value_mappings:
start_stir:
@@ -4279,6 +4305,7 @@ mock_stirrer_new:
target_stir_speed: float
vessel: str
type: python
config_info: []
description: Mock Stirrer Device (Copy Version)
handles: []
icon: ''
@@ -4327,8 +4354,10 @@ mock_stirrer_new:
- progress
- status_info
type: object
version: 0.0.1
version: 1.0.0
mock_vacuum:
category:
- mock_devices
class:
action_value_mappings:
auto-emergency_stop:
@@ -4610,6 +4639,7 @@ mock_vacuum:
target_vacuum: float
vacuum_level: float
type: python
config_info: []
description: Mock Vacuum Pump Device
handles: []
icon: ''
@@ -4652,4 +4682,4 @@ mock_vacuum:
- max_pump_speed
- status_info
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
moveit.arm_slider:
category:
- moveit_config
class:
action_value_mappings:
auto-check_tf_update_actions:
@@ -321,6 +323,7 @@ moveit.arm_slider:
module: unilabos.devices.ros_dev.moveit_interface:MoveitInterface
status_types: {}
type: python
config_info: []
description: 机械臂与滑块运动系统基于MoveIt2运动规划框架的多自由度机械臂控制设备。该系统集成机械臂和线性滑块通过ROS2和MoveIt2实现精确的轨迹规划和协调运动控制。支持笛卡尔空间和关节空间的运动规划、碰撞检测、逆运动学求解等功能。适用于复杂的pick-and-place操作、精密装配、多工位协作等需要高精度多轴协调运动的实验室自动化应用。
handles: []
icon: ''
@@ -346,8 +349,10 @@ moveit.arm_slider:
model:
mesh: arm_slider
type: device
version: 0.0.1
version: 1.0.0
moveit.toyo_xyz:
category:
- moveit_config
class:
action_value_mappings:
auto-check_tf_update_actions:
@@ -670,6 +675,7 @@ moveit.toyo_xyz:
module: unilabos.devices.ros_dev.moveit_interface:MoveitInterface
status_types: {}
type: python
config_info: []
description: 东洋XYZ三轴运动平台基于MoveIt2运动规划框架的精密定位设备。该设备通过ROS2和MoveIt2实现三维空间的精确运动控制支持复杂轨迹规划、多点定位、速度控制等功能。具备高精度定位、平稳运动、实时轨迹监控等特性。适用于精密加工、样品定位、检测扫描、自动化装配等需要高精度三维运动控制的实验室和工业应用场景。
handles: []
icon: ''
@@ -695,4 +701,4 @@ moveit.toyo_xyz:
model:
mesh: toyo_xyz
type: device
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
rotavap.one:
category:
- organic_miscellaneous
class:
action_value_mappings:
auto-cmd_write:
@@ -140,6 +142,7 @@ rotavap.one:
module: unilabos.devices.rotavap.rotavap_one:RotavapOne
status_types: {}
type: python
config_info: []
description: 旋转蒸发仪设备,用于有机化学实验中的溶剂回收和浓缩操作。该设备通过串口通信控制,集成旋转和真空泵功能,支持定时控制和自动化操作。具备旋转速度调节、真空度控制、温度管理等功能,实现高效的溶剂蒸发和回收。适用于有机合成、天然产物提取、药物制备等需要溶剂去除和浓缩的实验室应用场景。
handles: []
icon: ''
@@ -158,8 +161,10 @@ rotavap.one:
properties: {}
required: []
type: object
version: 0.0.1
version: 1.0.0
separator.homemade:
category:
- organic_miscellaneous
class:
action_value_mappings:
auto-read_sensor_loop:
@@ -446,6 +451,7 @@ separator.homemade:
module: unilabos.devices.separator.homemade_grbl_conductivity:SeparatorController
status_types: {}
type: python
config_info: []
description: 液-液分离器设备基于自制Grbl控制器的自动化分离系统。该设备集成搅拌、沉降、阀门控制和电导率传感器通过串口通信实现精确的分离操作控制。支持自动搅拌、分层沉降、基于传感器反馈的智能分液等功能。适用于有机化学中的萃取分离、相分离、液-液提取等需要精确分离控制的实验应用。
handles: []
icon: ''
@@ -470,4 +476,4 @@ separator.homemade:
properties: {}
required: []
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
solenoid_valve:
category:
- pump_and_valve
class:
action_value_mappings:
auto-close:
@@ -170,6 +172,7 @@ solenoid_valve:
status: str
valve_position: str
type: python
config_info: []
description: 电磁阀控制设备,用于精确的流体路径控制和开关操作。该设备通过串口通信控制电磁阀的开关状态,支持远程操作和状态监测。具备快速响应、可靠密封、状态反馈等特性,广泛应用于流体输送、样品进样、路径切换等需要精确流体控制的实验室自动化应用。
handles: []
icon: ''
@@ -191,8 +194,10 @@ solenoid_valve:
- status
- valve_position
type: object
version: 0.0.1
version: 1.0.0
solenoid_valve.mock:
category:
- pump_and_valve
class:
action_value_mappings:
auto-is_closed:
@@ -328,6 +333,7 @@ solenoid_valve.mock:
status: str
valve_position: str
type: python
config_info: []
description: 模拟电磁阀设备,用于系统测试和开发调试。该设备模拟真实电磁阀的开关操作和状态变化,提供与实际设备相同的控制接口和反馈机制。支持流体路径的虚拟控制,便于在没有实际硬件的情况下进行流体系统的集成测试和算法验证。适用于系统开发、流程调试和培训演示等场景。
handles:
- data_type: fluid
@@ -359,8 +365,10 @@ solenoid_valve.mock:
- status
- valve_position
type: object
version: 0.0.1
version: 1.0.0
syringe_pump_with_valve.runze:
category:
- pump_and_valve
class:
action_value_mappings:
auto-close:
@@ -754,6 +762,7 @@ syringe_pump_with_valve.runze:
velocity_grade: String
velocity_init: String
type: python
config_info: []
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
handles: []
icon: ''
@@ -804,4 +813,4 @@ syringe_pump_with_valve.runze:
- position
- plunger_position
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
agv.SEER:
category:
- robot_agv
class:
action_value_mappings:
auto-send:
@@ -82,6 +84,7 @@ agv.SEER:
pose: list
status: str
type: python
config_info: []
description: SEER AGV自动导引车设备用于实验室内物料和设备的自主移动运输。该AGV通过TCP socket与导航系统通信具备精确的定位和路径规划能力。支持实时位置监控、状态查询和导航任务执行可在预设的实验室环境中自主移动至指定位置。适用于样品运输、设备转移、多工位协作等实验室自动化物流场景。
handles: []
icon: ''
@@ -103,4 +106,4 @@ agv.SEER:
- pose
- status
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
robotic_arm.UR:
category:
- robot_arm
class:
action_value_mappings:
auto-arm_init:
@@ -140,6 +142,7 @@ robotic_arm.UR:
gripper_pose: float
gripper_status: str
type: python
config_info: []
description: Universal Robots机械臂设备用于实验室精密操作和自动化作业。该设备集成了UR机械臂本体、Robotiq夹爪和RTDE通信接口支持六自由度精确运动控制和力觉反馈。具备实时位置监控、状态反馈、轨迹规划等功能可执行复杂的多点位运动任务。适用于样品抓取、精密装配、实验器具操作等需要高精度和高重复性的实验室自动化场景。
handles: []
icon: ''
@@ -170,4 +173,4 @@ robotic_arm.UR:
- arm_status
- gripper_status
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
gripper.misumi_rz:
category:
- robot_gripper
class:
action_value_mappings:
auto-data_loop:
@@ -417,6 +419,7 @@ gripper.misumi_rz:
status_types:
status: str
type: python
config_info: []
description: Misumi RZ系列电子夹爪设备集成旋转和抓取双重功能的精密夹爪系统。该设备通过Modbus RTU协议与控制系统通信支持位置、速度、力矩的精确控制。具备高精度的位置反馈、实时状态监控和故障检测功能。适用于需要精密抓取和旋转操作的实验室自动化场景如样品管理、精密装配、器件操作等应用。
handles: []
icon: ''
@@ -444,8 +447,10 @@ gripper.misumi_rz:
required:
- status
type: object
version: 0.0.1
version: 1.0.0
gripper.mock:
category:
- robot_gripper
class:
action_value_mappings:
auto-edit_id:
@@ -563,6 +568,7 @@ gripper.mock:
torque: float
velocity: float
type: python
config_info: []
description: 模拟夹爪设备,用于系统测试和开发调试。该设备模拟真实夹爪的位置、速度、力矩等物理特性,支持虚拟的抓取和移动操作。提供与真实夹爪相同的接口和状态反馈,便于在没有实际硬件的情况下进行系统集成测试和算法验证。适用于软件开发、系统调试和培训演示等场景。
handles: []
icon: ''
@@ -587,4 +593,4 @@ gripper.mock:
- torque
- status
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
linear_motion.grbl:
category:
- robot_linear_motion
class:
action_value_mappings:
auto-initialize:
@@ -430,6 +432,7 @@ linear_motion.grbl:
spindle_speed: float
status: str
type: python
config_info: []
description: Grbl数控机床CNC设备用于实验室精密加工和三轴定位操作。该设备基于Grbl固件通过串口通信控制步进电机实现X、Y、Z三轴的精确运动。支持绝对定位、轨迹规划、主轴控制和实时状态监控。具备安全限位保护和运动平滑控制功能。适用于精密钻孔、铣削、雕刻、样品制备等需要高精度定位和加工的实验室应用场景。
handles: []
icon: ''
@@ -466,8 +469,10 @@ linear_motion.grbl:
- position
- spindle_speed
type: object
version: 0.0.1
version: 1.0.0
motor.iCL42:
category:
- robot_linear_motion
class:
action_value_mappings:
auto-execute_run_motor:
@@ -604,6 +609,7 @@ motor.iCL42:
motor_position: int
success: bool
type: python
config_info: []
description: iCL42步进电机驱动器用于实验室设备的精密线性运动控制。该设备通过串口通信控制iCL42型步进电机驱动器支持多种运动模式和精确的位置、速度控制。具备位置反馈、运行状态监控和故障检测功能。适用于自动进样器、样品传送、精密定位平台等需要准确线性运动控制的实验室自动化设备。
handles: []
icon: ''
@@ -631,4 +637,4 @@ motor.iCL42:
- is_executing_run
- success
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
lh_joint_publisher:
category:
- sim_nodes
class:
action_value_mappings:
auto-check_tf_update_actions:
@@ -285,6 +287,7 @@ lh_joint_publisher:
module: unilabos.devices.ros_dev.liquid_handler_joint_publisher:LiquidHandlerJointPublisher
status_types: {}
type: ros2
config_info: []
description: 液体处理器关节发布器用于ROS2仿真系统中的液体处理设备运动控制。该节点通过发布关节状态驱动仿真模型中的机械臂运动支持三维坐标到关节空间的逆运动学转换、多关节协调控制、资源跟踪和TF变换。具备精确的位置控制、速度调节、pick-and-place操作等功能。适用于液体处理系统的虚拟仿真、运动规划验证、系统集成测试等应用场景。
handles: []
icon: ''
@@ -309,4 +312,4 @@ lh_joint_publisher:
properties: {}
required: []
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
chiller:
category:
- temperature
class:
action_value_mappings:
auto-build_modbus_frame:
@@ -156,6 +158,7 @@ chiller:
module: unilabos.devices.temperature.chiller:Chiller
status_types: {}
type: python
config_info: []
description: 实验室制冷设备用于精确的温度控制和冷却操作。该设备通过Modbus RTU协议与控制系统通信支持精确的温度设定和监控。具备快速降温、恒温控制和温度保持功能广泛应用于需要低温环境的化学反应、样品保存、结晶操作等实验场景。提供稳定可靠的冷却性能确保实验过程的温度精度。
handles: []
icon: ''
@@ -174,8 +177,10 @@ chiller:
properties: {}
required: []
type: object
version: 0.0.1
version: 1.0.0
heaterstirrer.dalong:
category:
- temperature
class:
action_value_mappings:
auto-close:
@@ -523,6 +528,7 @@ heaterstirrer.dalong:
temp_target: float
temp_warning: float
type: python
config_info: []
description: 大龙加热搅拌器,集成加热和搅拌双重功能的实验室设备。该设备通过串口通信控制,支持精确的温度调节、搅拌速度控制和安全保护功能。具备实时温度监测、目标温度设定、安全温度报警等特性。适用于化学合成、样品制备、反应控制等需要同时进行加热和搅拌的实验操作,提供稳定均匀的反应环境。
handles: []
icon: ''
@@ -559,8 +565,10 @@ heaterstirrer.dalong:
- temp_warning
- temp_target
type: object
version: 0.0.1
version: 1.0.0
tempsensor:
category:
- temperature
class:
action_value_mappings:
auto-build_modbus_request:
@@ -719,6 +727,7 @@ tempsensor:
status_types:
value: float
type: python
config_info: []
description: 高精度温度传感器设备用于实验室环境和设备的温度监测。该传感器通过Modbus RTU协议与控制系统通信提供实时准确的温度数据。具备高精度测量、报警温度设定、数据稳定性好等特点。适用于反应器监控、环境温度监测、设备保护等需要精确温度测量的实验场景为实验安全和数据可靠性提供保障。
handles: []
icon: ''
@@ -746,4 +755,4 @@ tempsensor:
required:
- value
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
gas_source.mock:
category:
- vacuum_and_purge
class:
action_value_mappings:
auto-is_closed:
@@ -149,6 +151,7 @@ gas_source.mock:
status_types:
status: str
type: python
config_info: []
description: 模拟气体源设备,用于系统测试和开发调试。该设备模拟真实气体源的开关控制和状态监测功能,支持气体供应的启停操作。提供与真实气体源相同的接口和状态反馈,便于在没有实际硬件的情况下进行系统集成测试和算法验证。适用于气路系统调试、软件开发和实验流程验证等场景。
handles:
- data_key: fluid_out
@@ -173,8 +176,10 @@ gas_source.mock:
required:
- status
type: object
version: 0.0.1
version: 1.0.0
vacuum_pump.mock:
category:
- vacuum_and_purge
class:
action_value_mappings:
auto-is_closed:
@@ -325,6 +330,7 @@ vacuum_pump.mock:
status_types:
status: str
type: python
config_info: []
description: 模拟真空泵设备,用于系统测试和开发调试。该设备模拟真实真空泵的抽气功能和状态控制,支持真空系统的启停操作和状态监测。提供与真实真空泵相同的接口和控制逻辑,便于在没有实际硬件的情况下进行真空系统的集成测试。适用于真空工艺调试、软件开发和实验流程验证等场景。
handles:
- data_key: fluid_in
@@ -349,4 +355,4 @@ vacuum_pump.mock:
required:
- status
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
virtual_centrifuge:
category:
- virtual_device
class:
action_value_mappings:
auto-cleanup:
@@ -222,6 +224,7 @@ virtual_centrifuge:
target_temp: float
time_remaining: float
type: python
config_info: []
description: Virtual Centrifuge for CentrifugeProtocol Testing
handles:
- data_key: vessel
@@ -282,8 +285,10 @@ virtual_centrifuge:
- progress
- message
type: object
version: 0.0.1
version: 1.0.0
virtual_column:
category:
- virtual_device
class:
action_value_mappings:
auto-cleanup:
@@ -609,6 +614,7 @@ virtual_column:
progress: float
status: str
type: python
config_info: []
description: Virtual Column Chromatography Device for RunColumn Protocol Testing
handles:
- data_key: from_vessel
@@ -674,8 +680,10 @@ virtual_column:
- current_phase
- final_volume
type: object
version: 0.0.1
version: 1.0.0
virtual_filter:
category:
- virtual_device
class:
action_value_mappings:
auto-cleanup:
@@ -1002,6 +1010,7 @@ virtual_filter:
progress: float
status: str
type: python
config_info: []
description: Virtual Filter for FilterProtocol Testing
handles:
- data_key: vessel_in
@@ -1069,8 +1078,10 @@ virtual_filter:
- max_stir_speed
- max_volume
type: object
version: 0.0.1
version: 1.0.0
virtual_gas_source:
category:
- virtual_device
class:
action_value_mappings:
auto-cleanup:
@@ -1261,6 +1272,7 @@ virtual_gas_source:
status_types:
status: str
type: python
config_info: []
description: Virtual gas source
handles:
- data_key: fluid_out
@@ -1288,8 +1300,10 @@ virtual_gas_source:
required:
- status
type: object
version: 0.0.1
version: 1.0.0
virtual_heatchill:
category:
- virtual_device
class:
action_value_mappings:
auto-cleanup:
@@ -1803,6 +1817,7 @@ virtual_heatchill:
status: str
stir_speed: float
type: python
config_info: []
description: Virtual HeatChill for HeatChillProtocol Testing
handles:
- data_key: vessel
@@ -1854,8 +1869,10 @@ virtual_heatchill:
- min_temp
- max_stir_speed
type: object
version: 0.0.1
version: 1.0.0
virtual_multiway_valve:
category:
- virtual_device
class:
action_value_mappings:
auto-close:
@@ -2155,6 +2172,7 @@ virtual_multiway_valve:
valve_position: int
valve_state: str
type: python
config_info: []
description: Virtual 8-Way Valve for flow direction control
handles:
- data_key: fluid_in
@@ -2275,8 +2293,10 @@ virtual_multiway_valve:
- flow_path
- info
type: object
version: 0.0.1
version: 1.0.0
virtual_pump:
category:
- virtual_device
class:
action_value_mappings:
auto-clean_vessel:
@@ -2741,6 +2761,7 @@ virtual_pump:
transferred_volume: float
valve_position: int
type: python
config_info: []
description: Virtual Pump for PumpTransferProtocol Testing
handles:
- data_key: fluid_in
@@ -2794,8 +2815,10 @@ virtual_pump:
- transferred_volume
- current_status
type: object
version: 0.0.1
version: 1.0.0
virtual_rotavap:
category:
- virtual_device
class:
action_value_mappings:
auto-cleanup:
@@ -3048,6 +3071,7 @@ virtual_rotavap:
status: str
vacuum_pressure: float
type: python
config_info: []
description: Virtual Rotary Evaporator for EvaporateProtocol Testing
handles:
- data_key: vessel_in
@@ -3121,8 +3145,10 @@ virtual_rotavap:
- max_rotation_speed
- remaining_time
type: object
version: 0.0.1
version: 1.0.0
virtual_separator:
category:
- virtual_device
class:
action_value_mappings:
auto-cleanup:
@@ -3938,6 +3964,7 @@ virtual_separator:
stir_speed: float
volume: float
type: python
config_info: []
description: Virtual Separator for SeparateProtocol Testing
handles:
- data_key: from_vessel
@@ -4013,8 +4040,10 @@ virtual_separator:
- progress
- message
type: object
version: 0.0.1
version: 1.0.0
virtual_solenoid_valve:
category:
- virtual_device
class:
action_value_mappings:
auto-cleanup:
@@ -4277,6 +4306,7 @@ virtual_solenoid_valve:
valve_position: str
valve_state: str
type: python
config_info: []
description: Virtual Solenoid Valve for simple on/off flow control
handles:
- data_key: fluid_port_in
@@ -4324,8 +4354,10 @@ virtual_solenoid_valve:
- valve_position
- state
type: object
version: 0.0.1
version: 1.0.0
virtual_solid_dispenser:
category:
- virtual_device
class:
action_value_mappings:
add_solid:
@@ -4548,7 +4580,6 @@ virtual_solid_dispenser:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
@@ -4556,30 +4587,6 @@ virtual_solid_dispenser:
title: cleanup参数
type: object
type: UniLabJsonCommandAsync
auto-find_solid_reagent_bottle:
feedback: {}
goal: {}
goal_default:
reagent_name: null
handles: []
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
reagent_name:
type: string
required:
- reagent_name
type: object
result: {}
required:
- goal
title: find_solid_reagent_bottle参数
type: object
type: UniLabJsonCommand
auto-initialize:
feedback: {}
goal: {}
@@ -4592,7 +4599,6 @@ virtual_solid_dispenser:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
@@ -4600,62 +4606,14 @@ virtual_solid_dispenser:
title: initialize参数
type: object
type: UniLabJsonCommandAsync
auto-parse_mass_string:
feedback: {}
goal: {}
goal_default:
mass_str: null
handles: []
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
mass_str:
type: string
required:
- mass_str
type: object
result: {}
required:
- goal
title: parse_mass_string参数
type: object
type: UniLabJsonCommand
auto-parse_mol_string:
feedback: {}
goal: {}
goal_default:
mol_str: null
handles: []
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
mol_str:
type: string
required:
- mol_str
type: object
result: {}
required:
- goal
title: parse_mol_string参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser
status_types:
current_reagent: str
device_info: dict
dispensed_amount: float
status: str
total_operations: int
type: python
config_info: []
description: Virtual Solid Dispenser for Add Protocol Testing - supports mass and
molar additions
handles:
@@ -4683,14 +4641,18 @@ virtual_solid_dispenser:
type: object
device_id:
type: string
max_capacity:
default: 100.0
type: number
precision:
default: 0.001
type: number
required: []
type: object
data:
properties:
current_reagent:
type: string
device_info:
type: object
dispensed_amount:
type: number
status:
@@ -4702,10 +4664,11 @@ virtual_solid_dispenser:
- current_reagent
- dispensed_amount
- total_operations
- device_info
type: object
version: 0.0.1
version: 1.0.0
virtual_stirrer:
category:
- virtual_device
class:
action_value_mappings:
auto-cleanup:
@@ -5219,6 +5182,7 @@ virtual_stirrer:
remaining_time: float
status: str
type: python
config_info: []
description: Virtual Stirrer for StirProtocol Testing
handles:
- data_key: vessel
@@ -5270,8 +5234,10 @@ virtual_stirrer:
- min_speed
- device_info
type: object
version: 0.0.1
version: 1.0.0
virtual_transfer_pump:
category:
- virtual_device
class:
action_value_mappings:
auto-aspirate:
@@ -5728,6 +5694,7 @@ virtual_transfer_pump:
status: str
transfer_rate: float
type: python
config_info: []
description: Virtual Transfer Pump for TransferProtocol Testing (Syringe-style)
handles:
- data_key: fluid_port
@@ -5773,8 +5740,10 @@ virtual_transfer_pump:
- remaining_capacity
- pump_info
type: object
version: 0.0.1
version: 1.0.0
virtual_vacuum_pump:
category:
- virtual_device
class:
action_value_mappings:
auto-cleanup:
@@ -5965,6 +5934,7 @@ virtual_vacuum_pump:
status_types:
status: str
type: python
config_info: []
description: Virtual vacuum pump
handles:
- data_key: fluid_in
@@ -5992,4 +5962,4 @@ virtual_vacuum_pump:
required:
- status
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
workstation:
category:
- work_station
class:
action_value_mappings:
AGVTransferProtocol:
@@ -243,8 +245,13 @@ workstation:
feedback: {}
goal:
amount: amount
equiv: equiv
event: event
mass: mass
mol: mol
purpose: purpose
rate_spec: rate_spec
ratio: ratio
reagent: reagent
stir: stir
stir_speed: stir_speed
@@ -468,6 +475,11 @@ workstation:
ph_value: ph_value
reagent: reagent
vessel: vessel
volume: volume
stir: stir
stir_speed: stir_speed
stir_time: stir_time
settling_time: settling_time
goal_default:
ph_value: 0.0
reagent: ''
@@ -491,6 +503,11 @@ workstation:
z: 0.0
sample_id: ''
type: ''
volume: 0.0
stir: false
stir_speed: 300.0
stir_time: 60.0
settling_time: 30.0
handles:
input:
- data_key: vessel
@@ -509,6 +526,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -603,6 +622,21 @@ workstation:
- data
title: Resource
type: object
volume:
type: number
description: 'Volume of the solution to adjust pH'
stir:
type: boolean
description: "是否启用搅拌"
stir_speed:
type: number
description: "搅拌速度(RPM)"
stir_time:
type: number
description: "搅拌时间(秒)"
settling_time:
type: number
description: "pH平衡时间(秒)"
required:
- vessel
- ph_value
@@ -672,6 +706,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -851,6 +887,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -1058,6 +1096,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -1189,6 +1229,10 @@ workstation:
feedback: {}
goal:
amount: amount
event: event
mass: mass
mol: mol
reagent: reagent
solvent: solvent
stir_speed: stir_speed
temp: temp
@@ -1244,6 +1288,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -1427,6 +1473,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -1583,6 +1631,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -1776,6 +1826,8 @@ workstation:
data_type: resource
handler_key: vessel_out
label: Evaporation Vessel
placeholder_keys:
vessel: unilabos_nodes
result: {}
schema:
description: ''
@@ -2012,6 +2064,9 @@ workstation:
data_type: resource
handler_key: filtrate_out
label: Filtrate Vessel
placeholder_keys:
filtrate_vessel: unilabos_resources
vessel: unilabos_nodes
result: {}
schema:
description: ''
@@ -2193,7 +2248,7 @@ workstation:
type: number
required:
- vessel
- filtrate_vessel
- #filtrate_vessel
- stir
- stir_speed
- temp
@@ -2323,6 +2378,9 @@ workstation:
data_type: resource
handler_key: ToVesselOut
label: To Vessel
placeholder_keys:
from_vessel: unilabos_resources
to_vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -2654,6 +2712,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -2833,6 +2893,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -2984,6 +3046,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -3133,6 +3197,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -3352,6 +3418,9 @@ workstation:
data_type: resource
handler_key: ToVesselOut
label: To Vessel
placeholder_keys:
from_vessel: unilabos_nodes
to_vessel: unilabos_nodes
result: {}
schema:
description: ''
@@ -3665,6 +3734,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -4017,6 +4088,10 @@ workstation:
data_type: resource
handler_key: ToVesselOut
label: To Vessel
placeholder_keys:
column: unilabos_devices
from_vessel: unilabos_resources
to_vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -4420,6 +4495,11 @@ workstation:
data_type: resource
handler_key: ToVesselOut
label: To Vessel
placeholder_keys:
from_vessel: unilabos_resources
to_vessel: unilabos_resources
waste_phase_to_vessel: unilabos_resources
waste_vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -5051,6 +5131,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -5223,6 +5305,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -5389,6 +5473,8 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
placeholder_keys:
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -5554,6 +5640,9 @@ workstation:
data_type: resource
handler_key: ToVesselOut
label: To Vessel
placeholder_keys:
from_vessel: unilabos_nodes
to_vessel: unilabos_nodes
result: {}
schema:
description: ''
@@ -5720,6 +5809,9 @@ workstation:
data_type: resource
handler_key: filtrate_vessel_out
label: Filtrate Vessel
placeholder_keys:
filtrate_vessel: unilabos_resources
vessel: unilabos_resources
result: {}
schema:
description: ''
@@ -6033,6 +6125,7 @@ workstation:
module: unilabos.ros.nodes.presets.protocol_node:ROS2ProtocolNode
status_types: {}
type: ros2
config_info: []
description: Workstation
handles: []
icon: ''
@@ -6057,4 +6150,4 @@ workstation:
properties: {}
required: []
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -1,4 +1,6 @@
zhida_hplc:
category:
- zhida_hplc
class:
action_value_mappings:
abort:
@@ -150,6 +152,7 @@ zhida_hplc:
methods: dict
status: dict
type: python
config_info: []
description: 智达高效液相色谱HPLC分析设备用于实验室样品的分离、检测和定量分析。该设备通过TCP socket与HPLC控制系统通信支持远程控制和状态监控。具备自动进样、梯度洗脱、多检测器数据采集等功能可执行复杂的色谱分析方法。适用于化学分析、药物检测、环境监测、生物样品分析等需要高精度分离分析的实验室应用场景。
handles: []
icon: ''
@@ -177,4 +180,4 @@ zhida_hplc:
- status
- methods
type: object
version: 0.0.1
version: 1.0.0

View File

@@ -9,10 +9,11 @@ from typing import Any, Dict, List
import yaml
from unilabos.resources.graphio import resource_plr_to_ulab, tree_to_list
from unilabos.ros.msgs.message_converter import msg_converter_manager, ros_action_to_json_schema, String
from unilabos.utils import logger
from unilabos.utils.decorator import singleton
from unilabos.utils.import_manager import get_enhanced_class_info
from unilabos.utils.import_manager import get_enhanced_class_info, get_class
from unilabos.utils.type_check import NoAliasDumper
DEFAULT_PATHS = [Path(__file__).absolute().parent]
@@ -65,7 +66,9 @@ class Registry:
},
"feedback": {},
"result": {"success": "success"},
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuter, '用于创建或更新物料资源,每次传入多个物料信息。'),
"schema": ros_action_to_json_schema(
self.ResourceCreateFromOuter, "用于创建或更新物料资源,每次传入多个物料信息。"
),
"goal_default": yaml.safe_load(
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuter.Goal))
),
@@ -86,7 +89,9 @@ class Registry:
},
"feedback": {},
"result": {"success": "success"},
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuterEasy, '用于创建或更新物料资源,每次传入一个物料信息。'),
"schema": ros_action_to_json_schema(
self.ResourceCreateFromOuterEasy, "用于创建或更新物料资源,每次传入一个物料信息。"
),
"goal_default": yaml.safe_load(
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuterEasy.Goal))
),
@@ -113,13 +118,15 @@ class Registry:
"goal": {},
"feedback": {},
"result": {"latency_ms": "latency_ms", "time_diff_ms": "time_diff_ms"},
"schema": ros_action_to_json_schema(self.EmptyIn, '用于测试延迟的动作,返回延迟时间和时间差。'),
"schema": ros_action_to_json_schema(
self.EmptyIn, "用于测试延迟的动作,返回延迟时间和时间差。"
),
"goal_default": {},
"handles": {},
},
},
},
"version": "0.0.1",
"version": "1.0.0",
"icon": "icon_device.webp",
"registry_type": "device",
"handles": [],
@@ -147,20 +154,47 @@ class Registry:
logger.debug(f"[UniLab Registry] resources: {resource_path.exists()}, total: {len(files)}")
current_resource_number = len(self.resource_type_registry) + 1
for i, file in enumerate(files):
data = yaml.safe_load(open(file, encoding="utf-8"))
with open(file, encoding="utf-8", mode="r") as f:
data = yaml.safe_load(io.StringIO(f.read()))
complete_data = {}
if data:
# 为每个资源添加文件路径信息
for resource_id, resource_info in data.items():
resource_info["file_path"] = str(file.absolute()).replace("\\", "/")
if "description" not in resource_info:
resource_info["description"] = ""
if "version" not in resource_info:
resource_info["version"] = "1.0.0"
if "category" not in resource_info:
resource_info["category"] = [file.stem]
elif file.stem not in resource_info["category"]:
resource_info["category"].append(file.stem)
if "config_info" not in resource_info:
resource_info["config_info"] = []
if "icon" not in resource_info:
resource_info["icon"] = ""
if "handles" not in resource_info:
resource_info["handles"] = []
if "init_param_schema" not in resource_info:
resource_info["init_param_schema"] = {}
if complete_registry:
class_info = resource_info.get("class", {})
if len(class_info) and "module" in class_info:
if class_info.get("type") == "pylabrobot":
res_class = get_class(class_info["module"])
if callable(res_class) and not isinstance(
res_class, type
): # 有的是类,有的是函数,这里暂时只登记函数类的
res_instance = res_class(res_class.__name__)
res_ulr = tree_to_list([resource_plr_to_ulab(res_instance)])
resource_info["config_info"] = res_ulr
resource_info["registry_type"] = "resource"
complete_data[resource_id] = copy.deepcopy(dict(sorted(resource_info.items()))) # 稍后dump到文件
complete_data = dict(sorted(complete_data.items()))
complete_data = copy.deepcopy(complete_data)
if complete_registry:
with open(file, "w", encoding="utf-8") as f:
yaml.dump(complete_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper)
self.resource_type_registry.update(data)
logger.debug(
f"[UniLab Registry] Resource-{current_resource_number} File-{i+1}/{len(files)} "
@@ -344,6 +378,7 @@ class Registry:
}
def load_device_types(self, path: os.PathLike, complete_registry: bool):
# return
abs_path = Path(path).absolute()
devices_path = abs_path / "devices"
device_comms_path = abs_path / "device_comms"
@@ -369,7 +404,13 @@ class Registry:
for device_id, device_config in data.items():
# 添加文件路径信息 - 使用规范化的完整文件路径
if "version" not in device_config:
device_config["version"] = "0.0.1"
device_config["version"] = "1.0.0"
if "category" not in device_config:
device_config["category"] = [file.stem]
elif file.stem not in device_config["category"]:
device_config["category"].append(file.stem)
if "config_info" not in device_config:
device_config["config_info"] = []
if "description" not in device_config:
device_config["description"] = ""
if "icon" not in device_config:
@@ -439,7 +480,9 @@ class Registry:
# 恢复原有的description信息auto开头的不修改
for action_name, description in old_descriptions.items():
if action_name in device_config["class"]["action_value_mappings"]: # 有一些会被删除
device_config["class"]["action_value_mappings"][action_name]["schema"]["description"] = description
device_config["class"]["action_value_mappings"][action_name]["schema"][
"description"
] = description
device_config["init_param_schema"] = {}
device_config["init_param_schema"]["config"] = self._generate_unilab_json_command_schema(
enhanced_info["init_params"], "__init__"

View File

@@ -1,8 +1,17 @@
OTDeck:
description: Opentrons deck
class:
category:
- deck
class:
module: pylabrobot.resources.opentrons.deck:OTDeck
type: pylabrobot
config_info: []
description: Opentrons deck
file_path: C:/Users/10230/PycharmProjects/Uni-Lab-OS/unilabos/registry/resources/opentrons/deck.yaml
handles: []
icon: ''
init_param_schema: {}
model:
mesh: opentrons_liquid_handler
type: device
mesh: opentrons_liquid_handler
registry_type: resource
version: 1.0.0

View File

@@ -1,5 +1,39 @@
Opentrons_96_adapter_Vb:
description: Opentrons 96 adapter Vb
class:
category:
- plate_adapters
class:
module: pylabrobot.resources.opentrons.plate_adapters:Opentrons_96_adapter_Vb
type: pylabrobot
type: pylabrobot
config_info:
- children: []
class: ''
config:
barcode: null
category: plate_adapter
model: Opentrons_96_adapter_Vb
rotation:
type: Rotation
x: 0
y: 0
z: 0
size_x: 127.76
size_y: 85.48
size_z: 18.55
type: PlateAdapter
data: {}
id: Opentrons_96_adapter_Vb
name: Opentrons_96_adapter_Vb
parent: null
position:
x: 0
y: 0
z: 0
sample_id: null
type: container
description: Opentrons 96 adapter Vb
file_path: C:/Users/10230/PycharmProjects/Uni-Lab-OS/unilabos/registry/resources/opentrons/plate_adapters.yaml
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,35 @@
container:
description: regular organic container
icon: Flask.webp
class:
category:
- container
class:
module: unilabos.resources.container:RegularContainer
type: unilabos
config_info: []
description: regular organic container
file_path: C:/Users/10230/PycharmProjects/Uni-Lab-OS/unilabos/registry/resources/organic/container.yaml
handles:
- handler_key: top
label: top
io_type: target
data_type: fluid
side: NORTH
data_source: handle
data_key: fluid_in
- handler_key: bottom
label: bottom
io_type: source
data_type: fluid
side: SOUTH
data_source: handle
data_key: fluid_out
- handler_key: bind
label: bind
io_type: target
data_type: mechanical
side: SOUTH
data_source: handle
data_key: mechanical_port
- data_key: fluid_in
data_source: handle
data_type: fluid
handler_key: top
io_type: target
label: top
side: NORTH
- data_key: fluid_out
data_source: handle
data_type: fluid
handler_key: bottom
io_type: source
label: bottom
side: SOUTH
- data_key: mechanical_port
data_source: handle
data_type: mechanical
handler_key: bind
io_type: target
label: bind
side: SOUTH
icon: Flask.webp
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -248,7 +248,7 @@ def dict_to_nested_dict(nodes: dict, devices_only: bool = False) -> dict:
root_nodes = {
node["id"]: node
for node in nodes_list
if node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan]
if node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan] or len(nodes_list) == 1
}
# 如果存在多个根节点,返回所有根节点

View File

@@ -656,6 +656,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
self.lab_logger().info(f"查询资源状态: Key: {k} Type: {v}")
current_resources = []
# TODO: resource后面需要分组
only_one_resource = False
try:
if len(action_kwargs[k]) > 1:
for i in action_kwargs[k]:
@@ -665,6 +667,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
response = await self._resource_clients["resource_get"].call_async(r)
current_resources.extend(response.resources)
else:
only_one_resource = True
r = ResourceGet.Request()
r.id = (
action_kwargs[k]["id"]
@@ -682,7 +685,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
type_hint = action_paramtypes[k]
final_type = get_type_class(type_hint)
# 判断 ACTION 是否需要特殊的物料类型如 pylabrobot.resources.Resource并做转换
final_resource = [convert_resources_to_type([i], final_type)[0] for i in resources_list]
if only_one_resource:
final_resource = convert_resources_to_type(resources_list, final_type)
else:
final_resource = [convert_resources_to_type([i], final_type)[0] for i in resources_list]
action_kwargs[k] = self.resource_tracker.figure_resource(final_resource)
##### self.lab_logger().info(f"准备执行: {action_kwargs}, 函数: {ACTION.__name__}")

View File

@@ -189,45 +189,26 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
# # 🔧 完全禁用Host查询直接使用转换后的数据
# print(f"🔧 跳过Host查询直接使用转换后的数据")
# 🔧 额外验证确保vessel数据完整
if 'vessel' in protocol_kwargs:
vessel_data = protocol_kwargs['vessel']
#print(f"🔍 验证vessel数据: {vessel_data}")
# 如果vessel是空字典尝试重新构建
if not vessel_data or (isinstance(vessel_data, dict) and not vessel_data):
# print(f"⚠️ vessel数据为空尝试从原始goal重新提取...")
# 直接从原始goal提取vessel
if hasattr(goal, 'vessel') and goal.vessel:
# print(f"🔍 原始goal.vessel: {goal.vessel}")
# 手动转换vessel
vessel_data = {
'id': goal.vessel.id,
'name': goal.vessel.name,
'type': goal.vessel.type,
'category': goal.vessel.category,
'config': goal.vessel.config,
'data': goal.vessel.data
}
protocol_kwargs['vessel'] = vessel_data
# print(f"✅ 手动重建vessel数据: {vessel_data}")
else:
# print(f"❌ 无法从原始goal提取vessel数据")
# 创建一个基本的vessel
vessel_data = {'id': 'default_vessel'}
protocol_kwargs['vessel'] = vessel_data
# print(f"🔧 创建默认vessel: {vessel_data}")
# 向Host查询物料当前状态
for k, v in goal.get_fields_and_field_types().items():
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
r = ResourceGet.Request()
resource_id = (
protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"]
)
r.id = resource_id
r.with_children = True
response = await self._resource_clients["resource_get"].call_async(r)
protocol_kwargs[k] = list_to_nested_dict(
[convert_from_ros_msg(rs) for rs in response.resources]
)
#print(f"🔍 最终传递给协议的 protocol_kwargs: {protocol_kwargs}")
#print(f"🔍 最终的 vessel: {protocol_kwargs.get('vessel', 'NOT_FOUND')}")
self.lab_logger().info(f"🔍 最终传递给协议的 protocol_kwargs: {protocol_kwargs}")
self.lab_logger().info(f"🔍 最终的 vessel: {protocol_kwargs.get('vessel', 'NOT_FOUND')}")
from unilabos.resources.graphio import physical_setup_graph
self.lab_logger().info(f"Working on physical setup: {physical_setup_graph}")
self.lab_logger().info(f"Protocol kwargs: {goal}")
self.lab_logger().info(f"Protocol kwargs: {action_value_mapping}")
protocol_steps = protocol_steps_generator(G=physical_setup_graph, **protocol_kwargs)
self.lab_logger().info(f"Goal received: {protocol_kwargs}, running steps: \n{protocol_steps}")
@@ -263,14 +244,14 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
}
)
# # 向Host更新物料当前状态
# for k, v in goal.get_fields_and_field_types().items():
# if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
# r = ResourceUpdate.Request()
# r.resources = [
# convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k])
# ]
# response = await self._resource_clients["resource_update"].call_async(r)
# 向Host更新物料当前状态
for k, v in goal.get_fields_and_field_types().items():
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
r = ResourceUpdate.Request()
r.resources = [
convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k])
]
response = await self._resource_clients["resource_update"].call_async(r)
# 设置成功状态和返回值
execution_success = True

View File

@@ -1,5 +1,6 @@
import collections.abc
import json
from collections import OrderedDict
from typing import get_origin, get_args
import yaml
@@ -29,6 +30,14 @@ class NoAliasDumper(yaml.SafeDumper):
return True
# 为NoAliasDumper添加OrderedDict的representation方法
def represent_ordereddict(dumper, data):
return dumper.represent_mapping("tag:yaml.org,2002:map", data.items())
# 注册OrderedDict的representer
NoAliasDumper.add_representer(OrderedDict, represent_ordereddict)
class ResultInfoEncoder(json.JSONEncoder):
"""专门用于处理任务执行结果信息的JSON编码器"""

View File

@@ -65,6 +65,7 @@ set(action_files
"action/LiquidHandlerReturnTips.action"
"action/LiquidHandlerReturnTips96.action"
"action/LiquidHandlerSetLiquid.action"
"action/LiquidHandlerSetTipRack.action"
"action/LiquidHandlerStamp.action"
"action/LiquidHandlerTransfer.action"

View File

@@ -0,0 +1,5 @@
Resource[] tip_racks
---
string return_info
bool success
---