Files
Uni-Lab-OS/.cursor/skills/add-workstation/SKILL.md
2026-03-06 16:54:31 +08:00

16 KiB
Raw Blame History

name, description
name description
add-workstation Guide for adding new workstations to Uni-Lab-OS (接入新工作站). Walks through workstation type selection, sub-device composition, external system integration, driver creation, registry YAML, deck setup, and graph file configuration. Use when the user wants to add/integrate a new workstation, create a workstation driver, configure a station with sub-devices, set up deck and materials, or mentions 工作站/工站/station/workstation.

Uni-Lab-OS 工作站接入指南

工作站workstation是组合多个子设备的大型设备拥有独立的物料管理系统PLR Deck和工作流引擎。本指南覆盖从需求分析到验证的全流程。

前置知识:工作站接入基于 docs/ai_guides/add_device.md 的通用设备接入框架,但有显著差异。阅读本指南前无需先读通用指南。

第一步:确定工作站类型

向用户确认以下信息:

Q1: 工作站的业务场景?

类型 基类 适用场景 示例
Protocol 工作站 ProtocolNode 标准化学操作协议(过滤、转移、加热等) FilterProtocolStation
外部系统工作站 WorkstationBase 与外部 LIMS/MES 系统对接,有专属 API BioyondStation
硬件控制工作站 WorkstationBase 直接控制 PLC/硬件,无外部系统 CoinCellAssembly

