From 9fa36881968d6b9858716279e779559746717292 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Sun, 12 Oct 2025 22:38:04 +0800 Subject: [PATCH] Update registry. Update uuid loop figure method. Update install docs. --- docs/user_guide/quick_install_guide.md | 197 ++++++++++++++++++ unilabos/registry/devices/virtual_device.yaml | 4 + .../resources/bioyond/bottle_carriers.yaml | 25 ++- .../registry/resources/bioyond/bottles.yaml | 58 ++++-- unilabos/ros/main_slave_run.py | 13 +- unilabos/ros/nodes/presets/host_node.py | 24 ++- unilabos/ros/nodes/presets/workstation.py | 27 --- unilabos/ros/nodes/resource_tracker.py | 2 + 8 files changed, 285 insertions(+), 65 deletions(-) create mode 100644 docs/user_guide/quick_install_guide.md diff --git a/docs/user_guide/quick_install_guide.md b/docs/user_guide/quick_install_guide.md new file mode 100644 index 00000000..eefa3ae9 --- /dev/null +++ b/docs/user_guide/quick_install_guide.md @@ -0,0 +1,197 @@ +# Uni-Lab-OS 一键安装快速指南 + +## 概述 + +本指南提供最快速的 Uni-Lab-OS 安装方法,使用预打包的 conda 环境,无需手动配置依赖。 + +## 前置要求 + +- 已安装 Conda/Miniconda/Miniforge/Mamba +- 至少 10GB 可用磁盘空间 +- Windows 10+, macOS 10.14+, 或 Linux (Ubuntu 20.04+) + +## 安装步骤 + +### 第一步:下载预打包环境 + +1. 访问 [GitHub Actions - Conda Pack Build](https://github.com/dptech-corp/Uni-Lab-OS/actions/workflows/conda-pack-build.yml) + +2. 选择最新的成功构建记录(绿色勾号 ✓) + +3. 在页面底部的 "Artifacts" 部分,下载对应你操作系统的压缩包: + - Windows: `unilab-pack-win-64-{branch}.zip` + - macOS (Intel): `unilab-pack-osx-64-{branch}.tar.gz` + - macOS (Apple Silicon): `unilab-pack-osx-arm64-{branch}.tar.gz` + - Linux: `unilab-pack-linux-64-{branch}.tar.gz` + +### 第二步:解压并运行安装脚本 + +#### Windows + +```batch +REM 使用 Windows 资源管理器解压下载的 zip 文件 +REM 或使用命令行: +tar -xzf unilab-pack-win-64-dev.zip + +REM 进入解压后的目录 +cd unilab-pack-win-64-dev + +REM 双击运行 install_unilab.bat +REM 或在命令行中执行: +install_unilab.bat +``` + +#### macOS + +```bash +# 解压下载的压缩包 +tar -xzf unilab-pack-osx-arm64-dev.tar.gz + +# 进入解压后的目录 +cd unilab-pack-osx-arm64-dev + +# 运行安装脚本 +bash install_unilab.sh +``` + +#### Linux + +```bash +# 解压下载的压缩包 +tar -xzf unilab-pack-linux-64-dev.tar.gz + +# 进入解压后的目录 +cd unilab-pack-linux-64-dev + +# 添加执行权限(如果需要) +chmod +x install_unilab.sh + +# 运行安装脚本 +./install_unilab.sh +``` + +### 第三步:激活环境 + +```bash +conda activate unilab +``` + +### 第四步:验证安装(推荐) + +```bash +# 确保已激活环境 +conda activate unilab + +# 运行验证脚本 +python verify_installation.py +``` + +如果看到 "✓ All checks passed!",说明安装成功! + +## 常见问题 + +### Q: 安装脚本找不到 conda? + +**A:** 确保你已经安装了 conda/miniconda/miniforge,并且安装在标准位置: + +- **Windows**: + + - `%USERPROFILE%\miniforge3` + - `%USERPROFILE%\miniconda3` + - `%USERPROFILE%\anaconda3` + - `C:\ProgramData\miniforge3` + +- **macOS/Linux**: + - `~/miniforge3` + - `~/miniconda3` + - `~/anaconda3` + - `/opt/conda` + +如果安装在其他位置,可以先激活 conda base 环境,然后手动运行安装脚本。 + +### Q: 安装后激活环境提示找不到? + +**A:** 尝试以下方法: + +```bash +# 方法 1: 使用 conda activate +conda activate unilab + +# 方法 2: 使用完整路径激活(Windows) +call C:\Users\{YourUsername}\miniforge3\envs\unilab\Scripts\activate.bat + +# 方法 2: 使用完整路径激活(Unix) +source ~/miniforge3/envs/unilab/bin/activate +``` + +### Q: conda-unpack 失败怎么办? + +**A:** 尝试手动运行: + +```bash +# Windows +cd %CONDA_PREFIX%\envs\unilab +.\Scripts\conda-unpack.exe + +# macOS/Linux +cd $CONDA_PREFIX/envs/unilab +./bin/conda-unpack +``` + +### Q: 验证脚本报错? + +**A:** 首先确认环境已激活: + +```bash +# 检查当前环境 +conda env list + +# 应该看到 unilab 前面有 * 标记 +``` + +如果仍有问题,查看具体报错信息,可能需要: + +- 重新运行安装脚本 +- 检查磁盘空间 +- 查看详细文档 + +### Q: 环境很大,有办法减小吗? + +**A:** 预打包的环境包含所有依赖,通常较大(压缩后 2-5GB)。这是为了确保离线安装和完整功能。如果空间有限,考虑使用手动安装方式,只安装需要的组件。 + +### Q: 如何更新到最新版本? + +**A:** 重新下载最新的预打包环境,运行安装脚本时选择覆盖现有环境。 + +或者在现有环境中更新: + +```bash +conda activate unilab + +# 更新 unilabos +cd /path/to/Uni-Lab-OS +git pull +pip install -e . --upgrade + +# 更新 ros-humble-unilabos-msgs +mamba update ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge +``` + +## 下一步 + +安装完成后,你可以: + +1. **查看启动指南**: {doc}`launch` +2. **运行示例**: {doc}`../boot_examples/index` +3. **配置设备**: 编辑 `unilabos_data/startup_config.json` +4. **阅读开发文档**: {doc}`../developer_guide/workstation_architecture` + +## 需要帮助? + +- **文档**: [docs/user_guide/installation.md](installation.md) +- **问题反馈**: [GitHub Issues](https://github.com/dptech-corp/Uni-Lab-OS/issues) +- **开发版安装**: 参考 {doc}`installation` 的方式二 + +--- + +**提示**: 这个预打包环境包含了从指定分支(通常是 `dev`)构建的最新代码。如果需要稳定版本,请使用方式二手动安装 release 版本。 diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index 22cbe819..18575821 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -4741,6 +4741,7 @@ virtual_stirrer: status_types: current_speed: float current_vessel: str + device_info: dict is_stirring: bool max_speed: float min_speed: float @@ -4775,6 +4776,8 @@ virtual_stirrer: type: number current_vessel: type: string + device_info: + type: object is_stirring: type: boolean max_speed: @@ -4796,6 +4799,7 @@ virtual_stirrer: - remaining_time - max_speed - min_speed + - device_info type: object version: 1.0.0 virtual_transfer_pump: diff --git a/unilabos/registry/resources/bioyond/bottle_carriers.yaml b/unilabos/registry/resources/bioyond/bottle_carriers.yaml index 73df9c89..44e54270 100644 --- a/unilabos/registry/resources/bioyond/bottle_carriers.yaml +++ b/unilabos/registry/resources/bioyond/bottle_carriers.yaml @@ -22,18 +22,6 @@ BIOYOND_PolymerStation_1FlaskCarrier: init_param_schema: {} registry_type: resource version: 1.0.0 -BIOYOND_PolymerStation_6VialCarrier: - category: - - bottle_carriers - class: - module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6VialCarrier - type: pylabrobot - description: BIOYOND_PolymerStation_6VialCarrier - handles: [] - icon: '' - init_param_schema: {} - registry_type: resource - version: 1.0.0 BIOYOND_PolymerStation_6StockCarrier: category: - bottle_carriers @@ -46,4 +34,15 @@ BIOYOND_PolymerStation_6StockCarrier: init_param_schema: {} registry_type: resource version: 1.0.0 - +BIOYOND_PolymerStation_6VialCarrier: + category: + - bottle_carriers + class: + module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6VialCarrier + type: pylabrobot + description: BIOYOND_PolymerStation_6VialCarrier + 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 06d45145..55da6908 100644 --- a/unilabos/registry/resources/bioyond/bottles.yaml +++ b/unilabos/registry/resources/bioyond/bottles.yaml @@ -1,24 +1,50 @@ -BIOYOND_PolymerStation_Solid_Stock: - class: - module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solid_Stock - type: pylabrobot - -BIOYOND_PolymerStation_Solid_Vial: - class: - module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solid_Vial - type: pylabrobot - BIOYOND_PolymerStation_Liquid_Vial: + category: + - bottles class: module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Liquid_Vial type: pylabrobot - + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +BIOYOND_PolymerStation_Reagent_Bottle: + category: + - bottles + class: + module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Reagent_Bottle + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +BIOYOND_PolymerStation_Solid_Stock: + category: + - bottles + class: + module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solid_Stock + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +BIOYOND_PolymerStation_Solid_Vial: + category: + - bottles + class: + module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solid_Vial + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 BIOYOND_PolymerStation_Solution_Beaker: + category: + - bottles class: module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solution_Beaker type: pylabrobot - -BIOYOND_PolymerStation_Reagent_Bottle: - class: - module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Reagent_Bottle - type: pylabrobot \ No newline at end of file + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 diff --git a/unilabos/ros/main_slave_run.py b/unilabos/ros/main_slave_run.py index c903a4be..b57d30b0 100644 --- a/unilabos/ros/main_slave_run.py +++ b/unilabos/ros/main_slave_run.py @@ -1,4 +1,3 @@ -import copy import json import threading import time @@ -183,14 +182,22 @@ def slave( ) tree_response: SerialCommand_Response = rclient.call_async(request).result() uuid_mapping = json.loads(tree_response.response) + # 创建反向映射:new_uuid -> old_uuid + reverse_uuid_mapping = {new_uuid: old_uuid for old_uuid, new_uuid in uuid_mapping.items()} for node in resources_config.root_nodes: if node.res_content.type == "device": for sub_node in node.children: # 只有二级子设备 if sub_node.res_content.type != "device": device_tracker = devices_instances[node.res_content.id].resource_tracker - resource_instance = device_tracker.figure_resource( - {"uuid": sub_node.res_content.uuid}) + # sub_node.res_content.uuid 已经是新UUID,需要用旧UUID去查找 + old_uuid = reverse_uuid_mapping.get(sub_node.res_content.uuid) + if old_uuid: + # 找到旧UUID,使用UUID查找 + resource_instance = device_tracker.figure_resource({"uuid": old_uuid}) + else: + # 未找到旧UUID,使用name查找 + resource_instance = device_tracker.figure_resource({"name": sub_node.res_content.name}) device_tracker.loop_update_uuid(resource_instance, uuid_mapping) else: logger.error("Slave模式不允许新增非设备节点下的物料") diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 3e2f4d49..3d5bd165 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -252,6 +252,8 @@ class HostNode(BaseROS2DeviceNode): ) # resources_config 通过各个设备的 resource_tracker 进行uuid更新,利用uuid_mapping # resources_config 的 root node 是 + # 创建反向映射:new_uuid -> old_uuid + reverse_uuid_mapping = {new_uuid: old_uuid for old_uuid, new_uuid in uuid_mapping.items()} for tree in resources_config.trees: node = tree.root_node if node.res_content.type == "device": @@ -260,8 +262,16 @@ class HostNode(BaseROS2DeviceNode): if sub_node.res_content.type != "device": # slave节点走c2s更新接口,拿到add自行update uuid device_tracker = self.devices_instances[node.res_content.id].resource_tracker - resource_instance = device_tracker.figure_resource( - {"uuid": sub_node.res_content.uuid}) + # sub_node.res_content.uuid 已经是新UUID,需要用旧UUID去查找 + old_uuid = reverse_uuid_mapping.get(sub_node.res_content.uuid) + if old_uuid: + # 找到旧UUID,使用UUID查找 + resource_instance = device_tracker.figure_resource({"uuid": old_uuid}) + else: + # 未找到旧UUID,使用name查找 + resource_instance = device_tracker.figure_resource( + {"name": sub_node.res_content.name} + ) device_tracker.loop_update_uuid(resource_instance, uuid_mapping) else: try: @@ -897,6 +907,7 @@ class HostNode(BaseROS2DeviceNode): uuid_list: List[str] = data["data"] with_children: bool = data["with_children"] from unilabos.app.web.client import http_client + resource_response = http_client.resource_tree_get(uuid_list, with_children) response.response = json.dumps(resource_response) @@ -920,6 +931,7 @@ class HostNode(BaseROS2DeviceNode): ) from unilabos.app.web.client import http_client + resource_start_time = time.time() uuid_mapping = http_client.resource_tree_update(resource_tree_set, "", False) success = bool(uuid_mapping) @@ -1254,7 +1266,9 @@ class HostNode(BaseROS2DeviceNode): "status": "success", } - def test_resource(self, resource: ResourceSlot, resources: List[ResourceSlot], device: DeviceSlot, devices: List[DeviceSlot]): + def test_resource( + self, resource: ResourceSlot, resources: List[ResourceSlot], device: DeviceSlot, devices: List[DeviceSlot] + ): return { "resources": ResourceTreeSet.from_plr_resources([resource, *resources]).dump(), "devices": [device, *devices], @@ -1280,9 +1294,7 @@ class HostNode(BaseROS2DeviceNode): else: self.lab_logger().warning("⚠️ 收到无效的Pong响应(缺少ping_id)") - def notify_resource_tree_update( - self, device_id: str, action: str, resource_uuid_list: List[str] - ) -> bool: + def notify_resource_tree_update(self, device_id: str, action: str, resource_uuid_list: List[str]) -> bool: """ 通知设备节点更新资源树 diff --git a/unilabos/ros/nodes/presets/workstation.py b/unilabos/ros/nodes/presets/workstation.py index b943b5a2..07e35ee6 100644 --- a/unilabos/ros/nodes/presets/workstation.py +++ b/unilabos/ros/nodes/presets/workstation.py @@ -1,13 +1,8 @@ import json import time import traceback -<<<<<<<< HEAD:unilabos/ros/nodes/presets/workstation.py from pprint import pformat from typing import List, Dict, Any, Optional, TYPE_CHECKING -======== -from pprint import pprint, saferepr, pformat -from typing import Union ->>>>>>>> main:unilabos/ros/nodes/presets/protocol_node.py import rclpy from rosidl_runtime_py import message_to_ordereddict @@ -198,13 +193,8 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): execute_callback=self._create_protocol_execute_callback(action_name, protocol_steps_generator), callback_group=ReentrantCallbackGroup(), ) -<<<<<<<< HEAD:unilabos/ros/nodes/presets/workstation.py self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}") return -======== - - self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}") ->>>>>>>> main:unilabos/ros/nodes/presets/protocol_node.py def _create_protocol_execute_callback(self, protocol_name, protocol_steps_generator): async def execute_protocol(goal_handle: ServerGoalHandle): @@ -255,15 +245,10 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): logs.append(step) elif isinstance(step, list): logs.append(step) -<<<<<<<< HEAD:unilabos/ros/nodes/presets/workstation.py self.lab_logger().info( f"Goal received: {protocol_kwargs}, running steps: " f"{json.dumps(logs, indent=4, ensure_ascii=False)}" ) -======== - self.lab_logger().info(f"Goal received: {protocol_kwargs}, running steps: " - f"{json.dumps(logs, indent=4, ensure_ascii=False)}") ->>>>>>>> main:unilabos/ros/nodes/presets/protocol_node.py time_start = time.time() time_overall = 100 @@ -278,7 +263,6 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): time.sleep(action["action_kwargs"]["time"]) step_results.append({"step": i + 1, "action": "wait", "result": "completed"}) else: -<<<<<<<< HEAD:unilabos/ros/nodes/presets/workstation.py try: result = await self.execute_single_action(**action) step_results.append({"step": i + 1, "action": action["action_name"], "result": result}) @@ -289,13 +273,6 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): step_results.append( {"step": i + 1, "action": action["action_name"], "result": ex.args[0]} ) -======== - result = await self.execute_single_action(**action) - step_results.append({"step": i + 1, "action": action["action_name"], "result": result}) - ret_info = json.loads(getattr(result, "return_info", "{}")) - if not ret_info.get("suc", False): - raise RuntimeError(f"Step {i + 1} failed.") ->>>>>>>> main:unilabos/ros/nodes/presets/protocol_node.py elif isinstance(action, list): # 如果是并行动作,同时执行 actions = action @@ -333,7 +310,6 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): except Exception as e: # 捕获并记录错误信息 -<<<<<<<< HEAD:unilabos/ros/nodes/presets/workstation.py str_step_results = [ { k: dict(message_to_ordereddict(v)) if k == "result" and hasattr(v, "SLOT_TYPES") else v @@ -341,9 +317,6 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): } for i in step_results ] -======== - str_step_results = [{k: dict(message_to_ordereddict(v)) if k == "result" and hasattr(v, "SLOT_TYPES") else v for k, v in i.items()} for i in step_results] ->>>>>>>> main:unilabos/ros/nodes/presets/protocol_node.py execution_error = f"{traceback.format_exc()}\n\nStep Result: {pformat(str_step_results)}" execution_success = False self.lab_logger().error(f"协议 {protocol_name} 执行出错: {str(e)} \n{traceback.format_exc()}") diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index 999ee254..da2b9271 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -1060,6 +1060,8 @@ class DeviceNodeResourceTracker(object): else: # 对于实例类型,需要特殊处理 uuid 字段 # 如果查找的是 unilabos_uuid,使用 getattr + if identifier_key == "uuid": + identifier_key = "unilabos_uuid" if hasattr(resource, identifier_key): if getattr(resource, identifier_key) == compare_value: res_list.append((parent_res, resource))