From 216f19fb6201ebff48b8dd11894ba94c1f6ad268 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 26 Jan 2026 02:15:13 +0800 Subject: [PATCH 01/34] Workbench example, adjust log level, and ci check (#220) * TestLatency Return Value Example & gitignore update * Adjust log level & Add workbench virtual example & Add not action decorator & Add check_mode & * Add CI Check --- .conda/scripts/post-link.bat | 9 - .conda/scripts/post-link.sh | 9 - .cursorignore | 26 - .github/dependabot.yml | 19 + .github/workflows/ci-check.yml | 52 + .gitignore | 1 + unilabos/app/main.py | 24 +- unilabos/app/web/controller.py | 4 +- unilabos/app/ws_client.py | 31 +- unilabos/config/config.py | 1 + unilabos/devices/virtual/workbench.py | 687 ++ unilabos/registry/devices/virtual_device.yaml | 6054 +---------------- unilabos/registry/registry.py | 38 +- unilabos/ros/msgs/message_converter.py | 40 +- unilabos/ros/nodes/presets/host_node.py | 67 +- unilabos/test/experiments/virtual_bench.json | 28 + unilabos/utils/decorator.py | 46 + unilabos/utils/import_manager.py | 14 + 18 files changed, 1329 insertions(+), 5821 deletions(-) delete mode 100644 .conda/scripts/post-link.bat delete mode 100644 .conda/scripts/post-link.sh delete mode 100644 .cursorignore create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci-check.yml create mode 100644 unilabos/devices/virtual/workbench.py create mode 100644 unilabos/test/experiments/virtual_bench.json diff --git a/.conda/scripts/post-link.bat b/.conda/scripts/post-link.bat deleted file mode 100644 index 352b78c..0000000 --- a/.conda/scripts/post-link.bat +++ /dev/null @@ -1,9 +0,0 @@ -@echo off -setlocal enabledelayedexpansion - -REM upgrade pip -"%PREFIX%\python.exe" -m pip install --upgrade pip - -REM install extra deps -"%PREFIX%\python.exe" -m pip install paho-mqtt opentrons_shared_data -"%PREFIX%\python.exe" -m pip install git+https://github.com/Xuwznln/pylabrobot.git diff --git a/.conda/scripts/post-link.sh b/.conda/scripts/post-link.sh deleted file mode 100644 index ef96f15..0000000 --- a/.conda/scripts/post-link.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -set -euxo pipefail - -# make sure pip is available -"$PREFIX/bin/python" -m pip install --upgrade pip - -# install extra deps -"$PREFIX/bin/python" -m pip install paho-mqtt opentrons_shared_data -"$PREFIX/bin/python" -m pip install git+https://github.com/Xuwznln/pylabrobot.git diff --git a/.cursorignore b/.cursorignore deleted file mode 100644 index 7b0d4f9..0000000 --- a/.cursorignore +++ /dev/null @@ -1,26 +0,0 @@ -.conda -# .github -.idea -# .vscode -output -pylabrobot_repo -recipes -scripts -service -temp -# unilabos/test -# unilabos/app/web -unilabos/device_mesh -unilabos_data -unilabos_msgs -unilabos.egg-info -CONTRIBUTORS -# LICENSE -MANIFEST.in -pyrightconfig.json -# README.md -# README_zh.md -setup.py -setup.cfg -.gitattrubutes -**/__pycache__ diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..20a5faa --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: +# GitHub Actions +- package-ecosystem: "github-actions" + directory: "/" + target-branch: "dev" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + open-pull-requests-limit: 5 + reviewers: + - "msgcenterpy-team" + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "ci" + include: "scope" diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml new file mode 100644 index 0000000..fa5ada7 --- /dev/null +++ b/.github/workflows/ci-check.yml @@ -0,0 +1,52 @@ +name: CI Check + +on: + pull_request: + branches: [main, dev] + +jobs: + registry-check: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash -l {0} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Miniconda + uses: conda-incubator/setup-miniconda@v3 + with: + miniconda-version: 'latest' + channels: conda-forge,robostack-staging,uni-lab,defaults + channel-priority: strict + activate-environment: check-env + auto-activate-base: false + auto-update-conda: false + show-channel-urls: true + + - name: Install minimal ROS dependencies + run: | + conda install ros-humble-ros-core ros-humble-std-msgs ros-humble-geometry-msgs ros-humble-control-msgs -c robostack-staging -c conda-forge + + - name: Install unilabos-msgs and project + run: | + conda install ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge + pip install -e . + + - name: Run check mode (complete_registry) + run: | + python -m unilabos --check_mode --skip_env_check + + - name: Check for uncommitted changes + run: | + if ! git diff --exit-code; then + echo "::error::检测到文件变化!请先在本地运行 'python -m unilabos --complete_registry' 并提交变更" + echo "变化的文件:" + git diff --name-only + exit 1 + fi + echo "检查通过:无文件变化" diff --git a/.gitignore b/.gitignore index 610be61..838331e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ temp/ output/ unilabos_data/ pyrightconfig.json +.cursorignore ## Python # Byte-compiled / optimized / DLL files diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 8ec26c0..8cea5f6 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -161,6 +161,12 @@ def parse_args(): default=False, help="Complete registry information", ) + parser.add_argument( + "--check_mode", + action="store_true", + default=False, + help="Run in check mode for CI: validates registry imports and ensures no file changes", + ) parser.add_argument( "--no_update_feedback", action="store_true", @@ -314,6 +320,12 @@ def main(): BasicConfig.machine_name = machine_name BasicConfig.vis_2d_enable = args_dict["2d_vis"] + # Check mode 处理 + check_mode = args_dict.get("check_mode", False) + BasicConfig.check_mode = check_mode + if check_mode: + print_status("Check mode 启用,将进行 complete_registry 检查", "info") + from unilabos.resources.graphio import ( read_node_link_json, read_graphml, @@ -331,10 +343,14 @@ def main(): # 显示启动横幅 print_unilab_banner(args_dict) - # 注册表 - lab_registry = build_registry( - args_dict["registry_path"], args_dict.get("complete_registry", False), BasicConfig.upload_registry - ) + # 注册表 - check_mode 时强制启用 complete_registry + complete_registry = args_dict.get("complete_registry", False) or check_mode + lab_registry = build_registry(args_dict["registry_path"], complete_registry, BasicConfig.upload_registry) + + # Check mode: complete_registry 完成后直接退出,git diff 检测由 CI workflow 执行 + if check_mode: + print_status("Check mode: complete_registry 完成,退出", "info") + os._exit(0) if BasicConfig.upload_registry: # 设备注册到服务端 - 需要 ak 和 sk diff --git a/unilabos/app/web/controller.py b/unilabos/app/web/controller.py index 9b0f1ff..acd1f56 100644 --- a/unilabos/app/web/controller.py +++ b/unilabos/app/web/controller.py @@ -58,14 +58,14 @@ class JobResultStore: feedback=feedback or {}, timestamp=time.time(), ) - logger.debug(f"[JobResultStore] Stored result for job {job_id[:8]}, status={status}") + logger.trace(f"[JobResultStore] Stored result for job {job_id[:8]}, status={status}") def get_and_remove(self, job_id: str) -> Optional[JobResult]: """获取并删除任务结果""" with self._results_lock: result = self._results.pop(job_id, None) if result: - logger.debug(f"[JobResultStore] Retrieved and removed result for job {job_id[:8]}") + logger.trace(f"[JobResultStore] Retrieved and removed result for job {job_id[:8]}") return result def get_result(self, job_id: str) -> Optional[JobResult]: diff --git a/unilabos/app/ws_client.py b/unilabos/app/ws_client.py index 95526f0..17d69ac 100644 --- a/unilabos/app/ws_client.py +++ b/unilabos/app/ws_client.py @@ -154,7 +154,7 @@ class DeviceActionManager: job_info.set_ready_timeout(10) # 设置10秒超时 self.active_jobs[device_key] = job_info job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name) - logger.info(f"[DeviceActionManager] Job {job_log} can start immediately for {device_key}") + logger.trace(f"[DeviceActionManager] Job {job_log} can start immediately for {device_key}") return True def start_job(self, job_id: str) -> bool: @@ -210,8 +210,9 @@ class DeviceActionManager: job_info.update_timestamp() # 从all_jobs中移除已结束的job 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.info(f"[DeviceActionManager] Job {job_log} ended for {device_key}") + # job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name) + # logger.debug(f"[DeviceActionManager] Job {job_log} ended for {device_key}") + pass else: job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name) logger.warning(f"[DeviceActionManager] Job {job_log} was not active for {device_key}") @@ -227,7 +228,7 @@ class DeviceActionManager: next_job_log = format_job_log( next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name ) - logger.info(f"[DeviceActionManager] Next job {next_job_log} can start for {device_key}") + logger.trace(f"[DeviceActionManager] Next job {next_job_log} can start for {device_key}") return next_job return None @@ -268,7 +269,7 @@ class DeviceActionManager: # 从all_jobs中移除 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.info(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}") + logger.trace(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}") # 启动下一个任务 if device_key in self.device_queues and self.device_queues[device_key]: @@ -281,7 +282,7 @@ class DeviceActionManager: next_job_log = format_job_log( next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name ) - logger.info(f"[DeviceActionManager] Next job {next_job_log} can start after cancel") + logger.trace(f"[DeviceActionManager] Next job {next_job_log} can start after cancel") return True # 如果是排队中的任务 @@ -295,7 +296,7 @@ class DeviceActionManager: job_log = format_job_log( job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name ) - logger.info(f"[DeviceActionManager] Queued job {job_log} cancelled for {device_key}") + logger.trace(f"[DeviceActionManager] Queued job {job_log} cancelled for {device_key}") return True job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name) @@ -565,7 +566,7 @@ class MessageProcessor: async def _process_message(self, message_type: str, message_data: Dict[str, Any]): """处理收到的消息""" - logger.debug(f"[MessageProcessor] Processing message: {message_type}") + logger.trace(f"[MessageProcessor] Processing message: {message_type}") try: if message_type == "pong": @@ -637,13 +638,13 @@ class MessageProcessor: await self._send_action_state_response( device_id, action_name, task_id, job_id, "query_action_status", True, 0 ) - logger.info(f"[MessageProcessor] Job {job_log} can start immediately") + logger.trace(f"[MessageProcessor] Job {job_log} can start immediately") else: # 需要排队 await self._send_action_state_response( device_id, action_name, task_id, job_id, "query_action_status", False, 10 ) - logger.info(f"[MessageProcessor] Job {job_log} queued") + logger.trace(f"[MessageProcessor] Job {job_log} queued") # 通知QueueProcessor有新的队列更新 if self.queue_processor: @@ -1128,7 +1129,7 @@ class QueueProcessor: success = self.message_processor.send_message(message) job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name) if success: - logger.debug(f"[QueueProcessor] Sent busy/need_more for queued job {job_log}") + logger.trace(f"[QueueProcessor] Sent busy/need_more for queued job {job_log}") else: logger.warning(f"[QueueProcessor] Failed to send busy status for job {job_log}") @@ -1151,7 +1152,7 @@ class QueueProcessor: job_info.action_name, ) - logger.info(f"[QueueProcessor] Job {job_log} completed with status: {status}") + logger.trace(f"[QueueProcessor] Job {job_log} completed with status: {status}") # 结束任务,获取下一个可执行的任务 next_job = self.device_manager.end_job(job_id) @@ -1171,8 +1172,8 @@ class QueueProcessor: }, } self.message_processor.send_message(message) - next_job_log = format_job_log(next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name) - logger.info(f"[QueueProcessor] Notified next job {next_job_log} can start") + # next_job_log = format_job_log(next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name) + # logger.debug(f"[QueueProcessor] Notified next job {next_job_log} can start") # 立即触发下一轮状态检查 self.notify_queue_update() @@ -1314,7 +1315,7 @@ class WebSocketClient(BaseCommunicationClient): except (KeyError, AttributeError): logger.warning(f"[WebSocketClient] Failed to remove job {item.job_id} from HostNode status") - logger.info(f"[WebSocketClient] Intercepting final status for job_id: {item.job_id} - {status}") + # logger.debug(f"[WebSocketClient] Intercepting final status for job_id: {item.job_id} - {status}") # 通知队列处理器job完成(包括timeout的job) self.queue_processor.handle_job_completed(item.job_id, status) diff --git a/unilabos/config/config.py b/unilabos/config/config.py index f3dba5d..c91a07d 100644 --- a/unilabos/config/config.py +++ b/unilabos/config/config.py @@ -22,6 +22,7 @@ class BasicConfig: startup_json_path = None # 填写绝对路径 disable_browser = False # 禁止浏览器自动打开 port = 8002 # 本地HTTP服务 + check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性 # 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL' log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG" diff --git a/unilabos/devices/virtual/workbench.py b/unilabos/devices/virtual/workbench.py new file mode 100644 index 0000000..7a8e145 --- /dev/null +++ b/unilabos/devices/virtual/workbench.py @@ -0,0 +1,687 @@ +""" +Virtual Workbench Device - 模拟工作台设备 +包含: +- 1个机械臂 (每次操作3s, 独占锁) +- 3个加热台 (每次加热10s, 可并行) + +工作流程: +1. A1-A5 物料同时启动,竞争机械臂 +2. 机械臂将物料移动到空闲加热台 +3. 加热完成后,机械臂将物料移动到C1-C5 + +注意:调用来自线程池,使用 threading.Lock 进行同步 +""" +import logging +import time +from typing import Dict, Any, Optional +from dataclasses import dataclass +from enum import Enum +from threading import Lock, RLock + +from typing_extensions import TypedDict + +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode +from unilabos.utils.decorator import not_action + + +# ============ TypedDict 返回类型定义 ============ + +class MoveToHeatingStationResult(TypedDict): + """move_to_heating_station 返回类型""" + success: bool + station_id: int + material_id: str + material_number: int + message: str + + +class StartHeatingResult(TypedDict): + """start_heating 返回类型""" + success: bool + station_id: int + material_id: str + material_number: int + message: str + + +class MoveToOutputResult(TypedDict): + """move_to_output 返回类型""" + success: bool + station_id: int + material_id: str + + +class PrepareMaterialsResult(TypedDict): + """prepare_materials 返回类型 - 批量准备物料""" + success: bool + count: int + material_1: int # 物料编号1 + material_2: int # 物料编号2 + material_3: int # 物料编号3 + material_4: int # 物料编号4 + material_5: int # 物料编号5 + message: str + + +# ============ 状态枚举 ============ + +class HeatingStationState(Enum): + """加热台状态枚举""" + IDLE = "idle" # 空闲 + OCCUPIED = "occupied" # 已放置物料,等待加热 + HEATING = "heating" # 加热中 + COMPLETED = "completed" # 加热完成,等待取走 + + +class ArmState(Enum): + """机械臂状态枚举""" + IDLE = "idle" # 空闲 + BUSY = "busy" # 工作中 + + +@dataclass +class HeatingStation: + """加热台数据结构""" + station_id: int + state: HeatingStationState = HeatingStationState.IDLE + current_material: Optional[str] = None # 当前物料 (如 "A1", "A2") + material_number: Optional[int] = None # 物料编号 (1-5) + heating_start_time: Optional[float] = None + heating_progress: float = 0.0 + + +class VirtualWorkbench: + """ + Virtual Workbench Device - 虚拟工作台设备 + + 模拟一个包含1个机械臂和3个加热台的工作站 + - 机械臂操作耗时3秒,同一时间只能执行一个操作 + - 加热台加热耗时10秒,3个加热台可并行工作 + + 工作流: + 1. 物料A1-A5并发启动(线程池),竞争机械臂使用权 + 2. 获取机械臂后,查找空闲加热台 + 3. 机械臂将物料放入加热台,开始加热 + 4. 加热完成后,机械臂将物料移动到目标位置Cn + """ + + _ros_node: BaseROS2DeviceNode + + # 配置常量 + ARM_OPERATION_TIME: float = 3.0 # 机械臂操作时间(秒) + HEATING_TIME: float = 10.0 # 加热时间(秒) + NUM_HEATING_STATIONS: int = 3 # 加热台数量 + + def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): + # 处理可能的不同调用方式 + if device_id is None and "id" in kwargs: + device_id = kwargs.pop("id") + if config is None and "config" in kwargs: + config = kwargs.pop("config") + + self.device_id = device_id or "virtual_workbench" + self.config = config or {} + + self.logger = logging.getLogger(f"VirtualWorkbench.{self.device_id}") + self.data: Dict[str, Any] = {} + + # 从config中获取可配置参数 + self.ARM_OPERATION_TIME = float(self.config.get("arm_operation_time", 3.0)) + self.HEATING_TIME = float(self.config.get("heating_time", 10.0)) + self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", 3)) + + # 机械臂状态和锁 (使用threading.Lock) + self._arm_lock = Lock() + self._arm_state = ArmState.IDLE + self._arm_current_task: Optional[str] = None + + # 加热台状态 (station_id -> HeatingStation) - 立即初始化,不依赖initialize() + self._heating_stations: Dict[int, HeatingStation] = { + i: HeatingStation(station_id=i) + for i in range(1, self.NUM_HEATING_STATIONS + 1) + } + self._stations_lock = RLock() # 可重入锁,保护加热台状态 + + # 任务追踪 + self._active_tasks: Dict[str, Dict[str, Any]] = {} # material_id -> task_info + self._tasks_lock = Lock() + + # 处理其他kwargs参数 + skip_keys = {"arm_operation_time", "heating_time", "num_heating_stations"} + for key, value in kwargs.items(): + if key not in skip_keys and not hasattr(self, key): + setattr(self, key, value) + + self.logger.info(f"=== 虚拟工作台 {self.device_id} 已创建 ===") + self.logger.info( + f"机械臂操作时间: {self.ARM_OPERATION_TIME}s | " + f"加热时间: {self.HEATING_TIME}s | " + f"加热台数量: {self.NUM_HEATING_STATIONS}" + ) + + @not_action + def post_init(self, ros_node: BaseROS2DeviceNode): + """ROS节点初始化后回调""" + self._ros_node = ros_node + + @not_action + def initialize(self) -> bool: + """初始化虚拟工作台""" + self.logger.info(f"初始化虚拟工作台 {self.device_id}") + + # 重置加热台状态 (已在__init__中创建,这里重置为初始状态) + with self._stations_lock: + for station in self._heating_stations.values(): + station.state = HeatingStationState.IDLE + station.current_material = None + station.material_number = None + station.heating_progress = 0.0 + + # 初始化状态 + self.data.update({ + "status": "Ready", + "arm_state": ArmState.IDLE.value, + "arm_current_task": None, + "heating_stations": self._get_stations_status(), + "active_tasks_count": 0, + "message": "工作台就绪", + }) + + self.logger.info(f"工作台初始化完成: {self.NUM_HEATING_STATIONS}个加热台就绪") + return True + + @not_action + def cleanup(self) -> bool: + """清理虚拟工作台""" + self.logger.info(f"清理虚拟工作台 {self.device_id}") + + self._arm_state = ArmState.IDLE + self._arm_current_task = None + + with self._stations_lock: + self._heating_stations.clear() + + with self._tasks_lock: + self._active_tasks.clear() + + self.data.update({ + "status": "Offline", + "arm_state": ArmState.IDLE.value, + "heating_stations": {}, + "message": "工作台已关闭", + }) + return True + + def _get_stations_status(self) -> Dict[int, Dict[str, Any]]: + """获取所有加热台状态""" + with self._stations_lock: + return { + station_id: { + "state": station.state.value, + "current_material": station.current_material, + "material_number": station.material_number, + "heating_progress": station.heating_progress, + } + for station_id, station in self._heating_stations.items() + } + + def _update_data_status(self, message: Optional[str] = None): + """更新状态数据""" + self.data.update({ + "arm_state": self._arm_state.value, + "arm_current_task": self._arm_current_task, + "heating_stations": self._get_stations_status(), + "active_tasks_count": len(self._active_tasks), + }) + if message: + self.data["message"] = message + + def _find_available_heating_station(self) -> Optional[int]: + """查找空闲的加热台 + + Returns: + 空闲加热台ID,如果没有则返回None + """ + with self._stations_lock: + for station_id, station in self._heating_stations.items(): + if station.state == HeatingStationState.IDLE: + return station_id + return None + + def _acquire_arm(self, task_description: str) -> bool: + """获取机械臂使用权(阻塞直到获取) + + Args: + task_description: 任务描述,用于日志 + + Returns: + 是否成功获取 + """ + self.logger.info(f"[{task_description}] 等待获取机械臂...") + + # 阻塞等待获取锁 + self._arm_lock.acquire() + + self._arm_state = ArmState.BUSY + self._arm_current_task = task_description + self._update_data_status(f"机械臂执行: {task_description}") + + self.logger.info(f"[{task_description}] 成功获取机械臂使用权") + return True + + def _release_arm(self): + """释放机械臂""" + task = self._arm_current_task + self._arm_state = ArmState.IDLE + self._arm_current_task = None + self._arm_lock.release() + self._update_data_status(f"机械臂已释放 (完成: {task})") + self.logger.info(f"机械臂已释放 (完成: {task})") + + def prepare_materials( + self, + count: int = 5, + ) -> PrepareMaterialsResult: + """ + 批量准备物料 - 虚拟起始节点 + + 作为工作流的起始节点,生成指定数量的物料编号供后续节点使用。 + 输出5个handle (material_1 ~ material_5),分别对应实验1~5。 + + Args: + count: 待生成的物料数量,默认5 (生成 A1-A5) + + Returns: + PrepareMaterialsResult: 包含 material_1 ~ material_5 用于传递给 move_to_heating_station + """ + # 生成物料列表 A1 - A{count} + materials = [i for i in range(1, count + 1)] + + self.logger.info( + f"[准备物料] 生成 {count} 个物料: " + f"A1-A{count} -> material_1~material_{count}" + ) + + return { + "success": True, + "count": count, + "material_1": materials[0] if len(materials) > 0 else 0, + "material_2": materials[1] if len(materials) > 1 else 0, + "material_3": materials[2] if len(materials) > 2 else 0, + "material_4": materials[3] if len(materials) > 3 else 0, + "material_5": materials[4] if len(materials) > 4 else 0, + "message": f"已准备 {count} 个物料: A1-A{count}", + } + + def move_to_heating_station( + self, + material_number: int, + ) -> MoveToHeatingStationResult: + """ + 将物料从An位置移动到加热台 + + 多线程并发调用时,会竞争机械臂使用权,并自动查找空闲加热台 + + Args: + material_number: 物料编号 (1-5) + + Returns: + MoveToHeatingStationResult: 包含 station_id, material_number 等用于传递给下一个节点 + """ + # 根据物料编号生成物料ID + material_id = f"A{material_number}" + task_desc = f"移动{material_id}到加热台" + self.logger.info(f"[任务] {task_desc} - 开始执行") + + # 记录任务 + with self._tasks_lock: + self._active_tasks[material_id] = { + "status": "waiting_for_arm", + "start_time": time.time(), + } + + try: + # 步骤1: 等待获取机械臂使用权(竞争) + with self._tasks_lock: + self._active_tasks[material_id]["status"] = "waiting_for_arm" + self._acquire_arm(task_desc) + + # 步骤2: 查找空闲加热台 + with self._tasks_lock: + self._active_tasks[material_id]["status"] = "finding_station" + station_id = None + + # 循环等待直到找到空闲加热台 + while station_id is None: + station_id = self._find_available_heating_station() + if station_id is None: + self.logger.info(f"[{material_id}] 没有空闲加热台,等待中...") + # 释放机械臂,等待后重试 + self._release_arm() + time.sleep(0.5) + self._acquire_arm(task_desc) + + # 步骤3: 占用加热台 - 立即标记为OCCUPIED,防止其他任务选择同一加热台 + with self._stations_lock: + self._heating_stations[station_id].state = HeatingStationState.OCCUPIED + self._heating_stations[station_id].current_material = material_id + self._heating_stations[station_id].material_number = material_number + + # 步骤4: 模拟机械臂移动操作 (3秒) + with self._tasks_lock: + self._active_tasks[material_id]["status"] = "arm_moving" + self._active_tasks[material_id]["assigned_station"] = station_id + self.logger.info(f"[{material_id}] 机械臂正在移动到加热台{station_id}...") + + time.sleep(self.ARM_OPERATION_TIME) + + # 步骤5: 放入加热台完成 + self._update_data_status(f"{material_id}已放入加热台{station_id}") + self.logger.info(f"[{material_id}] 已放入加热台{station_id} (用时{self.ARM_OPERATION_TIME}s)") + + # 释放机械臂 + self._release_arm() + + with self._tasks_lock: + self._active_tasks[material_id]["status"] = "placed_on_station" + + return { + "success": True, + "station_id": station_id, + "material_id": material_id, + "material_number": material_number, + "message": f"{material_id}已成功移动到加热台{station_id}", + } + + except Exception as e: + self.logger.error(f"[{material_id}] 移动失败: {str(e)}") + if self._arm_lock.locked(): + self._release_arm() + return { + "success": False, + "station_id": -1, + "material_id": material_id, + "material_number": material_number, + "message": f"移动失败: {str(e)}", + } + + def start_heating( + self, + station_id: int, + material_number: int, + ) -> StartHeatingResult: + """ + 启动指定加热台的加热程序 + + Args: + station_id: 加热台ID (1-3),从 move_to_heating_station 的 handle 传入 + material_number: 物料编号,从 move_to_heating_station 的 handle 传入 + + Returns: + StartHeatingResult: 包含 station_id, material_number 等用于传递给下一个节点 + """ + self.logger.info(f"[加热台{station_id}] 开始加热") + + if station_id not in self._heating_stations: + return { + "success": False, + "station_id": station_id, + "material_id": "", + "material_number": material_number, + "message": f"无效的加热台ID: {station_id}", + } + + with self._stations_lock: + station = self._heating_stations[station_id] + + if station.current_material is None: + return { + "success": False, + "station_id": station_id, + "material_id": "", + "material_number": material_number, + "message": f"加热台{station_id}上没有物料", + } + + if station.state == HeatingStationState.HEATING: + return { + "success": False, + "station_id": station_id, + "material_id": station.current_material, + "material_number": material_number, + "message": f"加热台{station_id}已经在加热中", + } + + material_id = station.current_material + + # 开始加热 + station.state = HeatingStationState.HEATING + station.heating_start_time = time.time() + station.heating_progress = 0.0 + + with self._tasks_lock: + if material_id in self._active_tasks: + self._active_tasks[material_id]["status"] = "heating" + + self._update_data_status(f"加热台{station_id}开始加热{material_id}") + + # 模拟加热过程 (10秒) + start_time = time.time() + while True: + elapsed = time.time() - start_time + progress = min(100.0, (elapsed / self.HEATING_TIME) * 100) + + with self._stations_lock: + self._heating_stations[station_id].heating_progress = progress + + self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%") + + if elapsed >= self.HEATING_TIME: + break + + time.sleep(1.0) + + # 加热完成 + with self._stations_lock: + self._heating_stations[station_id].state = HeatingStationState.COMPLETED + self._heating_stations[station_id].heating_progress = 100.0 + + with self._tasks_lock: + if material_id in self._active_tasks: + self._active_tasks[material_id]["status"] = "heating_completed" + + self._update_data_status(f"加热台{station_id}加热完成") + self.logger.info(f"[加热台{station_id}] {material_id}加热完成 (用时{self.HEATING_TIME}s)") + + return { + "success": True, + "station_id": station_id, + "material_id": material_id, + "material_number": material_number, + "message": f"加热台{station_id}加热完成", + } + + def move_to_output( + self, + station_id: int, + material_number: int, + ) -> MoveToOutputResult: + """ + 将物料从加热台移动到输出位置Cn + + Args: + station_id: 加热台ID (1-3),从 start_heating 的 handle 传入 + material_number: 物料编号,从 start_heating 的 handle 传入,用于确定输出位置 Cn + + Returns: + MoveToOutputResult: 包含执行结果 + """ + output_number = material_number # 物料编号决定输出位置 + + if station_id not in self._heating_stations: + return { + "success": False, + "station_id": station_id, + "material_id": "", + "output_position": f"C{output_number}", + "message": f"无效的加热台ID: {station_id}", + } + + with self._stations_lock: + station = self._heating_stations[station_id] + material_id = station.current_material + + if material_id is None: + return { + "success": False, + "station_id": station_id, + "material_id": "", + "output_position": f"C{output_number}", + "message": f"加热台{station_id}上没有物料", + } + + if station.state != HeatingStationState.COMPLETED: + return { + "success": False, + "station_id": station_id, + "material_id": material_id, + "output_position": f"C{output_number}", + "message": f"加热台{station_id}尚未完成加热 (当前状态: {station.state.value})", + } + + output_position = f"C{output_number}" + task_desc = f"从加热台{station_id}移动{material_id}到{output_position}" + self.logger.info(f"[任务] {task_desc}") + + try: + with self._tasks_lock: + if material_id in self._active_tasks: + self._active_tasks[material_id]["status"] = "waiting_for_arm_output" + + # 获取机械臂 + self._acquire_arm(task_desc) + + with self._tasks_lock: + if material_id in self._active_tasks: + self._active_tasks[material_id]["status"] = "arm_moving_to_output" + + # 模拟机械臂操作 (3秒) + self.logger.info(f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}...") + time.sleep(self.ARM_OPERATION_TIME) + + # 清空加热台 + with self._stations_lock: + self._heating_stations[station_id].state = HeatingStationState.IDLE + self._heating_stations[station_id].current_material = None + self._heating_stations[station_id].material_number = None + self._heating_stations[station_id].heating_progress = 0.0 + self._heating_stations[station_id].heating_start_time = None + + # 释放机械臂 + self._release_arm() + + # 任务完成 + with self._tasks_lock: + if material_id in self._active_tasks: + self._active_tasks[material_id]["status"] = "completed" + self._active_tasks[material_id]["end_time"] = time.time() + + self._update_data_status(f"{material_id}已移动到{output_position}") + self.logger.info(f"[{material_id}] 已成功移动到{output_position} (用时{self.ARM_OPERATION_TIME}s)") + + return { + "success": True, + "station_id": station_id, + "material_id": material_id, + "output_position": output_position, + "message": f"{material_id}已成功移动到{output_position}", + } + + except Exception as e: + self.logger.error(f"移动到输出位置失败: {str(e)}") + if self._arm_lock.locked(): + self._release_arm() + return { + "success": False, + "station_id": station_id, + "material_id": "", + "output_position": output_position, + "message": f"移动失败: {str(e)}", + } + + # ============ 状态属性 ============ + + @property + def status(self) -> str: + return self.data.get("status", "Unknown") + + @property + def arm_state(self) -> str: + return self._arm_state.value + + @property + def arm_current_task(self) -> str: + return self._arm_current_task or "" + + @property + def heating_station_1_state(self) -> str: + with self._stations_lock: + station = self._heating_stations.get(1) + return station.state.value if station else "unknown" + + @property + def heating_station_1_material(self) -> str: + with self._stations_lock: + station = self._heating_stations.get(1) + return station.current_material or "" if station else "" + + @property + def heating_station_1_progress(self) -> float: + with self._stations_lock: + station = self._heating_stations.get(1) + return station.heating_progress if station else 0.0 + + @property + def heating_station_2_state(self) -> str: + with self._stations_lock: + station = self._heating_stations.get(2) + return station.state.value if station else "unknown" + + @property + def heating_station_2_material(self) -> str: + with self._stations_lock: + station = self._heating_stations.get(2) + return station.current_material or "" if station else "" + + @property + def heating_station_2_progress(self) -> float: + with self._stations_lock: + station = self._heating_stations.get(2) + return station.heating_progress if station else 0.0 + + @property + def heating_station_3_state(self) -> str: + with self._stations_lock: + station = self._heating_stations.get(3) + return station.state.value if station else "unknown" + + @property + def heating_station_3_material(self) -> str: + with self._stations_lock: + station = self._heating_stations.get(3) + return station.current_material or "" if station else "" + + @property + def heating_station_3_progress(self) -> float: + with self._stations_lock: + station = self._heating_stations.get(3) + return station.heating_progress if station else 0.0 + + @property + def active_tasks_count(self) -> int: + with self._tasks_lock: + return len(self._active_tasks) + + @property + def message(self) -> str: + return self.data.get("message", "") diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index 77ac533..b1b1ab6 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -1,267 +1,324 @@ -virtual_centrifuge: +virtual_workbench: category: - virtual_device class: action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: + auto-move_to_heating_station: feedback: {} goal: {} goal_default: - ros_node: null - handles: {} + material_number: null + handles: + input: + - data_key: material_number + data_source: handle + data_type: workbench_material + handler_key: material_input + label: 物料编号 + output: + - data_key: station_id + data_source: executor + data_type: workbench_station + handler_key: heating_station_output + label: 加热台ID + - data_key: material_number + data_source: executor + data_type: workbench_material + handler_key: material_number_output + label: 物料编号 placeholder_keys: {} result: {} schema: - description: '' + description: 将物料从An位置移动到空闲加热台,返回分配的加热台ID properties: feedback: {} goal: properties: - ros_node: - type: object + material_number: + description: 物料编号,1-5,物料ID自动生成为A{n} + type: integer required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - centrifuge: - feedback: - current_speed: current_speed - current_status: status - current_temp: current_temp - progress: progress - goal: - speed: speed - temp: temp - time: time - vessel: vessel - goal_default: - speed: 0.0 - temp: 0.0 - time: 0.0 - vessel: - 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: '' - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_speed: - type: number - current_status: - type: string - current_temp: - type: number - progress: - type: number - required: - - progress - - current_speed - - current_temp - - current_status - title: Centrifuge_Feedback - type: object - goal: - properties: - speed: - type: number - temp: - type: number - time: - type: number - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - speed - - time - - temp - title: Centrifuge_Goal + - material_number type: object result: + description: move_to_heating_station 返回类型 properties: + material_id: + title: Material Id + type: string + material_number: + title: Material Number + type: integer message: + title: Message type: string - return_info: - type: string + station_id: + description: 分配的加热台ID + title: Station Id + type: integer success: + title: Success type: boolean required: - success + - station_id + - material_id + - material_number - message - - return_info - title: Centrifuge_Result + title: MoveToHeatingStationResult type: object required: - goal - title: Centrifuge + title: move_to_heating_station参数 type: object - type: Centrifuge - module: unilabos.devices.virtual.virtual_centrifuge:VirtualCentrifuge + type: UniLabJsonCommand + auto-move_to_output: + feedback: {} + goal: {} + goal_default: + material_number: null + station_id: null + handles: + input: + - data_key: station_id + data_source: handle + data_type: workbench_station + handler_key: output_station_input + label: 加热台ID + - data_key: material_number + data_source: handle + data_type: workbench_material + handler_key: output_material_input + label: 物料编号 + placeholder_keys: {} + result: {} + schema: + description: 将物料从加热台移动到输出位置Cn + properties: + feedback: {} + goal: + properties: + material_number: + description: 物料编号,用于确定输出位置Cn + type: integer + station_id: + description: 加热台ID,1-3,从上一节点传入 + type: integer + required: + - station_id + - material_number + type: object + result: + description: move_to_output 返回类型 + properties: + material_id: + title: Material Id + type: string + station_id: + title: Station Id + type: integer + success: + title: Success + type: boolean + required: + - success + - station_id + - material_id + title: MoveToOutputResult + type: object + required: + - goal + title: move_to_output参数 + type: object + type: UniLabJsonCommand + auto-prepare_materials: + feedback: {} + goal: {} + goal_default: + count: 5 + handles: + output: + - data_key: material_1 + data_source: executor + data_type: workbench_material + handler_key: channel_1 + label: 实验1 + - data_key: material_2 + data_source: executor + data_type: workbench_material + handler_key: channel_2 + label: 实验2 + - data_key: material_3 + data_source: executor + data_type: workbench_material + handler_key: channel_3 + label: 实验3 + - data_key: material_4 + data_source: executor + data_type: workbench_material + handler_key: channel_4 + label: 实验4 + - data_key: material_5 + data_source: executor + data_type: workbench_material + handler_key: channel_5 + label: 实验5 + placeholder_keys: {} + result: {} + schema: + description: 批量准备物料 - 虚拟起始节点,生成A1-A5物料,输出5个handle供后续节点使用 + properties: + feedback: {} + goal: + properties: + count: + default: 5 + description: 待生成的物料数量,默认5 (生成 A1-A5) + type: integer + required: [] + type: object + result: + description: prepare_materials 返回类型 - 批量准备物料 + properties: + count: + title: Count + type: integer + material_1: + title: Material 1 + type: integer + material_2: + title: Material 2 + type: integer + material_3: + title: Material 3 + type: integer + material_4: + title: Material 4 + type: integer + material_5: + title: Material 5 + type: integer + message: + title: Message + type: string + success: + title: Success + type: boolean + required: + - success + - count + - material_1 + - material_2 + - material_3 + - material_4 + - material_5 + - message + title: PrepareMaterialsResult + type: object + required: + - goal + title: prepare_materials参数 + type: object + type: UniLabJsonCommand + auto-start_heating: + feedback: {} + goal: {} + goal_default: + material_number: null + station_id: null + handles: + input: + - data_key: station_id + data_source: handle + data_type: workbench_station + handler_key: station_id_input + label: 加热台ID + - data_key: material_number + data_source: handle + data_type: workbench_material + handler_key: material_number_input + label: 物料编号 + output: + - data_key: station_id + data_source: executor + data_type: workbench_station + handler_key: heating_done_station + label: 加热完成-加热台ID + - data_key: material_number + data_source: executor + data_type: workbench_material + handler_key: heating_done_material + label: 加热完成-物料编号 + placeholder_keys: {} + result: {} + schema: + description: 启动指定加热台的加热程序 + properties: + feedback: {} + goal: + properties: + material_number: + description: 物料编号,从上一节点传入 + type: integer + station_id: + description: 加热台ID,1-3,从上一节点传入 + type: integer + required: + - station_id + - material_number + type: object + result: + description: start_heating 返回类型 + properties: + material_id: + title: Material Id + type: string + material_number: + title: Material Number + type: integer + message: + title: Message + type: string + station_id: + title: Station Id + type: integer + success: + title: Success + type: boolean + required: + - success + - station_id + - material_id + - material_number + - message + title: StartHeatingResult + type: object + required: + - goal + title: start_heating参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.virtual.workbench:VirtualWorkbench status_types: - centrifuge_state: str - current_speed: float - current_temp: float - max_speed: float - max_temp: float + active_tasks_count: int + arm_current_task: str + arm_state: str + heating_station_1_material: str + heating_station_1_progress: float + heating_station_1_state: str + heating_station_2_material: str + heating_station_2_progress: float + heating_station_2_state: str + heating_station_3_material: str + heating_station_3_progress: float + heating_station_3_state: str message: str - min_temp: float - progress: float status: str - target_speed: float - target_temp: float - time_remaining: float type: python config_info: [] - description: Virtual Centrifuge for CentrifugeProtocol Testing - handles: - - data_key: vessel - data_source: handle - data_type: transport - description: 需要离心的样品容器 - handler_key: centrifuge - io_type: target - label: centrifuge - side: NORTH + description: Virtual Workbench with 1 robotic arm and 3 heating stations for concurrent + material processing + handles: [] icon: '' init_param_schema: config: @@ -274,5521 +331,48 @@ virtual_centrifuge: type: object data: properties: - centrifuge_state: + active_tasks_count: + type: integer + arm_current_task: type: string - current_speed: + arm_state: + type: string + heating_station_1_material: + type: string + heating_station_1_progress: type: number - current_temp: + heating_station_1_state: + type: string + heating_station_2_material: + type: string + heating_station_2_progress: type: number - max_speed: - type: number - max_temp: + heating_station_2_state: + type: string + heating_station_3_material: + type: string + heating_station_3_progress: type: number + heating_station_3_state: + type: string message: type: string - min_temp: - type: number - progress: - type: number status: type: string - target_speed: - type: number - target_temp: - type: number - time_remaining: - type: number required: - status - - centrifuge_state - - current_speed - - target_speed - - current_temp - - target_temp - - max_speed - - max_temp - - min_temp - - time_remaining - - progress + - arm_state + - arm_current_task + - heating_station_1_state + - heating_station_1_material + - heating_station_1_progress + - heating_station_2_state + - heating_station_2_material + - heating_station_2_progress + - heating_station_3_state + - heating_station_3_material + - heating_station_3_progress + - active_tasks_count - message type: object version: 1.0.0 -virtual_column: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - run_column: - feedback: - current_status: current_status - processed_volume: processed_volume - progress: progress - goal: - column: column - from_vessel: from_vessel - to_vessel: to_vessel - goal_default: - column: '' - from_vessel: - 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: '' - pct1: '' - pct2: '' - ratio: '' - rf: '' - solvent1: '' - solvent2: '' - to_vessel: - 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: '' - handles: {} - result: - message: current_status - return_info: current_status - success: success - schema: - description: '' - properties: - feedback: - properties: - progress: - type: number - status: - type: string - required: - - status - - progress - title: RunColumn_Feedback - type: object - goal: - properties: - column: - type: string - from_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: from_vessel - type: object - pct1: - type: string - pct2: - type: string - ratio: - type: string - rf: - type: string - solvent1: - type: string - solvent2: - type: string - to_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: to_vessel - type: object - required: - - from_vessel - - to_vessel - - column - - rf - - pct1 - - pct2 - - solvent1 - - solvent2 - - ratio - title: RunColumn_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: RunColumn_Result - type: object - required: - - goal - title: RunColumn - type: object - type: RunColumn - module: unilabos.devices.virtual.virtual_column:VirtualColumn - status_types: - column_diameter: float - column_length: float - column_state: str - current_flow_rate: float - current_phase: str - current_status: str - final_volume: float - max_flow_rate: float - processed_volume: float - progress: float - status: str - type: python - config_info: [] - description: Virtual Column Chromatography Device for RunColumn Protocol Testing - handles: - - data_key: from_vessel - data_source: handle - data_type: transport - description: 样品输入口 - handler_key: columnin - io_type: target - label: columnin - side: WEST - - data_key: to_vessel - data_source: handle - data_type: transport - description: 产物输出口 - handler_key: columnout - io_type: source - label: columnout - side: EAST - icon: '' - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - column_diameter: - type: number - column_length: - type: number - column_state: - type: string - current_flow_rate: - type: number - current_phase: - type: string - current_status: - type: string - final_volume: - type: number - max_flow_rate: - type: number - processed_volume: - type: number - progress: - type: number - status: - type: string - required: - - status - - column_state - - current_flow_rate - - max_flow_rate - - column_length - - column_diameter - - processed_volume - - progress - - current_status - - current_phase - - final_volume - type: object - version: 1.0.0 -virtual_filter: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - filter: - feedback: - current_status: current_status - current_temp: current_temp - filtered_volume: filtered_volume - progress: progress - goal: - continue_heatchill: continue_heatchill - filtrate_vessel: filtrate_vessel - stir: stir - stir_speed: stir_speed - temp: temp - vessel: vessel - volume: volume - goal_default: - continue_heatchill: false - filtrate_vessel: - 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: '' - stir: false - stir_speed: 0.0 - temp: 0.0 - vessel: - 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: '' - volume: 0.0 - handles: {} - result: - message: message - return_info: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_status: - type: string - current_temp: - type: number - filtered_volume: - type: number - progress: - type: number - required: - - progress - - current_temp - - filtered_volume - - current_status - title: Filter_Feedback - type: object - goal: - properties: - continue_heatchill: - type: boolean - filtrate_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: filtrate_vessel - type: object - stir: - type: boolean - stir_speed: - type: number - temp: - type: number - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - volume: - type: number - required: - - vessel - - filtrate_vessel - - stir - - stir_speed - - temp - - continue_heatchill - - volume - title: Filter_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Filter_Result - type: object - required: - - goal - title: Filter - type: object - type: Filter - module: unilabos.devices.virtual.virtual_filter:VirtualFilter - status_types: - current_status: str - current_temp: float - filtered_volume: float - max_stir_speed: float - max_temp: float - max_volume: float - message: str - progress: float - status: str - type: python - config_info: [] - description: Virtual Filter for FilterProtocol Testing - handles: - - data_key: vessel_in - data_source: handle - data_type: transport - description: 需要过滤的样品容器 - handler_key: filterin - io_type: target - label: filter_in - side: NORTH - - data_key: filtrate_out - data_source: handle - data_type: transport - description: 滤液出口 - handler_key: filtrateout - io_type: source - label: filtrate_out - side: SOUTH - - data_key: retentate_out - data_source: handle - data_type: transport - description: 滤渣/固体出口 - handler_key: retentateout - io_type: source - label: retentate_out - side: EAST - icon: '' - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - current_status: - type: string - current_temp: - type: number - filtered_volume: - type: number - max_stir_speed: - type: number - max_temp: - type: number - max_volume: - type: number - message: - type: string - progress: - type: number - status: - type: string - required: - - status - - progress - - current_temp - - current_status - - filtered_volume - - message - - max_temp - - max_stir_speed - - max_volume - type: object - version: 1.0.0 -virtual_gas_source: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-is_closed: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_closed的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_closed参数 - type: object - type: UniLabJsonCommand - auto-is_open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_open的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_open参数 - type: object - type: UniLabJsonCommand - close: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - set_status: - feedback: {} - goal: - string: string - goal_default: - string: '' - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: StrSingleInput_Feedback - type: object - goal: - properties: - string: - type: string - required: - - string - title: StrSingleInput_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: StrSingleInput_Result - type: object - required: - - goal - title: StrSingleInput - type: object - type: StrSingleInput - module: unilabos.devices.virtual.virtual_gas_source:VirtualGasSource - status_types: - status: str - type: python - config_info: [] - description: Virtual gas source - handles: - - data_key: fluid_out - data_source: executor - data_type: fluid - description: 气源出气口 - handler_key: gassource - io_type: source - label: gassource - side: SOUTH - icon: '' - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - status: - type: string - required: - - status - type: object - version: 1.0.0 -virtual_heatchill: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - heat_chill: - feedback: - status: status - goal: - purpose: purpose - stir: stir - stir_speed: stir_speed - temp: temp - time: time - vessel: vessel - goal_default: - pressure: '' - purpose: '' - reflux_solvent: '' - stir: false - stir_speed: 0.0 - temp: 0.0 - temp_spec: '' - time: '' - time_spec: '' - vessel: - 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: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: HeatChill_Feedback - type: object - goal: - properties: - pressure: - type: string - purpose: - type: string - reflux_solvent: - type: string - stir: - type: boolean - stir_speed: - type: number - temp: - type: number - temp_spec: - type: string - time: - type: string - time_spec: - type: string - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - temp - - time - - temp_spec - - time_spec - - pressure - - reflux_solvent - - stir - - stir_speed - - purpose - title: HeatChill_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: HeatChill_Result - type: object - required: - - goal - title: HeatChill - type: object - type: HeatChill - heat_chill_start: - feedback: - status: status - goal: - purpose: purpose - temp: temp - vessel: vessel - goal_default: - purpose: '' - temp: 0.0 - vessel: - 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: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: HeatChillStart_Feedback - type: object - goal: - properties: - purpose: - type: string - temp: - type: number - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - temp - - purpose - title: HeatChillStart_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: HeatChillStart_Result - type: object - required: - - goal - title: HeatChillStart - type: object - type: HeatChillStart - heat_chill_stop: - feedback: - status: status - goal: - vessel: vessel - goal_default: - vessel: - 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: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: HeatChillStop_Feedback - type: object - goal: - properties: - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - title: HeatChillStop_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: HeatChillStop_Result - type: object - required: - - goal - title: HeatChillStop - type: object - type: HeatChillStop - module: unilabos.devices.virtual.virtual_heatchill:VirtualHeatChill - status_types: - is_stirring: bool - max_stir_speed: float - max_temp: float - min_temp: float - operation_mode: str - progress: float - remaining_time: float - status: str - stir_speed: float - type: python - config_info: [] - description: Virtual HeatChill for HeatChillProtocol Testing - handles: - - data_key: vessel - data_source: handle - data_type: mechanical - description: 加热/冷却器的物理连接口 - handler_key: heatchill - io_type: source - label: heatchill - side: NORTH - icon: Heater.webp - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - is_stirring: - type: boolean - max_stir_speed: - type: number - max_temp: - type: number - min_temp: - type: number - operation_mode: - type: string - progress: - type: number - remaining_time: - type: number - status: - type: string - stir_speed: - type: number - required: - - status - - operation_mode - - is_stirring - - stir_speed - - remaining_time - - progress - - max_temp - - min_temp - - max_stir_speed - type: object - version: 1.0.0 -virtual_multiway_valve: - category: - - virtual_device - class: - action_value_mappings: - auto-close: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: close的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: close参数 - type: object - type: UniLabJsonCommand - auto-is_at_port: - feedback: {} - goal: {} - goal_default: - port_number: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_at_port的参数schema - properties: - feedback: {} - goal: - properties: - port_number: - type: integer - required: - - port_number - type: object - result: {} - required: - - goal - title: is_at_port参数 - type: object - type: UniLabJsonCommand - auto-is_at_position: - feedback: {} - goal: {} - goal_default: - position: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_at_position的参数schema - properties: - feedback: {} - goal: - properties: - position: - type: integer - required: - - position - type: object - result: {} - required: - - goal - title: is_at_position参数 - type: object - type: UniLabJsonCommand - auto-is_at_pump_position: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_at_pump_position的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_at_pump_position参数 - type: object - type: UniLabJsonCommand - auto-open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: open参数 - type: object - type: UniLabJsonCommand - auto-reset: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: reset的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: reset参数 - type: object - type: UniLabJsonCommand - auto-set_to_port: - feedback: {} - goal: {} - goal_default: - port_number: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: set_to_port的参数schema - properties: - feedback: {} - goal: - properties: - port_number: - type: integer - required: - - port_number - type: object - result: {} - required: - - goal - title: set_to_port参数 - type: object - type: UniLabJsonCommand - auto-set_to_pump_position: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: set_to_pump_position的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: set_to_pump_position参数 - type: object - type: UniLabJsonCommand - auto-switch_between_pump_and_port: - feedback: {} - goal: {} - goal_default: - port_number: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: switch_between_pump_and_port的参数schema - properties: - feedback: {} - goal: - properties: - port_number: - type: integer - required: - - port_number - type: object - result: {} - required: - - goal - title: switch_between_pump_and_port参数 - type: object - type: UniLabJsonCommand - set_position: - feedback: {} - goal: - command: command - goal_default: - command: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback - type: object - goal: - properties: - command: - type: string - required: - - command - title: SendCmd_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: SendCmd_Result - type: object - required: - - goal - title: SendCmd - type: object - type: SendCmd - set_valve_position: - feedback: {} - goal: - command: command - goal_default: - command: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback - type: object - goal: - properties: - command: - type: string - required: - - command - title: SendCmd_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: SendCmd_Result - type: object - required: - - goal - title: SendCmd - type: object - type: SendCmd - module: unilabos.devices.virtual.virtual_multiway_valve:VirtualMultiwayValve - status_types: - current_port: str - current_position: int - flow_path: str - status: str - target_position: int - valve_position: int - valve_state: str - type: python - config_info: [] - description: Virtual 8-Way Valve for flow direction control - handles: - - data_key: fluid_in - data_source: handle - data_type: fluid - description: 八通阀门进液口 - handler_key: transferpump - io_type: target - label: transferpump - side: NORTH - - data_key: fluid_port_1 - data_source: executor - data_type: fluid - description: 八通阀门端口1 - handler_key: '1' - io_type: source - label: '1' - side: NORTH - - data_key: fluid_port_2 - data_source: executor - data_type: fluid - description: 八通阀门端口2 - handler_key: '2' - io_type: source - label: '2' - side: EAST - - data_key: fluid_port_3 - data_source: executor - data_type: fluid - description: 八通阀门端口3 - handler_key: '3' - io_type: source - label: '3' - side: EAST - - data_key: fluid_port_4 - data_source: executor - data_type: fluid - description: 八通阀门端口4 - handler_key: '4' - io_type: source - label: '4' - side: SOUTH - - data_key: fluid_port_5 - data_source: executor - data_type: fluid - description: 八通阀门端口5 - handler_key: '5' - io_type: source - label: '5' - side: SOUTH - - data_key: fluid_port_6 - data_source: executor - data_type: fluid - description: 八通阀门端口6 - handler_key: '6' - io_type: source - label: '6' - side: WEST - - data_key: fluid_port_7 - data_source: executor - data_type: fluid - description: 八通阀门端口7 - handler_key: '7' - io_type: source - label: '7' - side: WEST - - data_key: fluid_port_8 - data_source: executor - data_type: fluid - description: 八通阀门端口8-特殊输入 - handler_key: '8' - io_type: target - label: '8' - side: WEST - - data_key: fluid_port_8 - data_source: executor - data_type: fluid - description: 八通阀门端口8 - handler_key: '8' - io_type: source - label: '8' - side: NORTH - icon: EightPipeline.webp - init_param_schema: - config: - properties: - port: - default: VIRTUAL - type: string - positions: - default: 8 - type: integer - required: [] - type: object - data: - properties: - current_port: - type: string - current_position: - type: integer - flow_path: - type: string - status: - type: string - target_position: - type: integer - valve_position: - type: integer - valve_state: - type: string - required: - - status - - valve_state - - current_position - - target_position - - current_port - - valve_position - - flow_path - type: object - version: 1.0.0 -virtual_rotavap: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - evaporate: - feedback: - current_device: current_device - status: status - goal: - pressure: pressure - stir_speed: stir_speed - temp: temp - time: time - vessel: vessel - goal_default: - pressure: 0.0 - solvent: '' - stir_speed: 0.0 - temp: 0.0 - time: '' - vessel: - 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: '' - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_device: - type: string - status: - type: string - time_remaining: - properties: - nanosec: - maximum: 4294967295 - minimum: 0 - type: integer - sec: - maximum: 2147483647 - minimum: -2147483648 - type: integer - required: - - sec - - nanosec - title: time_remaining - type: object - time_spent: - properties: - nanosec: - maximum: 4294967295 - minimum: 0 - type: integer - sec: - maximum: 2147483647 - minimum: -2147483648 - type: integer - required: - - sec - - nanosec - title: time_spent - type: object - required: - - status - - current_device - - time_spent - - time_remaining - title: Evaporate_Feedback - type: object - goal: - properties: - pressure: - type: number - solvent: - type: string - stir_speed: - type: number - temp: - type: number - time: - type: string - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - pressure - - temp - - time - - stir_speed - - solvent - title: Evaporate_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: Evaporate_Result - type: object - required: - - goal - title: Evaporate - type: object - type: Evaporate - module: unilabos.devices.virtual.virtual_rotavap:VirtualRotavap - status_types: - current_temp: float - evaporated_volume: float - max_rotation_speed: float - max_temp: float - message: str - progress: float - remaining_time: float - rotation_speed: float - rotavap_state: str - status: str - vacuum_pressure: float - type: python - config_info: [] - description: Virtual Rotary Evaporator for EvaporateProtocol Testing - handles: - - data_key: vessel_in - data_source: handle - data_type: fluid - description: 样品连接口 - handler_key: samplein - io_type: target - label: sample_in - side: NORTH - - data_key: product_out - data_source: handle - data_type: fluid - description: 浓缩产物出口 - handler_key: productout - io_type: source - label: product_out - side: SOUTH - - data_key: solvent_out - data_source: handle - data_type: fluid - description: 冷凝溶剂出口 - handler_key: solventout - io_type: source - label: solvent_out - side: EAST - icon: Rotaryevaporator.webp - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - current_temp: - type: number - evaporated_volume: - type: number - max_rotation_speed: - type: number - max_temp: - type: number - message: - type: string - progress: - type: number - remaining_time: - type: number - rotation_speed: - type: number - rotavap_state: - type: string - status: - type: string - vacuum_pressure: - type: number - required: - - status - - rotavap_state - - current_temp - - rotation_speed - - vacuum_pressure - - evaporated_volume - - progress - - message - - max_temp - - max_rotation_speed - - remaining_time - type: object - version: 1.0.0 -virtual_separator: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - separate: - feedback: - current_status: status - progress: progress - goal: - from_vessel: from_vessel - product_phase: product_phase - purpose: purpose - repeats: repeats - separation_vessel: separation_vessel - settling_time: settling_time - solvent: solvent - solvent_volume: solvent_volume - stir_speed: stir_speed - stir_time: stir_time - through: through - to_vessel: to_vessel - waste_phase_to_vessel: waste_phase_to_vessel - goal_default: - from_vessel: - 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: '' - product_phase: '' - product_vessel: - 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: '' - purpose: '' - repeats: 0 - separation_vessel: - 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: '' - settling_time: 0.0 - solvent: '' - solvent_volume: '' - stir_speed: 0.0 - stir_time: 0.0 - through: '' - to_vessel: - 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: '' - vessel: - 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: '' - volume: '' - waste_phase_to_vessel: - 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: '' - waste_vessel: - 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: '' - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - progress: - type: number - status: - type: string - required: - - status - - progress - title: Separate_Feedback - type: object - goal: - properties: - from_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: from_vessel - type: object - product_phase: - type: string - product_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: product_vessel - type: object - purpose: - type: string - repeats: - maximum: 2147483647 - minimum: -2147483648 - type: integer - separation_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: separation_vessel - type: object - settling_time: - type: number - solvent: - type: string - solvent_volume: - type: string - stir_speed: - type: number - stir_time: - type: number - through: - type: string - to_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: to_vessel - type: object - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - volume: - type: string - waste_phase_to_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: waste_phase_to_vessel - type: object - waste_vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: waste_vessel - type: object - required: - - vessel - - purpose - - product_phase - - from_vessel - - separation_vessel - - to_vessel - - waste_phase_to_vessel - - product_vessel - - waste_vessel - - solvent - - solvent_volume - - volume - - through - - repeats - - stir_time - - stir_speed - - settling_time - title: Separate_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Separate_Result - type: object - required: - - goal - title: Separate - type: object - type: Separate - module: unilabos.devices.virtual.virtual_separator:VirtualSeparator - status_types: - has_phases: bool - message: str - phase_separation: bool - progress: float - separator_state: str - settling_time: float - status: str - stir_speed: float - volume: float - type: python - config_info: [] - description: Virtual Separator for SeparateProtocol Testing - handles: - - data_key: from_vessel - data_source: handle - data_type: fluid - description: 需要分离的混合液体输入口 - handler_key: separatorin - io_type: target - label: separator_in - side: NORTH - - data_key: bottom_outlet - data_source: executor - data_type: fluid - description: 下相(重相)液体输出口 - handler_key: bottomphaseout - io_type: source - label: bottom_phase_out - side: SOUTH - - data_key: mechanical_port - data_source: handle - data_type: mechanical - description: 用于连接搅拌器等机械设备的接口 - handler_key: bind - io_type: target - label: bind - side: WEST - icon: Separator.webp - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - has_phases: - type: boolean - message: - type: string - phase_separation: - type: boolean - progress: - type: number - separator_state: - type: string - settling_time: - type: number - status: - type: string - stir_speed: - type: number - volume: - type: number - required: - - status - - separator_state - - volume - - has_phases - - phase_separation - - stir_speed - - settling_time - - progress - - message - type: object - version: 1.0.0 -virtual_solenoid_valve: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-is_closed: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_closed的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_closed参数 - type: object - type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - auto-reset: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: reset的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: reset参数 - type: object - type: UniLabJsonCommandAsync - auto-toggle: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: toggle的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: toggle参数 - type: object - type: UniLabJsonCommand - close: - feedback: {} - goal: - command: CLOSED - goal_default: {} - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - set_status: - feedback: {} - goal: - string: string - goal_default: - string: '' - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: StrSingleInput_Feedback - type: object - goal: - properties: - string: - type: string - required: - - string - title: StrSingleInput_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: StrSingleInput_Result - type: object - required: - - goal - title: StrSingleInput - type: object - type: StrSingleInput - set_valve_position: - feedback: {} - goal: - command: command - goal_default: - command: '' - handles: {} - result: - message: message - success: success - valve_position: valve_position - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback - type: object - goal: - properties: - command: - type: string - required: - - command - title: SendCmd_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: SendCmd_Result - type: object - required: - - goal - title: SendCmd - type: object - type: SendCmd - module: unilabos.devices.virtual.virtual_solenoid_valve:VirtualSolenoidValve - status_types: - is_open: bool - status: str - valve_position: str - valve_state: str - type: python - config_info: [] - description: Virtual Solenoid Valve for simple on/off flow control - handles: - - data_key: fluid_port_in - data_source: handle - data_type: fluid - description: 电磁阀的进液口 - handler_key: in - io_type: target - label: in - side: NORTH - - data_key: fluid_port_out - data_source: handle - data_type: fluid - description: 电磁阀的出液口 - handler_key: out - io_type: source - label: out - side: SOUTH - icon: '' - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - is_open: - type: boolean - status: - type: string - valve_position: - type: string - valve_state: - type: string - required: - - status - - valve_state - - is_open - - valve_position - type: object - version: 1.0.0 -virtual_solid_dispenser: - category: - - virtual_device - class: - action_value_mappings: - add_solid: - feedback: - current_status: status - progress: progress - goal: - equiv: equiv - event: event - mass: mass - mol: mol - purpose: purpose - rate_spec: rate_spec - ratio: ratio - reagent: reagent - vessel: vessel - goal_default: - amount: '' - equiv: '' - event: '' - mass: '' - mol: '' - purpose: '' - rate_spec: '' - ratio: '' - reagent: '' - stir: false - stir_speed: 0.0 - time: '' - vessel: - 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: '' - viscous: false - volume: '' - handles: {} - result: - message: message - return_info: return_info - success: success - schema: - description: '' - properties: - feedback: - properties: - current_status: - type: string - progress: - type: number - required: - - progress - - current_status - title: Add_Feedback - type: object - goal: - properties: - amount: - type: string - equiv: - type: string - event: - type: string - mass: - type: string - mol: - type: string - purpose: - type: string - rate_spec: - type: string - ratio: - type: string - reagent: - type: string - stir: - type: boolean - stir_speed: - type: number - time: - type: string - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - viscous: - type: boolean - volume: - type: string - required: - - vessel - - reagent - - volume - - mass - - amount - - time - - stir - - stir_speed - - viscous - - purpose - - event - - mol - - rate_spec - - equiv - - ratio - title: Add_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Add_Result - type: object - required: - - goal - title: Add - type: object - type: Add - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-find_solid_reagent_bottle: - feedback: {} - goal: {} - goal_default: - reagent_name: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - reagent_name: - type: string - required: - - reagent_name - type: object - result: {} - required: - - goal - title: find_solid_reagent_bottle参数 - type: object - type: UniLabJsonCommand - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-parse_mass_string: - feedback: {} - goal: {} - goal_default: - mass_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - mass_str: - type: string - required: - - mass_str - type: object - result: {} - required: - - goal - title: parse_mass_string参数 - type: object - type: UniLabJsonCommand - auto-parse_mol_string: - feedback: {} - goal: {} - goal_default: - mol_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - mol_str: - type: string - required: - - mol_str - type: object - result: {} - required: - - goal - title: parse_mol_string参数 - type: object - type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser - status_types: - current_reagent: str - dispensed_amount: float - status: str - total_operations: int - type: python - config_info: [] - description: Virtual Solid Dispenser for Add Protocol Testing - supports mass and - molar additions - handles: - - data_key: solid_out - data_source: executor - data_type: resource - description: 固体试剂输出口 - handler_key: SolidOut - io_type: source - label: SolidOut - side: SOUTH - - data_key: solid_in - data_source: handle - data_type: resource - description: 固体试剂输入口(连接试剂瓶) - handler_key: SolidIn - io_type: target - label: SolidIn - side: NORTH - icon: '' - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - current_reagent: - type: string - dispensed_amount: - type: number - status: - type: string - total_operations: - type: integer - required: - - status - - current_reagent - - dispensed_amount - - total_operations - type: object - version: 1.0.0 -virtual_stirrer: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - start_stir: - feedback: - status: status - goal: - purpose: purpose - stir_speed: stir_speed - vessel: vessel - goal_default: - purpose: '' - stir_speed: 0.0 - vessel: - 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: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - current_speed: - type: number - current_status: - type: string - progress: - type: number - required: - - progress - - current_speed - - current_status - title: StartStir_Feedback - type: object - goal: - properties: - purpose: - type: string - stir_speed: - type: number - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - stir_speed - - purpose - title: StartStir_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: StartStir_Result - type: object - required: - - goal - title: StartStir - type: object - type: StartStir - stir: - feedback: - status: status - goal: - settling_time: settling_time - stir_speed: stir_speed - stir_time: stir_time - goal_default: - event: '' - settling_time: '' - stir_speed: 0.0 - stir_time: 0.0 - time: '' - time_spec: '' - vessel: - 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: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: Stir_Feedback - type: object - goal: - properties: - event: - type: string - settling_time: - type: string - stir_speed: - type: number - stir_time: - type: number - time: - type: string - time_spec: - type: string - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - - time - - event - - time_spec - - stir_time - - stir_speed - - settling_time - title: Stir_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Stir_Result - type: object - required: - - goal - title: Stir - type: object - type: Stir - stop_stir: - feedback: - status: status - goal: - vessel: vessel - goal_default: - vessel: - 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: '' - handles: {} - result: - success: success - schema: - description: '' - properties: - feedback: - properties: - current_status: - type: string - progress: - type: number - required: - - progress - - current_status - title: StopStir_Feedback - type: object - goal: - properties: - vessel: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: vessel - type: object - required: - - vessel - title: StopStir_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: StopStir_Result - type: object - required: - - goal - title: StopStir - type: object - type: StopStir - module: unilabos.devices.virtual.virtual_stirrer:VirtualStirrer - status_types: - current_speed: float - current_vessel: str - device_info: dict - is_stirring: bool - max_speed: float - min_speed: float - operation_mode: str - remaining_time: float - status: str - type: python - config_info: [] - description: Virtual Stirrer for StirProtocol Testing - handles: - - data_key: vessel - data_source: handle - data_type: mechanical - description: 搅拌器的机械连接口 - handler_key: stirrer - io_type: source - label: stirrer - side: NORTH - icon: Stirrer.webp - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - current_speed: - type: number - current_vessel: - type: string - device_info: - type: object - is_stirring: - type: boolean - max_speed: - type: number - min_speed: - type: number - operation_mode: - type: string - remaining_time: - type: number - status: - type: string - required: - - status - - operation_mode - - current_vessel - - current_speed - - is_stirring - - remaining_time - - max_speed - - min_speed - - device_info - type: object - version: 1.0.0 -virtual_transfer_pump: - category: - - virtual_device - class: - action_value_mappings: - auto-aspirate: - feedback: {} - goal: {} - goal_default: - velocity: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: aspirate的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - volume: - type: number - required: - - volume - type: object - result: {} - required: - - goal - title: aspirate参数 - type: object - type: UniLabJsonCommandAsync - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-dispense: - feedback: {} - goal: {} - goal_default: - velocity: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: dispense的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - volume: - type: number - required: - - volume - type: object - result: {} - required: - - goal - title: dispense参数 - type: object - type: UniLabJsonCommandAsync - auto-empty_syringe: - feedback: {} - goal: {} - goal_default: - velocity: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: empty_syringe的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - required: [] - type: object - result: {} - required: - - goal - title: empty_syringe参数 - type: object - type: UniLabJsonCommandAsync - auto-fill_syringe: - feedback: {} - goal: {} - goal_default: - velocity: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: fill_syringe的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - required: [] - type: object - result: {} - required: - - goal - title: fill_syringe参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-is_empty: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_empty的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_empty参数 - type: object - type: UniLabJsonCommand - auto-is_full: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_full的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_full参数 - type: object - type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - auto-pull_plunger: - feedback: {} - goal: {} - goal_default: - velocity: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: pull_plunger的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - volume: - type: number - required: - - volume - type: object - result: {} - required: - - goal - title: pull_plunger参数 - type: object - type: UniLabJsonCommandAsync - auto-push_plunger: - feedback: {} - goal: {} - goal_default: - velocity: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: push_plunger的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - volume: - type: number - required: - - volume - type: object - result: {} - required: - - goal - title: push_plunger参数 - type: object - type: UniLabJsonCommandAsync - auto-set_max_velocity: - feedback: {} - goal: {} - goal_default: - velocity: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: set_max_velocity的参数schema - properties: - feedback: {} - goal: - properties: - velocity: - type: number - required: - - velocity - type: object - result: {} - required: - - goal - title: set_max_velocity参数 - type: object - type: UniLabJsonCommand - auto-stop_operation: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: stop_operation的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: stop_operation参数 - type: object - type: UniLabJsonCommandAsync - set_position: - feedback: - current_position: current_position - progress: progress - status: status - goal: - max_velocity: max_velocity - position: position - goal_default: - max_velocity: 0.0 - position: 0.0 - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_position: - type: number - progress: - type: number - status: - type: string - required: - - status - - current_position - - progress - title: SetPumpPosition_Feedback - type: object - goal: - properties: - max_velocity: - type: number - position: - type: number - required: - - position - - max_velocity - title: SetPumpPosition_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - - message - title: SetPumpPosition_Result - type: object - required: - - goal - title: SetPumpPosition - type: object - type: SetPumpPosition - transfer: - feedback: - current_status: current_status - progress: progress - transferred_volume: transferred_volume - goal: - amount: amount - from_vessel: from_vessel - rinsing_repeats: rinsing_repeats - rinsing_solvent: rinsing_solvent - rinsing_volume: rinsing_volume - solid: solid - time: time - to_vessel: to_vessel - viscous: viscous - volume: volume - goal_default: - amount: '' - from_vessel: '' - rinsing_repeats: 0 - rinsing_solvent: '' - rinsing_volume: 0.0 - solid: false - time: 0.0 - to_vessel: '' - viscous: false - volume: 0.0 - handles: {} - result: - message: message - success: success - schema: - description: '' - properties: - feedback: - properties: - current_status: - type: string - progress: - type: number - transferred_volume: - type: number - required: - - progress - - transferred_volume - - current_status - title: Transfer_Feedback - type: object - goal: - properties: - amount: - type: string - from_vessel: - type: string - rinsing_repeats: - maximum: 2147483647 - minimum: -2147483648 - type: integer - rinsing_solvent: - type: string - rinsing_volume: - type: number - solid: - type: boolean - time: - type: number - to_vessel: - type: string - viscous: - type: boolean - volume: - type: number - required: - - from_vessel - - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid - title: Transfer_Goal - type: object - result: - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Transfer_Result - type: object - required: - - goal - title: Transfer - type: object - type: Transfer - module: unilabos.devices.virtual.virtual_transferpump:VirtualTransferPump - status_types: - current_volume: float - max_velocity: float - position: float - remaining_capacity: float - status: str - transfer_rate: float - type: python - config_info: [] - description: Virtual Transfer Pump for TransferProtocol Testing (Syringe-style) - handles: - - data_key: fluid_port - data_source: handle - data_type: fluid - description: 注射器式转移泵的连接口 - handler_key: transferpump - io_type: source - label: transferpump - side: SOUTH - icon: Pump.webp - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - required: [] - type: object - data: - properties: - current_volume: - type: number - max_velocity: - type: number - position: - type: number - remaining_capacity: - type: number - status: - type: string - transfer_rate: - type: number - required: - - status - - position - - current_volume - - max_velocity - - transfer_rate - - remaining_capacity - type: object - version: 1.0.0 -virtual_vacuum_pump: - category: - - virtual_device - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: cleanup的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: cleanup参数 - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: initialize的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: initialize参数 - type: object - type: UniLabJsonCommandAsync - auto-is_closed: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_closed的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_closed参数 - type: object - type: UniLabJsonCommand - auto-is_open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: is_open的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: is_open参数 - type: object - type: UniLabJsonCommand - close: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - open: - feedback: {} - goal: {} - goal_default: {} - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: EmptyIn_Feedback - type: object - goal: - properties: {} - required: [] - title: EmptyIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: EmptyIn_Result - type: object - required: - - goal - title: EmptyIn - type: object - type: EmptyIn - set_status: - feedback: {} - goal: - string: string - goal_default: - string: '' - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: StrSingleInput_Feedback - type: object - goal: - properties: - string: - type: string - required: - - string - title: StrSingleInput_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: StrSingleInput_Result - type: object - required: - - goal - title: StrSingleInput - type: object - type: StrSingleInput - module: unilabos.devices.virtual.virtual_vacuum_pump:VirtualVacuumPump - status_types: - status: str - type: python - config_info: [] - description: Virtual vacuum pump - handles: - - data_key: fluid_in - data_source: handle - data_type: fluid - description: 真空泵进气口 - handler_key: vacuumpump - io_type: source - label: vacuumpump - side: SOUTH - icon: Vacuum.webp - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - status: - type: string - required: - - status - type: object - version: 1.0.0 diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index f09b79c..e486d6c 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -71,6 +71,20 @@ class Registry: from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type + # 获取 HostNode 类的增强信息,用于自动生成 action schema + host_node_enhanced_info = get_enhanced_class_info( + "unilabos.ros.nodes.presets.host_node:HostNode", use_dynamic=True + ) + + # 为 test_latency 生成 schema,保留原有 description + test_latency_method_info = host_node_enhanced_info.get("action_methods", {}).get("test_latency", {}) + test_latency_schema = self._generate_unilab_json_command_schema( + test_latency_method_info.get("args", []), + "test_latency", + test_latency_method_info.get("return_annotation"), + ) + test_latency_schema["description"] = "用于测试延迟的动作,返回延迟时间和时间差。" + self.device_type_registry.update( { "host_node": { @@ -152,14 +166,19 @@ class Registry: }, }, "test_latency": { - "type": self.EmptyIn, + "type": ( + "UniLabJsonCommandAsync" + if test_latency_method_info.get("is_async", False) + else "UniLabJsonCommand" + ), "goal": {}, "feedback": {}, "result": {}, - "schema": ros_action_to_json_schema( - self.EmptyIn, "用于测试延迟的动作,返回延迟时间和时间差。" - ), - "goal_default": {}, + "schema": test_latency_schema, + "goal_default": { + arg["name"]: arg["default"] + for arg in test_latency_method_info.get("args", []) + }, "handles": {}, }, "auto-test_resource": { @@ -540,11 +559,9 @@ class Registry: return final_schema - def _preserve_field_descriptions( - self, new_schema: Dict[str, Any], previous_schema: Dict[str, Any] - ) -> None: + def _preserve_field_descriptions(self, new_schema: Dict[str, Any], previous_schema: Dict[str, Any]) -> None: """ - 保留之前 schema 中 goal/feedback/result 下一级字段的 description + 保留之前 schema 中 goal/feedback/result 下一级字段的 description 和 title Args: new_schema: 新生成的 schema(会被修改) @@ -566,6 +583,9 @@ class Registry: # 保留字段的 description if "description" in prev_field and prev_field["description"]: field_schema["description"] = prev_field["description"] + # 保留字段的 title(用户自定义的中文名) + if "title" in prev_field and prev_field["title"]: + field_schema["title"] = prev_field["title"] def _is_typed_dict(self, annotation: Any) -> bool: """ diff --git a/unilabos/ros/msgs/message_converter.py b/unilabos/ros/msgs/message_converter.py index 632d5e1..b526d5f 100644 --- a/unilabos/ros/msgs/message_converter.py +++ b/unilabos/ros/msgs/message_converter.py @@ -770,13 +770,16 @@ def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any return schema -def ros_action_to_json_schema(action_class: Any, description="") -> Dict[str, Any]: +def ros_action_to_json_schema( + action_class: Any, description="", previous_schema: Optional[Dict[str, Any]] = None +) -> Dict[str, Any]: """ 将 ROS Action 类转换为 JSON Schema Args: action_class: ROS Action 类 description: 描述 + previous_schema: 之前的 schema,用于保留 goal/feedback/result 下一级字段的 description Returns: 完整的 JSON Schema 定义 @@ -810,9 +813,44 @@ def ros_action_to_json_schema(action_class: Any, description="") -> Dict[str, An "required": ["goal"], } + # 保留之前 schema 中 goal/feedback/result 下一级字段的 description + if previous_schema: + _preserve_field_descriptions(schema, previous_schema) + return schema +def _preserve_field_descriptions( + new_schema: Dict[str, Any], previous_schema: Dict[str, Any] +) -> None: + """ + 保留之前 schema 中 goal/feedback/result 下一级字段的 description 和 title + + Args: + new_schema: 新生成的 schema(会被修改) + previous_schema: 之前的 schema + """ + for section in ["goal", "feedback", "result"]: + new_section = new_schema.get("properties", {}).get(section, {}) + prev_section = previous_schema.get("properties", {}).get(section, {}) + + if not new_section or not prev_section: + continue + + new_props = new_section.get("properties", {}) + prev_props = prev_section.get("properties", {}) + + for field_name, field_schema in new_props.items(): + if field_name in prev_props: + prev_field = prev_props[field_name] + # 保留字段的 description + if "description" in prev_field and prev_field["description"]: + field_schema["description"] = prev_field["description"] + # 保留字段的 title(用户自定义的中文名) + if "title" in prev_field and prev_field["title"]: + field_schema["title"] = prev_field["title"] + + def convert_ros_action_to_jsonschema( action_name_or_type: Union[str, Type], output_file: Optional[str] = None, format: str = "json" ) -> Dict[str, Any]: diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 101476a..9a27e04 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -5,7 +5,8 @@ import threading import time import traceback import uuid -from typing import TYPE_CHECKING, Optional, Dict, Any, List, ClassVar, Set, TypedDict, Union +from typing import TYPE_CHECKING, Optional, Dict, Any, List, ClassVar, Set, Union +from typing_extensions import TypedDict from action_msgs.msg import GoalStatus from geometry_msgs.msg import Point @@ -62,6 +63,18 @@ class TestResourceReturn(TypedDict): devices: List[DeviceSlot] +class TestLatencyReturn(TypedDict): + """test_latency方法的返回值类型""" + + avg_rtt_ms: float + avg_time_diff_ms: float + max_time_error_ms: float + task_delay_ms: float + raw_delay_ms: float + test_count: int + status: str + + class HostNode(BaseROS2DeviceNode): """ 主机节点类,负责管理设备、资源和控制器 @@ -853,8 +866,13 @@ class HostNode(BaseROS2DeviceNode): # 适配后端的一些额外处理 return_value = return_info.get("return_value") if isinstance(return_value, dict): - unilabos_samples = return_info.get("unilabos_samples") - if isinstance(unilabos_samples, list): + unilabos_samples = return_value.pop("unilabos_samples", None) + if isinstance(unilabos_samples, list) and unilabos_samples: + self.lab_logger().info( + f"[Host Node] Job {job_id[:8]} returned {len(unilabos_samples)} sample(s): " + f"{[s.get('name', s.get('id', 'unknown')) if isinstance(s, dict) else str(s)[:20] for s in unilabos_samples[:5]]}" + f"{'...' if len(unilabos_samples) > 5 else ''}" + ) return_info["unilabos_samples"] = unilabos_samples suc = return_info.get("suc", False) if not suc: @@ -881,7 +899,7 @@ class HostNode(BaseROS2DeviceNode): # 清理 _goals 中的记录 if job_id in self._goals: del self._goals[job_id] - self.lab_logger().debug(f"[Host Node] Removed goal {job_id[:8]} from _goals") + self.lab_logger().trace(f"[Host Node] Removed goal {job_id[:8]} from _goals") # 存储结果供 HTTP API 查询 try: @@ -1326,10 +1344,20 @@ class HostNode(BaseROS2DeviceNode): self.lab_logger().debug(f"[Host Node-Resource] List parameters: {request}") return response - def test_latency(self): + def test_latency(self) -> TestLatencyReturn: """ 测试网络延迟的action实现 通过5次ping-pong机制校对时间误差并计算实际延迟 + + Returns: + TestLatencyReturn: 包含延迟测试结果的字典,包括: + - avg_rtt_ms: 平均往返时间(毫秒) + - avg_time_diff_ms: 平均时间差(毫秒) + - max_time_error_ms: 最大时间误差(毫秒) + - task_delay_ms: 实际任务延迟(毫秒),-1表示无法计算 + - raw_delay_ms: 原始时间差(毫秒),-1表示无法计算 + - test_count: 有效测试次数 + - status: 测试状态,"success"表示成功,"all_timeout"表示全部超时 """ import uuid as uuid_module @@ -1392,7 +1420,15 @@ class HostNode(BaseROS2DeviceNode): if not ping_results: self.lab_logger().error("❌ 所有ping-pong测试都失败了") - return {"status": "all_timeout"} + return { + "avg_rtt_ms": -1.0, + "avg_time_diff_ms": -1.0, + "max_time_error_ms": -1.0, + "task_delay_ms": -1.0, + "raw_delay_ms": -1.0, + "test_count": 0, + "status": "all_timeout", + } # 统计分析 rtts = [r["rtt_ms"] for r in ping_results] @@ -1400,7 +1436,7 @@ class HostNode(BaseROS2DeviceNode): avg_rtt_ms = sum(rtts) / len(rtts) avg_time_diff_ms = sum(time_diffs) / len(time_diffs) - max_time_diff_error_ms = max(abs(min(time_diffs)), abs(max(time_diffs))) + max_time_diff_error_ms: float = max(abs(min(time_diffs)), abs(max(time_diffs))) self.lab_logger().info("-" * 50) self.lab_logger().info("[测试统计]") @@ -1440,7 +1476,7 @@ class HostNode(BaseROS2DeviceNode): self.lab_logger().info("=" * 60) - return { + res: TestLatencyReturn = { "avg_rtt_ms": avg_rtt_ms, "avg_time_diff_ms": avg_time_diff_ms, "max_time_error_ms": max_time_diff_error_ms, @@ -1451,9 +1487,14 @@ class HostNode(BaseROS2DeviceNode): "test_count": len(ping_results), "status": "success", } + return res def test_resource( - self, resource: ResourceSlot = None, resources: List[ResourceSlot] = None, device: DeviceSlot = None, devices: List[DeviceSlot] = None + self, + resource: ResourceSlot = None, + resources: List[ResourceSlot] = None, + device: DeviceSlot = None, + devices: List[DeviceSlot] = None, ) -> TestResourceReturn: if resources is None: resources = [] @@ -1514,7 +1555,9 @@ class HostNode(BaseROS2DeviceNode): # 构建服务地址 srv_address = f"/srv{namespace}/s2c_resource_tree" - self.lab_logger().trace(f"[Host Node-Resource] Host -> {device_id} ResourceTree {action} operation started -------") + self.lab_logger().trace( + f"[Host Node-Resource] Host -> {device_id} ResourceTree {action} operation started -------" + ) # 创建服务客户端 sclient = self.create_client(SerialCommand, srv_address) @@ -1549,7 +1592,9 @@ class HostNode(BaseROS2DeviceNode): time.sleep(0.05) response = future.result() - self.lab_logger().trace(f"[Host Node-Resource] Host -> {device_id} ResourceTree {action} operation completed -------") + self.lab_logger().trace( + f"[Host Node-Resource] Host -> {device_id} ResourceTree {action} operation completed -------" + ) return True except Exception as e: diff --git a/unilabos/test/experiments/virtual_bench.json b/unilabos/test/experiments/virtual_bench.json new file mode 100644 index 0000000..d37fa6e --- /dev/null +++ b/unilabos/test/experiments/virtual_bench.json @@ -0,0 +1,28 @@ +{ + "nodes": [ + { + "id": "workbench_1", + "name": "虚拟工作台", + "children": [], + "parent": null, + "type": "device", + "class": "virtual_workbench", + "position": { + "x": 400, + "y": 300, + "z": 0 + }, + "config": { + "arm_operation_time": 3.0, + "heating_time": 10.0, + "num_heating_stations": 3 + }, + "data": { + "status": "Ready", + "arm_state": "idle", + "message": "工作台就绪" + } + } + ], + "links": [] +} diff --git a/unilabos/utils/decorator.py b/unilabos/utils/decorator.py index 667f353..57e968a 100644 --- a/unilabos/utils/decorator.py +++ b/unilabos/utils/decorator.py @@ -182,3 +182,49 @@ def get_all_subscriptions(instance) -> list: except Exception: pass return subscriptions + + +def not_action(func: F) -> F: + """ + 标记方法为非动作的装饰器 + + 用于装饰 driver 类中的方法,使其在 complete_registry 时不被识别为动作。 + 适用于辅助方法、内部工具方法等不应暴露为设备动作的公共方法。 + + Example: + class MyDriver: + @not_action + def helper_method(self): + # 这个方法不会被注册为动作 + pass + + def actual_action(self, param: str): + # 这个方法会被注册为动作 + self.helper_method() + + Note: + - 可以与其他装饰器组合使用,@not_action 应放在最外层 + - 仅影响 complete_registry 的动作识别,不影响方法的正常调用 + """ + + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + # 在函数上附加标记 + wrapper._is_not_action = True # type: ignore[attr-defined] + + return wrapper # type: ignore[return-value] + + +def is_not_action(func) -> bool: + """ + 检查函数是否被标记为非动作 + + Args: + func: 被检查的函数 + + Returns: + 如果函数被 @not_action 装饰则返回 True,否则返回 False + """ + return getattr(func, "_is_not_action", False) diff --git a/unilabos/utils/import_manager.py b/unilabos/utils/import_manager.py index 00fcd06..2df7636 100644 --- a/unilabos/utils/import_manager.py +++ b/unilabos/utils/import_manager.py @@ -28,6 +28,7 @@ __all__ = [ from ast import Constant from unilabos.utils import logger +from unilabos.utils.decorator import is_not_action class ImportManager: @@ -275,6 +276,9 @@ class ImportManager: method_info = self._analyze_method_signature(method) result["status_methods"][actual_name] = method_info elif not name.startswith("_"): + # 检查是否被 @not_action 装饰器标记 + if is_not_action(method): + continue # 其他非_开头的方法归类为action method_info = self._analyze_method_signature(method) result["action_methods"][name] = method_info @@ -330,6 +334,9 @@ class ImportManager: if actual_name not in result["status_methods"]: result["status_methods"][actual_name] = method_info else: + # 检查是否被 @not_action 装饰器标记 + if self._is_not_action_method(node): + continue # 其他非_开头的方法归类为action result["action_methods"][method_name] = method_info return result @@ -450,6 +457,13 @@ class ImportManager: return True return False + def _is_not_action_method(self, node: ast.FunctionDef) -> bool: + """检查是否是@not_action装饰的方法""" + for decorator in node.decorator_list: + if isinstance(decorator, ast.Name) and decorator.id == "not_action": + return True + return False + def _get_property_name_from_setter(self, node: ast.FunctionDef) -> str: """从setter装饰器中获取属性名""" for decorator in node.decorator_list: From 37b1fca962fd4bb2830f6cbfee0ac34f3fc1a694 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 26 Jan 2026 02:22:21 +0800 Subject: [PATCH 02/34] CI Check Fix 1 --- .github/workflows/ci-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index fa5ada7..f11d484 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -30,7 +30,7 @@ jobs: - name: Install minimal ROS dependencies run: | - conda install ros-humble-ros-core ros-humble-std-msgs ros-humble-geometry-msgs ros-humble-control-msgs -c robostack-staging -c conda-forge + conda install robostack-staging::ros-humble-ros-core robostack-staging::ros-humble-action-msgs robostack-staging::ros-humble-std-msgs robostack-staging::ros-humble-geometry-msgs robostack-staging::ros-humble-control-msgs -c robostack-staging -c conda-forge - name: Install unilabos-msgs and project run: | From 39dc4433994f0ee1259a80aeeb09de3f56356ae5 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 26 Jan 2026 02:23:40 +0800 Subject: [PATCH 03/34] CI Check Fix 2 --- .github/workflows/ci-check.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index f11d484..54d6710 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -1,6 +1,8 @@ name: CI Check on: + push: + branches: [main, dev] pull_request: branches: [main, dev] From d6accc3f1cb72abcdce45b26d302e92c8e2c00b3 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:14:21 +0800 Subject: [PATCH 04/34] CI Check Fix 3 --- .github/workflows/ci-check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 54d6710..6a40e48 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -32,11 +32,11 @@ jobs: - name: Install minimal ROS dependencies run: | - conda install robostack-staging::ros-humble-ros-core robostack-staging::ros-humble-action-msgs robostack-staging::ros-humble-std-msgs robostack-staging::ros-humble-geometry-msgs robostack-staging::ros-humble-control-msgs -c robostack-staging -c conda-forge + conda install -n check-env robostack-staging::ros-humble-ros-core robostack-staging::ros-humble-action-msgs robostack-staging::ros-humble-std-msgs robostack-staging::ros-humble-geometry-msgs robostack-staging::ros-humble-control-msgs -c robostack-staging -c conda-forge -y - name: Install unilabos-msgs and project run: | - conda install ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge + conda install -n check-env ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y pip install -e . - name: Run check mode (complete_registry) From a02cecfd187ed63a508b6c8984e3c33199c6a762 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:20:17 +0800 Subject: [PATCH 05/34] CI Check Fix 4 --- .github/workflows/ci-check.yml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 6a40e48..b292549 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -23,20 +23,27 @@ jobs: uses: conda-incubator/setup-miniconda@v3 with: miniconda-version: 'latest' - channels: conda-forge,robostack-staging,uni-lab,defaults - channel-priority: strict + channels: robostack-staging,conda-forge,uni-lab,defaults + channel-priority: flexible activate-environment: check-env auto-activate-base: false auto-update-conda: false show-channel-urls: true - - name: Install minimal ROS dependencies + - name: Install ROS dependencies and unilabos-msgs run: | - conda install -n check-env robostack-staging::ros-humble-ros-core robostack-staging::ros-humble-action-msgs robostack-staging::ros-humble-std-msgs robostack-staging::ros-humble-geometry-msgs robostack-staging::ros-humble-control-msgs -c robostack-staging -c conda-forge -y + # Install all packages together for proper dependency resolution + conda install -n check-env \ + robostack-staging::ros-humble-ros-core \ + robostack-staging::ros-humble-action-msgs \ + robostack-staging::ros-humble-std-msgs \ + robostack-staging::ros-humble-geometry-msgs \ + robostack-staging::ros-humble-control-msgs \ + uni-lab::ros-humble-unilabos-msgs \ + -c robostack-staging -c conda-forge -c uni-lab -y - - name: Install unilabos-msgs and project + - name: Install unilabos project run: | - conda install -n check-env ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y pip install -e . - name: Run check mode (complete_registry) From fd73bb7dcb5f8efc30e4b2dff4e94d75a988979d Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:47:27 +0800 Subject: [PATCH 06/34] CI Check Fix 5 --- .github/workflows/ci-check.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index b292549..f62676e 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -19,11 +19,12 @@ jobs: with: fetch-depth: 0 - - name: Setup Miniconda + - name: Setup Miniforge uses: conda-incubator/setup-miniconda@v3 with: - miniconda-version: 'latest' - channels: robostack-staging,conda-forge,uni-lab,defaults + miniforge-version: latest + use-mamba: true + channels: robostack-staging,conda-forge,uni-lab channel-priority: flexible activate-environment: check-env auto-activate-base: false @@ -33,7 +34,9 @@ jobs: - name: Install ROS dependencies and unilabos-msgs run: | # Install all packages together for proper dependency resolution - conda install -n check-env \ + # Use mamba for faster and more reliable solving + mamba install -n check-env \ + python=3.11.11 \ robostack-staging::ros-humble-ros-core \ robostack-staging::ros-humble-action-msgs \ robostack-staging::ros-humble-std-msgs \ From 2cf58ca4523caf42ca1bebf912573b583ebdba86 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:47:54 +0800 Subject: [PATCH 07/34] Upgrade to py 3.11.14; ros 0.7; unilabos 0.10.16 --- .github/workflows/ci-check.yml | 44 +++++- recipes/msgs/recipe.yaml | 8 +- recipes/unilabos/recipe.yaml | 2 +- setup.py | 2 +- unilabos/__init__.py | 2 +- unilabos/__main__.py | 6 + unilabos/app/ws_client.py | 50 ++++--- unilabos/ros/nodes/base_device_node.py | 28 +++- unilabos/ros/x/__init__.py | 0 unilabos/ros/x/rclpyx.py | 182 ------------------------- unilabos/utils/pywinauto_util.py | 8 +- unilabos_msgs/package.xml | 2 +- 12 files changed, 118 insertions(+), 216 deletions(-) create mode 100644 unilabos/__main__.py delete mode 100644 unilabos/ros/x/__init__.py delete mode 100644 unilabos/ros/x/rclpyx.py diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index f62676e..6341c27 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -36,21 +36,61 @@ jobs: # Install all packages together for proper dependency resolution # Use mamba for faster and more reliable solving mamba install -n check-env \ - python=3.11.11 \ + python=3.11.14 \ robostack-staging::ros-humble-ros-core \ robostack-staging::ros-humble-action-msgs \ robostack-staging::ros-humble-std-msgs \ robostack-staging::ros-humble-geometry-msgs \ robostack-staging::ros-humble-control-msgs \ + robostack-staging::ros-humble-nav2-msgs \ uni-lab::ros-humble-unilabos-msgs \ + robostack-staging::ros-humble-cv-bridge \ + robostack-staging::ros-humble-vision-opencv \ + robostack-staging::ros-humble-tf-transformations \ + robostack-staging::ros-humble-moveit-msgs \ + robostack-staging::ros-humble-tf2-ros \ + robostack-staging::ros-humble-tf2-ros-py \ + conda-forge::transforms3d \ -c robostack-staging -c conda-forge -c uni-lab -y - - name: Install unilabos project + - name: Install pip dependencies and unilabos run: | + # Activate the environment + conda activate check-env + + # Core dependencies for devices + pip install uv + uv pip install networkx \ + typing_extensions \ + websockets \ + msgcenterpy \ + opentrons_shared_data \ + pint \ + fastapi \ + jinja2 \ + requests \ + uvicorn \ + git+https://github.com/Xuwznln/pylabrobot.git \ + opencv-python \ + pyautogui \ + opcua \ + pyserial \ + pandas \ + crcmod-plus \ + pymodbus \ + pywinauto_recorder \ + matplotlib \ + + + # PyLabRobot (custom fork) + pip install + + # Install unilabos in editable mode pip install -e . - name: Run check mode (complete_registry) run: | + conda activate check-env python -m unilabos --check_mode --skip_env_check - name: Check for uncommitted changes diff --git a/recipes/msgs/recipe.yaml b/recipes/msgs/recipe.yaml index 6d32908..168f427 100644 --- a/recipes/msgs/recipe.yaml +++ b/recipes/msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.10.15 + version: 0.10.16 source: path: ../../unilabos_msgs target_directory: src @@ -25,7 +25,7 @@ requirements: build: - ${{ compiler('cxx') }} - ${{ compiler('c') }} - - python ==3.11.11 + - python ==3.11.14 - numpy - if: build_platform != target_platform then: @@ -63,14 +63,14 @@ requirements: - robostack-staging::ros-humble-rosidl-default-generators - robostack-staging::ros-humble-std-msgs - robostack-staging::ros-humble-geometry-msgs - - robostack-staging::ros2-distro-mutex=0.6 + - robostack-staging::ros2-distro-mutex=0.7 run: - robostack-staging::ros-humble-action-msgs - robostack-staging::ros-humble-ros-workspace - robostack-staging::ros-humble-rosidl-default-runtime - robostack-staging::ros-humble-std-msgs - robostack-staging::ros-humble-geometry-msgs - - robostack-staging::ros2-distro-mutex=0.6 + - robostack-staging::ros2-distro-mutex=0.7 - if: osx and x86_64 then: - __osx >=${{ MACOSX_DEPLOYMENT_TARGET|default('10.14') }} diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index be3f1a1..641d5b1 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.10.15" + version: "0.10.16" source: path: ../.. diff --git a/setup.py b/setup.py index b6ae5ed..d286cf8 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ package_name = 'unilabos' setup( name=package_name, - version='0.10.15', + version='0.10.16', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/unilabos/__init__.py b/unilabos/__init__.py index d5ac10a..c69a94b 100644 --- a/unilabos/__init__.py +++ b/unilabos/__init__.py @@ -1 +1 @@ -__version__ = "0.10.15" +__version__ = "0.10.16" diff --git a/unilabos/__main__.py b/unilabos/__main__.py new file mode 100644 index 0000000..6483226 --- /dev/null +++ b/unilabos/__main__.py @@ -0,0 +1,6 @@ +"""Entry point for `python -m unilabos`.""" + +from unilabos.app.main import main + +if __name__ == "__main__": + main() diff --git a/unilabos/app/ws_client.py b/unilabos/app/ws_client.py index 17d69ac..8644353 100644 --- a/unilabos/app/ws_client.py +++ b/unilabos/app/ws_client.py @@ -23,7 +23,7 @@ from typing import Optional, Dict, Any, List from urllib.parse import urlparse from enum import Enum -from jedi.inference.gradual.typing import TypedDict +from typing_extensions import TypedDict from unilabos.app.model import JobAddReq from unilabos.ros.nodes.presets.host_node import HostNode @@ -495,8 +495,12 @@ class MessageProcessor: await self._process_message(message_type, message_data) else: if message_type.endswith("_material"): - logger.trace(f"[MessageProcessor] 收到一条归属 {data.get('edge_session')} 的旧消息:{data}") - logger.debug(f"[MessageProcessor] 跳过了一条归属 {data.get('edge_session')} 的旧消息: {data.get('action')}") + logger.trace( + f"[MessageProcessor] 收到一条归属 {data.get('edge_session')} 的旧消息:{data}" + ) + logger.debug( + f"[MessageProcessor] 跳过了一条归属 {data.get('edge_session')} 的旧消息: {data.get('action')}" + ) else: await self._process_message(message_type, message_data) except json.JSONDecodeError: @@ -848,9 +852,7 @@ class MessageProcessor: device_action_groups[key_add] = [] device_action_groups[key_add].append(item["uuid"]) - logger.info( - f"[资源同步] 跨站Transfer: {item['uuid'][:8]} from {device_old_id} to {device_id}" - ) + logger.info(f"[资源同步] 跨站Transfer: {item['uuid'][:8]} from {device_old_id} to {device_id}") else: # 正常update key = (device_id, "update") @@ -864,7 +866,9 @@ class MessageProcessor: device_action_groups[key] = [] device_action_groups[key].append(item["uuid"]) - logger.trace(f"[资源同步] 动作 {action} 分组数量: {len(device_action_groups)}, 总数量: {len(resource_uuid_list)}") + logger.trace( + f"[资源同步] 动作 {action} 分组数量: {len(device_action_groups)}, 总数量: {len(resource_uuid_list)}" + ) # 为每个(device_id, action)创建独立的更新线程 for (device_id, actual_action), items in device_action_groups.items(): @@ -912,13 +916,13 @@ class MessageProcessor: # 发送确认消息 if self.websocket_client: - await self.websocket_client.send_message({ - "action": "restart_acknowledged", - "data": {"reason": reason, "delay": delay} - }) + await self.websocket_client.send_message( + {"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}} + ) # 设置全局重启标志 import unilabos.app.main as main_module + main_module._restart_requested = True main_module._restart_reason = reason @@ -928,10 +932,12 @@ class MessageProcessor: # 在新线程中执行清理,避免阻塞当前事件循环 def do_cleanup(): import time + time.sleep(0.5) # 给当前消息处理完成的时间 logger.info(f"[MessageProcessor] Starting cleanup for restart, reason: {reason}") try: from unilabos.app.utils import cleanup_for_restart + if cleanup_for_restart(): logger.info("[MessageProcessor] Cleanup successful, main() will restart") else: @@ -1382,7 +1388,9 @@ class WebSocketClient(BaseCommunicationClient): if host_node: # 获取设备信息 for device_id, namespace in host_node.devices_names.items(): - device_key = f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}" + device_key = ( + f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}" + ) is_online = device_key in host_node._online_devices # 获取设备的动作信息 @@ -1396,14 +1404,16 @@ class WebSocketClient(BaseCommunicationClient): "action_type": str(type(client).__name__), } - devices.append({ - "device_id": device_id, - "namespace": namespace, - "device_key": device_key, - "is_online": is_online, - "machine_name": host_node.device_machine_names.get(device_id, machine_name), - "actions": actions, - }) + devices.append( + { + "device_id": device_id, + "namespace": namespace, + "device_key": device_key, + "is_online": is_online, + "machine_name": host_node.device_machine_names.get(device_id, machine_name), + "actions": actions, + } + ) logger.info(f"[WebSocketClient] Collected {len(devices)} devices for host_ready") except Exception as e: diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 737167a..95fc075 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -49,7 +49,6 @@ from unilabos.resources.resource_tracker import ( ResourceTreeInstance, ResourceDictInstance, ) -from unilabos.ros.x.rclpyx import get_event_loop from unilabos.ros.utils.driver_creator import WorkstationNodeCreator, PyLabRobotCreator, DeviceClassCreator from rclpy.task import Task, Future from unilabos.utils.import_manager import default_manager @@ -185,7 +184,7 @@ class PropertyPublisher: f"创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {ex}\n{traceback.format_exc()}" ) self.timer = node.create_timer(self.timer_period, self.publish_property) - self.__loop = get_event_loop() + self.__loop = ROS2DeviceNode.get_asyncio_loop() str_msg_type = str(msg_type)[8:-2] self.node.lab_logger().trace(f"发布属性: {name}, 类型: {str_msg_type}, 周期: {initial_period}秒, QoS: {qos}") @@ -1757,6 +1756,15 @@ class ROS2DeviceNode: 它不继承设备类,而是通过代理模式访问设备类的属性和方法。 """ + # 类变量,用于循环管理 + _asyncio_loop = None + _asyncio_loop_running = False + _asyncio_loop_thread = None + + @classmethod + def get_asyncio_loop(cls): + return cls._asyncio_loop + @staticmethod async def safe_task_wrapper(trace_callback, func, **kwargs): try: @@ -1833,6 +1841,11 @@ class ROS2DeviceNode: print_publish: 是否打印发布信息 driver_is_ros: """ + # 在初始化时检查循环状态 + if ROS2DeviceNode._asyncio_loop_running and ROS2DeviceNode._asyncio_loop_thread is not None: + pass + elif ROS2DeviceNode._asyncio_loop_thread is None: + self._start_loop() # 保存设备类是否支持异步上下文 self._has_async_context = hasattr(driver_class, "__aenter__") and hasattr(driver_class, "__aexit__") @@ -1924,6 +1937,17 @@ class ROS2DeviceNode: except Exception as e: self._ros_node.lab_logger().error(f"设备后初始化失败: {e}") + def _start_loop(self): + def run_event_loop(): + loop = asyncio.new_event_loop() + ROS2DeviceNode._asyncio_loop = loop + asyncio.set_event_loop(loop) + loop.run_forever() + + ROS2DeviceNode._asyncio_loop_thread = threading.Thread(target=run_event_loop, daemon=True, name="ROS2DeviceNode") + ROS2DeviceNode._asyncio_loop_thread.start() + logger.info(f"循环线程已启动") + class DeviceInfoType(TypedDict): id: str diff --git a/unilabos/ros/x/__init__.py b/unilabos/ros/x/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/unilabos/ros/x/rclpyx.py b/unilabos/ros/x/rclpyx.py deleted file mode 100644 index a723922..0000000 --- a/unilabos/ros/x/rclpyx.py +++ /dev/null @@ -1,182 +0,0 @@ -import asyncio -from asyncio import events -import threading - -import rclpy -from rclpy.impl.implementation_singleton import rclpy_implementation as _rclpy -from rclpy.executors import await_or_execute, Executor -from rclpy.action import ActionClient, ActionServer -from rclpy.action.server import ServerGoalHandle, GoalResponse, GoalInfo, GoalStatus -from std_msgs.msg import String -from action_tutorials_interfaces.action import Fibonacci - - -loop = None - -def get_event_loop(): - global loop - return loop - - -async def default_handle_accepted_callback_async(goal_handle): - """Execute the goal.""" - await goal_handle.execute() - - -class ServerGoalHandleX(ServerGoalHandle): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - async def execute(self, execute_callback=None): - # It's possible that there has been a request to cancel the goal prior to executing. - # In this case we want to avoid the illegal state transition to EXECUTING - # but still call the users execute callback to let them handle canceling the goal. - if not self.is_cancel_requested: - self._update_state(_rclpy.GoalEvent.EXECUTE) - await self._action_server.notify_execute_async(self, execute_callback) - - -class ActionServerX(ActionServer): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.register_handle_accepted_callback(default_handle_accepted_callback_async) - - async def _execute_goal_request(self, request_header_and_message): - request_header, goal_request = request_header_and_message - goal_uuid = goal_request.goal_id - goal_info = GoalInfo() - goal_info.goal_id = goal_uuid - - self._node.get_logger().debug('New goal request with ID: {0}'.format(goal_uuid.uuid)) - - # Check if goal ID is already being tracked by this action server - with self._lock: - goal_id_exists = self._handle.goal_exists(goal_info) - - accepted = False - if not goal_id_exists: - # Call user goal callback - response = await await_or_execute(self._goal_callback, goal_request.goal) - if not isinstance(response, GoalResponse): - self._node.get_logger().warning( - 'Goal request callback did not return a GoalResponse type. Rejecting goal.') - else: - accepted = GoalResponse.ACCEPT == response - - if accepted: - # Stamp time of acceptance - goal_info.stamp = self._node.get_clock().now().to_msg() - - # Create a goal handle - try: - with self._lock: - goal_handle = ServerGoalHandleX(self, goal_info, goal_request.goal) - except RuntimeError as e: - self._node.get_logger().error( - 'Failed to accept new goal with ID {0}: {1}'.format(goal_uuid.uuid, e)) - accepted = False - else: - self._goal_handles[bytes(goal_uuid.uuid)] = goal_handle - - # Send response - response_msg = self._action_type.Impl.SendGoalService.Response() - response_msg.accepted = accepted - response_msg.stamp = goal_info.stamp - self._handle.send_goal_response(request_header, response_msg) - - if not accepted: - self._node.get_logger().debug('New goal rejected: {0}'.format(goal_uuid.uuid)) - return - - self._node.get_logger().debug('New goal accepted: {0}'.format(goal_uuid.uuid)) - - # Provide the user a reference to the goal handle - # await await_or_execute(self._handle_accepted_callback, goal_handle) - asyncio.create_task(self._handle_accepted_callback(goal_handle)) - - async def notify_execute_async(self, goal_handle, execute_callback): - # Use provided callback, defaulting to a previously registered callback - if execute_callback is None: - if self._execute_callback is None: - return - execute_callback = self._execute_callback - - # Schedule user callback for execution - self._node.get_logger().info(f"{events.get_running_loop()}") - asyncio.create_task(self._execute_goal(execute_callback, goal_handle)) - # loop = asyncio.new_event_loop() - # asyncio.set_event_loop(loop) - # task = loop.create_task(self._execute_goal(execute_callback, goal_handle)) - # await task - - -class ActionClientX(ActionClient): - feedback_queue = asyncio.Queue() - - async def feedback_cb(self, msg): - await self.feedback_queue.put(msg) - - async def send_goal_async(self, goal_msg): - goal_future = super().send_goal_async( - goal_msg, - feedback_callback=self.feedback_cb - ) - client_goal_handle = await asyncio.ensure_future(goal_future) - if not client_goal_handle.accepted: - raise Exception("Goal rejected.") - result_future = client_goal_handle.get_result_async() - while True: - feedback_future = asyncio.ensure_future(self.feedback_queue.get()) - tasks = [result_future, feedback_future] - await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) - if result_future.done(): - result = result_future.result().result - yield (None, result) - break - else: - feedback = feedback_future.result().feedback - yield (feedback, None) - - -async def main(node): - print('Node started.') - action_client = ActionClientX(node, Fibonacci, 'fibonacci') - goal_msg = Fibonacci.Goal() - goal_msg.order = 10 - async for (feedback, result) in action_client.send_goal_async(goal_msg): - if feedback: - print(f'Feedback: {feedback}') - else: - print(f'Result: {result}') - print('Finished.') - - -async def ros_loop_node(node): - while rclpy.ok(): - rclpy.spin_once(node, timeout_sec=0) - await asyncio.sleep(1e-4) - - -async def ros_loop(executor: Executor): - while rclpy.ok(): - executor.spin_once(timeout_sec=0) - await asyncio.sleep(1e-4) - - -def run_event_loop(): - global loop - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.run_forever() - - -def run_event_loop_in_thread(): - thread = threading.Thread(target=run_event_loop, args=()) - thread.start() - - -if __name__ == "__main__": - rclpy.init() - node = rclpy.create_node('async_subscriber') - future = asyncio.wait([ros_loop(node), main()]) - asyncio.get_event_loop().run_until_complete(future) \ No newline at end of file diff --git a/unilabos/utils/pywinauto_util.py b/unilabos/utils/pywinauto_util.py index 3b78632..70eeb96 100644 --- a/unilabos/utils/pywinauto_util.py +++ b/unilabos/utils/pywinauto_util.py @@ -1,7 +1,11 @@ import psutil import pywinauto -from pywinauto_recorder import UIApplication -from pywinauto_recorder.player import UIPath, click, focus_on_application, exists, find, get_wrapper_path +try: + from pywinauto_recorder import UIApplication + from pywinauto_recorder.player import UIPath, click, focus_on_application, exists, find, get_wrapper_path +except ImportError: + print("未安装pywinauto_recorder,部分功能无法使用,安装时注意enum") + pass from pywinauto.controls.uiawrapper import UIAWrapper from pywinauto.application import WindowSpecification from pywinauto import findbestmatch diff --git a/unilabos_msgs/package.xml b/unilabos_msgs/package.xml index b9c2632..42d295a 100644 --- a/unilabos_msgs/package.xml +++ b/unilabos_msgs/package.xml @@ -2,7 +2,7 @@ unilabos_msgs - 0.10.15 + 0.10.16 ROS2 Messages package for unilabos devices Junhan Chang Xuwznln From 43e4c71a8e395c13729693073f715686fbef7e3a Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:31:24 +0800 Subject: [PATCH 08/34] Update to ROS2 Humble 0.7 --- .conda/base/recipe.yaml | 60 ++++++ .conda/full/recipe.yaml | 42 ++++ .conda/recipe.yaml | 91 --------- .github/workflows/ci-check.yml | 40 +--- .github/workflows/conda-pack-build.yml | 39 +++- .github/workflows/deploy-docs.yml | 31 ++- .github/workflows/unilabos-conda-build.yml | 50 ++++- MANIFEST.in | 1 + README.md | 38 +++- README_zh.md | 38 +++- docs/user_guide/best_practice.md | 75 +++++++- docs/user_guide/installation.md | 200 ++++++++++++++----- scripts/create_readme.py | 4 +- scripts/dev_install.py | 214 +++++++++++++++++++++ unilabos/app/main.py | 4 +- unilabos/app/utils.py | 34 +++- unilabos/utils/requirements.txt | 17 ++ 17 files changed, 759 insertions(+), 219 deletions(-) create mode 100644 .conda/base/recipe.yaml create mode 100644 .conda/full/recipe.yaml delete mode 100644 .conda/recipe.yaml create mode 100644 scripts/dev_install.py create mode 100644 unilabos/utils/requirements.txt diff --git a/.conda/base/recipe.yaml b/.conda/base/recipe.yaml new file mode 100644 index 0000000..40c29fc --- /dev/null +++ b/.conda/base/recipe.yaml @@ -0,0 +1,60 @@ +# unilabos: Production package (depends on unilabos-env + pip unilabos) +# For production deployment + +package: + name: unilabos + version: 0.10.16 + +source: + path: ../../unilabos + target_directory: unilabos + +build: + python: + entry_points: + - unilab = unilabos.app.main:main + script: + - set PIP_NO_INDEX= + - if: win + then: + - copy %RECIPE_DIR%\..\..\MANIFEST.in %SRC_DIR% + - copy %RECIPE_DIR%\..\..\setup.cfg %SRC_DIR% + - copy %RECIPE_DIR%\..\..\setup.py %SRC_DIR% + - pip install %SRC_DIR% + - if: unix + then: + - cp $RECIPE_DIR/../../MANIFEST.in $SRC_DIR + - cp $RECIPE_DIR/../../setup.cfg $SRC_DIR + - cp $RECIPE_DIR/../../setup.py $SRC_DIR + - uv pip install $SRC_DIR + +requirements: + host: + - python ==3.11.14 + - pip + - setuptools + - zstd + - zstandard + run: + - zstd + - zstandard + - networkx + - typing_extensions + - websockets + - opentrons_shared_data + - pint + - fastapi + - jinja2 + - requests + - uvicorn + - opcua + - pyserial + - pandas + - pymodbus + - matplotlib + - uni-lab::unilabos-env ==0.10.16 + +about: + repository: https://github.com/deepmodeling/Uni-Lab-OS + license: GPL-3.0-only + description: "UniLabOS - Production package with minimal ROS2 dependencies" diff --git a/.conda/full/recipe.yaml b/.conda/full/recipe.yaml new file mode 100644 index 0000000..137d9db --- /dev/null +++ b/.conda/full/recipe.yaml @@ -0,0 +1,42 @@ +# unilabos-full: Full package with all features +# Depends on unilabos + complete ROS2 desktop + dev tools + +package: + name: unilabos-full + version: 0.10.16 + +build: + noarch: generic + +requirements: + run: + # Base unilabos package (includes unilabos-env) + - uni-lab::unilabos ==0.10.16 + # Documentation tools + - sphinx + - sphinx_rtd_theme + # Web UI + - gradio + - flask + # Interactive development + - ipython + - jupyter + - jupyros + - colcon-common-extensions + # ROS2 full desktop (includes rviz2, gazebo, etc.) + - robostack-staging::ros-humble-desktop-full + # Navigation and motion control + - ros-humble-navigation2 + - ros-humble-ros2-control + - ros-humble-robot-state-publisher + - ros-humble-joint-state-publisher + # MoveIt motion planning + - ros-humble-moveit + - ros-humble-moveit-servo + # Simulation + - ros-humble-simulation + +about: + repository: https://github.com/deepmodeling/Uni-Lab-OS + license: GPL-3.0-only + description: "UniLabOS Full - Complete package with ROS2 Desktop, MoveIt, Navigation2, Gazebo, Jupyter" diff --git a/.conda/recipe.yaml b/.conda/recipe.yaml deleted file mode 100644 index 2b041c8..0000000 --- a/.conda/recipe.yaml +++ /dev/null @@ -1,91 +0,0 @@ -package: - name: unilabos - version: 0.10.15 - -source: - path: ../unilabos - target_directory: unilabos - -build: - python: - entry_points: - - unilab = unilabos.app.main:main - script: - - set PIP_NO_INDEX= - - if: win - then: - - copy %RECIPE_DIR%\..\MANIFEST.in %SRC_DIR% - - copy %RECIPE_DIR%\..\setup.cfg %SRC_DIR% - - copy %RECIPE_DIR%\..\setup.py %SRC_DIR% - - call %PYTHON% -m pip install %SRC_DIR% - - if: unix - then: - - cp $RECIPE_DIR/../MANIFEST.in $SRC_DIR - - cp $RECIPE_DIR/../setup.cfg $SRC_DIR - - cp $RECIPE_DIR/../setup.py $SRC_DIR - - $PYTHON -m pip install $SRC_DIR - -requirements: - host: - - python ==3.11.11 - - pip - - setuptools - - zstd - - zstandard - run: - - conda-forge::python ==3.11.11 - - compilers - - cmake - - zstd - - zstandard - - ninja - - if: unix - then: - - make - - sphinx - - sphinx_rtd_theme - - numpy - - scipy - - pandas - - networkx - - matplotlib - - pint - - pyserial - - pyusb - - pylibftdi - - pymodbus - - python-can - - pyvisa - - opencv - - pydantic - - fastapi - - uvicorn - - gradio - - flask - - websockets - - ipython - - jupyter - - jupyros - - colcon-common-extensions - - robostack-staging::ros-humble-desktop-full - - robostack-staging::ros-humble-control-msgs - - robostack-staging::ros-humble-sensor-msgs - - robostack-staging::ros-humble-trajectory-msgs - - ros-humble-navigation2 - - ros-humble-ros2-control - - ros-humble-robot-state-publisher - - ros-humble-joint-state-publisher - - ros-humble-rosbridge-server - - ros-humble-cv-bridge - - ros-humble-tf2 - - ros-humble-moveit - - ros-humble-moveit-servo - - ros-humble-simulation - - ros-humble-tf-transformations - - transforms3d - - uni-lab::ros-humble-unilabos-msgs - -about: - repository: https://github.com/deepmodeling/Uni-Lab-OS - license: GPL-3.0-only - description: "Uni-Lab-OS" diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 6341c27..2151a66 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -31,12 +31,14 @@ jobs: auto-update-conda: false show-channel-urls: true - - name: Install ROS dependencies and unilabos-msgs + - name: Install ROS dependencies, uv and unilabos-msgs run: | # Install all packages together for proper dependency resolution # Use mamba for faster and more reliable solving mamba install -n check-env \ python=3.11.14 \ + conda-forge::uv \ + conda-forge::opencv \ robostack-staging::ros-humble-ros-core \ robostack-staging::ros-humble-action-msgs \ robostack-staging::ros-humble-std-msgs \ @@ -57,36 +59,14 @@ jobs: run: | # Activate the environment conda activate check-env - - # Core dependencies for devices - pip install uv - uv pip install networkx \ - typing_extensions \ - websockets \ - msgcenterpy \ - opentrons_shared_data \ - pint \ - fastapi \ - jinja2 \ - requests \ - uvicorn \ - git+https://github.com/Xuwznln/pylabrobot.git \ - opencv-python \ - pyautogui \ - opcua \ - pyserial \ - pandas \ - crcmod-plus \ - pymodbus \ - pywinauto_recorder \ - matplotlib \ - - - # PyLabRobot (custom fork) - pip install - + # Install pip dependencies from requirements.txt (uv already installed via conda) + uv pip install -r unilabos/utils/requirements.txt + # Install special packages (git-based) + uv pip install pywinauto git+https://github.com/Xuwznln/pylabrobot.git + # Remove conflicting package + uv pip uninstall enum34 || true # Install unilabos in editable mode - pip install -e . + uv pip install -e . - name: Run check mode (complete_registry) run: | diff --git a/.github/workflows/conda-pack-build.yml b/.github/workflows/conda-pack-build.yml index 3a379fa..6476be9 100644 --- a/.github/workflows/conda-pack-build.yml +++ b/.github/workflows/conda-pack-build.yml @@ -13,6 +13,11 @@ on: required: false default: 'win-64' type: string + build_full: + description: '是否构建完整版 unilabos-full (默认构建轻量版 unilabos)' + required: false + default: false + type: boolean jobs: build-conda-pack: @@ -69,7 +74,7 @@ jobs: with: miniforge-version: latest use-mamba: true - python-version: '3.11.11' + python-version: '3.11.14' channels: conda-forge,robostack-staging,uni-lab,defaults channel-priority: flexible activate-environment: unilab @@ -81,7 +86,14 @@ jobs: run: | echo Installing unilabos and dependencies to unilab environment... echo Using mamba for faster and more reliable dependency resolution... - mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y + echo Build full: ${{ github.event.inputs.build_full }} + if "${{ github.event.inputs.build_full }}"=="true" ( + echo Installing unilabos-full ^(complete package^)... + mamba install -n unilab uni-lab::unilabos-full conda-pack -c uni-lab -c robostack-staging -c conda-forge -y + ) else ( + echo Installing unilabos ^(minimal package^)... + mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y + ) - name: Install conda-pack, unilabos and dependencies (Unix) if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64' @@ -89,7 +101,14 @@ jobs: run: | echo "Installing unilabos and dependencies to unilab environment..." echo "Using mamba for faster and more reliable dependency resolution..." - mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y + echo "Build full: ${{ github.event.inputs.build_full }}" + if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then + echo "Installing unilabos-full (complete package)..." + mamba install -n unilab uni-lab::unilabos-full conda-pack -c uni-lab -c robostack-staging -c conda-forge -y + else + echo "Installing unilabos (minimal package)..." + mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y + fi - name: Get latest ros-humble-unilabos-msgs version (Windows) if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64' @@ -308,7 +327,12 @@ jobs: echo ========================================== echo Platform: ${{ matrix.platform }} echo Branch: ${{ github.event.inputs.branch }} - echo Python version: 3.11.11 + echo Python version: 3.11.14 + if "${{ github.event.inputs.build_full }}"=="true" ( + echo Package: unilabos-full ^(complete^) + ) else ( + echo Package: unilabos ^(minimal^) + ) echo. echo Distribution package contents: dir dist-package @@ -328,7 +352,12 @@ jobs: echo "==========================================" echo "Platform: ${{ matrix.platform }}" echo "Branch: ${{ github.event.inputs.branch }}" - echo "Python version: 3.11.11" + echo "Python version: 3.11.14" + if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then + echo "Package: unilabos-full (complete)" + else + echo "Package: unilabos (minimal)" + fi echo "" echo "Distribution package contents:" ls -lh dist-package/ diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 66aef8d..cf2d338 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,10 +1,12 @@ name: Deploy Docs on: - push: - branches: [main] - pull_request: + # 在 CI Check 成功后自动触发(仅 main 分支) + workflow_run: + workflows: ["CI Check"] + types: [completed] branches: [main] + # 手动触发 workflow_dispatch: inputs: branch: @@ -33,12 +35,19 @@ concurrency: jobs: # Build documentation build: + # 只在以下情况运行: + # 1. workflow_run 触发且 CI Check 成功 + # 2. 手动触发 + if: | + github.event_name == 'workflow_dispatch' || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: - ref: ${{ github.event.inputs.branch || github.ref }} + # workflow_run 时使用触发工作流的分支,手动触发时使用输入的分支 + ref: ${{ github.event.workflow_run.head_branch || github.event.inputs.branch || github.ref }} fetch-depth: 0 - name: Setup Miniforge (with mamba) @@ -46,7 +55,7 @@ jobs: with: miniforge-version: latest use-mamba: true - python-version: '3.11.11' + python-version: '3.11.14' channels: conda-forge,robostack-staging,uni-lab,defaults channel-priority: flexible activate-environment: unilab @@ -76,7 +85,9 @@ jobs: - name: Setup Pages id: pages uses: actions/configure-pages@v4 - if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true') + if: | + github.event.workflow_run.head_branch == 'main' || + (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true') - name: Build Sphinx documentation run: | @@ -95,13 +106,17 @@ jobs: - name: Upload build artifacts uses: actions/upload-pages-artifact@v3 - if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true') + if: | + github.event.workflow_run.head_branch == 'main' || + (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true') with: path: docs/_build/html # Deploy to GitHub Pages deploy: - if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true') + if: | + github.event.workflow_run.head_branch == 'main' || + (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true') environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} diff --git a/.github/workflows/unilabos-conda-build.yml b/.github/workflows/unilabos-conda-build.yml index 214f9bf..2ace422 100644 --- a/.github/workflows/unilabos-conda-build.yml +++ b/.github/workflows/unilabos-conda-build.yml @@ -1,17 +1,26 @@ name: UniLabOS Conda Build on: + # 在 CI Check 成功后自动触发 + workflow_run: + workflows: ["CI Check"] + types: [completed] + branches: [main, dev] + # 标签推送时直接触发(发布版本) push: - branches: [main, dev] tags: ['v*'] - pull_request: - branches: [main, dev] + # 手动触发 workflow_dispatch: inputs: platforms: description: '选择构建平台 (逗号分隔): linux-64, osx-64, osx-arm64, win-64' required: false default: 'linux-64' + build_full: + description: '是否构建 unilabos-full 完整包 (默认只构建 unilabos 基础包)' + required: false + default: false + type: boolean upload_to_anaconda: description: '是否上传到Anaconda.org' required: false @@ -20,6 +29,14 @@ on: jobs: build: + # 只在以下情况运行: + # 1. workflow_run 触发且 CI Check 成功 + # 2. 标签推送(发布版本) + # 3. 手动触发 + if: | + github.event_name == 'workflow_dispatch' || + github.event_name == 'push' || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') strategy: fail-fast: false matrix: @@ -81,12 +98,33 @@ jobs: conda list | grep -E "(rattler-build|anaconda-client)" echo "Platform: ${{ matrix.platform }}" echo "OS: ${{ matrix.os }}" - echo "Building UniLabOS package" + echo "Build full package: ${{ github.event.inputs.build_full || 'false' }}" + echo "Building packages:" + echo " - unilabos-env (environment dependencies)" + echo " - unilabos (with pip package)" + if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then + echo " - unilabos-full (complete package)" + fi - - name: Build conda package + - name: Build unilabos-env (conda environment only, noarch) if: steps.should_build.outputs.should_build == 'true' run: | - rattler-build build -r .conda/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge + echo "Building unilabos-env (conda environment dependencies)..." + rattler-build build -r .conda/env/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge + + - name: Build unilabos (with pip package) + if: steps.should_build.outputs.should_build == 'true' + run: | + echo "Building unilabos package..." + rattler-build build -r .conda/base/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output + + - name: Build unilabos-full - Only when explicitly requested + if: | + steps.should_build.outputs.should_build == 'true' && + github.event.inputs.build_full == 'true' + run: | + echo "Building unilabos-full package on ${{ matrix.platform }}..." + rattler-build build -r .conda/full/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output - name: List built packages if: steps.should_build.outputs.should_build == 'true' diff --git a/MANIFEST.in b/MANIFEST.in index d81945e..156ca52 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ recursive-include unilabos/test * +recursive-include unilabos/utils * recursive-include unilabos/registry *.yaml recursive-include unilabos/app/web/static * recursive-include unilabos/app/web/templates * diff --git a/README.md b/README.md index f10cc0f..fa0d9dd 100644 --- a/README.md +++ b/README.md @@ -31,26 +31,46 @@ Detailed documentation can be found at: ## Quick Start -1. Setup Conda Environment +### 1. Setup Conda Environment -Uni-Lab-OS recommends using `mamba` for environment management: +Uni-Lab-OS recommends using `mamba` for environment management. Choose the package that fits your needs: + +| Package | Use Case | Contents | +|---------|----------|----------| +| `unilabos` | **Recommended for most users** | Complete package, ready to use | +| `unilabos-env` | Developers (editable install) | Environment only, install unilabos via pip | +| `unilabos-full` | Simulation/Visualization | unilabos + ROS2 Desktop + Gazebo + MoveIt | ```bash # Create new environment -mamba create -n unilab python=3.11.11 +mamba create -n unilab python=3.11.14 mamba activate unilab -mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge + +# Option A: Standard installation (recommended for most users) +mamba install uni-lab::unilabos -c robostack-staging -c conda-forge + +# Option B: For developers (editable mode development) +mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge +# Then install unilabos and dependencies: +git clone https://github.com/deepmodeling/Uni-Lab-OS.git && cd Uni-Lab-OS +pip install -e . +uv pip install -r unilabos/utils/requirements.txt + +# Option C: Full installation (simulation/visualization) +mamba install uni-lab::unilabos-full -c robostack-staging -c conda-forge ``` -2. Install Dev Uni-Lab-OS +**When to use which?** +- **unilabos**: Standard installation for production deployment and general usage (recommended) +- **unilabos-env**: For developers who need `pip install -e .` editable mode, modify source code +- **unilabos-full**: For simulation (Gazebo), visualization (rviz2), and Jupyter notebooks + +### 2. Clone Repository (Optional, for developers) ```bash -# Clone the repository +# Clone the repository (only needed for development or examples) git clone https://github.com/deepmodeling/Uni-Lab-OS.git cd Uni-Lab-OS - -# Install Uni-Lab-OS -pip install . ``` 3. Start Uni-Lab System diff --git a/README_zh.md b/README_zh.md index c4dba7d..20b8f53 100644 --- a/README_zh.md +++ b/README_zh.md @@ -31,26 +31,46 @@ Uni-Lab-OS 是一个用于实验室自动化的综合平台,旨在连接和控 ## 快速开始 -1. 配置 Conda 环境 +### 1. 配置 Conda 环境 -Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件: +Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的需求选择合适的安装包: + +| 安装包 | 适用场景 | 包含内容 | +|--------|----------|----------| +| `unilabos` | **推荐大多数用户** | 完整安装包,开箱即用 | +| `unilabos-env` | 开发者(可编辑安装) | 仅环境依赖,通过 pip 安装 unilabos | +| `unilabos-full` | 仿真/可视化 | unilabos + ROS2 桌面版 + Gazebo + MoveIt | ```bash # 创建新环境 -mamba create -n unilab python=3.11.11 +mamba create -n unilab python=3.11.14 mamba activate unilab -mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge + +# 方案 A:标准安装(推荐大多数用户) +mamba install uni-lab::unilabos -c robostack-staging -c conda-forge + +# 方案 B:开发者环境(可编辑模式开发) +mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge +# 然后安装 unilabos 和依赖: +git clone https://github.com/deepmodeling/Uni-Lab-OS.git && cd Uni-Lab-OS +pip install -e . +uv pip install -r unilabos/utils/requirements.txt + +# 方案 C:完整安装(仿真/可视化) +mamba install uni-lab::unilabos-full -c robostack-staging -c conda-forge ``` -2. 安装开发版 Uni-Lab-OS: +**如何选择?** +- **unilabos**:标准安装,适用于生产部署和日常使用(推荐) +- **unilabos-env**:开发者使用,支持 `pip install -e .` 可编辑模式,可修改源代码 +- **unilabos-full**:需要仿真(Gazebo)、可视化(rviz2)或 Jupyter Notebook + +### 2. 克隆仓库(可选,供开发者使用) ```bash -# 克隆仓库 +# 克隆仓库(仅开发或查看示例时需要) git clone https://github.com/deepmodeling/Uni-Lab-OS.git cd Uni-Lab-OS - -# 安装 Uni-Lab-OS -pip install . ``` 3. 启动 Uni-Lab 系统 diff --git a/docs/user_guide/best_practice.md b/docs/user_guide/best_practice.md index e1ffc24..0fa4d1e 100644 --- a/docs/user_guide/best_practice.md +++ b/docs/user_guide/best_practice.md @@ -31,6 +31,14 @@ 详细的安装步骤请参考 [安装指南](installation.md)。 +**选择合适的安装包:** + +| 安装包 | 适用场景 | 包含组件 | +|--------|----------|----------| +| `unilabos` | **推荐大多数用户**,生产部署 | 完整安装包,开箱即用 | +| `unilabos-env` | 开发者(可编辑安装) | 仅环境依赖,通过 pip 安装 unilabos | +| `unilabos-full` | 仿真/可视化 | unilabos + 完整 ROS2 桌面版 + Gazebo + MoveIt | + **关键步骤:** ```bash @@ -38,15 +46,30 @@ # 下载 Miniforge: https://github.com/conda-forge/miniforge/releases # 2. 创建 Conda 环境 -mamba create -n unilab python=3.11.11 +mamba create -n unilab python=3.11.14 # 3. 激活环境 mamba activate unilab -# 4. 安装 Uni-Lab-OS +# 4. 安装 Uni-Lab-OS(选择其一) + +# 方案 A:标准安装(推荐大多数用户) mamba install uni-lab::unilabos -c robostack-staging -c conda-forge + +# 方案 B:开发者环境(可编辑模式开发) +mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge +pip install -e /path/to/Uni-Lab-OS # 可编辑安装 +uv pip install -r unilabos/utils/requirements.txt # 安装 pip 依赖 + +# 方案 C:完整版(仿真/可视化) +mamba install uni-lab::unilabos-full -c robostack-staging -c conda-forge ``` +**选择建议:** +- **日常使用/生产部署**:使用 `unilabos`(推荐),完整功能,开箱即用 +- **开发者**:使用 `unilabos-env` + `pip install -e .` + `uv pip install -r unilabos/utils/requirements.txt`,代码修改立即生效 +- **仿真/可视化**:使用 `unilabos-full`,含 Gazebo、rviz2、MoveIt + #### 1.2 验证安装 ```bash @@ -768,7 +791,43 @@ Waiting for host service... 详细的设备驱动编写指南请参考 [添加设备驱动](../developer_guide/add_device.md)。 -#### 9.1 为什么需要自定义设备? +#### 9.1 开发环境准备 + +**推荐使用 `unilabos-env` + `pip install -e .` + `uv pip install`** 进行设备开发: + +```bash +# 1. 创建环境并安装 unilabos-env(ROS2 + conda 依赖 + uv) +mamba create -n unilab python=3.11.14 +conda activate unilab +mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge + +# 2. 克隆代码 +git clone https://github.com/deepmodeling/Uni-Lab-OS.git +cd Uni-Lab-OS + +# 3. 以可编辑模式安装(推荐使用脚本,自动检测中文环境) +python scripts/dev_install.py + +# 或手动安装: +pip install -e . +uv pip install -r unilabos/utils/requirements.txt +``` + +**为什么使用这种方式?** +- `unilabos-env` 提供 ROS2 核心组件和 uv(通过 conda 安装,避免编译) +- `unilabos/utils/requirements.txt` 包含所有运行时需要的 pip 依赖 +- `dev_install.py` 自动检测中文环境,中文系统自动使用清华镜像 +- 使用 `uv` 替代 `pip`,安装速度更快 +- 可编辑模式:代码修改**立即生效**,无需重新安装 + +**如果安装失败或速度太慢**,可以手动执行(使用清华镜像): + +```bash +pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple +uv pip install -r unilabos/utils/requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple +``` + +#### 9.2 为什么需要自定义设备? Uni-Lab-OS 内置了常见设备,但您的实验室可能有特殊设备需要集成: @@ -777,7 +836,7 @@ Uni-Lab-OS 内置了常见设备,但您的实验室可能有特殊设备需要 - 特殊的实验流程 - 第三方设备集成 -#### 9.2 创建 Python 包 +#### 9.3 创建 Python 包 为了方便开发和管理,建议为您的实验室创建独立的 Python 包。 @@ -814,7 +873,7 @@ touch my_lab_devices/my_lab_devices/__init__.py touch my_lab_devices/my_lab_devices/devices/__init__.py ``` -#### 9.3 创建 setup.py +#### 9.4 创建 setup.py ```python # my_lab_devices/setup.py @@ -845,7 +904,7 @@ setup( ) ``` -#### 9.4 开发安装 +#### 9.5 开发安装 使用 `-e` 参数进行可编辑安装,这样代码修改后立即生效: @@ -860,7 +919,7 @@ pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple - 方便调试和测试 - 支持版本控制(git) -#### 9.5 编写设备驱动 +#### 9.6 编写设备驱动 创建设备驱动文件: @@ -1001,7 +1060,7 @@ class MyPump: - **返回 Dict**:所有动作方法返回字典类型 - **文档字符串**:详细说明参数和功能 -#### 9.6 测试设备驱动 +#### 9.7 测试设备驱动 创建简单的测试脚本: diff --git a/docs/user_guide/installation.md b/docs/user_guide/installation.md index 3f94f2f..acf8fb6 100644 --- a/docs/user_guide/installation.md +++ b/docs/user_guide/installation.md @@ -13,15 +13,26 @@ - 开发者需要 Git 和基本的 Python 开发知识 - 自定义 msgs 需要 GitHub 账号 +## 安装包选择 + +Uni-Lab-OS 提供三个安装包版本,根据您的需求选择: + +| 安装包 | 适用场景 | 包含组件 | 磁盘占用 | +|--------|----------|----------|----------| +| **unilabos** | **推荐大多数用户**,生产部署 | 完整安装包,开箱即用 | ~2-3 GB | +| **unilabos-env** | 开发者环境(可编辑安装) | 仅环境依赖,通过 pip 安装 unilabos | ~2 GB | +| **unilabos-full** | 仿真可视化、完整功能体验 | unilabos + 完整 ROS2 桌面版 + Gazebo + MoveIt | ~8-10 GB | + ## 安装方式选择 根据您的使用场景,选择合适的安装方式: -| 安装方式 | 适用人群 | 特点 | 安装时间 | -| ---------------------- | -------------------- | ------------------------------ | ---------------------------- | -| **方式一:一键安装** | 实验室用户、快速体验 | 预打包环境,离线可用,无需配置 | 5-10 分钟 (网络良好的情况下) | -| **方式二:手动安装** | 标准用户、生产环境 | 灵活配置,版本可控 | 10-20 分钟 | -| **方式三:开发者安装** | 开发者、需要修改源码 | 可编辑模式,支持自定义 msgs | 20-30 分钟 | +| 安装方式 | 适用人群 | 推荐安装包 | 特点 | 安装时间 | +| ---------------------- | -------------------- | ----------------- | ------------------------------ | ---------------------------- | +| **方式一:一键安装** | 快速体验、演示 | 预打包环境 | 离线可用,无需配置 | 5-10 分钟 (网络良好的情况下) | +| **方式二:手动安装** | **大多数用户** | `unilabos` | 完整功能,开箱即用 | 10-20 分钟 | +| **方式三:开发者安装** | 开发者、需要修改源码 | `unilabos-env` | 可编辑模式,支持自定义开发 | 20-30 分钟 | +| **仿真/可视化** | 仿真测试、可视化调试 | `unilabos-full` | 含 Gazebo、rviz2、MoveIt | 30-60 分钟 | --- @@ -144,17 +155,38 @@ bash Miniforge3-$(uname)-$(uname -m).sh 使用以下命令创建 Uni-Lab 专用环境: ```bash -mamba create -n unilab python=3.11.11 # 目前ros2组件依赖版本大多为3.11.11 +mamba create -n unilab python=3.11.14 # 目前ros2组件依赖版本大多为3.11.14 mamba activate unilab -mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge + +# 选择安装包(三选一): + +# 方案 A:标准安装(推荐大多数用户) +mamba install uni-lab::unilabos -c robostack-staging -c conda-forge + +# 方案 B:开发者环境(可编辑模式开发) +mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge +# 然后安装 unilabos 和 pip 依赖: +git clone https://github.com/deepmodeling/Uni-Lab-OS.git && cd Uni-Lab-OS +pip install -e . +uv pip install -r unilabos/utils/requirements.txt + +# 方案 C:完整版(含仿真和可视化工具) +mamba install uni-lab::unilabos-full -c robostack-staging -c conda-forge ``` **参数说明**: - `-n unilab`: 创建名为 "unilab" 的环境 -- `uni-lab::unilabos`: 从 uni-lab channel 安装 unilabos 包 +- `uni-lab::unilabos`: 安装 unilabos 完整包,开箱即用(推荐) +- `uni-lab::unilabos-env`: 仅安装环境依赖,适合开发者使用 `pip install -e .` +- `uni-lab::unilabos-full`: 安装完整包(含 ROS2 Desktop、Gazebo、MoveIt 等) - `-c robostack-staging -c conda-forge`: 添加额外的软件源 +**包选择建议**: +- **日常使用/生产部署**:安装 `unilabos`(推荐,完整功能,开箱即用) +- **开发者**:安装 `unilabos-env`,然后使用 `uv pip install -r unilabos/utils/requirements.txt` 安装依赖,再 `pip install -e .` 进行可编辑安装 +- **仿真/可视化**:安装 `unilabos-full`(Gazebo、rviz2、MoveIt) + **如果遇到网络问题**,可以使用清华镜像源加速下载: ```bash @@ -163,8 +195,14 @@ mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/m mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/ -# 然后重新执行安装命令 +# 然后重新执行安装命令(推荐标准安装) mamba create -n unilab uni-lab::unilabos -c robostack-staging + +# 或完整版(仿真/可视化) +mamba create -n unilab uni-lab::unilabos-full -c robostack-staging + +# pip 安装时使用清华镜像(开发者安装时使用) +uv pip install -r unilabos/utils/requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple ``` ### 第三步:激活环境 @@ -203,58 +241,87 @@ cd Uni-Lab-OS cd Uni-Lab-OS ``` -### 第二步:安装基础环境 +### 第二步:安装开发环境(unilabos-env) -**推荐方式**:先通过**方式一(一键安装)**或**方式二(手动安装)**完成基础环境的安装,这将包含所有必需的依赖项(ROS2、msgs 等)。 - -#### 选项 A:通过一键安装(推荐) - -参考上文"方式一:一键安装",完成基础环境的安装后,激活环境: +**重要**:开发者请使用 `unilabos-env` 包,它专为开发者设计: +- 包含 ROS2 核心组件和消息包(ros-humble-ros-core、std-msgs、geometry-msgs 等) +- 包含 transforms3d、cv-bridge、tf2 等 conda 依赖 +- 包含 `uv` 工具,用于快速安装 pip 依赖 +- **不包含** pip 依赖和 unilabos 包(由 `pip install -e .` 和 `uv pip install` 安装) ```bash +# 创建并激活环境 +mamba create -n unilab python=3.11.14 conda activate unilab + +# 安装开发者环境包(ROS2 + conda 依赖 + uv) +mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge ``` -#### 选项 B:通过手动安装 +### 第三步:安装 pip 依赖和可编辑模式安装 -参考上文"方式二:手动安装",创建并安装环境: - -```bash -mamba create -n unilab python=3.11.11 -conda activate unilab -mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge -``` - -**说明**:这会安装包括 Python 3.11.11、ROS2 Humble、ros-humble-unilabos-msgs 和所有必需依赖 - -### 第三步:切换到开发版本 - -现在你已经有了一个完整可用的 Uni-Lab 环境,接下来将 unilabos 包切换为开发版本: +克隆代码并安装依赖: ```bash # 确保环境已激活 conda activate unilab -# 卸载 pip 安装的 unilabos(保留所有 conda 依赖) -pip uninstall unilabos -y - -# 克隆 dev 分支(如果还未克隆) -cd /path/to/your/workspace -git clone -b dev https://github.com/deepmodeling/Uni-Lab-OS.git -# 或者如果已经克隆,切换到 dev 分支 +# 克隆仓库(如果还未克隆) +git clone https://github.com/deepmodeling/Uni-Lab-OS.git cd Uni-Lab-OS + +# 切换到 dev 分支(可选) git checkout dev git pull - -# 以可编辑模式安装开发版 unilabos -pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple ``` -**参数说明**: +**推荐:使用安装脚本**(自动检测中文环境,使用 uv 加速): -- `-e`: editable mode(可编辑模式),代码修改立即生效,无需重新安装 -- `-i`: 使用清华镜像源加速下载 -- `pip uninstall unilabos`: 只卸载 pip 安装的 unilabos 包,不影响 conda 安装的其他依赖(如 ROS2、msgs 等) +```bash +# 自动检测中文环境,如果是中文系统则使用清华镜像 +python scripts/dev_install.py + +# 或者手动指定: +python scripts/dev_install.py --china # 强制使用清华镜像 +python scripts/dev_install.py --no-mirror # 强制使用 PyPI +python scripts/dev_install.py --skip-deps # 跳过 pip 依赖安装 +python scripts/dev_install.py --use-pip # 使用 pip 而非 uv +``` + +**手动安装**(如果脚本安装失败或速度太慢): + +```bash +# 1. 安装 unilabos(可编辑模式) +pip install -e . + +# 2. 使用 uv 安装 pip 依赖(推荐,速度更快) +uv pip install -r unilabos/utils/requirements.txt + +# 国内用户使用清华镜像: +pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple +uv pip install -r unilabos/utils/requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple +``` + +**注意**: +- `uv` 已包含在 `unilabos-env` 中,无需单独安装 +- `unilabos/utils/requirements.txt` 包含运行 unilabos 所需的所有 pip 依赖 +- 部分特殊包(如 pylabrobot)会在运行时由 unilabos 自动检测并安装 + +**为什么使用可编辑模式?** + +- `-e` (editable mode):代码修改**立即生效**,无需重新安装 +- 适合开发调试:修改代码后直接运行测试 +- 与 `unilabos-env` 配合:环境依赖由 conda 管理,unilabos 代码由 pip 管理 + +**验证安装**: + +```bash +# 检查 unilabos 版本 +python -c "import unilabos; print(unilabos.__version__)" + +# 检查安装位置(应该指向你的代码目录) +pip show unilabos | grep Location +``` ### 第四步:安装或自定义 ros-humble-unilabos-msgs(可选) @@ -464,7 +531,45 @@ cd $CONDA_PREFIX/envs/unilab ### 问题 8: 环境很大,有办法减小吗? -**解决方案**: 预打包的环境包含所有依赖,通常较大(压缩后 2-5GB)。这是为了确保离线安装和完整功能。如果空间有限,考虑使用方式二手动安装,只安装需要的组件。 +**解决方案**: + +1. **使用 `unilabos` 标准版**(推荐大多数用户): + ```bash + mamba install uni-lab::unilabos -c robostack-staging -c conda-forge + ``` + 标准版包含完整功能,环境大小约 2-3GB(相比完整版的 8-10GB)。 + +2. **使用 `unilabos-env` 开发者版**(最小化): + ```bash + mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge + # 然后手动安装依赖 + pip install -e . + uv pip install -r unilabos/utils/requirements.txt + ``` + 开发者版只包含环境依赖,体积最小约 2GB。 + +3. **按需安装额外组件**: + 如果后续需要特定功能,可以单独安装: + ```bash + # 需要 Jupyter + mamba install jupyter jupyros + + # 需要可视化 + mamba install matplotlib opencv + + # 需要仿真(注意:这会安装大量依赖) + mamba install ros-humble-gazebo-ros + ``` + +4. **预打包环境问题**: + 预打包环境(方式一)包含所有依赖,通常较大(压缩后 2-5GB)。这是为了确保离线安装和完整功能。 + +**包选择建议**: +| 需求 | 推荐包 | 预估大小 | +|------|--------|----------| +| 日常使用/生产部署 | `unilabos` | ~2-3 GB | +| 开发调试(可编辑模式) | `unilabos-env` | ~2 GB | +| 仿真/可视化 | `unilabos-full` | ~8-10 GB | ### 问题 9: 如何更新到最新版本? @@ -511,6 +616,7 @@ mamba update ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-f **提示**: -- 生产环境推荐使用方式二(手动安装)的稳定版本 -- 开发和测试推荐使用方式三(开发者安装) -- 快速体验和演示推荐使用方式一(一键安装) +- **大多数用户**推荐使用方式二(手动安装)的 `unilabos` 标准版 +- **开发者**推荐使用方式三(开发者安装),安装 `unilabos-env` 后使用 `uv pip install -r unilabos/utils/requirements.txt` 安装依赖 +- **仿真/可视化**推荐安装 `unilabos-full` 完整版 +- **快速体验和演示**推荐使用方式一(一键安装) diff --git a/scripts/create_readme.py b/scripts/create_readme.py index c4f3933..e87c1d8 100644 --- a/scripts/create_readme.py +++ b/scripts/create_readme.py @@ -85,7 +85,7 @@ Verification: ------------- The verify_installation.py script will check: - - Python version (3.11.11) + - Python version (3.11.14) - ROS2 rclpy installation - UniLabOS installation and dependencies @@ -104,7 +104,7 @@ Build Information: Branch: {branch} Platform: {platform} - Python: 3.11.11 + Python: 3.11.14 Date: {build_date} Troubleshooting: diff --git a/scripts/dev_install.py b/scripts/dev_install.py new file mode 100644 index 0000000..002db24 --- /dev/null +++ b/scripts/dev_install.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +""" +Development installation script for UniLabOS. +Auto-detects Chinese locale and uses appropriate mirror. + +Usage: + python scripts/dev_install.py + python scripts/dev_install.py --no-mirror # Force no mirror + python scripts/dev_install.py --china # Force China mirror + python scripts/dev_install.py --skip-deps # Skip pip dependencies installation + +Flow: + 1. pip install -e . (install unilabos in editable mode) + 2. Detect Chinese locale + 3. Use uv to install pip dependencies from requirements.txt + 4. Special packages (like pylabrobot) are handled by environment_check.py at runtime +""" + +import locale +import subprocess +import sys +import argparse +from pathlib import Path + +# Tsinghua mirror URL +TSINGHUA_MIRROR = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" + + +def is_chinese_locale() -> bool: + """ + Detect if system is in Chinese locale. + Same logic as EnvironmentChecker._is_chinese_locale() + """ + try: + lang = locale.getdefaultlocale()[0] + if lang and ("zh" in lang.lower() or "chinese" in lang.lower()): + return True + except Exception: + pass + return False + + +def run_command(cmd: list, description: str, retry: int = 2) -> bool: + """Run command with retry support.""" + print(f"[INFO] {description}") + print(f"[CMD] {' '.join(cmd)}") + + for attempt in range(retry + 1): + try: + result = subprocess.run(cmd, check=True, timeout=600) + print(f"[OK] {description}") + return True + except subprocess.CalledProcessError as e: + if attempt < retry: + print(f"[WARN] Attempt {attempt + 1} failed, retrying...") + else: + print(f"[ERROR] {description} failed: {e}") + return False + except subprocess.TimeoutExpired: + print(f"[ERROR] {description} timed out") + return False + return False + + +def install_editable(project_root: Path, use_mirror: bool) -> bool: + """Install unilabos in editable mode using pip.""" + cmd = [sys.executable, "-m", "pip", "install", "-e", str(project_root)] + if use_mirror: + cmd.extend(["-i", TSINGHUA_MIRROR]) + + return run_command(cmd, "Installing unilabos in editable mode") + + +def install_requirements_uv(requirements_file: Path, use_mirror: bool) -> bool: + """Install pip dependencies using uv (installed via conda-forge::uv).""" + cmd = ["uv", "pip", "install", "-r", str(requirements_file)] + if use_mirror: + cmd.extend(["-i", TSINGHUA_MIRROR]) + + return run_command(cmd, "Installing pip dependencies with uv", retry=2) + + +def install_requirements_pip(requirements_file: Path, use_mirror: bool) -> bool: + """Fallback: Install pip dependencies using pip.""" + cmd = [sys.executable, "-m", "pip", "install", "-r", str(requirements_file)] + if use_mirror: + cmd.extend(["-i", TSINGHUA_MIRROR]) + + return run_command(cmd, "Installing pip dependencies with pip", retry=2) + + +def check_uv_available() -> bool: + """Check if uv is available (installed via conda-forge::uv).""" + try: + subprocess.run(["uv", "--version"], capture_output=True, check=True) + return True + except (subprocess.CalledProcessError, FileNotFoundError): + return False + + +def main(): + parser = argparse.ArgumentParser(description="Development installation script for UniLabOS") + parser.add_argument("--china", action="store_true", help="Force use China mirror (Tsinghua)") + parser.add_argument("--no-mirror", action="store_true", help="Force use default PyPI (no mirror)") + parser.add_argument( + "--skip-deps", action="store_true", help="Skip pip dependencies installation (only install unilabos)" + ) + parser.add_argument("--use-pip", action="store_true", help="Use pip instead of uv for dependencies") + args = parser.parse_args() + + # Determine project root + script_dir = Path(__file__).parent + project_root = script_dir.parent + requirements_file = project_root / "unilabos" / "utils" / "requirements.txt" + + if not (project_root / "setup.py").exists(): + print(f"[ERROR] setup.py not found in {project_root}") + sys.exit(1) + + print("=" * 60) + print("UniLabOS Development Installation") + print("=" * 60) + print(f"Project root: {project_root}") + print() + + # Determine mirror usage based on locale + if args.no_mirror: + use_mirror = False + print("[INFO] Mirror disabled by --no-mirror flag") + elif args.china: + use_mirror = True + print("[INFO] China mirror enabled by --china flag") + else: + use_mirror = is_chinese_locale() + if use_mirror: + print("[INFO] Chinese locale detected, using Tsinghua mirror") + else: + print("[INFO] Non-Chinese locale detected, using default PyPI") + + print() + + # Step 1: Install unilabos in editable mode + print("[STEP 1] Installing unilabos in editable mode...") + if not install_editable(project_root, use_mirror): + print("[ERROR] Failed to install unilabos") + print() + print("Manual fallback:") + if use_mirror: + print(f" pip install -e {project_root} -i {TSINGHUA_MIRROR}") + else: + print(f" pip install -e {project_root}") + sys.exit(1) + + print() + + # Step 2: Install pip dependencies + if args.skip_deps: + print("[INFO] Skipping pip dependencies installation (--skip-deps)") + else: + print("[STEP 2] Installing pip dependencies...") + + if not requirements_file.exists(): + print(f"[WARN] Requirements file not found: {requirements_file}") + print("[INFO] Skipping dependencies installation") + else: + # Try uv first (faster), fallback to pip + if args.use_pip: + print("[INFO] Using pip (--use-pip flag)") + success = install_requirements_pip(requirements_file, use_mirror) + elif check_uv_available(): + print("[INFO] Using uv (installed via conda-forge::uv)") + success = install_requirements_uv(requirements_file, use_mirror) + if not success: + print("[WARN] uv failed, falling back to pip...") + success = install_requirements_pip(requirements_file, use_mirror) + else: + print("[WARN] uv not available (should be installed via: mamba install conda-forge::uv)") + print("[INFO] Falling back to pip...") + success = install_requirements_pip(requirements_file, use_mirror) + + if not success: + print() + print("[WARN] Failed to install some dependencies automatically.") + print("You can manually install them:") + if use_mirror: + print(f" uv pip install -r {requirements_file} -i {TSINGHUA_MIRROR}") + print(" or:") + print(f" pip install -r {requirements_file} -i {TSINGHUA_MIRROR}") + else: + print(f" uv pip install -r {requirements_file}") + print(" or:") + print(f" pip install -r {requirements_file}") + + print() + print("=" * 60) + print("Installation complete!") + print("=" * 60) + print() + print("Note: Some special packages (like pylabrobot) are installed") + print("automatically at runtime by unilabos if needed.") + print() + print("Verify installation:") + print(' python -c "import unilabos; print(unilabos.__version__)"') + print() + print("If you encounter issues, you can manually install dependencies:") + if use_mirror: + print(f" uv pip install -r unilabos/utils/requirements.txt -i {TSINGHUA_MIRROR}") + else: + print(" uv pip install -r unilabos/utils/requirements.txt") + print() + + +if __name__ == "__main__": + main() diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 8cea5f6..063bf7a 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -1,13 +1,11 @@ import argparse import asyncio import os -import shutil import signal import sys import threading import time from typing import Dict, Any, List - import networkx as nx import yaml @@ -17,9 +15,9 @@ unilabos_dir = os.path.dirname(os.path.dirname(current_dir)) if unilabos_dir not in sys.path: sys.path.append(unilabos_dir) +from unilabos.app.utils import cleanup_for_restart from unilabos.utils.banner_print import print_status, print_unilab_banner from unilabos.config.config import load_config, BasicConfig, HTTPConfig -from unilabos.app.utils import cleanup_for_restart # Global restart flags (used by ws_client and web/server) _restart_requested: bool = False diff --git a/unilabos/app/utils.py b/unilabos/app/utils.py index d10c2e0..f6114a1 100644 --- a/unilabos/app/utils.py +++ b/unilabos/app/utils.py @@ -4,8 +4,40 @@ UniLabOS 应用工具函数 提供清理、重启等工具函数 """ -import gc +import glob import os +import shutil +import sys + + +def patch_rclpy_dll_windows(): + """在 Windows + conda 环境下为 rclpy 打 DLL 加载补丁""" + if sys.platform != "win32" or not os.environ.get("CONDA_PREFIX"): + return + try: + import rclpy + + return + except ImportError as e: + if not str(e).startswith("DLL load failed"): + return + cp = os.environ["CONDA_PREFIX"] + impl = os.path.join(cp, "Lib", "site-packages", "rclpy", "impl", "implementation_singleton.py") + pyd = glob.glob(os.path.join(cp, "Lib", "site-packages", "rclpy", "_rclpy_pybind11*.pyd")) + if not os.path.exists(impl) or not pyd: + return + with open(impl, "r", encoding="utf-8") as f: + content = f.read() + lib_bin = os.path.join(cp, "Library", "bin").replace("\\", "/") + patch = f'# UniLabOS DLL Patch\nimport os,ctypes\nos.add_dll_directory("{lib_bin}") if hasattr(os,"add_dll_directory") else None\ntry: ctypes.CDLL("{pyd[0].replace(chr(92),"/")}")\nexcept: pass\n# End Patch\n' + shutil.copy2(impl, impl + ".bak") + with open(impl, "w", encoding="utf-8") as f: + f.write(patch + content) + + +patch_rclpy_dll_windows() + +import gc import threading import time diff --git a/unilabos/utils/requirements.txt b/unilabos/utils/requirements.txt new file mode 100644 index 0000000..86fbef3 --- /dev/null +++ b/unilabos/utils/requirements.txt @@ -0,0 +1,17 @@ +networkx +typing_extensions +websockets +msgcenterpy>=0.1.5 +opentrons_shared_data +pint +fastapi +jinja2 +requests +uvicorn +pyautogui +opcua +pyserial +pandas +crcmod-plus +pymodbus +matplotlib \ No newline at end of file From ab05b858e1a353839382c2eac633feb6eda6d027 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:35:35 +0800 Subject: [PATCH 09/34] Fix Build 1 --- .github/workflows/multi-platform-build.yml | 17 +- unilabos/app/main.py | 20 ++- unilabos/utils/README_LOGGING.md | 187 --------------------- unilabos/utils/environment_check.py | 1 + 4 files changed, 32 insertions(+), 193 deletions(-) delete mode 100644 unilabos/utils/README_LOGGING.md diff --git a/.github/workflows/multi-platform-build.yml b/.github/workflows/multi-platform-build.yml index bcba6db..719d460 100644 --- a/.github/workflows/multi-platform-build.yml +++ b/.github/workflows/multi-platform-build.yml @@ -1,11 +1,16 @@ name: Multi-Platform Conda Build on: + # 在 CI Check 工作流完成后触发(仅限 main/dev 分支) + workflow_run: + workflows: ["CI Check"] + types: + - completed + branches: [main, dev] + # 支持 tag 推送(不依赖 CI Check) push: - branches: [main, dev] tags: ['v*'] - pull_request: - branches: [main, dev] + # 手动触发 workflow_dispatch: inputs: platforms: @@ -20,6 +25,10 @@ on: jobs: build: + # 只有当不是 workflow_run 触发,或者 CI Check 成功时才执行 + if: >- + github.event_name != 'workflow_run' || + github.event.workflow_run.conclusion == 'success' strategy: fail-fast: false matrix: @@ -46,6 +55,8 @@ jobs: steps: - uses: actions/checkout@v4 with: + # 如果是 workflow_run 触发,使用触发 CI Check 的 commit + ref: ${{ github.event.workflow_run.head_sha || github.ref }} fetch-depth: 0 - name: Check if platform should be built diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 063bf7a..2dedc55 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -1,6 +1,7 @@ import argparse import asyncio import os +import shutil import signal import sys import threading @@ -215,7 +216,8 @@ def main(): args_dict = vars(args) # 环境检查 - 检查并自动安装必需的包 (可选) - if not args_dict.get("skip_env_check", False): + skip_env_check = args_dict.get("skip_env_check", False) + if not skip_env_check: from unilabos.utils.environment_check import check_environment if not check_environment(auto_install=True): @@ -226,7 +228,19 @@ def main(): # 加载配置文件,优先加载config,然后从env读取 config_path = args_dict.get("config") - if os.getcwd().endswith("unilabos_data"): + + # 当 skip_env_check 时,默认使用当前目录作为 working_dir + if skip_env_check and not args_dict.get("working_dir") and not config_path: + working_dir = os.path.abspath(os.getcwd()) + print_status(f"跳过环境检查模式:使用当前目录作为工作目录 {working_dir}", "info") + # 检查当前目录是否有 local_config.py + local_config_in_cwd = os.path.join(working_dir, "local_config.py") + if os.path.exists(local_config_in_cwd): + config_path = local_config_in_cwd + print_status(f"发现本地配置文件: {config_path}", "info") + else: + print_status(f"未指定config路径,可通过 --config 传入 local_config.py 文件路径", "info") + elif os.getcwd().endswith("unilabos_data"): working_dir = os.path.abspath(os.getcwd()) else: working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data")) @@ -245,7 +259,7 @@ def main(): 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 config_path and ( + 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") diff --git a/unilabos/utils/README_LOGGING.md b/unilabos/utils/README_LOGGING.md deleted file mode 100644 index 9cb551b..0000000 --- a/unilabos/utils/README_LOGGING.md +++ /dev/null @@ -1,187 +0,0 @@ -# UniLabOS 日志配置说明 - -> **文件位置**: `unilabos/utils/log.py` -> **最后更新**: 2026-01-11 -> **维护者**: Uni-Lab-OS 开发团队 - -本文档说明 UniLabOS 日志系统中对第三方库和内部模块的日志级别配置,避免控制台被过多的 DEBUG 日志淹没。 - ---- - -## 📋 已屏蔽的日志 - -以下库/模块的日志已被设置为 **WARNING** 或 **INFO** 级别,不再显示 DEBUG 日志: - -### 1. pymodbus(Modbus 通信库) - -**配置位置**: `log.py` 第196-200行 - -```python -# pymodbus 库的日志太详细,设置为 WARNING -logging.getLogger('pymodbus').setLevel(logging.WARNING) -logging.getLogger('pymodbus.logging').setLevel(logging.WARNING) -logging.getLogger('pymodbus.logging.base').setLevel(logging.WARNING) -logging.getLogger('pymodbus.logging.decoders').setLevel(logging.WARNING) -``` - -**屏蔽原因**: -- pymodbus 在 DEBUG 级别会输出每一次 Modbus 通信的详细信息 -- 包括 `Processing: 0x5 0x1e 0x0 0x0...` 等原始数据 -- 包括 `decoded PDU function_code(3 sub -1) -> ReadHoldingRegistersResponse(...)` 等解码信息 -- 这些信息对日常使用价值不大,但会快速刷屏 - -**典型被屏蔽的日志**: -``` -[DEBUG] Processing: 0x5 0x1e 0x0 0x0 0x0 0x7 0x1 0x3 0x4 0x0 0x0 0x0 0x0 [handleFrame:72] [pymodbus.logging.base] -[DEBUG] decoded PDU function_code(3 sub -1) -> ReadHoldingRegistersResponse(...) [decode:79] [pymodbus.logging.decoders] -``` - ---- - -### 2. websockets(WebSocket 库) - -**配置位置**: `log.py` 第202-205行 - -```python -# websockets 库的日志输出较多,设置为 WARNING -logging.getLogger('websockets').setLevel(logging.WARNING) -logging.getLogger('websockets.client').setLevel(logging.WARNING) -logging.getLogger('websockets.server').setLevel(logging.WARNING) -``` - -**屏蔽原因**: -- WebSocket 连接、断开、心跳等信息在 DEBUG 级别会频繁输出 -- 对于长时间运行的服务,这些日志意义不大 - ---- - -### 3. ROS Host Node(设备状态更新) - -**配置位置**: `log.py` 第207-208行 - -```python -# ROS 节点的状态更新日志过于频繁,设置为 INFO -logging.getLogger('unilabos.ros.nodes.presets.host_node').setLevel(logging.INFO) -``` - -**屏蔽原因**: -- 设备状态更新(如手套箱压力)每隔几秒就会更新一次 -- DEBUG 日志会记录每一次状态变化,导致日志刷屏 -- 这些频繁的状态更新对调试价值不大 - -**典型被屏蔽的日志**: -``` -[DEBUG] [/devices/host_node] Status updated: BatteryStation.data_glove_box_pressure = 4.229457855224609 [property_callback:666] [unilabos.ros.nodes.presets.host_node] -``` - ---- - -### 4. asyncio 和 urllib3 - -**配置位置**: `log.py` 第224-225行 - -```python -logging.getLogger("asyncio").setLevel(logging.INFO) -logging.getLogger("urllib3").setLevel(logging.INFO) -``` - -**屏蔽原因**: -- asyncio: 异步 IO 的内部调试信息 -- urllib3: HTTP 请求库的连接池、重试等详细信息 - ---- - -## 🔧 如何临时启用这些日志(调试用) - -### 方法1: 修改 log.py(永久启用) - -在 `log.py` 的 `configure_logger()` 函数中,将对应库的日志级别改为 `logging.DEBUG`: - -```python -# 临时启用 pymodbus 的 DEBUG 日志 -logging.getLogger('pymodbus').setLevel(logging.DEBUG) -logging.getLogger('pymodbus.logging').setLevel(logging.DEBUG) -logging.getLogger('pymodbus.logging.base').setLevel(logging.DEBUG) -logging.getLogger('pymodbus.logging.decoders').setLevel(logging.DEBUG) -``` - -### 方法2: 在代码中临时启用(单次调试) - -在需要调试的代码文件中添加: - -```python -import logging - -# 临时启用 pymodbus DEBUG 日志 -logging.getLogger('pymodbus').setLevel(logging.DEBUG) - -# 你的 Modbus 调试代码 -... - -# 调试完成后恢复 -logging.getLogger('pymodbus').setLevel(logging.WARNING) -``` - -### 方法3: 使用环境变量或配置文件(推荐) - -未来可以考虑在启动参数中添加 `--debug-modbus` 等选项来动态控制。 - ---- - -## 📊 日志级别说明 - -| 级别 | 数值 | 用途 | 是否显示 | -|------|------|------|---------| -| TRACE | 5 | 最详细的跟踪信息 | ✅ | -| DEBUG | 10 | 调试信息 | ✅ | -| INFO | 20 | 一般信息 | ✅ | -| WARNING | 30 | 警告信息 | ✅ | -| ERROR | 40 | 错误信息 | ✅ | -| CRITICAL | 50 | 严重错误 | ✅ | - -**当前配置**: -- UniLabOS 自身代码: DEBUG 及以上全部显示 -- pymodbus/websockets: **WARNING** 及以上显示(屏蔽 DEBUG/INFO) -- ROS host_node: **INFO** 及以上显示(屏蔽 DEBUG) - ---- - -## ⚠️ 重要提示 - -### 修改生效时间 -- 修改 `log.py` 后需要 **重启 unilab 服务** 才能生效 -- 不需要重新安装或重新编译 - -### 调试 Modbus 通信问题 -如果需要调试 Modbus 通信故障,应该: -1. 临时启用 pymodbus DEBUG 日志(方法2) -2. 复现问题 -3. 查看详细的通信日志 -4. 调试完成后记得恢复 WARNING 级别 - -### 调试设备状态问题 -如果需要调试设备状态更新问题: -```python -logging.getLogger('unilabos.ros.nodes.presets.host_node').setLevel(logging.DEBUG) -``` - ---- - -## 📝 维护记录 - -| 日期 | 修改内容 | 操作人 | -|------|---------|--------| -| 2026-01-11 | 初始创建,添加 pymodbus、websockets、ROS host_node 屏蔽 | - | -| 2026-01-07 | 添加 pymodbus 和 websockets 屏蔽(log-0107.py) | - | - ---- - -## 🔗 相关文件 - -- `log.py` - 日志配置主文件 -- `unilabos/devices/workstation/coin_cell_assembly/` - 使用 Modbus 的扣电工作站代码 -- `unilabos/ros/nodes/presets/host_node.py` - ROS 主机节点代码 - ---- - -**维护提示**: 如果添加了新的第三方库或发现新的日志刷屏问题,请在此文档中记录并更新 `log.py` 配置。 diff --git a/unilabos/utils/environment_check.py b/unilabos/utils/environment_check.py index 3963b9e..73c0b10 100644 --- a/unilabos/utils/environment_check.py +++ b/unilabos/utils/environment_check.py @@ -24,6 +24,7 @@ class EnvironmentChecker: "msgcenterpy": "msgcenterpy", "opentrons_shared_data": "opentrons_shared_data", "typing_extensions": "typing_extensions", + "crcmod": "crcmod-plus", } # 特殊安装包(需要特殊处理的包) From 6db7fbd7216c40608be3231ae013007de2beef51 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:45:32 +0800 Subject: [PATCH 10/34] Fix Build 2 --- unilabos/app/main.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 2dedc55..a6539c3 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -217,6 +217,8 @@ def main(): # 环境检查 - 检查并自动安装必需的包 (可选) skip_env_check = args_dict.get("skip_env_check", False) + check_mode = args_dict.get("check_mode", False) + if not skip_env_check: from unilabos.utils.environment_check import check_environment @@ -229,6 +231,8 @@ def main(): # 加载配置文件,优先加载config,然后从env读取 config_path = args_dict.get("config") + if check_mode: + args_dict["working_dir"] = os.path.abspath(os.getcwd()) # 当 skip_env_check 时,默认使用当前目录作为 working_dir if skip_env_check and not args_dict.get("working_dir") and not config_path: working_dir = os.path.abspath(os.getcwd()) @@ -273,9 +277,11 @@ def main(): print_status(f"已创建 local_config.py 路径: {config_path}", "info") else: os._exit(1) - # 加载配置文件 + + # 加载配置文件 (check_mode 跳过) print_status(f"当前工作目录为 {working_dir}", "info") - load_config_from_file(config_path) + if not check_mode: + load_config_from_file(config_path) # 根据配置重新设置日志级别 from unilabos.utils.log import configure_logger, logger @@ -331,12 +337,7 @@ def main(): machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name]) BasicConfig.machine_name = machine_name BasicConfig.vis_2d_enable = args_dict["2d_vis"] - - # Check mode 处理 - check_mode = args_dict.get("check_mode", False) BasicConfig.check_mode = check_mode - if check_mode: - print_status("Check mode 启用,将进行 complete_registry 检查", "info") from unilabos.resources.graphio import ( read_node_link_json, From d245ceef1b40119d9661dd848bd0033aef170bf1 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:15:16 +0800 Subject: [PATCH 11/34] Fix Build 3 --- .github/workflows/ci-check.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 2151a66..02f0238 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -8,7 +8,7 @@ on: jobs: registry-check: - runs-on: ubuntu-latest + runs-on: windows-latest defaults: run: @@ -33,8 +33,6 @@ jobs: - name: Install ROS dependencies, uv and unilabos-msgs run: | - # Install all packages together for proper dependency resolution - # Use mamba for faster and more reliable solving mamba install -n check-env \ python=3.11.14 \ conda-forge::uv \ @@ -57,20 +55,13 @@ jobs: - name: Install pip dependencies and unilabos run: | - # Activate the environment - conda activate check-env - # Install pip dependencies from requirements.txt (uv already installed via conda) uv pip install -r unilabos/utils/requirements.txt - # Install special packages (git-based) uv pip install pywinauto git+https://github.com/Xuwznln/pylabrobot.git - # Remove conflicting package uv pip uninstall enum34 || true - # Install unilabos in editable mode uv pip install -e . - name: Run check mode (complete_registry) run: | - conda activate check-env python -m unilabos --check_mode --skip_env_check - name: Check for uncommitted changes From e4cc111523e989fc58d171d2e96b418c6f76eca5 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:19:56 +0800 Subject: [PATCH 12/34] Fix Build 4 --- .github/workflows/ci-check.yml | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 02f0238..f4b60bf 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -12,7 +12,7 @@ jobs: defaults: run: - shell: bash -l {0} + shell: cmd steps: - uses: actions/checkout@v4 @@ -27,44 +27,29 @@ jobs: channels: robostack-staging,conda-forge,uni-lab channel-priority: flexible activate-environment: check-env - auto-activate-base: false auto-update-conda: false show-channel-urls: true - name: Install ROS dependencies, uv and unilabos-msgs run: | - mamba install -n check-env \ - python=3.11.14 \ - conda-forge::uv \ - conda-forge::opencv \ - robostack-staging::ros-humble-ros-core \ - robostack-staging::ros-humble-action-msgs \ - robostack-staging::ros-humble-std-msgs \ - robostack-staging::ros-humble-geometry-msgs \ - robostack-staging::ros-humble-control-msgs \ - robostack-staging::ros-humble-nav2-msgs \ - uni-lab::ros-humble-unilabos-msgs \ - robostack-staging::ros-humble-cv-bridge \ - robostack-staging::ros-humble-vision-opencv \ - robostack-staging::ros-humble-tf-transformations \ - robostack-staging::ros-humble-moveit-msgs \ - robostack-staging::ros-humble-tf2-ros \ - robostack-staging::ros-humble-tf2-ros-py \ - conda-forge::transforms3d \ - -c robostack-staging -c conda-forge -c uni-lab -y + echo Installing ROS dependencies... + mamba install -n check-env conda-forge::uv conda-forge::opencv robostack-staging::ros-humble-ros-core robostack-staging::ros-humble-action-msgs robostack-staging::ros-humble-std-msgs robostack-staging::ros-humble-geometry-msgs robostack-staging::ros-humble-control-msgs robostack-staging::ros-humble-nav2-msgs uni-lab::ros-humble-unilabos-msgs robostack-staging::ros-humble-cv-bridge robostack-staging::ros-humble-vision-opencv robostack-staging::ros-humble-tf-transformations robostack-staging::ros-humble-moveit-msgs robostack-staging::ros-humble-tf2-ros robostack-staging::ros-humble-tf2-ros-py conda-forge::transforms3d -c robostack-staging -c conda-forge -c uni-lab -y - name: Install pip dependencies and unilabos run: | + echo Installing pip dependencies... uv pip install -r unilabos/utils/requirements.txt uv pip install pywinauto git+https://github.com/Xuwznln/pylabrobot.git - uv pip uninstall enum34 || true + uv pip uninstall enum34 || echo enum34 not installed, skipping uv pip install -e . - name: Run check mode (complete_registry) run: | + echo Running check mode... python -m unilabos --check_mode --skip_env_check - name: Check for uncommitted changes + shell: bash run: | if ! git diff --exit-code; then echo "::error::检测到文件变化!请先在本地运行 'python -m unilabos --complete_registry' 并提交变更" From 329349639e102511bbdf63f5737b9554875d0c4e Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:25:34 +0800 Subject: [PATCH 13/34] Fix Build 5 --- .github/workflows/ci-check.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index f4b60bf..5895a7c 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -37,6 +37,7 @@ jobs: - name: Install pip dependencies and unilabos run: | + call conda activate check-env echo Installing pip dependencies... uv pip install -r unilabos/utils/requirements.txt uv pip install pywinauto git+https://github.com/Xuwznln/pylabrobot.git @@ -45,6 +46,7 @@ jobs: - name: Run check mode (complete_registry) run: | + call conda activate check-env echo Running check mode... python -m unilabos --check_mode --skip_env_check From f2a96b20414cbbec5288760bb2fe2e3e7564e4f6 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:36:35 +0800 Subject: [PATCH 14/34] Fix Build 6 --- .github/workflows/ci-check.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 5895a7c..65edb1f 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -10,6 +10,11 @@ jobs: registry-check: runs-on: windows-latest + env: + # Fix Unicode encoding issue on Windows runner (cp1252 -> utf-8) + PYTHONIOENCODING: utf-8 + PYTHONUTF8: 1 + defaults: run: shell: cmd From e1555d10a087979e5eae6428a10d47f3963cce22 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:14:31 +0800 Subject: [PATCH 15/34] Fix Build 7 --- .github/workflows/multi-platform-build.yml | 33 ++++++++++++++++--- .github/workflows/unilabos-conda-build.yml | 37 +++++++++++++++++----- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/.github/workflows/multi-platform-build.yml b/.github/workflows/multi-platform-build.yml index 719d460..697fa3b 100644 --- a/.github/workflows/multi-platform-build.yml +++ b/.github/workflows/multi-platform-build.yml @@ -22,13 +22,37 @@ on: required: false default: false type: boolean + skip_ci_check: + description: '跳过等待 CI Check (手动触发时可选)' + required: false + default: false + type: boolean jobs: + # 等待 CI Check 完成的 job (仅用于 workflow_run 触发) + wait-for-ci: + runs-on: ubuntu-latest + if: github.event_name == 'workflow_run' + outputs: + should_continue: ${{ steps.check.outputs.should_continue }} + steps: + - name: Check CI status + id: check + run: | + if [[ "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then + echo "should_continue=true" >> $GITHUB_OUTPUT + echo "CI Check passed, proceeding with build" + else + echo "should_continue=false" >> $GITHUB_OUTPUT + echo "CI Check did not succeed (status: ${{ github.event.workflow_run.conclusion }}), skipping build" + fi + build: - # 只有当不是 workflow_run 触发,或者 CI Check 成功时才执行 - if: >- - github.event_name != 'workflow_run' || - github.event.workflow_run.conclusion == 'success' + needs: [wait-for-ci] + # 运行条件:workflow_run 触发且 CI 成功,或者其他触发方式 + if: | + always() && + (needs.wait-for-ci.result == 'skipped' || needs.wait-for-ci.outputs.should_continue == 'true') strategy: fail-fast: false matrix: @@ -80,7 +104,6 @@ jobs: channels: conda-forge,robostack-staging,defaults channel-priority: strict activate-environment: build-env - auto-activate-base: false auto-update-conda: false show-channel-urls: true diff --git a/.github/workflows/unilabos-conda-build.yml b/.github/workflows/unilabos-conda-build.yml index 2ace422..df7efeb 100644 --- a/.github/workflows/unilabos-conda-build.yml +++ b/.github/workflows/unilabos-conda-build.yml @@ -26,17 +26,37 @@ on: required: false default: false type: boolean + skip_ci_check: + description: '跳过等待 CI Check (手动触发时可选)' + required: false + default: false + type: boolean jobs: + # 等待 CI Check 完成的 job (仅用于 workflow_run 触发) + wait-for-ci: + runs-on: ubuntu-latest + if: github.event_name == 'workflow_run' + outputs: + should_continue: ${{ steps.check.outputs.should_continue }} + steps: + - name: Check CI status + id: check + run: | + if [[ "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then + echo "should_continue=true" >> $GITHUB_OUTPUT + echo "CI Check passed, proceeding with build" + else + echo "should_continue=false" >> $GITHUB_OUTPUT + echo "CI Check did not succeed (status: ${{ github.event.workflow_run.conclusion }}), skipping build" + fi + build: - # 只在以下情况运行: - # 1. workflow_run 触发且 CI Check 成功 - # 2. 标签推送(发布版本) - # 3. 手动触发 + needs: [wait-for-ci] + # 运行条件:workflow_run 触发且 CI 成功,或者其他触发方式 if: | - github.event_name == 'workflow_dispatch' || - github.event_name == 'push' || - (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') + always() && + (needs.wait-for-ci.result == 'skipped' || needs.wait-for-ci.outputs.should_continue == 'true') strategy: fail-fast: false matrix: @@ -59,6 +79,8 @@ jobs: steps: - uses: actions/checkout@v4 with: + # 如果是 workflow_run 触发,使用触发 CI Check 的 commit + ref: ${{ github.event.workflow_run.head_sha || github.ref }} fetch-depth: 0 - name: Check if platform should be built @@ -82,7 +104,6 @@ jobs: channels: conda-forge,robostack-staging,uni-lab,defaults channel-priority: strict activate-environment: build-env - auto-activate-base: false auto-update-conda: false show-channel-urls: true From 039c96fe01bfa3bb2790ad68db9014913b070d43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:30:22 +0800 Subject: [PATCH 16/34] ci(deps): bump actions/configure-pages from 4 to 5 (#222) Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 4 to 5. - [Release notes](https://github.com/actions/configure-pages/releases) - [Commits](https://github.com/actions/configure-pages/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/configure-pages dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index cf2d338..7f78b19 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -84,7 +84,7 @@ jobs: - name: Setup Pages id: pages - uses: actions/configure-pages@v4 + uses: actions/configure-pages@v5 if: | github.event.workflow_run.head_branch == 'main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true') From 1bcdad9448d9c026168b837eaf582b75cd7a3686 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:30:31 +0800 Subject: [PATCH 17/34] ci(deps): bump actions/upload-artifact from 4 to 6 (#224) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/conda-pack-build.yml | 2 +- .github/workflows/multi-platform-build.yml | 2 +- .github/workflows/unilabos-conda-build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/conda-pack-build.yml b/.github/workflows/conda-pack-build.yml index 6476be9..0be4cf1 100644 --- a/.github/workflows/conda-pack-build.yml +++ b/.github/workflows/conda-pack-build.yml @@ -312,7 +312,7 @@ jobs: - name: Upload distribution package if: steps.should_build.outputs.should_build == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }} path: dist-package/ diff --git a/.github/workflows/multi-platform-build.yml b/.github/workflows/multi-platform-build.yml index 697fa3b..b3c1983 100644 --- a/.github/workflows/multi-platform-build.yml +++ b/.github/workflows/multi-platform-build.yml @@ -149,7 +149,7 @@ jobs: - name: Upload conda package artifacts if: steps.should_build.outputs.should_build == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: conda-package-${{ matrix.platform }} path: conda-packages-temp diff --git a/.github/workflows/unilabos-conda-build.yml b/.github/workflows/unilabos-conda-build.yml index df7efeb..53ad958 100644 --- a/.github/workflows/unilabos-conda-build.yml +++ b/.github/workflows/unilabos-conda-build.yml @@ -167,7 +167,7 @@ jobs: - name: Upload conda package artifacts if: steps.should_build.outputs.should_build == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: conda-package-unilabos-${{ matrix.platform }} path: conda-packages-temp From 67ddee2ab2f9fc67a23ebff192f45013ed20cb7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:30:38 +0800 Subject: [PATCH 18/34] ci(deps): bump actions/upload-pages-artifact from 3 to 4 (#225) Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 4. - [Release notes](https://github.com/actions/upload-pages-artifact/releases) - [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/upload-pages-artifact dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 7f78b19..11c7337 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -105,7 +105,7 @@ jobs: test -f docs/_build/html/index.html && echo "✓ index.html exists" || echo "✗ index.html missing" - name: Upload build artifacts - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 if: | github.event.workflow_run.head_branch == 'main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true') From 4b0d1553e94a1393fcb3394ea53cffa65d325e92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:30:47 +0800 Subject: [PATCH 19/34] ci(deps): bump actions/checkout from 4 to 6 (#223) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-check.yml | 2 +- .github/workflows/conda-pack-build.yml | 2 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/multi-platform-build.yml | 2 +- .github/workflows/unilabos-conda-build.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 65edb1f..33bceaf 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -20,7 +20,7 @@ jobs: shell: cmd steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/conda-pack-build.yml b/.github/workflows/conda-pack-build.yml index 0be4cf1..ed45db9 100644 --- a/.github/workflows/conda-pack-build.yml +++ b/.github/workflows/conda-pack-build.yml @@ -62,7 +62,7 @@ jobs: echo "should_build=false" >> $GITHUB_OUTPUT fi - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 if: steps.should_build.outputs.should_build == 'true' with: ref: ${{ github.event.inputs.branch }} diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 11c7337..f3ac4d1 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -44,7 +44,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: # workflow_run 时使用触发工作流的分支,手动触发时使用输入的分支 ref: ${{ github.event.workflow_run.head_branch || github.event.inputs.branch || github.ref }} diff --git a/.github/workflows/multi-platform-build.yml b/.github/workflows/multi-platform-build.yml index b3c1983..4e1cf4f 100644 --- a/.github/workflows/multi-platform-build.yml +++ b/.github/workflows/multi-platform-build.yml @@ -77,7 +77,7 @@ jobs: shell: bash -l {0} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: # 如果是 workflow_run 触发,使用触发 CI Check 的 commit ref: ${{ github.event.workflow_run.head_sha || github.ref }} diff --git a/.github/workflows/unilabos-conda-build.yml b/.github/workflows/unilabos-conda-build.yml index 53ad958..5529eeb 100644 --- a/.github/workflows/unilabos-conda-build.yml +++ b/.github/workflows/unilabos-conda-build.yml @@ -77,7 +77,7 @@ jobs: shell: bash -l {0} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: # 如果是 workflow_run 触发,使用触发 CI Check 的 commit ref: ${{ github.event.workflow_run.head_sha || github.ref }} From b2f26ffb2827934913d6ac941f346845d07b8542 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:39:15 +0800 Subject: [PATCH 20/34] Fix Build 8 --- .conda/environment/recipe.yaml | 39 ++++++++++++++++++++++ .github/workflows/unilabos-conda-build.yml | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 .conda/environment/recipe.yaml diff --git a/.conda/environment/recipe.yaml b/.conda/environment/recipe.yaml new file mode 100644 index 0000000..8038379 --- /dev/null +++ b/.conda/environment/recipe.yaml @@ -0,0 +1,39 @@ +# unilabos-env: conda environment dependencies (ROS2 + conda packages) + +package: + name: unilabos-env + version: 0.10.16 + +build: + noarch: generic + +requirements: + run: + # Python + - zstd + - zstandard + - conda-forge::python ==3.11.14 + - conda-forge::opencv + # ROS2 dependencies (from ci-check.yml) + - robostack-staging::ros-humble-ros-core + - robostack-staging::ros-humble-action-msgs + - robostack-staging::ros-humble-std-msgs + - robostack-staging::ros-humble-geometry-msgs + - robostack-staging::ros-humble-control-msgs + - robostack-staging::ros-humble-nav2-msgs + - robostack-staging::ros-humble-cv-bridge + - robostack-staging::ros-humble-vision-opencv + - robostack-staging::ros-humble-tf-transformations + - robostack-staging::ros-humble-moveit-msgs + - robostack-staging::ros-humble-tf2-ros + - robostack-staging::ros-humble-tf2-ros-py + - conda-forge::transforms3d + - conda-forge::uv + + # UniLabOS custom messages + - uni-lab::ros-humble-unilabos-msgs + +about: + repository: https://github.com/deepmodeling/Uni-Lab-OS + license: GPL-3.0-only + description: "UniLabOS Environment - ROS2 and conda dependencies (for developers: pip install -e .)" diff --git a/.github/workflows/unilabos-conda-build.yml b/.github/workflows/unilabos-conda-build.yml index 5529eeb..dbfad80 100644 --- a/.github/workflows/unilabos-conda-build.yml +++ b/.github/workflows/unilabos-conda-build.yml @@ -131,7 +131,7 @@ jobs: if: steps.should_build.outputs.should_build == 'true' run: | echo "Building unilabos-env (conda environment dependencies)..." - rattler-build build -r .conda/env/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge + rattler-build build -r .conda/environment/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge - name: Build unilabos (with pip package) if: steps.should_build.outputs.should_build == 'true' From 9feeb0c4309c79709efba31a604eee9633bcb63a Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:51:40 +0800 Subject: [PATCH 21/34] Fix Build 9 --- .conda/base/recipe.yaml | 1 + unilabos/utils/requirements.txt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.conda/base/recipe.yaml b/.conda/base/recipe.yaml index 40c29fc..2008b4b 100644 --- a/.conda/base/recipe.yaml +++ b/.conda/base/recipe.yaml @@ -52,6 +52,7 @@ requirements: - pandas - pymodbus - matplotlib + - pylibftdi - uni-lab::unilabos-env ==0.10.16 about: diff --git a/unilabos/utils/requirements.txt b/unilabos/utils/requirements.txt index 86fbef3..65d724f 100644 --- a/unilabos/utils/requirements.txt +++ b/unilabos/utils/requirements.txt @@ -14,4 +14,5 @@ pyserial pandas crcmod-plus pymodbus -matplotlib \ No newline at end of file +matplotlib +pylibftdi \ No newline at end of file From 467d75dc03e814700c92f96952cf896c961a4541 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:41:06 +0800 Subject: [PATCH 22/34] Fix Build 10 --- .conda/base/recipe.yaml | 2 ++ .github/workflows/unilabos-conda-build.yml | 36 +++++++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.conda/base/recipe.yaml b/.conda/base/recipe.yaml index 2008b4b..26d95b8 100644 --- a/.conda/base/recipe.yaml +++ b/.conda/base/recipe.yaml @@ -29,6 +29,8 @@ build: - uv pip install $SRC_DIR requirements: + build: + - conda-forge::uv host: - python ==3.11.14 - pip diff --git a/.github/workflows/unilabos-conda-build.yml b/.github/workflows/unilabos-conda-build.yml index dbfad80..d116a67 100644 --- a/.github/workflows/unilabos-conda-build.yml +++ b/.github/workflows/unilabos-conda-build.yml @@ -133,12 +133,29 @@ jobs: echo "Building unilabos-env (conda environment dependencies)..." rattler-build build -r .conda/environment/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge + - name: Upload unilabos-env to Anaconda.org (if enabled) + if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true' + run: | + echo "Uploading unilabos-env to uni-lab organization..." + for package in $(find ./output -name "unilabos-env*.conda"); do + anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package" + done + - name: Build unilabos (with pip package) if: steps.should_build.outputs.should_build == 'true' run: | echo "Building unilabos package..." + # 如果已上传到 Anaconda,从 uni-lab channel 获取 unilabos-env;否则从本地 output 获取 rattler-build build -r .conda/base/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output + - name: Upload unilabos to Anaconda.org (if enabled) + if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true' + run: | + echo "Uploading unilabos to uni-lab organization..." + for package in $(find ./output -name "unilabos-0*.conda" -o -name "unilabos-[0-9]*.conda"); do + anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package" + done + - name: Build unilabos-full - Only when explicitly requested if: | steps.should_build.outputs.should_build == 'true' && @@ -147,6 +164,17 @@ jobs: echo "Building unilabos-full package on ${{ matrix.platform }}..." rattler-build build -r .conda/full/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output + - name: Upload unilabos-full to Anaconda.org (if enabled) + if: | + steps.should_build.outputs.should_build == 'true' && + github.event.inputs.build_full == 'true' && + github.event.inputs.upload_to_anaconda == 'true' + run: | + echo "Uploading unilabos-full to uni-lab organization..." + for package in $(find ./output -name "unilabos-full*.conda"); do + anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package" + done + - name: List built packages if: steps.should_build.outputs.should_build == 'true' run: | @@ -173,11 +201,3 @@ jobs: path: conda-packages-temp if-no-files-found: warn retention-days: 30 - - - name: Upload to Anaconda.org (uni-lab organization) - if: github.event.inputs.upload_to_anaconda == 'true' - run: | - for package in $(find ./output -name "*.conda"); do - echo "Uploading $package to uni-lab organization..." - anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package" - done From e70a5bea6667a3fb8f7cd17f967758ff35f4cc96 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:09:39 +0800 Subject: [PATCH 23/34] Fix Build 11 --- .conda/base/recipe.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.conda/base/recipe.yaml b/.conda/base/recipe.yaml index 26d95b8..4a5a900 100644 --- a/.conda/base/recipe.yaml +++ b/.conda/base/recipe.yaml @@ -26,11 +26,9 @@ build: - cp $RECIPE_DIR/../../MANIFEST.in $SRC_DIR - cp $RECIPE_DIR/../../setup.cfg $SRC_DIR - cp $RECIPE_DIR/../../setup.py $SRC_DIR - - uv pip install $SRC_DIR + - pip install $SRC_DIR requirements: - build: - - conda-forge::uv host: - python ==3.11.14 - pip From d48e77c9ae1bd9b3c6b8bb90783cf2a28332ae1f Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:16:21 +0800 Subject: [PATCH 24/34] Fix Build 12 --- .conda/full/recipe.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.conda/full/recipe.yaml b/.conda/full/recipe.yaml index 137d9db..51197d0 100644 --- a/.conda/full/recipe.yaml +++ b/.conda/full/recipe.yaml @@ -1,4 +1,4 @@ -# unilabos-full: Full package with all features +# unilabos-full: Full package with all features # Depends on unilabos + complete ROS2 desktop + dev tools package: From 27c0544bfc6a36336241c38132c0250ccb6897d5 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:36:42 +0800 Subject: [PATCH 25/34] Fix Build 13 --- unilabos/devices/Qone_nmr/__init__.py | 0 unilabos/devices/neware_battery_test_system/__init__.py | 0 .../devices/workstation/bioyond_studio/bioyond_cell/__init__.py | 0 .../workstation/bioyond_studio/dispensing_station/__init__.py | 0 .../workstation/bioyond_studio/reaction_station/__init__.py | 0 unilabos/devices/xrd_d7mate/__init__.py | 0 unilabos/devices/zhida_hplc/__init__.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 unilabos/devices/Qone_nmr/__init__.py create mode 100644 unilabos/devices/neware_battery_test_system/__init__.py create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/__init__.py create mode 100644 unilabos/devices/workstation/bioyond_studio/dispensing_station/__init__.py create mode 100644 unilabos/devices/workstation/bioyond_studio/reaction_station/__init__.py create mode 100644 unilabos/devices/xrd_d7mate/__init__.py create mode 100644 unilabos/devices/zhida_hplc/__init__.py diff --git a/unilabos/devices/Qone_nmr/__init__.py b/unilabos/devices/Qone_nmr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unilabos/devices/neware_battery_test_system/__init__.py b/unilabos/devices/neware_battery_test_system/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/__init__.py b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unilabos/devices/workstation/bioyond_studio/dispensing_station/__init__.py b/unilabos/devices/workstation/bioyond_studio/dispensing_station/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unilabos/devices/workstation/bioyond_studio/reaction_station/__init__.py b/unilabos/devices/workstation/bioyond_studio/reaction_station/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unilabos/devices/xrd_d7mate/__init__.py b/unilabos/devices/xrd_d7mate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unilabos/devices/zhida_hplc/__init__.py b/unilabos/devices/zhida_hplc/__init__.py new file mode 100644 index 0000000..e69de29 From bf980d7248d43102dec366f143b7b523b149d369 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:41:12 +0800 Subject: [PATCH 26/34] v0.10.17 (cherry picked from commit 176de521b44693a890c9d676d31aee0027442724) --- .conda/base/recipe.yaml | 4 ++-- .conda/environment/recipe.yaml | 2 +- .conda/full/recipe.yaml | 4 ++-- recipes/msgs/recipe.yaml | 2 +- recipes/unilabos/recipe.yaml | 2 +- setup.py | 2 +- unilabos/__init__.py | 2 +- unilabos_msgs/package.xml | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.conda/base/recipe.yaml b/.conda/base/recipe.yaml index 4a5a900..cec00e2 100644 --- a/.conda/base/recipe.yaml +++ b/.conda/base/recipe.yaml @@ -3,7 +3,7 @@ package: name: unilabos - version: 0.10.16 + version: 0.10.17 source: path: ../../unilabos @@ -53,7 +53,7 @@ requirements: - pymodbus - matplotlib - pylibftdi - - uni-lab::unilabos-env ==0.10.16 + - uni-lab::unilabos-env ==0.10.17 about: repository: https://github.com/deepmodeling/Uni-Lab-OS diff --git a/.conda/environment/recipe.yaml b/.conda/environment/recipe.yaml index 8038379..bd3eecc 100644 --- a/.conda/environment/recipe.yaml +++ b/.conda/environment/recipe.yaml @@ -2,7 +2,7 @@ package: name: unilabos-env - version: 0.10.16 + version: 0.10.17 build: noarch: generic diff --git a/.conda/full/recipe.yaml b/.conda/full/recipe.yaml index 51197d0..037f5b4 100644 --- a/.conda/full/recipe.yaml +++ b/.conda/full/recipe.yaml @@ -3,7 +3,7 @@ package: name: unilabos-full - version: 0.10.16 + version: 0.10.17 build: noarch: generic @@ -11,7 +11,7 @@ build: requirements: run: # Base unilabos package (includes unilabos-env) - - uni-lab::unilabos ==0.10.16 + - uni-lab::unilabos ==0.10.17 # Documentation tools - sphinx - sphinx_rtd_theme diff --git a/recipes/msgs/recipe.yaml b/recipes/msgs/recipe.yaml index 168f427..f78df2e 100644 --- a/recipes/msgs/recipe.yaml +++ b/recipes/msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.10.16 + version: 0.10.17 source: path: ../../unilabos_msgs target_directory: src diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index 641d5b1..feca503 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.10.16" + version: "0.10.17" source: path: ../.. diff --git a/setup.py b/setup.py index d286cf8..b3a00f1 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ package_name = 'unilabos' setup( name=package_name, - version='0.10.16', + version='0.10.17', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/unilabos/__init__.py b/unilabos/__init__.py index c69a94b..50ab2b0 100644 --- a/unilabos/__init__.py +++ b/unilabos/__init__.py @@ -1 +1 @@ -__version__ = "0.10.16" +__version__ = "0.10.17" diff --git a/unilabos_msgs/package.xml b/unilabos_msgs/package.xml index 42d295a..68ad132 100644 --- a/unilabos_msgs/package.xml +++ b/unilabos_msgs/package.xml @@ -2,7 +2,7 @@ unilabos_msgs - 0.10.16 + 0.10.17 ROS2 Messages package for unilabos devices Junhan Chang Xuwznln From f7d2cb4b9edb3304190e26dcc417a7225c4afaad Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:59:06 +0800 Subject: [PATCH 27/34] CI Check use production mode --- .conda/environment/recipe.yaml | 2 +- .github/workflows/ci-check.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.conda/environment/recipe.yaml b/.conda/environment/recipe.yaml index bd3eecc..3f8df0f 100644 --- a/.conda/environment/recipe.yaml +++ b/.conda/environment/recipe.yaml @@ -36,4 +36,4 @@ requirements: about: repository: https://github.com/deepmodeling/Uni-Lab-OS license: GPL-3.0-only - description: "UniLabOS Environment - ROS2 and conda dependencies (for developers: pip install -e .)" + description: "UniLabOS Environment - ROS2 and conda dependencies" diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 33bceaf..57245d9 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -47,7 +47,7 @@ jobs: uv pip install -r unilabos/utils/requirements.txt uv pip install pywinauto git+https://github.com/Xuwznln/pylabrobot.git uv pip uninstall enum34 || echo enum34 not installed, skipping - uv pip install -e . + uv pip install . - name: Run check mode (complete_registry) run: | From 7eacae64424e5590215e3c92929b6e427163a043 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 28 Jan 2026 01:05:32 +0800 Subject: [PATCH 28/34] Fix OT2 & ReAdd Virtual Devices --- .conda/base/recipe.yaml | 1 - unilabos/registry/devices/virtual_device.yaml | 5794 +++++++++++++++++ unilabos/resources/graphio.py | 2 + 3 files changed, 5796 insertions(+), 1 deletion(-) diff --git a/.conda/base/recipe.yaml b/.conda/base/recipe.yaml index cec00e2..3c5bb88 100644 --- a/.conda/base/recipe.yaml +++ b/.conda/base/recipe.yaml @@ -41,7 +41,6 @@ requirements: - networkx - typing_extensions - websockets - - opentrons_shared_data - pint - fastapi - jinja2 diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index b1b1ab6..c38655c 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -1,3 +1,5797 @@ +virtual_centrifuge: + category: + - virtual_device + class: + action_value_mappings: + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: object + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + centrifuge: + feedback: + current_speed: current_speed + current_status: status + current_temp: current_temp + progress: progress + goal: + speed: speed + temp: temp + time: time + vessel: vessel + goal_default: + speed: 0.0 + temp: 0.0 + time: 0.0 + vessel: + 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: '' + handles: {} + result: + message: message + success: success + schema: + description: '' + properties: + feedback: + properties: + current_speed: + type: number + current_status: + type: string + current_temp: + type: number + progress: + type: number + required: + - progress + - current_speed + - current_temp + - current_status + title: Centrifuge_Feedback + type: object + goal: + properties: + speed: + type: number + temp: + type: number + time: + type: number + vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: vessel + type: object + required: + - vessel + - speed + - time + - temp + title: Centrifuge_Goal + type: object + result: + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: Centrifuge_Result + type: object + required: + - goal + title: Centrifuge + type: object + type: Centrifuge + module: unilabos.devices.virtual.virtual_centrifuge:VirtualCentrifuge + status_types: + centrifuge_state: str + current_speed: float + current_temp: float + max_speed: float + max_temp: float + message: str + min_temp: float + progress: float + status: str + target_speed: float + target_temp: float + time_remaining: float + type: python + config_info: [] + description: Virtual Centrifuge for CentrifugeProtocol Testing + handles: + - data_key: vessel + data_source: handle + data_type: transport + description: 需要离心的样品容器 + handler_key: centrifuge + io_type: target + label: centrifuge + side: NORTH + icon: '' + init_param_schema: + config: + properties: + config: + type: string + device_id: + type: string + required: [] + type: object + data: + properties: + centrifuge_state: + type: string + current_speed: + type: number + current_temp: + type: number + max_speed: + type: number + max_temp: + type: number + message: + type: string + min_temp: + type: number + progress: + type: number + status: + type: string + target_speed: + type: number + target_temp: + type: number + time_remaining: + type: number + required: + - status + - centrifuge_state + - current_speed + - target_speed + - current_temp + - target_temp + - max_speed + - max_temp + - min_temp + - time_remaining + - progress + - message + type: object + version: 1.0.0 +virtual_column: + category: + - virtual_device + class: + action_value_mappings: + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: object + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + run_column: + feedback: + current_status: current_status + processed_volume: processed_volume + progress: progress + goal: + column: column + from_vessel: from_vessel + to_vessel: to_vessel + goal_default: + column: '' + from_vessel: + 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: '' + pct1: '' + pct2: '' + ratio: '' + rf: '' + solvent1: '' + solvent2: '' + to_vessel: + 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: '' + handles: {} + result: + message: current_status + return_info: current_status + success: success + schema: + description: '' + properties: + feedback: + properties: + progress: + type: number + status: + type: string + required: + - status + - progress + title: RunColumn_Feedback + type: object + goal: + properties: + column: + type: string + from_vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: from_vessel + type: object + pct1: + type: string + pct2: + type: string + ratio: + type: string + rf: + type: string + solvent1: + type: string + solvent2: + type: string + to_vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: to_vessel + type: object + required: + - from_vessel + - to_vessel + - column + - rf + - pct1 + - pct2 + - solvent1 + - solvent2 + - ratio + title: RunColumn_Goal + type: object + result: + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: RunColumn_Result + type: object + required: + - goal + title: RunColumn + type: object + type: RunColumn + module: unilabos.devices.virtual.virtual_column:VirtualColumn + status_types: + column_diameter: float + column_length: float + column_state: str + current_flow_rate: float + current_phase: str + current_status: str + final_volume: float + max_flow_rate: float + processed_volume: float + progress: float + status: str + type: python + config_info: [] + description: Virtual Column Chromatography Device for RunColumn Protocol Testing + handles: + - data_key: from_vessel + data_source: handle + data_type: transport + description: 样品输入口 + handler_key: columnin + io_type: target + label: columnin + side: WEST + - data_key: to_vessel + data_source: handle + data_type: transport + description: 产物输出口 + handler_key: columnout + io_type: source + label: columnout + side: EAST + icon: '' + init_param_schema: + config: + properties: + config: + type: object + device_id: + type: string + required: [] + type: object + data: + properties: + column_diameter: + type: number + column_length: + type: number + column_state: + type: string + current_flow_rate: + type: number + current_phase: + type: string + current_status: + type: string + final_volume: + type: number + max_flow_rate: + type: number + processed_volume: + type: number + progress: + type: number + status: + type: string + required: + - status + - column_state + - current_flow_rate + - max_flow_rate + - column_length + - column_diameter + - processed_volume + - progress + - current_status + - current_phase + - final_volume + type: object + version: 1.0.0 +virtual_filter: + category: + - virtual_device + class: + action_value_mappings: + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: object + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + filter: + feedback: + current_status: current_status + current_temp: current_temp + filtered_volume: filtered_volume + progress: progress + goal: + continue_heatchill: continue_heatchill + filtrate_vessel: filtrate_vessel + stir: stir + stir_speed: stir_speed + temp: temp + vessel: vessel + volume: volume + goal_default: + continue_heatchill: false + filtrate_vessel: + 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: '' + stir: false + stir_speed: 0.0 + temp: 0.0 + vessel: + 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: '' + volume: 0.0 + handles: {} + result: + message: message + return_info: message + success: success + schema: + description: '' + properties: + feedback: + properties: + current_status: + type: string + current_temp: + type: number + filtered_volume: + type: number + progress: + type: number + required: + - progress + - current_temp + - filtered_volume + - current_status + title: Filter_Feedback + type: object + goal: + properties: + continue_heatchill: + type: boolean + filtrate_vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: filtrate_vessel + type: object + stir: + type: boolean + stir_speed: + type: number + temp: + type: number + vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: vessel + type: object + volume: + type: number + required: + - vessel + - filtrate_vessel + - stir + - stir_speed + - temp + - continue_heatchill + - volume + title: Filter_Goal + type: object + result: + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: Filter_Result + type: object + required: + - goal + title: Filter + type: object + type: Filter + module: unilabos.devices.virtual.virtual_filter:VirtualFilter + status_types: + current_status: str + current_temp: float + filtered_volume: float + max_stir_speed: float + max_temp: float + max_volume: float + message: str + progress: float + status: str + type: python + config_info: [] + description: Virtual Filter for FilterProtocol Testing + handles: + - data_key: vessel_in + data_source: handle + data_type: transport + description: 需要过滤的样品容器 + handler_key: filterin + io_type: target + label: filter_in + side: NORTH + - data_key: filtrate_out + data_source: handle + data_type: transport + description: 滤液出口 + handler_key: filtrateout + io_type: source + label: filtrate_out + side: SOUTH + - data_key: retentate_out + data_source: handle + data_type: transport + description: 滤渣/固体出口 + handler_key: retentateout + io_type: source + label: retentate_out + side: EAST + icon: '' + init_param_schema: + config: + properties: + config: + type: string + device_id: + type: string + required: [] + type: object + data: + properties: + current_status: + type: string + current_temp: + type: number + filtered_volume: + type: number + max_stir_speed: + type: number + max_temp: + type: number + max_volume: + type: number + message: + type: string + progress: + type: number + status: + type: string + required: + - status + - progress + - current_temp + - current_status + - filtered_volume + - message + - max_temp + - max_stir_speed + - max_volume + type: object + version: 1.0.0 +virtual_gas_source: + category: + - virtual_device + class: + action_value_mappings: + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + auto-is_closed: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: is_closed的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: is_closed参数 + type: object + type: UniLabJsonCommand + auto-is_open: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: is_open的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: is_open参数 + type: object + type: UniLabJsonCommand + close: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: EmptyIn_Feedback + type: object + goal: + properties: {} + required: [] + title: EmptyIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: EmptyIn_Result + type: object + required: + - goal + title: EmptyIn + type: object + type: EmptyIn + open: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: EmptyIn_Feedback + type: object + goal: + properties: {} + required: [] + title: EmptyIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: EmptyIn_Result + type: object + required: + - goal + title: EmptyIn + type: object + type: EmptyIn + set_status: + feedback: {} + goal: + string: string + goal_default: + string: '' + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: StrSingleInput_Feedback + type: object + goal: + properties: + string: + type: string + required: + - string + title: StrSingleInput_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: StrSingleInput_Result + type: object + required: + - goal + title: StrSingleInput + type: object + type: StrSingleInput + module: unilabos.devices.virtual.virtual_gas_source:VirtualGasSource + status_types: + status: str + type: python + config_info: [] + description: Virtual gas source + handles: + - data_key: fluid_out + data_source: executor + data_type: fluid + description: 气源出气口 + handler_key: gassource + io_type: source + label: gassource + side: SOUTH + icon: '' + init_param_schema: + config: + properties: + config: + type: string + device_id: + type: string + required: [] + type: object + data: + properties: + status: + type: string + required: + - status + type: object + version: 1.0.0 +virtual_heatchill: + category: + - virtual_device + class: + action_value_mappings: + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: object + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + heat_chill: + feedback: + status: status + goal: + purpose: purpose + stir: stir + stir_speed: stir_speed + temp: temp + time: time + vessel: vessel + goal_default: + pressure: '' + purpose: '' + reflux_solvent: '' + stir: false + stir_speed: 0.0 + temp: 0.0 + temp_spec: '' + time: '' + time_spec: '' + vessel: + 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: '' + handles: {} + result: + success: success + schema: + description: '' + properties: + feedback: + properties: + status: + type: string + required: + - status + title: HeatChill_Feedback + type: object + goal: + properties: + pressure: + type: string + purpose: + type: string + reflux_solvent: + type: string + stir: + type: boolean + stir_speed: + type: number + temp: + type: number + temp_spec: + type: string + time: + type: string + time_spec: + type: string + vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: vessel + type: object + required: + - vessel + - temp + - time + - temp_spec + - time_spec + - pressure + - reflux_solvent + - stir + - stir_speed + - purpose + title: HeatChill_Goal + type: object + result: + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: HeatChill_Result + type: object + required: + - goal + title: HeatChill + type: object + type: HeatChill + heat_chill_start: + feedback: + status: status + goal: + purpose: purpose + temp: temp + vessel: vessel + goal_default: + purpose: '' + temp: 0.0 + vessel: + 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: '' + handles: {} + result: + success: success + schema: + description: '' + properties: + feedback: + properties: + status: + type: string + required: + - status + title: HeatChillStart_Feedback + type: object + goal: + properties: + purpose: + type: string + temp: + type: number + vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: vessel + type: object + required: + - vessel + - temp + - purpose + title: HeatChillStart_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: HeatChillStart_Result + type: object + required: + - goal + title: HeatChillStart + type: object + type: HeatChillStart + heat_chill_stop: + feedback: + status: status + goal: + vessel: vessel + goal_default: + vessel: + 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: '' + handles: {} + result: + success: success + schema: + description: '' + properties: + feedback: + properties: + status: + type: string + required: + - status + title: HeatChillStop_Feedback + type: object + goal: + properties: + vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: vessel + type: object + required: + - vessel + title: HeatChillStop_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: HeatChillStop_Result + type: object + required: + - goal + title: HeatChillStop + type: object + type: HeatChillStop + module: unilabos.devices.virtual.virtual_heatchill:VirtualHeatChill + status_types: + is_stirring: bool + max_stir_speed: float + max_temp: float + min_temp: float + operation_mode: str + progress: float + remaining_time: float + status: str + stir_speed: float + type: python + config_info: [] + description: Virtual HeatChill for HeatChillProtocol Testing + handles: + - data_key: vessel + data_source: handle + data_type: mechanical + description: 加热/冷却器的物理连接口 + handler_key: heatchill + io_type: source + label: heatchill + side: NORTH + icon: Heater.webp + init_param_schema: + config: + properties: + config: + type: object + device_id: + type: string + required: [] + type: object + data: + properties: + is_stirring: + type: boolean + max_stir_speed: + type: number + max_temp: + type: number + min_temp: + type: number + operation_mode: + type: string + progress: + type: number + remaining_time: + type: number + status: + type: string + stir_speed: + type: number + required: + - status + - operation_mode + - is_stirring + - stir_speed + - remaining_time + - progress + - max_temp + - min_temp + - max_stir_speed + type: object + version: 1.0.0 +virtual_multiway_valve: + category: + - virtual_device + class: + action_value_mappings: + auto-close: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: close的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: close参数 + type: object + type: UniLabJsonCommand + auto-is_at_port: + feedback: {} + goal: {} + goal_default: + port_number: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: is_at_port的参数schema + properties: + feedback: {} + goal: + properties: + port_number: + type: integer + required: + - port_number + type: object + result: {} + required: + - goal + title: is_at_port参数 + type: object + type: UniLabJsonCommand + auto-is_at_position: + feedback: {} + goal: {} + goal_default: + position: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: is_at_position的参数schema + properties: + feedback: {} + goal: + properties: + position: + type: integer + required: + - position + type: object + result: {} + required: + - goal + title: is_at_position参数 + type: object + type: UniLabJsonCommand + auto-is_at_pump_position: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: is_at_pump_position的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: is_at_pump_position参数 + type: object + type: UniLabJsonCommand + auto-open: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: open参数 + type: object + type: UniLabJsonCommand + auto-reset: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: reset的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: reset参数 + type: object + type: UniLabJsonCommand + auto-set_to_port: + feedback: {} + goal: {} + goal_default: + port_number: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: set_to_port的参数schema + properties: + feedback: {} + goal: + properties: + port_number: + type: integer + required: + - port_number + type: object + result: {} + required: + - goal + title: set_to_port参数 + type: object + type: UniLabJsonCommand + auto-set_to_pump_position: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: set_to_pump_position的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: set_to_pump_position参数 + type: object + type: UniLabJsonCommand + auto-switch_between_pump_and_port: + feedback: {} + goal: {} + goal_default: + port_number: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: switch_between_pump_and_port的参数schema + properties: + feedback: {} + goal: + properties: + port_number: + type: integer + required: + - port_number + type: object + result: {} + required: + - goal + title: switch_between_pump_and_port参数 + type: object + type: UniLabJsonCommand + set_position: + feedback: {} + goal: + command: command + goal_default: + command: '' + handles: {} + result: + success: success + schema: + description: '' + properties: + feedback: + properties: + status: + type: string + required: + - status + title: SendCmd_Feedback + type: object + goal: + properties: + command: + type: string + required: + - command + title: SendCmd_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: SendCmd_Result + type: object + required: + - goal + title: SendCmd + type: object + type: SendCmd + set_valve_position: + feedback: {} + goal: + command: command + goal_default: + command: '' + handles: {} + result: + success: success + schema: + description: '' + properties: + feedback: + properties: + status: + type: string + required: + - status + title: SendCmd_Feedback + type: object + goal: + properties: + command: + type: string + required: + - command + title: SendCmd_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: SendCmd_Result + type: object + required: + - goal + title: SendCmd + type: object + type: SendCmd + module: unilabos.devices.virtual.virtual_multiway_valve:VirtualMultiwayValve + status_types: + current_port: str + current_position: int + flow_path: str + status: str + target_position: int + valve_position: int + valve_state: str + type: python + config_info: [] + description: Virtual 8-Way Valve for flow direction control + handles: + - data_key: fluid_in + data_source: handle + data_type: fluid + description: 八通阀门进液口 + handler_key: transferpump + io_type: target + label: transferpump + side: NORTH + - data_key: fluid_port_1 + data_source: executor + data_type: fluid + description: 八通阀门端口1 + handler_key: '1' + io_type: source + label: '1' + side: NORTH + - data_key: fluid_port_2 + data_source: executor + data_type: fluid + description: 八通阀门端口2 + handler_key: '2' + io_type: source + label: '2' + side: EAST + - data_key: fluid_port_3 + data_source: executor + data_type: fluid + description: 八通阀门端口3 + handler_key: '3' + io_type: source + label: '3' + side: EAST + - data_key: fluid_port_4 + data_source: executor + data_type: fluid + description: 八通阀门端口4 + handler_key: '4' + io_type: source + label: '4' + side: SOUTH + - data_key: fluid_port_5 + data_source: executor + data_type: fluid + description: 八通阀门端口5 + handler_key: '5' + io_type: source + label: '5' + side: SOUTH + - data_key: fluid_port_6 + data_source: executor + data_type: fluid + description: 八通阀门端口6 + handler_key: '6' + io_type: source + label: '6' + side: WEST + - data_key: fluid_port_7 + data_source: executor + data_type: fluid + description: 八通阀门端口7 + handler_key: '7' + io_type: source + label: '7' + side: WEST + - data_key: fluid_port_8 + data_source: executor + data_type: fluid + description: 八通阀门端口8-特殊输入 + handler_key: '8' + io_type: target + label: '8' + side: WEST + - data_key: fluid_port_8 + data_source: executor + data_type: fluid + description: 八通阀门端口8 + handler_key: '8' + io_type: source + label: '8' + side: NORTH + icon: EightPipeline.webp + init_param_schema: + config: + properties: + port: + default: VIRTUAL + type: string + positions: + default: 8 + type: integer + required: [] + type: object + data: + properties: + current_port: + type: string + current_position: + type: integer + flow_path: + type: string + status: + type: string + target_position: + type: integer + valve_position: + type: integer + valve_state: + type: string + required: + - status + - valve_state + - current_position + - target_position + - current_port + - valve_position + - flow_path + type: object + version: 1.0.0 +virtual_rotavap: + category: + - virtual_device + class: + action_value_mappings: + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: object + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + evaporate: + feedback: + current_device: current_device + status: status + goal: + pressure: pressure + stir_speed: stir_speed + temp: temp + time: time + vessel: vessel + goal_default: + pressure: 0.0 + solvent: '' + stir_speed: 0.0 + temp: 0.0 + time: '' + vessel: + 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: '' + handles: {} + result: + message: message + success: success + schema: + description: '' + properties: + feedback: + properties: + current_device: + type: string + status: + type: string + time_remaining: + properties: + nanosec: + maximum: 4294967295 + minimum: 0 + type: integer + sec: + maximum: 2147483647 + minimum: -2147483648 + type: integer + required: + - sec + - nanosec + title: time_remaining + type: object + time_spent: + properties: + nanosec: + maximum: 4294967295 + minimum: 0 + type: integer + sec: + maximum: 2147483647 + minimum: -2147483648 + type: integer + required: + - sec + - nanosec + title: time_spent + type: object + required: + - status + - current_device + - time_spent + - time_remaining + title: Evaporate_Feedback + type: object + goal: + properties: + pressure: + type: number + solvent: + type: string + stir_speed: + type: number + temp: + type: number + time: + type: string + vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: vessel + type: object + required: + - vessel + - pressure + - temp + - time + - stir_speed + - solvent + title: Evaporate_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: Evaporate_Result + type: object + required: + - goal + title: Evaporate + type: object + type: Evaporate + module: unilabos.devices.virtual.virtual_rotavap:VirtualRotavap + status_types: + current_temp: float + evaporated_volume: float + max_rotation_speed: float + max_temp: float + message: str + progress: float + remaining_time: float + rotation_speed: float + rotavap_state: str + status: str + vacuum_pressure: float + type: python + config_info: [] + description: Virtual Rotary Evaporator for EvaporateProtocol Testing + handles: + - data_key: vessel_in + data_source: handle + data_type: fluid + description: 样品连接口 + handler_key: samplein + io_type: target + label: sample_in + side: NORTH + - data_key: product_out + data_source: handle + data_type: fluid + description: 浓缩产物出口 + handler_key: productout + io_type: source + label: product_out + side: SOUTH + - data_key: solvent_out + data_source: handle + data_type: fluid + description: 冷凝溶剂出口 + handler_key: solventout + io_type: source + label: solvent_out + side: EAST + icon: Rotaryevaporator.webp + init_param_schema: + config: + properties: + config: + type: string + device_id: + type: string + required: [] + type: object + data: + properties: + current_temp: + type: number + evaporated_volume: + type: number + max_rotation_speed: + type: number + max_temp: + type: number + message: + type: string + progress: + type: number + remaining_time: + type: number + rotation_speed: + type: number + rotavap_state: + type: string + status: + type: string + vacuum_pressure: + type: number + required: + - status + - rotavap_state + - current_temp + - rotation_speed + - vacuum_pressure + - evaporated_volume + - progress + - message + - max_temp + - max_rotation_speed + - remaining_time + type: object + version: 1.0.0 +virtual_separator: + category: + - virtual_device + class: + action_value_mappings: + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: object + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + separate: + feedback: + current_status: status + progress: progress + goal: + from_vessel: from_vessel + product_phase: product_phase + purpose: purpose + repeats: repeats + separation_vessel: separation_vessel + settling_time: settling_time + solvent: solvent + solvent_volume: solvent_volume + stir_speed: stir_speed + stir_time: stir_time + through: through + to_vessel: to_vessel + waste_phase_to_vessel: waste_phase_to_vessel + goal_default: + from_vessel: + 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: '' + product_phase: '' + product_vessel: + 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: '' + purpose: '' + repeats: 0 + separation_vessel: + 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: '' + settling_time: 0.0 + solvent: '' + solvent_volume: '' + stir_speed: 0.0 + stir_time: 0.0 + through: '' + to_vessel: + 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: '' + vessel: + 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: '' + volume: '' + waste_phase_to_vessel: + 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: '' + waste_vessel: + 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: '' + handles: {} + result: + message: message + success: success + schema: + description: '' + properties: + feedback: + properties: + progress: + type: number + status: + type: string + required: + - status + - progress + title: Separate_Feedback + type: object + goal: + properties: + from_vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: from_vessel + type: object + product_phase: + type: string + product_vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: product_vessel + type: object + purpose: + type: string + repeats: + maximum: 2147483647 + minimum: -2147483648 + type: integer + separation_vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: separation_vessel + type: object + settling_time: + type: number + solvent: + type: string + solvent_volume: + type: string + stir_speed: + type: number + stir_time: + type: number + through: + type: string + to_vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: to_vessel + type: object + vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: vessel + type: object + volume: + type: string + waste_phase_to_vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: waste_phase_to_vessel + type: object + waste_vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: waste_vessel + type: object + required: + - vessel + - purpose + - product_phase + - from_vessel + - separation_vessel + - to_vessel + - waste_phase_to_vessel + - product_vessel + - waste_vessel + - solvent + - solvent_volume + - volume + - through + - repeats + - stir_time + - stir_speed + - settling_time + title: Separate_Goal + type: object + result: + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: Separate_Result + type: object + required: + - goal + title: Separate + type: object + type: Separate + module: unilabos.devices.virtual.virtual_separator:VirtualSeparator + status_types: + has_phases: bool + message: str + phase_separation: bool + progress: float + separator_state: str + settling_time: float + status: str + stir_speed: float + volume: float + type: python + config_info: [] + description: Virtual Separator for SeparateProtocol Testing + handles: + - data_key: from_vessel + data_source: handle + data_type: fluid + description: 需要分离的混合液体输入口 + handler_key: separatorin + io_type: target + label: separator_in + side: NORTH + - data_key: bottom_outlet + data_source: executor + data_type: fluid + description: 下相(重相)液体输出口 + handler_key: bottomphaseout + io_type: source + label: bottom_phase_out + side: SOUTH + - data_key: mechanical_port + data_source: handle + data_type: mechanical + description: 用于连接搅拌器等机械设备的接口 + handler_key: bind + io_type: target + label: bind + side: WEST + icon: Separator.webp + init_param_schema: + config: + properties: + config: + type: string + device_id: + type: string + required: [] + type: object + data: + properties: + has_phases: + type: boolean + message: + type: string + phase_separation: + type: boolean + progress: + type: number + separator_state: + type: string + settling_time: + type: number + status: + type: string + stir_speed: + type: number + volume: + type: number + required: + - status + - separator_state + - volume + - has_phases + - phase_separation + - stir_speed + - settling_time + - progress + - message + type: object + version: 1.0.0 +virtual_solenoid_valve: + category: + - virtual_device + class: + action_value_mappings: + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + auto-is_closed: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: is_closed的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: is_closed参数 + type: object + type: UniLabJsonCommand + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: object + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + auto-reset: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: reset的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: reset参数 + type: object + type: UniLabJsonCommandAsync + auto-toggle: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: toggle的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: toggle参数 + type: object + type: UniLabJsonCommand + close: + feedback: {} + goal: + command: CLOSED + goal_default: {} + handles: {} + result: + success: success + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: EmptyIn_Feedback + type: object + goal: + properties: {} + required: [] + title: EmptyIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: EmptyIn_Result + type: object + required: + - goal + title: EmptyIn + type: object + type: EmptyIn + open: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: EmptyIn_Feedback + type: object + goal: + properties: {} + required: [] + title: EmptyIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: EmptyIn_Result + type: object + required: + - goal + title: EmptyIn + type: object + type: EmptyIn + set_status: + feedback: {} + goal: + string: string + goal_default: + string: '' + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: StrSingleInput_Feedback + type: object + goal: + properties: + string: + type: string + required: + - string + title: StrSingleInput_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: StrSingleInput_Result + type: object + required: + - goal + title: StrSingleInput + type: object + type: StrSingleInput + set_valve_position: + feedback: {} + goal: + command: command + goal_default: + command: '' + handles: {} + result: + message: message + success: success + valve_position: valve_position + schema: + description: '' + properties: + feedback: + properties: + status: + type: string + required: + - status + title: SendCmd_Feedback + type: object + goal: + properties: + command: + type: string + required: + - command + title: SendCmd_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: SendCmd_Result + type: object + required: + - goal + title: SendCmd + type: object + type: SendCmd + module: unilabos.devices.virtual.virtual_solenoid_valve:VirtualSolenoidValve + status_types: + is_open: bool + status: str + valve_position: str + valve_state: str + type: python + config_info: [] + description: Virtual Solenoid Valve for simple on/off flow control + handles: + - data_key: fluid_port_in + data_source: handle + data_type: fluid + description: 电磁阀的进液口 + handler_key: in + io_type: target + label: in + side: NORTH + - data_key: fluid_port_out + data_source: handle + data_type: fluid + description: 电磁阀的出液口 + handler_key: out + io_type: source + label: out + side: SOUTH + icon: '' + init_param_schema: + config: + properties: + config: + type: object + device_id: + type: string + required: [] + type: object + data: + properties: + is_open: + type: boolean + status: + type: string + valve_position: + type: string + valve_state: + type: string + required: + - status + - valve_state + - is_open + - valve_position + type: object + version: 1.0.0 +virtual_solid_dispenser: + category: + - virtual_device + class: + action_value_mappings: + add_solid: + feedback: + current_status: status + progress: progress + goal: + equiv: equiv + event: event + mass: mass + mol: mol + purpose: purpose + rate_spec: rate_spec + ratio: ratio + reagent: reagent + vessel: vessel + goal_default: + amount: '' + equiv: '' + event: '' + mass: '' + mol: '' + purpose: '' + rate_spec: '' + ratio: '' + reagent: '' + stir: false + stir_speed: 0.0 + time: '' + vessel: + 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: '' + viscous: false + volume: '' + handles: {} + result: + message: message + return_info: return_info + success: success + schema: + description: '' + properties: + feedback: + properties: + current_status: + type: string + progress: + type: number + required: + - progress + - current_status + title: Add_Feedback + type: object + goal: + properties: + amount: + type: string + equiv: + type: string + event: + type: string + mass: + type: string + mol: + type: string + purpose: + type: string + rate_spec: + type: string + ratio: + type: string + reagent: + type: string + stir: + type: boolean + stir_speed: + type: number + time: + type: string + vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: vessel + type: object + viscous: + type: boolean + volume: + type: string + required: + - vessel + - reagent + - volume + - mass + - amount + - time + - stir + - stir_speed + - viscous + - purpose + - event + - mol + - rate_spec + - equiv + - ratio + title: Add_Goal + type: object + result: + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: Add_Result + type: object + required: + - goal + title: Add + type: object + type: Add + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-find_solid_reagent_bottle: + feedback: {} + goal: {} + goal_default: + reagent_name: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + reagent_name: + type: string + required: + - reagent_name + type: object + result: {} + required: + - goal + title: find_solid_reagent_bottle参数 + type: object + type: UniLabJsonCommand + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + auto-parse_mass_string: + feedback: {} + goal: {} + goal_default: + mass_str: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + mass_str: + type: string + required: + - mass_str + type: object + result: {} + required: + - goal + title: parse_mass_string参数 + type: object + type: UniLabJsonCommand + auto-parse_mol_string: + feedback: {} + goal: {} + goal_default: + mol_str: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + mol_str: + type: string + required: + - mol_str + type: object + result: {} + required: + - goal + title: parse_mol_string参数 + type: object + type: UniLabJsonCommand + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: object + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser + status_types: + current_reagent: str + dispensed_amount: float + status: str + total_operations: int + type: python + config_info: [] + description: Virtual Solid Dispenser for Add Protocol Testing - supports mass and + molar additions + handles: + - data_key: solid_out + data_source: executor + data_type: resource + description: 固体试剂输出口 + handler_key: SolidOut + io_type: source + label: SolidOut + side: SOUTH + - data_key: solid_in + data_source: handle + data_type: resource + description: 固体试剂输入口(连接试剂瓶) + handler_key: SolidIn + io_type: target + label: SolidIn + side: NORTH + icon: '' + init_param_schema: + config: + properties: + config: + type: object + device_id: + type: string + required: [] + type: object + data: + properties: + current_reagent: + type: string + dispensed_amount: + type: number + status: + type: string + total_operations: + type: integer + required: + - status + - current_reagent + - dispensed_amount + - total_operations + type: object + version: 1.0.0 +virtual_stirrer: + category: + - virtual_device + class: + action_value_mappings: + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: object + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + start_stir: + feedback: + status: status + goal: + purpose: purpose + stir_speed: stir_speed + vessel: vessel + goal_default: + purpose: '' + stir_speed: 0.0 + vessel: + 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: '' + handles: {} + result: + success: success + schema: + description: '' + properties: + feedback: + properties: + current_speed: + type: number + current_status: + type: string + progress: + type: number + required: + - progress + - current_speed + - current_status + title: StartStir_Feedback + type: object + goal: + properties: + purpose: + type: string + stir_speed: + type: number + vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: vessel + type: object + required: + - vessel + - stir_speed + - purpose + title: StartStir_Goal + type: object + result: + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: StartStir_Result + type: object + required: + - goal + title: StartStir + type: object + type: StartStir + stir: + feedback: + status: status + goal: + settling_time: settling_time + stir_speed: stir_speed + stir_time: stir_time + goal_default: + event: '' + settling_time: '' + stir_speed: 0.0 + stir_time: 0.0 + time: '' + time_spec: '' + vessel: + 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: '' + handles: {} + result: + success: success + schema: + description: '' + properties: + feedback: + properties: + status: + type: string + required: + - status + title: Stir_Feedback + type: object + goal: + properties: + event: + type: string + settling_time: + type: string + stir_speed: + type: number + stir_time: + type: number + time: + type: string + time_spec: + type: string + vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: vessel + type: object + required: + - vessel + - time + - event + - time_spec + - stir_time + - stir_speed + - settling_time + title: Stir_Goal + type: object + result: + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: Stir_Result + type: object + required: + - goal + title: Stir + type: object + type: Stir + stop_stir: + feedback: + status: status + goal: + vessel: vessel + goal_default: + vessel: + 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: '' + handles: {} + result: + success: success + schema: + description: '' + properties: + feedback: + properties: + current_status: + type: string + progress: + type: number + required: + - progress + - current_status + title: StopStir_Feedback + type: object + goal: + properties: + vessel: + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: vessel + type: object + required: + - vessel + title: StopStir_Goal + type: object + result: + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: StopStir_Result + type: object + required: + - goal + title: StopStir + type: object + type: StopStir + module: unilabos.devices.virtual.virtual_stirrer:VirtualStirrer + status_types: + current_speed: float + current_vessel: str + device_info: dict + is_stirring: bool + max_speed: float + min_speed: float + operation_mode: str + remaining_time: float + status: str + type: python + config_info: [] + description: Virtual Stirrer for StirProtocol Testing + handles: + - data_key: vessel + data_source: handle + data_type: mechanical + description: 搅拌器的机械连接口 + handler_key: stirrer + io_type: source + label: stirrer + side: NORTH + icon: Stirrer.webp + init_param_schema: + config: + properties: + config: + type: object + device_id: + type: string + required: [] + type: object + data: + properties: + current_speed: + type: number + current_vessel: + type: string + device_info: + type: object + is_stirring: + type: boolean + max_speed: + type: number + min_speed: + type: number + operation_mode: + type: string + remaining_time: + type: number + status: + type: string + required: + - status + - operation_mode + - current_vessel + - current_speed + - is_stirring + - remaining_time + - max_speed + - min_speed + - device_info + type: object + version: 1.0.0 +virtual_transfer_pump: + category: + - virtual_device + class: + action_value_mappings: + auto-aspirate: + feedback: {} + goal: {} + goal_default: + velocity: null + volume: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: aspirate的参数schema + properties: + feedback: {} + goal: + properties: + velocity: + type: number + volume: + type: number + required: + - volume + type: object + result: {} + required: + - goal + title: aspirate参数 + type: object + type: UniLabJsonCommandAsync + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-dispense: + feedback: {} + goal: {} + goal_default: + velocity: null + volume: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: dispense的参数schema + properties: + feedback: {} + goal: + properties: + velocity: + type: number + volume: + type: number + required: + - volume + type: object + result: {} + required: + - goal + title: dispense参数 + type: object + type: UniLabJsonCommandAsync + auto-empty_syringe: + feedback: {} + goal: {} + goal_default: + velocity: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: empty_syringe的参数schema + properties: + feedback: {} + goal: + properties: + velocity: + type: number + required: [] + type: object + result: {} + required: + - goal + title: empty_syringe参数 + type: object + type: UniLabJsonCommandAsync + auto-fill_syringe: + feedback: {} + goal: {} + goal_default: + velocity: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: fill_syringe的参数schema + properties: + feedback: {} + goal: + properties: + velocity: + type: number + required: [] + type: object + result: {} + required: + - goal + title: fill_syringe参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + auto-is_empty: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: is_empty的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: is_empty参数 + type: object + type: UniLabJsonCommand + auto-is_full: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: is_full的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: is_full参数 + type: object + type: UniLabJsonCommand + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: object + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + auto-pull_plunger: + feedback: {} + goal: {} + goal_default: + velocity: null + volume: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: pull_plunger的参数schema + properties: + feedback: {} + goal: + properties: + velocity: + type: number + volume: + type: number + required: + - volume + type: object + result: {} + required: + - goal + title: pull_plunger参数 + type: object + type: UniLabJsonCommandAsync + auto-push_plunger: + feedback: {} + goal: {} + goal_default: + velocity: null + volume: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: push_plunger的参数schema + properties: + feedback: {} + goal: + properties: + velocity: + type: number + volume: + type: number + required: + - volume + type: object + result: {} + required: + - goal + title: push_plunger参数 + type: object + type: UniLabJsonCommandAsync + auto-set_max_velocity: + feedback: {} + goal: {} + goal_default: + velocity: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: set_max_velocity的参数schema + properties: + feedback: {} + goal: + properties: + velocity: + type: number + required: + - velocity + type: object + result: {} + required: + - goal + title: set_max_velocity参数 + type: object + type: UniLabJsonCommand + auto-stop_operation: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: stop_operation的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: stop_operation参数 + type: object + type: UniLabJsonCommandAsync + set_position: + feedback: + current_position: current_position + progress: progress + status: status + goal: + max_velocity: max_velocity + position: position + goal_default: + max_velocity: 0.0 + position: 0.0 + handles: {} + result: + message: message + success: success + schema: + description: '' + properties: + feedback: + properties: + current_position: + type: number + progress: + type: number + status: + type: string + required: + - status + - current_position + - progress + title: SetPumpPosition_Feedback + type: object + goal: + properties: + max_velocity: + type: number + position: + type: number + required: + - position + - max_velocity + title: SetPumpPosition_Goal + type: object + result: + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + - message + title: SetPumpPosition_Result + type: object + required: + - goal + title: SetPumpPosition + type: object + type: SetPumpPosition + transfer: + feedback: + current_status: current_status + progress: progress + transferred_volume: transferred_volume + goal: + amount: amount + from_vessel: from_vessel + rinsing_repeats: rinsing_repeats + rinsing_solvent: rinsing_solvent + rinsing_volume: rinsing_volume + solid: solid + time: time + to_vessel: to_vessel + viscous: viscous + volume: volume + goal_default: + amount: '' + from_vessel: '' + rinsing_repeats: 0 + rinsing_solvent: '' + rinsing_volume: 0.0 + solid: false + time: 0.0 + to_vessel: '' + viscous: false + volume: 0.0 + handles: {} + result: + message: message + success: success + schema: + description: '' + properties: + feedback: + properties: + current_status: + type: string + progress: + type: number + transferred_volume: + type: number + required: + - progress + - transferred_volume + - current_status + title: Transfer_Feedback + type: object + goal: + properties: + amount: + type: string + from_vessel: + type: string + rinsing_repeats: + maximum: 2147483647 + minimum: -2147483648 + type: integer + rinsing_solvent: + type: string + rinsing_volume: + type: number + solid: + type: boolean + time: + type: number + to_vessel: + type: string + viscous: + type: boolean + volume: + type: number + required: + - from_vessel + - to_vessel + - volume + - amount + - time + - viscous + - rinsing_solvent + - rinsing_volume + - rinsing_repeats + - solid + title: Transfer_Goal + type: object + result: + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: Transfer_Result + type: object + required: + - goal + title: Transfer + type: object + type: Transfer + module: unilabos.devices.virtual.virtual_transferpump:VirtualTransferPump + status_types: + current_volume: float + max_velocity: float + position: float + remaining_capacity: float + status: str + transfer_rate: float + type: python + config_info: [] + description: Virtual Transfer Pump for TransferProtocol Testing (Syringe-style) + handles: + - data_key: fluid_port + data_source: handle + data_type: fluid + description: 注射器式转移泵的连接口 + handler_key: transferpump + io_type: source + label: transferpump + side: SOUTH + icon: Pump.webp + init_param_schema: + config: + properties: + config: + type: object + device_id: + type: string + required: [] + type: object + data: + properties: + current_volume: + type: number + max_velocity: + type: number + position: + type: number + remaining_capacity: + type: number + status: + type: string + transfer_rate: + type: number + required: + - status + - position + - current_volume + - max_velocity + - transfer_rate + - remaining_capacity + type: object + version: 1.0.0 +virtual_vacuum_pump: + category: + - virtual_device + class: + action_value_mappings: + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + auto-is_closed: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: is_closed的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: is_closed参数 + type: object + type: UniLabJsonCommand + auto-is_open: + feedback: {} + goal: {} + goal_default: {} + handles: {} + placeholder_keys: {} + result: {} + schema: + description: is_open的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: is_open参数 + type: object + type: UniLabJsonCommand + close: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: EmptyIn_Feedback + type: object + goal: + properties: {} + required: [] + title: EmptyIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: EmptyIn_Result + type: object + required: + - goal + title: EmptyIn + type: object + type: EmptyIn + open: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: EmptyIn_Feedback + type: object + goal: + properties: {} + required: [] + title: EmptyIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: EmptyIn_Result + type: object + required: + - goal + title: EmptyIn + type: object + type: EmptyIn + set_status: + feedback: {} + goal: + string: string + goal_default: + string: '' + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: StrSingleInput_Feedback + type: object + goal: + properties: + string: + type: string + required: + - string + title: StrSingleInput_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: StrSingleInput_Result + type: object + required: + - goal + title: StrSingleInput + type: object + type: StrSingleInput + module: unilabos.devices.virtual.virtual_vacuum_pump:VirtualVacuumPump + status_types: + status: str + type: python + config_info: [] + description: Virtual vacuum pump + handles: + - data_key: fluid_in + data_source: handle + data_type: fluid + description: 真空泵进气口 + handler_key: vacuumpump + io_type: source + label: vacuumpump + side: SOUTH + icon: Vacuum.webp + init_param_schema: + config: + properties: + config: + type: string + device_id: + type: string + required: [] + type: object + data: + properties: + status: + type: string + required: + - status + type: object + version: 1.0.0 virtual_workbench: category: - virtual_device diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 1c514d4..8233ae7 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -597,6 +597,8 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w "tube": "tube", "bottle_carrier": "bottle_carrier", "plate_adapter": "plate_adapter", + "electrode_sheet": "electrode_sheet", + "material_hole": "material_hole", } if source in replace_info: return replace_info[source] From 0ae94f7f3cea7ef0f28eaf91c2b2ad50b7e9fe27 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:21:43 +0800 Subject: [PATCH 29/34] add msg goal --- unilabos/ros/nodes/presets/host_node.py | 1 + 1 file changed, 1 insertion(+) diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 9a27e04..e95b393 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -808,6 +808,7 @@ class HostNode(BaseROS2DeviceNode): goal_msg = convert_to_ros_msg(action_client._action_type.Goal(), action_kwargs) self.lab_logger().info(f"[Host Node] Sending goal for {action_id}: {str(goal_msg)[:1000]}") + self.lab_logger().trace(f"[Host Node] Sending goal for {action_id}: {action_kwargs}") self.lab_logger().trace(f"[Host Node] Sending goal for {action_id}: {goal_msg}") action_client.wait_for_server() goal_uuid_obj = UUID(uuid=list(u.bytes)) From 3f63c3650504a5d4f3f5ac060b0ad123902f9065 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:45:45 +0800 Subject: [PATCH 30/34] transfer liquid handles --- unilabos/registry/devices/liquid_handler.yaml | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index 298eb70..7e3aaeb 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -9745,21 +9745,21 @@ liquid_handler.prcxi: - 0 handles: input: - - data_key: liquid + - data_key: sources data_source: handle data_type: resource - handler_key: sources - label: sources - - data_key: liquid - data_source: executor + handler_key: sources_identifier + label: 待移动液体 + - data_key: targets + data_source: handle data_type: resource - handler_key: targets - label: targets - - data_key: liquid - data_source: executor + handler_key: targets_identifier + label: 转移目标 + - data_key: tip_rack + data_source: handle data_type: resource - handler_key: tip_rack - label: tip_rack + handler_key: tip_rack_identifier + label: 墙头盒 output: - data_key: liquid data_source: handle From 400bb073d4228e70df88933a898d557e9cefb419 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:23:25 +0800 Subject: [PATCH 31/34] gather query --- unilabos/ros/nodes/base_device_node.py | 27 +++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 95fc075..5d27e73 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -1319,19 +1319,32 @@ class BaseROS2DeviceNode(Node, Generic[T]): resource_inputs = action_kwargs[k] if is_sequence else [action_kwargs[k]] # 批量查询资源 - queried_resources = [] - for resource_data in resource_inputs: + queried_resources: list = [None] * len(resource_inputs) + uuid_indices: list[tuple[int, str, dict]] = [] # (index, uuid, resource_data) + + # 第一遍:处理没有uuid的资源,收集有uuid的资源信息 + for idx, resource_data in enumerate(resource_inputs): unilabos_uuid = resource_data.get("data", {}).get("unilabos_uuid") if unilabos_uuid is None: plr_resource = await self.get_resource_with_dir( resource_id=resource_data["id"], with_children=True ) + if "sample_id" in resource_data: + plr_resource.unilabos_extra["sample_uuid"] = resource_data["sample_id"] + queried_resources[idx] = plr_resource else: - resource_tree = await self.get_resource([unilabos_uuid]) - plr_resource = resource_tree.to_plr_resources()[0] - if "sample_id" in resource_data: - plr_resource.unilabos_extra["sample_uuid"] = resource_data["sample_id"] - queried_resources.append(plr_resource) + uuid_indices.append((idx, unilabos_uuid, resource_data)) + + # 第二遍:批量查询有uuid的资源 + if uuid_indices: + uuids = [item[1] for item in uuid_indices] + resource_tree = await self.get_resource(uuids) + plr_resources = resource_tree.to_plr_resources() + for i, (idx, _, resource_data) in enumerate(uuid_indices): + plr_resource = plr_resources[i] + if "sample_id" in resource_data: + plr_resource.unilabos_extra["sample_uuid"] = resource_data["sample_id"] + queried_resources[idx] = plr_resource self.lab_logger().debug(f"资源查询结果: 共 {len(queried_resources)} 个资源") From 6bf57f18c17cc25382468cc40e2ab78d9fb09052 Mon Sep 17 00:00:00 2001 From: ZiWei <131428629+ZiWei09@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:29:38 +0800 Subject: [PATCH 32/34] Collaboration With Cursor --- .cursor/rules/device-drivers.mdc | 328 +++++++++++++++++++++++ .cursor/rules/protocol-development.mdc | 240 +++++++++++++++++ .cursor/rules/registry-config.mdc | 319 ++++++++++++++++++++++ .cursor/rules/ros-integration.mdc | 233 ++++++++++++++++ .cursor/rules/testing-patterns.mdc | 357 +++++++++++++++++++++++++ .cursor/rules/unilabos-project.mdc | 353 ++++++++++++++++++++++++ .cursorignore | 188 +++++++++++++ .gitignore | 1 - 8 files changed, 2018 insertions(+), 1 deletion(-) create mode 100644 .cursor/rules/device-drivers.mdc create mode 100644 .cursor/rules/protocol-development.mdc create mode 100644 .cursor/rules/registry-config.mdc create mode 100644 .cursor/rules/ros-integration.mdc create mode 100644 .cursor/rules/testing-patterns.mdc create mode 100644 .cursor/rules/unilabos-project.mdc create mode 100644 .cursorignore diff --git a/.cursor/rules/device-drivers.mdc b/.cursor/rules/device-drivers.mdc new file mode 100644 index 0000000..8adfb33 --- /dev/null +++ b/.cursor/rules/device-drivers.mdc @@ -0,0 +1,328 @@ +--- +description: 设备驱动开发规范 +globs: ["unilabos/devices/**/*.py"] +--- + +# 设备驱动开发规范 + +## 目录结构 + +``` +unilabos/devices/ +├── virtual/ # 虚拟设备(用于测试) +│ ├── virtual_stirrer.py +│ └── virtual_centrifuge.py +├── liquid_handling/ # 液体处理设备 +├── balance/ # 天平设备 +├── hplc/ # HPLC设备 +├── pump_and_valve/ # 泵和阀门 +├── temperature/ # 温度控制设备 +├── workstation/ # 工作站(组合设备) +└── ... +``` + +## 设备类完整模板 + +```python +import asyncio +import logging +import time as time_module +from typing import Dict, Any, Optional + +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + + +class MyDevice: + """ + 设备类描述 + + Attributes: + device_id: 设备唯一标识 + config: 设备配置字典 + data: 设备状态数据 + """ + + _ros_node: BaseROS2DeviceNode + + def __init__( + self, + device_id: str = None, + config: Dict[str, Any] = None, + **kwargs + ): + """ + 初始化设备 + + Args: + device_id: 设备ID + config: 配置字典 + **kwargs: 其他参数 + """ + # 兼容不同调用方式 + if device_id is None and 'id' in kwargs: + device_id = kwargs.pop('id') + if config is None and 'config' in kwargs: + config = kwargs.pop('config') + + self.device_id = device_id or "unknown_device" + self.config = config or {} + self.data = {} + + # 从config读取参数 + self.port = self.config.get('port') or kwargs.get('port', 'COM1') + self._max_value = self.config.get('max_value', 1000.0) + + # 初始化日志 + self.logger = logging.getLogger(f"MyDevice.{self.device_id}") + + self.logger.info(f"设备 {self.device_id} 已创建") + + def post_init(self, ros_node: BaseROS2DeviceNode): + """ + ROS节点注入 - 在ROS节点创建后调用 + + Args: + ros_node: ROS2设备节点实例 + """ + self._ros_node = ros_node + + async def initialize(self) -> bool: + """ + 初始化设备 - 连接硬件、设置初始状态 + + Returns: + bool: 初始化是否成功 + """ + self.logger.info(f"初始化设备 {self.device_id}") + + try: + # 执行硬件初始化 + # await self._connect_hardware() + + # 设置初始状态 + self.data.update({ + "status": "待机", + "is_running": False, + "current_value": 0.0, + }) + + self.logger.info(f"设备 {self.device_id} 初始化完成") + return True + + except Exception as e: + self.logger.error(f"初始化失败: {e}") + self.data["status"] = f"错误: {e}" + return False + + async def cleanup(self) -> bool: + """ + 清理设备 - 断开连接、释放资源 + + Returns: + bool: 清理是否成功 + """ + self.logger.info(f"清理设备 {self.device_id}") + + self.data.update({ + "status": "离线", + "is_running": False, + }) + + return True + + # ==================== 设备动作 ==================== + + async def execute_action( + self, + param1: float, + param2: str = "", + **kwargs + ) -> bool: + """ + 执行设备动作 + + Args: + param1: 参数1 + param2: 参数2(可选) + + Returns: + bool: 动作是否成功 + """ + # 类型转换和验证 + try: + param1 = float(param1) + except (ValueError, TypeError) as e: + self.logger.error(f"参数类型错误: {e}") + return False + + # 参数验证 + if param1 > self._max_value: + self.logger.error(f"参数超出范围: {param1} > {self._max_value}") + return False + + self.logger.info(f"执行动作: param1={param1}, param2={param2}") + + # 更新状态 + self.data.update({ + "status": "运行中", + "is_running": True, + }) + + # 执行动作(带进度反馈) + duration = 10.0 # 秒 + start_time = time_module.time() + + while True: + elapsed = time_module.time() - start_time + remaining = max(0, duration - elapsed) + progress = min(100, (elapsed / duration) * 100) + + self.data.update({ + "status": f"运行中: {progress:.0f}%", + "remaining_time": remaining, + }) + + if remaining <= 0: + break + + await self._ros_node.sleep(1.0) + + # 完成 + self.data.update({ + "status": "完成", + "is_running": False, + }) + + self.logger.info("动作执行完成") + return True + + # ==================== 状态属性 ==================== + + @property + def status(self) -> str: + """设备状态 - 自动发布为ROS Topic""" + return self.data.get("status", "未知") + + @property + def is_running(self) -> bool: + """是否正在运行""" + return self.data.get("is_running", False) + + @property + def current_value(self) -> float: + """当前值""" + return self.data.get("current_value", 0.0) + + # ==================== 辅助方法 ==================== + + def get_device_info(self) -> Dict[str, Any]: + """获取设备信息""" + return { + "device_id": self.device_id, + "status": self.status, + "is_running": self.is_running, + "current_value": self.current_value, + } + + def __str__(self) -> str: + return f"MyDevice({self.device_id}: {self.status})" +``` + +## 关键规则 + +### 1. 参数处理 + +所有动作方法的参数都可能以字符串形式传入,必须进行类型转换: + +```python +async def my_action(self, value: float, **kwargs) -> bool: + # 始终进行类型转换 + try: + value = float(value) + except (ValueError, TypeError) as e: + self.logger.error(f"参数类型错误: {e}") + return False +``` + +### 2. vessel 参数处理 + +vessel 参数可能是字符串ID或字典: + +```python +def extract_vessel_id(vessel: Union[str, dict]) -> str: + if isinstance(vessel, dict): + return vessel.get("id", "") + return str(vessel) if vessel else "" +``` + +### 3. 状态更新 + +使用 `self.data` 字典存储状态,属性读取状态: + +```python +# 更新状态 +self.data["status"] = "运行中" +self.data["current_speed"] = 300.0 + +# 读取状态(通过属性) +@property +def status(self) -> str: + return self.data.get("status", "待机") +``` + +### 4. 异步等待 + +使用 ROS 节点的 sleep 方法: + +```python +# 正确 +await self._ros_node.sleep(1.0) + +# 避免(除非在纯 Python 测试环境) +await asyncio.sleep(1.0) +``` + +### 5. 进度反馈 + +长时间运行的操作需要提供进度反馈: + +```python +while remaining > 0: + progress = (elapsed / total_time) * 100 + self.data["status"] = f"运行中: {progress:.0f}%" + self.data["remaining_time"] = remaining + + await self._ros_node.sleep(1.0) +``` + +## 虚拟设备 + +虚拟设备用于测试和演示,放在 `unilabos/devices/virtual/` 目录: + +- 类名以 `Virtual` 开头 +- 文件名以 `virtual_` 开头 +- 模拟真实设备的行为和时序 +- 使用表情符号增强日志可读性(可选) + +## 工作站设备 + +工作站是组合多个设备的复杂设备: + +```python +from unilabos.devices.workstation.workstation_base import WorkstationBase + +class MyWorkstation(WorkstationBase): + """组合工作站""" + + async def execute_workflow(self, workflow: Dict[str, Any]) -> bool: + """执行工作流""" + pass +``` + +## 设备注册 + +设备类开发完成后,需要在注册表中注册: + +1. 创建/编辑 `unilabos/registry/devices/my_category.yaml` +2. 添加设备配置(参考 `virtual_device.yaml`) +3. 运行 `--complete_registry` 自动生成 schema diff --git a/.cursor/rules/protocol-development.mdc b/.cursor/rules/protocol-development.mdc new file mode 100644 index 0000000..a94f947 --- /dev/null +++ b/.cursor/rules/protocol-development.mdc @@ -0,0 +1,240 @@ +--- +description: 协议编译器开发规范 +globs: ["unilabos/compile/**/*.py"] +--- + +# 协议编译器开发规范 + +## 概述 + +协议编译器负责将高级实验操作(如 Stir、Add、Filter)编译为设备可执行的动作序列。 + +## 文件命名 + +- 位置: `unilabos/compile/` +- 命名: `{operation}_protocol.py` +- 示例: `stir_protocol.py`, `add_protocol.py`, `filter_protocol.py` + +## 协议函数模板 + +```python +from typing import List, Dict, Any, Union +import networkx as nx +import logging + +from .utils.unit_parser import parse_time_input +from .utils.vessel_parser import extract_vessel_id + +logger = logging.getLogger(__name__) + + +def generate_{operation}_protocol( + G: nx.DiGraph, + vessel: Union[str, dict], + param1: Union[str, float] = "0", + param2: float = 0.0, + **kwargs +) -> List[Dict[str, Any]]: + """ + 生成{操作}协议序列 + + Args: + G: 物理拓扑图 (NetworkX DiGraph) + vessel: 容器ID或Resource字典 + param1: 参数1(支持字符串单位,如 "5 min") + param2: 参数2 + **kwargs: 其他参数 + + Returns: + List[Dict]: 动作序列 + + Raises: + ValueError: 参数无效时 + """ + # 1. 提取 vessel_id + vessel_id = extract_vessel_id(vessel) + + # 2. 验证参数 + if not vessel_id: + raise ValueError("vessel 参数不能为空") + + if vessel_id not in G.nodes(): + raise ValueError(f"容器 '{vessel_id}' 不存在于系统中") + + # 3. 解析参数(支持单位) + parsed_param1 = parse_time_input(param1) # "5 min" -> 300.0 + + # 4. 查找设备 + device_id = find_connected_device(G, vessel_id, device_type="my_device") + + # 5. 生成动作序列 + action_sequence = [] + + action = { + "device_id": device_id, + "action_name": "my_action", + "action_kwargs": { + "vessel": {"id": vessel_id}, # 始终使用字典格式 + "param1": float(parsed_param1), + "param2": float(param2), + } + } + action_sequence.append(action) + + logger.info(f"生成协议: {len(action_sequence)} 个动作") + return action_sequence + + +def find_connected_device( + G: nx.DiGraph, + vessel_id: str, + device_type: str = "" +) -> str: + """ + 查找与容器相连的设备 + + Args: + G: 拓扑图 + vessel_id: 容器ID + device_type: 设备类型关键字 + + Returns: + str: 设备ID + """ + # 查找所有匹配类型的设备 + device_nodes = [] + for node in G.nodes(): + node_class = G.nodes[node].get('class', '') or '' + if device_type.lower() in node_class.lower(): + device_nodes.append(node) + + # 检查连接 + if vessel_id and device_nodes: + for device in device_nodes: + if G.has_edge(device, vessel_id) or G.has_edge(vessel_id, device): + return device + + # 返回第一个可用设备 + if device_nodes: + return device_nodes[0] + + # 默认设备 + return f"{device_type}_1" +``` + +## 关键规则 + +### 1. vessel 参数处理 + +vessel 参数可能是字符串或字典,需要统一处理: + +```python +def extract_vessel_id(vessel: Union[str, dict]) -> str: + """提取vessel_id""" + if isinstance(vessel, dict): + # 可能是 {"id": "xxx"} 或完整 Resource 对象 + return vessel.get("id", list(vessel.values())[0].get("id", "")) + return str(vessel) if vessel else "" +``` + +### 2. action_kwargs 中的 vessel + +始终使用 `{"id": vessel_id}` 格式传递 vessel: + +```python +# 正确 +"action_kwargs": { + "vessel": {"id": vessel_id}, # 字符串ID包装为字典 +} + +# 避免 +"action_kwargs": { + "vessel": vessel_resource, # 不要传递完整 Resource 对象 +} +``` + +### 3. 单位解析 + +使用 `parse_time_input` 解析时间参数: + +```python +from .utils.unit_parser import parse_time_input + +# 支持格式: "5 min", "1 h", "300", "1.5 hours" +time_seconds = parse_time_input("5 min") # -> 300.0 +time_seconds = parse_time_input(120) # -> 120.0 +time_seconds = parse_time_input("1 h") # -> 3600.0 +``` + +### 4. 参数验证 + +所有参数必须进行验证和类型转换: + +```python +# 验证范围 +if speed < 10.0 or speed > 1500.0: + logger.warning(f"速度 {speed} 超出范围,修正为 300") + speed = 300.0 + +# 类型转换 +param = float(param) if not isinstance(param, (int, float)) else param +``` + +### 5. 日志记录 + +使用项目日志记录器: + +```python +logger = logging.getLogger(__name__) + +def generate_protocol(...): + logger.info(f"开始生成协议...") + logger.debug(f"参数: vessel={vessel_id}, time={time}") + logger.warning(f"参数修正: {old_value} -> {new_value}") +``` + +## 便捷函数 + +为常用操作提供便捷函数: + +```python +def stir_briefly(G: nx.DiGraph, vessel: Union[str, dict], + speed: float = 300.0) -> List[Dict[str, Any]]: + """短时间搅拌(30秒)""" + return generate_stir_protocol(G, vessel, time="30", stir_speed=speed) + +def stir_vigorously(G: nx.DiGraph, vessel: Union[str, dict], + time: str = "5 min") -> List[Dict[str, Any]]: + """剧烈搅拌""" + return generate_stir_protocol(G, vessel, time=time, stir_speed=800.0) +``` + +## 测试函数 + +每个协议文件应包含测试函数: + +```python +def test_{operation}_protocol(): + """测试协议生成""" + # 测试参数处理 + vessel_dict = {"id": "flask_1", "name": "反应瓶1"} + vessel_id = extract_vessel_id(vessel_dict) + assert vessel_id == "flask_1" + + # 测试单位解析 + time_s = parse_time_input("5 min") + assert time_s == 300.0 + + +if __name__ == "__main__": + test_{operation}_protocol() +``` + +## 现有协议参考 + +- `stir_protocol.py` - 搅拌操作 +- `add_protocol.py` - 添加物料 +- `filter_protocol.py` - 过滤操作 +- `heatchill_protocol.py` - 加热/冷却 +- `separate_protocol.py` - 分离操作 +- `evaporate_protocol.py` - 蒸发操作 diff --git a/.cursor/rules/registry-config.mdc b/.cursor/rules/registry-config.mdc new file mode 100644 index 0000000..bba2f22 --- /dev/null +++ b/.cursor/rules/registry-config.mdc @@ -0,0 +1,319 @@ +--- +description: 注册表配置规范 (YAML) +globs: ["unilabos/registry/**/*.yaml"] +--- + +# 注册表配置规范 + +## 概述 + +注册表使用 YAML 格式定义设备和资源类型,是 Uni-Lab-OS 的核心配置系统。 + +## 目录结构 + +``` +unilabos/registry/ +├── devices/ # 设备类型注册 +│ ├── virtual_device.yaml +│ ├── liquid_handler.yaml +│ └── ... +├── device_comms/ # 通信设备配置 +│ ├── communication_devices.yaml +│ └── modbus_ioboard.yaml +└── resources/ # 资源类型注册 + ├── bioyond/ + ├── organic/ + ├── opentrons/ + └── ... +``` + +## 设备注册表格式 + +### 基本结构 + +```yaml +device_type_id: + # 基本信息 + description: "设备描述" + version: "1.0.0" + category: + - category_name + icon: "icon_device.webp" + + # 类配置 + class: + module: "unilabos.devices.my_module:MyClass" + type: python + + # 状态类型(属性 -> ROS消息类型) + status_types: + status: String + temperature: Float64 + is_running: Bool + + # 动作映射 + action_value_mappings: + action_name: + type: UniLabJsonCommand # 或 UniLabJsonCommandAsync + goal: {} + feedback: {} + result: {} + schema: {...} + handles: {} +``` + +### action_value_mappings 详细格式 + +```yaml +action_value_mappings: + # 同步动作 + my_sync_action: + type: UniLabJsonCommand + goal: + param1: param1 + param2: param2 + feedback: {} + result: + success: success + message: message + goal_default: + param1: 0.0 + param2: "" + handles: {} + placeholder_keys: + device_param: unilabos_devices # 设备选择器 + resource_param: unilabos_resources # 资源选择器 + schema: + title: "动作名称参数" + description: "动作描述" + type: object + properties: + goal: + type: object + properties: + param1: + type: number + param2: + type: string + required: + - param1 + feedback: {} + result: + type: object + properties: + success: + type: boolean + message: + type: string + required: + - goal + + # 异步动作 + my_async_action: + type: UniLabJsonCommandAsync + goal: {} + feedback: + progress: progress + current_status: status + result: + success: success + schema: {...} +``` + +### 自动生成的动作 + +以 `auto-` 开头的动作由系统自动生成: + +```yaml +action_value_mappings: + auto-initialize: + type: UniLabJsonCommandAsync + goal: {} + feedback: {} + result: {} + schema: {...} + + auto-cleanup: + type: UniLabJsonCommandAsync + goal: {} + feedback: {} + result: {} + schema: {...} +``` + +### handles 配置 + +用于工作流编辑器中的数据流连接: + +```yaml +handles: + input: + - handler_key: "input_resource" + data_type: "resource" + label: "输入资源" + data_source: "handle" + data_key: "resources" + output: + - handler_key: "output_labware" + data_type: "resource" + label: "输出器皿" + data_source: "executor" + data_key: "created_resource.@flatten" +``` + +## 资源注册表格式 + +```yaml +resource_type_id: + description: "资源描述" + version: "1.0.0" + category: + - category_name + icon: "" + handles: [] + init_param_schema: {} + + class: + module: "unilabos.resources.my_module:MyResource" + type: pylabrobot # 或 python +``` + +### PyLabRobot 资源示例 + +```yaml +BIOYOND_Electrolyte_6VialCarrier: + category: + - bottle_carriers + - bioyond + class: + module: "unilabos.resources.bioyond.bottle_carriers:BIOYOND_Electrolyte_6VialCarrier" + type: pylabrobot + version: "1.0.0" +``` + +## 状态类型映射 + +Python 类型到 ROS 消息类型的映射: + +| Python 类型 | ROS 消息类型 | +|------------|-------------| +| `str` | `String` | +| `bool` | `Bool` | +| `int` | `Int64` | +| `float` | `Float64` | +| `list` | `String` (序列化) | +| `dict` | `String` (序列化) | + +## 自动完善注册表 + +使用 `--complete_registry` 参数自动生成 schema: + +```bash +python -m unilabos.app.main --complete_registry +``` + +这会: +1. 扫描设备类的方法签名 +2. 自动生成 `auto-` 前缀的动作 +3. 生成 JSON Schema +4. 更新 YAML 文件 + +## 验证规则 + +1. **device_type_id** 必须唯一 +2. **module** 路径必须正确可导入 +3. **status_types** 的类型必须是有效的 ROS 消息类型 +4. **schema** 必须是有效的 JSON Schema + +## 示例:完整设备配置 + +```yaml +virtual_stirrer: + category: + - virtual_device + description: "虚拟搅拌器设备" + version: "1.0.0" + icon: "icon_stirrer.webp" + handles: [] + init_param_schema: {} + + class: + module: "unilabos.devices.virtual.virtual_stirrer:VirtualStirrer" + type: python + + status_types: + status: String + operation_mode: String + current_speed: Float64 + is_stirring: Bool + remaining_time: Float64 + + action_value_mappings: + auto-initialize: + type: UniLabJsonCommandAsync + goal: {} + feedback: {} + result: {} + schema: + title: "initialize参数" + type: object + properties: + goal: + type: object + properties: {} + feedback: {} + result: {} + required: + - goal + + stir: + type: UniLabJsonCommandAsync + goal: + stir_time: stir_time + stir_speed: stir_speed + settling_time: settling_time + feedback: + current_speed: current_speed + remaining_time: remaining_time + result: + success: success + goal_default: + stir_time: 60.0 + stir_speed: 300.0 + settling_time: 30.0 + handles: {} + schema: + title: "stir参数" + description: "搅拌操作" + type: object + properties: + goal: + type: object + properties: + stir_time: + type: number + description: "搅拌时间(秒)" + stir_speed: + type: number + description: "搅拌速度(RPM)" + settling_time: + type: number + description: "沉降时间(秒)" + required: + - stir_time + - stir_speed + feedback: + type: object + properties: + current_speed: + type: number + remaining_time: + type: number + result: + type: object + properties: + success: + type: boolean + required: + - goal +``` diff --git a/.cursor/rules/ros-integration.mdc b/.cursor/rules/ros-integration.mdc new file mode 100644 index 0000000..4057b48 --- /dev/null +++ b/.cursor/rules/ros-integration.mdc @@ -0,0 +1,233 @@ +--- +description: ROS 2 集成开发规范 +globs: ["unilabos/ros/**/*.py", "**/*_node.py"] +--- + +# ROS 2 集成开发规范 + +## 概述 + +Uni-Lab-OS 使用 ROS 2 作为设备通信中间件,基于 rclpy 实现。 + +## 核心组件 + +### BaseROS2DeviceNode + +设备节点基类,提供: +- ROS Topic 自动发布(状态属性) +- Action Server 自动创建(设备动作) +- 资源管理服务 +- 异步任务调度 + +```python +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode +``` + +### 消息转换器 + +```python +from unilabos.ros.msgs.message_converter import ( + convert_to_ros_msg, + convert_from_ros_msg_with_mapping, + msg_converter_manager, + ros_action_to_json_schema, + ros_message_to_json_schema, +) +``` + +## 设备与 ROS 集成 + +### post_init 方法 + +设备类必须实现 `post_init` 方法接收 ROS 节点: + +```python +class MyDevice: + _ros_node: BaseROS2DeviceNode + + def post_init(self, ros_node: BaseROS2DeviceNode): + """ROS节点注入""" + self._ros_node = ros_node +``` + +### 状态属性发布 + +设备的 `@property` 属性会自动发布为 ROS Topic: + +```python +class MyDevice: + @property + def temperature(self) -> float: + return self._temperature + + # 自动发布到 /{namespace}/temperature Topic +``` + +### Topic 配置装饰器 + +```python +from unilabos.utils.decorator import topic_config + +class MyDevice: + @property + @topic_config(period=1.0, print_publish=False, qos=10) + def fast_data(self) -> float: + """高频数据 - 每秒发布一次""" + return self._fast_data + + @property + @topic_config(period=5.0) + def slow_data(self) -> str: + """低频数据 - 每5秒发布一次""" + return self._slow_data +``` + +### 订阅装饰器 + +```python +from unilabos.utils.decorator import subscribe + +class MyDevice: + @subscribe(topic="/external/sensor_data", qos=10) + def on_sensor_data(self, msg): + """订阅外部Topic""" + self._sensor_value = msg.data +``` + +## 异步操作 + +### 使用 ROS 节点睡眠 + +```python +# 推荐:使用ROS节点的睡眠方法 +await self._ros_node.sleep(1.0) + +# 不推荐:直接使用asyncio(可能导致回调阻塞) +await asyncio.sleep(1.0) +``` + +### 获取事件循环 + +```python +from unilabos.ros.x.rclpyx import get_event_loop + +loop = get_event_loop() +``` + +## 消息类型 + +### unilabos_msgs 包 + +```python +from unilabos_msgs.msg import Resource +from unilabos_msgs.srv import ( + ResourceAdd, + ResourceDelete, + ResourceUpdate, + ResourceList, + SerialCommand, +) +from unilabos_msgs.action import SendCmd +``` + +### Resource 消息结构 + +```python +Resource: + id: str + name: str + category: str + type: str + parent: str + children: List[str] + config: str # JSON字符串 + data: str # JSON字符串 + sample_id: str + pose: Pose +``` + +## 日志适配器 + +```python +from unilabos.utils.log import info, debug, warning, error, trace + +class MyDevice: + def __init__(self): + # 创建设备专属日志器 + self.logger = logging.getLogger(f"MyDevice.{self.device_id}") +``` + +ROSLoggerAdapter 同时向自定义日志和 ROS 日志发送消息。 + +## Action Server + +设备动作自动创建为 ROS Action Server: + +```yaml +# 在注册表中配置 +action_value_mappings: + my_action: + type: UniLabJsonCommandAsync # 异步Action + goal: {...} + feedback: {...} + result: {...} +``` + +### Action 类型 + +- **UniLabJsonCommand**: 同步动作 +- **UniLabJsonCommandAsync**: 异步动作(支持feedback) + +## 服务客户端 + +```python +from rclpy.client import Client + +# 调用其他节点的服务 +response = await self._ros_node.call_service( + service_name="/other_node/service", + request=MyServiceRequest(...) +) +``` + +## 命名空间 + +设备节点使用命名空间隔离: + +``` +/{device_id}/ # 设备命名空间 +/{device_id}/status # 状态Topic +/{device_id}/temperature # 温度Topic +/{device_id}/my_action # 动作Server +``` + +## 调试 + +### 查看 Topic + +```bash +ros2 topic list +ros2 topic echo /{device_id}/status +``` + +### 查看 Action + +```bash +ros2 action list +ros2 action info /{device_id}/my_action +``` + +### 查看 Service + +```bash +ros2 service list +ros2 service call /{device_id}/resource_list unilabos_msgs/srv/ResourceList +``` + +## 最佳实践 + +1. **状态属性命名**: 使用蛇形命名法(snake_case) +2. **Topic 频率**: 根据数据变化频率调整,避免过高频率 +3. **Action 反馈**: 长时间操作提供进度反馈 +4. **错误处理**: 使用 try-except 捕获并记录错误 +5. **资源清理**: 在 cleanup 方法中正确清理资源 diff --git a/.cursor/rules/testing-patterns.mdc b/.cursor/rules/testing-patterns.mdc new file mode 100644 index 0000000..73df7b0 --- /dev/null +++ b/.cursor/rules/testing-patterns.mdc @@ -0,0 +1,357 @@ +--- +description: 测试开发规范 +globs: ["tests/**/*.py", "**/test_*.py"] +--- + +# 测试开发规范 + +## 目录结构 + +``` +tests/ +├── __init__.py +├── devices/ # 设备测试 +│ └── liquid_handling/ +│ └── test_transfer_liquid.py +├── resources/ # 资源测试 +│ ├── test_bottle_carrier.py +│ └── test_resourcetreeset.py +├── ros/ # ROS消息测试 +│ └── msgs/ +│ ├── test_basic.py +│ ├── test_conversion.py +│ └── test_mapping.py +└── workflow/ # 工作流测试 + └── merge_workflow.py +``` + +## 测试框架 + +使用 pytest 作为测试框架: + +```bash +# 运行所有测试 +pytest tests/ + +# 运行特定测试文件 +pytest tests/resources/test_bottle_carrier.py + +# 运行特定测试函数 +pytest tests/resources/test_bottle_carrier.py::test_bottle_carrier + +# 显示详细输出 +pytest -v tests/ + +# 显示打印输出 +pytest -s tests/ +``` + +## 测试文件模板 + +```python +import pytest +from typing import List, Dict, Any + +# 导入被测试的模块 +from unilabos.resources.bioyond.bottle_carriers import ( + BIOYOND_Electrolyte_6VialCarrier, +) +from unilabos.resources.bioyond.bottles import ( + BIOYOND_PolymerStation_Solid_Vial, +) + + +class TestBottleCarrier: + """BottleCarrier 测试类""" + + def setup_method(self): + """每个测试方法前执行""" + self.carrier = BIOYOND_Electrolyte_6VialCarrier("test_carrier") + + def teardown_method(self): + """每个测试方法后执行""" + pass + + def test_carrier_creation(self): + """测试载架创建""" + assert self.carrier.name == "test_carrier" + assert len(self.carrier.sites) == 6 + + def test_bottle_placement(self): + """测试瓶子放置""" + bottle = BIOYOND_PolymerStation_Solid_Vial("test_bottle") + # 测试逻辑... + assert bottle.name == "test_bottle" + + +def test_standalone_function(): + """独立测试函数""" + result = some_function() + assert result is True + + +# 参数化测试 +@pytest.mark.parametrize("input,expected", [ + ("5 min", 300.0), + ("1 h", 3600.0), + ("120", 120.0), + (60, 60.0), +]) +def test_time_parsing(input, expected): + """测试时间解析""" + from unilabos.compile.utils.unit_parser import parse_time_input + assert parse_time_input(input) == expected + + +# 异常测试 +def test_invalid_input_raises_error(): + """测试无效输入抛出异常""" + with pytest.raises(ValueError) as exc_info: + invalid_function("bad_input") + assert "invalid" in str(exc_info.value).lower() + + +# 跳过条件测试 +@pytest.mark.skipif( + not os.environ.get("ROS_DISTRO"), + reason="需要ROS环境" +) +def test_ros_feature(): + """需要ROS环境的测试""" + pass +``` + +## 设备测试 + +### 虚拟设备测试 + +```python +import pytest +import asyncio +from unittest.mock import MagicMock, AsyncMock + +from unilabos.devices.virtual.virtual_stirrer import VirtualStirrer + + +class TestVirtualStirrer: + """VirtualStirrer 测试""" + + @pytest.fixture + def stirrer(self): + """创建测试用搅拌器""" + device = VirtualStirrer( + device_id="test_stirrer", + config={"max_speed": 1500.0, "min_speed": 50.0} + ) + + # Mock ROS节点 + mock_node = MagicMock() + mock_node.sleep = AsyncMock(return_value=None) + device.post_init(mock_node) + + return device + + @pytest.mark.asyncio + async def test_initialize(self, stirrer): + """测试初始化""" + result = await stirrer.initialize() + assert result is True + assert stirrer.status == "待机中" + + @pytest.mark.asyncio + async def test_stir_action(self, stirrer): + """测试搅拌动作""" + await stirrer.initialize() + + result = await stirrer.stir( + stir_time=5.0, + stir_speed=300.0, + settling_time=2.0 + ) + + assert result is True + assert stirrer.operation_mode == "Completed" + + @pytest.mark.asyncio + async def test_stir_invalid_speed(self, stirrer): + """测试无效速度""" + await stirrer.initialize() + + # 速度超出范围 + result = await stirrer.stir( + stir_time=5.0, + stir_speed=2000.0, # 超过max_speed + settling_time=0.0 + ) + + assert result is False + assert "错误" in stirrer.status +``` + +### 异步测试配置 + +```python +# conftest.py +import pytest +import asyncio + + +@pytest.fixture(scope="session") +def event_loop(): + """创建事件循环""" + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() +``` + +## 资源测试 + +```python +import pytest +from unilabos.resources.resource_tracker import ( + ResourceTreeSet, + ResourceTreeInstance, +) + + +def test_resource_tree_creation(): + """测试资源树创建""" + tree_set = ResourceTreeSet() + + # 添加资源 + resource = {"id": "res_1", "name": "Resource 1"} + tree_set.add_resource(resource) + + # 验证 + assert len(tree_set.all_nodes) == 1 + assert tree_set.get_resource("res_1") is not None + + +def test_resource_tree_merge(): + """测试资源树合并""" + local_set = ResourceTreeSet() + remote_set = ResourceTreeSet() + + # 设置数据... + + local_set.merge_remote_resources(remote_set) + + # 验证合并结果... +``` + +## ROS 消息测试 + +```python +import pytest +from unilabos.ros.msgs.message_converter import ( + convert_to_ros_msg, + convert_from_ros_msg_with_mapping, + msg_converter_manager, +) + + +def test_message_conversion(): + """测试消息转换""" + # Python -> ROS + python_data = {"id": "test", "value": 42} + ros_msg = convert_to_ros_msg(python_data, MyMsgType) + + assert ros_msg.id == "test" + assert ros_msg.value == 42 + + # ROS -> Python + result = convert_from_ros_msg_with_mapping(ros_msg, mapping) + assert result["id"] == "test" +``` + +## 协议测试 + +```python +import pytest +import networkx as nx +from unilabos.compile.stir_protocol import ( + generate_stir_protocol, + extract_vessel_id, +) + + +@pytest.fixture +def topology_graph(): + """创建测试拓扑图""" + G = nx.DiGraph() + G.add_node("flask_1", **{"class": "flask"}) + G.add_node("stirrer_1", **{"class": "virtual_stirrer"}) + G.add_edge("stirrer_1", "flask_1") + return G + + +def test_generate_stir_protocol(topology_graph): + """测试搅拌协议生成""" + actions = generate_stir_protocol( + G=topology_graph, + vessel="flask_1", + time="5 min", + stir_speed=300.0 + ) + + assert len(actions) == 1 + assert actions[0]["device_id"] == "stirrer_1" + assert actions[0]["action_name"] == "stir" + + +def test_extract_vessel_id(): + """测试vessel_id提取""" + # 字典格式 + assert extract_vessel_id({"id": "flask_1"}) == "flask_1" + + # 字符串格式 + assert extract_vessel_id("flask_2") == "flask_2" + + # 空值 + assert extract_vessel_id("") == "" +``` + +## 测试标记 + +```python +# 慢速测试 +@pytest.mark.slow +def test_long_running(): + pass + +# 需要网络 +@pytest.mark.network +def test_network_call(): + pass + +# 需要ROS +@pytest.mark.ros +def test_ros_feature(): + pass +``` + +运行特定标记的测试: + +```bash +pytest -m "not slow" # 排除慢速测试 +pytest -m ros # 仅ROS测试 +``` + +## 覆盖率 + +```bash +# 生成覆盖率报告 +pytest --cov=unilabos tests/ + +# HTML报告 +pytest --cov=unilabos --cov-report=html tests/ +``` + +## 最佳实践 + +1. **测试命名**: `test_{功能}_{场景}_{预期结果}` +2. **独立性**: 每个测试独立运行,不依赖其他测试 +3. **Mock外部依赖**: 使用 unittest.mock 模拟外部服务 +4. **参数化**: 使用 `@pytest.mark.parametrize` 减少重复代码 +5. **fixtures**: 使用 fixtures 共享测试设置 +6. **断言清晰**: 每个断言只验证一件事 diff --git a/.cursor/rules/unilabos-project.mdc b/.cursor/rules/unilabos-project.mdc new file mode 100644 index 0000000..1b6a24e --- /dev/null +++ b/.cursor/rules/unilabos-project.mdc @@ -0,0 +1,353 @@ +--- +description: Uni-Lab-OS 实验室自动化平台开发规范 - 核心规则 +globs: ["**/*.py", "**/*.yaml", "**/*.json"] +--- + +# Uni-Lab-OS 项目开发规范 + +## 项目概述 + +Uni-Lab-OS 是一个实验室自动化操作系统,用于连接和控制各种实验设备,实现实验工作流的自动化和标准化。 + +## 技术栈 + +- **Python 3.11** - 核心开发语言 +- **ROS 2** - 设备通信中间件 (rclpy) +- **Conda/Mamba** - 包管理 (robostack-staging, conda-forge) +- **FastAPI** - Web API 服务 +- **WebSocket** - 实时通信 +- **NetworkX** - 拓扑图管理 +- **YAML** - 配置和注册表定义 +- **PyLabRobot** - 实验室自动化库集成 +- **pytest** - 测试框架 +- **asyncio** - 异步编程 + +## 项目结构 + +``` +unilabos/ +├── app/ # 应用入口、Web服务、后端 +├── compile/ # 协议编译器 (stir, add, filter 等) +├── config/ # 配置管理 +├── devices/ # 设备驱动 (真实/虚拟) +├── device_comms/ # 设备通信协议 +├── device_mesh/ # 3D网格和可视化 +├── registry/ # 设备和资源类型注册表 (YAML) +├── resources/ # 资源定义 +├── ros/ # ROS 2 集成 +├── utils/ # 工具函数 +└── workflow/ # 工作流管理 +``` + +## 代码规范 + +### Python 风格 + +1. **类型注解**:所有函数必须使用类型注解 + ```python + def transfer_liquid( + source: str, + destination: str, + volume: float, + **kwargs + ) -> List[Dict[str, Any]]: + ``` + +2. **Docstring**:使用 Google 风格的文档字符串 + ```python + def initialize(self) -> bool: + """ + 初始化设备 + + Returns: + bool: 初始化是否成功 + """ + ``` + +3. **导入顺序**: + - 标准库 + - 第三方库 + - ROS 相关 (rclpy, unilabos_msgs) + - 项目内部模块 + +### 异步编程 + +1. 设备操作方法使用 `async def` +2. 使用 `await self._ros_node.sleep()` 而非 `asyncio.sleep()` +3. 长时间运行操作需提供进度反馈 + +```python +async def stir(self, stir_time: float, stir_speed: float, **kwargs) -> bool: + """执行搅拌操作""" + start_time = time_module.time() + while True: + elapsed = time_module.time() - start_time + remaining = max(0, stir_time - elapsed) + + self.data.update({ + "remaining_time": remaining, + "status": f"搅拌中: {stir_speed} RPM" + }) + + if remaining <= 0: + break + await self._ros_node.sleep(1.0) + return True +``` + +### 日志规范 + +使用项目自定义日志系统: + +```python +from unilabos.utils.log import logger, info, debug, warning, error, trace + +# 在设备类中使用 +self.logger = logging.getLogger(f"DeviceName.{self.device_id}") +self.logger.info("设备初始化完成") +``` + +## 设备驱动开发 + +### 设备类结构 + +```python +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + +class MyDevice: + """设备驱动类""" + + _ros_node: BaseROS2DeviceNode + + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): + self.device_id = device_id or "unknown_device" + self.config = config or {} + self.data = {} # 设备状态数据 + + def post_init(self, ros_node: BaseROS2DeviceNode): + """ROS节点注入""" + self._ros_node = ros_node + + async def initialize(self) -> bool: + """初始化设备""" + pass + + async def cleanup(self) -> bool: + """清理设备""" + pass + + # 状态属性 - 自动发布为 ROS Topic + @property + def status(self) -> str: + return self.data.get("status", "待机") +``` + +### 状态属性装饰器 + +```python +from unilabos.utils.decorator import topic_config + +class MyDevice: + @property + @topic_config(period=1.0, qos=10) # 每秒发布一次 + def temperature(self) -> float: + return self._temperature +``` + +### 虚拟设备 + +虚拟设备放置在 `unilabos/devices/virtual/` 目录下,命名为 `virtual_*.py` + +## 注册表配置 + +### 设备注册表 (YAML) + +位置: `unilabos/registry/devices/*.yaml` + +```yaml +my_device_type: + category: + - my_category + description: "设备描述" + version: "1.0.0" + class: + module: "unilabos.devices.my_device:MyDevice" + type: python + status_types: + status: String + temperature: Float64 + action_value_mappings: + auto-initialize: + type: UniLabJsonCommandAsync + goal: {} + feedback: {} + result: {} + schema: {...} +``` + +### 资源注册表 (YAML) + +位置: `unilabos/registry/resources/**/*.yaml` + +```yaml +my_container: + category: + - container + class: + module: "unilabos.resources.my_resource:MyContainer" + type: pylabrobot + version: "1.0.0" +``` + +## 协议编译器 + +位置: `unilabos/compile/*_protocol.py` + +### 协议生成函数模板 + +```python +from typing import List, Dict, Any, Union +import networkx as nx + +def generate_my_protocol( + G: nx.DiGraph, + vessel: Union[str, dict], + param1: float = 0.0, + **kwargs +) -> List[Dict[str, Any]]: + """ + 生成操作协议序列 + + Args: + G: 物理拓扑图 + vessel: 容器ID或字典 + param1: 参数1 + + Returns: + List[Dict]: 动作序列 + """ + # 提取vessel_id + vessel_id = vessel if isinstance(vessel, str) else vessel.get("id", "") + + # 查找设备 + device_id = find_connected_device(G, vessel_id) + + # 生成动作 + action_sequence = [{ + "device_id": device_id, + "action_name": "my_action", + "action_kwargs": { + "vessel": {"id": vessel_id}, + "param1": float(param1) + } + }] + + return action_sequence +``` + +## 测试规范 + +### 测试文件位置 + +- 单元测试: `tests/` 目录 +- 设备测试: `tests/devices/` +- 资源测试: `tests/resources/` +- ROS消息测试: `tests/ros/msgs/` + +### 测试命名 + +```python +# tests/devices/my_device/test_my_device.py + +import pytest + +def test_device_initialization(): + """测试设备初始化""" + pass + +def test_device_action(): + """测试设备动作""" + pass +``` + +## 错误处理 + +```python +from unilabos.utils.exception import UniLabException + +try: + result = await device.execute_action() +except ValueError as e: + self.logger.error(f"参数错误: {e}") + self.data["status"] = "错误: 参数无效" + return False +except Exception as e: + self.logger.error(f"执行失败: {e}") + raise +``` + +## 配置管理 + +```python +from unilabos.config.config import BasicConfig, HTTPConfig + +# 读取配置 +port = BasicConfig.port +is_host = BasicConfig.is_host_mode + +# 配置文件: local_config.py +``` + +## 常用工具 + +### 单例模式 + +```python +from unilabos.utils.decorator import singleton + +@singleton +class MyManager: + pass +``` + +### 类型检查 + +```python +from unilabos.utils.type_check import NoAliasDumper + +yaml.dump(data, f, Dumper=NoAliasDumper) +``` + +### 导入管理 + +```python +from unilabos.utils.import_manager import get_class + +device_class = get_class("unilabos.devices.my_device:MyDevice") +``` + +## Git 提交规范 + +提交信息格式: +``` +(): + + +``` + +类型: +- `feat`: 新功能 +- `fix`: 修复bug +- `docs`: 文档更新 +- `refactor`: 重构 +- `test`: 测试相关 +- `chore`: 构建/工具相关 + +示例: +``` +feat(devices): 添加虚拟搅拌器设备 + +- 实现VirtualStirrer类 +- 支持定时搅拌和持续搅拌模式 +- 添加速度验证逻辑 +``` diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..0bd258b --- /dev/null +++ b/.cursorignore @@ -0,0 +1,188 @@ +# ============================================================ +# Uni-Lab-OS Cursor Ignore 配置,控制 Cursor AI 的文件索引范围 +# ============================================================ + +# ==================== 敏感配置文件 ==================== +# 本地配置(可能包含密钥) +**/local_config.py +test_config.py +local_test*.py + +# 环境变量和密钥 +.env +.env.* +**/.certs/ +*.pem +*.key +credentials.json +secrets.yaml + +# ==================== 二进制和 3D 模型文件 ==================== +# 3D 模型文件(无需索引) +*.stl +*.dae +*.glb +*.gltf +*.obj +*.fbx +*.blend + +# URDF/Xacro 机器人描述文件(大型XML) +*.xacro + +# 图片文件 +*.png +*.jpg +*.jpeg +*.gif +*.webp +*.ico +*.svg +*.bmp + +# 压缩包 +*.zip +*.tar +*.tar.gz +*.tgz +*.bz2 +*.rar +*.7z + +# ==================== Python 生成文件 ==================== +__pycache__/ +*.py[cod] +*$py.class +*.so +*.pyd +*.egg +*.egg-info/ +.eggs/ +dist/ +build/ +*.manifest +*.spec + +# ==================== IDE 和编辑器 ==================== +.idea/ +.vscode/ +*.swp +*.swo +*~ +.#* + +# ==================== 测试和覆盖率 ==================== +.pytest_cache/ +.coverage +.coverage.* +htmlcov/ +.tox/ +.nox/ +coverage.xml +*.cover + +# ==================== 虚拟环境 ==================== +.venv/ +venv/ +env/ +ENV/ + +# ==================== ROS 2 生成文件 ==================== +# ROS 构建目录 +build/ +install/ +log/ +logs/ +devel/ + +# ROS 消息生成 +msg_gen/ +srv_gen/ +msg/*Action.msg +msg/*ActionFeedback.msg +msg/*ActionGoal.msg +msg/*ActionResult.msg +msg/*Feedback.msg +msg/*Goal.msg +msg/*Result.msg +msg/_*.py +srv/_*.py +build_isolated/ +devel_isolated/ + +# ROS 动态配置 +*.cfgc +/cfg/cpp/ +/cfg/*.py + +# ==================== 项目特定目录 ==================== +# 工作数据目录 +unilabos_data/ + +# 临时和输出目录 +temp/ +output/ +cursor_docs/ +configs/ + +# 文档构建 +docs/_build/ +/site + +# ==================== 大型数据文件 ==================== +# 点云数据 +*.pcd + +# GraphML 图形文件 +*.graphml + +# 日志文件 +*.log + +# 数据库 +*.sqlite3 +*.db + +# Jupyter 检查点 +.ipynb_checkpoints/ + +# ==================== 设备网格资源 ==================== +# 3D 网格文件目录(包含大量 STL/DAE 文件) +unilabos/device_mesh/devices/**/*.stl +unilabos/device_mesh/devices/**/*.dae +unilabos/device_mesh/resources/**/*.stl +unilabos/device_mesh/resources/**/*.glb +unilabos/device_mesh/resources/**/*.xacro + +# RViz 配置 +*.rviz + +# ==================== 系统文件 ==================== +.DS_Store +Thumbs.db +desktop.ini + +# ==================== 锁文件 ==================== +poetry.lock +Pipfile.lock +pdm.lock +package-lock.json +yarn.lock + +# ==================== 类型检查缓存 ==================== +.mypy_cache/ +.dmypy.json +.pytype/ +.pyre/ +pyrightconfig.json + +# ==================== 其他 ==================== +# Catkin +CATKIN_IGNORE + +# Eclipse/Qt +.project +.cproject +CMakeLists.txt.user +*.user +qtcreator-* diff --git a/.gitignore b/.gitignore index 838331e..610be61 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ temp/ output/ unilabos_data/ pyrightconfig.json -.cursorignore ## Python # Byte-compiled / optimized / DLL files From 37ec49f318c5130e8db527d43a9b78545fc84b34 Mon Sep 17 00:00:00 2001 From: ZiWei <131428629+ZiWei09@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:31:14 +0800 Subject: [PATCH 33/34] Refactor Bioyond resource handling: update warehouse mapping retrieval, add TipBox support, and improve liquid tracking logic. Migrate TipBox creation to bottle_carriers.py for better structure. --- .../workstation/bioyond_studio/station.py | 2 +- .../resources/bioyond/bottle_carriers.yaml | 13 +++ .../registry/resources/bioyond/bottles.yaml | 11 -- unilabos/resources/bioyond/bottle_carriers.py | 109 +++++++++++++++++- unilabos/resources/bioyond/bottles.py | 56 ++------- unilabos/resources/graphio.py | 47 ++++---- unilabos/resources/resource_tracker.py | 1 + 7 files changed, 154 insertions(+), 85 deletions(-) diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index 327d819..60c18e1 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -258,7 +258,7 @@ class BioyondResourceSynchronizer(ResourceSynchronizer): logger.info(f"[同步→Bioyond] ➕ 物料不存在于 Bioyond,将创建新物料并入库") # 第1步:从配置中获取仓库配置 - warehouse_mapping = self.bioyond_config.get("warehouse_mapping", {}) + warehouse_mapping = self.workstation.bioyond_config.get("warehouse_mapping", {}) # 确定目标仓库名称 parent_name = None diff --git a/unilabos/registry/resources/bioyond/bottle_carriers.yaml b/unilabos/registry/resources/bioyond/bottle_carriers.yaml index 764a8aa..89f2bdd 100644 --- a/unilabos/registry/resources/bioyond/bottle_carriers.yaml +++ b/unilabos/registry/resources/bioyond/bottle_carriers.yaml @@ -46,3 +46,16 @@ BIOYOND_PolymerStation_8StockCarrier: init_param_schema: {} registry_type: resource version: 1.0.0 +BIOYOND_PolymerStation_TipBox: + category: + - bottle_carriers + - tip_racks + class: + module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_TipBox + type: pylabrobot + description: BIOYOND_PolymerStation_TipBox (4x6布局,24个枪头孔位) + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 diff --git a/unilabos/registry/resources/bioyond/bottles.yaml b/unilabos/registry/resources/bioyond/bottles.yaml index ecc5525..e493e7b 100644 --- a/unilabos/registry/resources/bioyond/bottles.yaml +++ b/unilabos/registry/resources/bioyond/bottles.yaml @@ -82,14 +82,3 @@ BIOYOND_PolymerStation_Solution_Beaker: icon: '' init_param_schema: {} version: 1.0.0 -BIOYOND_PolymerStation_TipBox: - category: - - bottles - - tip_boxes - class: - module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_TipBox - type: pylabrobot - handles: [] - icon: '' - init_param_schema: {} - version: 1.0.0 diff --git a/unilabos/resources/bioyond/bottle_carriers.py b/unilabos/resources/bioyond/bottle_carriers.py index d79b849..e1932b2 100644 --- a/unilabos/resources/bioyond/bottle_carriers.py +++ b/unilabos/resources/bioyond/bottle_carriers.py @@ -1,4 +1,4 @@ -from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d +from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d, Container from unilabos.resources.itemized_carrier import BottleCarrier from unilabos.resources.bioyond.bottles import ( @@ -9,6 +9,28 @@ from unilabos.resources.bioyond.bottles import ( BIOYOND_PolymerStation_Reagent_Bottle, BIOYOND_PolymerStation_Flask, ) + + +def BIOYOND_PolymerStation_Tip(name: str, size_x: float = 8.0, size_y: float = 8.0, size_z: float = 50.0) -> Container: + """创建单个枪头资源 + + Args: + name: 枪头名称 + size_x: 枪头宽度 (mm) + size_y: 枪头长度 (mm) + size_z: 枪头高度 (mm) + + Returns: + Container: 枪头容器 + """ + return Container( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category="tip", + model="BIOYOND_PolymerStation_Tip", + ) # 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial @@ -322,3 +344,88 @@ def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier: carrier.num_items_z = 1 carrier[0] = BIOYOND_PolymerStation_Solution_Beaker(f"{name}_beaker_1") return carrier + + +def BIOYOND_PolymerStation_TipBox( + name: str, + size_x: float = 127.76, # 枪头盒宽度 + size_y: float = 85.48, # 枪头盒长度 + size_z: float = 100.0, # 枪头盒高度 + barcode: str = None, +) -> BottleCarrier: + """创建4×6枪头盒 (24个枪头) - 使用 BottleCarrier 结构 + + Args: + name: 枪头盒名称 + size_x: 枪头盒宽度 (mm) + size_y: 枪头盒长度 (mm) + size_z: 枪头盒高度 (mm) + barcode: 条形码 + + Returns: + BottleCarrier: 包含24个枪头孔位的枪头盒载架 + + 布局说明: + - 4行×6列 (A-D, 1-6) + - 枪头孔位间距: 18mm (x方向) × 18mm (y方向) + - 起始位置居中对齐 + - 索引顺序: 列优先 (0=A1, 1=B1, 2=C1, 3=D1, 4=A2, ...) + """ + # 枪头孔位参数 + num_cols = 6 # 1-6 (x方向) + num_rows = 4 # A-D (y方向) + tip_diameter = 8.0 # 枪头孔位直径 + tip_spacing_x = 18.0 # 列间距 (增加到18mm,更宽松) + tip_spacing_y = 18.0 # 行间距 (增加到18mm,更宽松) + + # 计算起始位置 (居中对齐) + total_width = (num_cols - 1) * tip_spacing_x + tip_diameter + total_height = (num_rows - 1) * tip_spacing_y + tip_diameter + start_x = (size_x - total_width) / 2 + start_y = (size_y - total_height) / 2 + + # 使用 create_ordered_items_2d 创建孔位 + # create_ordered_items_2d 返回的 key 是数字索引: 0, 1, 2, ... + # 顺序是列优先: 先y后x (即 0=A1, 1=B1, 2=C1, 3=D1, 4=A2, 5=B2, ...) + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=num_cols, + num_items_y=num_rows, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=tip_spacing_x, + item_dy=tip_spacing_y, + size_x=tip_diameter, + size_y=tip_diameter, + size_z=50.0, # 枪头深度 + ) + + # 更新 sites 中每个 ResourceHolder 的名称 + for k, v in sites.items(): + v.name = f"{name}_{v.name}" + + # 创建枪头盒载架 + # 注意:不设置 category,使用默认的 "bottle_carrier",这样前端会显示为完整的矩形载架 + tip_box = BottleCarrier( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + sites=sites, # 直接使用数字索引的 sites + model="BIOYOND_PolymerStation_TipBox", + ) + + # 设置自定义属性 + tip_box.barcode = barcode + tip_box.tip_count = 24 # 4行×6列 + tip_box.num_items_x = num_cols + tip_box.num_items_y = num_rows + tip_box.num_items_z = 1 + + # ⭐ 枪头盒不需要放入子资源 + # 与其他 carrier 不同,枪头盒在 Bioyond 中是一个整体 + # 不需要追踪每个枪头的状态,保持为空的 ResourceHolder 即可 + # 这样前端会显示24个空槽位,可以用于放置枪头 + + return tip_box diff --git a/unilabos/resources/bioyond/bottles.py b/unilabos/resources/bioyond/bottles.py index 7045d8b..73343bc 100644 --- a/unilabos/resources/bioyond/bottles.py +++ b/unilabos/resources/bioyond/bottles.py @@ -116,7 +116,9 @@ def BIOYOND_PolymerStation_TipBox( size_z: float = 100.0, # 枪头盒高度 barcode: str = None, ): - """创建4×6枪头盒 (24个枪头) + """创建4×6枪头盒 (24个枪头) - 使用 BottleCarrier 结构 + + 注意:此函数已弃用,请使用 bottle_carriers.py 中的版本 Args: name: 枪头盒名称 @@ -126,55 +128,11 @@ def BIOYOND_PolymerStation_TipBox( barcode: 条形码 Returns: - TipBoxCarrier: 包含24个枪头孔位的枪头盒 + BottleCarrier: 包含24个枪头孔位的枪头盒载架 """ - from pylabrobot.resources import Container, Coordinate - - # 创建枪头盒容器 - tip_box = Container( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - category="tip_rack", - model="BIOYOND_PolymerStation_TipBox_4x6", - ) - - # 设置自定义属性 - tip_box.barcode = barcode - tip_box.tip_count = 24 # 4行×6列 - tip_box.num_items_x = 6 # 6列 - tip_box.num_items_y = 4 # 4行 - - # 创建24个枪头孔位 (4行×6列) - # 假设孔位间距为 9mm - tip_spacing_x = 9.0 # 列间距 - tip_spacing_y = 9.0 # 行间距 - start_x = 14.38 # 第一个孔位的x偏移 - start_y = 11.24 # 第一个孔位的y偏移 - - for row in range(4): # A, B, C, D - for col in range(6): # 1-6 - spot_name = f"{chr(65 + row)}{col + 1}" # A1, A2, ..., D6 - x = start_x + col * tip_spacing_x - y = start_y + row * tip_spacing_y - - # 创建枪头孔位容器 - tip_spot = Container( - name=spot_name, - size_x=8.0, # 单个枪头孔位大小 - size_y=8.0, - size_z=size_z - 10.0, # 略低于盒子高度 - category="tip_spot", - ) - - # 添加到枪头盒 - tip_box.assign_child_resource( - tip_spot, - location=Coordinate(x=x, y=y, z=0) - ) - - return tip_box + # 重定向到 bottle_carriers.py 中的实现 + from unilabos.resources.bioyond.bottle_carriers import BIOYOND_PolymerStation_TipBox as TipBox_Carrier + return TipBox_Carrier(name=name, size_x=size_x, size_y=size_y, size_z=size_z, barcode=barcode) def BIOYOND_PolymerStation_Flask( diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 8233ae7..7c11cd8 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -759,9 +759,12 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st bottle = plr_material[number] = initialize_resource( {"name": f'{detail["name"]}_{number}', "class": reverse_type_mapping[typeName][0]}, resource_type=ResourcePLR ) - bottle.tracker.liquids = [ - (detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0) - ] + # 只有具有 tracker 属性的容器才设置液体信息(如 Bottle, Well) + # ResourceHolder 等不支持液体追踪的容器跳过 + if hasattr(bottle, "tracker"): + bottle.tracker.liquids = [ + (detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0) + ] bottle.code = detail.get("code", "") logger.debug(f" └─ [子物料] {detail['name']} → {plr_material.name}[{number}] (类型:{typeName})") else: @@ -770,9 +773,11 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st # 只对有 capacity 属性的容器(液体容器)处理液体追踪 if hasattr(plr_material, 'capacity'): bottle = plr_material[0] if plr_material.capacity > 0 else plr_material - bottle.tracker.liquids = [ - (material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0) - ] + # 确保 bottle 有 tracker 属性才设置液体信息 + if hasattr(bottle, "tracker"): + bottle.tracker.liquids = [ + (material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0) + ] plr_materials.append(plr_material) @@ -801,24 +806,29 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st wh_name = loc.get("whName") logger.debug(f"[物料位置] {unique_name} 尝试放置到 warehouse: {wh_name} (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')}, z={loc.get('z')})") + # Bioyond坐标映射 (重要!): x→行(1=A,2=B...), y→列(1=01,2=02...), z→层(通常=1) + # 必须在warehouse映射之前先获取坐标,以便后续调整 + x = loc.get("x", 1) # 行号 (1-based: 1=A, 2=B, 3=C, 4=D) + y = loc.get("y", 1) # 列号 (1-based: 1=01, 2=02, 3=03...) + z = loc.get("z", 1) # 层号 (1-based, 通常为1) + # 特殊处理: Bioyond的"堆栈1"需要映射到"堆栈1左"或"堆栈1右" - # 根据列号(x)判断: 1-4映射到左侧, 5-8映射到右侧 + # 根据列号(y)判断: 1-4映射到左侧, 5-8映射到右侧 if wh_name == "堆栈1": - x_val = loc.get("x", 1) - if 1 <= x_val <= 4: + if 1 <= y <= 4: wh_name = "堆栈1左" - elif 5 <= x_val <= 8: + elif 5 <= y <= 8: wh_name = "堆栈1右" + y = y - 4 # 调整列号: 5-8映射到1-4 else: - logger.warning(f"物料 {material['name']} 的列号 x={x_val} 超出范围,无法映射到堆栈1左或堆栈1右") + logger.warning(f"物料 {material['name']} 的列号 y={y} 超出范围,无法映射到堆栈1左或堆栈1右") continue # 特殊处理: Bioyond的"站内Tip盒堆栈"也需要进行拆分映射 if wh_name == "站内Tip盒堆栈": - y_val = loc.get("y", 1) - if y_val == 1: + if y == 1: wh_name = "站内Tip盒堆栈(右)" - elif y_val in [2, 3]: + elif y in [2, 3]: wh_name = "站内Tip盒堆栈(左)" y = y - 1 # 调整列号,因为左侧仓库对应的 Bioyond y=2 实际上是它的第1列 @@ -826,15 +836,6 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st warehouse = deck.warehouses[wh_name] logger.debug(f"[Warehouse匹配] 找到warehouse: {wh_name} (容量: {warehouse.capacity}, 行×列: {warehouse.num_items_x}×{warehouse.num_items_y})") - # Bioyond坐标映射 (重要!): x→行(1=A,2=B...), y→列(1=01,2=02...), z→层(通常=1) - x = loc.get("x", 1) # 行号 (1-based: 1=A, 2=B, 3=C, 4=D) - y = loc.get("y", 1) # 列号 (1-based: 1=01, 2=02, 3=03...) - z = loc.get("z", 1) # 层号 (1-based, 通常为1) - - # 如果是右侧堆栈,需要调整列号 (5→1, 6→2, 7→3, 8→4) - if wh_name == "堆栈1右": - y = y - 4 # 将5-8映射到1-4 - # 特殊处理竖向warehouse(站内试剂存放堆栈、测量小瓶仓库) # 这些warehouse使用 vertical-col-major 布局 if wh_name in ["站内试剂存放堆栈", "测量小瓶仓库(测密度)"]: diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index 4097782..9401182 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -338,6 +338,7 @@ class ResourceTreeSet(object): "deck": "deck", "tip_rack": "tip_rack", "tip_spot": "tip_spot", + "tip": "tip", # 添加 tip 类型支持 "tube": "tube", "bottle_carrier": "bottle_carrier", } From f8a52860ad8f39d3fc6179ede1025d67a5fcc152 Mon Sep 17 00:00:00 2001 From: ZiWei <131428629+ZiWei09@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:25:47 +0800 Subject: [PATCH 34/34] Add BIOYOND deck imports and update JSON configurations with new UUIDs for various components --- unilabos/resources/plr_additional_res_reg.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/unilabos/resources/plr_additional_res_reg.py b/unilabos/resources/plr_additional_res_reg.py index 1c019de..4dc1c4b 100644 --- a/unilabos/resources/plr_additional_res_reg.py +++ b/unilabos/resources/plr_additional_res_reg.py @@ -18,3 +18,9 @@ def register(): from unilabos.devices.liquid_handling.rviz_backend import UniLiquidHandlerRvizBackend from unilabos.devices.liquid_handling.laiyu.backend.laiyu_v_backend import UniLiquidHandlerLaiyuBackend + # noinspection PyUnresolvedReferences + from unilabos.resources.bioyond.decks import ( + BIOYOND_PolymerReactionStation_Deck, + BIOYOND_PolymerPreparationStation_Deck, + BIOYOND_YB_Deck, + )