Q2: 工作站英文名称?(如 my_reaction_station

Q3: 与外部系统的交互方式?

方式 适用场景 需要的配置
无外部系统 Protocol 工作站、纯硬件控制
HTTP API LIMS/MES 系统(如 Bioyond api_host, api_key
Modbus TCP PLC 控制 address, port
OPC UA 工业设备 url

Q4: 子设备组成?

  • 列出所有子设备(如反应器、泵、阀、传感器等)
  • 哪些是已有设备类型?哪些需要新增?
  • 子设备之间的硬件代理关系(如泵通过串口设备通信)

Q5: 物料管理需求?

  • 是否需要 Deck物料面板
  • 物料类型plate、tip_rack、bottle 等)
  • 是否需要与外部物料系统同步?

第二步:理解工作站架构

工作站与普通设备的核心差异:

维度 普通设备 工作站
基类 无(纯 Python 类) WorkstationBaseProtocolNode
ROS 节点 BaseROS2DeviceNode ROS2WorkstationNode
状态管理 self.data 字典 通常不用 self.data,用 @property 直接访问
子设备 children 列表,通过 self._children 访问
物料 self.deckPLR Deck
图文件角色 parent: nullparent: "<station>" parent: null,含 childrendeck

继承体系

WorkstationBase (ABC) → ProtocolNode (通用协议) / BioyondWorkstation (→ ReactionStation, DispensingStation) / CoinCellAssemblyWorkstation (硬件控制)

ROS 层

ROS2WorkstationNode 额外负责:初始化 children 子设备节点、为子设备创建 ActionClient、配置硬件代理、为 protocol_type 创建协议 ActionServer。


第三步:创建驱动文件

文件路径:unilabos/devices/workstation/<station_name>/<station_name>.py

模板 A基于外部系统的工作站

适用于与 LIMS/MES 等外部系统对接的场景。

import logging
from typing import Dict, Any, Optional, List
from pylabrobot.resources import Deck

from unilabos.devices.workstation.workstation_base import WorkstationBase

try:
    from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
except ImportError:
    ROS2WorkstationNode = None


class MyWorkstation(WorkstationBase):
    """工作站描述"""

    _ros_node: "ROS2WorkstationNode"

    def __init__(
        self,
        config: dict = None,
        deck: Optional[Deck] = None,
        protocol_type: list = None,
        **kwargs,
    ):
        super().__init__(deck=deck, **kwargs)
        self.config = config or {}
        self.logger = logging.getLogger(f"MyWorkstation")

        # 外部系统连接配置
        self.api_host = self.config.get("api_host", "")
        self.api_key = self.config.get("api_key", "")

        # 工作站业务状态(不同于 self.data 模式)
        self._status = "Idle"

    def post_init(self, ros_node: "ROS2WorkstationNode") -> None:
        super().post_init(ros_node)
        # 在这里启动后台服务、连接监控等

    # ============ 子设备访问 ============

    def _get_child_device(self, device_id: str):
        """通过 ID 获取子设备节点"""
        return self._children.get(device_id)

    # ============ 动作方法 ============

    async def scheduler_start(self, **kwargs) -> Dict[str, Any]:
        """启动调度器"""
        return {"success": True}

    async def create_order(self, json_str: str, **kwargs) -> Dict[str, Any]:
        """创建工单"""
        return {"success": True}

    # ============ 属性 ============

    @property
    def workflow_sequence(self) -> str:
        return "[]"

    @property
    def material_info(self) -> str:
        return "{}"

模板 B基于硬件控制的工作站

适用于直接与 PLC/硬件通信的场景。

import logging
from typing import Dict, Any, Optional
from pylabrobot.resources import Deck

from unilabos.devices.workstation.workstation_base import WorkstationBase

try:
    from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
except ImportError:
    ROS2WorkstationNode = None


class MyHardwareWorkstation(WorkstationBase):
    """硬件控制工作站"""

    _ros_node: "ROS2WorkstationNode"

    def __init__(
        self,
        config: dict = None,
        deck: Optional[Deck] = None,
        address: str = "192.168.1.100",
        port: str = "502",
        debug_mode: bool = False,
        *args,
        **kwargs,
    ):
        super().__init__(deck=deck, *args, **kwargs)
        self.config = config or {}
        self.address = address
        self.port = int(port)
        self.debug_mode = debug_mode
        self.logger = logging.getLogger("MyHardwareWorkstation")

        # 初始化通信客户端
        if not debug_mode:
            from unilabos.device_comms.modbus_plc.client import ModbusTcpClient
            self.client = ModbusTcpClient(host=self.address, port=self.port)
        else:
            self.client = None

    def post_init(self, ros_node: "ROS2WorkstationNode") -> None:
        super().post_init(ros_node)

    # ============ 硬件读写 ============

    def _read_register(self, name: str):
        """读取 Modbus 寄存器"""
        if self.debug_mode:
            return 0
        # 实际读取逻辑
        pass

    # ============ 动作方法 ============

    async def start_process(self, **kwargs) -> Dict[str, Any]:
        """启动加工流程"""
        return {"success": True}

    async def stop_process(self, **kwargs) -> Dict[str, Any]:
        """停止加工流程"""
        return {"success": True}

    # ============ 属性(从硬件实时读取)============

    @property
    def sys_status(self) -> str:
        return str(self._read_register("SYS_STATUS"))

模板 CProtocol 工作站

适用于标准化学操作协议的场景,直接使用 ProtocolNode

from typing import List, Optional
from pylabrobot.resources import Resource as PLRResource

from unilabos.devices.workstation.workstation_base import ProtocolNode


class MyProtocolStation(ProtocolNode):
    """Protocol 工作站 — 使用标准化学操作协议"""

    def __init__(
        self,
        protocol_type: List[str],
        deck: Optional[PLRResource] = None,
        *args,
        **kwargs,
    ):
        super().__init__(protocol_type=protocol_type, deck=deck, *args, **kwargs)

Protocol 工作站通常不需要自定义驱动类,直接使用 ProtocolNode 并在注册表和图文件中配置 protocol_type 即可。


第四步:创建子设备驱动(如需要)

工作站的子设备本身是独立设备。按 docs/ai_guides/add_device.md 的标准流程创建。

子设备的关键约束:

  • 在图文件中 parent 指向工作站 ID
  • 图文件中在工作站的 children 数组里列出
  • 如需硬件代理,在子设备的 config.hardware_interface.name 指向通信设备 ID

第五步:创建注册表 YAML

路径:unilabos/registry/devices/<station_name>.yaml

最小配置

my_workstation:
  category:
    - workstation
  class:
    module: unilabos.devices.workstation.my_station.my_station:MyWorkstation
    type: python

启动时 --complete_registry 自动补全 status_typesaction_value_mappings

完整配置参考

my_workstation:
  description: "我的工作站"
  version: "1.0.0"
  category:
    - workstation
    - my_category
  class:
    module: unilabos.devices.workstation.my_station.my_station:MyWorkstation
    type: python
    status_types:
      workflow_sequence: String
      material_info: String
    action_value_mappings:
      scheduler_start:
        type: UniLabJsonCommandAsync
        goal: {}
        result:
          success: success
      create_order:
        type: UniLabJsonCommandAsync
        goal:
          json_str: json_str
        result:
          success: success
  init_param_schema:
    config:
      type: object
    deck:
      type: object
    protocol_type:
      type: array

子设备注册表

子设备有独立的注册表文件,需要在 category 中包含工作站标识:

my_reactor:
  category:
    - reactor
    - my_workstation
  class:
    module: unilabos.devices.workstation.my_station.my_reactor:MyReactor
    type: python

第六步:配置 Deck 资源(如需要)

如果工作站有物料管理需求,需要定义 Deck 类。

使用已有 Deck 类

查看 unilabos/resources/ 目录下是否有适用的 Deck 类。

创建自定义 Deck

unilabos/resources/<category>/decks.py 中定义:

from pylabrobot.resources import Deck
from pylabrobot.resources.coordinate import Coordinate


def MyStation_Deck(name: str = "MyStation_Deck") -> Deck:
    deck = Deck(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0)
    # 在 deck 上定义子资源位置carrier、plate 等)
    return deck

unilabos/resources/<category>/ 下注册或通过注册表引用。


第七步:配置图文件

图文件路径:unilabos/test/experiments/<station_name>.json

完整结构

{
    "nodes": [
        {
            "id": "my_station",
            "name": "my_station",
            "children": ["my_deck", "sub_device_1", "sub_device_2"],
            "parent": null,
            "type": "device",
            "class": "my_workstation",
            "position": {"x": 0, "y": 0, "z": 0},
            "config": {
                "api_host": "http://192.168.1.100:8080",
                "api_key": "YOUR_KEY"
            },
            "deck": {
                "data": {
                    "_resource_child_name": "my_deck",
                    "_resource_type": "unilabos.resources.my_module.decks:MyStation_Deck"
                }
            },
            "size_x": 2700.0,
            "size_y": 1080.0,
            "size_z": 1500.0,
            "protocol_type": [],
            "data": {}
        },
        {
            "id": "my_deck",
            "name": "my_deck",
            "children": [],
            "parent": "my_station",
            "type": "deck",
            "class": "MyStation_Deck",
            "position": {"x": 0, "y": 0, "z": 0},
            "config": {
                "type": "MyStation_Deck",
                "setup": true,
                "rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"}
            },
            "data": {}
        },
        {
            "id": "sub_device_1",
            "name": "sub_device_1",
            "children": [],
            "parent": "my_station",
            "type": "device",
            "class": "sub_device_registry_name",
            "position": {"x": 100, "y": 0, "z": 0},
            "config": {},
            "data": {}
        }
    ]
}

