mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-30 09:33:09 +00:00
add:skill&agent
This commit is contained in:
500
.cursor/skills/add-workstation/SKILL.md
Normal file
500
.cursor/skills/add-workstation/SKILL.md
Normal file
@@ -0,0 +1,500 @@
|
||||
---
|
||||
name: add-workstation
|
||||
description: 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 类) | `WorkstationBase` 或 `ProtocolNode` |
|
||||
| ROS 节点 | `BaseROS2DeviceNode` | `ROS2WorkstationNode` |
|
||||
| 状态管理 | `self.data` 字典 | 通常不用 `self.data`,用 `@property` 直接访问 |
|
||||
| 子设备 | 无 | `children` 列表,通过 `self._children` 访问 |
|
||||
| 物料 | 无 | `self.deck`(PLR Deck) |
|
||||
| 图文件角色 | `parent: null` 或 `parent: "<station>"` | `parent: null`,含 `children` 和 `deck` |
|
||||
|
||||
### 继承体系
|
||||
|
||||
`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 等外部系统对接的场景。
|
||||
|
||||
```python
|
||||
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/硬件通信的场景。
|
||||
|
||||
```python
|
||||
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"))
|
||||
```
|
||||
|
||||
### 模板 C:Protocol 工作站
|
||||
|
||||
适用于标准化学操作协议的场景,直接使用 `ProtocolNode`。
|
||||
|
||||
```python
|
||||
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`
|
||||
|
||||
### 最小配置
|
||||
|
||||
```yaml
|
||||
my_workstation:
|
||||
category:
|
||||
- workstation
|
||||
class:
|
||||
module: unilabos.devices.workstation.my_station.my_station:MyWorkstation
|
||||
type: python
|
||||
```
|
||||
|
||||
启动时 `--complete_registry` 自动补全 `status_types` 和 `action_value_mappings`。
|
||||
|
||||
### 完整配置参考
|
||||
|
||||
```yaml
|
||||
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` 中包含工作站标识:
|
||||
|
||||
```yaml
|
||||
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` 中定义:
|
||||
|
||||
```python
|
||||
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`
|
||||
|
||||
### 完整结构
|
||||
|
||||
```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` 参数 |
|
||||
|
||||
---
|
||||
|
||||
## 第八步:验证
|
||||
|
||||
```bash
|
||||
# 1. 模块可导入
|
||||
python -c "from unilabos.devices.workstation.<name>.<name> import <ClassName>"
|
||||
|
||||
# 2. 注册表补全
|
||||
unilab -g <graph>.json --complete_registry
|
||||
|
||||
# 3. 启动测试
|
||||
unilab -g <graph>.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 高级模式
|
||||
|
||||
实现外部系统对接型工作站时,详见 [reference.md](reference.md):RPC 客户端、HTTP 回调服务、连接监控、Config 结构模式(material_type_mappings / warehouse_mapping / workflow_mappings)、ResourceSynchronizer、update_resource、工作流序列、站间物料转移、post_init 完整模式。
|
||||
|
||||
---
|
||||
|
||||
## 关键规则
|
||||
|
||||
1. **`__init__` 必须接受 `deck` 和 `**kwargs`** — `WorkstationBase.__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`
|
||||
371
.cursor/skills/add-workstation/reference.md
Normal file
371
.cursor/skills/add-workstation/reference.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# 工作站高级模式参考
|
||||
|
||||
本文件是 SKILL.md 的补充,包含外部系统集成、物料同步、配置结构等高级模式。
|
||||
Agent 在需要实现这些功能时按需阅读。
|
||||
|
||||
---
|
||||
|
||||
## 1. 外部系统集成模式
|
||||
|
||||
### 1.1 RPC 客户端
|
||||
|
||||
与外部 LIMS/MES 系统通信的标准模式。继承 `BaseRequest`,所有接口统一用 POST。
|
||||
|
||||
```python
|
||||
from unilabos.device_comms.rpc import BaseRequest
|
||||
|
||||
|
||||
class MySystemRPC(BaseRequest):
|
||||
"""外部系统 RPC 客户端"""
|
||||
|
||||
def __init__(self, host: str, api_key: str):
|
||||
super().__init__(host)
|
||||
self.api_key = api_key
|
||||
|
||||
def _request(self, endpoint: str, data: dict = None) -> dict:
|
||||
return self.post(
|
||||
url=f"{self.host}/api/{endpoint}",
|
||||
params={
|
||||
"apiKey": self.api_key,
|
||||
"requestTime": self.get_current_time_iso8601(),
|
||||
"data": data or {},
|
||||
},
|
||||
)
|
||||
|
||||
def query_status(self) -> dict:
|
||||
return self._request("status/query")
|
||||
|
||||
def create_order(self, order_data: dict) -> dict:
|
||||
return self._request("order/create", order_data)
|
||||
```
|
||||
|
||||
参考:`unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py`(`BioyondV1RPC`)
|
||||
|
||||
### 1.2 HTTP 回调服务
|
||||
|
||||
接收外部系统报送的标准模式。使用 `WorkstationHTTPService`,在 `post_init` 中启动。
|
||||
|
||||
```python
|
||||
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
|
||||
|
||||
|
||||
class MyWorkstation(WorkstationBase):
|
||||
def __init__(self, config=None, deck=None, **kwargs):
|
||||
super().__init__(deck=deck, **kwargs)
|
||||
self.config = config or {}
|
||||
http_cfg = self.config.get("http_service_config", {})
|
||||
self._http_service_config = {
|
||||
"host": http_cfg.get("http_service_host", "127.0.0.1"),
|
||||
"port": http_cfg.get("http_service_port", 8080),
|
||||
}
|
||||
self.http_service = None
|
||||
|
||||
def post_init(self, ros_node):
|
||||
super().post_init(ros_node)
|
||||
self.http_service = WorkstationHTTPService(
|
||||
workstation_instance=self,
|
||||
host=self._http_service_config["host"],
|
||||
port=self._http_service_config["port"],
|
||||
)
|
||||
self.http_service.start()
|
||||
```
|
||||
|
||||
**HTTP 服务路由**(固定端点,由 `WorkstationHTTPHandler` 自动分发):
|
||||
|
||||
| 端点 | 调用的工作站方法 |
|
||||
|------|-----------------|
|
||||
| `/report/step_finish` | `process_step_finish_report(report_request)` |
|
||||
| `/report/sample_finish` | `process_sample_finish_report(report_request)` |
|
||||
| `/report/order_finish` | `process_order_finish_report(report_request, used_materials)` |
|
||||
| `/report/material_change` | `process_material_change_report(report_data)` |
|
||||
| `/report/error_handling` | `handle_external_error(error_data)` |
|
||||
|
||||
实现对应方法即可接收回调:
|
||||
|
||||
```python
|
||||
def process_step_finish_report(self, report_request) -> Dict[str, Any]:
|
||||
"""处理步骤完成报告"""
|
||||
step_name = report_request.data.get("stepName")
|
||||
return {"success": True, "message": f"步骤 {step_name} 已处理"}
|
||||
|
||||
def process_order_finish_report(self, report_request, used_materials) -> Dict[str, Any]:
|
||||
"""处理订单完成报告"""
|
||||
order_code = report_request.data.get("orderCode")
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
参考:`unilabos/devices/workstation/workstation_http_service.py`
|
||||
|
||||
### 1.3 连接监控
|
||||
|
||||
独立线程周期性检测外部系统连接状态,状态变化时发布 ROS 事件。
|
||||
|
||||
```python
|
||||
class ConnectionMonitor:
|
||||
def __init__(self, workstation, check_interval=30):
|
||||
self.workstation = workstation
|
||||
self.check_interval = check_interval
|
||||
self._running = False
|
||||
self._thread = None
|
||||
|
||||
def start(self):
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._monitor_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def _monitor_loop(self):
|
||||
while self._running:
|
||||
try:
|
||||
# 调用外部系统接口检测连接
|
||||
self.workstation.hardware_interface.ping()
|
||||
status = "online"
|
||||
except Exception:
|
||||
status = "offline"
|
||||
time.sleep(self.check_interval)
|
||||
```
|
||||
|
||||
参考:`unilabos/devices/workstation/bioyond_studio/station.py`(`ConnectionMonitor`)
|
||||
|
||||
---
|
||||
|
||||
## 2. Config 结构模式
|
||||
|
||||
工作站的 `config` 在图文件中定义,传入 `__init__`。以下是常见字段模式:
|
||||
|
||||
### 2.1 外部系统连接
|
||||
|
||||
```json
|
||||
{
|
||||
"api_host": "http://192.168.1.100:8080",
|
||||
"api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 HTTP 回调服务
|
||||
|
||||
```json
|
||||
{
|
||||
"http_service_config": {
|
||||
"http_service_host": "127.0.0.1",
|
||||
"http_service_port": 8080
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 物料类型映射
|
||||
|
||||
将 PLR 资源类名映射到外部系统的物料类型(名称 + UUID)。用于双向物料转换。
|
||||
|
||||
```json
|
||||
{
|
||||
"material_type_mappings": {
|
||||
"PLR_ResourceClassName": ["外部系统显示名", "external-type-uuid"],
|
||||
"BIOYOND_PolymerStation_Reactor": ["反应器", "3a14233b-902d-0d7b-..."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 仓库映射
|
||||
|
||||
将仓库名映射到外部系统的仓库 UUID 和库位 UUID。用于入库/出库操作。
|
||||
|
||||
```json
|
||||
{
|
||||
"warehouse_mapping": {
|
||||
"仓库名": {
|
||||
"uuid": "warehouse-uuid",
|
||||
"site_uuids": {
|
||||
"A01": "site-uuid-A01",
|
||||
"A02": "site-uuid-A02"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 工作流映射
|
||||
|
||||
将内部工作流名映射到外部系统的工作流 ID。
|
||||
|
||||
```json
|
||||
{
|
||||
"workflow_mappings": {
|
||||
"internal_workflow_name": "external-workflow-uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.6 物料默认参数
|
||||
|
||||
```json
|
||||
{
|
||||
"material_default_parameters": {
|
||||
"NMP": {
|
||||
"unit": "毫升",
|
||||
"density": "1.03",
|
||||
"densityUnit": "g/mL",
|
||||
"description": "N-甲基吡咯烷酮"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 资源同步机制
|
||||
|
||||
### 3.1 ResourceSynchronizer
|
||||
|
||||
抽象基类,用于与外部物料系统双向同步。定义在 `workstation_base.py`。
|
||||
|
||||
```python
|
||||
from unilabos.devices.workstation.workstation_base import ResourceSynchronizer
|
||||
|
||||
|
||||
class MyResourceSynchronizer(ResourceSynchronizer):
|
||||
def __init__(self, workstation, api_client):
|
||||
super().__init__(workstation)
|
||||
self.api_client = api_client
|
||||
|
||||
def sync_from_external(self) -> bool:
|
||||
"""从外部系统拉取物料到 deck"""
|
||||
external_materials = self.api_client.list_materials()
|
||||
for material in external_materials:
|
||||
plr_resource = self._convert_to_plr(material)
|
||||
self.workstation.deck.assign_child_resource(plr_resource, coordinate)
|
||||
return True
|
||||
|
||||
def sync_to_external(self, plr_resource) -> bool:
|
||||
"""将 deck 中的物料变更推送到外部系统"""
|
||||
external_data = self._convert_from_plr(plr_resource)
|
||||
self.api_client.update_material(external_data)
|
||||
return True
|
||||
|
||||
def handle_external_change(self, change_info) -> bool:
|
||||
"""处理外部系统推送的物料变更"""
|
||||
return True
|
||||
```
|
||||
|
||||
### 3.2 update_resource — 上传资源树到云端
|
||||
|
||||
将 PLR Deck 序列化后通过 ROS 服务上传。典型使用场景:
|
||||
|
||||
```python
|
||||
# 在 post_init 中上传初始 deck
|
||||
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode
|
||||
|
||||
ROS2DeviceNode.run_async_func(
|
||||
self._ros_node.update_resource, True,
|
||||
**{"resources": [self.deck]}
|
||||
)
|
||||
|
||||
# 在动作方法中更新特定资源
|
||||
ROS2DeviceNode.run_async_func(
|
||||
self._ros_node.update_resource, True,
|
||||
**{"resources": [updated_plate]}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 工作流序列管理
|
||||
|
||||
工作站通过 `workflow_sequence` 属性管理任务队列(JSON 字符串形式)。
|
||||
|
||||
```python
|
||||
class MyWorkstation(WorkstationBase):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._workflow_sequence = []
|
||||
|
||||
@property
|
||||
def workflow_sequence(self) -> str:
|
||||
"""返回 JSON 字符串,ROS 自动发布"""
|
||||
import json
|
||||
return json.dumps(self._workflow_sequence)
|
||||
|
||||
async def append_to_workflow_sequence(self, workflow_name: str) -> Dict[str, Any]:
|
||||
"""添加工作流到队列"""
|
||||
self._workflow_sequence.append({
|
||||
"name": workflow_name,
|
||||
"status": "pending",
|
||||
"created_at": time.time(),
|
||||
})
|
||||
return {"success": True}
|
||||
|
||||
async def clear_workflows(self) -> Dict[str, Any]:
|
||||
"""清空工作流队列"""
|
||||
self._workflow_sequence = []
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 站间物料转移
|
||||
|
||||
工作站之间转移物料的模式。通过 ROS ActionClient 调用目标站的动作。
|
||||
|
||||
```python
|
||||
async def transfer_materials_to_another_station(
|
||||
self,
|
||||
target_device_id: str,
|
||||
transfer_groups: list,
|
||||
**kwargs,
|
||||
) -> Dict[str, Any]:
|
||||
"""将物料转移到另一个工作站"""
|
||||
target_node = self._children.get(target_device_id)
|
||||
if not target_node:
|
||||
# 通过 ROS 节点查找非子设备的目标站
|
||||
pass
|
||||
|
||||
for group in transfer_groups:
|
||||
resource = self.find_resource_by_name(group["resource_name"])
|
||||
# 从本站 deck 移除
|
||||
resource.unassign()
|
||||
# 调用目标站的接收方法
|
||||
# ...
|
||||
|
||||
return {"success": True, "transferred": len(transfer_groups)}
|
||||
```
|
||||
|
||||
参考:`BioyondDispensingStation.transfer_materials_to_reaction_station`
|
||||
|
||||
---
|
||||
|
||||
## 6. post_init 完整模式
|
||||
|
||||
`post_init` 是工作站初始化的关键阶段,此时 ROS 节点和子设备已就绪。
|
||||
|
||||
```python
|
||||
def post_init(self, ros_node):
|
||||
super().post_init(ros_node)
|
||||
|
||||
# 1. 初始化外部系统客户端(此时 config 已可用)
|
||||
self.rpc_client = MySystemRPC(
|
||||
host=self.config.get("api_host"),
|
||||
api_key=self.config.get("api_key"),
|
||||
)
|
||||
self.hardware_interface = self.rpc_client
|
||||
|
||||
# 2. 启动连接监控
|
||||
self.connection_monitor = ConnectionMonitor(self)
|
||||
self.connection_monitor.start()
|
||||
|
||||
# 3. 启动 HTTP 回调服务
|
||||
if hasattr(self, '_http_service_config'):
|
||||
self.http_service = WorkstationHTTPService(
|
||||
workstation_instance=self,
|
||||
host=self._http_service_config["host"],
|
||||
port=self._http_service_config["port"],
|
||||
)
|
||||
self.http_service.start()
|
||||
|
||||
# 4. 上传 deck 到云端
|
||||
ROS2DeviceNode.run_async_func(
|
||||
self._ros_node.update_resource, True,
|
||||
**{"resources": [self.deck]}
|
||||
)
|
||||
|
||||
# 5. 初始化资源同步器(可选)
|
||||
self.resource_synchronizer = MyResourceSynchronizer(self, self.rpc_client)
|
||||
```
|
||||
Reference in New Issue
Block a user