Files
Uni-Lab-OS/docs/user_guide/graph_files.md
Xuwznln 75f09034ff update docs, test examples
fix liquid_handler init bug
2025-11-18 18:42:27 +08:00

24 KiB
Raw Blame History

设备图文件说明

设备图文件定义了实验室中所有设备、资源及其连接关系。本文档说明如何创建和使用设备图文件。

概述

设备图文件采用 JSON 格式,节点定义基于 ResourceDict 标准模型(定义在 unilabos.ros.nodes.resource_tracker)。系统会自动处理旧格式并转换为标准格式,确保向后兼容性。

核心概念:

  • Nodes节点: 代表设备或资源,通过 parent 字段建立层级关系
  • Links连接: 可选的连接关系定义,用于展示设备间的物理或通信连接
  • UUID: 全局唯一标识符,用于跨系统的资源追踪
  • 自动转换: 旧格式会通过 ResourceDictInstance.get_resource_instance_from_dict() 自动转换

文件格式

Uni-Lab 支持两种格式的设备图文件:

JSON 格式(推荐)

优点:

  • 易于编辑和阅读
  • 支持注释(使用预处理)
  • 与 Web 界面完全兼容
  • 便于版本控制

示例: workshop1.json

GraphML 格式

优点:

  • 可用图形化工具编辑(如 yEd
  • 适合复杂拓扑可视化

示例: setup.graphml

JSON 文件结构

一个完整的 JSON 设备图文件包含两个主要部分:

{
  "nodes": [
    /* 设备和资源节点 */
  ],
  "links": [
    /* 连接关系(可选)*/
  ]
}

Nodes节点

每个节点代表一个设备或资源。节点的定义遵循 ResourceDict 标准模型:

{
  "id": "liquid_handler_1",
  "uuid": "550e8400-e29b-41d4-a716-446655440000",
  "name": "液体处理工作站",
  "type": "device",
  "class": "liquid_handler",
  "config": {
    "port": "/dev/ttyUSB0",
    "baudrate": 9600
  },
  "data": {},
  "position": {
    "x": 100,
    "y": 200
  },
  "parent": null
}

字段说明(基于 ResourceDict 标准定义):

字段 必需 说明 示例 默认值
id 唯一标识符 "pump_1" -
uuid 全局唯一标识符 (UUID) "550e8400-e29b-41d4-a716-446655440000" 自动生成
name 显示名称 "主反应泵" -
type 节点类型 "device", "resource", "container", "deck" -
class 设备/资源类别 "liquid_handler", "syringepump.runze" ""
config Python 类的初始化参数 {"port": "COM3"} {}
data 资源的运行状态数据 {"status": "Idle", "position": 0.0} {}
position 在图中的位置 {"x": 100, "y": 200} 或完整的 pose 结构 -
pose 完整的 3D 位置信息 参见下文 -
parent 父节点 ID "deck_1" null
parent_uuid 父节点 UUID "550e8400-..." null
children 子节点 ID 列表(旧格式) ["child1", "child2"] -
description 资源描述 "用于精确控制试剂A的加料速率" ""
schema 资源 schema 定义 {} {}
model 资源 3D 模型信息 {} {}
icon 资源图标 "pump.webp" ""
extra 额外的自定义数据 {"custom_field": "value"} {}

Position 和 Pose位置信息

简单格式(旧格式,兼容):

"position": {
  "x": 100,
  "y": 200,
  "z": 0
}

完整格式(推荐):

"pose": {
  "size": {
    "width": 127.76,
    "height": 85.48,
    "depth": 10.0
  },
  "scale": {
    "x": 1.0,
    "y": 1.0,
    "z": 1.0
  },
  "layout": "x-y",
  "position": {
    "x": 100,
    "y": 200,
    "z": 0
  },
  "position3d": {
    "x": 100,
    "y": 200,
    "z": 0
  },
  "rotation": {
    "x": 0,
    "y": 0,
    "z": 0
  },
  "cross_section_type": "rectangle"
}

Links连接

定义节点之间的连接关系(可选,主要用于物理连接或通信关系的可视化):

{
  "source": "pump_1",
  "target": "reactor_1",
  "sourceHandle": "output",
  "targetHandle": "input",
  "type": "physical"
}

字段说明:

字段 必需 说明 示例
source 源节点 ID "pump_1"
target 目标节点 ID "reactor_1"
sourceHandle 源节点的连接点 "output"
targetHandle 目标节点的连接点 "input"
type 连接类型 "physical", "communication"
port 端口映射信息 {"source": "port1", "target": "port2"}

注意: Links 主要用于图形化展示和文档说明,父子关系通过 parent 字段定义,不依赖 links。

完整示例

示例 1液体处理工作站PRCXI9300

这是一个真实的液体处理工作站配置,包含设备、工作台和多个板资源。

文件位置: test/experiments/prcxi_9300.json

{
  "nodes": [
    {
      "id": "PRCXI9300",
      "name": "PRCXI9300",
      "parent": null,
      "type": "device",
      "class": "liquid_handler.prcxi",
      "position": {
        "x": 0,
        "y": 0,
        "z": 0
      },
      "config": {
        "deck": {
          "_resource_child_name": "PRCXI_Deck_9300",
          "_resource_type": "unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Deck"
        },
        "host": "10.181.214.132",
        "port": 9999,
        "timeout": 10.0,
        "axis": "Left",
        "channel_num": 8,
        "setup": false,
        "debug": true,
        "simulator": true,
        "matrix_id": "71593"
      },
      "data": {},
      "children": ["PRCXI_Deck_9300"]
    },
    {
      "id": "PRCXI_Deck_9300",
      "name": "PRCXI_Deck_9300",
      "parent": "PRCXI9300",
      "type": "deck",
      "class": "",
      "position": {
        "x": 0,
        "y": 0,
        "z": 0
      },
      "config": {
        "type": "PRCXI9300Deck",
        "size_x": 100,
        "size_y": 100,
        "size_z": 100,
        "rotation": {
          "x": 0,
          "y": 0,
          "z": 0,
          "type": "Rotation"
        },
        "category": "deck"
      },
      "data": {},
      "children": [
        "RackT1",
        "PlateT2",
        "trash",
        "PlateT4",
        "PlateT5",
        "PlateT6"
      ]
    },
    {
      "id": "RackT1",
      "name": "RackT1",
      "parent": "PRCXI_Deck_9300",
      "type": "tip_rack",
      "class": "",
      "position": {
        "x": 0,
        "y": 0,
        "z": 0
      },
      "config": {
        "type": "TipRack",
        "size_x": 127.76,
        "size_y": 85.48,
        "size_z": 100
      },
      "data": {},
      "children": []
    }
  ]
}

关键点:

  • 使用 parent 字段建立层级关系PRCXI9300 → Deck → Rack/Plate
  • 使用 children 字段(旧格式)列出子节点
  • config 中包含设备特定的连接参数
  • data 存储运行时状态
  • position 使用简单的 x/y/z 坐标

这是一个格林纳德反应的流动化学工作站配置,展示了完整的设备连接和通信关系。

文件位置: test/experiments/Grignard_flow_batchreact_single_pumpvalve.json

{
  "nodes": [
    {
      "id": "YugongStation",
      "name": "愚公常量合成工作站",
      "parent": null,
      "type": "device",
      "class": "workstation",
      "position": {
        "x": 620.6111111111111,
        "y": 171,
        "z": 0
      },
      "config": {
        "protocol_type": [
          "PumpTransferProtocol",
          "CleanProtocol",
          "SeparateProtocol",
          "EvaporateProtocol"
        ]
      },
      "data": {},
      "children": [
        "serial_pump",
        "pump_reagents",
        "flask_CH2Cl2",
        "reactor",
        "pump_workup",
        "separator_controller",
        "flask_separator",
        "rotavap",
        "column"
      ]
    },
    {
      "id": "serial_pump",
      "name": "serial_pump",
      "parent": "YugongStation",
      "type": "device",
      "class": "serial",
      "position": {
        "x": 620.6111111111111,
        "y": 171,
        "z": 0
      },
      "config": {
        "port": "COM7",
        "baudrate": 9600
      },
      "data": {},
      "children": []
    },
    {
      "id": "pump_reagents",
      "name": "pump_reagents",
      "parent": "YugongStation",
      "type": "device",
      "class": "syringepump.runze",
      "position": {
        "x": 620.6111111111111,
        "y": 171,
        "z": 0
      },
      "config": {
        "port": "/devices/PumpBackbone/Serial/serialwrite",
        "address": "1",
        "max_volume": 25.0
      },
      "data": {
        "max_velocity": 1.0,
        "position": 0.0,
        "status": "Idle",
        "valve_position": "0"
      },
      "children": []
    },
    {
      "id": "reactor",
      "name": "reactor",
      "parent": "YugongStation",
      "type": "container",
      "class": null,
      "position": {
        "x": 430.4087301587302,
        "y": 428,
        "z": 0
      },
      "config": {},
      "data": {},
      "children": []
    }
  ],
  "links": [
    {
      "source": "pump_reagents",
      "target": "serial_pump",
      "type": "communication",
      "port": {
        "pump_reagents": "port",
        "serial_pump": "port"
      }
    },
    {
      "source": "pump_workup",
      "target": "serial_pump",
      "type": "communication",
      "port": {
        "pump_workup": "port",
        "serial_pump": "port"
      }
    }
  ]
}

关键点:

  • 多级设备层次:工作站包含多个子设备和容器
  • links 定义通信关系(泵通过串口连接)
  • data 字段存储设备状态(如泵的位置、速度等)
  • class 可以使用点号分层(如 "syringepump.runze"
  • 容器的 class 可以为 null

格式兼容性和转换

旧格式自动转换

Uni-Lab 使用 ResourceDictInstance.get_resource_instance_from_dict() 方法自动处理旧格式的节点数据,确保向后兼容性。

自动转换规则:

  1. 自动生成缺失字段:

    # 如果缺少 id使用 name 作为 id
    if "id" not in content:
        content["id"] = content["name"]
    
    # 如果缺少 uuid自动生成
    if "uuid" not in content:
        content["uuid"] = str(uuid.uuid4())
    
  2. Position 格式转换:

    # 旧格式:简单的 x/y 坐标
    "position": {"x": 100, "y": 200}
    
    # 自动转换为新格式
    "position": {
        "position": {"x": 100, "y": 200}
    }
    
  3. 默认值填充:

    # 自动填充空字段
    if not content.get("class"):
        content["class"] = ""
    if not content.get("config"):
        content["config"] = {}
    if not content.get("data"):
        content["data"] = {}
    if not content.get("extra"):
        content["extra"] = {}
    
  4. Pose 字段同步:

    # 如果没有 pose使用 position
    if "pose" not in content:
        content["pose"] = content.get("position", {})
    

使用示例

from unilabos.ros.nodes.resource_tracker import ResourceDictInstance

# 旧格式节点
old_format_node = {
    "name": "pump_1",
    "type": "device",
    "class": "syringepump",
    "position": {"x": 100, "y": 200}
}

# 自动转换为标准格式
instance = ResourceDictInstance.get_resource_instance_from_dict(old_format_node)

# 访问标准化后的数据
print(instance.res_content.id)      # "pump_1"
print(instance.res_content.uuid)    # 自动生成的 UUID
print(instance.res_content.config)  # {}
print(instance.res_content.data)    # {}

格式迁移建议

虽然系统会自动处理旧格式,但建议在新文件中使用完整的标准格式:

字段 旧格式(兼容) 新格式(推荐)
标识符 id 或仅 name id + uuid
位置 "position": {"x": 100, "y": 200} 完整的 pose 结构
父节点 "parent": "parent_id" "parent": "parent_id" + "parent_uuid": "..."
配置 可省略 显式设置为 {}
数据 可省略 显式设置为 {}

节点类型详解

Device 节点

设备节点代表实际的硬件设备:

{
  "id": "device_id",
  "name": "设备名称",
  "type": "device",
  "class": "设备类别",
  "parent": null,
  "config": {
    "port": "COM3"
  },
  "data": {},
  "children": []
}

常见设备类别:

  • liquid_handler: 液体处理工作站
  • liquid_handler.prcxi: PRCXI 液体处理工作站
  • syringepump: 注射泵
  • syringepump.runze: 润泽注射泵
  • heaterstirrer: 加热搅拌器
  • balance: 天平
  • reactor_vessel: 反应釜
  • serial: 串口通信设备
  • workstation: 自动化工作站

Resource 节点

资源节点代表物料容器、载具等:

{
  "id": "resource_id",
  "name": "资源名称",
  "type": "resource",
  "class": "资源类别",
  "parent": "父节点ID",
  "config": {
    "size_x": 127.76,
    "size_y": 85.48,
    "size_z": 100
  },
  "data": {},
  "children": []
}

常见资源类型:

  • deck: 工作台/甲板
  • plate: 板96 孔板等)
  • tip_rack: 枪头架
  • tube: 试管
  • container: 容器
  • well: 孔位
  • bottle_carrier: 瓶架

Handle连接点

每个设备和资源可以有多个连接点handles用于定义可以连接的接口。

查看可用 handles

设备和资源的可用 handles 定义在注册表中:

# 设备注册表示例
liquid_handler:
  handles:
    - handler_key: pipette
      io_type: source
    - handler_key: deck
      io_type: target

常见 handles

设备类型 Source Handles Target Handles
output input
反应釜 output, vessel input
液体处理器 pipette deck
wells access

使用 Web 界面创建图文件

Uni-Lab 提供 Web 界面来可视化创建和编辑设备图:

1. 启动 Uni-Lab

unilab

2. 访问 Web 界面

打开浏览器访问 http://localhost:8002

3. 图形化编辑

  • 拖拽添加设备和资源
  • 连线建立连接关系
  • 编辑节点属性
  • 保存为 JSON 文件

4. 导出图文件

点击"导出"按钮,下载 JSON 文件到本地。

从云端获取图文件

如果不指定-g参数Uni-Lab 会自动从云端获取:

# 使用云端配置
unilab

# 日志会显示:
# [INFO] 未指定设备加载文件路径尝试从HTTP获取...
# [INFO] 联网获取设备加载文件成功

云端图文件管理:

  1. 登录 https://uni-lab.bohrium.com
  2. 进入"设备配置"
  3. 创建或编辑配置
  4. 保存到云端

本地启动时会自动同步最新配置。

调试图文件

验证 JSON 格式

# 使用Python验证
python -c "import json; json.load(open('workshop1.json'))"

# 使用在线工具
# https://jsonlint.com/

检查节点引用

确保:

  • 所有links中的sourcetarget都存在于nodes
  • parent字段指向的节点存在
  • class字段对应的设备/资源在注册表中存在

启动时验证

# Uni-Lab启动时会验证图文件
unilab -g workshop1.json

# 查看日志中的错误或警告
# [ERROR] 节点 xxx 的source端点 yyy 不存在
# [WARNING] 节点 zzz missing 'name', defaulting to ...

最佳实践

1. 命名规范

{
  "id": "pump_reagent_1", // 小写+下划线,描述性
  "name": "试剂进料泵A", // 中文显示名称
  "class": "syringepump" // 使用注册表中的精确名称
}

2. 层级组织

host_node (主节点)
└── liquid_handler_1 (设备)
    └── deck_1 (资源)
        ├── tiprack_1 (资源)
        ├── plate_1 (资源)
        └── reservoir_1 (资源)

3. 配置分离

将设备特定配置放在config中:

{
  "id": "pump_1",
  "class": "syringepump",
  "config": {
    "port": "COM3", // 设备特定
    "max_flow_rate": 10, // 设备特定
    "volume": 50 // 设备特定
  }
}

4. 版本控制

# 使用Git管理图文件
git add workshop1.json
git commit -m "Add new liquid handler configuration"

# 使用有意义的文件名
workshop_v1.json
workshop_production.json
workshop_test.json

5. 注释(通过描述字段)

虽然 JSON 不支持注释,但可以使用description字段:

{
  "id": "pump_1",
  "name": "进料泵",
  "description": "用于精确控制试剂A的加料速率最大流速10mL/min",
  "class": "syringepump"
}

示例文件位置

Uni-Lab 在安装时已预置了 40+ 个真实的设备图文件示例,位于 unilabos/test/experiments/ 目录。这些都是真实项目中使用的配置文件,可以直接使用或作为参考。

📁 主要示例文件

test/experiments/
├── workshop.json                                 # 综合工作台(推荐新手)
├── empty_devices.json                            # 空设备配置(最小化)
├── prcxi_9300.json                               # PRCXI液体处理工作站本文示例1
├── prcxi_9320.json                               # PRCXI 9320工作站
├── biomek.json                                   # Biomek液体处理工作站
├── Grignard_flow_batchreact_single_pumpvalve.json # 格林纳德反应工作站本文示例2
├── dispensing_station_bioyond.json               # Bioyond配液站
├── reaction_station_bioyond.json                 # Bioyond反应站
├── HPLC.json                                     # HPLC分析系统
├── plr_test.json                                 # PyLabRobot测试配置
├── lidocaine-graph.json                          # 利多卡因合成工作站
├── opcua_example.json                            # OPC UA设备集成示例
│
├── mock_devices/                                 # 虚拟设备(用于离线测试)
│   ├── mock_all.json                             # 完整虚拟设备集
│   ├── mock_pump.json                            # 虚拟泵
│   ├── mock_stirrer.json                         # 虚拟搅拌器
│   ├── mock_heater.json                          # 虚拟加热器
│   └── ...                                       # 更多虚拟设备
│
├── Protocol_Test_Station/                        # 协议测试工作站
│   ├── pumptransfer_test_station.json            # 泵转移协议测试
│   ├── heatchill_protocol_test_station.json      # 加热冷却协议测试
│   ├── filter_protocol_test_station.json         # 过滤协议测试
│   └── ...                                       # 更多协议测试
│
└── comprehensive_protocol/                       # 综合协议示例
    ├── comprehensive_station.json                # 综合工作站
    └── comprehensive_slim.json                   # 精简版综合工作站

🚀 快速使用

无需下载或创建,直接使用 -g 参数指定路径:

# 使用简单工作台(推荐新手)
unilab --ak your_ak --sk your_sk -g test/experiments/workshop.json

# 使用虚拟设备(无需真实硬件)
unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json

# 使用 PRCXI 液体处理工作站
unilab --ak your_ak --sk your_sk -g test/experiments/prcxi_9300.json

# 使用格林纳德反应工作站
unilab --ak your_ak --sk your_sk -g test/experiments/Grignard_flow_batchreact_single_pumpvalve.json

📚 文件分类

类别 说明 文件数量
主工作站 完整的实验工作站配置 15+
虚拟设备 用于开发测试的 mock 设备 10+
协议测试 各种实验协议的测试配置 12+
综合示例 包含多种协议的综合工作站 3+

这些文件展示了不同场景下的设备图配置,涵盖液体处理、有机合成、分析检测等多个领域,是学习和创建自己配置的绝佳参考。

快速参考ResourceDict 完整字段列表

基于 unilabos.ros.nodes.resource_tracker.ResourceDict 的完整字段定义:

class ResourceDict(BaseModel):
    # === 基础标识 ===
    id: str                          # 资源ID必需
    uuid: str                        # 全局唯一标识符(自动生成)
    name: str                        # 显示名称(必需)

    # === 类型和分类 ===
    type: Union[Literal["device"], str]  # 节点类型(必需)
    klass: str                       # 资源类别alias="class",必需)

    # === 层级关系 ===
    parent: Optional[ResourceDict]   # 父资源对象(不序列化)
    parent_uuid: Optional[str]       # 父资源UUID

    # === 位置和姿态 ===
    position: ResourceDictPosition   # 位置信息
    pose: ResourceDictPosition       # 姿态信息(推荐使用)

    # === 配置和数据 ===
    config: Dict[str, Any]           # 设备配置参数
    data: Dict[str, Any]             # 运行时状态数据
    extra: Dict[str, Any]            # 额外自定义数据

    # === 元数据 ===
    description: str                 # 资源描述
    resource_schema: Dict[str, Any]  # schema定义alias="schema"
    model: Dict[str, Any]            # 3D模型信息
    icon: str                        # 图标路径

Position/Pose 结构:

class ResourceDictPosition(BaseModel):
    size: ResourceDictPositionSize           # width, height, depth
    scale: ResourceDictPositionScale         # x, y, z
    layout: Literal["2d", "x-y", "z-y", "x-z"]
    position: ResourceDictPositionObject     # x, y, z
    position3d: ResourceDictPositionObject   # x, y, z
    rotation: ResourceDictPositionObject     # x, y, z
    cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"]

下一步

  • {doc}../boot_examples/index - 查看完整启动示例
  • {doc}../developer_guide/add_device - 了解如何添加新设备
  • {doc}06_troubleshooting - 图文件相关问题排查
  • 源码参考: unilabos/ros/nodes/resource_tracker.py - ResourceDict 标准定义

获取帮助

  • 在 Web 界面中使用模板创建
  • 参考示例文件:test/experiments/ 目录
  • 查看 ResourceDict 源码了解完整定义
  • GitHub 讨论区