图文件规则

字段 说明
id 节点唯一标识,与 children 数组中的引用一致
children 包含 deck ID 和所有子设备 ID
parent 工作站节点为 null;子设备/deck 指向工作站 ID
type 工作站和子设备为 "device"deck 为 "deck"
class 对应注册表中的设备名
deck.data._resource_child_name 必须与 deck 节点的 id 一致
deck.data._resource_type Deck 工厂函数的完整 Python 路径
protocol_type Protocol 工作站填入协议名列表;否则为 []
config 传入驱动 __init__config 参数

第八步:验证

# 1. 模块可导入
python -c "from unilabos.devices.workstation.<name>.<name> import <ClassName>"

# 2. 注册表补全
unilab -g <graph>.json --complete_registry

# 3. 启动测试
unilab -g <graph>.json

高级模式

实现外部系统对接型工作站时,详见 reference.mdRPC 客户端、HTTP 回调服务、连接监控、Config 结构模式material_type_mappings / warehouse_mapping / workflow_mappings、ResourceSynchronizer、update_resource、工作流序列、站间物料转移、post_init 完整模式。


关键规则

  1. __init__ 必须接受 deck**kwargsWorkstationBase.__init__ 需要 deck 参数
  2. 通过 self._children 访问子设备 — 不要自行维护子设备引用
  3. post_init 中启动后台服务 — 不要在 __init__ 中启动网络连接
  4. 异步方法使用 await self._ros_node.sleep() — 禁止 time.sleep()asyncio.sleep()
  5. 子设备在图文件中声明 — 不在驱动代码中创建子设备实例
  6. deck 配置中的 _resource_child_name 必须与 deck 节点 ID 一致
  7. Protocol 工作站优先使用 ProtocolNode — 不需要自定义类

工作流清单

工作站接入进度:
- [ ] 1. 确定工作站类型Protocol / 外部系统 / 硬件控制)
- [ ] 2. 确认子设备组成和物料需求
- [ ] 3. 创建工作站驱动 unilabos/devices/workstation/<name>/<name>.py
- [ ] 4. 创建子设备驱动(如需要,按 add_device.md 流程)
- [ ] 5. 创建注册表 unilabos/registry/devices/<name>.yaml
- [ ] 6. 创建/选择 Deck 资源类(如需要)
- [ ] 7. 配置图文件 unilabos/test/experiments/<name>.json
- [ ] 8. 验证:可导入 + 注册表补全 + 启动测试

现有工作站参考

工作站 注册表名 驱动类 类型
Protocol 通用 workstation ProtocolNode Protocol
Bioyond 反应站 reaction_station.bioyond BioyondReactionStation 外部系统
Bioyond 配液站 bioyond_dispensing_station BioyondDispensingStation 外部系统
纽扣电池组装 coincellassemblyworkstation_device CoinCellAssemblyWorkstation 硬件控制

参考文件路径

  • 基类: unilabos/devices/workstation/workstation_base.py
  • Bioyond 基类: unilabos/devices/workstation/bioyond_studio/station.py
  • 反应站: unilabos/devices/workstation/bioyond_studio/reaction_station/reaction_station.py
  • 配液站: unilabos/devices/workstation/bioyond_studio/dispensing_station/dispensing_station.py
  • 纽扣电池: unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py
  • ROS 节点: unilabos/ros/nodes/presets/workstation.py
  • 图文件: unilabos/test/experiments/reaction_station_bioyond.json, dispensing_station_bioyond.json