mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-24 16:49:17 +00:00
Compare commits
5 Commits
feat/lab_r
...
feat/sampl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b5497dd0 | ||
|
|
b9d6f71970 | ||
|
|
5dda5c61ce | ||
|
|
1d181743ea | ||
|
|
337789e270 |
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
package:
|
package:
|
||||||
name: unilabos
|
name: unilabos
|
||||||
version: 0.10.18
|
version: 0.10.17
|
||||||
|
|
||||||
source:
|
source:
|
||||||
path: ../../unilabos
|
path: ../../unilabos
|
||||||
@@ -46,15 +46,13 @@ requirements:
|
|||||||
- jinja2
|
- jinja2
|
||||||
- requests
|
- requests
|
||||||
- uvicorn
|
- uvicorn
|
||||||
- if: not osx
|
- opcua # [not osx]
|
||||||
then:
|
|
||||||
- opcua
|
|
||||||
- pyserial
|
- pyserial
|
||||||
- pandas
|
- pandas
|
||||||
- pymodbus
|
- pymodbus
|
||||||
- matplotlib
|
- matplotlib
|
||||||
- pylibftdi
|
- pylibftdi
|
||||||
- uni-lab::unilabos-env ==0.10.18
|
- uni-lab::unilabos-env ==0.10.17
|
||||||
|
|
||||||
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.18
|
version: 0.10.17
|
||||||
|
|
||||||
build:
|
build:
|
||||||
noarch: generic
|
noarch: generic
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
package:
|
package:
|
||||||
name: unilabos-full
|
name: unilabos-full
|
||||||
version: 0.10.18
|
version: 0.10.17
|
||||||
|
|
||||||
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.18
|
- uni-lab::unilabos ==0.10.17
|
||||||
# Documentation tools
|
# Documentation tools
|
||||||
- sphinx
|
- sphinx
|
||||||
- sphinx_rtd_theme
|
- sphinx_rtd_theme
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: ros-humble-unilabos-msgs
|
name: ros-humble-unilabos-msgs
|
||||||
version: 0.10.18
|
version: 0.10.17
|
||||||
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.18"
|
version: "0.10.17"
|
||||||
|
|
||||||
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.18',
|
version='0.10.17',
|
||||||
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.18"
|
__version__ = "0.10.17"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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
|
||||||
@@ -172,12 +171,6 @@ def parse_args():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Disable sending update feedback to server",
|
help="Disable sending update feedback to server",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--test_mode",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Test mode: all actions simulate execution and return mock results without running real hardware",
|
|
||||||
)
|
|
||||||
# workflow upload subcommand
|
# workflow upload subcommand
|
||||||
workflow_parser = subparsers.add_parser(
|
workflow_parser = subparsers.add_parser(
|
||||||
"workflow_upload",
|
"workflow_upload",
|
||||||
@@ -211,12 +204,6 @@ def parse_args():
|
|||||||
default=False,
|
default=False,
|
||||||
help="Whether to publish the workflow (default: False)",
|
help="Whether to publish the workflow (default: False)",
|
||||||
)
|
)
|
||||||
workflow_parser.add_argument(
|
|
||||||
"--description",
|
|
||||||
type=str,
|
|
||||||
default="",
|
|
||||||
help="Workflow description, used when publishing the workflow",
|
|
||||||
)
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -244,60 +231,52 @@ def main():
|
|||||||
# 加载配置文件,优先加载config,然后从env读取
|
# 加载配置文件,优先加载config,然后从env读取
|
||||||
config_path = args_dict.get("config")
|
config_path = args_dict.get("config")
|
||||||
|
|
||||||
# === 解析 working_dir ===
|
if check_mode:
|
||||||
# 规则1: working_dir 传入 → 检测 unilabos_data 子目录,已是则不修改
|
args_dict["working_dir"] = os.path.abspath(os.getcwd())
|
||||||
# 规则2: 仅 config_path 传入 → 用其父目录作为 working_dir
|
# 当 skip_env_check 时,默认使用当前目录作为 working_dir
|
||||||
# 规则4: 两者都传入 → 各用各的,但 working_dir 仍做 unilabos_data 子目录检测
|
if skip_env_check and not args_dict.get("working_dir") and not config_path:
|
||||||
raw_working_dir = args_dict.get("working_dir")
|
|
||||||
if raw_working_dir:
|
|
||||||
working_dir = os.path.abspath(raw_working_dir)
|
|
||||||
elif config_path and os.path.exists(config_path):
|
|
||||||
working_dir = os.path.dirname(os.path.abspath(config_path))
|
|
||||||
else:
|
|
||||||
working_dir = os.path.abspath(os.getcwd())
|
working_dir = os.path.abspath(os.getcwd())
|
||||||
|
print_status(f"跳过环境检查模式:使用当前目录作为工作目录 {working_dir}", "info")
|
||||||
# unilabos_data 子目录自动检测
|
# 检查当前目录是否有 local_config.py
|
||||||
if os.path.basename(working_dir) != "unilabos_data":
|
local_config_in_cwd = os.path.join(working_dir, "local_config.py")
|
||||||
unilabos_data_sub = os.path.join(working_dir, "unilabos_data")
|
if os.path.exists(local_config_in_cwd):
|
||||||
if os.path.isdir(unilabos_data_sub):
|
config_path = local_config_in_cwd
|
||||||
working_dir = unilabos_data_sub
|
|
||||||
elif not raw_working_dir and not (config_path and os.path.exists(config_path)):
|
|
||||||
# 未显式指定路径,默认使用 cwd/unilabos_data
|
|
||||||
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
|
||||||
|
|
||||||
# === 解析 config_path ===
|
|
||||||
if config_path and not os.path.exists(config_path):
|
|
||||||
# config_path 传入但不存在,尝试在 working_dir 中查找
|
|
||||||
candidate = os.path.join(working_dir, "local_config.py")
|
|
||||||
if os.path.exists(candidate):
|
|
||||||
config_path = candidate
|
|
||||||
print_status(f"在工作目录中发现配置文件: {config_path}", "info")
|
|
||||||
else:
|
|
||||||
print_status(
|
|
||||||
f"配置文件 {config_path} 不存在,工作目录 {working_dir} 中也未找到 local_config.py,"
|
|
||||||
f"请通过 --config 传入 local_config.py 文件路径",
|
|
||||||
"error",
|
|
||||||
)
|
|
||||||
os._exit(1)
|
|
||||||
elif not config_path:
|
|
||||||
# 规则3: 未传入 config_path,尝试 working_dir/local_config.py
|
|
||||||
candidate = os.path.join(working_dir, "local_config.py")
|
|
||||||
if os.path.exists(candidate):
|
|
||||||
config_path = candidate
|
|
||||||
print_status(f"发现本地配置文件: {config_path}", "info")
|
print_status(f"发现本地配置文件: {config_path}", "info")
|
||||||
else:
|
else:
|
||||||
print_status(f"未指定config路径,可通过 --config 传入 local_config.py 文件路径", "info")
|
print_status(f"未指定config路径,可通过 --config 传入 local_config.py 文件路径", "info")
|
||||||
print_status(f"您是否为第一次使用?并将当前路径 {working_dir} 作为工作目录? (Y/n)", "info")
|
elif os.getcwd().endswith("unilabos_data"):
|
||||||
if check_mode or input() != "n":
|
working_dir = os.path.abspath(os.getcwd())
|
||||||
os.makedirs(working_dir, exist_ok=True)
|
else:
|
||||||
config_path = os.path.join(working_dir, "local_config.py")
|
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
||||||
shutil.copy(
|
|
||||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"),
|
if args_dict.get("working_dir"):
|
||||||
config_path,
|
working_dir = args_dict.get("working_dir", "")
|
||||||
|
if config_path and not os.path.exists(config_path):
|
||||||
|
config_path = os.path.join(working_dir, "local_config.py")
|
||||||
|
if not os.path.exists(config_path):
|
||||||
|
print_status(
|
||||||
|
f"当前工作目录 {working_dir} 未找到local_config.py,请通过 --config 传入 local_config.py 文件路径",
|
||||||
|
"error",
|
||||||
)
|
)
|
||||||
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
|
|
||||||
else:
|
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
elif config_path and os.path.exists(config_path):
|
||||||
|
working_dir = os.path.dirname(config_path)
|
||||||
|
elif os.path.exists(working_dir) and os.path.exists(os.path.join(working_dir, "local_config.py")):
|
||||||
|
config_path = os.path.join(working_dir, "local_config.py")
|
||||||
|
elif not skip_env_check and not config_path and (
|
||||||
|
not os.path.exists(working_dir) or not os.path.exists(os.path.join(working_dir, "local_config.py"))
|
||||||
|
):
|
||||||
|
print_status(f"未指定config路径,可通过 --config 传入 local_config.py 文件路径", "info")
|
||||||
|
print_status(f"您是否为第一次使用?并将当前路径 {working_dir} 作为工作目录? (Y/n)", "info")
|
||||||
|
if input() != "n":
|
||||||
|
os.makedirs(working_dir, exist_ok=True)
|
||||||
|
config_path = os.path.join(working_dir, "local_config.py")
|
||||||
|
shutil.copy(
|
||||||
|
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"), config_path
|
||||||
|
)
|
||||||
|
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
|
||||||
|
else:
|
||||||
|
os._exit(1)
|
||||||
|
|
||||||
# 加载配置文件 (check_mode 跳过)
|
# 加载配置文件 (check_mode 跳过)
|
||||||
print_status(f"当前工作目录为 {working_dir}", "info")
|
print_status(f"当前工作目录为 {working_dir}", "info")
|
||||||
@@ -309,9 +288,7 @@ def main():
|
|||||||
|
|
||||||
if hasattr(BasicConfig, "log_level"):
|
if hasattr(BasicConfig, "log_level"):
|
||||||
logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.")
|
logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.")
|
||||||
file_path = configure_logger(loglevel=BasicConfig.log_level, working_dir=working_dir)
|
configure_logger(loglevel=BasicConfig.log_level, working_dir=working_dir)
|
||||||
if file_path is not None:
|
|
||||||
logger.info(f"[LOG_FILE] {file_path}")
|
|
||||||
|
|
||||||
if args.addr != parser.get_default("addr"):
|
if args.addr != parser.get_default("addr"):
|
||||||
if args.addr == "test":
|
if args.addr == "test":
|
||||||
@@ -355,11 +332,8 @@ def main():
|
|||||||
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
||||||
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
|
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
|
||||||
BasicConfig.no_update_feedback = args_dict.get("no_update_feedback", False)
|
BasicConfig.no_update_feedback = args_dict.get("no_update_feedback", False)
|
||||||
BasicConfig.test_mode = args_dict.get("test_mode", False)
|
|
||||||
if BasicConfig.test_mode:
|
|
||||||
print_status("启用测试模式:所有动作将模拟执行,不调用真实硬件", "warning")
|
|
||||||
BasicConfig.communication_protocol = "websocket"
|
BasicConfig.communication_protocol = "websocket"
|
||||||
machine_name = platform.node()
|
machine_name = os.popen("hostname").read().strip()
|
||||||
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
|
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"]
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[
|
|||||||
response = http_client.resource_registry({"resources": list(devices_to_register.values())})
|
response = http_client.resource_registry({"resources": list(devices_to_register.values())})
|
||||||
cost_time = time.time() - start_time
|
cost_time = time.time() - start_time
|
||||||
if response.status_code in [200, 201]:
|
if response.status_code in [200, 201]:
|
||||||
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}s")
|
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}ms")
|
||||||
else:
|
else:
|
||||||
logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}s")
|
logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}ms")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[UniLab Register] 设备注册异常: {e}")
|
logger.error(f"[UniLab Register] 设备注册异常: {e}")
|
||||||
|
|
||||||
@@ -51,9 +51,9 @@ def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[
|
|||||||
response = http_client.resource_registry({"resources": list(resources_to_register.values())})
|
response = http_client.resource_registry({"resources": list(resources_to_register.values())})
|
||||||
cost_time = time.time() - start_time
|
cost_time = time.time() - start_time
|
||||||
if response.status_code in [200, 201]:
|
if response.status_code in [200, 201]:
|
||||||
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}s")
|
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}ms")
|
||||||
else:
|
else:
|
||||||
logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}s")
|
logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}ms")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[UniLab Register] 资源注册异常: {e}")
|
logger.error(f"[UniLab Register] 资源注册异常: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -343,10 +343,9 @@ class HTTPClient:
|
|||||||
edges: List[Dict[str, Any]],
|
edges: List[Dict[str, Any]],
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
published: bool = False,
|
published: bool = False,
|
||||||
description: str = "",
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
导入工作流到服务器,如果 published 为 True,则额外发起发布请求
|
导入工作流到服务器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: 工作流名称(顶层)
|
name: 工作流名称(顶层)
|
||||||
@@ -356,7 +355,6 @@ class HTTPClient:
|
|||||||
edges: 工作流边列表
|
edges: 工作流边列表
|
||||||
tags: 工作流标签列表,默认为空列表
|
tags: 工作流标签列表,默认为空列表
|
||||||
published: 是否发布工作流,默认为False
|
published: 是否发布工作流,默认为False
|
||||||
description: 工作流描述,发布时使用
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: API响应数据,包含 code 和 data (uuid, name)
|
Dict: API响应数据,包含 code 和 data (uuid, name)
|
||||||
@@ -369,6 +367,7 @@ class HTTPClient:
|
|||||||
"nodes": nodes,
|
"nodes": nodes,
|
||||||
"edges": edges,
|
"edges": edges,
|
||||||
"tags": tags if tags is not None else [],
|
"tags": tags if tags is not None else [],
|
||||||
|
"published": published,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
# 保存请求到文件
|
# 保存请求到文件
|
||||||
@@ -389,51 +388,11 @@ class HTTPClient:
|
|||||||
res = response.json()
|
res = response.json()
|
||||||
if "code" in res and res["code"] != 0:
|
if "code" in res and res["code"] != 0:
|
||||||
logger.error(f"导入工作流失败: {response.text}")
|
logger.error(f"导入工作流失败: {response.text}")
|
||||||
return res
|
|
||||||
# 导入成功后,如果需要发布则额外发起发布请求
|
|
||||||
if published:
|
|
||||||
imported_uuid = res.get("data", {}).get("uuid", workflow_uuid)
|
|
||||||
publish_res = self.workflow_publish(imported_uuid, description)
|
|
||||||
res["publish_result"] = publish_res
|
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
logger.error(f"导入工作流失败: {response.status_code}, {response.text}")
|
logger.error(f"导入工作流失败: {response.status_code}, {response.text}")
|
||||||
return {"code": response.status_code, "message": response.text}
|
return {"code": response.status_code, "message": response.text}
|
||||||
|
|
||||||
def workflow_publish(self, workflow_uuid: str, description: str = "") -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
发布工作流
|
|
||||||
|
|
||||||
Args:
|
|
||||||
workflow_uuid: 工作流UUID
|
|
||||||
description: 工作流描述
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict: API响应数据
|
|
||||||
"""
|
|
||||||
payload = {
|
|
||||||
"uuid": workflow_uuid,
|
|
||||||
"description": description,
|
|
||||||
"published": True,
|
|
||||||
}
|
|
||||||
logger.info(f"正在发布工作流: {workflow_uuid}")
|
|
||||||
response = requests.patch(
|
|
||||||
f"{self.remote_addr}/lab/workflow/owner",
|
|
||||||
json=payload,
|
|
||||||
headers={"Authorization": f"Lab {self.auth}"},
|
|
||||||
timeout=60,
|
|
||||||
)
|
|
||||||
if response.status_code == 200:
|
|
||||||
res = response.json()
|
|
||||||
if "code" in res and res["code"] != 0:
|
|
||||||
logger.error(f"发布工作流失败: {response.text}")
|
|
||||||
else:
|
|
||||||
logger.info(f"工作流发布成功: {workflow_uuid}")
|
|
||||||
return res
|
|
||||||
else:
|
|
||||||
logger.error(f"发布工作流失败: {response.status_code}, {response.text}")
|
|
||||||
return {"code": response.status_code, "message": response.text}
|
|
||||||
|
|
||||||
|
|
||||||
# 创建默认客户端实例
|
# 创建默认客户端实例
|
||||||
http_client = HTTPClient()
|
http_client = HTTPClient()
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ class JobInfo:
|
|||||||
start_time: float
|
start_time: float
|
||||||
last_update_time: float = field(default_factory=time.time)
|
last_update_time: float = field(default_factory=time.time)
|
||||||
ready_timeout: Optional[float] = None # READY状态的超时时间
|
ready_timeout: Optional[float] = None # READY状态的超时时间
|
||||||
always_free: bool = False # 是否为永久闲置动作(不受排队限制)
|
|
||||||
|
|
||||||
def update_timestamp(self):
|
def update_timestamp(self):
|
||||||
"""更新最后更新时间"""
|
"""更新最后更新时间"""
|
||||||
@@ -128,15 +127,6 @@ class DeviceActionManager:
|
|||||||
# 总是将job添加到all_jobs中
|
# 总是将job添加到all_jobs中
|
||||||
self.all_jobs[job_info.job_id] = job_info
|
self.all_jobs[job_info.job_id] = job_info
|
||||||
|
|
||||||
# always_free的动作不受排队限制,直接设为READY
|
|
||||||
if job_info.always_free:
|
|
||||||
job_info.status = JobStatus.READY
|
|
||||||
job_info.update_timestamp()
|
|
||||||
job_info.set_ready_timeout(10)
|
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
|
||||||
logger.trace(f"[DeviceActionManager] Job {job_log} always_free, start immediately")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 检查是否有正在执行或准备执行的任务
|
# 检查是否有正在执行或准备执行的任务
|
||||||
if device_key in self.active_jobs:
|
if device_key in self.active_jobs:
|
||||||
# 有正在执行或准备执行的任务,加入队列
|
# 有正在执行或准备执行的任务,加入队列
|
||||||
@@ -186,15 +176,11 @@ class DeviceActionManager:
|
|||||||
logger.error(f"[DeviceActionManager] Job {job_log} is not in READY status, current: {job_info.status}")
|
logger.error(f"[DeviceActionManager] Job {job_log} is not in READY status, current: {job_info.status}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# always_free的job不需要检查active_jobs
|
# 检查设备上是否是这个job
|
||||||
if not job_info.always_free:
|
if device_key not in self.active_jobs or self.active_jobs[device_key].job_id != job_id:
|
||||||
# 检查设备上是否是这个job
|
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||||
if device_key not in self.active_jobs or self.active_jobs[device_key].job_id != job_id:
|
logger.error(f"[DeviceActionManager] Job {job_log} is not the active job for {device_key}")
|
||||||
job_log = format_job_log(
|
return False
|
||||||
job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name
|
|
||||||
)
|
|
||||||
logger.error(f"[DeviceActionManager] Job {job_log} is not the active job for {device_key}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 开始执行任务,将状态从READY转换为STARTED
|
# 开始执行任务,将状态从READY转换为STARTED
|
||||||
job_info.status = JobStatus.STARTED
|
job_info.status = JobStatus.STARTED
|
||||||
@@ -217,13 +203,6 @@ class DeviceActionManager:
|
|||||||
job_info = self.all_jobs[job_id]
|
job_info = self.all_jobs[job_id]
|
||||||
device_key = job_info.device_action_key
|
device_key = job_info.device_action_key
|
||||||
|
|
||||||
# always_free的job直接清理,不影响队列
|
|
||||||
if job_info.always_free:
|
|
||||||
job_info.status = JobStatus.ENDED
|
|
||||||
job_info.update_timestamp()
|
|
||||||
del self.all_jobs[job_id]
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 移除活跃任务
|
# 移除活跃任务
|
||||||
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
||||||
del self.active_jobs[device_key]
|
del self.active_jobs[device_key]
|
||||||
@@ -255,14 +234,9 @@ class DeviceActionManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_active_jobs(self) -> List[JobInfo]:
|
def get_active_jobs(self) -> List[JobInfo]:
|
||||||
"""获取所有正在执行的任务(含active_jobs和always_free的STARTED job)"""
|
"""获取所有正在执行的任务"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
jobs = list(self.active_jobs.values())
|
return list(self.active_jobs.values())
|
||||||
# 补充 always_free 的 STARTED job(它们不在 active_jobs 中)
|
|
||||||
for job in self.all_jobs.values():
|
|
||||||
if job.always_free and job.status == JobStatus.STARTED and job not in jobs:
|
|
||||||
jobs.append(job)
|
|
||||||
return jobs
|
|
||||||
|
|
||||||
def get_queued_jobs(self) -> List[JobInfo]:
|
def get_queued_jobs(self) -> List[JobInfo]:
|
||||||
"""获取所有排队中的任务"""
|
"""获取所有排队中的任务"""
|
||||||
@@ -287,14 +261,6 @@ class DeviceActionManager:
|
|||||||
job_info = self.all_jobs[job_id]
|
job_info = self.all_jobs[job_id]
|
||||||
device_key = job_info.device_action_key
|
device_key = job_info.device_action_key
|
||||||
|
|
||||||
# always_free的job直接清理
|
|
||||||
if job_info.always_free:
|
|
||||||
job_info.status = JobStatus.ENDED
|
|
||||||
del self.all_jobs[job_id]
|
|
||||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
|
||||||
logger.trace(f"[DeviceActionManager] Always-free job {job_log} cancelled")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 如果是正在执行的任务
|
# 如果是正在执行的任务
|
||||||
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
||||||
# 清理active job状态
|
# 清理active job状态
|
||||||
@@ -368,18 +334,13 @@ class DeviceActionManager:
|
|||||||
timeout_jobs = []
|
timeout_jobs = []
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# 收集所有需要检查的 READY 任务(active_jobs + always_free READY jobs)
|
# 统计READY状态的任务数量
|
||||||
ready_candidates = list(self.active_jobs.values())
|
ready_jobs_count = sum(1 for job in self.active_jobs.values() if job.status == JobStatus.READY)
|
||||||
for job in self.all_jobs.values():
|
|
||||||
if job.always_free and job.status == JobStatus.READY and job not in ready_candidates:
|
|
||||||
ready_candidates.append(job)
|
|
||||||
|
|
||||||
ready_jobs_count = sum(1 for job in ready_candidates if job.status == JobStatus.READY)
|
|
||||||
if ready_jobs_count > 0:
|
if ready_jobs_count > 0:
|
||||||
logger.trace(f"[DeviceActionManager] Checking {ready_jobs_count} READY jobs for timeout") # type: ignore # noqa: E501
|
logger.trace(f"[DeviceActionManager] Checking {ready_jobs_count} READY jobs for timeout") # type: ignore # noqa: E501
|
||||||
|
|
||||||
# 找到所有超时的READY任务(只检测,不处理)
|
# 找到所有超时的READY任务(只检测,不处理)
|
||||||
for job_info in ready_candidates:
|
for job_info in self.active_jobs.values():
|
||||||
if job_info.is_ready_timeout():
|
if job_info.is_ready_timeout():
|
||||||
timeout_jobs.append(job_info)
|
timeout_jobs.append(job_info)
|
||||||
job_log = format_job_log(
|
job_log = format_job_log(
|
||||||
@@ -647,24 +608,6 @@ class MessageProcessor:
|
|||||||
if host_node:
|
if host_node:
|
||||||
host_node.handle_pong_response(pong_data)
|
host_node.handle_pong_response(pong_data)
|
||||||
|
|
||||||
def _check_action_always_free(self, device_id: str, action_name: str) -> bool:
|
|
||||||
"""检查该action是否标记为always_free,通过HostNode统一的_action_value_mappings查找"""
|
|
||||||
try:
|
|
||||||
host_node = HostNode.get_instance(0)
|
|
||||||
if not host_node:
|
|
||||||
return False
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
action_mappings = host_node._action_value_mappings.get(device_id)
|
|
||||||
if not action_mappings:
|
|
||||||
return False
|
|
||||||
# 尝试直接匹配或 auto- 前缀匹配
|
|
||||||
for key in [action_name, f"auto-{action_name}"]:
|
|
||||||
if key in action_mappings:
|
|
||||||
return action_mappings[key].get("always_free", False)
|
|
||||||
return False
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def _handle_query_action_state(self, data: Dict[str, Any]):
|
async def _handle_query_action_state(self, data: Dict[str, Any]):
|
||||||
"""处理query_action_state消息"""
|
"""处理query_action_state消息"""
|
||||||
device_id = data.get("device_id", "")
|
device_id = data.get("device_id", "")
|
||||||
@@ -679,9 +622,6 @@ class MessageProcessor:
|
|||||||
|
|
||||||
device_action_key = f"/devices/{device_id}/{action_name}"
|
device_action_key = f"/devices/{device_id}/{action_name}"
|
||||||
|
|
||||||
# 检查action是否为always_free
|
|
||||||
action_always_free = self._check_action_always_free(device_id, action_name)
|
|
||||||
|
|
||||||
# 创建任务信息
|
# 创建任务信息
|
||||||
job_info = JobInfo(
|
job_info = JobInfo(
|
||||||
job_id=job_id,
|
job_id=job_id,
|
||||||
@@ -691,7 +631,6 @@ class MessageProcessor:
|
|||||||
device_action_key=device_action_key,
|
device_action_key=device_action_key,
|
||||||
status=JobStatus.QUEUE,
|
status=JobStatus.QUEUE,
|
||||||
start_time=time.time(),
|
start_time=time.time(),
|
||||||
always_free=action_always_free,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加到设备管理器
|
# 添加到设备管理器
|
||||||
@@ -1184,11 +1123,6 @@ 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": {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ class BasicConfig:
|
|||||||
disable_browser = False # 禁止浏览器自动打开
|
disable_browser = False # 禁止浏览器自动打开
|
||||||
port = 8002 # 本地HTTP服务
|
port = 8002 # 本地HTTP服务
|
||||||
check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性
|
check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性
|
||||||
test_mode = False # 测试模式,所有动作不实际执行,返回模拟结果
|
|
||||||
# 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
|
# 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
|
||||||
log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG"
|
log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG"
|
||||||
|
|
||||||
@@ -146,5 +145,5 @@ def load_config(config_path=None):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
exit(1)
|
exit(1)
|
||||||
else:
|
else:
|
||||||
config_path = os.path.join(os.path.dirname(__file__), "example_config.py")
|
config_path = os.path.join(os.path.dirname(__file__), "local_config.py")
|
||||||
load_config(config_path)
|
load_config(config_path)
|
||||||
|
|||||||
@@ -201,42 +201,17 @@ class ResourceVisualization:
|
|||||||
self.moveit_controllers_yaml['moveit_simple_controller_manager'][f"{name}_{controller_name}"] = moveit_dict['moveit_simple_controller_manager'][controller_name]
|
self.moveit_controllers_yaml['moveit_simple_controller_manager'][f"{name}_{controller_name}"] = moveit_dict['moveit_simple_controller_manager'][controller_name]
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _ensure_ros2_env() -> dict:
|
|
||||||
"""确保 ROS2 环境变量正确设置,返回可用于子进程的 env dict"""
|
|
||||||
import sys
|
|
||||||
env = dict(os.environ)
|
|
||||||
conda_prefix = os.path.dirname(os.path.dirname(sys.executable))
|
|
||||||
|
|
||||||
if "AMENT_PREFIX_PATH" not in env or not env["AMENT_PREFIX_PATH"].strip():
|
|
||||||
candidate = os.pathsep.join([conda_prefix, os.path.join(conda_prefix, "Library")])
|
|
||||||
env["AMENT_PREFIX_PATH"] = candidate
|
|
||||||
os.environ["AMENT_PREFIX_PATH"] = candidate
|
|
||||||
|
|
||||||
extra_bin_dirs = [
|
|
||||||
os.path.join(conda_prefix, "Library", "bin"),
|
|
||||||
os.path.join(conda_prefix, "Library", "lib"),
|
|
||||||
os.path.join(conda_prefix, "Scripts"),
|
|
||||||
conda_prefix,
|
|
||||||
]
|
|
||||||
current_path = env.get("PATH", "")
|
|
||||||
for d in extra_bin_dirs:
|
|
||||||
if d not in current_path:
|
|
||||||
current_path = d + os.pathsep + current_path
|
|
||||||
env["PATH"] = current_path
|
|
||||||
os.environ["PATH"] = current_path
|
|
||||||
|
|
||||||
return env
|
|
||||||
|
|
||||||
def create_launch_description(self) -> LaunchDescription:
|
def create_launch_description(self) -> LaunchDescription:
|
||||||
"""
|
"""
|
||||||
创建launch描述,包含robot_state_publisher和move_group节点
|
创建launch描述,包含robot_state_publisher和move_group节点
|
||||||
|
|
||||||
|
Args:
|
||||||
|
urdf_str: URDF文本
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
LaunchDescription: launch描述对象
|
LaunchDescription: launch描述对象
|
||||||
"""
|
"""
|
||||||
launch_env = self._ensure_ros2_env()
|
# 检查ROS 2环境变量
|
||||||
|
|
||||||
if "AMENT_PREFIX_PATH" not in os.environ:
|
if "AMENT_PREFIX_PATH" not in os.environ:
|
||||||
raise OSError(
|
raise OSError(
|
||||||
"ROS 2环境未正确设置。需要设置 AMENT_PREFIX_PATH 环境变量。\n"
|
"ROS 2环境未正确设置。需要设置 AMENT_PREFIX_PATH 环境变量。\n"
|
||||||
@@ -315,7 +290,7 @@ class ResourceVisualization:
|
|||||||
{"robot_description": robot_description},
|
{"robot_description": robot_description},
|
||||||
ros2_controllers,
|
ros2_controllers,
|
||||||
],
|
],
|
||||||
env=launch_env,
|
env=dict(os.environ)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for controller in self.moveit_controllers_yaml['moveit_simple_controller_manager']['controller_names']:
|
for controller in self.moveit_controllers_yaml['moveit_simple_controller_manager']['controller_names']:
|
||||||
@@ -325,7 +300,7 @@ class ResourceVisualization:
|
|||||||
executable="spawner",
|
executable="spawner",
|
||||||
arguments=[f"{controller}", "--controller-manager", f"controller_manager"],
|
arguments=[f"{controller}", "--controller-manager", f"controller_manager"],
|
||||||
output="screen",
|
output="screen",
|
||||||
env=launch_env,
|
env=dict(os.environ)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
controllers.append(
|
controllers.append(
|
||||||
@@ -334,7 +309,7 @@ class ResourceVisualization:
|
|||||||
executable="spawner",
|
executable="spawner",
|
||||||
arguments=["joint_state_broadcaster", "--controller-manager", f"controller_manager"],
|
arguments=["joint_state_broadcaster", "--controller-manager", f"controller_manager"],
|
||||||
output="screen",
|
output="screen",
|
||||||
env=launch_env,
|
env=dict(os.environ)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for i in controllers:
|
for i in controllers:
|
||||||
@@ -342,6 +317,7 @@ class ResourceVisualization:
|
|||||||
else:
|
else:
|
||||||
ros2_controllers = None
|
ros2_controllers = None
|
||||||
|
|
||||||
|
# 创建robot_state_publisher节点
|
||||||
robot_state_publisher = nd(
|
robot_state_publisher = nd(
|
||||||
package='robot_state_publisher',
|
package='robot_state_publisher',
|
||||||
executable='robot_state_publisher',
|
executable='robot_state_publisher',
|
||||||
@@ -351,8 +327,9 @@ class ResourceVisualization:
|
|||||||
'robot_description': robot_description,
|
'robot_description': robot_description,
|
||||||
'use_sim_time': False
|
'use_sim_time': False
|
||||||
},
|
},
|
||||||
|
# kinematics_dict
|
||||||
],
|
],
|
||||||
env=launch_env,
|
env=dict(os.environ)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -384,7 +361,7 @@ class ResourceVisualization:
|
|||||||
executable='move_group',
|
executable='move_group',
|
||||||
output='screen',
|
output='screen',
|
||||||
parameters=moveit_params,
|
parameters=moveit_params,
|
||||||
env=launch_env,
|
env=dict(os.environ)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -402,11 +379,13 @@ class ResourceVisualization:
|
|||||||
arguments=['-d', f"{str(self.mesh_path)}/view_robot.rviz"],
|
arguments=['-d', f"{str(self.mesh_path)}/view_robot.rviz"],
|
||||||
output='screen',
|
output='screen',
|
||||||
parameters=[
|
parameters=[
|
||||||
{'robot_description_kinematics': kinematics_dict},
|
{'robot_description_kinematics': kinematics_dict,
|
||||||
|
},
|
||||||
robot_description_planning,
|
robot_description_planning,
|
||||||
planning_pipelines,
|
planning_pipelines,
|
||||||
|
|
||||||
],
|
],
|
||||||
env=launch_env,
|
env=dict(os.environ)
|
||||||
)
|
)
|
||||||
self.launch_description.add_action(rviz_node)
|
self.launch_description.add_action(rviz_node)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -55,7 +55,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -91,107 +90,20 @@ class PRCXI9300Deck(Deck):
|
|||||||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# T1-T16 默认位置 (4列×4行)
|
def __init__(self, name: str, size_x: float, size_y: float, size_z: float, **kwargs):
|
||||||
_DEFAULT_SITE_POSITIONS = [
|
super().__init__(name, size_x, size_y, size_z)
|
||||||
(0, 0, 0), (138, 0, 0), (276, 0, 0), (414, 0, 0), # T1-T4
|
self.slots = [None] * 16 # PRCXI 9300/9320 最大有 16 个槽位
|
||||||
(0, 96, 0), (138, 96, 0), (276, 96, 0), (414, 96, 0), # T5-T8
|
self.slot_locations = [Coordinate(0, 0, 0)] * 16
|
||||||
(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=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
|
|
||||||
)
|
|
||||||
self.root = self.get_root()
|
|
||||||
|
|
||||||
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:
|
|
||||||
existing = self.root.get_resource(resource.name)
|
|
||||||
if existing is not resource and existing.parent is not None:
|
|
||||||
existing.parent.unassign_child_resource(existing)
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
self.assign_child_resource(resource, spot=slot - 1, reassign=reassign)
|
if self.slots[slot - 1] is not None and not reassign:
|
||||||
|
raise ValueError(f"Spot {slot} is already occupied")
|
||||||
|
|
||||||
def serialize(self) -> dict:
|
self.slots[slot - 1] = resource
|
||||||
data = super().serialize()
|
super().assign_child_resource(resource, location=self.slot_locations[slot - 1])
|
||||||
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(Container):
|
class PRCXI9300Container(Plate):
|
||||||
"""PRCXI 9300 的专用 Container 类,继承自 Plate,用于槽位定位和未知模块。
|
"""PRCXI 9300 的专用 Container 类,继承自 Plate,用于槽位定位和未知模块。
|
||||||
|
|
||||||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||||||
@@ -204,10 +116,11 @@ class PRCXI9300Container(Container):
|
|||||||
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, model=model)
|
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, 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:
|
||||||
@@ -335,15 +248,14 @@ 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 中的值类型来决定如何处理:
|
# 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况)
|
||||||
# - 字符串值(从 JSON 反序列化): 只用键创建 ordering_param
|
# 如果是字符串,说明这是位置名称,需要让 TipRack 自己创建 Tip 对象
|
||||||
# - None 值(从第二次往返序列化): 同样只用键创建 ordering_param
|
# 我们只传递位置信息(键),不传递值,使用 ordering 参数
|
||||||
# - 对象值(已经是实际的 Resource 对象): 直接作为 ordered_items 使用
|
if ordering and isinstance(next(iter(ordering.values()), None), str):
|
||||||
first_val = next(iter(ordering.values()), None) if ordering else None
|
# ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict
|
||||||
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 的值已经是对象,可以直接使用
|
||||||
@@ -485,15 +397,14 @@ 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 中的值类型来决定如何处理:
|
# 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况)
|
||||||
# - 字符串值(从 JSON 反序列化): 只用键创建 ordering_param
|
# 如果是字符串,说明这是位置名称,需要让 TubeRack 自己创建 Tube 对象
|
||||||
# - None 值(从第二次往返序列化): 同样只用键创建 ordering_param
|
# 我们只传递位置信息(键),不传递值,使用 ordering 参数
|
||||||
# - 对象值(已经是实际的 Resource 对象): 直接作为 ordered_items 使用
|
if ordering and isinstance(next(iter(ordering.values()), None), str):
|
||||||
first_val = next(iter(ordering.values()), None) if ordering else None
|
# ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict
|
||||||
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 的值已经是对象,可以直接使用
|
||||||
@@ -654,14 +565,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 hasattr(child, "_unilabos_state") and "Material" in child._unilabos_state:
|
if "Material" in child.children[0]._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._unilabos_state["Material"]
|
Number=number, Code=f"T{number}", Material=child.children[0]._unilabos_state["Material"]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
if is_9320:
|
if is_9320:
|
||||||
print("当前设备是9320")
|
print("当前设备是9320")
|
||||||
# 始终初始化 step_mode 属性
|
# 始终初始化 step_mode 属性
|
||||||
@@ -798,7 +709,6 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
touch_tip: bool = False,
|
touch_tip: bool = False,
|
||||||
liquid_height: Optional[List[Optional[float]]] = None,
|
liquid_height: Optional[List[Optional[float]]] = None,
|
||||||
blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
||||||
blow_out_air_volume_before: Optional[List[Optional[float]]] = None,
|
|
||||||
spread: Literal["wide", "tight", "custom"] = "wide",
|
spread: Literal["wide", "tight", "custom"] = "wide",
|
||||||
is_96_well: bool = False,
|
is_96_well: bool = False,
|
||||||
mix_stage: Optional[Literal["none", "before", "after", "both"]] = "none",
|
mix_stage: Optional[Literal["none", "before", "after", "both"]] = "none",
|
||||||
@@ -809,9 +719,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
delays: Optional[List[int]] = None,
|
delays: Optional[List[int]] = None,
|
||||||
none_keys: List[str] = [],
|
none_keys: List[str] = [],
|
||||||
) -> TransferLiquidReturn:
|
) -> TransferLiquidReturn:
|
||||||
if self.step_mode:
|
return await super().transfer_liquid(
|
||||||
await self.create_protocol(f"transfer_liquid{time.time()}")
|
|
||||||
res = await super().transfer_liquid(
|
|
||||||
sources,
|
sources,
|
||||||
targets,
|
targets,
|
||||||
tip_racks,
|
tip_racks,
|
||||||
@@ -824,7 +732,6 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
touch_tip=touch_tip,
|
touch_tip=touch_tip,
|
||||||
liquid_height=liquid_height,
|
liquid_height=liquid_height,
|
||||||
blow_out_air_volume=blow_out_air_volume,
|
blow_out_air_volume=blow_out_air_volume,
|
||||||
blow_out_air_volume_before=blow_out_air_volume_before,
|
|
||||||
spread=spread,
|
spread=spread,
|
||||||
is_96_well=is_96_well,
|
is_96_well=is_96_well,
|
||||||
mix_stage=mix_stage,
|
mix_stage=mix_stage,
|
||||||
@@ -835,9 +742,6 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
delays=delays,
|
delays=delays,
|
||||||
none_keys=none_keys,
|
none_keys=none_keys,
|
||||||
)
|
)
|
||||||
if self.step_mode:
|
|
||||||
await self.run_protocol()
|
|
||||||
return res
|
|
||||||
|
|
||||||
async def custom_delay(self, seconds=0, msg=None):
|
async def custom_delay(self, seconds=0, msg=None):
|
||||||
return await super().custom_delay(seconds, msg)
|
return await super().custom_delay(seconds, msg)
|
||||||
@@ -854,10 +758,9 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
offsets: Optional[Coordinate] = None,
|
offsets: Optional[Coordinate] = None,
|
||||||
mix_rate: Optional[float] = None,
|
mix_rate: Optional[float] = None,
|
||||||
none_keys: List[str] = [],
|
none_keys: List[str] = [],
|
||||||
use_channels: Optional[List[int]] = [0],
|
|
||||||
):
|
):
|
||||||
return await self._unilabos_backend.mix(
|
return await self._unilabos_backend.mix(
|
||||||
targets, mix_time, mix_vol, height_to_bottom, offsets, mix_rate, none_keys, use_channels
|
targets, mix_time, mix_vol, height_to_bottom, offsets, mix_rate, none_keys
|
||||||
)
|
)
|
||||||
|
|
||||||
def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]:
|
def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]:
|
||||||
@@ -1286,15 +1189,9 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
offsets: Optional[Coordinate] = None,
|
offsets: Optional[Coordinate] = None,
|
||||||
mix_rate: Optional[float] = None,
|
mix_rate: Optional[float] = None,
|
||||||
none_keys: List[str] = [],
|
none_keys: List[str] = [],
|
||||||
use_channels: Optional[List[int]] = [0],
|
|
||||||
):
|
):
|
||||||
"""Mix liquid in the specified resources."""
|
"""Mix liquid in the specified resources."""
|
||||||
if use_channels == [0]:
|
|
||||||
axis = "Left"
|
|
||||||
elif use_channels == [1]:
|
|
||||||
axis = "Right"
|
|
||||||
else:
|
|
||||||
raise ValueError("Invalid use channels: " + str(use_channels))
|
|
||||||
plate_indexes = []
|
plate_indexes = []
|
||||||
for op in targets:
|
for op in targets:
|
||||||
deck = op.parent.parent.parent
|
deck = op.parent.parent.parent
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ class UniLiquidHandlerRvizBackend(LiquidHandlerBackend):
|
|||||||
self.total_height = total_height
|
self.total_height = total_height
|
||||||
self.joint_config = kwargs.get("joint_config", None)
|
self.joint_config = kwargs.get("joint_config", None)
|
||||||
self.lh_device_id = kwargs.get("lh_device_id", "lh_joint_publisher")
|
self.lh_device_id = kwargs.get("lh_device_id", "lh_joint_publisher")
|
||||||
self.simulate_rviz = kwargs.get("simulate_rviz", False)
|
|
||||||
if not rclpy.ok():
|
if not rclpy.ok():
|
||||||
rclpy.init()
|
rclpy.init()
|
||||||
self.joint_state_publisher = None
|
self.joint_state_publisher = None
|
||||||
@@ -70,7 +69,7 @@ class UniLiquidHandlerRvizBackend(LiquidHandlerBackend):
|
|||||||
self.joint_state_publisher = LiquidHandlerJointPublisher(
|
self.joint_state_publisher = LiquidHandlerJointPublisher(
|
||||||
joint_config=self.joint_config,
|
joint_config=self.joint_config,
|
||||||
lh_device_id=self.lh_device_id,
|
lh_device_id=self.lh_device_id,
|
||||||
simulate_rviz=self.simulate_rviz)
|
simulate_rviz=True)
|
||||||
|
|
||||||
# 启动ROS executor
|
# 启动ROS executor
|
||||||
self.executor = rclpy.executors.MultiThreadedExecutor()
|
self.executor = rclpy.executors.MultiThreadedExecutor()
|
||||||
|
|||||||
@@ -19,11 +19,10 @@ from rclpy.node import Node
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
||||||
def __init__(self,resources_config:list, resource_tracker, rate=50, device_id:str = "lh_joint_publisher", registry_name: str = "lh_joint_publisher", **kwargs):
|
def __init__(self,resources_config:list, resource_tracker, rate=50, device_id:str = "lh_joint_publisher", **kwargs):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
registry_name=registry_name,
|
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
hardware_interface={},
|
hardware_interface={},
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ class LiquidHandlerJointPublisher(Node):
|
|||||||
while self.resource_action is None:
|
while self.resource_action is None:
|
||||||
self.resource_action = self.check_tf_update_actions()
|
self.resource_action = self.check_tf_update_actions()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
self.get_logger().info(f'Waiting for TfUpdate server: {self.resource_action}')
|
|
||||||
|
|
||||||
self.resource_action_client = ActionClient(self, SendCmd, self.resource_action)
|
self.resource_action_client = ActionClient(self, SendCmd, self.resource_action)
|
||||||
while not self.resource_action_client.wait_for_server(timeout_sec=1.0):
|
while not self.resource_action_client.wait_for_server(timeout_sec=1.0):
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from threading import Lock, RLock
|
|||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
from unilabos.utils.decorator import not_action, always_free
|
from unilabos.utils.decorator import not_action
|
||||||
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, RETURN_UNILABOS_SAMPLES
|
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, RETURN_UNILABOS_SAMPLES
|
||||||
|
|
||||||
|
|
||||||
@@ -123,8 +123,8 @@ class VirtualWorkbench:
|
|||||||
_ros_node: BaseROS2DeviceNode
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
# 配置常量
|
# 配置常量
|
||||||
ARM_OPERATION_TIME: float = 2 # 机械臂操作时间(秒)
|
ARM_OPERATION_TIME: float = 3.0 # 机械臂操作时间(秒)
|
||||||
HEATING_TIME: float = 60.0 # 加热时间(秒)
|
HEATING_TIME: float = 10.0 # 加热时间(秒)
|
||||||
NUM_HEATING_STATIONS: int = 3 # 加热台数量
|
NUM_HEATING_STATIONS: int = 3 # 加热台数量
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||||
@@ -141,9 +141,9 @@ class VirtualWorkbench:
|
|||||||
self.data: Dict[str, Any] = {}
|
self.data: Dict[str, Any] = {}
|
||||||
|
|
||||||
# 从config中获取可配置参数
|
# 从config中获取可配置参数
|
||||||
self.ARM_OPERATION_TIME = float(self.config.get("arm_operation_time", self.ARM_OPERATION_TIME))
|
self.ARM_OPERATION_TIME = float(self.config.get("arm_operation_time", 3.0))
|
||||||
self.HEATING_TIME = float(self.config.get("heating_time", self.HEATING_TIME))
|
self.HEATING_TIME = float(self.config.get("heating_time", 10.0))
|
||||||
self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", self.NUM_HEATING_STATIONS))
|
self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", 3))
|
||||||
|
|
||||||
# 机械臂状态和锁 (使用threading.Lock)
|
# 机械臂状态和锁 (使用threading.Lock)
|
||||||
self._arm_lock = Lock()
|
self._arm_lock = Lock()
|
||||||
@@ -431,7 +431,6 @@ class VirtualWorkbench:
|
|||||||
sample_uuid, content in sample_uuids.items()]
|
sample_uuid, content in sample_uuids.items()]
|
||||||
}
|
}
|
||||||
|
|
||||||
@always_free
|
|
||||||
def start_heating(
|
def start_heating(
|
||||||
self,
|
self,
|
||||||
sample_uuids: SampleUUIDsType,
|
sample_uuids: SampleUUIDsType,
|
||||||
@@ -502,21 +501,10 @@ class VirtualWorkbench:
|
|||||||
|
|
||||||
self._update_data_status(f"加热台{station_id}开始加热{material_id}")
|
self._update_data_status(f"加热台{station_id}开始加热{material_id}")
|
||||||
|
|
||||||
# 打印当前所有正在加热的台位
|
# 模拟加热过程 (10秒)
|
||||||
with self._stations_lock:
|
|
||||||
heating_list = [
|
|
||||||
f"加热台{sid}:{s.current_material}"
|
|
||||||
for sid, s in self._heating_stations.items()
|
|
||||||
if s.state == HeatingStationState.HEATING and s.current_material
|
|
||||||
]
|
|
||||||
self.logger.info(f"[并行加热] 当前同时加热中: {', '.join(heating_list)}")
|
|
||||||
|
|
||||||
# 模拟加热过程
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
last_countdown_log = start_time
|
|
||||||
while True:
|
while True:
|
||||||
elapsed = time.time() - start_time
|
elapsed = time.time() - start_time
|
||||||
remaining = max(0.0, self.HEATING_TIME - elapsed)
|
|
||||||
progress = min(100.0, (elapsed / self.HEATING_TIME) * 100)
|
progress = min(100.0, (elapsed / self.HEATING_TIME) * 100)
|
||||||
|
|
||||||
with self._stations_lock:
|
with self._stations_lock:
|
||||||
@@ -524,11 +512,6 @@ class VirtualWorkbench:
|
|||||||
|
|
||||||
self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%")
|
self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%")
|
||||||
|
|
||||||
# 每5秒打印一次倒计时
|
|
||||||
if time.time() - last_countdown_log >= 5.0:
|
|
||||||
self.logger.info(f"[加热台{station_id}] {material_id} 剩余 {remaining:.1f}s")
|
|
||||||
last_countdown_log = time.time()
|
|
||||||
|
|
||||||
if elapsed >= self.HEATING_TIME:
|
if elapsed >= self.HEATING_TIME:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -96,13 +96,10 @@ serial:
|
|||||||
type: string
|
type: string
|
||||||
port:
|
port:
|
||||||
type: string
|
type: string
|
||||||
registry_name:
|
|
||||||
type: string
|
|
||||||
resource_tracker:
|
resource_tracker:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- device_id
|
- device_id
|
||||||
- registry_name
|
|
||||||
- port
|
- port
|
||||||
type: object
|
type: object
|
||||||
data:
|
data:
|
||||||
|
|||||||
@@ -67,9 +67,6 @@ camera:
|
|||||||
period:
|
period:
|
||||||
default: 0.1
|
default: 0.1
|
||||||
type: number
|
type: number
|
||||||
registry_name:
|
|
||||||
default: ''
|
|
||||||
type: string
|
|
||||||
resource_tracker:
|
resource_tracker:
|
||||||
type: object
|
type: object
|
||||||
required: []
|
required: []
|
||||||
|
|||||||
@@ -4976,13 +4976,13 @@ liquid_handler.biomek:
|
|||||||
handler_key: tip_rack
|
handler_key: tip_rack
|
||||||
label: tip_rack
|
label: tip_rack
|
||||||
output:
|
output:
|
||||||
- data_key: sources
|
- data_key: liquid
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: sources_out
|
handler_key: sources_out
|
||||||
label: sources
|
label: sources
|
||||||
- data_key: targets
|
- data_key: liquid
|
||||||
data_source: handle
|
data_source: executor
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets_out
|
handler_key: targets_out
|
||||||
label: targets
|
label: targets
|
||||||
@@ -7656,43 +7656,6 @@ liquid_handler.prcxi:
|
|||||||
title: iter_tips参数
|
title: iter_tips参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-magnetic_action:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
height: null
|
|
||||||
is_wait: null
|
|
||||||
module_no: null
|
|
||||||
time: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
height:
|
|
||||||
type: integer
|
|
||||||
is_wait:
|
|
||||||
type: boolean
|
|
||||||
module_no:
|
|
||||||
type: integer
|
|
||||||
time:
|
|
||||||
type: integer
|
|
||||||
required:
|
|
||||||
- time
|
|
||||||
- module_no
|
|
||||||
- height
|
|
||||||
- is_wait
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: magnetic_action参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommandAsync
|
|
||||||
auto-move_to:
|
auto-move_to:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -7726,31 +7689,6 @@ liquid_handler.prcxi:
|
|||||||
title: move_to参数
|
title: move_to参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommandAsync
|
type: UniLabJsonCommandAsync
|
||||||
auto-plr_pos_to_prcxi:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
resource: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
resource:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- resource
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: plr_pos_to_prcxi参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-post_init:
|
auto-post_init:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -7871,47 +7809,6 @@ liquid_handler.prcxi:
|
|||||||
title: shaker_action参数
|
title: shaker_action参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommandAsync
|
type: UniLabJsonCommandAsync
|
||||||
auto-shaking_incubation_action:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
amplitude: null
|
|
||||||
is_wait: null
|
|
||||||
module_no: null
|
|
||||||
temperature: null
|
|
||||||
time: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
amplitude:
|
|
||||||
type: integer
|
|
||||||
is_wait:
|
|
||||||
type: boolean
|
|
||||||
module_no:
|
|
||||||
type: integer
|
|
||||||
temperature:
|
|
||||||
type: integer
|
|
||||||
time:
|
|
||||||
type: integer
|
|
||||||
required:
|
|
||||||
- time
|
|
||||||
- module_no
|
|
||||||
- amplitude
|
|
||||||
- is_wait
|
|
||||||
- temperature
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: shaking_incubation_action参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommandAsync
|
|
||||||
auto-touch_tip:
|
auto-touch_tip:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -10137,28 +10034,116 @@ liquid_handler.prcxi:
|
|||||||
type: Transfer
|
type: Transfer
|
||||||
transfer_liquid:
|
transfer_liquid:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal:
|
||||||
|
asp_flow_rates: asp_flow_rates
|
||||||
|
asp_vols: asp_vols
|
||||||
|
blow_out_air_volume: blow_out_air_volume
|
||||||
|
delays: delays
|
||||||
|
dis_flow_rates: dis_flow_rates
|
||||||
|
dis_vols: dis_vols
|
||||||
|
is_96_well: is_96_well
|
||||||
|
liquid_height: liquid_height
|
||||||
|
mix_liquid_height: mix_liquid_height
|
||||||
|
mix_rate: mix_rate
|
||||||
|
mix_stage: mix_stage
|
||||||
|
mix_times: mix_times
|
||||||
|
mix_vol: mix_vol
|
||||||
|
none_keys: none_keys
|
||||||
|
offsets: offsets
|
||||||
|
sources: sources
|
||||||
|
spread: spread
|
||||||
|
targets: targets
|
||||||
|
tip_racks: tip_racks
|
||||||
|
touch_tip: touch_tip
|
||||||
|
use_channels: use_channels
|
||||||
goal_default:
|
goal_default:
|
||||||
asp_flow_rates: null
|
asp_flow_rates:
|
||||||
asp_vols: null
|
- 0.0
|
||||||
blow_out_air_volume: null
|
asp_vols:
|
||||||
blow_out_air_volume_before: null
|
- 0.0
|
||||||
delays: null
|
blow_out_air_volume:
|
||||||
dis_flow_rates: null
|
- 0.0
|
||||||
dis_vols: null
|
delays:
|
||||||
|
- 0
|
||||||
|
dis_flow_rates:
|
||||||
|
- 0.0
|
||||||
|
dis_vols:
|
||||||
|
- 0.0
|
||||||
is_96_well: false
|
is_96_well: false
|
||||||
liquid_height: null
|
liquid_height:
|
||||||
mix_liquid_height: null
|
- 0.0
|
||||||
mix_rate: null
|
mix_liquid_height: 0.0
|
||||||
mix_stage: none
|
mix_rate: 0
|
||||||
mix_times: null
|
mix_stage: ''
|
||||||
mix_vol: null
|
mix_times: 0
|
||||||
none_keys: []
|
mix_vol: 0
|
||||||
offsets: null
|
none_keys:
|
||||||
sources: null
|
- ''
|
||||||
spread: wide
|
offsets:
|
||||||
targets: null
|
- x: 0.0
|
||||||
tip_racks: null
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
sources:
|
||||||
|
- category: ''
|
||||||
|
children: []
|
||||||
|
config: ''
|
||||||
|
data: ''
|
||||||
|
id: ''
|
||||||
|
name: ''
|
||||||
|
parent: ''
|
||||||
|
pose:
|
||||||
|
orientation:
|
||||||
|
w: 1.0
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
position:
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
sample_id: ''
|
||||||
|
type: ''
|
||||||
|
spread: ''
|
||||||
|
targets:
|
||||||
|
- category: ''
|
||||||
|
children: []
|
||||||
|
config: ''
|
||||||
|
data: ''
|
||||||
|
id: ''
|
||||||
|
name: ''
|
||||||
|
parent: ''
|
||||||
|
pose:
|
||||||
|
orientation:
|
||||||
|
w: 1.0
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
position:
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
sample_id: ''
|
||||||
|
type: ''
|
||||||
|
tip_racks:
|
||||||
|
- category: ''
|
||||||
|
children: []
|
||||||
|
config: ''
|
||||||
|
data: ''
|
||||||
|
id: ''
|
||||||
|
name: ''
|
||||||
|
parent: ''
|
||||||
|
pose:
|
||||||
|
orientation:
|
||||||
|
w: 1.0
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
position:
|
||||||
|
x: 0.0
|
||||||
|
y: 0.0
|
||||||
|
z: 0.0
|
||||||
|
sample_id: ''
|
||||||
|
type: ''
|
||||||
touch_tip: false
|
touch_tip: false
|
||||||
use_channels:
|
use_channels:
|
||||||
- 0
|
- 0
|
||||||
@@ -10174,7 +10159,7 @@ liquid_handler.prcxi:
|
|||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets_identifier
|
handler_key: targets_identifier
|
||||||
label: 转移目标
|
label: 转移目标
|
||||||
- data_key: tip_racks
|
- data_key: tip_rack
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: tip_rack_identifier
|
handler_key: tip_rack_identifier
|
||||||
@@ -10198,7 +10183,11 @@ liquid_handler.prcxi:
|
|||||||
schema:
|
schema:
|
||||||
description: ''
|
description: ''
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
feedback:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
title: LiquidHandlerTransfer_Feedback
|
||||||
|
type: object
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
asp_flow_rates:
|
asp_flow_rates:
|
||||||
@@ -10213,10 +10202,6 @@ liquid_handler.prcxi:
|
|||||||
items:
|
items:
|
||||||
type: number
|
type: number
|
||||||
type: array
|
type: array
|
||||||
blow_out_air_volume_before:
|
|
||||||
items:
|
|
||||||
type: number
|
|
||||||
type: array
|
|
||||||
delays:
|
delays:
|
||||||
items:
|
items:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -10232,7 +10217,6 @@ liquid_handler.prcxi:
|
|||||||
type: number
|
type: number
|
||||||
type: array
|
type: array
|
||||||
is_96_well:
|
is_96_well:
|
||||||
default: false
|
|
||||||
type: boolean
|
type: boolean
|
||||||
liquid_height:
|
liquid_height:
|
||||||
items:
|
items:
|
||||||
@@ -10245,7 +10229,6 @@ liquid_handler.prcxi:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
mix_stage:
|
mix_stage:
|
||||||
default: none
|
|
||||||
type: string
|
type: string
|
||||||
mix_times:
|
mix_times:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
@@ -10256,7 +10239,6 @@ liquid_handler.prcxi:
|
|||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
type: integer
|
type: integer
|
||||||
none_keys:
|
none_keys:
|
||||||
default: []
|
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
@@ -10352,7 +10334,6 @@ liquid_handler.prcxi:
|
|||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
spread:
|
spread:
|
||||||
default: wide
|
|
||||||
type: string
|
type: string
|
||||||
targets:
|
targets:
|
||||||
items:
|
items:
|
||||||
@@ -10505,7 +10486,6 @@ liquid_handler.prcxi:
|
|||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
touch_tip:
|
touch_tip:
|
||||||
default: false
|
|
||||||
type: boolean
|
type: boolean
|
||||||
use_channels:
|
use_channels:
|
||||||
items:
|
items:
|
||||||
@@ -10514,221 +10494,45 @@ liquid_handler.prcxi:
|
|||||||
type: integer
|
type: integer
|
||||||
type: array
|
type: array
|
||||||
required:
|
required:
|
||||||
|
- asp_vols
|
||||||
|
- dis_vols
|
||||||
- sources
|
- sources
|
||||||
- targets
|
- targets
|
||||||
- tip_racks
|
- tip_racks
|
||||||
- asp_vols
|
- use_channels
|
||||||
- dis_vols
|
- asp_flow_rates
|
||||||
|
- dis_flow_rates
|
||||||
|
- offsets
|
||||||
|
- touch_tip
|
||||||
|
- liquid_height
|
||||||
|
- blow_out_air_volume
|
||||||
|
- spread
|
||||||
|
- is_96_well
|
||||||
|
- mix_stage
|
||||||
|
- mix_times
|
||||||
|
- mix_vol
|
||||||
|
- mix_rate
|
||||||
|
- mix_liquid_height
|
||||||
|
- delays
|
||||||
|
- none_keys
|
||||||
|
title: LiquidHandlerTransfer_Goal
|
||||||
type: object
|
type: object
|
||||||
result:
|
result:
|
||||||
$defs:
|
|
||||||
ResourceDict:
|
|
||||||
properties:
|
|
||||||
class:
|
|
||||||
description: Resource class name
|
|
||||||
title: Class
|
|
||||||
type: string
|
|
||||||
config:
|
|
||||||
additionalProperties: true
|
|
||||||
description: Resource configuration
|
|
||||||
title: Config
|
|
||||||
type: object
|
|
||||||
data:
|
|
||||||
additionalProperties: true
|
|
||||||
description: 'Resource data, eg: container liquid data'
|
|
||||||
title: Data
|
|
||||||
type: object
|
|
||||||
description:
|
|
||||||
default: ''
|
|
||||||
description: Resource description
|
|
||||||
title: Description
|
|
||||||
type: string
|
|
||||||
extra:
|
|
||||||
additionalProperties: true
|
|
||||||
description: 'Extra data, eg: slot index'
|
|
||||||
title: Extra
|
|
||||||
type: object
|
|
||||||
icon:
|
|
||||||
default: ''
|
|
||||||
description: Resource icon
|
|
||||||
title: Icon
|
|
||||||
type: string
|
|
||||||
id:
|
|
||||||
description: Resource ID
|
|
||||||
title: Id
|
|
||||||
type: string
|
|
||||||
model:
|
|
||||||
additionalProperties: true
|
|
||||||
description: Resource model
|
|
||||||
title: Model
|
|
||||||
type: object
|
|
||||||
name:
|
|
||||||
description: Resource name
|
|
||||||
title: Name
|
|
||||||
type: string
|
|
||||||
parent:
|
|
||||||
anyOf:
|
|
||||||
- $ref: '#/$defs/ResourceDict'
|
|
||||||
- type: 'null'
|
|
||||||
default: null
|
|
||||||
description: Parent resource object
|
|
||||||
parent_uuid:
|
|
||||||
anyOf:
|
|
||||||
- type: string
|
|
||||||
- type: 'null'
|
|
||||||
default: null
|
|
||||||
description: Parent resource uuid
|
|
||||||
title: Parent Uuid
|
|
||||||
pose:
|
|
||||||
$ref: '#/$defs/ResourceDictPosition'
|
|
||||||
description: Resource position
|
|
||||||
schema:
|
|
||||||
additionalProperties: true
|
|
||||||
description: Resource schema
|
|
||||||
title: Schema
|
|
||||||
type: object
|
|
||||||
type:
|
|
||||||
anyOf:
|
|
||||||
- const: device
|
|
||||||
type: string
|
|
||||||
- type: string
|
|
||||||
description: Resource type
|
|
||||||
title: Type
|
|
||||||
uuid:
|
|
||||||
description: Resource UUID
|
|
||||||
title: Uuid
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
- uuid
|
|
||||||
- name
|
|
||||||
- type
|
|
||||||
- class
|
|
||||||
- config
|
|
||||||
- data
|
|
||||||
- extra
|
|
||||||
title: ResourceDict
|
|
||||||
type: object
|
|
||||||
ResourceDictPosition:
|
|
||||||
properties:
|
|
||||||
cross_section_type:
|
|
||||||
default: rectangle
|
|
||||||
description: Cross section type
|
|
||||||
enum:
|
|
||||||
- rectangle
|
|
||||||
- circle
|
|
||||||
- rounded_rectangle
|
|
||||||
title: Cross Section Type
|
|
||||||
type: string
|
|
||||||
layout:
|
|
||||||
default: x-y
|
|
||||||
description: Resource layout
|
|
||||||
enum:
|
|
||||||
- 2d
|
|
||||||
- x-y
|
|
||||||
- z-y
|
|
||||||
- x-z
|
|
||||||
title: Layout
|
|
||||||
type: string
|
|
||||||
position:
|
|
||||||
$ref: '#/$defs/ResourceDictPositionObject'
|
|
||||||
description: Resource position
|
|
||||||
position3d:
|
|
||||||
$ref: '#/$defs/ResourceDictPositionObject'
|
|
||||||
description: Resource position in 3D space
|
|
||||||
rotation:
|
|
||||||
$ref: '#/$defs/ResourceDictPositionObject'
|
|
||||||
description: Resource rotation
|
|
||||||
scale:
|
|
||||||
$ref: '#/$defs/ResourceDictPositionScale'
|
|
||||||
description: Resource scale
|
|
||||||
size:
|
|
||||||
$ref: '#/$defs/ResourceDictPositionSize'
|
|
||||||
description: Resource size
|
|
||||||
title: ResourceDictPosition
|
|
||||||
type: object
|
|
||||||
ResourceDictPositionObject:
|
|
||||||
properties:
|
|
||||||
x:
|
|
||||||
default: 0.0
|
|
||||||
description: X coordinate
|
|
||||||
title: X
|
|
||||||
type: number
|
|
||||||
y:
|
|
||||||
default: 0.0
|
|
||||||
description: Y coordinate
|
|
||||||
title: Y
|
|
||||||
type: number
|
|
||||||
z:
|
|
||||||
default: 0.0
|
|
||||||
description: Z coordinate
|
|
||||||
title: Z
|
|
||||||
type: number
|
|
||||||
title: ResourceDictPositionObject
|
|
||||||
type: object
|
|
||||||
ResourceDictPositionScale:
|
|
||||||
properties:
|
|
||||||
x:
|
|
||||||
default: 0.0
|
|
||||||
description: x scale
|
|
||||||
title: X
|
|
||||||
type: number
|
|
||||||
y:
|
|
||||||
default: 0.0
|
|
||||||
description: y scale
|
|
||||||
title: Y
|
|
||||||
type: number
|
|
||||||
z:
|
|
||||||
default: 0.0
|
|
||||||
description: z scale
|
|
||||||
title: Z
|
|
||||||
type: number
|
|
||||||
title: ResourceDictPositionScale
|
|
||||||
type: object
|
|
||||||
ResourceDictPositionSize:
|
|
||||||
properties:
|
|
||||||
depth:
|
|
||||||
default: 0.0
|
|
||||||
description: Depth
|
|
||||||
title: Depth
|
|
||||||
type: number
|
|
||||||
height:
|
|
||||||
default: 0.0
|
|
||||||
description: Height
|
|
||||||
title: Height
|
|
||||||
type: number
|
|
||||||
width:
|
|
||||||
default: 0.0
|
|
||||||
description: Width
|
|
||||||
title: Width
|
|
||||||
type: number
|
|
||||||
title: ResourceDictPositionSize
|
|
||||||
type: object
|
|
||||||
properties:
|
properties:
|
||||||
sources:
|
return_info:
|
||||||
items:
|
type: string
|
||||||
items:
|
success:
|
||||||
$ref: '#/$defs/ResourceDict'
|
type: boolean
|
||||||
type: array
|
|
||||||
title: Sources
|
|
||||||
type: array
|
|
||||||
targets:
|
|
||||||
items:
|
|
||||||
items:
|
|
||||||
$ref: '#/$defs/ResourceDict'
|
|
||||||
type: array
|
|
||||||
title: Targets
|
|
||||||
type: array
|
|
||||||
required:
|
required:
|
||||||
- sources
|
- return_info
|
||||||
- targets
|
- success
|
||||||
title: TransferLiquidReturn
|
title: LiquidHandlerTransfer_Result
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: transfer_liquid参数
|
title: LiquidHandlerTransfer
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommandAsync
|
type: LiquidHandlerTransfer
|
||||||
module: unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Handler
|
module: unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Handler
|
||||||
status_types:
|
status_types:
|
||||||
reset_ok: bool
|
reset_ok: bool
|
||||||
@@ -10751,12 +10555,6 @@ liquid_handler.prcxi:
|
|||||||
type: string
|
type: string
|
||||||
deck:
|
deck:
|
||||||
type: object
|
type: object
|
||||||
deck_y:
|
|
||||||
default: 400
|
|
||||||
type: string
|
|
||||||
deck_z:
|
|
||||||
default: 300
|
|
||||||
type: string
|
|
||||||
host:
|
host:
|
||||||
type: string
|
type: string
|
||||||
is_9320:
|
is_9320:
|
||||||
@@ -10767,44 +10565,17 @@ liquid_handler.prcxi:
|
|||||||
type: string
|
type: string
|
||||||
port:
|
port:
|
||||||
type: integer
|
type: integer
|
||||||
rail_interval:
|
|
||||||
default: 0
|
|
||||||
type: string
|
|
||||||
rail_nums:
|
|
||||||
default: 4
|
|
||||||
type: string
|
|
||||||
rail_width:
|
|
||||||
default: 27.5
|
|
||||||
type: string
|
|
||||||
setup:
|
setup:
|
||||||
default: true
|
default: true
|
||||||
type: string
|
type: string
|
||||||
simulator:
|
simulator:
|
||||||
default: false
|
default: false
|
||||||
type: string
|
type: string
|
||||||
start_rail:
|
|
||||||
default: 2
|
|
||||||
type: string
|
|
||||||
step_mode:
|
step_mode:
|
||||||
default: false
|
default: false
|
||||||
type: string
|
type: string
|
||||||
timeout:
|
timeout:
|
||||||
type: number
|
type: number
|
||||||
x_increase:
|
|
||||||
default: -0.003636
|
|
||||||
type: string
|
|
||||||
x_offset:
|
|
||||||
default: -0.8
|
|
||||||
type: string
|
|
||||||
xy_coupling:
|
|
||||||
default: -0.0045
|
|
||||||
type: string
|
|
||||||
y_increase:
|
|
||||||
default: -0.003636
|
|
||||||
type: string
|
|
||||||
y_offset:
|
|
||||||
default: -37.98
|
|
||||||
type: string
|
|
||||||
required:
|
required:
|
||||||
- deck
|
- deck
|
||||||
- host
|
- host
|
||||||
|
|||||||
@@ -6090,7 +6090,6 @@ virtual_workbench:
|
|||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-start_heating:
|
auto-start_heating:
|
||||||
always_free: true
|
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
goal_default:
|
goal_default:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import sys
|
|||||||
import inspect
|
import inspect
|
||||||
import importlib
|
import importlib
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Union, Tuple
|
from typing import Any, Dict, List, Union, Tuple
|
||||||
@@ -89,14 +88,6 @@ class Registry:
|
|||||||
)
|
)
|
||||||
test_latency_schema["description"] = "用于测试延迟的动作,返回延迟时间和时间差。"
|
test_latency_schema["description"] = "用于测试延迟的动作,返回延迟时间和时间差。"
|
||||||
|
|
||||||
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_method_info.get("args", []),
|
|
||||||
"test_resource",
|
|
||||||
test_resource_method_info.get("return_annotation"),
|
|
||||||
)
|
|
||||||
test_resource_schema["description"] = "用于测试物料、设备和样本。"
|
|
||||||
|
|
||||||
self.device_type_registry.update(
|
self.device_type_registry.update(
|
||||||
{
|
{
|
||||||
"host_node": {
|
"host_node": {
|
||||||
@@ -175,8 +166,7 @@ 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
|
"class_name": "unilabos_class",
|
||||||
"slot_on_deck": "unilabos_resource_slot:parent", # 勾选的parent的config中的sites的name,展示name,参数对应slot(index)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"test_latency": {
|
"test_latency": {
|
||||||
@@ -199,7 +189,32 @@ class Registry:
|
|||||||
"goal": {},
|
"goal": {},
|
||||||
"feedback": {},
|
"feedback": {},
|
||||||
"result": {},
|
"result": {},
|
||||||
"schema": test_resource_schema,
|
"schema": {
|
||||||
|
"description": "",
|
||||||
|
"properties": {
|
||||||
|
"feedback": {},
|
||||||
|
"goal": {
|
||||||
|
"properties": {
|
||||||
|
"resource": ros_message_to_json_schema(Resource, "resource"),
|
||||||
|
"resources": {
|
||||||
|
"items": {
|
||||||
|
"properties": ros_message_to_json_schema(
|
||||||
|
Resource, "resources"
|
||||||
|
),
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
"device": {"type": "string"},
|
||||||
|
"devices": {"items": {"type": "string"}, "type": "array"},
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"result": {},
|
||||||
|
},
|
||||||
|
"title": "test_resource",
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
"placeholder_keys": {
|
"placeholder_keys": {
|
||||||
"device": "unilabos_devices",
|
"device": "unilabos_devices",
|
||||||
"devices": "unilabos_devices",
|
"devices": "unilabos_devices",
|
||||||
@@ -823,7 +838,6 @@ class Registry:
|
|||||||
("list", "unilabos.registry.placeholder_type:DeviceSlot"),
|
("list", "unilabos.registry.placeholder_type:DeviceSlot"),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
**({"always_free": True} if v.get("always_free") else {}),
|
|
||||||
}
|
}
|
||||||
for k, v in enhanced_info["action_methods"].items()
|
for k, v in enhanced_info["action_methods"].items()
|
||||||
if k not in device_config["class"]["action_value_mappings"]
|
if k not in device_config["class"]["action_value_mappings"]
|
||||||
@@ -929,7 +943,6 @@ class Registry:
|
|||||||
if is_valid:
|
if is_valid:
|
||||||
results.append((file, data, device_ids))
|
results.append((file, data, device_ids))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
|
||||||
logger.warning(f"[UniLab Registry] 处理设备文件异常: {file}, 错误: {e}")
|
logger.warning(f"[UniLab Registry] 处理设备文件异常: {file}, 错误: {e}")
|
||||||
|
|
||||||
# 线程安全地更新注册表
|
# 线程安全地更新注册表
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,10 @@
|
|||||||
|
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):
|
||||||
@@ -12,12 +16,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]):
|
||||||
super().load_state(state)
|
self.state = state
|
||||||
|
|
||||||
|
|
||||||
def get_regular_container(name="container"):
|
def get_regular_container(name="container"):
|
||||||
@@ -25,6 +29,7 @@ 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
|
||||||
|
|||||||
@@ -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", "extra"]:
|
if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children", "pose"]:
|
||||||
v = node.pop(k)
|
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:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -16,7 +16,6 @@ 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"
|
||||||
|
|
||||||
@@ -39,60 +38,24 @@ 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 ResourceDictPoseExtraObjectType(BaseModel):
|
|
||||||
z_index: int
|
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictPoseExtraObject(BaseModel):
|
|
||||||
z_index: Optional[int] = Field(alias="zIndex", default=None)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -109,25 +72,6 @@ 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[ResourceDictPoseExtraObject] = 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 不序列化
|
||||||
@@ -421,15 +365,6 @@ 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]
|
||||||
@@ -473,7 +408,6 @@ 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)
|
||||||
@@ -534,17 +468,10 @@ class ResourceTreeSet(object):
|
|||||||
trees.append(tree_instance)
|
trees.append(tree_instance)
|
||||||
return cls(trees)
|
return cls(trees)
|
||||||
|
|
||||||
def to_plr_resources(
|
def to_plr_resources(self, skip_devices=True) -> List["PLRResource"]:
|
||||||
self, skip_devices: bool = True, requested_uuids: Optional[List[str]] = None
|
|
||||||
) -> List["PLRResource"]:
|
|
||||||
"""
|
"""
|
||||||
将 ResourceTreeSet 转换为 PLR 资源列表
|
将 ResourceTreeSet 转换为 PLR 资源列表
|
||||||
|
|
||||||
Args:
|
|
||||||
skip_devices: 是否跳过 device 类型节点
|
|
||||||
requested_uuids: 若指定,则按此 UUID 顺序返回对应资源(用于批量查询时一一对应),
|
|
||||||
否则返回各树的根节点列表
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[PLRResource]: PLR 资源实例列表
|
List[PLRResource]: PLR 资源实例列表
|
||||||
"""
|
"""
|
||||||
@@ -566,7 +493,6 @@ 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)
|
||||||
@@ -600,71 +526,6 @@ class ResourceTreeSet(object):
|
|||||||
d["model"] = res.config.get("model", None)
|
d["model"] = res.config.get("model", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
# deserialize 会单独处理的元数据 key,不传给构造函数
|
|
||||||
_META_KEYS = {"type", "parent_name", "location", "children", "rotation", "barcode"}
|
|
||||||
# deserialize 自定义逻辑使用的 key(如 TipSpot 用 prototype_tip 构建 make_tip),需保留
|
|
||||||
_DESERIALIZE_PRESERVED_KEYS = {"prototype_tip"}
|
|
||||||
|
|
||||||
def remove_incompatible_params(plr_d: dict) -> None:
|
|
||||||
"""递归移除 PLR 类不接受的参数,避免 deserialize 报错。
|
|
||||||
- 移除构造函数不接受的参数(如 compute_height_from_volume、ordering、category)
|
|
||||||
- 对 TubeRack:将 ordering 转为 ordered_items
|
|
||||||
- 保留 deserialize 自定义逻辑需要的 key(如 prototype_tip)
|
|
||||||
"""
|
|
||||||
if "type" in plr_d:
|
|
||||||
sub_cls = find_subclass(plr_d["type"], PLRResource)
|
|
||||||
if sub_cls is not None:
|
|
||||||
spec = inspect.signature(sub_cls)
|
|
||||||
valid_params = set(spec.parameters.keys())
|
|
||||||
# TubeRack 特殊处理:先转换 ordering,再参与后续过滤
|
|
||||||
if "ordering" not in valid_params and "ordering" in plr_d:
|
|
||||||
ordering = plr_d.pop("ordering", None)
|
|
||||||
if sub_cls.__name__ == "TubeRack":
|
|
||||||
plr_d["ordered_items"] = (
|
|
||||||
_ordering_to_ordered_items(plr_d, ordering)
|
|
||||||
if ordering
|
|
||||||
else {}
|
|
||||||
)
|
|
||||||
# 移除构造函数不接受的参数(保留 META 和 deserialize 自定义逻辑需要的 key)
|
|
||||||
for key in list(plr_d.keys()):
|
|
||||||
if (
|
|
||||||
key not in _META_KEYS
|
|
||||||
and key not in _DESERIALIZE_PRESERVED_KEYS
|
|
||||||
and key not in valid_params
|
|
||||||
):
|
|
||||||
plr_d.pop(key, None)
|
|
||||||
for child in plr_d.get("children", []):
|
|
||||||
remove_incompatible_params(child)
|
|
||||||
|
|
||||||
def _ordering_to_ordered_items(plr_d: dict, ordering: dict) -> dict:
|
|
||||||
"""将 ordering 转为 ordered_items,从 children 构建 Tube 对象"""
|
|
||||||
from pylabrobot.resources import Tube, Coordinate
|
|
||||||
from pylabrobot.serializer import deserialize as plr_deserialize
|
|
||||||
|
|
||||||
children = plr_d.get("children", [])
|
|
||||||
ordered_items = {}
|
|
||||||
for idx, (ident, child_name) in enumerate(ordering.items()):
|
|
||||||
child_data = children[idx] if idx < len(children) else None
|
|
||||||
if child_data is None:
|
|
||||||
continue
|
|
||||||
loc_data = child_data.get("location")
|
|
||||||
loc = (
|
|
||||||
plr_deserialize(loc_data)
|
|
||||||
if loc_data
|
|
||||||
else Coordinate(0, 0, 0)
|
|
||||||
)
|
|
||||||
tube = Tube(
|
|
||||||
name=child_data.get("name", child_name or ident),
|
|
||||||
size_x=child_data.get("size_x", 10),
|
|
||||||
size_y=child_data.get("size_y", 10),
|
|
||||||
size_z=child_data.get("size_z", 50),
|
|
||||||
max_volume=child_data.get("max_volume", 1000),
|
|
||||||
)
|
|
||||||
tube.location = loc
|
|
||||||
ordered_items[ident] = tube
|
|
||||||
plr_d["children"] = [] # 已并入 ordered_items,避免重复反序列化
|
|
||||||
return ordered_items
|
|
||||||
|
|
||||||
plr_resources = []
|
plr_resources = []
|
||||||
tracker = DeviceNodeResourceTracker()
|
tracker = DeviceNodeResourceTracker()
|
||||||
|
|
||||||
@@ -684,7 +545,9 @@ class ResourceTreeSet(object):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}"
|
f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}"
|
||||||
)
|
)
|
||||||
remove_incompatible_params(plr_dict)
|
spec = inspect.signature(sub_cls)
|
||||||
|
if "category" not in spec.parameters:
|
||||||
|
plr_dict.pop("category", None)
|
||||||
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
|
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
|
||||||
from pylabrobot.resources import Coordinate
|
from pylabrobot.resources import Coordinate
|
||||||
from pylabrobot.serializer import deserialize
|
from pylabrobot.serializer import deserialize
|
||||||
@@ -704,41 +567,6 @@ class ResourceTreeSet(object):
|
|||||||
logger.error(f"堆栈: {traceback.format_exc()}")
|
logger.error(f"堆栈: {traceback.format_exc()}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if requested_uuids:
|
|
||||||
# 按请求的 UUID 顺序返回对应资源(从整棵树中按 uuid 提取)
|
|
||||||
# 优先使用 tracker.uuid_to_resources;若映射缺失,再递归遍历 PLR 树兜底搜索。
|
|
||||||
def _find_plr_by_uuid(roots: List["PLRResource"], uid: str) -> Optional["PLRResource"]:
|
|
||||||
stack = list(roots)
|
|
||||||
while stack:
|
|
||||||
node = stack.pop()
|
|
||||||
node_uid = getattr(node, "unilabos_uuid", None)
|
|
||||||
if node_uid == uid:
|
|
||||||
return node
|
|
||||||
children = getattr(node, "children", None) or []
|
|
||||||
stack.extend(children)
|
|
||||||
return None
|
|
||||||
|
|
||||||
result = []
|
|
||||||
missing_uuids = []
|
|
||||||
for uid in requested_uuids:
|
|
||||||
found = tracker.uuid_to_resources.get(uid)
|
|
||||||
if found is None:
|
|
||||||
found = _find_plr_by_uuid(plr_resources, uid)
|
|
||||||
if found is not None:
|
|
||||||
# 回填缓存,后续相同 uuid 可直接命中
|
|
||||||
tracker.uuid_to_resources[uid] = found
|
|
||||||
if found is None:
|
|
||||||
missing_uuids.append(uid)
|
|
||||||
else:
|
|
||||||
result.append(found)
|
|
||||||
|
|
||||||
if missing_uuids:
|
|
||||||
raise ValueError(
|
|
||||||
f"请求的 UUID 未在资源树中找到: {missing_uuids}。"
|
|
||||||
f"可用 UUID 数量: {len(tracker.uuid_to_resources)},"
|
|
||||||
f"资源树数量: {len(self.trees)}"
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
return plr_resources
|
return plr_resources
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ def ros2_device_node(
|
|||||||
# 从属性中自动发现可发布状态
|
# 从属性中自动发现可发布状态
|
||||||
if status_types is None:
|
if status_types is None:
|
||||||
status_types = {}
|
status_types = {}
|
||||||
assert device_config is not None, "device_config cannot be None"
|
if device_config is None:
|
||||||
|
raise ValueError("device_config cannot be None")
|
||||||
if action_value_mappings is None:
|
if action_value_mappings is None:
|
||||||
action_value_mappings = {}
|
action_value_mappings = {}
|
||||||
if hardware_interface is None:
|
if hardware_interface is None:
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ def main(
|
|||||||
bridges: List[Any] = [],
|
bridges: List[Any] = [],
|
||||||
visual: str = "disable",
|
visual: str = "disable",
|
||||||
resources_mesh_config: dict = {},
|
resources_mesh_config: dict = {},
|
||||||
resources_mesh_resource_list: list = [],
|
|
||||||
rclpy_init_args: List[str] = ["--log-level", "debug"],
|
rclpy_init_args: List[str] = ["--log-level", "debug"],
|
||||||
discovery_interval: float = 15.0,
|
discovery_interval: float = 15.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -78,12 +77,12 @@ def main(
|
|||||||
if visual != "disable":
|
if visual != "disable":
|
||||||
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
|
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
|
||||||
|
|
||||||
# 优先使用从 main.py 传入的完整资源列表(包含所有子资源)
|
# 将 ResourceTreeSet 转换为 list 用于 visual 组件
|
||||||
if resources_mesh_resource_list:
|
resources_list = (
|
||||||
resources_list = resources_mesh_resource_list
|
[node.res_content.model_dump(by_alias=True) for node in resources_config.all_nodes]
|
||||||
else:
|
if resources_config
|
||||||
# fallback: 从 ResourceTreeSet 获取
|
else []
|
||||||
resources_list = [node.res_content.model_dump(by_alias=True) for node in resources_config.all_nodes]
|
)
|
||||||
resource_mesh_manager = ResourceMeshManager(
|
resource_mesh_manager = ResourceMeshManager(
|
||||||
resources_mesh_config,
|
resources_mesh_config,
|
||||||
resources_list,
|
resources_list,
|
||||||
@@ -91,7 +90,7 @@ def main(
|
|||||||
device_id="resource_mesh_manager",
|
device_id="resource_mesh_manager",
|
||||||
device_uuid=str(uuid.uuid4()),
|
device_uuid=str(uuid.uuid4()),
|
||||||
)
|
)
|
||||||
joint_republisher = JointRepublisher("joint_republisher","joint_republisher", host_node.resource_tracker)
|
joint_republisher = JointRepublisher("joint_republisher", host_node.resource_tracker)
|
||||||
# lh_joint_pub = LiquidHandlerJointPublisher(
|
# lh_joint_pub = LiquidHandlerJointPublisher(
|
||||||
# resources_config=resources_list, resource_tracker=host_node.resource_tracker
|
# resources_config=resources_list, resource_tracker=host_node.resource_tracker
|
||||||
# )
|
# )
|
||||||
@@ -115,7 +114,6 @@ def slave(
|
|||||||
bridges: List[Any] = [],
|
bridges: List[Any] = [],
|
||||||
visual: str = "disable",
|
visual: str = "disable",
|
||||||
resources_mesh_config: dict = {},
|
resources_mesh_config: dict = {},
|
||||||
resources_mesh_resource_list: list = [],
|
|
||||||
rclpy_init_args: List[str] = ["--log-level", "debug"],
|
rclpy_init_args: List[str] = ["--log-level", "debug"],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""从节点函数"""
|
"""从节点函数"""
|
||||||
@@ -210,12 +208,12 @@ def slave(
|
|||||||
if visual != "disable":
|
if visual != "disable":
|
||||||
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
|
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
|
||||||
|
|
||||||
# 优先使用从 main.py 传入的完整资源列表(包含所有子资源)
|
# 将 ResourceTreeSet 转换为 list 用于 visual 组件
|
||||||
if resources_mesh_resource_list:
|
resources_list = (
|
||||||
resources_list = resources_mesh_resource_list
|
[node.res_content.model_dump(by_alias=True) for node in resources_config.all_nodes]
|
||||||
else:
|
if resources_config
|
||||||
# fallback: 从 ResourceTreeSet 获取
|
else []
|
||||||
resources_list = [node.res_content.model_dump(by_alias=True) for node in resources_config.all_nodes]
|
)
|
||||||
resource_mesh_manager = ResourceMeshManager(
|
resource_mesh_manager = ResourceMeshManager(
|
||||||
resources_mesh_config,
|
resources_mesh_config,
|
||||||
resources_list,
|
resources_list,
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ def init_wrapper(
|
|||||||
device_id: str,
|
device_id: str,
|
||||||
device_uuid: str,
|
device_uuid: str,
|
||||||
driver_class: type[T],
|
driver_class: type[T],
|
||||||
device_config: ResourceDictInstance,
|
device_config: ResourceTreeInstance,
|
||||||
status_types: Dict[str, Any],
|
status_types: Dict[str, Any],
|
||||||
action_value_mappings: Dict[str, Any],
|
action_value_mappings: Dict[str, Any],
|
||||||
hardware_interface: Dict[str, Any],
|
hardware_interface: Dict[str, Any],
|
||||||
@@ -279,7 +279,6 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
self,
|
self,
|
||||||
driver_instance: T,
|
driver_instance: T,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
registry_name: str,
|
|
||||||
device_uuid: str,
|
device_uuid: str,
|
||||||
status_types: Dict[str, Any],
|
status_types: Dict[str, Any],
|
||||||
action_value_mappings: Dict[str, Any],
|
action_value_mappings: Dict[str, Any],
|
||||||
@@ -301,7 +300,6 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
"""
|
"""
|
||||||
self.driver_instance = driver_instance
|
self.driver_instance = driver_instance
|
||||||
self.device_id = device_id
|
self.device_id = device_id
|
||||||
self.registry_name = registry_name
|
|
||||||
self.uuid = device_uuid
|
self.uuid = device_uuid
|
||||||
self.publish_high_frequency = False
|
self.publish_high_frequency = False
|
||||||
self.callback_group = ReentrantCallbackGroup()
|
self.callback_group = ReentrantCallbackGroup()
|
||||||
@@ -418,9 +416,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
if len(rts.root_nodes) == 1 and isinstance(rts_plr_instances[0], RegularContainer):
|
if len(rts.root_nodes) == 1 and isinstance(rts_plr_instances[0], RegularContainer):
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
container_instance: RegularContainer = rts_plr_instances[0]
|
container_instance: RegularContainer = rts_plr_instances[0]
|
||||||
found_resources = self.resource_tracker.figure_resource(
|
found_resources = self.resource_tracker.figure_resource({"name": container_instance.name}, try_mode=True)
|
||||||
{"name": container_instance.name}, try_mode=True
|
|
||||||
)
|
|
||||||
if not len(found_resources):
|
if not len(found_resources):
|
||||||
self.resource_tracker.add_resource(container_instance)
|
self.resource_tracker.add_resource(container_instance)
|
||||||
logger.info(f"添加物料{container_instance.name}到资源跟踪器")
|
logger.info(f"添加物料{container_instance.name}到资源跟踪器")
|
||||||
@@ -460,7 +456,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") and self.node_name != "host_node":
|
if hasattr(self.driver_instance, "create_resource"):
|
||||||
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,24 +911,8 @@ 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 = None
|
site_index = sites.index(original_instance)
|
||||||
try:
|
site_name = site_names[site_index]
|
||||||
# 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:
|
||||||
@@ -940,14 +920,6 @@ 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
|
||||||
@@ -1008,7 +980,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
].call_async(
|
].call_async(
|
||||||
r
|
r
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
self.lab_logger().trace(f"确认资源云端 Add 结果: {response.response}")
|
self.lab_logger().info(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:
|
||||||
@@ -1034,7 +1006,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
].call_async(
|
].call_async(
|
||||||
r
|
r
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
self.lab_logger().trace(f"确认资源云端 Update 结果: {response.response}")
|
self.lab_logger().info(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)
|
||||||
@@ -1180,7 +1152,6 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
"machine_name": BasicConfig.machine_name,
|
"machine_name": BasicConfig.machine_name,
|
||||||
"type": "slave",
|
"type": "slave",
|
||||||
"edge_device_id": self.device_id,
|
"edge_device_id": self.device_id,
|
||||||
"registry_name": self.registry_name,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
@@ -1655,7 +1626,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
else:
|
else:
|
||||||
resolved_sample_uuids[sample_uuid] = material_uuid
|
resolved_sample_uuids[sample_uuid] = material_uuid
|
||||||
function_args[PARAM_SAMPLE_UUIDS] = resolved_sample_uuids
|
function_args[PARAM_SAMPLE_UUIDS] = resolved_sample_uuids
|
||||||
self.lab_logger().debug(f"[JsonCommand] 注入 {PARAM_SAMPLE_UUIDS}: {resolved_sample_uuids}")
|
self.lab_logger().debug(
|
||||||
|
f"[JsonCommand] 注入 {PARAM_SAMPLE_UUIDS}: {resolved_sample_uuids}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 处理单个 ResourceSlot
|
# 处理单个 ResourceSlot
|
||||||
@@ -2032,7 +2005,6 @@ class ROS2DeviceNode:
|
|||||||
|
|
||||||
if driver_is_ros:
|
if driver_is_ros:
|
||||||
driver_params["device_id"] = device_id
|
driver_params["device_id"] = device_id
|
||||||
driver_params["registry_name"] = device_config.res_content.klass
|
|
||||||
driver_params["resource_tracker"] = self.resource_tracker
|
driver_params["resource_tracker"] = self.resource_tracker
|
||||||
self._driver_instance = self._driver_creator.create_instance(driver_params)
|
self._driver_instance = self._driver_creator.create_instance(driver_params)
|
||||||
if self._driver_instance is None:
|
if self._driver_instance is None:
|
||||||
@@ -2050,7 +2022,6 @@ class ROS2DeviceNode:
|
|||||||
children=children,
|
children=children,
|
||||||
driver_instance=self._driver_instance, # type: ignore
|
driver_instance=self._driver_instance, # type: ignore
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
registry_name=device_config.res_content.klass,
|
|
||||||
device_uuid=device_uuid,
|
device_uuid=device_uuid,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings=action_value_mappings,
|
action_value_mappings=action_value_mappings,
|
||||||
@@ -2062,7 +2033,6 @@ class ROS2DeviceNode:
|
|||||||
self._ros_node = BaseROS2DeviceNode(
|
self._ros_node = BaseROS2DeviceNode(
|
||||||
driver_instance=self._driver_instance,
|
driver_instance=self._driver_instance,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
registry_name=device_config.res_content.klass,
|
|
||||||
device_uuid=device_uuid,
|
device_uuid=device_uuid,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings=action_value_mappings,
|
action_value_mappings=action_value_mappings,
|
||||||
@@ -2071,7 +2041,6 @@ class ROS2DeviceNode:
|
|||||||
resource_tracker=self.resource_tracker,
|
resource_tracker=self.resource_tracker,
|
||||||
)
|
)
|
||||||
self._ros_node: BaseROS2DeviceNode
|
self._ros_node: BaseROS2DeviceNode
|
||||||
# 将注册表类型名传递给BaseROS2DeviceNode,用于slave上报
|
|
||||||
self._ros_node.lab_logger().info(f"初始化完成 {self._ros_node.uuid} {self.driver_is_ros}")
|
self._ros_node.lab_logger().info(f"初始化完成 {self._ros_node.uuid} {self.driver_is_ros}")
|
||||||
self.driver_instance._ros_node = self._ros_node # type: ignore
|
self.driver_instance._ros_node = self._ros_node # type: ignore
|
||||||
self.driver_instance._execute_driver_command = self._ros_node._execute_driver_command # type: ignore
|
self.driver_instance._execute_driver_command = self._ros_node._execute_driver_command # type: ignore
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ from cv_bridge import CvBridge
|
|||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker
|
||||||
|
|
||||||
class VideoPublisher(BaseROS2DeviceNode):
|
class VideoPublisher(BaseROS2DeviceNode):
|
||||||
def __init__(self, device_id='video_publisher', registry_name="", device_uuid='', camera_index=0, period: float = 0.1, resource_tracker: DeviceNodeResourceTracker = None):
|
def __init__(self, device_id='video_publisher', device_uuid='', camera_index=0, period: float = 0.1, resource_tracker: DeviceNodeResourceTracker = None):
|
||||||
# 初始化BaseROS2DeviceNode,使用自身作为driver_instance
|
# 初始化BaseROS2DeviceNode,使用自身作为driver_instance
|
||||||
BaseROS2DeviceNode.__init__(
|
BaseROS2DeviceNode.__init__(
|
||||||
self,
|
self,
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
registry_name=registry_name,
|
|
||||||
device_uuid=device_uuid,
|
device_uuid=device_uuid,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ class ControllerNode(BaseROS2DeviceNode):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
registry_name: str,
|
|
||||||
controller_func: Callable,
|
controller_func: Callable,
|
||||||
update_rate: float,
|
update_rate: float,
|
||||||
inputs: Dict[str, Dict[str, type | str]],
|
inputs: Dict[str, Dict[str, type | str]],
|
||||||
@@ -52,7 +51,6 @@ class ControllerNode(BaseROS2DeviceNode):
|
|||||||
self,
|
self,
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
registry_name=registry_name,
|
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings=action_value_mappings,
|
action_value_mappings=action_value_mappings,
|
||||||
hardware_interface=hardware_interface,
|
hardware_interface=hardware_interface,
|
||||||
|
|||||||
@@ -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, SampleUUIDsType, LabSample,
|
PARAM_SAMPLE_UUIDS,
|
||||||
)
|
)
|
||||||
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 (
|
||||||
@@ -51,7 +51,6 @@ from unilabos.utils import logger
|
|||||||
from unilabos.utils.exception import DeviceClassInvalid
|
from unilabos.utils.exception import DeviceClassInvalid
|
||||||
from unilabos.utils.log import warning
|
from unilabos.utils.log import warning
|
||||||
from unilabos.utils.type_check import serialize_result_info
|
from unilabos.utils.type_check import serialize_result_info
|
||||||
from unilabos.config.config import BasicConfig
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from unilabos.app.ws_client import QueueItem
|
from unilabos.app.ws_client import QueueItem
|
||||||
@@ -64,8 +63,7 @@ class DeviceActionStatus:
|
|||||||
|
|
||||||
class TestResourceReturn(TypedDict):
|
class TestResourceReturn(TypedDict):
|
||||||
resources: List[List[ResourceDict]]
|
resources: List[List[ResourceDict]]
|
||||||
devices: List[Dict[str, Any]]
|
devices: List[DeviceSlot]
|
||||||
unilabos_samples: List[LabSample]
|
|
||||||
|
|
||||||
|
|
||||||
class TestLatencyReturn(TypedDict):
|
class TestLatencyReturn(TypedDict):
|
||||||
@@ -250,7 +248,6 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self,
|
self,
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
registry_name="host_node",
|
|
||||||
device_uuid=host_node_dict["uuid"],
|
device_uuid=host_node_dict["uuid"],
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"],
|
action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"],
|
||||||
@@ -305,8 +302,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
} # 用来存储多个ActionClient实例
|
} # 用来存储多个ActionClient实例
|
||||||
self._action_value_mappings: Dict[str, Dict] = (
|
self._action_value_mappings: Dict[str, Dict] = (
|
||||||
{}
|
{}
|
||||||
) # device_id -> action_value_mappings(本地+远程设备统一存储)
|
) # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系
|
||||||
self._slave_registry_configs: Dict[str, Dict] = {} # registry_name -> registry_config(含action_value_mappings)
|
|
||||||
self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态
|
self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态
|
||||||
self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备
|
self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备
|
||||||
self._last_discovery_time = 0.0 # 上次设备发现的时间
|
self._last_discovery_time = 0.0 # 上次设备发现的时间
|
||||||
@@ -640,8 +636,6 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
self.device_machine_names[device_id] = "本地"
|
self.device_machine_names[device_id] = "本地"
|
||||||
self.devices_instances[device_id] = d
|
self.devices_instances[device_id] = d
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
self._action_value_mappings[device_id] = d._ros_node._action_value_mappings
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
for action_name, action_value_mapping in d._ros_node._action_value_mappings.items():
|
for action_name, action_value_mapping in d._ros_node._action_value_mappings.items():
|
||||||
if action_name.startswith("auto-") or str(action_value_mapping.get("type", "")).startswith(
|
if action_name.startswith("auto-") or str(action_value_mapping.get("type", "")).startswith(
|
||||||
"UniLabJsonCommand"
|
"UniLabJsonCommand"
|
||||||
@@ -778,17 +772,6 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
u = uuid.UUID(item.job_id)
|
u = uuid.UUID(item.job_id)
|
||||||
device_id = item.device_id
|
device_id = item.device_id
|
||||||
action_name = item.action_name
|
action_name = item.action_name
|
||||||
|
|
||||||
if BasicConfig.test_mode:
|
|
||||||
action_id = f"/devices/{device_id}/{action_name}"
|
|
||||||
self.lab_logger().info(
|
|
||||||
f"[TEST MODE] 模拟执行: {action_id} (job={item.job_id[:8]}), 参数: {str(action_kwargs)[:500]}"
|
|
||||||
)
|
|
||||||
# 根据注册表 handles 构建模拟返回值
|
|
||||||
mock_return = self._build_test_mode_return(device_id, action_name, action_kwargs)
|
|
||||||
self._handle_test_mode_result(item, action_id, mock_return)
|
|
||||||
return
|
|
||||||
|
|
||||||
if action_type.startswith("UniLabJsonCommand"):
|
if action_type.startswith("UniLabJsonCommand"):
|
||||||
if action_name.startswith("auto-"):
|
if action_name.startswith("auto-"):
|
||||||
action_name = action_name[5:]
|
action_name = action_name[5:]
|
||||||
@@ -826,51 +809,6 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
)
|
)
|
||||||
future.add_done_callback(lambda f: self.goal_response_callback(item, action_id, f))
|
future.add_done_callback(lambda f: self.goal_response_callback(item, action_id, f))
|
||||||
|
|
||||||
def _build_test_mode_return(
|
|
||||||
self, device_id: str, action_name: str, action_kwargs: Dict[str, Any]
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
根据注册表 handles 的 output 定义构建测试模式的模拟返回值
|
|
||||||
|
|
||||||
根据 data_key 中 @flatten 的层数决定嵌套数组层数,叶子值为空字典。
|
|
||||||
例如: "vessel" → {}, "plate.@flatten" → [{}], "a.@flatten.@flatten" → [[{}]]
|
|
||||||
"""
|
|
||||||
mock_return: Dict[str, Any] = {"test_mode": True, "action_name": action_name}
|
|
||||||
action_mappings = self._action_value_mappings.get(device_id, {})
|
|
||||||
action_mapping = action_mappings.get(action_name, {})
|
|
||||||
handles = action_mapping.get("handles", {})
|
|
||||||
if isinstance(handles, dict):
|
|
||||||
for output_handle in handles.get("output", []):
|
|
||||||
data_key = output_handle.get("data_key", "")
|
|
||||||
handler_key = output_handle.get("handler_key", "")
|
|
||||||
# 根据 @flatten 层数构建嵌套数组,叶子为空字典
|
|
||||||
flatten_count = data_key.count("@flatten")
|
|
||||||
value: Any = {}
|
|
||||||
for _ in range(flatten_count):
|
|
||||||
value = [value]
|
|
||||||
mock_return[handler_key] = value
|
|
||||||
return mock_return
|
|
||||||
|
|
||||||
def _handle_test_mode_result(
|
|
||||||
self, item: "QueueItem", action_id: str, mock_return: Dict[str, Any]
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
测试模式下直接构建结果并走正常的结果回调流程(跳过 ROS)
|
|
||||||
"""
|
|
||||||
job_id = item.job_id
|
|
||||||
status = "success"
|
|
||||||
return_info = serialize_result_info("", True, mock_return)
|
|
||||||
|
|
||||||
self.lab_logger().info(f"[TEST MODE] Result for {action_id} ({job_id[:8]}): {status}")
|
|
||||||
|
|
||||||
from unilabos.app.web.controller import store_job_result
|
|
||||||
store_job_result(job_id, status, return_info, mock_return)
|
|
||||||
|
|
||||||
# 发布状态到桥接器
|
|
||||||
for bridge in self.bridges:
|
|
||||||
if hasattr(bridge, "publish_job_status"):
|
|
||||||
bridge.publish_job_status(mock_return, item, status, return_info)
|
|
||||||
|
|
||||||
def goal_response_callback(self, item: "QueueItem", action_id: str, future) -> None:
|
def goal_response_callback(self, item: "QueueItem", action_id: str, future) -> None:
|
||||||
"""目标响应回调"""
|
"""目标响应回调"""
|
||||||
goal_handle = future.result()
|
goal_handle = future.result()
|
||||||
@@ -1195,7 +1133,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 update completed, success: {success}")
|
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
|
||||||
|
|
||||||
async def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response):
|
async def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response):
|
||||||
"""
|
"""
|
||||||
@@ -1230,10 +1168,6 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
def _node_info_update_callback(self, request, response):
|
def _node_info_update_callback(self, request, response):
|
||||||
"""
|
"""
|
||||||
更新节点信息回调
|
更新节点信息回调
|
||||||
|
|
||||||
处理两种消息:
|
|
||||||
1. 首次上报(main_slave_run): 带 devices_config + registry_config,存储 action_value_mappings
|
|
||||||
2. 设备重注册(SYNC_SLAVE_NODE_INFO): 带 edge_device_id + registry_name,用 registry_name 索引已存储的 mappings
|
|
||||||
"""
|
"""
|
||||||
self.lab_logger().trace(f"[Host Node] Node info update request received: {request}")
|
self.lab_logger().trace(f"[Host Node] Node info update request received: {request}")
|
||||||
try:
|
try:
|
||||||
@@ -1245,48 +1179,12 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
info = info["SYNC_SLAVE_NODE_INFO"]
|
info = info["SYNC_SLAVE_NODE_INFO"]
|
||||||
machine_name = info["machine_name"]
|
machine_name = info["machine_name"]
|
||||||
edge_device_id = info["edge_device_id"]
|
edge_device_id = info["edge_device_id"]
|
||||||
registry_name = info.get("registry_name", "")
|
|
||||||
self.device_machine_names[edge_device_id] = machine_name
|
self.device_machine_names[edge_device_id] = machine_name
|
||||||
|
|
||||||
# 用 registry_name 索引已存储的 registry_config,获取 action_value_mappings
|
|
||||||
if registry_name and registry_name in self._slave_registry_configs:
|
|
||||||
action_mappings = self._slave_registry_configs[registry_name].get(
|
|
||||||
"class", {}
|
|
||||||
).get("action_value_mappings", {})
|
|
||||||
if action_mappings:
|
|
||||||
self._action_value_mappings[edge_device_id] = action_mappings
|
|
||||||
self.lab_logger().info(
|
|
||||||
f"[Host Node] Loaded {len(action_mappings)} action mappings "
|
|
||||||
f"for remote device {edge_device_id} (registry: {registry_name})"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
devices_config = info.pop("devices_config")
|
devices_config = info.pop("devices_config")
|
||||||
registry_config = info.pop("registry_config")
|
registry_config = info.pop("registry_config")
|
||||||
if registry_config:
|
if registry_config:
|
||||||
http_client.resource_registry({"resources": registry_config})
|
http_client.resource_registry({"resources": registry_config})
|
||||||
|
|
||||||
# 存储 slave 的 registry_config,用于后续 SYNC_SLAVE_NODE_INFO 索引
|
|
||||||
for reg_name, reg_data in registry_config.items():
|
|
||||||
if isinstance(reg_data, dict) and "class" in reg_data:
|
|
||||||
self._slave_registry_configs[reg_name] = reg_data
|
|
||||||
|
|
||||||
# 解析 devices_config,建立 device_id -> action_value_mappings 映射
|
|
||||||
if devices_config:
|
|
||||||
for device_tree in devices_config:
|
|
||||||
for device_dict in device_tree:
|
|
||||||
device_id = device_dict.get("id", "")
|
|
||||||
class_name = device_dict.get("class", "")
|
|
||||||
if device_id and class_name and class_name in self._slave_registry_configs:
|
|
||||||
action_mappings = self._slave_registry_configs[class_name].get(
|
|
||||||
"class", {}
|
|
||||||
).get("action_value_mappings", {})
|
|
||||||
if action_mappings:
|
|
||||||
self._action_value_mappings[device_id] = action_mappings
|
|
||||||
self.lab_logger().info(
|
|
||||||
f"[Host Node] Stored {len(action_mappings)} action mappings "
|
|
||||||
f"for remote device {device_id} (class: {class_name})"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.lab_logger().debug(f"[Host Node] Node info update: {info}")
|
self.lab_logger().debug(f"[Host Node] Node info update: {info}")
|
||||||
response.response = "OK"
|
response.response = "OK"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1583,7 +1481,6 @@ 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,
|
||||||
@@ -1598,7 +1495,6 @@ 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):
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ from rclpy.callback_groups import ReentrantCallbackGroup
|
|||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
class JointRepublisher(BaseROS2DeviceNode):
|
class JointRepublisher(BaseROS2DeviceNode):
|
||||||
def __init__(self,device_id, registry_name, resource_tracker, **kwargs):
|
def __init__(self,device_id,resource_tracker, **kwargs):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
registry_name=registry_name,
|
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
hardware_interface={},
|
hardware_interface={},
|
||||||
|
|||||||
@@ -23,36 +23,20 @@ from unilabos_msgs.action import SendCmd
|
|||||||
from rclpy.action.server import ServerGoalHandle
|
from rclpy.action.server import ServerGoalHandle
|
||||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode,DeviceNodeResourceTracker
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode,DeviceNodeResourceTracker
|
||||||
from unilabos.resources.graphio import initialize_resources
|
from unilabos.resources.graphio import initialize_resources
|
||||||
from unilabos.resources.resource_tracker import EXTRA_CLASS
|
|
||||||
from unilabos.registry.registry import lab_registry
|
from unilabos.registry.registry import lab_registry
|
||||||
|
|
||||||
class ResourceMeshManager(BaseROS2DeviceNode):
|
class ResourceMeshManager(BaseROS2DeviceNode):
|
||||||
def __init__(
|
def __init__(self, resource_model: dict, resource_config: list,resource_tracker, device_id: str = "resource_mesh_manager", rate=50, **kwargs):
|
||||||
self,
|
|
||||||
resource_model: Optional[dict] = None,
|
|
||||||
resource_config: Optional[list] = None,
|
|
||||||
resource_tracker=None,
|
|
||||||
device_id: str = "resource_mesh_manager",
|
|
||||||
registry_name: str = "",
|
|
||||||
rate=50,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""初始化资源网格管理器节点
|
"""初始化资源网格管理器节点
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
resource_model: 资源模型字典(可选,为 None 时自动从 registry 构建)
|
resource_model (dict): 资源模型字典,包含资源的3D模型信息
|
||||||
resource_config: 资源配置列表(可选,为 None 时启动后通过 ActionServer 或 load_from_resource_tree 加载)
|
resource_config (dict): 资源配置字典,包含资源的配置信息
|
||||||
resource_tracker: 资源追踪器
|
device_id (str): 节点名称
|
||||||
device_id: 节点名称
|
|
||||||
rate: TF 发布频率
|
|
||||||
"""
|
"""
|
||||||
if resource_tracker is None:
|
|
||||||
resource_tracker = DeviceNodeResourceTracker()
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
registry_name=registry_name,
|
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
hardware_interface={},
|
hardware_interface={},
|
||||||
@@ -61,10 +45,8 @@ class ResourceMeshManager(BaseROS2DeviceNode):
|
|||||||
device_uuid=kwargs.get("uuid", str(uuid.uuid4())),
|
device_uuid=kwargs.get("uuid", str(uuid.uuid4())),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.resource_model = resource_model if resource_model is not None else {}
|
self.resource_model = resource_model
|
||||||
self.resource_config_dict = (
|
self.resource_config_dict = {item['uuid']: item for item in resource_config}
|
||||||
{item['uuid']: item for item in resource_config} if resource_config else {}
|
|
||||||
)
|
|
||||||
self.move_group_ready = False
|
self.move_group_ready = False
|
||||||
self.resource_tf_dict = {}
|
self.resource_tf_dict = {}
|
||||||
self.tf_broadcaster = TransformBroadcaster(self)
|
self.tf_broadcaster = TransformBroadcaster(self)
|
||||||
@@ -94,6 +76,7 @@ class ResourceMeshManager(BaseROS2DeviceNode):
|
|||||||
callback_group=callback_group,
|
callback_group=callback_group,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create a service for applying the planning scene
|
||||||
self._apply_planning_scene_service = self.create_client(
|
self._apply_planning_scene_service = self.create_client(
|
||||||
srv_type=ApplyPlanningScene,
|
srv_type=ApplyPlanningScene,
|
||||||
srv_name="/apply_planning_scene",
|
srv_name="/apply_planning_scene",
|
||||||
@@ -119,36 +102,27 @@ class ResourceMeshManager(BaseROS2DeviceNode):
|
|||||||
AttachedCollisionObject, "/attached_collision_object", 0
|
AttachedCollisionObject, "/attached_collision_object", 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 创建一个Action Server用于修改resource_tf_dict
|
||||||
self._action_server = ActionServer(
|
self._action_server = ActionServer(
|
||||||
self,
|
self,
|
||||||
SendCmd,
|
SendCmd,
|
||||||
f"tf_update",
|
f"tf_update",
|
||||||
self.tf_update,
|
self.tf_update,
|
||||||
callback_group=callback_group,
|
callback_group=callback_group
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 创建一个Action Server用于添加新的资源模型与resource_tf_dict
|
||||||
self._add_resource_mesh_action_server = ActionServer(
|
self._add_resource_mesh_action_server = ActionServer(
|
||||||
self,
|
self,
|
||||||
SendCmd,
|
SendCmd,
|
||||||
f"add_resource_mesh",
|
f"add_resource_mesh",
|
||||||
self.add_resource_mesh_callback,
|
self.add_resource_mesh_callback,
|
||||||
callback_group=callback_group,
|
callback_group=callback_group
|
||||||
)
|
)
|
||||||
|
|
||||||
self._reload_resource_mesh_action_server = ActionServer(
|
self.resource_tf_dict = self.resource_mesh_setup(self.resource_config_dict)
|
||||||
self,
|
self.create_timer(1/self.rate, self.publish_resource_tf)
|
||||||
SendCmd,
|
self.create_timer(1/self.rate, self.check_resource_pose_changes)
|
||||||
f"reload_resource_mesh",
|
|
||||||
self._reload_resource_mesh_callback,
|
|
||||||
callback_group=callback_group,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.resource_config_dict:
|
|
||||||
self.resource_tf_dict = self.resource_mesh_setup(self.resource_config_dict)
|
|
||||||
else:
|
|
||||||
self.get_logger().info("未提供 resource_config,将通过 ActionServer 或 load_from_resource_tree 加载")
|
|
||||||
self.create_timer(1 / self.rate, self.publish_resource_tf)
|
|
||||||
self.create_timer(1 / self.rate, self.check_resource_pose_changes)
|
|
||||||
|
|
||||||
def check_move_group_ready(self):
|
def check_move_group_ready(self):
|
||||||
"""检查move_group节点是否已初始化完成"""
|
"""检查move_group节点是否已初始化完成"""
|
||||||
@@ -165,107 +139,10 @@ class ResourceMeshManager(BaseROS2DeviceNode):
|
|||||||
self.add_resource_collision_meshes(self.resource_tf_dict)
|
self.add_resource_collision_meshes(self.resource_tf_dict)
|
||||||
|
|
||||||
|
|
||||||
def _build_resource_model_for_config(self, resource_config_dict: dict):
|
def add_resource_mesh_callback(self, goal_handle : ServerGoalHandle):
|
||||||
"""从 registry 中为给定的资源配置自动构建 resource_model(mesh 信息)"""
|
|
||||||
registry = lab_registry
|
|
||||||
for _uuid, res_cfg in resource_config_dict.items():
|
|
||||||
resource_id = res_cfg.get('id', '')
|
|
||||||
resource_class = res_cfg.get('class', '')
|
|
||||||
if not resource_class:
|
|
||||||
continue
|
|
||||||
if resource_class not in registry.resource_type_registry:
|
|
||||||
continue
|
|
||||||
reg_entry = registry.resource_type_registry[resource_class]
|
|
||||||
if 'model' not in reg_entry:
|
|
||||||
continue
|
|
||||||
model_config = reg_entry['model']
|
|
||||||
if model_config.get('type') != 'resource':
|
|
||||||
continue
|
|
||||||
if resource_id in self.resource_model:
|
|
||||||
continue
|
|
||||||
self.resource_model[resource_id] = {
|
|
||||||
'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['mesh']}",
|
|
||||||
'mesh_tf': model_config['mesh_tf'],
|
|
||||||
}
|
|
||||||
if model_config.get('children_mesh') is not None:
|
|
||||||
self.resource_model[f"{resource_id}_"] = {
|
|
||||||
'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['children_mesh']}",
|
|
||||||
'mesh_tf': model_config['children_mesh_tf'],
|
|
||||||
}
|
|
||||||
|
|
||||||
def load_from_resource_tree(self):
|
|
||||||
"""从 resource_tracker 中读取资源树,自动构建 resource_config_dict / resource_model 并刷新 TF"""
|
|
||||||
new_config_dict: dict = {}
|
|
||||||
|
|
||||||
def _collect_plr_resource(res, parent_uuid: Optional[str] = None):
|
|
||||||
res_uuid = getattr(res, 'unilabos_uuid', None)
|
|
||||||
if not res_uuid:
|
|
||||||
res_uuid = str(uuid.uuid4())
|
|
||||||
extra = getattr(res, 'unilabos_extra', {}) or {}
|
|
||||||
resource_class = extra.get(EXTRA_CLASS, '')
|
|
||||||
|
|
||||||
location = getattr(res, 'location', None)
|
|
||||||
pos_x = float(location.x) if location else 0.0
|
|
||||||
pos_y = float(location.y) if location else 0.0
|
|
||||||
pos_z = float(location.z) if location else 0.0
|
|
||||||
|
|
||||||
rotation = extra.get('rotation', {'x': 0, 'y': 0, 'z': 0})
|
|
||||||
|
|
||||||
new_config_dict[res_uuid] = {
|
|
||||||
'id': res.name,
|
|
||||||
'uuid': res_uuid,
|
|
||||||
'class': resource_class,
|
|
||||||
'parent_uuid': parent_uuid,
|
|
||||||
'pose': {
|
|
||||||
'position': {'x': pos_x, 'y': pos_y, 'z': pos_z},
|
|
||||||
'rotation': rotation,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for child in getattr(res, 'children', []) or []:
|
|
||||||
_collect_plr_resource(child, res_uuid)
|
|
||||||
|
|
||||||
for resource in self.resource_tracker.resources:
|
|
||||||
root_parent_uuid = None
|
|
||||||
plr_parent = getattr(resource, 'parent', None)
|
|
||||||
if plr_parent is not None:
|
|
||||||
root_parent_uuid = getattr(plr_parent, 'unilabos_uuid', None)
|
|
||||||
_collect_plr_resource(resource, root_parent_uuid)
|
|
||||||
|
|
||||||
if not new_config_dict:
|
|
||||||
self.get_logger().warning("resource_tracker 中没有找到任何资源")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.resource_config_dict = {**self.resource_config_dict, **new_config_dict}
|
|
||||||
self._build_resource_model_for_config(new_config_dict)
|
|
||||||
|
|
||||||
tf_dict = self.resource_mesh_setup(new_config_dict)
|
|
||||||
self.resource_tf_dict = {**self.resource_tf_dict, **tf_dict}
|
|
||||||
self.publish_resource_tf()
|
|
||||||
if self.move_group_ready:
|
|
||||||
self.add_resource_collision_meshes(tf_dict)
|
|
||||||
self.get_logger().info(f"从资源树加载了 {len(new_config_dict)} 个资源")
|
|
||||||
|
|
||||||
def _reload_resource_mesh_callback(self, goal_handle: ServerGoalHandle):
|
|
||||||
"""ActionServer 回调:重新从资源树加载所有 mesh"""
|
|
||||||
try:
|
|
||||||
self.load_from_resource_tree()
|
|
||||||
except Exception as e:
|
|
||||||
self.get_logger().error(f"重新加载资源失败: {e}")
|
|
||||||
goal_handle.abort()
|
|
||||||
return SendCmd.Result(success=False)
|
|
||||||
goal_handle.succeed()
|
|
||||||
return SendCmd.Result(success=True)
|
|
||||||
|
|
||||||
def add_resource_mesh_callback(self, goal_handle: ServerGoalHandle):
|
|
||||||
tf_update_msg = goal_handle.request
|
tf_update_msg = goal_handle.request
|
||||||
try:
|
try:
|
||||||
parsed = json.loads(tf_update_msg.command.replace("'", '"'))
|
self.add_resource_mesh(tf_update_msg.command)
|
||||||
if 'resources' in parsed:
|
|
||||||
for res_config in parsed['resources']:
|
|
||||||
self.add_resource_mesh(json.dumps(res_config))
|
|
||||||
else:
|
|
||||||
self.add_resource_mesh(tf_update_msg.command)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.get_logger().error(f"添加资源失败: {e}")
|
self.get_logger().error(f"添加资源失败: {e}")
|
||||||
goal_handle.abort()
|
goal_handle.abort()
|
||||||
@@ -273,48 +150,45 @@ class ResourceMeshManager(BaseROS2DeviceNode):
|
|||||||
goal_handle.succeed()
|
goal_handle.succeed()
|
||||||
return SendCmd.Result(success=True)
|
return SendCmd.Result(success=True)
|
||||||
|
|
||||||
def add_resource_mesh(self, resource_config_str: str):
|
def add_resource_mesh(self,resource_config_str:str):
|
||||||
"""添加单个资源的 mesh 配置"""
|
"""刷新资源配置"""
|
||||||
|
|
||||||
registry = lab_registry
|
registry = lab_registry
|
||||||
resource_config = json.loads(resource_config_str.replace("'", '"'))
|
resource_config = json.loads(resource_config_str.replace("'",'"'))
|
||||||
|
|
||||||
if resource_config['id'] in self.resource_config_dict:
|
if resource_config['id'] in self.resource_config_dict:
|
||||||
self.get_logger().info(f'资源 {resource_config["id"]} 已存在')
|
self.get_logger().info(f'资源 {resource_config["id"]} 已存在')
|
||||||
return
|
return
|
||||||
resource_class = resource_config.get('class', '')
|
if resource_config['class'] in registry.resource_type_registry.keys():
|
||||||
if resource_class and resource_class in registry.resource_type_registry:
|
model_config = registry.resource_type_registry[resource_config['class']]['model']
|
||||||
reg_entry = registry.resource_type_registry[resource_class]
|
if model_config['type'] == 'resource':
|
||||||
if 'model' in reg_entry:
|
self.resource_model[resource_config['id']] = {
|
||||||
model_config = reg_entry['model']
|
'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['mesh']}",
|
||||||
if model_config.get('type') == 'resource':
|
'mesh_tf': model_config['mesh_tf']}
|
||||||
self.resource_model[resource_config['id']] = {
|
if 'children_mesh' in model_config.keys():
|
||||||
'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['mesh']}",
|
self.resource_model[f"{resource_config['id']}_"] = {
|
||||||
'mesh_tf': model_config['mesh_tf'],
|
'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['children_mesh']}",
|
||||||
|
'mesh_tf': model_config['children_mesh_tf']
|
||||||
}
|
}
|
||||||
if model_config.get('children_mesh') is not None:
|
|
||||||
self.resource_model[f"{resource_config['id']}_"] = {
|
|
||||||
'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['children_mesh']}",
|
|
||||||
'mesh_tf': model_config['children_mesh_tf'],
|
|
||||||
}
|
|
||||||
resources = initialize_resources([resource_config])
|
resources = initialize_resources([resource_config])
|
||||||
resource_dict = {item['id']: item for item in resources}
|
resource_dict = {item['id']: item for item in resources}
|
||||||
self.resource_config_dict = {**self.resource_config_dict, **resource_dict}
|
self.resource_config_dict = {**self.resource_config_dict,**resource_dict}
|
||||||
tf_dict = self.resource_mesh_setup(resource_dict)
|
tf_dict = self.resource_mesh_setup(resource_dict)
|
||||||
self.resource_tf_dict = {**self.resource_tf_dict, **tf_dict}
|
self.resource_tf_dict = {**self.resource_tf_dict,**tf_dict}
|
||||||
self.publish_resource_tf()
|
self.publish_resource_tf()
|
||||||
self.add_resource_collision_meshes(tf_dict)
|
self.add_resource_collision_meshes(tf_dict)
|
||||||
|
|
||||||
def resource_mesh_setup(self, resource_config_dict: dict):
|
|
||||||
"""根据资源配置字典设置 TF 关系"""
|
def resource_mesh_setup(self, resource_config_dict:dict):
|
||||||
|
"""move_group初始化完成后的设置"""
|
||||||
self.get_logger().info('开始设置资源网格管理器')
|
self.get_logger().info('开始设置资源网格管理器')
|
||||||
|
#遍历resource_config中的资源配置,判断panent是否在resource_model中,
|
||||||
resource_tf_dict = {}
|
resource_tf_dict = {}
|
||||||
for resource_uuid, resource_config in resource_config_dict.items():
|
for resource_uuid, resource_config in resource_config_dict.items():
|
||||||
parent = None
|
parent = None
|
||||||
resource_id = resource_config['id']
|
resource_id = resource_config['id']
|
||||||
parent_uuid = resource_config.get('parent_uuid')
|
if resource_config['parent_uuid'] is not None and resource_config['parent_uuid'] != "":
|
||||||
if parent_uuid is not None and parent_uuid != "":
|
parent = resource_config_dict[resource_config['parent_uuid']]['id']
|
||||||
parent_entry = resource_config_dict.get(parent_uuid) or self.resource_config_dict.get(parent_uuid)
|
|
||||||
parent = parent_entry['id'] if parent_entry else None
|
|
||||||
|
|
||||||
parent_link = 'world'
|
parent_link = 'world'
|
||||||
if parent in self.resource_model:
|
if parent in self.resource_model:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeRe
|
|||||||
|
|
||||||
|
|
||||||
class ROS2SerialNode(BaseROS2DeviceNode):
|
class ROS2SerialNode(BaseROS2DeviceNode):
|
||||||
def __init__(self, device_id, registry_name, port: str, baudrate: int = 9600, resource_tracker: DeviceNodeResourceTracker=None):
|
def __init__(self, device_id, port: str, baudrate: int = 9600, resource_tracker: DeviceNodeResourceTracker=None):
|
||||||
# 保存属性,以便在调用父类初始化前使用
|
# 保存属性,以便在调用父类初始化前使用
|
||||||
self.port = port
|
self.port = port
|
||||||
self.baudrate = baudrate
|
self.baudrate = baudrate
|
||||||
@@ -28,7 +28,6 @@ class ROS2SerialNode(BaseROS2DeviceNode):
|
|||||||
BaseROS2DeviceNode.__init__(
|
BaseROS2DeviceNode.__init__(
|
||||||
self,
|
self,
|
||||||
driver_instance=self,
|
driver_instance=self,
|
||||||
registry_name=registry_name,
|
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings={},
|
action_value_mappings={},
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
*,
|
*,
|
||||||
driver_instance: "WorkstationBase",
|
driver_instance: "WorkstationBase",
|
||||||
device_id: str,
|
device_id: str,
|
||||||
registry_name: str,
|
|
||||||
device_uuid: str,
|
device_uuid: str,
|
||||||
status_types: Dict[str, Any],
|
status_types: Dict[str, Any],
|
||||||
action_value_mappings: Dict[str, Any],
|
action_value_mappings: Dict[str, Any],
|
||||||
@@ -63,7 +62,6 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=driver_instance,
|
driver_instance=driver_instance,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
registry_name=registry_name,
|
|
||||||
device_uuid=device_uuid,
|
device_uuid=device_uuid,
|
||||||
status_types=status_types,
|
status_types=status_types,
|
||||||
action_value_mappings={**action_value_mappings, **self.protocol_action_mappings},
|
action_value_mappings={**action_value_mappings, **self.protocol_action_mappings},
|
||||||
|
|||||||
@@ -339,8 +339,13 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
"max_volume": 500.0,
|
||||||
"type": "RegularContainer",
|
"type": "RegularContainer",
|
||||||
"category": "container"
|
"category": "container",
|
||||||
|
"max_temp": 200.0,
|
||||||
|
"min_temp": -20.0,
|
||||||
|
"has_stirrer": true,
|
||||||
|
"has_heater": true
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"liquids": [],
|
"liquids": [],
|
||||||
@@ -764,7 +769,9 @@
|
|||||||
"size_y": 250,
|
"size_y": 250,
|
||||||
"size_z": 0,
|
"size_z": 0,
|
||||||
"type": "RegularContainer",
|
"type": "RegularContainer",
|
||||||
"category": "container"
|
"category": "container",
|
||||||
|
"reagent": "sodium_chloride",
|
||||||
|
"physical_state": "solid"
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_mass": 500.0,
|
"current_mass": 500.0,
|
||||||
@@ -785,11 +792,14 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
"volume": 500.0,
|
||||||
"size_x": 600,
|
"size_x": 600,
|
||||||
"size_y": 250,
|
"size_y": 250,
|
||||||
"size_z": 0,
|
"size_z": 0,
|
||||||
"type": "RegularContainer",
|
"type": "RegularContainer",
|
||||||
"category": "container"
|
"category": "container",
|
||||||
|
"reagent": "sodium_carbonate",
|
||||||
|
"physical_state": "solid"
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_mass": 500.0,
|
"current_mass": 500.0,
|
||||||
@@ -810,11 +820,14 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
"volume": 500.0,
|
||||||
"size_x": 650,
|
"size_x": 650,
|
||||||
"size_y": 250,
|
"size_y": 250,
|
||||||
"size_z": 0,
|
"size_z": 0,
|
||||||
"type": "RegularContainer",
|
"type": "RegularContainer",
|
||||||
"category": "container"
|
"category": "container",
|
||||||
|
"reagent": "magnesium_chloride",
|
||||||
|
"physical_state": "solid"
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_mass": 500.0,
|
"current_mass": 500.0,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -184,51 +184,6 @@ def get_all_subscriptions(instance) -> list:
|
|||||||
return subscriptions
|
return subscriptions
|
||||||
|
|
||||||
|
|
||||||
def always_free(func: F) -> F:
|
|
||||||
"""
|
|
||||||
标记动作为永久闲置(不受busy队列限制)的装饰器
|
|
||||||
|
|
||||||
被此装饰器标记的 action 方法,在执行时不会受到设备级别的排队限制,
|
|
||||||
任何时候请求都可以立即执行。适用于查询类、状态读取类等轻量级操作。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
class MyDriver:
|
|
||||||
@always_free
|
|
||||||
def query_status(self, param: str):
|
|
||||||
# 这个动作可以随时执行,不需要排队
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
def transfer(self, volume: float):
|
|
||||||
# 这个动作会按正常排队逻辑执行
|
|
||||||
pass
|
|
||||||
|
|
||||||
Note:
|
|
||||||
- 可以与其他装饰器组合使用,@always_free 应放在最外层
|
|
||||||
- 仅影响 WebSocket 调度层的 busy/free 判断,不影响 ROS2 层
|
|
||||||
"""
|
|
||||||
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
wrapper._is_always_free = True # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
return wrapper # type: ignore[return-value]
|
|
||||||
|
|
||||||
|
|
||||||
def is_always_free(func) -> bool:
|
|
||||||
"""
|
|
||||||
检查函数是否被标记为永久闲置
|
|
||||||
|
|
||||||
Args:
|
|
||||||
func: 被检查的函数
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
如果函数被 @always_free 装饰则返回 True,否则返回 False
|
|
||||||
"""
|
|
||||||
return getattr(func, "_is_always_free", False)
|
|
||||||
|
|
||||||
|
|
||||||
def not_action(func: F) -> F:
|
def not_action(func: F) -> F:
|
||||||
"""
|
"""
|
||||||
标记方法为非动作的装饰器
|
标记方法为非动作的装饰器
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from ast import Constant
|
|||||||
|
|
||||||
from unilabos.resources.resource_tracker import PARAM_SAMPLE_UUIDS
|
from unilabos.resources.resource_tracker import PARAM_SAMPLE_UUIDS
|
||||||
from unilabos.utils import logger
|
from unilabos.utils import logger
|
||||||
from unilabos.utils.decorator import is_not_action, is_always_free
|
from unilabos.utils.decorator import is_not_action
|
||||||
|
|
||||||
|
|
||||||
class ImportManager:
|
class ImportManager:
|
||||||
@@ -282,9 +282,6 @@ class ImportManager:
|
|||||||
continue
|
continue
|
||||||
# 其他非_开头的方法归类为action
|
# 其他非_开头的方法归类为action
|
||||||
method_info = self._analyze_method_signature(method)
|
method_info = self._analyze_method_signature(method)
|
||||||
# 检查是否被 @always_free 装饰器标记
|
|
||||||
if is_always_free(method):
|
|
||||||
method_info["always_free"] = True
|
|
||||||
result["action_methods"][name] = method_info
|
result["action_methods"][name] = method_info
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -342,9 +339,6 @@ class ImportManager:
|
|||||||
if self._is_not_action_method(node):
|
if self._is_not_action_method(node):
|
||||||
continue
|
continue
|
||||||
# 其他非_开头的方法归类为action
|
# 其他非_开头的方法归类为action
|
||||||
# 检查是否被 @always_free 装饰器标记
|
|
||||||
if self._is_always_free_method(node):
|
|
||||||
method_info["always_free"] = True
|
|
||||||
result["action_methods"][method_name] = method_info
|
result["action_methods"][method_name] = method_info
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -480,13 +474,6 @@ class ImportManager:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _is_always_free_method(self, node: ast.FunctionDef) -> bool:
|
|
||||||
"""检查是否是@always_free装饰的方法"""
|
|
||||||
for decorator in node.decorator_list:
|
|
||||||
if isinstance(decorator, ast.Name) and decorator.id == "always_free":
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _get_property_name_from_setter(self, node: ast.FunctionDef) -> str:
|
def _get_property_name_from_setter(self, node: ast.FunctionDef) -> str:
|
||||||
"""从setter装饰器中获取属性名"""
|
"""从setter装饰器中获取属性名"""
|
||||||
for decorator in node.decorator_list:
|
for decorator in node.decorator_list:
|
||||||
|
|||||||
@@ -193,7 +193,6 @@ def configure_logger(loglevel=None, working_dir=None):
|
|||||||
root_logger.addHandler(console_handler)
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
# 如果指定了工作目录,添加文件处理器
|
# 如果指定了工作目录,添加文件处理器
|
||||||
log_filepath = None
|
|
||||||
if working_dir is not None:
|
if working_dir is not None:
|
||||||
logs_dir = os.path.join(working_dir, "logs")
|
logs_dir = os.path.join(working_dir, "logs")
|
||||||
os.makedirs(logs_dir, exist_ok=True)
|
os.makedirs(logs_dir, exist_ok=True)
|
||||||
@@ -214,7 +213,6 @@ def configure_logger(loglevel=None, working_dir=None):
|
|||||||
|
|
||||||
logging.getLogger("asyncio").setLevel(logging.INFO)
|
logging.getLogger("asyncio").setLevel(logging.INFO)
|
||||||
logging.getLogger("urllib3").setLevel(logging.INFO)
|
logging.getLogger("urllib3").setLevel(logging.INFO)
|
||||||
return log_filepath
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
parent: /PRCXI/PRCXI_Deck/T{slot}
|
||||||
slot_on_deck: "{slot}"
|
slot_on_deck: "{slot}"
|
||||||
- 输出端口: labware(用于连接 set_liquid_from_plate)
|
- 输出端口: labware(用于连接 set_liquid_from_plate)
|
||||||
- 控制流: create_resource 之间通过 ready 端口串联
|
- 控制流: create_resource 之间通过 ready 端口串联
|
||||||
@@ -51,7 +51,6 @@
|
|||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
- 遍历 workflow 数组,为每个动作创建步骤节点
|
- 遍历 workflow 数组,为每个动作创建步骤节点
|
||||||
- 参数重命名: asp_vol -> asp_vols, dis_vol -> dis_vols, asp_flow_rate -> asp_flow_rates, dis_flow_rate -> dis_flow_rates
|
- 参数重命名: asp_vol -> asp_vols, dis_vol -> dis_vols, asp_flow_rate -> asp_flow_rates, dis_flow_rate -> dis_flow_rates
|
||||||
- 参数输入转换: liquid_height(按 wells 扩展);mix_stage/mix_times/mix_vol/mix_rate/mix_liquid_height 保持标量
|
|
||||||
- 参数扩展: 根据 targets 的 wells 数量,将单值扩展为数组
|
- 参数扩展: 根据 targets 的 wells 数量,将单值扩展为数组
|
||||||
例: asp_vol=100.0, targets 有 3 个 wells -> asp_vols=[100.0, 100.0, 100.0]
|
例: asp_vol=100.0, targets 有 3 个 wells -> asp_vols=[100.0, 100.0, 100.0]
|
||||||
- 连接处理: 如果 sources/targets 已通过 set_liquid_from_plate 连接,参数值改为 []
|
- 连接处理: 如果 sources/targets 已通过 set_liquid_from_plate 连接,参数值改为 []
|
||||||
@@ -120,14 +119,11 @@ DEVICE_NAME_DEFAULT = "PRCXI" # transfer_liquid, set_liquid_from_plate 等动
|
|||||||
# 节点类型
|
# 节点类型
|
||||||
NODE_TYPE_DEFAULT = "ILab" # 所有节点的默认类型
|
NODE_TYPE_DEFAULT = "ILab" # 所有节点的默认类型
|
||||||
|
|
||||||
CLASS_NAMES_MAPPING = {
|
|
||||||
"plate": "PRCXI_BioER_96_wellplate",
|
|
||||||
"tip_rack": "PRCXI_300ul_Tips",
|
|
||||||
}
|
|
||||||
# create_resource 节点默认参数
|
# create_resource 节点默认参数
|
||||||
CREATE_RESOURCE_DEFAULTS = {
|
CREATE_RESOURCE_DEFAULTS = {
|
||||||
"device_id": "/PRCXI",
|
"device_id": "/PRCXI",
|
||||||
"parent_template": "/PRCXI/PRCXI_Deck",
|
"parent_template": "/PRCXI/PRCXI_Deck/T{slot}", # {slot} 会被替换为实际的 slot 值
|
||||||
|
"class_name": "PRCXI_BioER_96_wellplate",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 默认液体体积 (uL)
|
# 默认液体体积 (uL)
|
||||||
@@ -366,7 +362,6 @@ def build_protocol_graph(
|
|||||||
protocol_steps: List[Dict[str, Any]],
|
protocol_steps: List[Dict[str, Any]],
|
||||||
workstation_name: str,
|
workstation_name: str,
|
||||||
action_resource_mapping: Optional[Dict[str, str]] = None,
|
action_resource_mapping: Optional[Dict[str, str]] = None,
|
||||||
labware_defs: Optional[List[Dict[str, Any]]] = None,
|
|
||||||
) -> WorkflowGraph:
|
) -> WorkflowGraph:
|
||||||
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑
|
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑
|
||||||
|
|
||||||
@@ -387,14 +382,11 @@ def build_protocol_graph(
|
|||||||
slots_info = {} # slot -> {labware, res_id}
|
slots_info = {} # slot -> {labware, res_id}
|
||||||
for labware_id, item in labware_info.items():
|
for labware_id, item in labware_info.items():
|
||||||
slot = str(item.get("slot", ""))
|
slot = str(item.get("slot", ""))
|
||||||
labware = item.get("labware", "")
|
|
||||||
if slot and slot not in slots_info:
|
if slot and slot not in slots_info:
|
||||||
res_id = f"{labware}_slot_{slot}"
|
res_id = f"plate_slot_{slot}"
|
||||||
slots_info[slot] = {
|
slots_info[slot] = {
|
||||||
"labware": labware,
|
"labware": item.get("labware", ""),
|
||||||
"res_id": res_id,
|
"res_id": res_id,
|
||||||
"labware_id": labware_id,
|
|
||||||
"object": item.get("object", ""),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 创建 Group 节点,包含所有 create_resource 节点
|
# 创建 Group 节点,包含所有 create_resource 节点
|
||||||
@@ -412,22 +404,18 @@ def build_protocol_graph(
|
|||||||
param=None,
|
param=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
trash_create_node_id = None # 记录 trash 的 create_resource 节点
|
|
||||||
|
|
||||||
# 为每个唯一的 slot 创建 create_resource 节点
|
# 为每个唯一的 slot 创建 create_resource 节点
|
||||||
|
res_index = 0
|
||||||
for slot, info in slots_info.items():
|
for slot, info in slots_info.items():
|
||||||
node_id = str(uuid.uuid4())
|
node_id = str(uuid.uuid4())
|
||||||
res_id = info["res_id"]
|
res_id = info["res_id"]
|
||||||
res_type_name = info["labware"].lower().replace(".", "point")
|
|
||||||
object_type = info.get("object", "")
|
res_index += 1
|
||||||
res_type_name = f"lab_{res_type_name}"
|
|
||||||
if object_type == "trash":
|
|
||||||
res_type_name = "PRCXI_trash"
|
|
||||||
G.add_node(
|
G.add_node(
|
||||||
node_id,
|
node_id,
|
||||||
template_name="create_resource",
|
template_name="create_resource",
|
||||||
resource_name="host_node",
|
resource_name="host_node",
|
||||||
name=f"{res_type_name}_slot{slot}",
|
name=f"Plate {res_index}",
|
||||||
description=f"Create plate on slot {slot}",
|
description=f"Create plate on slot {slot}",
|
||||||
lab_node_type="Labware",
|
lab_node_type="Labware",
|
||||||
footer="create_resource-host_node",
|
footer="create_resource-host_node",
|
||||||
@@ -438,17 +426,14 @@ def build_protocol_graph(
|
|||||||
param={
|
param={
|
||||||
"res_id": res_id,
|
"res_id": res_id,
|
||||||
"device_id": CREATE_RESOURCE_DEFAULTS["device_id"],
|
"device_id": CREATE_RESOURCE_DEFAULTS["device_id"],
|
||||||
"class_name": res_type_name,
|
"class_name": CREATE_RESOURCE_DEFAULTS["class_name"],
|
||||||
"parent": CREATE_RESOURCE_DEFAULTS["parent_template"].format(slot=slot),
|
"parent": CREATE_RESOURCE_DEFAULTS["parent_template"].format(slot=slot),
|
||||||
"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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
slot_to_create_resource[slot] = node_id
|
slot_to_create_resource[slot] = node_id
|
||||||
if object_type == "tiprack":
|
|
||||||
resource_last_writer[info["labware_id"]] = f"{node_id}:labware"
|
|
||||||
if object_type == "trash":
|
|
||||||
trash_create_node_id = node_id
|
|
||||||
# create_resource 之间不需要 ready 连接
|
# create_resource 之间不需要 ready 连接
|
||||||
|
|
||||||
# ==================== 第二步:为每个 reagent 创建 set_liquid_from_plate 节点 ====================
|
# ==================== 第二步:为每个 reagent 创建 set_liquid_from_plate 节点 ====================
|
||||||
@@ -484,8 +469,6 @@ def build_protocol_graph(
|
|||||||
# res_id 不能有空格
|
# res_id 不能有空格
|
||||||
res_id = str(labware_id).replace(" ", "_")
|
res_id = str(labware_id).replace(" ", "_")
|
||||||
well_count = len(wells)
|
well_count = len(wells)
|
||||||
object_type = item.get("object", "")
|
|
||||||
liquid_volume = DEFAULT_LIQUID_VOLUME if object_type == "source" else 0
|
|
||||||
|
|
||||||
node_id = str(uuid.uuid4())
|
node_id = str(uuid.uuid4())
|
||||||
set_liquid_index += 1
|
set_liquid_index += 1
|
||||||
@@ -506,7 +489,7 @@ def build_protocol_graph(
|
|||||||
"plate": [], # 通过连接传递
|
"plate": [], # 通过连接传递
|
||||||
"well_names": wells, # 孔位名数组,如 ["A1", "A3", "A5"]
|
"well_names": wells, # 孔位名数组,如 ["A1", "A3", "A5"]
|
||||||
"liquid_names": [res_id] * well_count,
|
"liquid_names": [res_id] * well_count,
|
||||||
"volumes": [liquid_volume] * well_count,
|
"volumes": [DEFAULT_LIQUID_VOLUME] * well_count,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -520,8 +503,8 @@ def build_protocol_graph(
|
|||||||
# set_liquid_from_plate 的输出 output_wells 用于连接 transfer_liquid
|
# set_liquid_from_plate 的输出 output_wells 用于连接 transfer_liquid
|
||||||
resource_last_writer[labware_id] = f"{node_id}:output_wells"
|
resource_last_writer[labware_id] = f"{node_id}:output_wells"
|
||||||
|
|
||||||
# transfer_liquid 之间通过 ready 串联;若存在 trash 节点,第一个 transfer_liquid 从 trash 的 ready 开始
|
# transfer_liquid 之间通过 ready 串联,从 None 开始
|
||||||
last_control_node_id = trash_create_node_id
|
last_control_node_id = None
|
||||||
|
|
||||||
# 端口名称映射:JSON 字段名 -> 实际 handle key
|
# 端口名称映射:JSON 字段名 -> 实际 handle key
|
||||||
INPUT_PORT_MAPPING = {
|
INPUT_PORT_MAPPING = {
|
||||||
@@ -533,7 +516,6 @@ def build_protocol_graph(
|
|||||||
"reagent": "reagent",
|
"reagent": "reagent",
|
||||||
"solvent": "solvent",
|
"solvent": "solvent",
|
||||||
"compound": "compound",
|
"compound": "compound",
|
||||||
"tip_racks": "tip_rack_identifier",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OUTPUT_PORT_MAPPING = {
|
OUTPUT_PORT_MAPPING = {
|
||||||
@@ -548,17 +530,8 @@ def build_protocol_graph(
|
|||||||
"compound": "compound",
|
"compound": "compound",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 需要根据 wells 数量扩展的参数列表:
|
# 需要根据 wells 数量扩展的参数列表(复数形式)
|
||||||
# - 复数参数(asp_vols 等)支持单值自动扩展
|
EXPAND_BY_WELLS_PARAMS = ["asp_vols", "dis_vols", "asp_flow_rates", "dis_flow_rates"]
|
||||||
# - liquid_height 按 wells 扩展为数组
|
|
||||||
# - mix_* 参数保持标量,避免被转换为 list
|
|
||||||
EXPAND_BY_WELLS_PARAMS = [
|
|
||||||
"asp_vols",
|
|
||||||
"dis_vols",
|
|
||||||
"asp_flow_rates",
|
|
||||||
"dis_flow_rates",
|
|
||||||
"liquid_height",
|
|
||||||
]
|
|
||||||
|
|
||||||
# 处理协议步骤
|
# 处理协议步骤
|
||||||
for step in protocol_steps:
|
for step in protocol_steps:
|
||||||
@@ -572,57 +545,6 @@ def build_protocol_graph(
|
|||||||
if old_name in params:
|
if old_name in params:
|
||||||
params[new_name] = params.pop(old_name)
|
params[new_name] = params.pop(old_name)
|
||||||
|
|
||||||
# touch_tip 输入归一化:
|
|
||||||
# - 支持 bool / 0/1 / "true"/"false" / 单元素 list
|
|
||||||
# - 最终统一为 bool 标量,避免被下游误当作序列处理
|
|
||||||
if "touch_tip" in params:
|
|
||||||
touch_tip_value = params.get("touch_tip")
|
|
||||||
if isinstance(touch_tip_value, list):
|
|
||||||
if len(touch_tip_value) == 1:
|
|
||||||
touch_tip_value = touch_tip_value[0]
|
|
||||||
elif len(touch_tip_value) == 0:
|
|
||||||
touch_tip_value = False
|
|
||||||
else:
|
|
||||||
warnings.append(f"touch_tip 期望标量,但收到长度为 {len(touch_tip_value)} 的列表,使用首个值")
|
|
||||||
touch_tip_value = touch_tip_value[0]
|
|
||||||
if isinstance(touch_tip_value, str):
|
|
||||||
norm = touch_tip_value.strip().lower()
|
|
||||||
if norm in {"true", "1", "yes", "y", "on"}:
|
|
||||||
touch_tip_value = True
|
|
||||||
elif norm in {"false", "0", "no", "n", "off", ""}:
|
|
||||||
touch_tip_value = False
|
|
||||||
else:
|
|
||||||
warnings.append(f"touch_tip 字符串值无法识别: {touch_tip_value},按 True 处理")
|
|
||||||
touch_tip_value = True
|
|
||||||
elif isinstance(touch_tip_value, (int, float)):
|
|
||||||
touch_tip_value = bool(touch_tip_value)
|
|
||||||
elif touch_tip_value is None:
|
|
||||||
touch_tip_value = False
|
|
||||||
else:
|
|
||||||
touch_tip_value = bool(touch_tip_value)
|
|
||||||
params["touch_tip"] = touch_tip_value
|
|
||||||
|
|
||||||
# delays 输入归一化:
|
|
||||||
# - 支持标量(int/float/字符串数字)与 list
|
|
||||||
# - 最终统一为数字列表,供下游按 delays[0]/delays[1] 使用
|
|
||||||
if "delays" in params:
|
|
||||||
delays_value = params.get("delays")
|
|
||||||
if delays_value is None or delays_value == "":
|
|
||||||
params["delays"] = []
|
|
||||||
else:
|
|
||||||
raw_list = delays_value if isinstance(delays_value, list) else [delays_value]
|
|
||||||
normalized_delays = []
|
|
||||||
for delay_item in raw_list:
|
|
||||||
if isinstance(delay_item, str):
|
|
||||||
delay_item = delay_item.strip()
|
|
||||||
if delay_item == "":
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
normalized_delays.append(float(delay_item))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
warnings.append(f"delays 包含无法转换为数字的值: {delay_item},已忽略")
|
|
||||||
params["delays"] = normalized_delays
|
|
||||||
|
|
||||||
# 处理输入连接
|
# 处理输入连接
|
||||||
for param_key, target_port in INPUT_PORT_MAPPING.items():
|
for param_key, target_port in INPUT_PORT_MAPPING.items():
|
||||||
resource_name = params.get(param_key)
|
resource_name = params.get(param_key)
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
JSON 工作流转换模块
|
JSON 工作流转换模块
|
||||||
|
|
||||||
将 workflow/reagent/labware 格式的 JSON 转换为统一工作流格式。
|
将 workflow/reagent 格式的 JSON 转换为统一工作流格式。
|
||||||
|
|
||||||
输入格式:
|
输入格式:
|
||||||
{
|
{
|
||||||
"labware": [
|
|
||||||
{"name": "...", "slot": "1", "type": "lab_xxx"},
|
|
||||||
...
|
|
||||||
],
|
|
||||||
"workflow": [
|
"workflow": [
|
||||||
{"action": "...", "action_args": {...}},
|
{"action": "...", "action_args": {...}},
|
||||||
...
|
...
|
||||||
],
|
],
|
||||||
"reagent": {
|
"reagent": {
|
||||||
"reagent_name": {"slot": int, "well": [...]},
|
"reagent_name": {"slot": int, "well": [...], "labware": "..."},
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,18 +245,18 @@ def convert_from_json(
|
|||||||
if "workflow" not in json_data or "reagent" not in json_data:
|
if "workflow" not in json_data or "reagent" not in json_data:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"不支持的 JSON 格式。请使用标准格式:\n"
|
"不支持的 JSON 格式。请使用标准格式:\n"
|
||||||
'{"labware": [...], "workflow": [...], "reagent": {...}}'
|
'{"workflow": [{"action": "...", "action_args": {...}}, ...], '
|
||||||
|
'"reagent": {"name": {"slot": int, "well": [...], "labware": "..."}, ...}}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# 提取数据
|
# 提取数据
|
||||||
workflow = json_data["workflow"]
|
workflow = json_data["workflow"]
|
||||||
reagent = json_data["reagent"]
|
reagent = json_data["reagent"]
|
||||||
labware_defs = json_data.get("labware", []) # 新的 labware 定义列表
|
|
||||||
|
|
||||||
# 规范化步骤数据
|
# 规范化步骤数据
|
||||||
protocol_steps = normalize_workflow_steps(workflow)
|
protocol_steps = normalize_workflow_steps(workflow)
|
||||||
|
|
||||||
# reagent 已经是字典格式,用于 set_liquid 和 well 数量查找
|
# reagent 已经是字典格式,直接使用
|
||||||
labware_info = reagent
|
labware_info = reagent
|
||||||
|
|
||||||
# 构建工作流图
|
# 构建工作流图
|
||||||
@@ -269,7 +265,6 @@ def convert_from_json(
|
|||||||
protocol_steps=protocol_steps,
|
protocol_steps=protocol_steps,
|
||||||
workstation_name=workstation_name,
|
workstation_name=workstation_name,
|
||||||
action_resource_mapping=ACTION_RESOURCE_MAPPING,
|
action_resource_mapping=ACTION_RESOURCE_MAPPING,
|
||||||
labware_defs=labware_defs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 校验句柄配置
|
# 校验句柄配置
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ def upload_workflow(
|
|||||||
workflow_name: Optional[str] = None,
|
workflow_name: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
published: bool = False,
|
published: bool = False,
|
||||||
description: str = "",
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
上传工作流到服务器
|
上传工作流到服务器
|
||||||
@@ -57,7 +56,6 @@ def upload_workflow(
|
|||||||
workflow_name: 工作流名称,如果不提供则从文件中读取或使用文件名
|
workflow_name: 工作流名称,如果不提供则从文件中读取或使用文件名
|
||||||
tags: 工作流标签列表,默认为空列表
|
tags: 工作流标签列表,默认为空列表
|
||||||
published: 是否发布工作流,默认为False
|
published: 是否发布工作流,默认为False
|
||||||
description: 工作流描述,发布时使用
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: API响应数据
|
Dict: API响应数据
|
||||||
@@ -77,14 +75,6 @@ def upload_workflow(
|
|||||||
print_status(f"工作流文件JSON解析失败: {e}", "error")
|
print_status(f"工作流文件JSON解析失败: {e}", "error")
|
||||||
return {"code": -1, "message": f"JSON解析失败: {e}"}
|
return {"code": -1, "message": f"JSON解析失败: {e}"}
|
||||||
|
|
||||||
# 从 JSON 文件中提取 description 和 tags(作为 fallback)
|
|
||||||
if not description and "description" in workflow_data:
|
|
||||||
description = workflow_data["description"]
|
|
||||||
print_status(f"从文件中读取 description", "info")
|
|
||||||
if not tags and "tags" in workflow_data:
|
|
||||||
tags = workflow_data["tags"]
|
|
||||||
print_status(f"从文件中读取 tags: {tags}", "info")
|
|
||||||
|
|
||||||
# 自动检测并转换格式
|
# 自动检测并转换格式
|
||||||
if not _is_node_link_format(workflow_data):
|
if not _is_node_link_format(workflow_data):
|
||||||
try:
|
try:
|
||||||
@@ -106,7 +96,6 @@ def upload_workflow(
|
|||||||
print_status(f" - 节点数量: {len(nodes)}", "info")
|
print_status(f" - 节点数量: {len(nodes)}", "info")
|
||||||
print_status(f" - 边数量: {len(edges)}", "info")
|
print_status(f" - 边数量: {len(edges)}", "info")
|
||||||
print_status(f" - 标签: {tags or []}", "info")
|
print_status(f" - 标签: {tags or []}", "info")
|
||||||
print_status(f" - 描述: {description[:50]}{'...' if len(description) > 50 else ''}", "info")
|
|
||||||
print_status(f" - 发布状态: {published}", "info")
|
print_status(f" - 发布状态: {published}", "info")
|
||||||
|
|
||||||
# 调用 http_client 上传
|
# 调用 http_client 上传
|
||||||
@@ -118,7 +107,6 @@ def upload_workflow(
|
|||||||
edges=edges,
|
edges=edges,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
published=published,
|
published=published,
|
||||||
description=description,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.get("code") == 0:
|
if result.get("code") == 0:
|
||||||
@@ -143,9 +131,8 @@ def handle_workflow_upload_command(args_dict: Dict[str, Any]) -> None:
|
|||||||
workflow_name = args_dict.get("workflow_name")
|
workflow_name = args_dict.get("workflow_name")
|
||||||
tags = args_dict.get("tags", [])
|
tags = args_dict.get("tags", [])
|
||||||
published = args_dict.get("published", False)
|
published = args_dict.get("published", False)
|
||||||
description = args_dict.get("description", "")
|
|
||||||
|
|
||||||
if workflow_file:
|
if workflow_file:
|
||||||
upload_workflow(workflow_file, workflow_name, tags, published, description)
|
upload_workflow(workflow_file, workflow_name, tags, published)
|
||||||
else:
|
else:
|
||||||
print_status("未指定工作流文件路径,请使用 -f/--workflow_file 参数", "error")
|
print_status("未指定工作流文件路径,请使用 -f/--workflow_file 参数", "error")
|
||||||
|
|||||||
@@ -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.18</version>
|
<version>0.10.17</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