Compare commits

...

14 Commits

Author SHA1 Message Date
Xuwznln
d2f204c5b0 bump to 0.10.19 2026-03-22 04:17:21 +08:00
Xuwznln
d8922884b1 fast registry load 2026-03-22 04:14:47 +08:00
Xuwznln
427afe83d4 minor fix on skill & registry 2026-03-22 03:36:28 +08:00
Xuwznln
23c2e3b2f7 stripe ros2 schema desc
add create-device-skill
2026-03-22 03:21:13 +08:00
Xuwznln
59c26265e9 new registry system backwards to yaml 2026-03-22 02:19:54 +08:00
Xuwznln
4c2adea55a remove not exist resource 2026-03-21 23:35:51 +08:00
Xuwznln
0f6264503a new registry sys
exp. support with add device
2026-03-21 19:26:24 +08:00
Junhan Chang
2c554182d3 add ai conventions 2026-03-19 14:14:40 +08:00
Xuwznln
6d319d91ff correct raise create resource error 2026-03-10 16:26:37 +08:00
Xuwznln
3155b2f97e ret info fix revert 2026-03-10 16:04:27 +08:00
Xuwznln
e5e30a1c7d ret info fix 2026-03-10 16:00:24 +08:00
Xuwznln
4e82f62327 fix prcxi check 2026-03-10 15:57:27 +08:00
Xuwznln
95d3456214 add create_resource schema 2026-03-10 15:27:39 +08:00
Xuwznln
38bf95b13c re signal host ready event 2026-03-10 14:13:06 +08:00
99 changed files with 10844 additions and 7173 deletions

View File

@@ -3,7 +3,7 @@
package: package:
name: unilabos name: unilabos
version: 0.10.18 version: 0.10.19
source: source:
path: ../../unilabos path: ../../unilabos
@@ -54,7 +54,7 @@ requirements:
- pymodbus - pymodbus
- matplotlib - matplotlib
- pylibftdi - pylibftdi
- uni-lab::unilabos-env ==0.10.18 - uni-lab::unilabos-env ==0.10.19
about: about:
repository: https://github.com/deepmodeling/Uni-Lab-OS repository: https://github.com/deepmodeling/Uni-Lab-OS

View File

@@ -2,7 +2,7 @@
package: package:
name: unilabos-env name: unilabos-env
version: 0.10.18 version: 0.10.19
build: build:
noarch: generic noarch: generic

View File

@@ -3,7 +3,7 @@
package: package:
name: unilabos-full name: unilabos-full
version: 0.10.18 version: 0.10.19
build: build:
noarch: generic noarch: generic
@@ -11,7 +11,7 @@ build:
requirements: requirements:
run: run:
# Base unilabos package (includes unilabos-env) # Base unilabos package (includes unilabos-env)
- uni-lab::unilabos ==0.10.18 - uni-lab::unilabos ==0.10.19
# Documentation tools # Documentation tools
- sphinx - sphinx
- sphinx_rtd_theme - sphinx_rtd_theme

View File

@@ -0,0 +1,328 @@
---
name: create-device-skill
description: Create a skill for any Uni-Lab device by extracting action schemas from the device registry. Use when the user wants to create a new device skill, add device API documentation, or set up action schemas for a device.
---
# 创建设备 Skill 指南
本 meta-skill 教你如何为任意 Uni-Lab-OS 设备创建完整的 API 操作技能(参考 `unilab-device-api` 的成功案例)。
## 数据源
- **设备注册表**: `unilabos_data/req_device_registry_upload.json`
- **结构**: `{ "resources": [{ "id": "<device_id>", "class": { "module": "<python_module:ClassName>", "action_value_mappings": { ... } } }] }`
- **生成时机**: `unilab` 启动并完成注册表上传后自动生成
- **module 字段**: 格式 `unilabos.devices.xxx.yyy:ClassName`,可转为源码路径 `unilabos/devices/xxx/yyy.py`,阅读源码可了解参数含义和设备行为
## 创建流程
### Step 0 — 收集必备信息(缺一不可,否则询问后终止)
开始前**必须**确认以下 4 项信息全部就绪。如果用户未提供任何一项,**立即询问并终止当前流程**,等用户补齐后再继续。
向用户提问:「请提供你的 unilab 启动参数,我需要以下信息:」
#### 必备项 ①ak / sk认证凭据
来源:启动命令的 `--ak` `--sk` 参数,或 config.py 中的 `ak = "..."` `sk = "..."`
获取后立即生成 AUTH token
```bash
python ./scripts/gen_auth.py <ak> <sk>
# 或从 config.py 提取
python ./scripts/gen_auth.py --config <config.py>
```
认证算法:`base64(ak:sk)``Authorization: Lab <token>`
#### 必备项 ②:--addr目标环境
决定 API 请求发往哪个服务器。从启动命令的 `--addr` 参数获取:
| `--addr` 值 | BASE URL |
|-------------|----------|
| `test` | `https://uni-lab.test.bohrium.com` |
| `uat` | `https://uni-lab.uat.bohrium.com` |
| `local` | `http://127.0.0.1:48197` |
| 不传(默认) | `https://uni-lab.bohrium.com` |
| 其他自定义 URL | 直接使用该 URL |
#### 必备项 ③req_device_registry_upload.json设备注册表
数据文件由 `unilab` 启动时自动生成,需要定位它:
**推断 working_dir**(即 `unilabos_data` 所在目录):
| 条件 | working_dir 取值 |
|------|------------------|
| 传了 `--working_dir` | `<working_dir>/unilabos_data/`(若子目录已存在则直接用) |
| 仅传了 `--config` | `<config 文件所在目录>/unilabos_data/` |
| 都没传 | `<当前工作目录>/unilabos_data/` |
**按优先级搜索文件**
```
<推断的 working_dir>/unilabos_data/req_device_registry_upload.json
<推断的 working_dir>/req_device_registry_upload.json
<workspace 根目录>/unilabos_data/req_device_registry_upload.json
```
也可以直接 Glob 搜索:`**/req_device_registry_upload.json`
找到后**必须检查文件修改时间**并告知用户:「找到注册表文件 `<路径>`,生成于 `<时间>`。请确认这是最近一次启动生成的。」超过 1 天提醒用户是否需要重新启动 `unilab`
**如果文件不存在** → 告知用户先运行 `unilab` 启动命令,等日志出现 `注册表响应数据已保存` 后再执行本流程。**终止。**
#### 必备项 ④:目标设备
用户需要明确要为哪个设备创建 skill。可以是设备名称如「PRCXI 移液站」)或 device_id`liquid_handler.prcxi`)。
如果用户不确定,运行提取脚本列出所有设备供选择:
```bash
python ./scripts/extract_device_actions.py --registry <找到的文件路径>
```
#### 完整示例
用户提供:
```
--ak a1fd9d4e-xxxx-xxxx-xxxx-d9a69c09f0fd
--sk 136ff5c6-xxxx-xxxx-xxxx-a03e301f827b
--addr test
--port 8003
--disable_browser
```
从中提取:
- ✅ ak/sk → 运行 `gen_auth.py` 得到 `AUTH="Authorization: Lab YTFmZDlk..."`
- ✅ addr=test → `BASE=https://uni-lab.test.bohrium.com`
- ✅ 搜索 `unilabos_data/req_device_registry_upload.json` → 找到并确认时间
- ✅ 用户指明目标设备 → 如 `liquid_handler.prcxi`
**四项全部就绪后才进入 Step 1。**
### Step 1 — 列出可用设备
运行提取脚本,列出所有设备及 action 数量和 Python 源码路径,让用户选择:
```bash
# 自动搜索(默认在 unilabos_data/ 和当前目录查找)
python ./scripts/extract_device_actions.py
# 指定注册表文件路径
python ./scripts/extract_device_actions.py --registry <path/to/req_device_registry_upload.json>
```
脚本输出包含每个设备的 **Python 源码路径**(从 `class.module` 转换),可用于后续阅读源码理解参数含义。
### Step 2 — 提取 Action Schema
用户选择设备后,运行提取脚本:
```bash
python ./scripts/extract_device_actions.py [--registry <path>] <device_id> ./skills/<skill-name>/actions/
```
脚本会显示设备的 Python 源码路径和类名,方便阅读源码了解参数含义。
每个 action 生成一个 JSON 文件,包含:
- `type` — 作为 API 调用的 `action_type`
- `schema` — 完整 JSON Schema`properties.goal.properties` 参数定义)
- `goal` — goal 字段映射(含占位符 `$placeholder`
- `goal_default` — 默认值
### Step 3 — 写 action-index.md
按模板为每个 action 写条目:
```markdown
### `<action_name>`
<用途描述(一句话)>
- **Schema**: [`actions/<filename>.json`](actions/<filename>.json)
- **核心参数**: `param1`, `param2`(从 schema.required 获取)
- **可选参数**: `param3`, `param4`
- **占位符字段**: `field`(需填入物料信息,值以 `$` 开头)
```
描述规则:
-`schema.properties` 读参数列表schema 已提升为 goal 内容)
-`schema.required` 区分核心/可选参数
- 按功能分类(移液、枪头、外设等)
- 标注 `placeholder_keys` 中的字段类型:
- `unilabos_resources`**ResourceSlot**,填入 `{id, name, uuid}`id 是路径格式,从资源树取物料节点)
- `unilabos_devices`**DeviceSlot**,填入路径字符串如 `"/host_node"`(从资源树筛选 type=device
- `unilabos_nodes`**NodeSlot**,填入路径字符串如 `"/PRCXI/PRCXI_Deck"`(资源树中任意节点)
- `unilabos_class`**ClassSlot**,填入类名字符串如 `"container"`(从注册表查找)
- array 类型字段 → `[{id, name, uuid}, ...]`
- 特殊:`create_resource``res_id`ResourceSlot可填不存在的路径
### Step 4 — 写 SKILL.md
直接复用 `unilab-device-api` 的 API 模板10 个 endpoint修改
- 设备名称
- Action 数量
- 目录列表
- Session state 中的 `device_name`
- **AUTH 头** — 使用 Step 0 中 `gen_auth.py` 生成的 `Authorization: Lab <token>`(不要硬编码 `Api` 类型的 key
- **Python 源码路径** — 在 SKILL.md 开头注明设备对应的源码文件,方便参考参数含义
- **Slot 字段表** — 列出本设备哪些 action 的哪些字段需要填入 Slot物料/设备/节点/类名)
API 模板结构:
```markdown
## 设备信息
- device_id, Python 源码路径, 设备类名
## 前置条件(缺一不可)
- ak/sk → AUTH, --addr → BASE URL
## Session State
- lab_uuid通过 API #1 自动匹配,不要问用户), device_name
## API Endpoints (10 个)
# 注意:
# - #1 获取 lab 列表 + 自动匹配 lab_uuid遍历 is_admin 的 lab
# 调用 /lab/info/{uuid} 比对 access_key == ak
# - #2 创建工作流用 POST /lab/workflow
# - #10 获取资源树路径含 lab_uuid: /lab/material/download/{lab_uuid}
## Placeholder Slot 填写规则
- unilabos_resources → ResourceSlot → {"id":"/path/name","name":"name","uuid":"xxx"}
- unilabos_devices → DeviceSlot → "/parent/device" 路径字符串
- unilabos_nodes → NodeSlot → "/parent/node" 路径字符串
- unilabos_class → ClassSlot → "class_name" 字符串
- 特例create_resource 的 res_id 允许填不存在的路径
- 列出本设备所有 Slot 字段、类型及含义
## 渐进加载策略
## 完整工作流 Checklist
```
### Step 5 — 验证
检查文件完整性:
- [ ] `SKILL.md` 包含 10 个 API endpoint
- [ ] `SKILL.md` 包含 Placeholder Slot 填写规则ResourceSlot / DeviceSlot / NodeSlot / ClassSlot + create_resource 特例)和本设备的 Slot 字段表
- [ ] `action-index.md` 列出所有 action 并有描述
- [ ] `actions/` 目录中每个 action 有对应 JSON 文件
- [ ] JSON 文件包含 `type`, `schema`(已提升为 goal 内容), `goal`, `goal_default`, `placeholder_keys` 字段
- [ ] 描述能让 agent 判断该用哪个 action
## Action JSON 文件结构
```json
{
"type": "LiquidHandlerTransfer", // → API 的 action_type
"goal": { // goal 字段映射
"sources": "sources",
"targets": "targets",
"tip_racks": "tip_racks",
"asp_vols": "asp_vols"
},
"schema": { // ← 直接是 goal 的 schema已提升
"type": "object",
"properties": { // 参数定义(即请求中 goal 的字段)
"sources": { "type": "array", "items": { "type": "object" } },
"targets": { "type": "array", "items": { "type": "object" } },
"asp_vols": { "type": "array", "items": { "type": "number" } }
},
"required": [...],
"_unilabos_placeholder_info": { // ← Slot 类型标记
"sources": "unilabos_resources",
"targets": "unilabos_resources",
"tip_racks": "unilabos_resources"
}
},
"goal_default": { ... }, // 默认值
"placeholder_keys": { // ← 汇总所有 Slot 字段
"sources": "unilabos_resources", // ResourceSlot
"targets": "unilabos_resources",
"tip_racks": "unilabos_resources",
"target_device_id": "unilabos_devices" // DeviceSlot
}
}
```
> **注意**`schema` 已由脚本从原始 `schema.properties.goal` 提升为顶层,直接包含参数定义。
> `schema.properties` 中的字段即为 API 请求 `param.goal` 中的字段。
## Placeholder Slot 类型体系
`placeholder_keys` / `_unilabos_placeholder_info` 中有 4 种值,对应不同的填写方式:
| placeholder 值 | Slot 类型 | 填写格式 | 选取范围 |
|---------------|-----------|---------|---------|
| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` | 仅**物料**节点(不含设备) |
| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` | 仅**设备**节点type=device路径字符串 |
| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` | **设备 + 物料**,即所有节点,路径字符串 |
| `unilabos_class` | ClassSlot | `"class_name"` | 注册表中已上报的资源类 name |
### ResourceSlot`unilabos_resources`
最常见的类型。从资源树中选取**物料**节点(孔板、枪头盒、试剂槽等):
```json
{"id": "/workstation/container1", "name": "container1", "uuid": "ff149a9a-2cb8-419d-8db5-d3ba056fb3c2"}
```
- 单个schema type=object`{"id": "/path/name", "name": "name", "uuid": "xxx"}`
- 数组schema type=array`[{"id": "/path/a", "name": "a", "uuid": "xxx"}, ...]`
- `id` 本身是从 parent 计算的路径格式
- 根据 action 语义选择正确的物料(如 `sources` = 液体来源,`targets` = 目标位置)
> **特例**`create_resource` 的 `res_id` 字段,目标物料可能**尚不存在**,此时直接填写期望的路径(如 `"/workstation/container1"`),不需要 uuid。
### DeviceSlot`unilabos_devices`
填写**设备路径字符串**。从资源树中筛选 type=device 的节点,从 parent 计算路径:
```
"/host_node"
"/bioyond_cell/reaction_station"
```
- 只填路径字符串,不需要 `{id, uuid}` 对象
- 根据 action 语义选择正确的设备(如 `target_device_id` = 目标设备)
### NodeSlot`unilabos_nodes`
范围 = 设备 + 物料。即资源树中**所有节点**都可以选,填写**路径字符串**
```
"/PRCXI/PRCXI_Deck"
```
- 使用场景:当参数既可能指向物料也可能指向设备时(如 `PumpTransferProtocol``from_vessel`/`to_vessel``create_resource``parent`
### ClassSlot`unilabos_class`
填写注册表中已上报的**资源类 name**。从本地 `req_resource_registry_upload.json` 中查找:
```
"container"
```
### 通过 API #10 获取资源树
```bash
curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH"
```
注意 `lab_uuid` 在路径中(不是查询参数)。资源树返回所有节点,每个节点包含 `id`(路径格式)、`name``uuid``type``parent` 等字段。填写 Slot 时需根据 placeholder 类型筛选正确的节点。
## 最终目录结构
```
./<skill-name>/
├── SKILL.md # API 端点 + 渐进加载指引
├── action-index.md # 动作索引:描述/用途/核心参数
└── actions/ # 每个 action 的完整 JSON Schema
├── action1.json
├── action2.json
└── ...
```

View File

@@ -0,0 +1,200 @@
#!/usr/bin/env python3
"""
从 req_device_registry_upload.json 中提取指定设备的 action schema。
用法:
# 列出所有设备及 action 数量(自动搜索注册表文件)
python extract_device_actions.py
# 指定注册表文件路径
python extract_device_actions.py --registry <path/to/req_device_registry_upload.json>
# 提取指定设备的 action 到目录
python extract_device_actions.py <device_id> <output_dir>
python extract_device_actions.py --registry <path> <device_id> <output_dir>
示例:
python extract_device_actions.py --registry unilabos_data/req_device_registry_upload.json
python extract_device_actions.py liquid_handler.prcxi .cursor/skills/unilab-device-api/actions/
"""
import json
import os
import sys
from datetime import datetime
REGISTRY_FILENAME = "req_device_registry_upload.json"
def find_registry(explicit_path=None):
"""
查找 req_device_registry_upload.json 文件。
搜索优先级:
1. 用户通过 --registry 显式指定的路径
2. <cwd>/unilabos_data/req_device_registry_upload.json
3. <cwd>/req_device_registry_upload.json
4. <script所在目录>/../../.. (workspace根) 下的 unilabos_data/
5. 向上逐级搜索父目录(最多 5 层)
"""
if explicit_path:
if os.path.isfile(explicit_path):
return explicit_path
if os.path.isdir(explicit_path):
fp = os.path.join(explicit_path, REGISTRY_FILENAME)
if os.path.isfile(fp):
return fp
print(f"警告: 指定的路径不存在: {explicit_path}")
return None
candidates = [
os.path.join("unilabos_data", REGISTRY_FILENAME),
REGISTRY_FILENAME,
]
for c in candidates:
if os.path.isfile(c):
return c
script_dir = os.path.dirname(os.path.abspath(__file__))
workspace_root = os.path.normpath(os.path.join(script_dir, "..", "..", ".."))
for c in candidates:
path = os.path.join(workspace_root, c)
if os.path.isfile(path):
return path
cwd = os.getcwd()
for _ in range(5):
parent = os.path.dirname(cwd)
if parent == cwd:
break
cwd = parent
for c in candidates:
path = os.path.join(cwd, c)
if os.path.isfile(path):
return path
return None
def load_registry(path):
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def list_devices(data):
"""列出所有包含 action_value_mappings 的设备,同时返回 module 路径"""
resources = data.get('resources', [])
devices = []
for res in resources:
rid = res.get('id', '')
cls = res.get('class', {})
avm = cls.get('action_value_mappings', {})
module = cls.get('module', '')
if avm:
devices.append((rid, len(avm), module))
return devices
def flatten_schema_to_goal(action_data):
"""将 schema 中嵌套的 goal 内容提升为顶层 schema去掉 feedback/result 包装"""
schema = action_data.get('schema', {})
goal_schema = schema.get('properties', {}).get('goal', {})
if goal_schema:
action_data = dict(action_data)
action_data['schema'] = goal_schema
return action_data
def extract_actions(data, device_id, output_dir):
"""提取指定设备的 action schema 到独立 JSON 文件"""
resources = data.get('resources', [])
for res in resources:
if res.get('id') == device_id:
cls = res.get('class', {})
module = cls.get('module', '')
avm = cls.get('action_value_mappings', {})
if not avm:
print(f"设备 {device_id} 没有 action_value_mappings")
return []
if module:
py_path = module.split(":")[0].replace(".", "/") + ".py"
class_name = module.split(":")[-1] if ":" in module else ""
print(f"Python 源码: {py_path}")
if class_name:
print(f"设备类: {class_name}")
os.makedirs(output_dir, exist_ok=True)
written = []
for action_name in sorted(avm.keys()):
action_data = flatten_schema_to_goal(avm[action_name])
filename = action_name.replace('-', '_') + '.json'
filepath = os.path.join(output_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(action_data, f, indent=2, ensure_ascii=False)
written.append(filename)
print(f" {filepath}")
return written
print(f"设备 {device_id} 未找到")
return []
def main():
args = sys.argv[1:]
explicit_registry = None
if "--registry" in args:
idx = args.index("--registry")
if idx + 1 < len(args):
explicit_registry = args[idx + 1]
args = args[:idx] + args[idx + 2:]
else:
print("错误: --registry 需要指定路径")
sys.exit(1)
registry_path = find_registry(explicit_registry)
if not registry_path:
print(f"错误: 找不到 {REGISTRY_FILENAME}")
print()
print("解决方法:")
print(" 1. 先运行 unilab 启动命令,等待注册表生成")
print(" 2. 用 --registry 指定文件路径:")
print(f" python {sys.argv[0]} --registry <path/to/{REGISTRY_FILENAME}>")
print()
print("搜索过的路径:")
for p in [
os.path.join("unilabos_data", REGISTRY_FILENAME),
REGISTRY_FILENAME,
os.path.join("<workspace_root>", "unilabos_data", REGISTRY_FILENAME),
]:
print(f" - {p}")
sys.exit(1)
print(f"注册表: {registry_path}")
mtime = os.path.getmtime(registry_path)
gen_time = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
size_mb = os.path.getsize(registry_path) / (1024 * 1024)
print(f"生成时间: {gen_time} (文件大小: {size_mb:.1f} MB)")
data = load_registry(registry_path)
if len(args) == 0:
devices = list_devices(data)
print(f"\n找到 {len(devices)} 个设备:")
print(f"{'设备 ID':<50} {'Actions':>7} {'Python 模块'}")
print("-" * 120)
for did, count, module in sorted(devices, key=lambda x: x[0]):
py_path = module.split(":")[0].replace(".", "/") + ".py" if module else ""
print(f"{did:<50} {count:>7} {py_path}")
elif len(args) == 2:
device_id = args[0]
output_dir = args[1]
print(f"\n提取 {device_id} 的 actions 到 {output_dir}/")
written = extract_actions(data, device_id, output_dir)
if written:
print(f"\n共写入 {len(written)} 个 action 文件")
else:
print("用法:")
print(" python extract_device_actions.py [--registry <path>] # 列出设备")
print(" python extract_device_actions.py [--registry <path>] <device_id> <dir> # 提取 actions")
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
从 ak/sk 生成 UniLab API Authorization header。
算法: base64(ak:sk) → "Authorization: Lab <token>"
用法:
python gen_auth.py <ak> <sk>
python gen_auth.py --config <config.py>
示例:
python gen_auth.py myak mysk
python gen_auth.py --config experiments/config.py
"""
import base64
import re
import sys
def gen_auth(ak: str, sk: str) -> str:
token = base64.b64encode(f"{ak}:{sk}".encode("utf-8")).decode("utf-8")
return token
def extract_from_config(config_path: str) -> tuple:
"""从 config.py 中提取 ak 和 sk"""
with open(config_path, "r", encoding="utf-8") as f:
content = f.read()
ak_match = re.search(r'''ak\s*=\s*["']([^"']+)["']''', content)
sk_match = re.search(r'''sk\s*=\s*["']([^"']+)["']''', content)
if not ak_match or not sk_match:
return None, None
return ak_match.group(1), sk_match.group(1)
def main():
args = sys.argv[1:]
if len(args) == 2 and args[0] == "--config":
ak, sk = extract_from_config(args[1])
if not ak or not sk:
print(f"错误: 在 {args[1]} 中未找到 ak/sk 配置")
print("期望格式: ak = \"xxx\" sk = \"xxx\"")
sys.exit(1)
print(f"配置文件: {args[1]}")
elif len(args) == 2:
ak, sk = args
else:
print("用法:")
print(" python gen_auth.py <ak> <sk>")
print(" python gen_auth.py --config <config.py>")
sys.exit(1)
token = gen_auth(ak, sk)
print(f"ak: {ak}")
print(f"sk: {sk}")
print()
print(f"Authorization header:")
print(f" Authorization: Lab {token}")
print()
print(f"curl 用法:")
print(f' curl -H "Authorization: Lab {token}" ...')
print()
print(f"Shell 变量:")
print(f' AUTH="Authorization: Lab {token}"')
if __name__ == "__main__":
main()

View File

@@ -49,7 +49,7 @@ jobs:
uv pip uninstall enum34 || echo enum34 not installed, skipping uv pip uninstall enum34 || echo enum34 not installed, skipping
uv pip install . uv pip install .
- name: Run check mode (complete_registry) - name: Run check mode (AST registry validation)
run: | run: |
call conda activate check-env call conda activate check-env
echo Running check mode... echo Running check mode...

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ output/
unilabos_data/ unilabos_data/
pyrightconfig.json pyrightconfig.json
.cursorignore .cursorignore
device_package*/
## Python ## Python
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files

87
AGENTS.md Normal file
View File

@@ -0,0 +1,87 @@
# AGENTS.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Also follow the monorepo-level rules in `../AGENTS.md`.
## Build & Development
```bash
# Install in editable mode (requires mamba env with python 3.11)
pip install -e .
uv pip install -r unilabos/utils/requirements.txt
# Run with a device graph
unilab --graph <graph.json> --config <config.py> --backend ros
unilab --graph <graph.json> --config <config.py> --backend simple # no ROS2 needed
# Common CLI flags
unilab --app_bridges websocket fastapi # communication bridges
unilab --test_mode # simulate hardware, no real execution
unilab --check_mode # CI validation of registry imports
unilab --skip_env_check # skip auto-install of dependencies
unilab --visual rviz|web|disable # visualization mode
unilab --is_slave # run as slave node
# Workflow upload subcommand
unilab workflow_upload -f <workflow.json> -n <name> --tags tag1 tag2
# Tests
pytest tests/ # all tests
pytest tests/resources/test_resourcetreeset.py # single test file
pytest tests/resources/test_resourcetreeset.py::TestClassName::test_method # single test
```
## Architecture
### Startup Flow
`unilab` CLI → `unilabos/app/main.py:main()` → loads config → builds registry → reads device graph (JSON/GraphML) → starts backend thread (ROS2/simple) → starts FastAPI web server + WebSocket client.
### Core Layers
**Registry** (`unilabos/registry/`): Singleton `Registry` class discovers and catalogs all device types, resource types, and communication devices from YAML definitions. Device types live in `registry/devices/*.yaml`, resources in `registry/resources/`, comms in `registry/device_comms/`. The registry resolves class paths to actual Python classes via `utils/import_manager.py`.
**Resource Tracking** (`unilabos/resources/resource_tracker.py`): Pydantic-based `ResourceDict``ResourceDictInstance``ResourceTreeSet` hierarchy. `ResourceTreeSet` is the canonical in-memory representation of all devices and resources, used throughout the system. Graph I/O is in `resources/graphio.py` (reads JSON/GraphML device topology files into `nx.Graph` + `ResourceTreeSet`).
**Device Drivers** (`unilabos/devices/`): 30+ hardware drivers organized by device type (liquid_handling, hplc, balance, arm, etc.). Each driver is a Python class that gets wrapped by `ros/device_node_wrapper.py:ros2_device_node()` to become a ROS2 node with publishers, subscribers, and action servers.
**ROS2 Layer** (`unilabos/ros/`): `device_node_wrapper.py` dynamically wraps any device class into `ROS2DeviceNode` (defined in `ros/nodes/base_device_node.py`). Preset node types in `ros/nodes/presets/` include `host_node`, `controller_node`, `workstation`, `serial_node`, `camera`. Messages use custom `unilabos_msgs` (pre-built, distributed via releases).
**Protocol Compilation** (`unilabos/compile/`): 20+ protocol compilers (add, centrifuge, dissolve, filter, heatchill, stir, pump, etc.) that transform YAML protocol definitions into executable sequences.
**Communication** (`unilabos/device_comms/`): Hardware communication adapters — OPC-UA client, Modbus PLC, RPC, and a universal driver. `app/communication.py` provides a factory pattern for WebSocket client connections to the cloud.
**Web/API** (`unilabos/app/web/`): FastAPI server with REST API (`api.py`), Jinja2 template pages (`pages.py`), and HTTP client for cloud communication (`client.py`). Runs on port 8002 by default.
### Configuration System
- **Config classes** in `unilabos/config/config.py`: `BasicConfig`, `WSConfig`, `HTTPConfig`, `ROSConfig` — all class-level attributes, loaded from Python config files
- Config files are `.py` files with matching class names (see `config/example_config.py`)
- Environment variables override with prefix `UNILABOS_` (e.g., `UNILABOS_BASICCONFIG_PORT=9000`)
- Device topology defined in graph files (JSON with node-link format, or GraphML)
### Key Data Flow
1. Graph file → `graphio.read_node_link_json()``(nx.Graph, ResourceTreeSet, resource_links)`
2. `ResourceTreeSet` + `Registry``initialize_device.initialize_device_from_dict()``ROS2DeviceNode` instances
3. Device nodes communicate via ROS2 topics/actions or direct Python calls (simple backend)
4. Cloud sync via WebSocket (`app/ws_client.py`) and HTTP (`app/web/client.py`)
### Test Data
Example device graphs and experiment configs are in `unilabos/test/experiments/` (not `tests/`). Registry test fixtures in `unilabos/test/registry/`.
## Code Conventions
- Code comments and log messages in simplified Chinese
- Python 3.11+, type hints expected
- Pydantic models for data validation (`resource_tracker.py`)
- Singleton pattern via `@singleton` decorator (`utils/decorator.py`)
- Dynamic class loading via `utils/import_manager.py` — device classes resolved at runtime from registry YAML paths
- CLI argument dashes auto-converted to underscores for consistency
## Licensing
- Framework code: GPL-3.0
- Device drivers (`unilabos/devices/`): DP Technology Proprietary License — do not redistribute

4
CLAUDE.md Normal file
View File

@@ -0,0 +1,4 @@
Please follow the rules defined in:
@AGENTS.md

View File

@@ -15,6 +15,9 @@ Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 中使用,
**示例:** **示例:**
```python ```python
from unilabos.registry.decorators import device, topic_config
@device(id="mock_gripper", category=["gripper"], description="Mock Gripper")
class MockGripper: class MockGripper:
def __init__(self): def __init__(self):
self._position: float = 0.0 self._position: float = 0.0
@@ -23,19 +26,23 @@ class MockGripper:
self._status = "Idle" self._status = "Idle"
@property @property
@topic_config() # 添加 @topic_config 才会定时广播
def position(self) -> float: def position(self) -> float:
return self._position return self._position
@property @property
@topic_config()
def velocity(self) -> float: def velocity(self) -> float:
return self._velocity return self._velocity
@property @property
@topic_config()
def torque(self) -> float: def torque(self) -> float:
return self._torque return self._torque
# 会被自动识别的设备属性,接入 Uni-Lab 时会定时对外广播 # 使用 @topic_config 装饰的属性,接入 Uni-Lab 时会定时对外广播
@property @property
@topic_config(period=2.0) # 可自定义发布周期
def status(self) -> str: def status(self) -> str:
return self._status return self._status
@@ -149,7 +156,7 @@ my_device: # 设备唯一标识符
系统会自动分析您的 Python 驱动类并生成: 系统会自动分析您的 Python 驱动类并生成:
- `status_types`:从 `@property` 装饰的方法自动识别状态属性 - `status_types`:从 `@topic_config` 装饰的 `@property` 方法自动识别状态属性
- `action_value_mappings`:从类方法自动生成动作映射 - `action_value_mappings`:从类方法自动生成动作映射
- `init_param_schema`:从 `__init__` 方法分析初始化参数 - `init_param_schema`:从 `__init__` 方法分析初始化参数
- `schema`:前端显示用的属性类型定义 - `schema`:前端显示用的属性类型定义
@@ -179,7 +186,9 @@ Uni-Lab 设备驱动是一个 Python 类,需要遵循以下结构:
```python ```python
from typing import Dict, Any from typing import Dict, Any
from unilabos.registry.decorators import device, topic_config
@device(id="my_device", category=["general"], description="My Device")
class MyDevice: class MyDevice:
"""设备类文档字符串 """设备类文档字符串
@@ -198,8 +207,9 @@ class MyDevice:
# 初始化硬件连接 # 初始化硬件连接
@property @property
@topic_config() # 必须添加 @topic_config 才会广播
def status(self) -> str: def status(self) -> str:
"""设备状态(会自动广播)""" """设备状态(通过 @topic_config 广播)"""
return self._status return self._status
def my_action(self, param: float) -> Dict[str, Any]: def my_action(self, param: float) -> Dict[str, Any]:
@@ -217,34 +227,61 @@ class MyDevice:
## 状态属性 vs 动作方法 ## 状态属性 vs 动作方法
### 状态属性(@property ### 状态属性(@property + @topic_config
状态属性会被自动识别并定期广播: 状态属性需要同时使用 `@property``@topic_config` 装饰器才会被识别并定期广播:
```python ```python
from unilabos.registry.decorators import topic_config
@property @property
@topic_config() # 必须添加,否则不会广播
def temperature(self) -> float: def temperature(self) -> float:
"""当前温度""" """当前温度"""
return self._read_temperature() return self._read_temperature()
@property @property
@topic_config(period=2.0) # 可自定义发布周期(秒)
def status(self) -> str: def status(self) -> str:
"""设备状态: idle, running, error""" """设备状态: idle, running, error"""
return self._status return self._status
@property @property
@topic_config(name="ready") # 可自定义发布名称
def is_ready(self) -> bool: def is_ready(self) -> bool:
"""设备是否就绪""" """设备是否就绪"""
return self._status == "idle" return self._status == "idle"
``` ```
也可以使用普通方法(非 @property)配合 `@topic_config`
```python
@topic_config(period=10.0)
def get_sensor_data(self) -> Dict[str, float]:
"""获取传感器数据get_ 前缀会自动去除,发布名为 sensor_data"""
return {"temp": self._temp, "humidity": self._humidity}
```
**`@topic_config` 参数**:
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `period` | float | 5.0 | 发布周期(秒) |
| `print_publish` | bool | 节点默认 | 是否打印发布日志 |
| `qos` | int | 10 | QoS 深度 |
| `name` | str | None | 自定义发布名称 |
**发布名称优先级**`@topic_config(name=...)` > `get_` 前缀去除 > 方法名
**特点**: **特点**:
- 使用`@property`装饰器 - 必须使用 `@topic_config` 装饰器
- 只读,不能有参数 - 支持 `@property` 和普通方法
- 自动添加到注册表的`status_types` - 添加到注册表的 `status_types`
- 定期发布到 ROS2 topic - 定期发布到 ROS2 topic
> **⚠️ 重要:** 仅有 `@property` 装饰器而没有 `@topic_config` 的属性**不会**被广播。这是一个 Breaking Change。
### 动作方法 ### 动作方法
动作方法是设备可以执行的操作: 动作方法是设备可以执行的操作:
@@ -497,6 +534,7 @@ class LiquidHandler:
self._status = "idle" self._status = "idle"
@property @property
@topic_config()
def status(self) -> str: def status(self) -> str:
return self._status return self._status
@@ -886,7 +924,52 @@ class MyDevice:
## 最佳实践 ## 最佳实践
### 1. 类型注解 ### 1. 使用 `@device` 装饰器标识设备
```python
from unilabos.registry.decorators import device
@device(id="my_device", category=["heating"], description="My Heating Device", icon="heater.webp")
class MyDevice:
...
```
- `id`:设备唯一标识符,用于注册表匹配
- `category`:分类列表,前端用于分组显示
- `description`:设备描述
- `icon`:图标文件名(可选)
### 2. 使用 `@topic_config` 声明需要广播的状态
```python
from unilabos.registry.decorators import topic_config
# ✓ @property + @topic_config → 会广播
@property
@topic_config(period=2.0)
def temperature(self) -> float:
return self._temp
# ✓ 普通方法 + @topic_config → 会广播get_ 前缀自动去除)
@topic_config(period=10.0)
def get_sensor_data(self) -> Dict[str, float]:
return {"temp": self._temp}
# ✓ 使用 name 参数自定义发布名称
@property
@topic_config(name="ready")
def is_ready(self) -> bool:
return self._status == "idle"
# ✗ 仅有 @property没有 @topic_config → 不会广播
@property
def internal_state(self) -> str:
return self._state
```
> **注意:** 与 `@property` 连用时,`@topic_config` 必须放在 `@property` 下面。
### 3. 类型注解
```python ```python
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List
@@ -901,7 +984,7 @@ def method(
pass pass
``` ```
### 2. 文档字符串 ### 4. 文档字符串
```python ```python
def method(self, param: float) -> Dict[str, Any]: def method(self, param: float) -> Dict[str, Any]:
@@ -923,7 +1006,7 @@ def method(self, param: float) -> Dict[str, Any]:
pass pass
``` ```
### 3. 配置验证 ### 5. 配置验证
```python ```python
def __init__(self, config: Dict[str, Any]): def __init__(self, config: Dict[str, Any]):
@@ -937,7 +1020,7 @@ def __init__(self, config: Dict[str, Any]):
self.baudrate = config['baudrate'] self.baudrate = config['baudrate']
``` ```
### 4. 资源清理 ### 6. 资源清理
```python ```python
def __del__(self): def __del__(self):
@@ -946,7 +1029,7 @@ def __del__(self):
self.connection.close() self.connection.close()
``` ```
### 5. 设计前端友好的返回值 ### 7. 设计前端友好的返回值
**记住:返回值会直接显示在 Web 界面** **记住:返回值会直接显示在 Web 界面**

View File

@@ -422,18 +422,20 @@ placeholder_keys:
### status_types ### status_types
系统会扫描你的 Python 类,从状态方法property 或 get\_方法自动生成这部分: 系统会扫描你的 Python 类,从带有 `@topic_config` 装饰器的 `@property`方法自动生成这部分:
```yaml ```yaml
status_types: status_types:
current_temperature: float # 从 get_current_temperature() 或 @property current_temperature current_temperature: float # 从 @topic_config 装饰的 @property 或方法
is_heating: bool # 从 get_is_heating() 或 @property is_heating is_heating: bool
status: str # 从 get_status() 或 @property status status: str
``` ```
**注意事项** **注意事项**
- 系统会查找所有 `get_` 开头的方法和 `@property` 装饰的属性 - 仅有带 `@topic_config` 装饰器的 `@property` 或方法才会被识别为状态属性
- 没有 `@topic_config``@property` 不会生成 status_types也不会广播
- `get_` 前缀的方法名会自动去除前缀(如 `get_temperature``temperature`
- 类型会自动转成相应的类型(如 `str``float``bool` - 类型会自动转成相应的类型(如 `str``float``bool`
- 如果类型是 `Any``None` 或未知的,默认使用 `String` - 如果类型是 `Any``None` 或未知的,默认使用 `String`
@@ -537,11 +539,13 @@ class AdvancedLiquidHandler:
self._temperature = 25.0 self._temperature = 25.0
@property @property
@topic_config()
def status(self) -> str: def status(self) -> str:
"""设备状态""" """设备状态"""
return self._status return self._status
@property @property
@topic_config()
def temperature(self) -> float: def temperature(self) -> float:
"""当前温度""" """当前温度"""
return self._temperature return self._temperature
@@ -809,21 +813,23 @@ my_temperature_controller:
你的设备类需要符合以下要求: 你的设备类需要符合以下要求:
```python ```python
from unilabos.common.device_base import DeviceBase from unilabos.registry.decorators import device, topic_config
class MyDevice(DeviceBase): @device(id="my_device", category=["temperature"], description="My Device")
class MyDevice:
def __init__(self, config): def __init__(self, config):
"""初始化,参数会自动分析到 init_param_schema.config""" """初始化,参数会自动分析到 init_param_schema.config"""
super().__init__(config)
self.port = config.get('port', '/dev/ttyUSB0') self.port = config.get('port', '/dev/ttyUSB0')
# 状态方法(会自动生成到 status_types # 状态方法(必须添加 @topic_config 才会生成到 status_types 并广播
@property @property
@topic_config()
def status(self): def status(self):
"""返回设备状态""" """返回设备状态"""
return "idle" return "idle"
@property @property
@topic_config()
def temperature(self): def temperature(self):
"""返回当前温度""" """返回当前温度"""
return 25.0 return 25.0
@@ -1039,7 +1045,34 @@ resource.type # "resource"
### 代码规范 ### 代码规范
1. **始终使用类型注解** 1. **使用 `@device` 装饰器标识设备类**
```python
from unilabos.registry.decorators import device
@device(id="my_device", category=["heating"], description="My Device")
class MyDevice:
...
```
2. **使用 `@topic_config` 声明广播属性**
```python
from unilabos.registry.decorators import topic_config
# ✓ 需要广播的状态属性
@property
@topic_config(period=2.0)
def temperature(self) -> float:
return self._temp
# ✗ 仅有 @property 不会广播
@property
def internal_counter(self) -> int:
return self._counter
```
3. **始终使用类型注解**
```python ```python
# ✓ 好 # ✓ 好
@@ -1051,7 +1084,7 @@ def method(self, resource, device):
pass pass
``` ```
2. **提供有意义的参数名** 4. **提供有意义的参数名**
```python ```python
# ✓ 好 - 清晰的参数名 # ✓ 好 - 清晰的参数名
@@ -1063,7 +1096,7 @@ def transfer(self, r1: ResourceSlot, r2: ResourceSlot):
pass pass
``` ```
3. **使用 Optional 表示可选参数** 5. **使用 Optional 表示可选参数**
```python ```python
from typing import Optional from typing import Optional
@@ -1076,7 +1109,7 @@ def method(
pass pass
``` ```
4. **添加详细的文档字符串** 6. **添加详细的文档字符串**
```python ```python
def method( def method(
@@ -1096,13 +1129,13 @@ def method(
pass pass
``` ```
5. **方法命名规范** 7. **方法命名规范**
- 状态方法使用 `@property` 装饰器或 `get_` 前缀 - 状态方法使用 `@property` + `@topic_config` 装饰器,或普通方法 + `@topic_config`
- 动作方法使用动词开头 - 动作方法使用动词开头
- 保持命名清晰、一致 - 保持命名清晰、一致
6. **完善的错误处理** 8. **完善的错误处理**
- 实现完善的错误处理 - 实现完善的错误处理
- 添加日志记录 - 添加日志记录
- 提供有意义的错误信息 - 提供有意义的错误信息

View File

@@ -221,10 +221,10 @@ Laboratory A Laboratory B
```bash ```bash
# 实验室A # 实验室A
unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource unilab --ak your_ak --sk your_sk --upload_registry
# 实验室B # 实验室B
unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource unilab --ak your_ak --sk your_sk --upload_registry
``` ```
--- ---

View File

@@ -22,7 +22,6 @@ options:
--is_slave Run the backend as slave node (without host privileges). --is_slave Run the backend as slave node (without host privileges).
--slave_no_host Skip waiting for host service in slave mode --slave_no_host Skip waiting for host service in slave mode
--upload_registry Upload registry information when starting unilab --upload_registry Upload registry information when starting unilab
--use_remote_resource Use remote resources when starting unilab
--config CONFIG Configuration file path, supports .py format Python config files --config CONFIG Configuration file path, supports .py format Python config files
--port PORT Port for web service information page --port PORT Port for web service information page
--disable_browser Disable opening information page on startup --disable_browser Disable opening information page on startup
@@ -85,7 +84,7 @@ Uni-Lab 的启动过程分为以下几个阶段:
支持两种方式: 支持两种方式:
- **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式) - **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式)
- **远程资源**使用 `--use_remote_resource` 从云端获取 - **远程资源**不指定本地文件即可
### 7. 注册表构建 ### 7. 注册表构建
@@ -196,7 +195,7 @@ unilab --config path/to/your/config.py
unilab --ak your_ak --sk your_sk -g path/to/graph.json --upload_registry unilab --ak your_ak --sk your_sk -g path/to/graph.json --upload_registry
# 使用远程资源启动 # 使用远程资源启动
unilab --ak your_ak --sk your_sk --use_remote_resource unilab --ak your_ak --sk your_sk
# 更新注册表 # 更新注册表
unilab --ak your_ak --sk your_sk --complete_registry unilab --ak your_ak --sk your_sk --complete_registry

View File

@@ -1,6 +1,6 @@
package: package:
name: ros-humble-unilabos-msgs name: ros-humble-unilabos-msgs
version: 0.10.18 version: 0.10.19
source: source:
path: ../../unilabos_msgs path: ../../unilabos_msgs
target_directory: src target_directory: src

View File

@@ -1,6 +1,6 @@
package: package:
name: unilabos name: unilabos
version: "0.10.18" version: "0.10.19"
source: source:
path: ../.. path: ../..

View File

@@ -4,7 +4,7 @@ package_name = 'unilabos'
setup( setup(
name=package_name, name=package_name,
version='0.10.18', version='0.10.19',
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
install_requires=['setuptools'], install_requires=['setuptools'],

View File

@@ -1 +1 @@
__version__ = "0.10.18" __version__ = "0.10.19"

View File

@@ -4,6 +4,7 @@ import os
import platform import platform
import shutil import shutil
import signal import signal
import subprocess
import sys import sys
import threading import threading
import time import time
@@ -25,6 +26,84 @@ from unilabos.config.config import load_config, BasicConfig, HTTPConfig
_restart_requested: bool = False _restart_requested: bool = False
_restart_reason: str = "" _restart_reason: str = ""
RESTART_EXIT_CODE = 42
def _build_child_argv():
"""Build sys.argv for child process, stripping supervisor-only arguments."""
result = []
skip_next = False
for arg in sys.argv:
if skip_next:
skip_next = False
continue
if arg in ("--restart_mode", "--restart-mode"):
continue
if arg in ("--auto_restart_count", "--auto-restart-count"):
skip_next = True
continue
if arg.startswith("--auto_restart_count=") or arg.startswith("--auto-restart-count="):
continue
result.append(arg)
return result
def _run_as_supervisor(max_restarts: int):
"""
Supervisor process that spawns and monitors child processes.
Similar to Uvicorn's --reload: the supervisor itself does no heavy work,
it only launches the real process as a child and restarts it when the child
exits with RESTART_EXIT_CODE.
"""
child_argv = [sys.executable] + _build_child_argv()
restart_count = 0
print_status(
f"[Supervisor] Restart mode enabled (max restarts: {max_restarts}), "
f"child command: {' '.join(child_argv)}",
"info",
)
while True:
print_status(
f"[Supervisor] Launching process (restart {restart_count}/{max_restarts})...",
"info",
)
try:
process = subprocess.Popen(child_argv)
exit_code = process.wait()
except KeyboardInterrupt:
print_status("[Supervisor] Interrupted, terminating child process...", "info")
process.terminate()
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
sys.exit(1)
if exit_code == RESTART_EXIT_CODE:
restart_count += 1
if restart_count > max_restarts:
print_status(
f"[Supervisor] Maximum restart count ({max_restarts}) reached, exiting",
"warning",
)
sys.exit(1)
print_status(
f"[Supervisor] Child requested restart ({restart_count}/{max_restarts}), restarting in 2s...",
"info",
)
time.sleep(2)
else:
if exit_code != 0:
print_status(f"[Supervisor] Child exited with code {exit_code}", "warning")
else:
print_status("[Supervisor] Child exited normally", "info")
sys.exit(exit_code)
def load_config_from_file(config_path): def load_config_from_file(config_path):
if config_path is None: if config_path is None:
@@ -66,6 +145,13 @@ def parse_args():
action="append", action="append",
help="Path to the registry directory", help="Path to the registry directory",
) )
parser.add_argument(
"--devices",
type=str,
default=None,
action="append",
help="Path to Python code directory for AST-based device/resource scanning",
)
parser.add_argument( parser.add_argument(
"--working_dir", "--working_dir",
type=str, type=str,
@@ -155,18 +241,18 @@ def parse_args():
action="store_true", action="store_true",
help="Skip environment dependency check on startup", help="Skip environment dependency check on startup",
) )
parser.add_argument(
"--complete_registry",
action="store_true",
default=False,
help="Complete registry information",
)
parser.add_argument( parser.add_argument(
"--check_mode", "--check_mode",
action="store_true", action="store_true",
default=False, default=False,
help="Run in check mode for CI: validates registry imports and ensures no file changes", help="Run in check mode for CI: validates registry imports and ensures no file changes",
) )
parser.add_argument(
"--complete_registry",
action="store_true",
default=False,
help="Complete and rewrite YAML registry files using AST analysis results",
)
parser.add_argument( parser.add_argument(
"--no_update_feedback", "--no_update_feedback",
action="store_true", action="store_true",
@@ -178,6 +264,24 @@ def parse_args():
default=False, default=False,
help="Test mode: all actions simulate execution and return mock results without running real hardware", help="Test mode: all actions simulate execution and return mock results without running real hardware",
) )
parser.add_argument(
"--extra_resource",
action="store_true",
default=False,
help="Load extra lab_ prefixed labware resources (529 auto-generated definitions from lab_resources.py)",
)
parser.add_argument(
"--restart_mode",
action="store_true",
default=False,
help="Enable supervisor mode: automatically restart the process when triggered via WebSocket",
)
parser.add_argument(
"--auto_restart_count",
type=int,
default=500,
help="Maximum number of automatic restarts in restart mode (default: 500)",
)
# workflow upload subcommand # workflow upload subcommand
workflow_parser = subparsers.add_parser( workflow_parser = subparsers.add_parser(
"workflow_upload", "workflow_upload",
@@ -228,6 +332,11 @@ def main():
args = parser.parse_args() args = parser.parse_args()
args_dict = vars(args) args_dict = vars(args)
# Supervisor mode: spawn child processes and monitor for restart
if args_dict.get("restart_mode", False):
_run_as_supervisor(args_dict.get("auto_restart_count", 5))
return
# 环境检查 - 检查并自动安装必需的包 (可选) # 环境检查 - 检查并自动安装必需的包 (可选)
skip_env_check = args_dict.get("skip_env_check", False) skip_env_check = args_dict.get("skip_env_check", False)
check_mode = args_dict.get("check_mode", False) check_mode = args_dict.get("check_mode", False)
@@ -358,6 +467,9 @@ def main():
BasicConfig.test_mode = args_dict.get("test_mode", False) BasicConfig.test_mode = args_dict.get("test_mode", False)
if BasicConfig.test_mode: if BasicConfig.test_mode:
print_status("启用测试模式:所有动作将模拟执行,不调用真实硬件", "warning") print_status("启用测试模式:所有动作将模拟执行,不调用真实硬件", "warning")
BasicConfig.extra_resource = args_dict.get("extra_resource", False)
if BasicConfig.extra_resource:
print_status("启用额外资源加载将加载lab_开头的labware资源定义", "info")
BasicConfig.communication_protocol = "websocket" BasicConfig.communication_protocol = "websocket"
machine_name = platform.node() machine_name = platform.node()
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name]) machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
@@ -382,22 +494,32 @@ def main():
# 显示启动横幅 # 显示启动横幅
print_unilab_banner(args_dict) print_unilab_banner(args_dict)
# 注册表 - check_mode 时强制启用 complete_registry # Step 0: AST 分析优先 + YAML 注册表加载
# check_mode 和 upload_registry 都会执行实际 import 验证
devices_dirs = args_dict.get("devices", None)
complete_registry = args_dict.get("complete_registry", False) or check_mode complete_registry = args_dict.get("complete_registry", False) or check_mode
lab_registry = build_registry(args_dict["registry_path"], complete_registry, BasicConfig.upload_registry) lab_registry = build_registry(
registry_paths=args_dict["registry_path"],
devices_dirs=devices_dirs,
upload_registry=BasicConfig.upload_registry,
check_mode=check_mode,
complete_registry=complete_registry,
)
# Check mode: complete_registry 完成后直接退出git diff 检测由 CI workflow 执行 # Check mode: 注册表验证完成后直接退出
if check_mode: if check_mode:
print_status("Check mode: complete_registry 完成,退出", "info") device_count = len(lab_registry.device_type_registry)
resource_count = len(lab_registry.resource_type_registry)
print_status(f"Check mode: 注册表验证完成 ({device_count} 设备, {resource_count} 资源),退出", "info")
os._exit(0) os._exit(0)
# Step 1: 上传全部注册表到服务端,同步保存到 unilabos_data
if BasicConfig.upload_registry: if BasicConfig.upload_registry:
# 设备注册到服务端 - 需要 ak 和 sk
if BasicConfig.ak and BasicConfig.sk: if BasicConfig.ak and BasicConfig.sk:
print_status("开始注册设备到服务端...", "info") # print_status("开始注册设备到服务端...", "info")
try: try:
register_devices_and_resources(lab_registry) register_devices_and_resources(lab_registry)
print_status("设备注册完成", "info") # print_status("设备注册完成", "info")
except Exception as e: except Exception as e:
print_status(f"设备注册失败: {e}", "error") print_status(f"设备注册失败: {e}", "error")
else: else:
@@ -482,7 +604,7 @@ def main():
continue continue
# 如果从远端获取了物料信息,则与本地物料进行同步 # 如果从远端获取了物料信息,则与本地物料进行同步
if request_startup_json and "nodes" in request_startup_json: if file_path is not None and request_startup_json and "nodes" in request_startup_json:
print_status("开始同步远端物料到本地...", "info") print_status("开始同步远端物料到本地...", "info")
remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"]) remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"])
resource_tree_set.merge_remote_resources(remote_tree_set) resource_tree_set.merge_remote_resources(remote_tree_set)
@@ -579,6 +701,10 @@ def main():
open_browser=not args_dict["disable_browser"], open_browser=not args_dict["disable_browser"],
port=BasicConfig.port, port=BasicConfig.port,
) )
if restart_requested:
print_status("[Main] Restart requested, cleaning up...", "info")
cleanup_for_restart()
os._exit(RESTART_EXIT_CODE)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,9 +1,8 @@
import json
import time import time
from typing import Optional, Tuple, Dict, Any from typing import Any, Dict, Optional, Tuple
from unilabos.utils.log import logger from unilabos.utils.log import logger
from unilabos.utils.type_check import TypeEncoder from unilabos.utils.tools import normalize_json as _normalize_device
def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]: def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]:
@@ -11,50 +10,63 @@ def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[
注册设备和资源到服务器仅支持HTTP 注册设备和资源到服务器仅支持HTTP
""" """
# 注册资源信息 - 使用HTTP方式
from unilabos.app.web.client import http_client from unilabos.app.web.client import http_client
logger.info("[UniLab Register] 开始注册设备和资源...") logger.info("[UniLab Register] 开始注册设备和资源...")
# 注册设备信息
devices_to_register = {} devices_to_register = {}
for device_info in lab_registry.obtain_registry_device_info(): for device_info in lab_registry.obtain_registry_device_info():
devices_to_register[device_info["id"]] = json.loads( devices_to_register[device_info["id"]] = _normalize_device(device_info)
json.dumps(device_info, ensure_ascii=False, cls=TypeEncoder) logger.trace(f"[UniLab Register] 收集设备: {device_info['id']}")
)
logger.debug(f"[UniLab Register] 收集设备: {device_info['id']}")
resources_to_register = {} resources_to_register = {}
for resource_info in lab_registry.obtain_registry_resource_info(): for resource_info in lab_registry.obtain_registry_resource_info():
resources_to_register[resource_info["id"]] = resource_info resources_to_register[resource_info["id"]] = resource_info
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}") logger.trace(f"[UniLab Register] 收集资源: {resource_info['id']}")
if gather_only: if gather_only:
return devices_to_register, resources_to_register return devices_to_register, resources_to_register
# 注册设备
if devices_to_register: if devices_to_register:
try: try:
start_time = time.time() start_time = time.time()
response = http_client.resource_registry({"resources": list(devices_to_register.values())}) response = http_client.resource_registry(
{"resources": list(devices_to_register.values())},
tag="device_registry",
)
cost_time = time.time() - start_time cost_time = time.time() - start_time
if response.status_code in [200, 201]: res_data = response.json() if response.status_code == 200 else {}
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}s") skipped = res_data.get("data", {}).get("skipped", False)
if skipped:
logger.info(
f"[UniLab Register] 设备注册跳过(内容未变化)"
f" {len(devices_to_register)}{cost_time:.3f}s"
)
elif response.status_code in [200, 201]:
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time:.3f}s")
else: else:
logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}s") logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time:.3f}s")
except Exception as e: except Exception as e:
logger.error(f"[UniLab Register] 设备注册异常: {e}") logger.error(f"[UniLab Register] 设备注册异常: {e}")
# 注册资源
if resources_to_register: if resources_to_register:
try: try:
start_time = time.time() start_time = time.time()
response = http_client.resource_registry({"resources": list(resources_to_register.values())}) response = http_client.resource_registry(
{"resources": list(resources_to_register.values())},
tag="resource_registry",
)
cost_time = time.time() - start_time cost_time = time.time() - start_time
if response.status_code in [200, 201]: res_data = response.json() if response.status_code == 200 else {}
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}s") skipped = res_data.get("data", {}).get("skipped", False)
if skipped:
logger.info(
f"[UniLab Register] 资源注册跳过(内容未变化)"
f" {len(resources_to_register)}{cost_time:.3f}s"
)
elif response.status_code in [200, 201]:
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time:.3f}s")
else: else:
logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}s") logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time:.3f}s")
except Exception as e: except Exception as e:
logger.error(f"[UniLab Register] 资源注册异常: {e}") logger.error(f"[UniLab Register] 资源注册异常: {e}")
logger.info("[UniLab Register] 设备和资源注册完成.")

View File

@@ -1052,7 +1052,7 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
"result": {}, "result": {},
"schema": lab_registry._generate_unilab_json_command_schema(v["args"], k), "schema": lab_registry._generate_unilab_json_command_schema(v["args"], k),
"goal_default": {i["name"]: i["default"] for i in v["args"]}, "goal_default": {i["name"]: i["default"] for i in v["args"]},
"handles": [], "handles": {},
} }
# 不生成已配置action的动作 # 不生成已配置action的动作
for k, v in enhanced_info["action_methods"].items() for k, v in enhanced_info["action_methods"].items()
@@ -1340,5 +1340,5 @@ def setup_api_routes(app):
# 启动广播任务 # 启动广播任务
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
asyncio.create_task(broadcast_device_status()) asyncio.create_task(broadcast_device_status(), name="web-api-startup-device")
asyncio.create_task(broadcast_status_page_data()) asyncio.create_task(broadcast_status_page_data(), name="web-api-startup-status")

View File

@@ -8,6 +8,8 @@ import json
import os import os
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from unilabos.utils.tools import fast_dumps as _fast_dumps, fast_dumps_pretty as _fast_dumps_pretty
import requests import requests
from unilabos.resources.resource_tracker import ResourceTreeSet from unilabos.resources.resource_tracker import ResourceTreeSet
from unilabos.utils.log import info from unilabos.utils.log import info
@@ -280,29 +282,54 @@ class HTTPClient:
) )
return response return response
def resource_registry(self, registry_data: Dict[str, Any] | List[Dict[str, Any]]) -> requests.Response: def resource_registry(
self, registry_data: Dict[str, Any] | List[Dict[str, Any]], tag: str = "registry",
) -> requests.Response:
""" """
注册资源到服务器 注册资源到服务器,同步保存请求/响应到 unilabos_data
Args: Args:
registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}] registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}]
tag: 保存文件的标签后缀 (如 "device_registry" / "resource_registry")
Returns: Returns:
Response: API响应对象 Response: API响应对象
""" """
compressed_body = gzip.compress( # 序列化一次,同时用于保存和发送
json.dumps(registry_data, ensure_ascii=False, default=str).encode("utf-8") json_bytes = _fast_dumps(registry_data)
)
# 保存请求数据到 unilabos_data
req_path = os.path.join(BasicConfig.working_dir, f"req_{tag}_upload.json")
try:
os.makedirs(BasicConfig.working_dir, exist_ok=True)
with open(req_path, "wb") as f:
f.write(_fast_dumps_pretty(registry_data))
logger.trace(f"注册表请求数据已保存: {req_path}")
except Exception as e:
logger.warning(f"保存注册表请求数据失败: {e}")
compressed_body = gzip.compress(json_bytes)
headers = {
"Authorization": f"Lab {self.auth}",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
}
response = requests.post( response = requests.post(
f"{self.remote_addr}/lab/resource", f"{self.remote_addr}/lab/resource",
data=compressed_body, data=compressed_body,
headers={ headers=headers,
"Authorization": f"Lab {self.auth}",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
timeout=30, timeout=30,
) )
# 保存响应数据到 unilabos_data
res_path = os.path.join(BasicConfig.working_dir, f"res_{tag}_upload.json")
try:
with open(res_path, "w", encoding="utf-8") as f:
f.write(f"{response.status_code}\n{response.text}")
logger.trace(f"注册表响应数据已保存: {res_path}")
except Exception as e:
logger.warning(f"保存注册表响应数据失败: {e}")
if response.status_code not in [200, 201]: if response.status_code not in [200, 201]:
logger.error(f"注册资源失败: {response.status_code}, {response.text}") logger.error(f"注册资源失败: {response.status_code}, {response.text}")
if response.status_code == 200: if response.status_code == 200:

View File

@@ -86,7 +86,7 @@ def setup_server() -> FastAPI:
# 设置页面路由 # 设置页面路由
try: try:
setup_web_pages(pages) setup_web_pages(pages)
info("[Web] 已加载Web UI模块") # info("[Web] 已加载Web UI模块")
except ImportError as e: except ImportError as e:
info(f"[Web] 未找到Web页面模块: {str(e)}") info(f"[Web] 未找到Web页面模块: {str(e)}")
except Exception as e: except Exception as e:
@@ -138,7 +138,7 @@ def start_server(host: str = "0.0.0.0", port: int = 8002, open_browser: bool = T
server_thread = threading.Thread(target=server.run, daemon=True, name="uvicorn_server") server_thread = threading.Thread(target=server.run, daemon=True, name="uvicorn_server")
server_thread.start() server_thread.start()
info("[Web] Server started, monitoring for restart requests...") # info("[Web] Server started, monitoring for restart requests...")
# 监控重启标志 # 监控重启标志
import unilabos.app.main as main_module import unilabos.app.main as main_module

View File

@@ -26,6 +26,7 @@ from enum import Enum
from typing_extensions import TypedDict from typing_extensions import TypedDict
from unilabos.app.model import JobAddReq from unilabos.app.model import JobAddReq
from unilabos.resources.resource_tracker import ResourceDictType
from unilabos.ros.nodes.presets.host_node import HostNode from unilabos.ros.nodes.presets.host_node import HostNode
from unilabos.utils.type_check import serialize_result_info from unilabos.utils.type_check import serialize_result_info
from unilabos.app.communication import BaseCommunicationClient from unilabos.app.communication import BaseCommunicationClient
@@ -408,6 +409,7 @@ class MessageProcessor:
# 线程控制 # 线程控制
self.is_running = False self.is_running = False
self.thread = None self.thread = None
self._loop = None # asyncio event loop引用用于外部关闭websocket
self.reconnect_count = 0 self.reconnect_count = 0
logger.info(f"[MessageProcessor] Initialized for URL: {websocket_url}") logger.info(f"[MessageProcessor] Initialized for URL: {websocket_url}")
@@ -434,22 +436,31 @@ class MessageProcessor:
def stop(self) -> None: def stop(self) -> None:
"""停止消息处理线程""" """停止消息处理线程"""
self.is_running = False self.is_running = False
# 主动关闭websocket以快速中断消息接收循环
ws = self.websocket
loop = self._loop
if ws and loop and loop.is_running():
try:
asyncio.run_coroutine_threadsafe(ws.close(), loop)
except Exception:
pass
if self.thread and self.thread.is_alive(): if self.thread and self.thread.is_alive():
self.thread.join(timeout=2) self.thread.join(timeout=2)
logger.info("[MessageProcessor] Stopped") logger.info("[MessageProcessor] Stopped")
def _run(self): def _run(self):
"""运行消息处理主循环""" """运行消息处理主循环"""
loop = asyncio.new_event_loop() self._loop = asyncio.new_event_loop()
try: try:
asyncio.set_event_loop(loop) asyncio.set_event_loop(self._loop)
loop.run_until_complete(self._connection_handler()) self._loop.run_until_complete(self._connection_handler())
except Exception as e: except Exception as e:
logger.error(f"[MessageProcessor] Thread error: {str(e)}") logger.error(f"[MessageProcessor] Thread error: {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
finally: finally:
if loop: if self._loop:
loop.close() self._loop.close()
self._loop = None
async def _connection_handler(self): async def _connection_handler(self):
"""处理WebSocket连接和重连逻辑""" """处理WebSocket连接和重连逻辑"""
@@ -469,6 +480,7 @@ class MessageProcessor:
open_timeout=20, open_timeout=20,
ping_interval=WSConfig.ping_interval, ping_interval=WSConfig.ping_interval,
ping_timeout=10, ping_timeout=10,
close_timeout=5,
additional_headers={ additional_headers={
"Authorization": f"Lab {BasicConfig.auth_secret()}", "Authorization": f"Lab {BasicConfig.auth_secret()}",
"EdgeSession": f"{self.session_id}", "EdgeSession": f"{self.session_id}",
@@ -479,42 +491,45 @@ class MessageProcessor:
self.connected = True self.connected = True
self.reconnect_count = 0 self.reconnect_count = 0
logger.trace(f"[MessageProcessor] Connected to {self.websocket_url}") logger.info(f"[MessageProcessor] 已连接到 {self.websocket_url}")
# 启动发送协程 # 启动发送协程
send_task = asyncio.create_task(self._send_handler()) send_task = asyncio.create_task(self._send_handler(), name="websocket-send_task")
# 每次连接(含重连)后重新向服务端注册,
# 否则服务端不知道客户端已上线,不会推送消息。
if self.websocket_client:
self.websocket_client.publish_host_ready()
try: try:
# 接收消息循环 # 接收消息循环
await self._message_handler() await self._message_handler()
finally: finally:
# 必须在 async with __aexit__ 之前停止 send_task
# 否则 send_task 会在关闭握手期间继续发送数据,
# 干扰 websockets 库的内部清理,导致 task 泄漏。
self.connected = False
send_task.cancel() send_task.cancel()
try: try:
await send_task await send_task
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
self.connected = False
except websockets.exceptions.ConnectionClosed: except websockets.exceptions.ConnectionClosed:
logger.warning("[MessageProcessor] Connection closed") logger.warning("[MessageProcessor] 与服务端连接中断")
self.connected = False
except TimeoutError: except TimeoutError:
logger.warning( logger.warning(
f"[MessageProcessor] Connection timeout (attempt {self.reconnect_count + 1}), " f"[MessageProcessor] 与服务端连接通信超时 (已尝试 {self.reconnect_count + 1} 次),请检查您的网络状况"
f"server may be temporarily unavailable"
) )
self.connected = False
except websockets.exceptions.InvalidStatus as e: except websockets.exceptions.InvalidStatus as e:
logger.warning( logger.warning(
f"[MessageProcessor] Server returned unexpected HTTP status {e.response.status_code}, " f"[MessageProcessor] 收到服务端注册码 {e.response.status_code}, 上一进程可能还未退出"
f"WebSocket endpoint may not be ready yet"
) )
self.connected = False
except Exception as e: except Exception as e:
logger.error(f"[MessageProcessor] Connection error: {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
self.connected = False logger.error(f"[MessageProcessor] 尝试重连时出错 {str(e)}")
finally: finally:
self.connected = False
self.websocket = None self.websocket = None
# 重连逻辑 # 重连逻辑
@@ -522,10 +537,9 @@ class MessageProcessor:
break break
if self.reconnect_count < WSConfig.max_reconnect_attempts: if self.reconnect_count < WSConfig.max_reconnect_attempts:
self.reconnect_count += 1 self.reconnect_count += 1
backoff = min(WSConfig.reconnect_interval * (2 ** (self.reconnect_count - 1)), 60) backoff = WSConfig.reconnect_interval
logger.info( logger.info(
f"[MessageProcessor] Reconnecting in {backoff}s " f"[MessageProcessor] 即将在 {backoff} 秒后重连 (已尝试 {self.reconnect_count}/{WSConfig.max_reconnect_attempts})"
f"(attempt {self.reconnect_count}/{WSConfig.max_reconnect_attempts})"
) )
await asyncio.sleep(backoff) await asyncio.sleep(backoff)
else: else:
@@ -533,40 +547,38 @@ class MessageProcessor:
break break
async def _message_handler(self): async def _message_handler(self):
"""处理接收到的消息""" """处理接收到的消息
ConnectionClosed 不在此处捕获,让其向上传播到 _connection_handler
以便 async with websockets.connect() 的 __aexit__ 能感知连接已断,
正确清理内部 task避免 task 泄漏。
"""
if not self.websocket: if not self.websocket:
logger.error("[MessageProcessor] WebSocket connection is None") logger.error("[MessageProcessor] WebSocket connection is None")
return return
try: async for message in self.websocket:
async for message in self.websocket: try:
try: data = json.loads(message)
data = json.loads(message) message_type = data.get("action", "")
message_type = data.get("action", "") message_data = data.get("data")
message_data = data.get("data") if self.session_id and self.session_id == data.get("edge_session"):
if self.session_id and self.session_id == data.get("edge_session"): await self._process_message(message_type, message_data)
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')}"
)
else: else:
if message_type.endswith("_material"): await self._process_message(message_type, message_data)
logger.trace( except json.JSONDecodeError:
f"[MessageProcessor] 收到一条归属 {data.get('edge_session')} 的旧消息:{data}" logger.error(f"[MessageProcessor] Invalid JSON received: {message}")
) except Exception as e:
logger.debug( logger.error(f"[MessageProcessor] Error processing message: {str(e)}")
f"[MessageProcessor] 跳过了一条归属 {data.get('edge_session')} 的旧消息: {data.get('action')}" logger.error(traceback.format_exc())
)
else:
await self._process_message(message_type, message_data)
except json.JSONDecodeError:
logger.error(f"[MessageProcessor] Invalid JSON received: {message}")
except Exception as e:
logger.error(f"[MessageProcessor] Error processing message: {str(e)}")
logger.error(traceback.format_exc())
except websockets.exceptions.ConnectionClosed:
logger.info("[MessageProcessor] Message handler stopped - connection closed")
except Exception as e:
logger.error(f"[MessageProcessor] Message handler error: {str(e)}")
logger.error(traceback.format_exc())
async def _send_handler(self): async def _send_handler(self):
"""处理发送队列中的消息""" """处理发送队列中的消息"""
@@ -615,6 +627,7 @@ class MessageProcessor:
except asyncio.CancelledError: except asyncio.CancelledError:
logger.debug("[MessageProcessor] Send handler cancelled") logger.debug("[MessageProcessor] Send handler cancelled")
raise
except Exception as e: except Exception as e:
logger.error(f"[MessageProcessor] Fatal error in send handler: {str(e)}") logger.error(f"[MessageProcessor] Fatal error in send handler: {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
@@ -646,6 +659,10 @@ class MessageProcessor:
# elif message_type == "session_id": # elif message_type == "session_id":
# self.session_id = message_data.get("session_id") # self.session_id = message_data.get("session_id")
# logger.info(f"[MessageProcessor] Session ID: {self.session_id}") # logger.info(f"[MessageProcessor] Session ID: {self.session_id}")
elif message_type == "add_device":
await self._handle_device_manage(message_data, "add")
elif message_type == "remove_device":
await self._handle_device_manage(message_data, "remove")
elif message_type == "request_restart": elif message_type == "request_restart":
await self._handle_request_restart(message_data) await self._handle_request_restart(message_data)
else: else:
@@ -982,6 +999,37 @@ class MessageProcessor:
) )
thread.start() thread.start()
async def _handle_device_manage(self, device_list: list[ResourceDictType], action: str):
"""Handle add_device / remove_device from LabGo server."""
if not device_list:
return
for item in device_list:
target_node_id = item.get("target_node_id", "host_node")
def _notify(target_id: str, act: str, cfg: ResourceDictType):
try:
host_node = HostNode.get_instance(timeout=5)
if not host_node:
logger.error(f"[DeviceManage] HostNode not available for {act}_device")
return
success = host_node.notify_device_manage(target_id, act, cfg)
if success:
logger.info(f"[DeviceManage] {act}_device completed on {target_id}")
else:
logger.warning(f"[DeviceManage] {act}_device failed on {target_id}")
except Exception as e:
logger.error(f"[DeviceManage] Error in {act}_device: {e}")
logger.error(traceback.format_exc())
thread = threading.Thread(
target=_notify,
args=(target_node_id, action, item),
daemon=True,
name=f"DeviceManage-{action}-{item.get('id', '')}",
)
thread.start()
async def _handle_request_restart(self, data: Dict[str, Any]): async def _handle_request_restart(self, data: Dict[str, Any]):
""" """
处理重启请求 处理重启请求
@@ -993,10 +1041,9 @@ class MessageProcessor:
logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s") logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s")
# 发送确认消息 # 发送确认消息
if self.websocket_client: self.send_message(
await self.websocket_client.send_message( {"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}}
{"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}} )
)
# 设置全局重启标志 # 设置全局重启标志
import unilabos.app.main as main_module import unilabos.app.main as main_module
@@ -1098,6 +1145,7 @@ class QueueProcessor:
def stop(self) -> None: def stop(self) -> None:
"""停止队列处理线程""" """停止队列处理线程"""
self.is_running = False self.is_running = False
self.queue_update_event.set() # 立即唤醒等待中的线程
if self.thread and self.thread.is_alive(): if self.thread and self.thread.is_alive():
self.thread.join(timeout=2) self.thread.join(timeout=2)
logger.info("[QueueProcessor] Stopped") logger.info("[QueueProcessor] Stopped")
@@ -1351,8 +1399,8 @@ class WebSocketClient(BaseCommunicationClient):
message = {"action": "normal_exit", "data": {"session_id": session_id}} message = {"action": "normal_exit", "data": {"session_id": session_id}}
self.message_processor.send_message(message) self.message_processor.send_message(message)
logger.info(f"[WebSocketClient] Sent normal_exit message with session_id: {session_id}") logger.info(f"[WebSocketClient] Sent normal_exit message with session_id: {session_id}")
# 给一点时间让消息发送出去 # send_handler 每100ms检查一次队列等300ms足以让消息发
time.sleep(1) time.sleep(0.3)
except Exception as e: except Exception as e:
logger.warning(f"[WebSocketClient] Failed to send normal_exit message: {str(e)}") logger.warning(f"[WebSocketClient] Failed to send normal_exit message: {str(e)}")

View File

@@ -24,6 +24,7 @@ class BasicConfig:
port = 8002 # 本地HTTP服务 port = 8002 # 本地HTTP服务
check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性 check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性
test_mode = False # 测试模式,所有动作不实际执行,返回模拟结果 test_mode = False # 测试模式,所有动作不实际执行,返回模拟结果
extra_resource = False # 是否加载lab_开头的额外资源
# 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL' # 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG" log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG"
@@ -40,7 +41,7 @@ class BasicConfig:
class WSConfig: class WSConfig:
reconnect_interval = 5 # 重连间隔(秒) reconnect_interval = 5 # 重连间隔(秒)
max_reconnect_attempts = 999 # 最大重连次数 max_reconnect_attempts = 999 # 最大重连次数
ping_interval = 30 # ping间隔 ping_interval = 20 # ping间隔
# HTTP配置 # HTTP配置

View File

@@ -1,4 +1,3 @@
from abc import abstractmethod from abc import abstractmethod
from functools import wraps from functools import wraps
import inspect import inspect

View File

@@ -634,7 +634,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
def __init__( def __init__(
self, self,
deck: Deck, deck: PRCXI9300Deck,
host: str, host: str,
port: int, port: int,
timeout: float, timeout: float,
@@ -648,11 +648,11 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
is_9320=False, is_9320=False,
): ):
tablets_info = [] tablets_info = []
count = 0 for site_id in range(len(deck.sites)):
for child in deck.children: child = deck._get_site_resource(site_id)
# 如果放其他类型的物料,是不可以的 # 如果放其他类型的物料,是不可以的
if hasattr(child, "_unilabos_state") and "Material" in child._unilabos_state: if hasattr(child, "_unilabos_state") and "Material" in child._unilabos_state:
number = int(child.name.replace("T", "")) number = site_id + 1
tablets_info.append( tablets_info.append(
WorkTablets( WorkTablets(
Number=number, Code=f"T{number}", Material=child._unilabos_state["Material"] Number=number, Code=f"T{number}", Material=child._unilabos_state["Material"]

View File

@@ -1,15 +1,15 @@
""" """
Virtual Workbench Device - 模拟工作台设备 Virtual Workbench Device - 模拟工作台设备
包含 包含:
- 1个机械臂 (每次操作3s, 独占锁) - 1个机械臂 (每次操作3s, 独占锁)
- 3个加热台 (每次加热10s, 可并行) - 3个加热台 (每次加热10s, 可并行)
工作流程 工作流程:
1. A1-A5 物料同时启动竞争机械臂 1. A1-A5 物料同时启动, 竞争机械臂
2. 机械臂将物料移动到空闲加热台 2. 机械臂将物料移动到空闲加热台
3. 加热完成后机械臂将物料移动到C1-C5 3. 加热完成后, 机械臂将物料移动到C1-C5
注意调用来自线程池使用 threading.Lock 进行同步 注意: 调用来自线程池, 使用 threading.Lock 进行同步
""" """
import logging import logging
@@ -21,9 +21,11 @@ from threading import Lock, RLock
from typing_extensions import TypedDict from typing_extensions import TypedDict
from unilabos.registry.decorators import (
device, action, ActionInputHandle, ActionOutputHandle, DataSource, topic_config, not_action
)
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
from unilabos.utils.decorator import not_action, always_free from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, RETURN_UNILABOS_SAMPLES
# ============ TypedDict 返回类型定义 ============ # ============ TypedDict 返回类型定义 ============
@@ -57,6 +59,8 @@ class MoveToOutputResult(TypedDict):
success: bool success: bool
station_id: int station_id: int
material_id: str material_id: str
output_position: str
message: str
unilabos_samples: List[LabSample] unilabos_samples: List[LabSample]
@@ -81,9 +85,9 @@ class HeatingStationState(Enum):
"""加热台状态枚举""" """加热台状态枚举"""
IDLE = "idle" # 空闲 IDLE = "idle" # 空闲
OCCUPIED = "occupied" # 已放置物料等待加热 OCCUPIED = "occupied" # 已放置物料, 等待加热
HEATING = "heating" # 加热中 HEATING = "heating" # 加热中
COMPLETED = "completed" # 加热完成等待取走 COMPLETED = "completed" # 加热完成, 等待取走
class ArmState(Enum): class ArmState(Enum):
@@ -105,19 +109,24 @@ class HeatingStation:
heating_progress: float = 0.0 heating_progress: float = 0.0
@device(
id="virtual_workbench",
category=["virtual_device"],
description="Virtual Workbench with 1 robotic arm and 3 heating stations for concurrent material processing",
)
class VirtualWorkbench: class VirtualWorkbench:
""" """
Virtual Workbench Device - 虚拟工作台设备 Virtual Workbench Device - 虚拟工作台设备
模拟一个包含1个机械臂和3个加热台的工作站 模拟一个包含1个机械臂和3个加热台的工作站
- 机械臂操作耗时3秒同一时间只能执行一个操作 - 机械臂操作耗时3秒, 同一时间只能执行一个操作
- 加热台加热耗时10秒3个加热台可并行工作 - 加热台加热耗时10秒, 3个加热台可并行工作
工作流: 工作流:
1. 物料A1-A5并发启动线程池竞争机械臂使用权 1. 物料A1-A5并发启动(线程池), 竞争机械臂使用权
2. 获取机械臂后查找空闲加热台 2. 获取机械臂后, 查找空闲加热台
3. 机械臂将物料放入加热台开始加热 3. 机械臂将物料放入加热台, 开始加热
4. 加热完成后机械臂将物料移动到目标位置Cn 4. 加热完成后, 机械臂将物料移动到目标位置Cn
""" """
_ros_node: BaseROS2DeviceNode _ros_node: BaseROS2DeviceNode
@@ -145,19 +154,19 @@ class VirtualWorkbench:
self.HEATING_TIME = float(self.config.get("heating_time", self.HEATING_TIME)) self.HEATING_TIME = float(self.config.get("heating_time", self.HEATING_TIME))
self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", self.NUM_HEATING_STATIONS)) self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", self.NUM_HEATING_STATIONS))
# 机械臂状态和锁 (使用threading.Lock) # 机械臂状态和锁
self._arm_lock = Lock() self._arm_lock = Lock()
self._arm_state = ArmState.IDLE self._arm_state = ArmState.IDLE
self._arm_current_task: Optional[str] = None self._arm_current_task: Optional[str] = None
# 加热台状态 (station_id -> HeatingStation) - 立即初始化不依赖initialize() # 加热台状态
self._heating_stations: Dict[int, HeatingStation] = { self._heating_stations: Dict[int, HeatingStation] = {
i: HeatingStation(station_id=i) for i in range(1, self.NUM_HEATING_STATIONS + 1) i: HeatingStation(station_id=i) for i in range(1, self.NUM_HEATING_STATIONS + 1)
} }
self._stations_lock = RLock() # 可重入锁,保护加热台状态 self._stations_lock = RLock()
# 任务追踪 # 任务追踪
self._active_tasks: Dict[str, Dict[str, Any]] = {} # material_id -> task_info self._active_tasks: Dict[str, Dict[str, Any]] = {}
self._tasks_lock = Lock() self._tasks_lock = Lock()
# 处理其他kwargs参数 # 处理其他kwargs参数
@@ -183,7 +192,6 @@ class VirtualWorkbench:
"""初始化虚拟工作台""" """初始化虚拟工作台"""
self.logger.info(f"初始化虚拟工作台 {self.device_id}") self.logger.info(f"初始化虚拟工作台 {self.device_id}")
# 重置加热台状态 (已在__init__中创建这里重置为初始状态)
with self._stations_lock: with self._stations_lock:
for station in self._heating_stations.values(): for station in self._heating_stations.values():
station.state = HeatingStationState.IDLE station.state = HeatingStationState.IDLE
@@ -191,7 +199,6 @@ class VirtualWorkbench:
station.material_number = None station.material_number = None
station.heating_progress = 0.0 station.heating_progress = 0.0
# 初始化状态
self.data.update( self.data.update(
{ {
"status": "Ready", "status": "Ready",
@@ -257,11 +264,7 @@ class VirtualWorkbench:
self.data["message"] = message self.data["message"] = message
def _find_available_heating_station(self) -> Optional[int]: def _find_available_heating_station(self) -> Optional[int]:
"""查找空闲的加热台 """查找空闲的加热台"""
Returns:
空闲加热台ID如果没有则返回None
"""
with self._stations_lock: with self._stations_lock:
for station_id, station in self._heating_stations.items(): for station_id, station in self._heating_stations.items():
if station.state == HeatingStationState.IDLE: if station.state == HeatingStationState.IDLE:
@@ -269,23 +272,12 @@ class VirtualWorkbench:
return None return None
def _acquire_arm(self, task_description: str) -> bool: def _acquire_arm(self, task_description: str) -> bool:
"""获取机械臂使用权阻塞直到获取 """获取机械臂使用权(阻塞直到获取)"""
Args:
task_description: 任务描述,用于日志
Returns:
是否成功获取
"""
self.logger.info(f"[{task_description}] 等待获取机械臂...") self.logger.info(f"[{task_description}] 等待获取机械臂...")
# 阻塞等待获取锁
self._arm_lock.acquire() self._arm_lock.acquire()
self._arm_state = ArmState.BUSY self._arm_state = ArmState.BUSY
self._arm_current_task = task_description self._arm_current_task = task_description
self._update_data_status(f"机械臂执行: {task_description}") self._update_data_status(f"机械臂执行: {task_description}")
self.logger.info(f"[{task_description}] 成功获取机械臂使用权") self.logger.info(f"[{task_description}] 成功获取机械臂使用权")
return True return True
@@ -298,6 +290,22 @@ class VirtualWorkbench:
self._update_data_status(f"机械臂已释放 (完成: {task})") self._update_data_status(f"机械臂已释放 (完成: {task})")
self.logger.info(f"机械臂已释放 (完成: {task})") self.logger.info(f"机械臂已释放 (完成: {task})")
@action(
auto_prefix=True,
description="批量准备物料 - 虚拟起始节点, 生成A1-A5物料, 输出5个handle供后续节点使用",
handles=[
ActionOutputHandle(key="channel_1", data_type="workbench_material",
label="实验1", data_key="material_1", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="channel_2", data_type="workbench_material",
label="实验2", data_key="material_2", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="channel_3", data_type="workbench_material",
label="实验3", data_key="material_3", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="channel_4", data_type="workbench_material",
label="实验4", data_key="material_4", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="channel_5", data_type="workbench_material",
label="实验5", data_key="material_5", data_source=DataSource.EXECUTOR),
],
)
def prepare_materials( def prepare_materials(
self, self,
sample_uuids: SampleUUIDsType, sample_uuids: SampleUUIDsType,
@@ -306,19 +314,14 @@ class VirtualWorkbench:
""" """
批量准备物料 - 虚拟起始节点 批量准备物料 - 虚拟起始节点
作为工作流的起始节点生成指定数量的物料编号供后续节点使用。 作为工作流的起始节点, 生成指定数量的物料编号供后续节点使用。
输出5个handle (material_1 ~ material_5)分别对应实验1~5。 输出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)] materials = [i for i in range(1, count + 1)]
self.logger.info(f"[准备物料] 生成 {count} 个物料: " f"A1-A{count} -> material_1~material_{count}") self.logger.info(
f"[准备物料] 生成 {count} 个物料: A1-A{count} -> material_1~material_{count}"
)
return { return {
"success": True, "success": True,
@@ -329,9 +332,28 @@ class VirtualWorkbench:
"material_4": materials[3] if len(materials) > 3 else 0, "material_4": materials[3] if len(materials) > 3 else 0,
"material_5": materials[4] if len(materials) > 4 else 0, "material_5": materials[4] if len(materials) > 4 else 0,
"message": f"已准备 {count} 个物料: A1-A{count}", "message": f"已准备 {count} 个物料: A1-A{count}",
"unilabos_samples": [LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for sample_uuid, content in sample_uuids.items()] "unilabos_samples": [
LabSample(
sample_uuid=sample_uuid,
oss_path="",
extra={"material_uuid": content} if isinstance(content, str) else (content.serialize() if content else {}),
)
for sample_uuid, content in sample_uuids.items()
],
} }
@action(
auto_prefix=True,
description="将物料从An位置移动到空闲加热台, 返回分配的加热台ID",
handles=[
ActionInputHandle(key="material_input", data_type="workbench_material",
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
ActionOutputHandle(key="heating_station_output", data_type="workbench_station",
label="加热台ID", data_key="station_id", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="material_number_output", data_type="workbench_material",
label="物料编号", data_key="material_number", data_source=DataSource.EXECUTOR),
],
)
def move_to_heating_station( def move_to_heating_station(
self, self,
sample_uuids: SampleUUIDsType, sample_uuids: SampleUUIDsType,
@@ -340,20 +362,12 @@ class VirtualWorkbench:
""" """
将物料从An位置移动到加热台 将物料从An位置移动到加热台
多线程并发调用时会竞争机械臂使用权并自动查找空闲加热台 多线程并发调用时, 会竞争机械臂使用权, 并自动查找空闲加热台
Args:
material_number: 物料编号 (1-5)
Returns:
MoveToHeatingStationResult: 包含 station_id, material_number 等用于传递给下一个节点
""" """
# 根据物料编号生成物料ID
material_id = f"A{material_number}" material_id = f"A{material_number}"
task_desc = f"移动{material_id}到加热台" task_desc = f"移动{material_id}到加热台"
self.logger.info(f"[任务] {task_desc} - 开始执行") self.logger.info(f"[任务] {task_desc} - 开始执行")
# 记录任务
with self._tasks_lock: with self._tasks_lock:
self._active_tasks[material_id] = { self._active_tasks[material_id] = {
"status": "waiting_for_arm", "status": "waiting_for_arm",
@@ -361,33 +375,27 @@ class VirtualWorkbench:
} }
try: try:
# 步骤1: 等待获取机械臂使用权(竞争)
with self._tasks_lock: with self._tasks_lock:
self._active_tasks[material_id]["status"] = "waiting_for_arm" self._active_tasks[material_id]["status"] = "waiting_for_arm"
self._acquire_arm(task_desc) self._acquire_arm(task_desc)
# 步骤2: 查找空闲加热台
with self._tasks_lock: with self._tasks_lock:
self._active_tasks[material_id]["status"] = "finding_station" self._active_tasks[material_id]["status"] = "finding_station"
station_id = None station_id = None
# 循环等待直到找到空闲加热台
while station_id is None: while station_id is None:
station_id = self._find_available_heating_station() station_id = self._find_available_heating_station()
if station_id is None: if station_id is None:
self.logger.info(f"[{material_id}] 没有空闲加热台等待中...") self.logger.info(f"[{material_id}] 没有空闲加热台, 等待中...")
# 释放机械臂,等待后重试
self._release_arm() self._release_arm()
time.sleep(0.5) time.sleep(0.5)
self._acquire_arm(task_desc) self._acquire_arm(task_desc)
# 步骤3: 占用加热台 - 立即标记为OCCUPIED防止其他任务选择同一加热台
with self._stations_lock: with self._stations_lock:
self._heating_stations[station_id].state = HeatingStationState.OCCUPIED self._heating_stations[station_id].state = HeatingStationState.OCCUPIED
self._heating_stations[station_id].current_material = material_id self._heating_stations[station_id].current_material = material_id
self._heating_stations[station_id].material_number = material_number self._heating_stations[station_id].material_number = material_number
# 步骤4: 模拟机械臂移动操作 (3秒)
with self._tasks_lock: with self._tasks_lock:
self._active_tasks[material_id]["status"] = "arm_moving" self._active_tasks[material_id]["status"] = "arm_moving"
self._active_tasks[material_id]["assigned_station"] = station_id self._active_tasks[material_id]["assigned_station"] = station_id
@@ -395,11 +403,11 @@ class VirtualWorkbench:
time.sleep(self.ARM_OPERATION_TIME) time.sleep(self.ARM_OPERATION_TIME)
# 步骤5: 放入加热台完成
self._update_data_status(f"{material_id}已放入加热台{station_id}") self._update_data_status(f"{material_id}已放入加热台{station_id}")
self.logger.info(f"[{material_id}] 已放入加热台{station_id} (用时{self.ARM_OPERATION_TIME}s)") self.logger.info(
f"[{material_id}] 已放入加热台{station_id} (用时{self.ARM_OPERATION_TIME}s)"
)
# 释放机械臂
self._release_arm() self._release_arm()
with self._tasks_lock: with self._tasks_lock:
@@ -412,8 +420,16 @@ class VirtualWorkbench:
"material_number": material_number, "material_number": material_number,
"message": f"{material_id}已成功移动到加热台{station_id}", "message": f"{material_id}已成功移动到加热台{station_id}",
"unilabos_samples": [ "unilabos_samples": [
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for LabSample(
sample_uuid, content in sample_uuids.items()] sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
except Exception as e: except Exception as e:
@@ -427,11 +443,33 @@ class VirtualWorkbench:
"material_number": material_number, "material_number": material_number,
"message": f"移动失败: {str(e)}", "message": f"移动失败: {str(e)}",
"unilabos_samples": [ "unilabos_samples": [
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for LabSample(
sample_uuid, content in sample_uuids.items()] sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
@always_free @action(
auto_prefix=True,
always_free=True,
description="启动指定加热台的加热程序",
handles=[
ActionInputHandle(key="station_id_input", data_type="workbench_station",
label="加热台ID", data_key="station_id", data_source=DataSource.HANDLE),
ActionInputHandle(key="material_number_input", data_type="workbench_material",
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
ActionOutputHandle(key="heating_done_station", data_type="workbench_station",
label="加热完成-加热台ID", data_key="station_id", data_source=DataSource.EXECUTOR),
ActionOutputHandle(key="heating_done_material", data_type="workbench_material",
label="加热完成-物料编号", data_key="material_number", data_source=DataSource.EXECUTOR),
],
)
def start_heating( def start_heating(
self, self,
sample_uuids: SampleUUIDsType, sample_uuids: SampleUUIDsType,
@@ -440,13 +478,6 @@ class VirtualWorkbench:
) -> StartHeatingResult: ) -> 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}] 开始加热") self.logger.info(f"[加热台{station_id}] 开始加热")
@@ -458,8 +489,16 @@ class VirtualWorkbench:
"material_number": material_number, "material_number": material_number,
"message": f"无效的加热台ID: {station_id}", "message": f"无效的加热台ID: {station_id}",
"unilabos_samples": [ "unilabos_samples": [
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for LabSample(
sample_uuid, content in sample_uuids.items()] sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
with self._stations_lock: with self._stations_lock:
@@ -473,8 +512,16 @@ class VirtualWorkbench:
"material_number": material_number, "material_number": material_number,
"message": f"加热台{station_id}上没有物料", "message": f"加热台{station_id}上没有物料",
"unilabos_samples": [ "unilabos_samples": [
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for LabSample(
sample_uuid, content in sample_uuids.items()] sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
if station.state == HeatingStationState.HEATING: if station.state == HeatingStationState.HEATING:
@@ -485,13 +532,20 @@ class VirtualWorkbench:
"material_number": material_number, "material_number": material_number,
"message": f"加热台{station_id}已经在加热中", "message": f"加热台{station_id}已经在加热中",
"unilabos_samples": [ "unilabos_samples": [
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for LabSample(
sample_uuid, content in sample_uuids.items()] sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
material_id = station.current_material material_id = station.current_material
# 开始加热
station.state = HeatingStationState.HEATING station.state = HeatingStationState.HEATING
station.heating_start_time = time.time() station.heating_start_time = time.time()
station.heating_progress = 0.0 station.heating_progress = 0.0
@@ -502,7 +556,6 @@ class VirtualWorkbench:
self._update_data_status(f"加热台{station_id}开始加热{material_id}") self._update_data_status(f"加热台{station_id}开始加热{material_id}")
# 打印当前所有正在加热的台位
with self._stations_lock: with self._stations_lock:
heating_list = [ heating_list = [
f"加热台{sid}:{s.current_material}" f"加热台{sid}:{s.current_material}"
@@ -511,7 +564,6 @@ class VirtualWorkbench:
] ]
self.logger.info(f"[并行加热] 当前同时加热中: {', '.join(heating_list)}") self.logger.info(f"[并行加热] 当前同时加热中: {', '.join(heating_list)}")
# 模拟加热过程
start_time = time.time() start_time = time.time()
last_countdown_log = start_time last_countdown_log = start_time
while True: while True:
@@ -524,7 +576,6 @@ class VirtualWorkbench:
self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%") self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%")
# 每5秒打印一次倒计时
if time.time() - last_countdown_log >= 5.0: if time.time() - last_countdown_log >= 5.0:
self.logger.info(f"[加热台{station_id}] {material_id} 剩余 {remaining:.1f}s") self.logger.info(f"[加热台{station_id}] {material_id} 剩余 {remaining:.1f}s")
last_countdown_log = time.time() last_countdown_log = time.time()
@@ -534,7 +585,6 @@ class VirtualWorkbench:
time.sleep(1.0) time.sleep(1.0)
# 加热完成
with self._stations_lock: with self._stations_lock:
self._heating_stations[station_id].state = HeatingStationState.COMPLETED self._heating_stations[station_id].state = HeatingStationState.COMPLETED
self._heating_stations[station_id].heating_progress = 100.0 self._heating_stations[station_id].heating_progress = 100.0
@@ -553,10 +603,28 @@ class VirtualWorkbench:
"material_number": material_number, "material_number": material_number,
"message": f"加热台{station_id}加热完成", "message": f"加热台{station_id}加热完成",
"unilabos_samples": [ "unilabos_samples": [
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for LabSample(
sample_uuid, content in sample_uuids.items()] sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
@action(
auto_prefix=True,
description="将物料从加热台移动到输出位置Cn",
handles=[
ActionInputHandle(key="output_station_input", data_type="workbench_station",
label="加热台ID", data_key="station_id", data_source=DataSource.HANDLE),
ActionInputHandle(key="output_material_input", data_type="workbench_material",
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
],
)
def move_to_output( def move_to_output(
self, self,
sample_uuids: SampleUUIDsType, sample_uuids: SampleUUIDsType,
@@ -565,15 +633,8 @@ class VirtualWorkbench:
) -> MoveToOutputResult: ) -> MoveToOutputResult:
""" """
将物料从加热台移动到输出位置Cn 将物料从加热台移动到输出位置Cn
Args:
station_id: 加热台ID (1-3),从 start_heating 的 handle 传入
material_number: 物料编号,从 start_heating 的 handle 传入,用于确定输出位置 Cn
Returns:
MoveToOutputResult: 包含执行结果
""" """
output_number = material_number # 物料编号决定输出位置 output_number = material_number
if station_id not in self._heating_stations: if station_id not in self._heating_stations:
return { return {
@@ -583,8 +644,16 @@ class VirtualWorkbench:
"output_position": f"C{output_number}", "output_position": f"C{output_number}",
"message": f"无效的加热台ID: {station_id}", "message": f"无效的加热台ID: {station_id}",
"unilabos_samples": [ "unilabos_samples": [
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for LabSample(
sample_uuid, content in sample_uuids.items()] sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
with self._stations_lock: with self._stations_lock:
@@ -599,8 +668,16 @@ class VirtualWorkbench:
"output_position": f"C{output_number}", "output_position": f"C{output_number}",
"message": f"加热台{station_id}上没有物料", "message": f"加热台{station_id}上没有物料",
"unilabos_samples": [ "unilabos_samples": [
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for LabSample(
sample_uuid, content in sample_uuids.items()] sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
if station.state != HeatingStationState.COMPLETED: if station.state != HeatingStationState.COMPLETED:
@@ -611,8 +688,16 @@ class VirtualWorkbench:
"output_position": f"C{output_number}", "output_position": f"C{output_number}",
"message": f"加热台{station_id}尚未完成加热 (当前状态: {station.state.value})", "message": f"加热台{station_id}尚未完成加热 (当前状态: {station.state.value})",
"unilabos_samples": [ "unilabos_samples": [
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for LabSample(
sample_uuid, content in sample_uuids.items()] sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
output_position = f"C{output_number}" output_position = f"C{output_number}"
@@ -624,18 +709,17 @@ class VirtualWorkbench:
if material_id in self._active_tasks: if material_id in self._active_tasks:
self._active_tasks[material_id]["status"] = "waiting_for_arm_output" self._active_tasks[material_id]["status"] = "waiting_for_arm_output"
# 获取机械臂
self._acquire_arm(task_desc) self._acquire_arm(task_desc)
with self._tasks_lock: with self._tasks_lock:
if material_id in self._active_tasks: if material_id in self._active_tasks:
self._active_tasks[material_id]["status"] = "arm_moving_to_output" self._active_tasks[material_id]["status"] = "arm_moving_to_output"
# 模拟机械臂操作 (3秒) self.logger.info(
self.logger.info(f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}...") f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}..."
)
time.sleep(self.ARM_OPERATION_TIME) time.sleep(self.ARM_OPERATION_TIME)
# 清空加热台
with self._stations_lock: with self._stations_lock:
self._heating_stations[station_id].state = HeatingStationState.IDLE self._heating_stations[station_id].state = HeatingStationState.IDLE
self._heating_stations[station_id].current_material = None self._heating_stations[station_id].current_material = None
@@ -643,17 +727,17 @@ class VirtualWorkbench:
self._heating_stations[station_id].heating_progress = 0.0 self._heating_stations[station_id].heating_progress = 0.0
self._heating_stations[station_id].heating_start_time = None self._heating_stations[station_id].heating_start_time = None
# 释放机械臂
self._release_arm() self._release_arm()
# 任务完成
with self._tasks_lock: with self._tasks_lock:
if material_id in self._active_tasks: if material_id in self._active_tasks:
self._active_tasks[material_id]["status"] = "completed" self._active_tasks[material_id]["status"] = "completed"
self._active_tasks[material_id]["end_time"] = time.time() self._active_tasks[material_id]["end_time"] = time.time()
self._update_data_status(f"{material_id}已移动到{output_position}") self._update_data_status(f"{material_id}已移动到{output_position}")
self.logger.info(f"[{material_id}] 已成功移动到{output_position} (用时{self.ARM_OPERATION_TIME}s)") self.logger.info(
f"[{material_id}] 已成功移动到{output_position} (用时{self.ARM_OPERATION_TIME}s)"
)
return { return {
"success": True, "success": True,
@@ -662,8 +746,17 @@ class VirtualWorkbench:
"output_position": output_position, "output_position": output_position,
"message": f"{material_id}已成功移动到{output_position}", "message": f"{material_id}已成功移动到{output_position}",
"unilabos_samples": [ "unilabos_samples": [
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for LabSample(
sample_uuid, content in sample_uuids.items()] sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str)
else (content.serialize() if content is not None else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
except Exception as e: except Exception as e:
@@ -677,83 +770,105 @@ class VirtualWorkbench:
"output_position": output_position, "output_position": output_position,
"message": f"移动失败: {str(e)}", "message": f"移动失败: {str(e)}",
"unilabos_samples": [ "unilabos_samples": [
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for LabSample(
sample_uuid, content in sample_uuids.items()] sample_uuid=sample_uuid,
oss_path="",
extra=(
{"material_uuid": content}
if isinstance(content, str) else (content.serialize() if content else {})
),
)
for sample_uuid, content in sample_uuids.items()
],
} }
# ============ 状态属性 ============ # ============ 状态属性 ============
@property @property
@topic_config()
def status(self) -> str: def status(self) -> str:
return self.data.get("status", "Unknown") return self.data.get("status", "Unknown")
@property @property
@topic_config()
def arm_state(self) -> str: def arm_state(self) -> str:
return self._arm_state.value return self._arm_state.value
@property @property
@topic_config()
def arm_current_task(self) -> str: def arm_current_task(self) -> str:
return self._arm_current_task or "" return self._arm_current_task or ""
@property @property
@topic_config()
def heating_station_1_state(self) -> str: def heating_station_1_state(self) -> str:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(1) station = self._heating_stations.get(1)
return station.state.value if station else "unknown" return station.state.value if station else "unknown"
@property @property
@topic_config()
def heating_station_1_material(self) -> str: def heating_station_1_material(self) -> str:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(1) station = self._heating_stations.get(1)
return station.current_material or "" if station else "" return station.current_material or "" if station else ""
@property @property
@topic_config()
def heating_station_1_progress(self) -> float: def heating_station_1_progress(self) -> float:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(1) station = self._heating_stations.get(1)
return station.heating_progress if station else 0.0 return station.heating_progress if station else 0.0
@property @property
@topic_config()
def heating_station_2_state(self) -> str: def heating_station_2_state(self) -> str:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(2) station = self._heating_stations.get(2)
return station.state.value if station else "unknown" return station.state.value if station else "unknown"
@property @property
@topic_config()
def heating_station_2_material(self) -> str: def heating_station_2_material(self) -> str:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(2) station = self._heating_stations.get(2)
return station.current_material or "" if station else "" return station.current_material or "" if station else ""
@property @property
@topic_config()
def heating_station_2_progress(self) -> float: def heating_station_2_progress(self) -> float:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(2) station = self._heating_stations.get(2)
return station.heating_progress if station else 0.0 return station.heating_progress if station else 0.0
@property @property
@topic_config()
def heating_station_3_state(self) -> str: def heating_station_3_state(self) -> str:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(3) station = self._heating_stations.get(3)
return station.state.value if station else "unknown" return station.state.value if station else "unknown"
@property @property
@topic_config()
def heating_station_3_material(self) -> str: def heating_station_3_material(self) -> str:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(3) station = self._heating_stations.get(3)
return station.current_material or "" if station else "" return station.current_material or "" if station else ""
@property @property
@topic_config()
def heating_station_3_progress(self) -> float: def heating_station_3_progress(self) -> float:
with self._stations_lock: with self._stations_lock:
station = self._heating_stations.get(3) station = self._heating_stations.get(3)
return station.heating_progress if station else 0.0 return station.heating_progress if station else 0.0
@property @property
@topic_config()
def active_tasks_count(self) -> int: def active_tasks_count(self) -> int:
with self._tasks_lock: with self._tasks_lock:
return len(self._active_tasks) return len(self._active_tasks)
@property @property
@topic_config()
def message(self) -> str: def message(self) -> str:
return self.data.get("message", "") return self.data.get("message", "")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,614 @@
"""
装饰器注册表系统
通过 @device, @action, @resource 装饰器替代 YAML 配置文件来定义设备/动作/资源注册表信息。
Usage:
from unilabos.registry.decorators import (
device, action, resource,
InputHandle, OutputHandle,
ActionInputHandle, ActionOutputHandle,
HardwareInterface, Side, DataSource,
)
@device(
id="solenoid_valve.mock",
category=["pump_and_valve"],
description="模拟电磁阀设备",
handles=[
InputHandle(key="in", data_type="fluid", label="in", side=Side.NORTH),
OutputHandle(key="out", data_type="fluid", label="out", side=Side.SOUTH),
],
hardware_interface=HardwareInterface(
name="hardware_interface",
read="send_command",
write="send_command",
),
)
class SolenoidValveMock:
@action(action_type=EmptyIn)
def close(self):
...
@action(
handles=[
ActionInputHandle(key="in", data_type="fluid", label="in"),
ActionOutputHandle(key="out", data_type="fluid", label="out"),
],
)
def set_valve_position(self, position):
...
# 无 @action 装饰器 => auto- 前缀动作
def is_open(self):
...
"""
from enum import Enum
from functools import wraps
from typing import Any, Callable, Dict, List, Optional, TypeVar
from pydantic import BaseModel, ConfigDict, Field
F = TypeVar("F", bound=Callable[..., Any])
# ---------------------------------------------------------------------------
# 枚举
# ---------------------------------------------------------------------------
class Side(str, Enum):
"""UI 上 Handle 的显示位置"""
NORTH = "NORTH"
SOUTH = "SOUTH"
EAST = "EAST"
WEST = "WEST"
class DataSource(str, Enum):
"""Handle 的数据来源"""
HANDLE = "handle" # 从上游 handle 获取数据 (用于 InputHandle)
EXECUTOR = "executor" # 从执行器输出数据 (用于 OutputHandle)
# ---------------------------------------------------------------------------
# Device / Resource Handle (设备/资源级别端口, 序列化时包含 io_type)
# ---------------------------------------------------------------------------
class _DeviceHandleBase(BaseModel):
"""设备/资源端口基类 (内部使用)"""
model_config = ConfigDict(populate_by_name=True)
key: str = Field(serialization_alias="handler_key")
data_type: str
label: str
side: Optional[Side] = None
data_key: Optional[str] = None
data_source: Optional[str] = None
description: Optional[str] = None
# 子类覆盖
io_type: str = ""
def to_registry_dict(self) -> Dict[str, Any]:
return self.model_dump(by_alias=True, exclude_none=True)
class InputHandle(_DeviceHandleBase):
"""
输入端口 (io_type="target"), 用于 @device / @resource handles
Example:
InputHandle(key="in", data_type="fluid", label="in", side=Side.NORTH)
"""
io_type: str = "target"
class OutputHandle(_DeviceHandleBase):
"""
输出端口 (io_type="source"), 用于 @device / @resource handles
Example:
OutputHandle(key="out", data_type="fluid", label="out", side=Side.SOUTH)
"""
io_type: str = "source"
# ---------------------------------------------------------------------------
# Action Handle (动作级别端口, 序列化时不含 io_type, 按类型自动分组)
# ---------------------------------------------------------------------------
class _ActionHandleBase(BaseModel):
"""动作端口基类 (内部使用)"""
model_config = ConfigDict(populate_by_name=True)
key: str = Field(serialization_alias="handler_key")
data_type: str
label: str
side: Optional[Side] = None
data_key: Optional[str] = None
data_source: Optional[str] = None
description: Optional[str] = None
io_type: Optional[str] = None # source/sink (dataflow) or target/source (device-style)
def to_registry_dict(self) -> Dict[str, Any]:
return self.model_dump(by_alias=True, exclude_none=True)
class ActionInputHandle(_ActionHandleBase):
"""
动作输入端口, 用于 @action handles, 序列化后归入 "input"
Example:
ActionInputHandle(
key="material_input", data_type="workbench_material",
label="物料编号", data_key="material_number", data_source="handle",
)
"""
pass
class ActionOutputHandle(_ActionHandleBase):
"""
动作输出端口, 用于 @action handles, 序列化后归入 "output"
Example:
ActionOutputHandle(
key="station_output", data_type="workbench_station",
label="加热台ID", data_key="station_id", data_source="executor",
)
"""
pass
# ---------------------------------------------------------------------------
# HardwareInterface
# ---------------------------------------------------------------------------
class HardwareInterface(BaseModel):
"""
硬件通信接口定义
描述设备与底层硬件通信的方式 (串口、Modbus 等)。
Example:
HardwareInterface(name="hardware_interface", read="send_command", write="send_command")
"""
name: str
read: Optional[str] = None
write: Optional[str] = None
extra_info: Optional[List[str]] = None
# ---------------------------------------------------------------------------
# 全局注册表 -- 记录所有被装饰器标记的类/函数
# ---------------------------------------------------------------------------
_registered_devices: Dict[str, type] = {} # device_id -> class
_registered_resources: Dict[str, Any] = {} # resource_id -> class or function
def _device_handles_to_list(
handles: Optional[List[_DeviceHandleBase]],
) -> List[Dict[str, Any]]:
"""将设备/资源 Handle 列表序列化为字典列表 (含 io_type)"""
if handles is None:
return []
return [h.to_registry_dict() for h in handles]
def _action_handles_to_dict(
handles: Optional[List[_ActionHandleBase]],
) -> Dict[str, Any]:
"""
将动作 Handle 列表序列化为 {"input": [...], "output": [...]} 格式。
ActionInputHandle => "input", ActionOutputHandle => "output"
"""
if handles is None:
return {}
input_list = [h.to_registry_dict() for h in handles if isinstance(h, ActionInputHandle)]
output_list = [h.to_registry_dict() for h in handles if isinstance(h, ActionOutputHandle)]
result: Dict[str, Any] = {}
if input_list:
result["input"] = input_list
if output_list:
result["output"] = output_list
return result
# ---------------------------------------------------------------------------
# @device 类装饰器
# ---------------------------------------------------------------------------
# noinspection PyShadowingBuiltins
def device(
id: Optional[str] = None,
ids: Optional[List[str]] = None,
id_meta: Optional[Dict[str, Dict[str, Any]]] = None,
category: Optional[List[str]] = None,
description: str = "",
display_name: str = "",
icon: str = "",
version: str = "1.0.0",
handles: Optional[List[_DeviceHandleBase]] = None,
model: Optional[Dict[str, Any]] = None,
device_type: str = "python",
hardware_interface: Optional[HardwareInterface] = None,
):
"""
设备类装饰器
将类标记为一个 UniLab-OS 设备,并附加注册表元数据。
支持两种模式:
1. 单设备: id="xxx", category=[...]
2. 多设备: ids=["id1","id2"], id_meta={"id1":{handles:[...]}, "id2":{...}}
Args:
id: 单设备时的注册表唯一标识
ids: 多设备时的 id 列表,与 id_meta 配合使用
id_meta: 每个 device_id 的覆盖元数据 (handles/description/icon/model)
category: 设备分类标签列表 (必填)
description: 设备描述
display_name: 人类可读的设备显示名称,缺失时默认使用 id
icon: 图标路径
version: 版本号
handles: 设备端口列表 (单设备或 id_meta 未覆盖时使用)
model: 可选的 3D 模型配置
device_type: 设备实现类型 ("python" / "ros2")
hardware_interface: 硬件通信接口 (HardwareInterface)
"""
# Resolve device ids
if ids is not None:
device_ids = list(ids)
if not device_ids:
raise ValueError("@device ids 不能为空")
id_meta = id_meta or {}
elif id is not None:
device_ids = [id]
id_meta = {}
else:
raise ValueError("@device 必须提供 id 或 ids")
if category is None:
raise ValueError("@device category 必填")
base_meta = {
"category": category,
"description": description,
"display_name": display_name,
"icon": icon,
"version": version,
"handles": _device_handles_to_list(handles),
"model": model,
"device_type": device_type,
"hardware_interface": (hardware_interface.model_dump(exclude_none=True) if hardware_interface else None),
}
def decorator(cls):
cls._device_registry_meta = base_meta
cls._device_registry_id_meta = id_meta
cls._device_registry_ids = device_ids
for did in device_ids:
if did in _registered_devices:
raise ValueError(f"@device id 重复: '{did}' 已被 {_registered_devices[did]} 注册")
_registered_devices[did] = cls
return cls
return decorator
# ---------------------------------------------------------------------------
# @action 方法装饰器
# ---------------------------------------------------------------------------
# 区分 "用户没传 action_type" 和 "用户传了 None"
_ACTION_TYPE_UNSET = object()
# noinspection PyShadowingNames
def action(
action_type: Any = _ACTION_TYPE_UNSET,
goal: Optional[Dict[str, str]] = None,
feedback: Optional[Dict[str, str]] = None,
result: Optional[Dict[str, str]] = None,
handles: Optional[List[_ActionHandleBase]] = None,
goal_default: Optional[Dict[str, Any]] = None,
placeholder_keys: Optional[Dict[str, str]] = None,
always_free: bool = False,
is_protocol: bool = False,
description: str = "",
auto_prefix: bool = False,
parent: bool = False,
):
"""
动作方法装饰器
标记方法为注册表动作。有三种用法:
1. @action(action_type=EmptyIn, ...) -- 非 auto, 使用指定 ROS Action 类型
2. @action() -- 非 auto, UniLabJsonCommand (从方法签名生成 schema)
3. 不加 @action -- auto- 前缀, UniLabJsonCommand
Protocol 用法:
@action(action_type=Add, is_protocol=True)
def AddProtocol(self): ...
标记该动作为高级协议 (protocol),运行时通过 ROS Action 路由到
protocol generator 执行。action_type 指向 unilabos_msgs 的 Action 类型。
Args:
action_type: ROS Action 消息类型 (如 EmptyIn, SendCmd, HeatChill).
不传/默认 = UniLabJsonCommand (非 auto).
goal: Goal 字段映射 (ROS字段名 -> 设备参数名).
protocol 模式下可留空,系统自动生成 identity 映射.
feedback: Feedback 字段映射
result: Result 字段映射
handles: 动作端口列表 (ActionInputHandle / ActionOutputHandle)
goal_default: Goal 字段默认值映射 (字段名 -> 默认值), 与自动生成的 goal_default 合并
placeholder_keys: 参数占位符配置
always_free: 是否为永久闲置动作 (不受排队限制)
is_protocol: 是否为工作站协议 (protocol)。True 时运行时走 protocol generator 路径。
description: 动作描述
auto_prefix: 若为 True动作名使用 auto-{method_name} 形式(与无 @action 时一致)
parent: 若为 True当方法参数为空 (*args, **kwargs) 时,通过 MRO 从父类获取真实方法参数
"""
def decorator(func: F) -> F:
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# action_type 为哨兵值 => 用户没传, 视为 None (UniLabJsonCommand)
resolved_type = None if action_type is _ACTION_TYPE_UNSET else action_type
meta = {
"action_type": resolved_type,
"goal": goal or {},
"feedback": feedback or {},
"result": result or {},
"handles": _action_handles_to_dict(handles),
"goal_default": goal_default or {},
"placeholder_keys": placeholder_keys or {},
"always_free": always_free,
"is_protocol": is_protocol,
"description": description,
"auto_prefix": auto_prefix,
"parent": parent,
}
wrapper._action_registry_meta = meta # type: ignore[attr-defined]
# 设置 _is_always_free 保持与旧 @always_free 装饰器兼容
if always_free:
wrapper._is_always_free = True # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
return decorator
def get_action_meta(func) -> Optional[Dict[str, Any]]:
"""获取方法上的 @action 装饰器元数据"""
return getattr(func, "_action_registry_meta", None)
def has_action_decorator(func) -> bool:
"""检查函数是否带有 @action 装饰器"""
return hasattr(func, "_action_registry_meta")
# ---------------------------------------------------------------------------
# @resource 类/函数装饰器
# ---------------------------------------------------------------------------
def resource(
id: str,
category: List[str],
description: str = "",
icon: str = "",
version: str = "1.0.0",
handles: Optional[List[_DeviceHandleBase]] = None,
model: Optional[Dict[str, Any]] = None,
class_type: str = "pylabrobot",
):
"""
资源类/函数装饰器
将类或工厂函数标记为一个 UniLab-OS 资源,附加注册表元数据。
Args:
id: 注册表唯一标识 (必填, 不可重复)
category: 资源分类标签列表 (必填)
description: 资源描述
icon: 图标路径
version: 版本号
handles: 端口列表 (InputHandle / OutputHandle)
model: 可选的 3D 模型配置
class_type: 资源实现类型 ("python" / "pylabrobot" / "unilabos")
"""
def decorator(obj):
meta = {
"resource_id": id,
"category": category,
"description": description,
"icon": icon,
"version": version,
"handles": _device_handles_to_list(handles),
"model": model,
"class_type": class_type,
}
obj._resource_registry_meta = meta
if id in _registered_resources:
raise ValueError(f"@resource id 重复: '{id}' 已被 {_registered_resources[id]} 注册")
_registered_resources[id] = obj
return obj
return decorator
def get_device_meta(cls, device_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""
获取类上的 @device 装饰器元数据。
当 device_id 存在且类使用 ids+id_meta 时,返回合并后的 meta
(base_meta 与 id_meta[device_id] 深度合并)。
"""
base = getattr(cls, "_device_registry_meta", None)
if base is None:
return None
id_meta = getattr(cls, "_device_registry_id_meta", None) or {}
if device_id is None or device_id not in id_meta:
result = dict(base)
ids = getattr(cls, "_device_registry_ids", None)
result["device_id"] = device_id if device_id is not None else (ids[0] if ids else None)
return result
overrides = id_meta[device_id]
result = dict(base)
result["device_id"] = device_id
for key in ["handles", "description", "icon", "model"]:
if key in overrides:
val = overrides[key]
if key == "handles" and isinstance(val, list):
# handles 必须是 Handle 对象列表
result[key] = [h.to_registry_dict() for h in val]
else:
result[key] = val
return result
def get_resource_meta(obj) -> Optional[Dict[str, Any]]:
"""获取对象上的 @resource 装饰器元数据"""
return getattr(obj, "_resource_registry_meta", None)
def get_all_registered_devices() -> Dict[str, type]:
"""获取所有已注册的设备类"""
return _registered_devices.copy()
def get_all_registered_resources() -> Dict[str, Any]:
"""获取所有已注册的资源"""
return _registered_resources.copy()
def clear_registry():
"""清空全局注册表 (用于测试)"""
_registered_devices.clear()
_registered_resources.clear()
# ---------------------------------------------------------------------------
# topic_config / not_action / always_free 装饰器
# ---------------------------------------------------------------------------
def topic_config(
period: Optional[float] = None,
print_publish: Optional[bool] = None,
qos: Optional[int] = None,
name: Optional[str] = None,
) -> Callable[[F], F]:
"""
Topic发布配置装饰器
用于装饰 get_{attr_name} 方法或 @property控制对应属性的ROS topic发布行为。
Args:
period: 发布周期。None 表示使用默认值 5.0
print_publish: 是否打印发布日志。None 表示使用节点默认配置
qos: QoS深度配置。None 表示使用默认值 10
name: 自定义发布名称。None 表示使用方法名(去掉 get_ 前缀)
Note:
与 @property 连用时,@topic_config 必须放在 @property 下面,
这样装饰器执行顺序为:先 topic_config 添加配置,再 property 包装。
"""
def decorator(func: F) -> F:
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper._topic_period = period # type: ignore[attr-defined]
wrapper._topic_print_publish = print_publish # type: ignore[attr-defined]
wrapper._topic_qos = qos # type: ignore[attr-defined]
wrapper._topic_name = name # type: ignore[attr-defined]
wrapper._has_topic_config = True # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
return decorator
def get_topic_config(func) -> dict:
"""获取函数上的 topic 配置 (period, print_publish, qos, name)"""
if hasattr(func, "_has_topic_config") and getattr(func, "_has_topic_config", False):
return {
"period": getattr(func, "_topic_period", None),
"print_publish": getattr(func, "_topic_print_publish", None),
"qos": getattr(func, "_topic_qos", None),
"name": getattr(func, "_topic_name", None),
}
return {}
def always_free(func: F) -> F:
"""
标记动作为永久闲置(不受busy队列限制)的装饰器
被此装饰器标记的 action 方法,在执行时不会受到设备级别的排队限制,
任何时候请求都可以立即执行。适用于查询类、状态读取类等轻量级操作。
"""
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper._is_always_free = True # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
def is_always_free(func) -> bool:
"""检查函数是否被标记为永久闲置"""
return getattr(func, "_is_always_free", False)
def not_action(func: F) -> F:
"""
标记方法为非动作的装饰器
用于装饰 driver 类中的方法,使其在注册表扫描时不被识别为动作。
适用于辅助方法、内部工具方法等不应暴露为设备动作的公共方法。
"""
@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:
"""检查函数是否被标记为非动作"""
return getattr(func, "_is_not_action", False)

View File

@@ -13,21 +13,18 @@ Qone_nmr:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -71,31 +68,6 @@ Qone_nmr:
title: monitor_folder_for_new_content参数 title: monitor_folder_for_new_content参数
type: object type: object
type: UniLabJsonCommand 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: string
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-strings_to_txt: auto-strings_to_txt:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -138,21 +110,18 @@ Qone_nmr:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -167,32 +136,31 @@ Qone_nmr:
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:

View File

@@ -22,7 +22,8 @@ bioyond_cell:
required: required:
- xlsx_path - xlsx_path
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: auto_batch_outbound_from_xlsx参数 title: auto_batch_outbound_from_xlsx参数
@@ -490,7 +491,9 @@ bioyond_cell:
goal: goal:
properties: properties:
material_names: material_names:
type: string items:
type: string
type: array
type_id: type_id:
default: 3a190ca0-b2f6-9aeb-8067-547e72c11469 default: 3a190ca0-b2f6-9aeb-8067-547e72c11469
type: string type: string
@@ -499,7 +502,8 @@ bioyond_cell:
type: string type: string
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: create_and_inbound_materials参数 title: create_and_inbound_materials参数
@@ -535,7 +539,8 @@ bioyond_cell:
- type_id - type_id
- warehouse_name - warehouse_name
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: create_material参数 title: create_material参数
@@ -556,11 +561,16 @@ bioyond_cell:
goal: goal:
properties: properties:
mappings: mappings:
additionalProperties:
type: object
type: object type: object
required: required:
- mappings - mappings
type: object type: object
result: {} result:
items:
type: object
type: array
required: required:
- goal - goal
title: create_materials参数 title: create_materials参数
@@ -592,7 +602,8 @@ bioyond_cell:
required: required:
- xlsx_path - xlsx_path
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: create_orders参数 title: create_orders参数
@@ -624,7 +635,8 @@ bioyond_cell:
required: required:
- xlsx_path - xlsx_path
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: create_orders_v2参数 title: create_orders_v2参数
@@ -665,7 +677,8 @@ bioyond_cell:
- bottle_type - bottle_type
- location_code - location_code
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: create_sample参数 title: create_sample参数
@@ -718,7 +731,8 @@ bioyond_cell:
type: string type: string
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: order_list_v2参数 title: order_list_v2参数
@@ -821,7 +835,8 @@ bioyond_cell:
required: required:
- material_obj - material_obj
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: report_material_change参数 title: report_material_change参数
@@ -875,7 +890,8 @@ bioyond_cell:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: scheduler_continue参数 title: scheduler_continue参数
@@ -896,7 +912,8 @@ bioyond_cell:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: scheduler_reset参数 title: scheduler_reset参数
@@ -917,7 +934,8 @@ bioyond_cell:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: scheduler_start参数 title: scheduler_start参数
@@ -1362,7 +1380,8 @@ bioyond_cell:
type: string type: string
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: scheduler_start_and_auto_feeding参数 title: scheduler_start_and_auto_feeding参数
@@ -1807,7 +1826,8 @@ bioyond_cell:
type: string type: string
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: scheduler_start_and_auto_feeding_v2参数 title: scheduler_start_and_auto_feeding_v2参数
@@ -1828,7 +1848,8 @@ bioyond_cell:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: scheduler_stop参数 title: scheduler_stop参数
@@ -1850,12 +1871,15 @@ bioyond_cell:
properties: properties:
items: items:
items: items:
additionalProperties:
type: string
type: object type: object
type: array type: array
required: required:
- items - items
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: storage_batch_inbound参数 title: storage_batch_inbound参数
@@ -1884,7 +1908,8 @@ bioyond_cell:
- material_id - material_id
- location_id - location_id
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: storage_inbound参数 title: storage_inbound参数
@@ -1905,7 +1930,8 @@ bioyond_cell:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: transfer_1_to_2参数 title: transfer_1_to_2参数
@@ -1946,7 +1972,8 @@ bioyond_cell:
type: integer type: integer
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: transfer_3_to_2参数 title: transfer_3_to_2参数
@@ -1983,7 +2010,8 @@ bioyond_cell:
type: integer type: integer
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: transfer_3_to_2_to_1参数 title: transfer_3_to_2_to_1参数
@@ -2007,10 +2035,11 @@ bioyond_cell:
ip: ip:
type: string type: string
port: port:
type: string type: integer
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: update_push_ip参数 title: update_push_ip参数
@@ -2039,7 +2068,8 @@ bioyond_cell:
required: required:
- order_code - order_code
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: wait_for_order_finish参数 title: wait_for_order_finish参数
@@ -2072,7 +2102,8 @@ bioyond_cell:
required: required:
- order_code - order_code
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: wait_for_order_finish_polling参数 title: wait_for_order_finish_polling参数
@@ -2104,7 +2135,8 @@ bioyond_cell:
type: integer type: integer
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: wait_for_transfer_task参数 title: wait_for_transfer_task参数
@@ -2112,8 +2144,7 @@ bioyond_cell:
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_studio.bioyond_cell.bioyond_cell_workstation:BioyondCellWorkstation module: unilabos.devices.workstation.bioyond_studio.bioyond_cell.bioyond_cell_workstation:BioyondCellWorkstation
status_types: status_types:
device_id: String device_id: ''
material_info: dict
type: python type: python
config_info: [] config_info: []
description: '' description: ''
@@ -2134,11 +2165,7 @@ bioyond_cell:
properties: properties:
device_id: device_id:
type: string type: string
material_info:
type: object
required: required:
- device_id - device_id
- material_info
type: object type: object
registry_type: device
version: 1.0.0 version: 1.0.0

View File

@@ -24,7 +24,8 @@ bioyond_dispensing_station:
required: required:
- data - data
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: brief_step_parameters参数 title: brief_step_parameters参数
@@ -53,7 +54,8 @@ bioyond_dispensing_station:
- report_request - report_request
- used_materials - used_materials
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: process_order_finish_report参数 title: process_order_finish_report参数
@@ -78,7 +80,8 @@ bioyond_dispensing_station:
required: required:
- order_id - order_id
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: project_order_report参数 title: project_order_report参数
@@ -128,7 +131,8 @@ bioyond_dispensing_station:
required: required:
- workflow_id - workflow_id
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: workflow_sample_locations参数 title: workflow_sample_locations参数
@@ -144,12 +148,12 @@ bioyond_dispensing_station:
temperature: temperature temperature: temperature
titration: titration titration: titration
goal_default: goal_default:
delay_time: '600' delay_time: null
hold_m_name: '' hold_m_name: null
liquid_material_name: NMP liquid_material_name: NMP
speed: '400' speed: null
temperature: '40' temperature: null
titration: '' titration: null
handles: handles:
input: input:
- data_key: titration - data_key: titration
@@ -165,20 +169,16 @@ bioyond_dispensing_station:
handler_key: BATCH_CREATE_RESULT handler_key: BATCH_CREATE_RESULT
io_type: sink io_type: sink
label: Complete Batch Create Result JSON (contains order_codes and order_ids) label: Complete Batch Create Result JSON (contains order_codes and order_ids)
result: placeholder_keys: {}
return_info: return_info result: {}
schema: schema:
description: 批量创建90%10%小瓶投料任务。从计算节点接收titration数据,包含物料名称、主称固体质量、滴定固体质量和滴定溶剂体积。返回的return_info中包含order_codes和order_ids列表。 description: 批量创建90%10%小瓶投料任务。从计算节点接收titration数据,包含物料名称、主称固体质量、滴定固体质量和滴定溶剂体积。返回的return_info中包含order_codes和order_ids列表。
properties: properties:
feedback: feedback:
properties: {}
required: []
title: BatchCreate9010VialFeedingTasks_Feedback title: BatchCreate9010VialFeedingTasks_Feedback
type: object
goal: goal:
properties: properties:
delay_time: delay_time:
default: '600'
description: 延迟时间(秒),默认600 description: 延迟时间(秒),默认600
type: string type: string
hold_m_name: hold_m_name:
@@ -189,11 +189,9 @@ bioyond_dispensing_station:
description: 10%物料的液体物料名称,默认为"NMP" description: 10%物料的液体物料名称,默认为"NMP"
type: string type: string
speed: speed:
default: '400'
description: 搅拌速度,默认400 description: 搅拌速度,默认400
type: string type: string
temperature: temperature:
default: '40'
description: 温度(℃),默认40 description: 温度(℃),默认40
type: string type: string
titration: titration:
@@ -202,21 +200,14 @@ bioyond_dispensing_station:
type: string type: string
required: required:
- titration - titration
- hold_m_name
title: BatchCreate9010VialFeedingTasks_Goal title: BatchCreate9010VialFeedingTasks_Goal
type: object type: object
result: result:
properties:
return_info:
description: 批量任务创建结果汇总JSON字符串包含total(总数)、success(成功数)、failed(失败数)、order_codes(任务编码数组)、order_ids(任务ID数组)、details(每个任务的详细信息)
type: string
required:
- return_info
title: BatchCreate9010VialFeedingTasks_Result title: BatchCreate9010VialFeedingTasks_Result
type: object type: string
required: required:
- goal - goal
title: BatchCreate9010VialFeedingTasks title: batch_create_90_10_vial_feeding_tasks参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
batch_create_diamine_solution_tasks: batch_create_diamine_solution_tasks:
@@ -228,11 +219,11 @@ bioyond_dispensing_station:
speed: speed speed: speed
temperature: temperature temperature: temperature
goal_default: goal_default:
delay_time: '600' delay_time: null
liquid_material_name: NMP liquid_material_name: NMP
solutions: '' solutions: null
speed: '400' speed: null
temperature: '20' temperature: null
handles: handles:
input: input:
- data_key: solutions - data_key: solutions
@@ -248,20 +239,16 @@ bioyond_dispensing_station:
handler_key: BATCH_CREATE_RESULT handler_key: BATCH_CREATE_RESULT
io_type: sink io_type: sink
label: Complete Batch Create Result JSON (contains order_codes and order_ids) label: Complete Batch Create Result JSON (contains order_codes and order_ids)
result: placeholder_keys: {}
return_info: return_info result: {}
schema: schema:
description: 批量创建二胺溶液配置任务。自动为多个二胺样品创建溶液配置任务每个任务包含固体物料称量、溶剂添加、搅拌混合等步骤。返回的return_info中包含order_codes和order_ids列表。 description: 批量创建二胺溶液配置任务。自动为多个二胺样品创建溶液配置任务每个任务包含固体物料称量、溶剂添加、搅拌混合等步骤。返回的return_info中包含order_codes和order_ids列表。
properties: properties:
feedback: feedback:
properties: {}
required: []
title: BatchCreateDiamineSolutionTasks_Feedback title: BatchCreateDiamineSolutionTasks_Feedback
type: object
goal: goal:
properties: properties:
delay_time: delay_time:
default: '600'
description: 溶液配置完成后的延迟时间用于充分混合和溶解默认600秒 description: 溶液配置完成后的延迟时间用于充分混合和溶解默认600秒
type: string type: string
liquid_material_name: liquid_material_name:
@@ -275,11 +262,9 @@ bioyond_dispensing_station:
4.5, "solvent_volume": 18}]' 4.5, "solvent_volume": 18}]'
type: string type: string
speed: speed:
default: '400'
description: 搅拌速度rpm用于混合溶液默认400转/分钟 description: 搅拌速度rpm用于混合溶液默认400转/分钟
type: string type: string
temperature: temperature:
default: '20'
description: 配置温度溶液配置过程的目标温度默认20℃室温 description: 配置温度溶液配置过程的目标温度默认20℃室温
type: string type: string
required: required:
@@ -287,17 +272,11 @@ bioyond_dispensing_station:
title: BatchCreateDiamineSolutionTasks_Goal title: BatchCreateDiamineSolutionTasks_Goal
type: object type: object
result: result:
properties:
return_info:
description: 批量任务创建结果汇总JSON字符串包含total(总数)、success(成功数)、failed(失败数)、order_codes(任务编码数组)、order_ids(任务ID数组)、details(每个任务的详细信息)
type: string
required:
- return_info
title: BatchCreateDiamineSolutionTasks_Result title: BatchCreateDiamineSolutionTasks_Result
type: object type: string
required: required:
- goal - goal
title: BatchCreateDiamineSolutionTasks title: batch_create_diamine_solution_tasks参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
compute_experiment_design: compute_experiment_design:
@@ -309,7 +288,7 @@ bioyond_dispensing_station:
wt_percent: wt_percent wt_percent: wt_percent
goal_default: goal_default:
m_tot: '70' m_tot: '70'
ratio: '' ratio: null
titration_percent: '0.03' titration_percent: '0.03'
wt_percent: '0.25' wt_percent: '0.25'
handles: handles:
@@ -338,12 +317,8 @@ bioyond_dispensing_station:
handler_key: feeding_order handler_key: feeding_order
io_type: sink io_type: sink
label: Feeding Order Data From Calculation Node label: Feeding Order Data From Calculation Node
result: placeholder_keys: {}
feeding_order: feeding_order result: {}
return_info: return_info
solutions: solutions
solvents: solvents
titration: titration
schema: schema:
description: 计算实验设计输出solutions/titration/solvents/feeding_order用于后续节点。 description: 计算实验设计输出solutions/titration/solvents/feeding_order用于后续节点。
properties: properties:
@@ -356,7 +331,7 @@ bioyond_dispensing_station:
type: string type: string
ratio: ratio:
description: 组分摩尔比的对象,保持输入顺序,如{"MDA":1,"BTDA":1} description: 组分摩尔比的对象,保持输入顺序,如{"MDA":1,"BTDA":1}
type: string type: object
titration_percent: titration_percent:
default: '0.03' default: '0.03'
description: 滴定比例(10%部分) description: 滴定比例(10%部分)
@@ -371,14 +346,23 @@ bioyond_dispensing_station:
result: result:
properties: properties:
feeding_order: feeding_order:
items: {}
title: Feeding Order
type: array type: array
return_info: return_info:
title: Return Info
type: string type: string
solutions: solutions:
items: {}
title: Solutions
type: array type: array
solvents: solvents:
additionalProperties: true
title: Solvents
type: object type: object
titration: titration:
additionalProperties: true
title: Titration
type: object type: object
required: required:
- solutions - solutions
@@ -386,11 +370,11 @@ bioyond_dispensing_station:
- solvents - solvents
- feeding_order - feeding_order
- return_info - return_info
title: ComputeExperimentDesign_Result title: ComputeExperimentDesignReturn
type: object type: object
required: required:
- goal - goal
title: ComputeExperimentDesign title: compute_experiment_design参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
create_90_10_vial_feeding_task: create_90_10_vial_feeding_task:
@@ -444,17 +428,18 @@ bioyond_dispensing_station:
speed: '' speed: ''
temperature: '' temperature: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: DispenStationVialFeed_Feedback title: DispenStationVialFeed_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
delay_time: delay_time:
type: string type: string
@@ -502,38 +487,13 @@ bioyond_dispensing_station:
type: string type: string
temperature: temperature:
type: string type: string
required:
- order_name
- percent_90_1_assign_material_name
- percent_90_1_target_weigh
- percent_90_2_assign_material_name
- percent_90_2_target_weigh
- percent_90_3_assign_material_name
- percent_90_3_target_weigh
- percent_10_1_assign_material_name
- percent_10_1_target_weigh
- percent_10_1_volume
- percent_10_1_liquid_material_name
- percent_10_2_assign_material_name
- percent_10_2_target_weigh
- percent_10_2_volume
- percent_10_2_liquid_material_name
- percent_10_3_assign_material_name
- percent_10_3_target_weigh
- percent_10_3_volume
- percent_10_3_liquid_material_name
- speed
- temperature
- delay_time
- hold_m_name
title: DispenStationVialFeed_Goal title: DispenStationVialFeed_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: DispenStationVialFeed_Result title: DispenStationVialFeed_Result
type: object type: object
required: required:
@@ -564,17 +524,18 @@ bioyond_dispensing_station:
temperature: '' temperature: ''
volume: '' volume: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: DispenStationSolnPrep_Feedback title: DispenStationSolnPrep_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
delay_time: delay_time:
type: string type: string
@@ -594,24 +555,13 @@ bioyond_dispensing_station:
type: string type: string
volume: volume:
type: string type: string
required:
- order_name
- material_name
- target_weigh
- volume
- liquid_material_name
- speed
- temperature
- delay_time
- hold_m_name
title: DispenStationSolnPrep_Goal title: DispenStationSolnPrep_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: DispenStationSolnPrep_Result title: DispenStationSolnPrep_Result
type: object type: object
required: required:
@@ -624,8 +574,8 @@ bioyond_dispensing_station:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
schema: schema:
description: 启动调度器 - 启动Bioyond配液站的任务调度器开始执行队列中的任务 description: 启动调度器 - 启动Bioyond配液站的任务调度器开始执行队列中的任务
properties: properties:
@@ -635,12 +585,6 @@ bioyond_dispensing_station:
required: [] required: []
type: object type: object
result: result:
properties:
return_info:
description: 调度器启动结果成功返回1失败返回0
type: integer
required:
- return_info
title: scheduler_start结果 title: scheduler_start结果
type: object type: object
required: required:
@@ -654,8 +598,8 @@ bioyond_dispensing_station:
target_device_id: target_device_id target_device_id: target_device_id
transfer_groups: transfer_groups transfer_groups: transfer_groups
goal_default: goal_default:
target_device_id: '' target_device_id: null
transfer_groups: '' transfer_groups: null
handles: {} handles: {}
placeholder_keys: placeholder_keys:
target_device_id: unilabos_devices target_device_id: unilabos_devices
@@ -671,32 +615,13 @@ bioyond_dispensing_station:
type: string type: string
transfer_groups: transfer_groups:
description: 转移任务组列表,每组包含物料名称、目标堆栈和目标库位,可以添加多组 description: 转移任务组列表,每组包含物料名称、目标堆栈和目标库位,可以添加多组
items:
properties:
materials:
description: 物料名称手动输入系统将通过RPC查询验证
type: string
target_sites:
description: 目标库位(手动输入,如"A01"
type: string
target_stack:
description: 目标堆栈名称(从列表选择)
enum:
- 堆栈1左
- 堆栈1右
- 站内试剂存放堆栈
type: string
required:
- materials
- target_stack
- target_sites
type: object
type: array type: array
required: required:
- target_device_id - target_device_id
- transfer_groups - transfer_groups
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: transfer_materials_to_reaction_station参数 title: transfer_materials_to_reaction_station参数
@@ -709,9 +634,9 @@ bioyond_dispensing_station:
check_interval: check_interval check_interval: check_interval
timeout: timeout timeout: timeout
goal_default: goal_default:
batch_create_result: '' batch_create_result: null
check_interval: '10' check_interval: 10
timeout: '7200' timeout: 7200
handles: handles:
input: input:
- data_key: batch_create_result - data_key: batch_create_result
@@ -727,47 +652,35 @@ bioyond_dispensing_station:
handler_key: batch_reports_result handler_key: batch_reports_result
io_type: sink io_type: sink
label: Batch Order Completion Reports label: Batch Order Completion Reports
result: placeholder_keys: {}
return_info: return_info result: {}
schema: schema:
description: 同时等待多个任务完成并获取所有实验报告。从上游batch_create任务接收包含order_codes和order_ids的结果对象并行监控所有任务状态并返回每个任务的报告。 description: 同时等待多个任务完成并获取所有实验报告。从上游batch_create任务接收包含order_codes和order_ids的结果对象并行监控所有任务状态并返回每个任务的报告。
properties: properties:
feedback: feedback:
properties: {}
required: []
title: WaitForMultipleOrdersAndGetReports_Feedback title: WaitForMultipleOrdersAndGetReports_Feedback
type: object
goal: goal:
properties: properties:
batch_create_result: batch_create_result:
description: 批量创建任务的返回结果对象包含order_codes和order_ids数组。从上游batch_create节点通过handle传递 description: 批量创建任务的返回结果对象包含order_codes和order_ids数组。从上游batch_create节点通过handle传递
type: string type: string
check_interval: check_interval:
default: '10' default: 10
description: 检查任务状态的时间间隔默认每10秒检查一次所有待完成任务 description: 检查任务状态的时间间隔默认每10秒检查一次所有待完成任务
type: string type: integer
timeout: timeout:
default: '7200' default: 7200
description: 等待超时时间默认7200秒2小时。超过此时间未完成的任务将标记为timeout description: 等待超时时间默认7200秒2小时。超过此时间未完成的任务将标记为timeout
type: string type: integer
required: required: []
- batch_create_result
title: WaitForMultipleOrdersAndGetReports_Goal title: WaitForMultipleOrdersAndGetReports_Goal
type: object type: object
result: result:
properties:
return_info:
description: 'JSON格式的批量任务完成信息包含: total(总数), completed(成功数), timeout(超时数),
error(错误数), elapsed_time(总耗时), reports(报告数组每个元素包含order_code,
order_id, status, completion_status, report, elapsed_time)'
type: string
required:
- return_info
title: WaitForMultipleOrdersAndGetReports_Result title: WaitForMultipleOrdersAndGetReports_Result
type: object type: object
required: required:
- goal - goal
title: WaitForMultipleOrdersAndGetReports title: wait_for_multiple_orders_and_get_reports参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_studio.dispensing_station.dispensing_station:BioyondDispensingStation module: unilabos.devices.workstation.bioyond_studio.dispensing_station.dispensing_station:BioyondDispensingStation

View File

@@ -1,81 +0,0 @@
camera:
category:
- camera
class:
action_value_mappings:
auto-destroy_node:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 用于安全地关闭摄像头设备释放摄像头资源停止视频采集和发布服务。调用此函数将清理OpenCV摄像头连接并销毁ROS2节点。
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: destroy_node参数
type: object
type: UniLabJsonCommand
auto-timer_callback:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 定时器回调函数的参数schema。此函数负责定期采集摄像头视频帧将OpenCV格式的图像转换为ROS Image消息格式并发布到指定的视频话题。默认以10Hz频率执行确保视频流的连续性和实时性。
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: timer_callback参数
type: object
type: UniLabJsonCommand
module: unilabos.ros.nodes.presets.camera:VideoPublisher
status_types: {}
type: ros2
config_info: []
description: VideoPublisher摄像头设备节点用于实时视频采集和流媒体发布。该设备通过OpenCV连接本地摄像头如USB摄像头、内置摄像头等定时采集视频帧并将其转换为ROS2的sensor_msgs/Image消息格式发布到视频话题。主要用于实验室自动化系统中的视觉监控、图像分析、实时观察等应用场景。支持可配置的摄像头索引、发布频率等参数。
handles: []
icon: ''
init_param_schema:
config:
properties:
camera_index:
default: 0
type: string
device_id:
default: video_publisher
type: string
device_uuid:
default: ''
type: string
period:
default: 0.1
type: number
registry_name:
default: ''
type: string
resource_tracker:
type: object
required: []
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0

View File

@@ -18,7 +18,7 @@ cameracontroller_device:
goal: goal:
properties: properties:
config: config:
type: string type: object
required: [] required: []
type: object type: object
result: {} result: {}
@@ -42,7 +42,8 @@ cameracontroller_device:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: stop参数 title: stop参数
@@ -50,7 +51,7 @@ cameracontroller_device:
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.cameraSII.cameraUSB:CameraController module: unilabos.devices.cameraSII.cameraUSB:CameraController
status_types: status_types:
status: dict status: Dict[str, Any]
type: python type: python
config_info: [] config_info: []
description: Uni-Lab-OS 摄像头驱动Linux USB 摄像头版,无 PTZ description: Uni-Lab-OS 摄像头驱动Linux USB 摄像头版,无 PTZ
@@ -103,5 +104,4 @@ cameracontroller_device:
required: required:
- status - status
type: object type: object
registry_type: device
version: 1.0.0 version: 1.0.0

View File

@@ -141,30 +141,26 @@ hplc.agilent:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -175,7 +171,6 @@ hplc.agilent:
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
status_types: status_types:
could_run: bool could_run: bool
data_file: String
device_status: str device_status: str
driver_init_ok: bool driver_init_ok: bool
finish_status: str finish_status: str
@@ -199,10 +194,6 @@ hplc.agilent:
properties: properties:
could_run: could_run:
type: boolean type: boolean
data_file:
items:
type: string
type: array
device_status: device_status:
type: string type: string
driver_init_ok: driver_init_ok:
@@ -216,14 +207,13 @@ hplc.agilent:
success: success:
type: boolean type: boolean
required: required:
- status_text
- device_status
- could_run - could_run
- device_status
- driver_init_ok - driver_init_ok
- is_running
- success
- finish_status - finish_status
- data_file - is_running
- status_text
- success
type: object type: object
version: 1.0.0 version: 1.0.0
hplc.agilent-zhida: hplc.agilent-zhida:
@@ -236,26 +226,25 @@ hplc.agilent-zhida:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -315,21 +304,18 @@ hplc.agilent-zhida:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -341,35 +327,35 @@ hplc.agilent-zhida:
feedback: {} feedback: {}
goal: goal:
string: string string: string
text: text
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:
@@ -407,7 +393,7 @@ hplc.agilent-zhida:
status: status:
type: object type: object
required: required:
- status
- methods - methods
- status
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -120,42 +120,41 @@ raman.home_made:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
raman_cmd: raman_cmd:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:

View File

@@ -19,7 +19,8 @@ separator.chinwe:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: connect参数 title: connect参数
@@ -65,135 +66,145 @@ separator.chinwe:
required: required:
- command_dict - command_dict
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: execute_command_from_outer参数 title: execute_command_from_outer参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
motor_rotate_quarter: motor_rotate_quarter:
feedback: {}
goal: goal:
direction: 顺时针 direction: 顺时针
motor_id: 4 motor_id: 4
speed: 60 speed: 60
goal_default:
direction: 顺时针
motor_id: null
speed: 60
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 电机旋转 1/4 圈 description: 电机旋转 1/4 圈
properties: properties:
feedback: {}
goal: goal:
properties: properties:
direction: direction:
default: 顺时针 default: 顺时针
description: 旋转方向 description: 旋转方向
enum:
- 顺时针
- 逆时针
type: string type: string
motor_id: motor_id:
default: '4'
description: 选择电机 (4:搅拌, 5:旋钮) description: 选择电机 (4:搅拌, 5:旋钮)
enum: type: integer
- '4'
- '5'
type: string
speed: speed:
default: 60 default: 60
description: 速度 (RPM) description: 速度 (RPM)
type: integer type: integer
required: required:
- motor_id - motor_id
- speed
type: object type: object
result: {}
required:
- goal
title: motor_rotate_quarter参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
motor_run_continuous: motor_run_continuous:
feedback: {}
goal: goal:
direction: 顺时针 direction: 顺时针
motor_id: 4 motor_id: 4
speed: 60 speed: 60
goal_default:
direction: 顺时针
motor_id: null
speed: null
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 电机一直旋转 (速度模式) description: 电机一直旋转 (速度模式)
properties: properties:
feedback: {}
goal: goal:
properties: properties:
direction: direction:
default: 顺时针 default: 顺时针
description: 旋转方向 description: 旋转方向
enum:
- 顺时针
- 逆时针
type: string type: string
motor_id: motor_id:
default: '4'
description: 选择电机 (4:搅拌, 5:旋钮) description: 选择电机 (4:搅拌, 5:旋钮)
enum: type: integer
- '4'
- '5'
type: string
speed: speed:
default: 60
description: 速度 (RPM) description: 速度 (RPM)
type: integer type: integer
required: required:
- motor_id - motor_id
- speed - speed
type: object type: object
result: {}
required:
- goal
title: motor_run_continuous参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
motor_stop: motor_stop:
feedback: {}
goal: goal:
motor_id: 4 motor_id: 4
goal_default:
motor_id: null
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 停止指定步进电机 description: 停止指定步进电机
properties: properties:
feedback: {}
goal: goal:
properties: properties:
motor_id: motor_id:
default: '4'
description: 选择电机 description: 选择电机
enum:
- '4'
- '5'
title: '注: 4=搅拌, 5=旋钮' title: '注: 4=搅拌, 5=旋钮'
type: string type: integer
required: required:
- motor_id - motor_id
type: object type: object
result: {}
required:
- goal
title: motor_stop参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
pump_aspirate: pump_aspirate:
feedback: {}
goal: goal:
pump_id: 1 pump_id: 1
valve_port: 1 valve_port: 1
volume: 1000 volume: 1000
goal_default:
pump_id: null
valve_port: null
volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 注射泵吸液 description: 注射泵吸液
properties: properties:
feedback: {}
goal: goal:
properties: properties:
pump_id: pump_id:
default: '1'
description: 选择泵 description: 选择泵
enum: type: integer
- '1'
- '2'
- '3'
type: string
valve_port: valve_port:
default: '1'
description: 阀门端口 description: 阀门端口
enum: type: integer
- '1'
- '2'
- '3'
- '4'
- '5'
- '6'
- '7'
- '8'
type: string
volume: volume:
default: 1000
description: 吸液步数 description: 吸液步数
type: integer type: integer
required: required:
@@ -201,41 +212,38 @@ separator.chinwe:
- volume - volume
- valve_port - valve_port
type: object type: object
result: {}
required:
- goal
title: pump_aspirate参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
pump_dispense: pump_dispense:
feedback: {}
goal: goal:
pump_id: 1 pump_id: 1
valve_port: 1 valve_port: 1
volume: 1000 volume: 1000
goal_default:
pump_id: null
valve_port: null
volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 注射泵排液 description: 注射泵排液
properties: properties:
feedback: {}
goal: goal:
properties: properties:
pump_id: pump_id:
default: '1'
description: 选择泵 description: 选择泵
enum: type: integer
- '1'
- '2'
- '3'
type: string
valve_port: valve_port:
default: '1'
description: 阀门端口 description: 阀门端口
enum: type: integer
- '1'
- '2'
- '3'
- '4'
- '5'
- '6'
- '7'
- '8'
type: string
volume: volume:
default: 1000
description: 排液步数 description: 排液步数
type: integer type: integer
required: required:
@@ -243,121 +251,152 @@ separator.chinwe:
- volume - volume
- valve_port - valve_port
type: object type: object
result: {}
required:
- goal
title: pump_dispense参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
pump_initialize: pump_initialize:
feedback: {}
goal: goal:
drain_port: 0 drain_port: 0
output_port: 0 output_port: 0
pump_id: 1 pump_id: 1
speed: 10 speed: 10
goal_default:
drain_port: 0
output_port: 0
pump_id: null
speed: 10
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 初始化指定注射泵 description: 初始化指定注射泵
properties: properties:
feedback: {}
goal: goal:
properties: properties:
drain_port: drain_port:
default: 0 default: 0
description: 排液口索引 description: 排液口索引
type: integer type: string
output_port: output_port:
default: 0 default: 0
description: 输出口索引 description: 输出口索引
type: integer
pump_id:
default: '1'
description: 选择泵
enum:
- '1'
- '2'
- '3'
title: '注: 1号泵, 2号泵, 3号泵'
type: string type: string
pump_id:
description: 选择泵
title: '注: 1号泵, 2号泵, 3号泵'
type: integer
speed: speed:
default: 10 default: 10
description: 运动速度 description: 运动速度
type: integer type: string
required: required:
- pump_id - pump_id
type: object type: object
result: {}
required:
- goal
title: pump_initialize参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
pump_valve: pump_valve:
feedback: {}
goal: goal:
port: 1 port: 1
pump_id: 1 pump_id: 1
goal_default:
port: null
pump_id: null
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 切换指定泵的阀门端口 description: 切换指定泵的阀门端口
properties: properties:
feedback: {}
goal: goal:
properties: properties:
port: port:
default: '1'
description: 阀门端口号 (1-8) description: 阀门端口号 (1-8)
enum: type: integer
- '1'
- '2'
- '3'
- '4'
- '5'
- '6'
- '7'
- '8'
type: string
pump_id: pump_id:
default: '1'
description: 选择泵 description: 选择泵
enum: type: integer
- '1'
- '2'
- '3'
type: string
required: required:
- pump_id - pump_id
- port - port
type: object type: object
result: {}
required:
- goal
title: pump_valve参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
wait_sensor_level: wait_sensor_level:
feedback: {}
goal: goal:
target_state: 有液 target_state: 有液
timeout: 30 timeout: 30
goal_default:
target_state: 有液
timeout: 30
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 等待传感器液位条件 description: 等待传感器液位条件
properties: properties:
feedback: {}
goal: goal:
properties: properties:
target_state: target_state:
default: 有液 default: 有液
description: 目标液位状态 description: 目标液位状态
enum:
- 有液
- 无液
type: string type: string
timeout: timeout:
default: 30 default: 30
description: 超时时间 (秒) description: 超时时间 (秒)
type: integer type: integer
required: required: []
- target_state
type: object type: object
result:
type: boolean
required:
- goal
title: wait_sensor_level参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
wait_time: wait_time:
feedback: {}
goal: goal:
duration: 10 duration: 10
goal_default:
duration: null
handles: {} handles: {}
placeholder_keys: {}
result: {}
schema: schema:
description: 等待指定时间 description: 等待指定时间
properties: properties:
feedback: {}
goal: goal:
properties: properties:
duration: duration:
default: 10
description: 等待时间 (秒) description: 等待时间 (秒)
type: integer type: integer
required: required:
- duration - duration
type: object type: object
result:
type: boolean
required:
- goal
title: wait_time参数
type: object
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.separator.chinwe:ChinweDevice module: unilabos.devices.separator.chinwe:ChinweDevice
status_types: status_types:
@@ -406,8 +445,8 @@ separator.chinwe:
sensor_rssi: sensor_rssi:
type: integer type: integer
required: required:
- is_connected
- sensor_level - sensor_level
- sensor_rssi - sensor_rssi
- is_connected
type: object type: object
version: 2.1.0 version: 2.1.0

View File

@@ -64,7 +64,8 @@ coincellassemblyworkstation_device:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: fun_wuliao_test参数 title: fun_wuliao_test参数
@@ -109,7 +110,8 @@ coincellassemblyworkstation_device:
- elec_num - elec_num
- elec_use_num - elec_use_num
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: func_allpack_cmd参数 title: func_allpack_cmd参数
@@ -220,7 +222,8 @@ coincellassemblyworkstation_device:
- elec_num - elec_num
- elec_use_num - elec_use_num
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: func_allpack_cmd_simp参数 title: func_allpack_cmd_simp参数
@@ -309,7 +312,8 @@ coincellassemblyworkstation_device:
type: boolean type: boolean
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: func_pack_device_init_auto_start_combined参数 title: func_pack_device_init_auto_start_combined参数
@@ -351,7 +355,8 @@ coincellassemblyworkstation_device:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: func_pack_device_stop参数 title: func_pack_device_stop参数
@@ -376,7 +381,8 @@ coincellassemblyworkstation_device:
type: string type: string
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: func_pack_get_msg_cmd参数 title: func_pack_get_msg_cmd参数
@@ -430,7 +436,8 @@ coincellassemblyworkstation_device:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: func_pack_send_finished_cmd参数 title: func_pack_send_finished_cmd参数
@@ -467,7 +474,8 @@ coincellassemblyworkstation_device:
- assembly_type - assembly_type
- assembly_pressure - assembly_pressure
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: func_pack_send_msg_cmd参数 title: func_pack_send_msg_cmd参数
@@ -611,7 +619,8 @@ coincellassemblyworkstation_device:
- elec_num - elec_num
- elec_use_num - elec_use_num
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: func_sendbottle_allpack_multi参数 title: func_sendbottle_allpack_multi参数
@@ -663,31 +672,6 @@ coincellassemblyworkstation_device:
title: modify_deck_name参数 title: modify_deck_name参数
type: object type: object
type: UniLabJsonCommand 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-qiming_coin_cell_code: auto-qiming_coin_cell_code:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -735,7 +719,8 @@ coincellassemblyworkstation_device:
required: required:
- fujipian_panshu - fujipian_panshu
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: qiming_coin_cell_code参数 title: qiming_coin_cell_code参数
@@ -826,25 +811,24 @@ coincellassemblyworkstation_device:
sys_status: sys_status:
type: string type: string
required: required:
- sys_status
- sys_mode
- request_rec_msg_status
- request_send_msg_status
- data_assembly_coin_cell_num - data_assembly_coin_cell_num
- data_assembly_pressure
- data_assembly_time - data_assembly_time
- data_open_circuit_voltage
- data_axis_x_pos - data_axis_x_pos
- data_axis_y_pos - data_axis_y_pos
- data_axis_z_pos - data_axis_z_pos
- data_pole_weight
- data_assembly_pressure
- data_electrolyte_volume
- data_coin_num
- data_coin_cell_code - data_coin_cell_code
- data_coin_num
- data_electrolyte_code - data_electrolyte_code
- data_glove_box_pressure - data_electrolyte_volume
- data_glove_box_o2_content - data_glove_box_o2_content
- data_glove_box_pressure
- data_glove_box_water_content - data_glove_box_water_content
- data_open_circuit_voltage
- data_pole_weight
- request_rec_msg_status
- request_send_msg_status
- sys_mode
- sys_status
type: object type: object
registry_type: device
version: 1.0.0 version: 1.0.0

View File

@@ -50,26 +50,25 @@ gas_source.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -82,26 +81,25 @@ gas_source.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -116,32 +114,31 @@ gas_source.mock:
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:
@@ -232,26 +229,25 @@ vacuum_pump.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -264,26 +260,25 @@ vacuum_pump.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -298,32 +293,31 @@ vacuum_pump.mock:
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:

View File

@@ -5,7 +5,7 @@ hotel.thermo_orbitor_rs2_hotel:
action_value_mappings: {} action_value_mappings: {}
module: unilabos.devices.resource_container.container:HotelContainer module: unilabos.devices.resource_container.container:HotelContainer
status_types: status_types:
rotation: String rotation: ''
type: python type: python
config_info: [] config_info: []
description: Thermo Orbitor RS2 Hotel容器设备用于实验室样品的存储和管理。该设备通过HotelContainer类实现容器的旋转控制和状态监控主要用于存储实验样品、试剂瓶或其他实验器具支持旋转功能以便于样品的自动化存取。适用于需要有序存储和快速访问大量样品的实验室自动化场景。 description: Thermo Orbitor RS2 Hotel容器设备用于实验室样品的存储和管理。该设备通过HotelContainer类实现容器的旋转控制和状态监控主要用于存储实验样品、试剂瓶或其他实验器具支持旋转功能以便于样品的自动化存取。适用于需要有序存储和快速访问大量样品的实验室自动化场景。

View File

@@ -22,7 +22,8 @@ xyz_stepper_controller:
required: required:
- degrees - degrees
type: object type: object
result: {} result:
type: integer
required: required:
- goal - goal
title: degrees_to_steps参数 title: degrees_to_steps参数
@@ -47,7 +48,8 @@ xyz_stepper_controller:
required: required:
- axis - axis
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: emergency_stop参数 title: emergency_stop参数
@@ -72,7 +74,10 @@ xyz_stepper_controller:
type: boolean type: boolean
required: [] required: []
type: object type: object
result: {} result:
additionalProperties:
type: boolean
type: object
required: required:
- goal - goal
title: enable_all_axes参数 title: enable_all_axes参数
@@ -101,7 +106,8 @@ xyz_stepper_controller:
required: required:
- axis - axis
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: enable_motor参数 title: enable_motor参数
@@ -122,7 +128,10 @@ xyz_stepper_controller:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
additionalProperties:
type: boolean
type: object
required: required:
- goal - goal
title: home_all_axes参数 title: home_all_axes参数
@@ -147,7 +156,8 @@ xyz_stepper_controller:
required: required:
- axis - axis
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: home_axis参数 title: home_axis参数
@@ -188,7 +198,8 @@ xyz_stepper_controller:
- axis - axis
- position - position
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: move_to_position参数 title: move_to_position参数
@@ -229,7 +240,8 @@ xyz_stepper_controller:
- axis - axis
- degrees - degrees
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: move_to_position_degrees参数 title: move_to_position_degrees参数
@@ -270,7 +282,8 @@ xyz_stepper_controller:
- axis - axis
- revolutions - revolutions
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: move_to_position_revolutions参数 title: move_to_position_revolutions参数
@@ -301,14 +314,17 @@ xyz_stepper_controller:
default: 5000 default: 5000
type: integer type: integer
x: x:
type: string type: integer
y: y:
type: string type: integer
z: z:
type: string type: integer
required: [] required: []
type: object type: object
result: {} result:
additionalProperties:
type: boolean
type: object
required: required:
- goal - goal
title: move_xyz参数 title: move_xyz参数
@@ -339,14 +355,17 @@ xyz_stepper_controller:
default: 5000 default: 5000
type: integer type: integer
x_deg: x_deg:
type: string type: number
y_deg: y_deg:
type: string type: number
z_deg: z_deg:
type: string type: number
required: [] required: []
type: object type: object
result: {} result:
additionalProperties:
type: boolean
type: object
required: required:
- goal - goal
title: move_xyz_degrees参数 title: move_xyz_degrees参数
@@ -377,14 +396,17 @@ xyz_stepper_controller:
default: 5000 default: 5000
type: integer type: integer
x_rev: x_rev:
type: string type: number
y_rev: y_rev:
type: string type: number
z_rev: z_rev:
type: string type: number
required: [] required: []
type: object type: object
result: {} result:
additionalProperties:
type: boolean
type: object
required: required:
- goal - goal
title: move_xyz_revolutions参数 title: move_xyz_revolutions参数
@@ -409,7 +431,8 @@ xyz_stepper_controller:
required: required:
- revolutions - revolutions
type: object type: object
result: {} result:
type: integer
required: required:
- goal - goal
title: revolutions_to_steps参数 title: revolutions_to_steps参数
@@ -442,7 +465,8 @@ xyz_stepper_controller:
- axis - axis
- speed - speed
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: set_speed_mode参数 title: set_speed_mode参数
@@ -467,7 +491,8 @@ xyz_stepper_controller:
required: required:
- steps - steps
type: object type: object
result: {} result:
type: number
required: required:
- goal - goal
title: steps_to_degrees参数 title: steps_to_degrees参数
@@ -492,7 +517,8 @@ xyz_stepper_controller:
required: required:
- steps - steps
type: object type: object
result: {} result:
type: number
required: required:
- goal - goal
title: steps_to_revolutions参数 title: steps_to_revolutions参数
@@ -513,7 +539,10 @@ xyz_stepper_controller:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
additionalProperties:
type: boolean
type: object
required: required:
- goal - goal
title: stop_all_axes参数 title: stop_all_axes参数
@@ -542,7 +571,8 @@ xyz_stepper_controller:
required: required:
- axis - axis
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: wait_for_completion参数 title: wait_for_completion参数
@@ -550,8 +580,7 @@ xyz_stepper_controller:
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:XYZStepperController module: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:XYZStepperController
status_types: status_types:
all_positions: dict all_positions: Dict[MotorAxis, MotorPosition]
motor_status: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:MotorPosition
type: python type: python
config_info: [] config_info: []
description: 新XYZ控制器 description: 新XYZ控制器
@@ -574,12 +603,10 @@ xyz_stepper_controller:
data: data:
properties: properties:
all_positions: all_positions:
type: object additionalProperties:
motor_status: type: object
type: object type: object
required: required:
- motor_status
- all_positions - all_positions
type: object type: object
registry_type: device
version: 1.0.0 version: 1.0.0

File diff suppressed because it is too large Load Diff

View File

@@ -5,31 +5,6 @@ neware_battery_test_system:
- battery_test - battery_test
class: class:
action_value_mappings: action_value_mappings:
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: string
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-print_status_summary: auto-print_status_summary:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -66,7 +41,8 @@ neware_battery_test_system:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: boolean
required: required:
- goal - goal
title: test_connection参数 title: test_connection参数
@@ -77,9 +53,8 @@ neware_battery_test_system:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
success: success
schema: schema:
description: 调试方法:显示所有资源的实际名称 description: 调试方法:显示所有资源的实际名称
properties: properties:
@@ -89,19 +64,10 @@ neware_battery_test_system:
required: [] required: []
type: object type: object
result: result:
properties:
return_info:
description: 资源调试信息
type: string
success:
description: 是否成功
type: boolean
required:
- return_info
- success
type: object type: object
required: required:
- goal - goal
title: debug_resource_names参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
export_status_json: export_status_json:
@@ -111,9 +77,8 @@ neware_battery_test_system:
goal_default: goal_default:
filepath: bts_status.json filepath: bts_status.json
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
success: success
schema: schema:
description: 导出当前状态数据到JSON文件 description: 导出当前状态数据到JSON文件
properties: properties:
@@ -127,19 +92,10 @@ neware_battery_test_system:
required: [] required: []
type: object type: object
result: result:
properties:
return_info:
description: 导出操作结果信息
type: string
success:
description: 导出是否成功
type: boolean
required:
- return_info
- success
type: object type: object
required: required:
- goal - goal
title: export_status_json参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
get_device_summary: get_device_summary:
@@ -181,10 +137,8 @@ neware_battery_test_system:
goal_default: goal_default:
plate_num: null plate_num: null
handles: {} handles: {}
result: placeholder_keys: {}
plate_data: plate_data result: {}
return_info: return_info
success: success
schema: schema:
description: 获取指定盘或所有盘的状态信息 description: 获取指定盘或所有盘的状态信息
properties: properties:
@@ -193,29 +147,14 @@ neware_battery_test_system:
properties: properties:
plate_num: plate_num:
description: 盘号 (1 或 2)如果为null则返回所有盘的状态 description: 盘号 (1 或 2)如果为null则返回所有盘的状态
maximum: 2
minimum: 1
type: integer type: integer
required: [] required: []
type: object type: object
result: result:
properties:
plate_data:
description: 盘状态数据(单盘或所有盘)
type: object
return_info:
description: 操作结果信息
type: string
success:
description: 查询是否成功
type: boolean
required:
- return_info
- success
- plate_data
type: object type: object
required: required:
- goal - goal
title: get_plate_status参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
print_status_summary_action: print_status_summary_action:
@@ -223,9 +162,8 @@ neware_battery_test_system:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
success: success
schema: schema:
description: 打印通道状态摘要信息到控制台 description: 打印通道状态摘要信息到控制台
properties: properties:
@@ -235,28 +173,21 @@ neware_battery_test_system:
required: [] required: []
type: object type: object
result: result:
properties:
return_info:
description: 打印操作结果信息
type: string
success:
description: 打印是否成功
type: boolean
required:
- return_info
- success
type: object type: object
required: required:
- goal - goal
title: print_status_summary_action参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
query_plate_action: query_plate_action:
feedback: {} feedback: {}
goal: goal:
string: plate_id plate_id: plate_id
string: string
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info return_info: return_info
success: success success: success
@@ -264,27 +195,23 @@ neware_battery_test_system:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:
@@ -298,13 +225,11 @@ neware_battery_test_system:
csv_path: string csv_path: string
output_dir: string output_dir: string
goal_default: goal_default:
csv_path: '' csv_path: null
output_dir: . output_dir: .
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
submitted_count: submitted_count
success: success
schema: schema:
description: 从CSV文件批量提交Neware测试任务 description: 从CSV文件批量提交Neware测试任务
properties: properties:
@@ -315,31 +240,17 @@ neware_battery_test_system:
description: 输入CSV文件的绝对路径 description: 输入CSV文件的绝对路径
type: string type: string
output_dir: output_dir:
default: .
description: 输出目录用于存储XML和备份文件默认当前目录 description: 输出目录用于存储XML和备份文件默认当前目录
type: string type: string
required: required:
- csv_path - csv_path
type: object type: object
result: result:
properties:
return_info:
description: 执行结果详细信息
type: string
submitted_count:
description: 成功提交的任务数量
type: integer
success:
description: 是否成功
type: boolean
total_count:
description: CSV文件中的总行数
type: integer
required:
- return_info
- success
type: object type: object
required: required:
- goal - goal
title: submit_from_csv参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
test_connection_action: test_connection_action:
@@ -347,9 +258,8 @@ neware_battery_test_system:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
success: success
schema: schema:
description: 测试与电池测试系统的TCP连接 description: 测试与电池测试系统的TCP连接
properties: properties:
@@ -359,19 +269,10 @@ neware_battery_test_system:
required: [] required: []
type: object type: object
result: result:
properties:
return_info:
description: 连接测试结果信息
type: string
success:
description: 连接测试是否成功
type: boolean
required:
- return_info
- success
type: object type: object
required: required:
- goal - goal
title: test_connection_action参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
upload_backup_to_oss: upload_backup_to_oss:
@@ -392,12 +293,8 @@ neware_battery_test_system:
handler_key: uploaded_files handler_key: uploaded_files
io_type: sink io_type: sink
label: Uploaded Files (with standard flow info) label: Uploaded Files (with standard flow info)
result: placeholder_keys: {}
failed_files: failed_files result: {}
return_info: return_info
success: success
total_count: total_count
uploaded_count: uploaded_count
schema: schema:
description: 上传备份文件到阿里云OSS description: 上传备份文件到阿里云OSS
properties: properties:
@@ -417,65 +314,17 @@ neware_battery_test_system:
required: [] required: []
type: object type: object
result: result:
properties:
failed_files:
description: 上传失败的文件名列表
items:
type: string
type: array
return_info:
description: 上传操作结果信息
type: string
success:
description: 上传是否成功
type: boolean
total_count:
description: 总文件数
type: integer
uploaded_count:
description: 成功上传的文件数
type: integer
uploaded_files:
description: 成功上传的文件详情列表
items:
properties:
Battery_Code:
description: 电池编码
type: string
Electrolyte_Code:
description: 电解液编码
type: string
filename:
description: 文件名
type: string
url:
description: OSS下载链接
type: string
required:
- filename
- url
- Battery_Code
- Electrolyte_Code
type: object
type: array
required:
- return_info
- success
- uploaded_count
- total_count
- failed_files
- uploaded_files
type: object type: object
required: required:
- goal - goal
title: upload_backup_to_oss参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem
status_types: status_types:
channel_status: dict channel_status: Dict[int, Dict]
connection_info: dict connection_info: Dict[str, str]
device_summary: dict device_summary: dict
plate_status: dict
status: str status: str
total_channels: int total_channels: int
type: python type: python
@@ -517,23 +366,24 @@ neware_battery_test_system:
data: data:
properties: properties:
channel_status: channel_status:
additionalProperties:
type: object
type: object type: object
connection_info: connection_info:
additionalProperties:
type: string
type: object type: object
device_summary: device_summary:
type: object type: object
plate_status:
type: object
status: status:
type: string type: string
total_channels: total_channels:
type: integer type: integer
required: required:
- status
- channel_status - channel_status
- connection_info - connection_info
- total_channels
- plate_status
- device_summary - device_summary
- status
- total_channels
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -142,8 +142,7 @@ opcua_example:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.device_comms.opcua_client.client:OpcUaClient module: unilabos.device_comms.opcua_client.client:OpcUaClient
status_types: status_types: {}
node_value: String
type: python type: python
config_info: [] config_info: []
description: null description: null
@@ -167,10 +166,7 @@ opcua_example:
- url - url
type: object type: object
data: data:
properties: properties: {}
node_value: required: []
type: string
required:
- node_value
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -80,7 +80,8 @@ opsky_ATR30007:
type: string type: string
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: run_once参数 title: run_once参数

View File

@@ -100,42 +100,41 @@ rotavap.one:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
set_timer: set_timer:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -250,9 +249,13 @@ separator.homemade:
feedback: feedback:
status: status status: status
goal: goal:
event: event
settling_time: settling_time settling_time: settling_time
stir_speed: stir_speed stir_speed: stir_speed
stir_time: stir_time, stir_time: stir_time
time: time
time_spec: time_spec
vessel: vessel
goal_default: goal_default:
event: '' event: ''
settling_time: '' settling_time: ''
@@ -281,34 +284,42 @@ separator.homemade:
sample_id: '' sample_id: ''
type: '' type: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
message: message
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: Stir_Feedback title: Stir_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
event: event:
type: string type: string
settling_time: settling_time:
type: string type: string
stir_speed: stir_speed:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
stir_time: stir_time:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
time: time:
type: string type: string
time_spec: time_spec:
type: string type: string
vessel: vessel:
additionalProperties: false
properties: properties:
category: category:
type: string type: string
@@ -327,16 +338,26 @@ separator.homemade:
parent: parent:
type: string type: string
pose: pose:
additionalProperties: false
properties: properties:
orientation: orientation:
additionalProperties: false
properties: properties:
w: w:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- x - x
@@ -346,12 +367,19 @@ separator.homemade:
title: orientation title: orientation
type: object type: object
position: position:
additionalProperties: false
properties: properties:
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- x - x
@@ -381,17 +409,10 @@ separator.homemade:
- data - data
title: vessel title: vessel
type: object type: object
required:
- vessel
- time
- event
- time_spec
- stir_time
- stir_speed
- settling_time
title: Stir_Goal title: Stir_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
message: message:
type: string type: string
@@ -399,10 +420,6 @@ separator.homemade:
type: string type: string
success: success:
type: boolean type: boolean
required:
- success
- message
- return_info
title: Stir_Result title: Stir_Result
type: object type: object
required: required:
@@ -418,36 +435,34 @@ separator.homemade:
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:

View File

@@ -28,31 +28,6 @@ post_process_station:
title: load_config参数 title: load_config参数
type: object type: object
type: UniLabJsonCommand 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: string
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-print_cache_stats: auto-print_cache_stats:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -104,42 +79,41 @@ post_process_station:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
disconnect: disconnect:
feedback: {} feedback:
status: status
goal: goal:
command: {} command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -149,42 +123,41 @@ post_process_station:
type: SendCmd type: SendCmd
read_node: read_node:
feedback: feedback:
result: result status: status
goal: goal:
command: node_name command: command
node_name: node_name
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -283,17 +256,19 @@ post_process_station:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: PostProcessTriggerClean_Feedback title: PostProcessTriggerClean_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
acetone_inner_wall_cleaning_count: acetone_inner_wall_cleaning_count:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
acetone_inner_wall_cleaning_injection: acetone_inner_wall_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
acetone_inner_wall_cleaning_waste_time: acetone_inner_wall_cleaning_waste_time:
maximum: 2147483647 maximum: 2147483647
@@ -304,6 +279,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
acetone_outer_wall_cleaning_injection: acetone_outer_wall_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
acetone_outer_wall_cleaning_wait_time: acetone_outer_wall_cleaning_wait_time:
maximum: 2147483647 maximum: 2147483647
@@ -322,6 +299,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
acetone_stirrer_cleaning_injection: acetone_stirrer_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
acetone_stirrer_cleaning_wait_time: acetone_stirrer_cleaning_wait_time:
maximum: 2147483647 maximum: 2147483647
@@ -348,6 +327,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
nmp_inner_wall_cleaning_injection: nmp_inner_wall_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
nmp_inner_wall_cleaning_waste_time: nmp_inner_wall_cleaning_waste_time:
maximum: 2147483647 maximum: 2147483647
@@ -358,6 +339,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
nmp_outer_wall_cleaning_injection: nmp_outer_wall_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
nmp_outer_wall_cleaning_wait_time: nmp_outer_wall_cleaning_wait_time:
maximum: 2147483647 maximum: 2147483647
@@ -376,6 +359,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
nmp_stirrer_cleaning_injection: nmp_stirrer_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
nmp_stirrer_cleaning_wait_time: nmp_stirrer_cleaning_wait_time:
maximum: 2147483647 maximum: 2147483647
@@ -394,6 +379,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
water_inner_wall_cleaning_injection: water_inner_wall_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
water_inner_wall_cleaning_waste_time: water_inner_wall_cleaning_waste_time:
maximum: 2147483647 maximum: 2147483647
@@ -404,6 +391,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
water_outer_wall_cleaning_injection: water_outer_wall_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
water_outer_wall_cleaning_wait_time: water_outer_wall_cleaning_wait_time:
maximum: 2147483647 maximum: 2147483647
@@ -422,6 +411,8 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
water_stirrer_cleaning_injection: water_stirrer_cleaning_injection:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
water_stirrer_cleaning_wait_time: water_stirrer_cleaning_wait_time:
maximum: 2147483647 maximum: 2147483647
@@ -431,55 +422,13 @@ post_process_station:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
required:
- nmp_outer_wall_cleaning_injection
- nmp_outer_wall_cleaning_count
- nmp_outer_wall_cleaning_wait_time
- nmp_outer_wall_cleaning_waste_time
- nmp_inner_wall_cleaning_injection
- nmp_inner_wall_cleaning_count
- nmp_pump_cleaning_suction_count
- nmp_inner_wall_cleaning_waste_time
- nmp_stirrer_cleaning_injection
- nmp_stirrer_cleaning_count
- nmp_stirrer_cleaning_wait_time
- nmp_stirrer_cleaning_waste_time
- water_outer_wall_cleaning_injection
- water_outer_wall_cleaning_count
- water_outer_wall_cleaning_wait_time
- water_outer_wall_cleaning_waste_time
- water_inner_wall_cleaning_injection
- water_inner_wall_cleaning_count
- water_pump_cleaning_suction_count
- water_inner_wall_cleaning_waste_time
- water_stirrer_cleaning_injection
- water_stirrer_cleaning_count
- water_stirrer_cleaning_wait_time
- water_stirrer_cleaning_waste_time
- acetone_outer_wall_cleaning_injection
- acetone_outer_wall_cleaning_count
- acetone_outer_wall_cleaning_wait_time
- acetone_outer_wall_cleaning_waste_time
- acetone_inner_wall_cleaning_injection
- acetone_inner_wall_cleaning_count
- acetone_pump_cleaning_suction_count
- acetone_inner_wall_cleaning_waste_time
- acetone_stirrer_cleaning_injection
- acetone_stirrer_cleaning_count
- acetone_stirrer_cleaning_wait_time
- acetone_stirrer_cleaning_waste_time
- pipe_blowing_time
- injection_pump_forward_empty_suction_count
- injection_pump_reverse_empty_suction_count
- filtration_liquid_selection
title: PostProcessTriggerClean_Goal title: PostProcessTriggerClean_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: PostProcessTriggerClean_Result title: PostProcessTriggerClean_Result
type: object type: object
required: required:
@@ -502,11 +451,11 @@ post_process_station:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: PostProcessGrab_Feedback title: PostProcessGrab_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
raw_tank_number: raw_tank_number:
maximum: 2147483647 maximum: 2147483647
@@ -516,17 +465,13 @@ post_process_station:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
required:
- reaction_tank_number
- raw_tank_number
title: PostProcessGrab_Goal title: PostProcessGrab_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: PostProcessGrab_Result title: PostProcessGrab_Result
type: object type: object
required: required:
@@ -573,13 +518,15 @@ post_process_station:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: PostProcessTriggerPostPro_Feedback title: PostProcessTriggerPostPro_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
atomization_fast_speed: atomization_fast_speed:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
atomization_pressure_kpa: atomization_pressure_kpa:
maximum: 2147483647 maximum: 2147483647
@@ -594,8 +541,12 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
first_wash_water_amount: first_wash_water_amount:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
initial_water_amount: initial_water_amount:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
injection_pump_push_speed: injection_pump_push_speed:
maximum: 2147483647 maximum: 2147483647
@@ -622,32 +573,20 @@ post_process_station:
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
second_wash_water_amount: second_wash_water_amount:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
wash_slow_speed: wash_slow_speed:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required:
- atomization_fast_speed
- wash_slow_speed
- injection_pump_suction_speed
- injection_pump_push_speed
- raw_liquid_suction_count
- first_wash_water_amount
- second_wash_water_amount
- first_powder_mixing_tim
- second_powder_mixing_time
- first_powder_wash_count
- second_powder_wash_count
- initial_water_amount
- pre_filtration_mixing_time
- atomization_pressure_kpa
title: PostProcessTriggerPostPro_Goal title: PostProcessTriggerPostPro_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: PostProcessTriggerPostPro_Result title: PostProcessTriggerPostPro_Result
type: object type: object
required: required:
@@ -669,30 +608,26 @@ post_process_station:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -702,8 +637,7 @@ post_process_station:
type: SendCmd type: SendCmd
module: unilabos.devices.workstation.post_process.post_process:OpcUaClient module: unilabos.devices.workstation.post_process.post_process:OpcUaClient
status_types: status_types:
cache_stats: dict cache_stats: Dict[str, Any]
node_value: String
type: python type: python
config_info: [] config_info: []
description: 后处理站 description: 后处理站
@@ -718,7 +652,9 @@ post_process_station:
config_path: config_path:
type: string type: string
deck: deck:
type: string anyOf:
- type: object
- type: object
password: password:
type: string type: string
subscription_interval: subscription_interval:
@@ -738,10 +674,7 @@ post_process_station:
properties: properties:
cache_stats: cache_stats:
type: object type: object
node_value:
type: string
required: required:
- node_value
- cache_stats - cache_stats
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -136,36 +136,36 @@ solenoid_valve:
set_valve_position: set_valve_position:
feedback: {} feedback: {}
goal: goal:
string: position position: position
string: string
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:
@@ -278,26 +278,25 @@ solenoid_valve.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -310,26 +309,25 @@ solenoid_valve.mock:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -422,6 +420,27 @@ syringe_pump_with_valve.runze.SY03B-T06:
title: initialize参数 title: initialize参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-list:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: list的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: list参数
type: object
type: UniLabJsonCommand
auto-pull_plunger: auto-pull_plunger:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -695,7 +714,10 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal: goal:
properties: properties:
position: position:
type: string anyOf:
- type: integer
- type: string
- type: number
required: required:
- position - position
type: object type: object
@@ -720,7 +742,9 @@ syringe_pump_with_valve.runze.SY03B-T06:
goal: goal:
properties: properties:
velocity: velocity:
type: string anyOf:
- type: integer
- type: string
required: required:
- velocity - velocity
type: object type: object
@@ -780,13 +804,13 @@ syringe_pump_with_valve.runze.SY03B-T06:
status_types: status_types:
max_velocity: float max_velocity: float
mode: int mode: int
plunger_position: String plunger_position: ''
position: float position: float
status: str status: str
valve_position: str valve_position: str
velocity_end: String velocity_end: ''
velocity_grade: String velocity_grade: ''
velocity_init: String velocity_init: ''
type: python type: python
config_info: [] config_info: []
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。 description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
@@ -885,15 +909,15 @@ syringe_pump_with_valve.runze.SY03B-T06:
velocity_init: velocity_init:
type: string type: string
required: required:
- status
- mode
- max_velocity - max_velocity
- mode
- plunger_position
- position
- status
- valve_position
- velocity_end
- velocity_grade - velocity_grade
- velocity_init - velocity_init
- velocity_end
- valve_position
- position
- plunger_position
type: object type: object
version: 1.0.0 version: 1.0.0
syringe_pump_with_valve.runze.SY03B-T08: syringe_pump_with_valve.runze.SY03B-T08:
@@ -943,6 +967,27 @@ syringe_pump_with_valve.runze.SY03B-T08:
title: initialize参数 title: initialize参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-list:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: list的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: list参数
type: object
type: UniLabJsonCommand
auto-pull_plunger: auto-pull_plunger:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -1216,7 +1261,10 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal: goal:
properties: properties:
position: position:
type: string anyOf:
- type: integer
- type: string
- type: number
required: required:
- position - position
type: object type: object
@@ -1241,7 +1289,9 @@ syringe_pump_with_valve.runze.SY03B-T08:
goal: goal:
properties: properties:
velocity: velocity:
type: string anyOf:
- type: integer
- type: string
required: required:
- velocity - velocity
type: object type: object
@@ -1301,13 +1351,13 @@ syringe_pump_with_valve.runze.SY03B-T08:
status_types: status_types:
max_velocity: float max_velocity: float
mode: int mode: int
plunger_position: String plunger_position: ''
position: float position: float
status: str status: str
valve_position: str valve_position: str
velocity_end: String velocity_end: ''
velocity_grade: String velocity_grade: ''
velocity_init: String velocity_init: ''
type: python type: python
config_info: [] config_info: []
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。 description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
@@ -1422,14 +1472,14 @@ syringe_pump_with_valve.runze.SY03B-T08:
velocity_init: velocity_init:
type: string type: string
required: required:
- status
- mode
- max_velocity - max_velocity
- mode
- plunger_position
- position
- status
- valve_position
- velocity_end
- velocity_grade - velocity_grade
- velocity_init - velocity_init
- velocity_end
- valve_position
- position
- plunger_position
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -13,12 +13,13 @@ reaction_station.bioyond:
start_point: start_point start_point: start_point
start_step_key: start_step_key start_step_key: start_step_key
goal_default: goal_default:
duration: 0 duration: null
end_point: 0 end_point: 0
end_step_key: '' end_step_key: ''
start_point: 0 start_point: 0
start_step_key: '' start_step_key: ''
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 添加时间约束 - 在两个工作流之间添加时间约束 description: 添加时间约束 - 在两个工作流之间添加时间约束
@@ -30,23 +31,19 @@ reaction_station.bioyond:
description: 时间(秒) description: 时间(秒)
type: integer type: integer
end_point: end_point:
default: Start default: 0
description: 终点计时点 (Start=开始前, End=结束后) description: 终点计时点 (Start=开始前, End=结束后)
enum: type: integer
- Start
- End
type: string
end_step_key: end_step_key:
default: ''
description: 终点步骤Key (可选, 默认为空则自动选择) description: 终点步骤Key (可选, 默认为空则自动选择)
type: string type: string
start_point: start_point:
default: Start default: 0
description: 起点计时点 (Start=开始前, End=结束后) description: 起点计时点 (Start=开始前, End=结束后)
enum: type: integer
- Start
- End
type: string
start_step_key: start_step_key:
default: ''
description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择) description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择)
type: string type: string
required: required:
@@ -98,7 +95,8 @@ reaction_station.bioyond:
required: required:
- json_str - json_str
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: create_order参数 title: create_order参数
@@ -125,7 +123,8 @@ reaction_station.bioyond:
required: required:
- workflow_ids - workflow_ids
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: hard_delete_merged_workflows参数 title: hard_delete_merged_workflows参数
@@ -150,7 +149,8 @@ reaction_station.bioyond:
required: required:
- json_str - json_str
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: merge_workflow_with_parameters参数 title: merge_workflow_with_parameters参数
@@ -175,7 +175,8 @@ reaction_station.bioyond:
required: required:
- report_request - report_request
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: process_temperature_cutoff_report参数 title: process_temperature_cutoff_report参数
@@ -200,7 +201,12 @@ reaction_station.bioyond:
required: required:
- web_workflow_json - web_workflow_json
type: object type: object
result: {} result:
items:
additionalProperties:
type: string
type: object
type: array
required: required:
- goal - goal
title: process_web_workflows参数 title: process_web_workflows参数
@@ -229,7 +235,8 @@ reaction_station.bioyond:
- reactor_id - reactor_id
- temperature - temperature
type: object type: object
result: {} result:
type: string
required: required:
- goal - goal
title: set_reactor_temperature参数 title: set_reactor_temperature参数
@@ -254,7 +261,8 @@ reaction_station.bioyond:
required: required:
- preintake_id - preintake_id
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: skip_titration_steps参数 title: skip_titration_steps参数
@@ -275,7 +283,8 @@ reaction_station.bioyond:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: sync_workflow_sequence_from_bioyond参数 title: sync_workflow_sequence_from_bioyond参数
@@ -307,7 +316,8 @@ reaction_station.bioyond:
type: integer type: integer
required: [] required: []
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: wait_for_multiple_orders_and_get_reports参数 title: wait_for_multiple_orders_and_get_reports参数
@@ -359,7 +369,8 @@ reaction_station.bioyond:
required: required:
- workflow_id - workflow_id
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: workflow_step_query参数 title: workflow_step_query参数
@@ -370,9 +381,8 @@ reaction_station.bioyond:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: placeholder_keys: {}
code: code result: {}
message: message
schema: schema:
description: 清空服务端所有非核心工作流 (保留核心流程) description: 清空服务端所有非核心工作流 (保留核心流程)
properties: properties:
@@ -382,13 +392,6 @@ reaction_station.bioyond:
required: [] required: []
type: object type: object
result: result:
properties:
code:
description: 操作结果代码(1表示成功)
type: integer
message:
description: 结果描述
type: string
type: object type: object
required: required:
- goal - goal
@@ -405,13 +408,14 @@ reaction_station.bioyond:
torque_variation: torque_variation torque_variation: torque_variation
volume: volume volume: volume
goal_default: goal_default:
assign_material_name: '' assign_material_name: null
temperature: '' temperature: 25.0
time: '' time: '90'
titration_type: '' titration_type: '1'
torque_variation: '' torque_variation: 2
volume: '' volume: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 滴回去 description: 滴回去
@@ -423,33 +427,27 @@ reaction_station.bioyond:
description: 物料名称(不能为空) description: 物料名称(不能为空)
type: string type: string
temperature: temperature:
default: 25.0
description: 温度设定(°C) description: 温度设定(°C)
type: string type: number
time: time:
default: '90'
description: 观察时间(分钟) description: 观察时间(分钟)
type: string type: string
titration_type: titration_type:
default: '1'
description: 是否滴定(NO=否, YES=是) description: 是否滴定(NO=否, YES=是)
enum:
- 'NO'
- 'YES'
type: string type: string
torque_variation: torque_variation:
default: 2
description: 是否观察 (NO=否, YES=是) description: 是否观察 (NO=否, YES=是)
enum: type: integer
- 'NO'
- 'YES'
type: string
volume: volume:
description: 分液公式(mL) description: 分液公式(mL)
type: string type: string
required: required:
- volume
- assign_material_name - assign_material_name
- time - volume
- torque_variation
- titration_type
- temperature
type: object type: object
result: {} result: {}
required: required:
@@ -462,7 +460,7 @@ reaction_station.bioyond:
goal: goal:
batch_reports_result: batch_reports_result batch_reports_result: batch_reports_result
goal_default: goal_default:
batch_reports_result: '' batch_reports_result: null
handles: handles:
input: input:
- data_key: batch_reports_result - data_key: batch_reports_result
@@ -478,8 +476,8 @@ reaction_station.bioyond:
handler_key: ACTUALS_EXTRACTED handler_key: ACTUALS_EXTRACTED
io_type: sink io_type: sink
label: Extracted Actuals label: Extracted Actuals
result: placeholder_keys: {}
return_info: return_info result: {}
schema: schema:
description: 从批量任务完成报告中提取每个订单的实际加料量输出extracted列表。 description: 从批量任务完成报告中提取每个订单的实际加料量输出extracted列表。
properties: properties:
@@ -493,13 +491,6 @@ reaction_station.bioyond:
- batch_reports_result - batch_reports_result
type: object type: object
result: result:
properties:
return_info:
description: JSON字符串包含actuals数组每项含order_code, order_id, actualTargetWeigh,
actualVolume
type: string
required:
- return_info
title: extract_actuals_from_batch_reports结果 title: extract_actuals_from_batch_reports结果
type: object type: object
required: required:
@@ -517,13 +508,14 @@ reaction_station.bioyond:
torque_variation: torque_variation torque_variation: torque_variation
volume: volume volume: volume
goal_default: goal_default:
assign_material_name: '' assign_material_name: BAPP
temperature: '' temperature: 25.0
time: '' time: '0'
titration_type: '' titration_type: '1'
torque_variation: '' torque_variation: 1
volume: '' volume: '350'
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 液体进料烧杯 description: 液体进料烧杯
@@ -532,36 +524,30 @@ reaction_station.bioyond:
goal: goal:
properties: properties:
assign_material_name: assign_material_name:
default: BAPP
description: 物料名称 description: 物料名称
type: string type: string
temperature: temperature:
default: 25.0
description: 温度设定(°C) description: 温度设定(°C)
type: string type: number
time: time:
default: '0'
description: 观察时间(分钟) description: 观察时间(分钟)
type: string type: string
titration_type: titration_type:
default: '1'
description: 是否滴定(NO=否, YES=是) description: 是否滴定(NO=否, YES=是)
enum:
- 'NO'
- 'YES'
type: string type: string
torque_variation: torque_variation:
default: 1
description: 是否观察 (NO=否, YES=是) description: 是否观察 (NO=否, YES=是)
enum: type: integer
- 'NO'
- 'YES'
type: string
volume: volume:
default: '350'
description: 分液公式(mL) description: 分液公式(mL)
type: string type: string
required: required: []
- volume
- assign_material_name
- time
- torque_variation
- titration_type
- temperature
type: object type: object
result: {} result: {}
required: required:
@@ -580,13 +566,13 @@ reaction_station.bioyond:
torque_variation: torque_variation torque_variation: torque_variation
volume: volume volume: volume
goal_default: goal_default:
assign_material_name: '' assign_material_name: null
solvents: '' solvents: null
temperature: '25.00' temperature: 25.0
time: '360' time: '360'
titration_type: '1' titration_type: '1'
torque_variation: '2' torque_variation: 2
volume: '' volume: null
handles: handles:
input: input:
- data_key: solvents - data_key: solvents
@@ -595,6 +581,7 @@ reaction_station.bioyond:
handler_key: solvents handler_key: solvents
io_type: source io_type: source
label: Solvents Data From Calculation Node label: Solvents Data From Calculation Node
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 液体投料-溶剂。可以直接提供volume(mL),或通过solvents对象自动从additional_solvent(mL)计算volume。 description: 液体投料-溶剂。可以直接提供volume(mL),或通过solvents对象自动从additional_solvent(mL)计算volume。
@@ -609,27 +596,21 @@ reaction_station.bioyond:
description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume' description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume'
type: string type: string
temperature: temperature:
default: '25.00' default: 25.0
description: 温度设定(°C),默认25.00 description: 温度设定(°C),默认25.00
type: string type: number
time: time:
default: '360' default: '360'
description: 观察时间(分钟),默认360 description: 观察时间(分钟),默认360
type: string type: string
titration_type: titration_type:
default: 'NO' default: '1'
description: 是否滴定(NO=否, YES=是),默认NO description: 是否滴定(NO=否, YES=是),默认NO
enum:
- 'NO'
- 'YES'
type: string type: string
torque_variation: torque_variation:
default: 'YES' default: 2
description: 是否观察 (NO=否, YES=是),默认YES description: 是否观察 (NO=否, YES=是),默认YES
enum: type: integer
- 'NO'
- 'YES'
type: string
volume: volume:
description: 分液量(mL)。可直接提供,或通过solvents参数自动计算 description: 分液量(mL)。可直接提供,或通过solvents参数自动计算
type: string type: string
@@ -655,15 +636,15 @@ reaction_station.bioyond:
volume_formula: volume_formula volume_formula: volume_formula
x_value: x_value x_value: x_value
goal_default: goal_default:
assign_material_name: '' assign_material_name: null
extracted_actuals: '' extracted_actuals: null
feeding_order_data: '' feeding_order_data: null
temperature: '25.00' temperature: 25.0
time: '90' time: '90'
titration_type: '2' titration_type: '2'
torque_variation: '2' torque_variation: 2
volume_formula: '' volume_formula: null
x_value: '' x_value: null
handles: handles:
input: input:
- data_key: extracted_actuals - data_key: extracted_actuals
@@ -678,6 +659,7 @@ reaction_station.bioyond:
handler_key: feeding_order handler_key: feeding_order
io_type: source io_type: source
label: Feeding Order Data From Calculation Node label: Feeding Order Data From Calculation Node
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 液体进料(滴定)。支持两种模式:1)直接提供volume_formula;2)自动计算-提供x_value+feeding_order_data+extracted_actuals,系统自动生成公式"1000*(m二酐-x)*V二酐滴定/m二酐滴定" description: 液体进料(滴定)。支持两种模式:1)直接提供volume_formula;2)自动计算-提供x_value+feeding_order_data+extracted_actuals,系统自动生成公式"1000*(m二酐-x)*V二酐滴定/m二酐滴定"
@@ -696,27 +678,21 @@ reaction_station.bioyond:
{"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}' {"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}'
type: string type: string
temperature: temperature:
default: '25.00' default: 25.0
description: 温度设定(°C),默认25.00 description: 温度设定(°C),默认25.00
type: string type: number
time: time:
default: '90' default: '90'
description: 观察时间(分钟),默认90 description: 观察时间(分钟),默认90
type: string type: string
titration_type: titration_type:
default: 'YES' default: '2'
description: 是否滴定(NO=否, YES=是),默认YES description: 是否滴定(NO=否, YES=是),默认YES
enum:
- 'NO'
- 'YES'
type: string type: string
torque_variation: torque_variation:
default: 'YES' default: 2
description: 是否观察 (NO=否, YES=是),默认YES description: 是否观察 (NO=否, YES=是),默认YES
enum: type: integer
- 'NO'
- 'YES'
type: string
volume_formula: volume_formula:
description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成 description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成
type: string type: string
@@ -742,13 +718,14 @@ reaction_station.bioyond:
torque_variation: torque_variation torque_variation: torque_variation
volume_formula: volume_formula volume_formula: volume_formula
goal_default: goal_default:
assign_material_name: '' assign_material_name: null
temperature: '' temperature: 25.0
time: '' time: '0'
titration_type: '' titration_type: '1'
torque_variation: '' torque_variation: 1
volume_formula: '' volume_formula: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 液体进料小瓶(非滴定) description: 液体进料小瓶(非滴定)
@@ -760,33 +737,27 @@ reaction_station.bioyond:
description: 物料名称 description: 物料名称
type: string type: string
temperature: temperature:
default: 25.0
description: 温度设定(°C) description: 温度设定(°C)
type: string type: number
time: time:
default: '0'
description: 观察时间(分钟) description: 观察时间(分钟)
type: string type: string
titration_type: titration_type:
default: '1'
description: 是否滴定(NO=否, YES=是) description: 是否滴定(NO=否, YES=是)
enum:
- 'NO'
- 'YES'
type: string type: string
torque_variation: torque_variation:
default: 1
description: 是否观察 (NO=否, YES=是) description: 是否观察 (NO=否, YES=是)
enum: type: integer
- 'NO'
- 'YES'
type: string
volume_formula: volume_formula:
description: 分液公式(mL) description: 分液公式(mL)
type: string type: string
required: required:
- volume_formula - volume_formula
- assign_material_name - assign_material_name
- time
- torque_variation
- titration_type
- temperature
type: object type: object
result: {} result: {}
required: required:
@@ -800,9 +771,10 @@ reaction_station.bioyond:
task_name: task_name task_name: task_name
workflow_name: workflow_name workflow_name: workflow_name
goal_default: goal_default:
task_name: '' task_name: null
workflow_name: '' workflow_name: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 处理并执行工作流 description: 处理并执行工作流
@@ -820,7 +792,8 @@ reaction_station.bioyond:
- workflow_name - workflow_name
- task_name - task_name
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: process_and_execute_workflow参数 title: process_and_execute_workflow参数
@@ -833,10 +806,11 @@ reaction_station.bioyond:
cutoff: cutoff cutoff: cutoff
temperature: temperature temperature: temperature
goal_default: goal_default:
assign_material_name: '' assign_material_name: null
cutoff: '' cutoff: '900000'
temperature: '' temperature: -10.0
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数 description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数
@@ -848,14 +822,14 @@ reaction_station.bioyond:
description: 物料名称 description: 物料名称
type: string type: string
cutoff: cutoff:
default: '900000'
description: 粘度上限 description: 粘度上限
type: string type: string
temperature: temperature:
default: -10.0
description: 温度设定(°C) description: 温度设定(°C)
type: string type: number
required: required:
- cutoff
- temperature
- assign_material_name - assign_material_name
type: object type: object
result: {} result: {}
@@ -869,6 +843,7 @@ reaction_station.bioyond:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作 description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作
@@ -878,15 +853,7 @@ reaction_station.bioyond:
properties: {} properties: {}
required: [] required: []
type: object type: object
result: result: {}
properties:
code:
description: 操作结果代码(1表示成功,0表示失败)
type: integer
return_info:
description: 操作结果详细信息
type: string
type: object
required: required:
- goal - goal
title: reactor_taken_out参数 title: reactor_taken_out参数
@@ -897,8 +864,8 @@ reaction_station.bioyond:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: placeholder_keys: {}
return_info: return_info result: {}
schema: schema:
description: 启动调度器 - 启动Bioyond工作站的任务调度器开始执行队列中的任务 description: 启动调度器 - 启动Bioyond工作站的任务调度器开始执行队列中的任务
properties: properties:
@@ -908,12 +875,6 @@ reaction_station.bioyond:
required: [] required: []
type: object type: object
result: result:
properties:
return_info:
description: 调度器启动结果成功返回1失败返回0
type: integer
required:
- return_info
title: scheduler_start结果 title: scheduler_start结果
type: object type: object
required: required:
@@ -930,12 +891,13 @@ reaction_station.bioyond:
time: time time: time
torque_variation: torque_variation torque_variation: torque_variation
goal_default: goal_default:
assign_material_name: '' assign_material_name: null
material_id: '' material_id: null
temperature: '' temperature: 25.0
time: '' time: '0'
torque_variation: '' torque_variation: 1
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA) description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA)
@@ -948,29 +910,21 @@ reaction_station.bioyond:
type: string type: string
material_id: material_id:
description: 粉末类型IDSalt=盐21分钟Flour=面粉27分钟BTDA=BTDA38分钟 description: 粉末类型IDSalt=盐21分钟Flour=面粉27分钟BTDA=BTDA38分钟
enum:
- Salt
- Flour
- BTDA
type: string type: string
temperature: temperature:
default: 25.0
description: 温度设定(°C) description: 温度设定(°C)
type: string type: number
time: time:
default: '0'
description: 观察时间(分钟) description: 观察时间(分钟)
type: string type: string
torque_variation: torque_variation:
default: 1
description: 是否观察 (NO=否, YES=是) description: 是否观察 (NO=否, YES=是)
enum: type: integer
- 'NO'
- 'YES'
type: string
required: required:
- assign_material_name
- material_id - material_id
- time
- torque_variation
- temperature
type: object type: object
result: {} result: {}
required: required:

View File

@@ -37,42 +37,41 @@ agv.SEER:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
send_nav_task: send_nav_task:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:

View File

@@ -122,31 +122,6 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
title: moveit_task参数 title: moveit_task参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: post_init的参数schema
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-resource_manager: auto-resource_manager:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -198,41 +173,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
pick_and_place: pick_and_place:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -241,41 +216,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
type: object type: object
type: SendCmd type: SendCmd
set_position: set_position:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -284,41 +259,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
type: object type: object
type: SendCmd type: SendCmd
set_status: set_status:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -455,42 +430,41 @@ robotic_arm.UR:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
move_pos_task: move_pos_task:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -532,8 +506,8 @@ robotic_arm.UR:
type: string type: string
required: required:
- arm_pose - arm_pose
- gripper_pose
- arm_status - arm_status
- gripper_pose
- gripper_status - gripper_status
type: object type: object
version: 1.0.0 version: 1.0.0
@@ -726,41 +700,41 @@ robotic_arm.elite:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
modbus_task_cmd: modbus_task_cmd:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -770,8 +744,8 @@ robotic_arm.elite:
type: SendCmd type: SendCmd
module: unilabos.devices.arm.elite_robot:EliteRobot module: unilabos.devices.arm.elite_robot:EliteRobot
status_types: status_types:
actual_joint_positions: String actual_joint_positions: ''
arm_pose: String arm_pose: list[float]
type: python type: python
config_info: [] config_info: []
description: Elite robot arm description: Elite robot arm
@@ -797,8 +771,8 @@ robotic_arm.elite:
type: number type: number
type: array type: array
required: required:
- arm_pose
- actual_joint_positions - actual_joint_positions
- arm_pose
type: object type: object
model: model:
mesh: elite_robot mesh: elite_robot

View File

@@ -114,11 +114,12 @@ gripper.misumi_rz:
goal: goal:
properties: properties:
data: data:
type: string type: object
required: required:
- data - data
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: modbus_crc参数 title: modbus_crc参数
@@ -398,30 +399,26 @@ gripper.misumi_rz:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -504,71 +501,82 @@ gripper.mock:
type: UniLabJsonCommand type: UniLabJsonCommand
push_to: push_to:
feedback: feedback:
effort: torque effort: effort
position: position position: position
reached_goal: reached_goal
stalled: stalled
goal: goal:
command.max_effort: torque command: command
command.position: position position: position
torque: torque
velocity: velocity
goal_default: goal_default:
command: command:
max_effort: 0.0 max_effort: 0.0
position: 0.0 position: 0.0
handles: {} handles: {}
placeholder_keys: {}
result: result:
effort: torque effort: effort
position: position position: position
reached_goal: reached_goal
stalled: stalled
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
effort: effort:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
position: position:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
reached_goal: reached_goal:
type: boolean type: boolean
stalled: stalled:
type: boolean type: boolean
required:
- position
- effort
- stalled
- reached_goal
title: GripperCommand_Feedback title: GripperCommand_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
additionalProperties: false
properties: properties:
max_effort: max_effort:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
position: position:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- position - position
- max_effort - max_effort
title: command title: command
type: object type: object
required:
- command
title: GripperCommand_Goal title: GripperCommand_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
effort: effort:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
position: position:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
reached_goal: reached_goal:
type: boolean type: boolean
stalled: stalled:
type: boolean type: boolean
required:
- position
- effort
- stalled
- reached_goal
title: GripperCommand_Result title: GripperCommand_Result
type: object type: object
required: required:
@@ -604,8 +612,8 @@ gripper.mock:
type: number type: number
required: required:
- position - position
- velocity
- torque
- status - status
- torque
- velocity
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -24,6 +24,27 @@ linear_motion.grbl:
title: initialize参数 title: initialize参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-list:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: list的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: list参数
type: object
type: UniLabJsonCommand
auto-set_position: auto-set_position:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -93,44 +114,39 @@ linear_motion.grbl:
type: UniLabJsonCommandAsync type: UniLabJsonCommandAsync
move_through_points: move_through_points:
feedback: feedback:
current_pose.pose.position: position current_pose: current_pose
estimated_time_remaining.sec: time_remaining distance_remaining: distance_remaining
navigation_time.sec: time_spent estimated_time_remaining: estimated_time_remaining
number_of_poses_remaining: pose_number_remaining navigation_time: navigation_time
number_of_poses_remaining: number_of_poses_remaining
number_of_recoveries: number_of_recoveries
goal: goal:
poses[].pose.position: positions[] behavior_tree: behavior_tree
poses: poses
positions: positions
goal_default: goal_default:
behavior_tree: '' behavior_tree: ''
poses: poses: []
- header:
frame_id: ''
stamp:
nanosec: 0
sec: 0
pose:
orientation:
w: 1.0
x: 0.0
y: 0.0
z: 0.0
position:
x: 0.0
y: 0.0
z: 0.0
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
result: result
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
current_pose: current_pose:
additionalProperties: false
properties: properties:
header: header:
additionalProperties: false
properties: properties:
frame_id: frame_id:
type: string type: string
stamp: stamp:
additionalProperties: false
properties: properties:
nanosec: nanosec:
maximum: 4294967295 maximum: 4294967295
@@ -151,16 +167,26 @@ linear_motion.grbl:
title: header title: header
type: object type: object
pose: pose:
additionalProperties: false
properties: properties:
orientation: orientation:
additionalProperties: false
properties: properties:
w: w:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- x - x
@@ -170,12 +196,19 @@ linear_motion.grbl:
title: orientation title: orientation
type: object type: object
position: position:
additionalProperties: false
properties: properties:
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- x - x
@@ -194,8 +227,11 @@ linear_motion.grbl:
title: current_pose title: current_pose
type: object type: object
distance_remaining: distance_remaining:
maximum: 3.4028235e+38
minimum: -3.4028235e+38
type: number type: number
estimated_time_remaining: estimated_time_remaining:
additionalProperties: false
properties: properties:
nanosec: nanosec:
maximum: 4294967295 maximum: 4294967295
@@ -211,6 +247,7 @@ linear_motion.grbl:
title: estimated_time_remaining title: estimated_time_remaining
type: object type: object
navigation_time: navigation_time:
additionalProperties: false
properties: properties:
nanosec: nanosec:
maximum: 4294967295 maximum: 4294967295
@@ -233,16 +270,10 @@ linear_motion.grbl:
maximum: 32767 maximum: 32767
minimum: -32768 minimum: -32768
type: integer type: integer
required:
- current_pose
- navigation_time
- estimated_time_remaining
- number_of_recoveries
- distance_remaining
- number_of_poses_remaining
title: NavigateThroughPoses_Feedback title: NavigateThroughPoses_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
behavior_tree: behavior_tree:
type: string type: string
@@ -256,12 +287,8 @@ linear_motion.grbl:
stamp: stamp:
properties: properties:
nanosec: nanosec:
maximum: 4294967295
minimum: 0
type: integer type: integer
sec: sec:
maximum: 2147483647
minimum: -2147483648
type: integer type: integer
required: required:
- sec - sec
@@ -314,23 +341,17 @@ linear_motion.grbl:
required: required:
- header - header
- pose - pose
title: poses
type: object type: object
type: array type: array
required:
- poses
- behavior_tree
title: NavigateThroughPoses_Goal title: NavigateThroughPoses_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
result: result:
properties: {} additionalProperties: true
required: []
title: result title: result
type: object type: object
required:
- result
title: NavigateThroughPoses_Result title: NavigateThroughPoses_Result
type: object type: object
required: required:
@@ -340,9 +361,15 @@ linear_motion.grbl:
type: NavigateThroughPoses type: NavigateThroughPoses
set_spindle_speed: set_spindle_speed:
feedback: feedback:
position: spindle_speed error: error
header: header
position: position
velocity: velocity
goal: goal:
position: spindle_speed max_velocity: max_velocity
min_duration: min_duration
position: position
spindle_speed: spindle_speed
goal_default: goal_default:
max_velocity: 0.0 max_velocity: 0.0
min_duration: min_duration:
@@ -350,19 +377,25 @@ linear_motion.grbl:
sec: 0 sec: 0
position: 0.0 position: 0.0
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
error: error:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
header: header:
additionalProperties: false
properties: properties:
frame_id: frame_id:
type: string type: string
stamp: stamp:
additionalProperties: false
properties: properties:
nanosec: nanosec:
maximum: 4294967295 maximum: 4294967295
@@ -383,21 +416,24 @@ linear_motion.grbl:
title: header title: header
type: object type: object
position: position:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
velocity: velocity:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required:
- header
- position
- velocity
- error
title: SingleJointPosition_Feedback title: SingleJointPosition_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
max_velocity: max_velocity:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
min_duration: min_duration:
additionalProperties: false
properties: properties:
nanosec: nanosec:
maximum: 4294967295 maximum: 4294967295
@@ -413,16 +449,13 @@ linear_motion.grbl:
title: min_duration title: min_duration
type: object type: object
position: position:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required:
- position
- min_duration
- max_velocity
title: SingleJointPosition_Goal title: SingleJointPosition_Goal
type: object type: object
result: result:
properties: {} additionalProperties: true
required: []
title: SingleJointPosition_Result title: SingleJointPosition_Result
type: object type: object
required: required:
@@ -432,7 +465,7 @@ linear_motion.grbl:
type: SingleJointPosition type: SingleJointPosition
module: unilabos.devices.cnc.grbl_sync:GrblCNC module: unilabos.devices.cnc.grbl_sync:GrblCNC
status_types: status_types:
position: unilabos.messages:Point3D position: Point3D
spindle_speed: float spindle_speed: float
status: str status: str
type: python type: python
@@ -471,9 +504,9 @@ linear_motion.grbl:
status: status:
type: string type: string
required: required:
- status
- position - position
- spindle_speed - spindle_speed
- status
type: object type: object
version: 1.0.0 version: 1.0.0
linear_motion.toyo_xyz.sim: linear_motion.toyo_xyz.sim:
@@ -600,31 +633,6 @@ linear_motion.toyo_xyz.sim:
title: moveit_task参数 title: moveit_task参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: post_init的参数schema
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-resource_manager: auto-resource_manager:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -676,41 +684,41 @@ linear_motion.toyo_xyz.sim:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
pick_and_place: pick_and_place:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -719,41 +727,41 @@ linear_motion.toyo_xyz.sim:
type: object type: object
type: SendCmd type: SendCmd
set_position: set_position:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -762,41 +770,41 @@ linear_motion.toyo_xyz.sim:
type: object type: object
type: SendCmd type: SendCmd
set_status: set_status:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -939,30 +947,26 @@ motor.iCL42:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -1000,8 +1004,8 @@ motor.iCL42:
success: success:
type: boolean type: boolean
required: required:
- motor_position
- is_executing_run - is_executing_run
- motor_position
- success - success
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -14,19 +14,24 @@ solid_dispenser.laiyu:
powder_tube_number: 0 powder_tube_number: 0
target_tube_position: '' target_tube_position: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
actual_mass_mg: actual_mass_mg actual_mass_mg: actual_mass_mg
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: SolidDispenseAddPowderTube_Feedback title: SolidDispenseAddPowderTube_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
compound_mass: compound_mass:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
powder_tube_number: powder_tube_number:
maximum: 2147483647 maximum: 2147483647
@@ -34,24 +39,19 @@ solid_dispenser.laiyu:
type: integer type: integer
target_tube_position: target_tube_position:
type: string type: string
required:
- powder_tube_number
- target_tube_position
- compound_mass
title: SolidDispenseAddPowderTube_Goal title: SolidDispenseAddPowderTube_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
actual_mass_mg: actual_mass_mg:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- actual_mass_mg
- success
title: SolidDispenseAddPowderTube_Result title: SolidDispenseAddPowderTube_Result
type: object type: object
required: required:
@@ -74,11 +74,12 @@ solid_dispenser.laiyu:
goal: goal:
properties: properties:
data: data:
type: string type: object
required: required:
- data - data
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: calculate_crc参数 title: calculate_crc参数
@@ -99,11 +100,12 @@ solid_dispenser.laiyu:
goal: goal:
properties: properties:
command: command:
type: string type: object
required: required:
- command - command
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: send_command参数 title: send_command参数
@@ -112,36 +114,37 @@ solid_dispenser.laiyu:
discharge: discharge:
feedback: {} feedback: {}
goal: goal:
float_input: float_input float_in: float_in
goal_default: goal_default:
float_in: 0.0 float_in: 0.0
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: FloatSingleInput_Feedback title: FloatSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
float_in: float_in:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required:
- float_in
title: FloatSingleInput_Goal title: FloatSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: FloatSingleInput_Result title: FloatSingleInput_Result
type: object type: object
required: required:
@@ -156,32 +159,31 @@ solid_dispenser.laiyu:
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:
@@ -200,38 +202,41 @@ solid_dispenser.laiyu:
y: 0.0 y: 0.0
z: 0.0 z: 0.0
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: Point3DSeparateInput_Feedback title: Point3DSeparateInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required:
- x
- y
- z
title: Point3DSeparateInput_Goal title: Point3DSeparateInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: Point3DSeparateInput_Result title: Point3DSeparateInput_Result
type: object type: object
required: required:
@@ -246,34 +251,33 @@ solid_dispenser.laiyu:
goal_default: goal_default:
int_input: 0 int_input: 0
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: IntSingleInput_Feedback title: IntSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
int_input: int_input:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
required:
- int_input
title: IntSingleInput_Goal title: IntSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: IntSingleInput_Result title: IntSingleInput_Result
type: object type: object
required: required:
@@ -288,34 +292,33 @@ solid_dispenser.laiyu:
goal_default: goal_default:
int_input: 0 int_input: 0
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: IntSingleInput_Feedback title: IntSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
int_input: int_input:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
required:
- int_input
title: IntSingleInput_Goal title: IntSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: IntSingleInput_Result title: IntSingleInput_Result
type: object type: object
required: required:
@@ -328,26 +331,25 @@ solid_dispenser.laiyu:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:

View File

@@ -34,7 +34,8 @@ chiller:
- register_address - register_address
- value - value
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: build_modbus_frame参数 title: build_modbus_frame参数
@@ -63,7 +64,8 @@ chiller:
required: required:
- temperature - temperature
type: object type: object
result: {} result:
type: integer
required: required:
- goal - goal
title: convert_temperature_to_modbus_value参数 title: convert_temperature_to_modbus_value参数
@@ -84,11 +86,12 @@ chiller:
goal: goal:
properties: properties:
data: data:
type: string type: object
required: required:
- data - data
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: modbus_crc参数 title: modbus_crc参数
@@ -116,42 +119,41 @@ chiller:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
set_temperature: set_temperature:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -266,9 +268,15 @@ heaterstirrer.dalong:
feedback: feedback:
status: status status: status
goal: goal:
pressure: pressure
purpose: purpose purpose: purpose
reflux_solvent: reflux_solvent
stir: stir
stir_speed: stir_speed
temp: temp temp: temp
temp_spec: temp_spec
time: time time: time
time_spec: time_spec
vessel: vessel vessel: vessel
goal_default: goal_default:
pressure: '' pressure: ''
@@ -301,20 +309,23 @@ heaterstirrer.dalong:
sample_id: '' sample_id: ''
type: '' type: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
message: message
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: HeatChill_Feedback title: HeatChill_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
pressure: pressure:
type: string type: string
@@ -325,8 +336,12 @@ heaterstirrer.dalong:
stir: stir:
type: boolean type: boolean
stir_speed: stir_speed:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
temp: temp:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
temp_spec: temp_spec:
type: string type: string
@@ -335,6 +350,7 @@ heaterstirrer.dalong:
time_spec: time_spec:
type: string type: string
vessel: vessel:
additionalProperties: false
properties: properties:
category: category:
type: string type: string
@@ -353,16 +369,26 @@ heaterstirrer.dalong:
parent: parent:
type: string type: string
pose: pose:
additionalProperties: false
properties: properties:
orientation: orientation:
additionalProperties: false
properties: properties:
w: w:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- x - x
@@ -372,12 +398,19 @@ heaterstirrer.dalong:
title: orientation title: orientation
type: object type: object
position: position:
additionalProperties: false
properties: properties:
x: x:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
y: y:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
z: z:
maximum: 1.7976931348623157e+308
minimum: -1.7976931348623157e+308
type: number type: number
required: required:
- x - x
@@ -407,20 +440,10 @@ heaterstirrer.dalong:
- data - data
title: vessel title: vessel
type: object type: object
required:
- vessel
- temp
- time
- temp_spec
- time_spec
- pressure
- reflux_solvent
- stir
- stir_speed
- purpose
title: HeatChill_Goal title: HeatChill_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
message: message:
type: string type: string
@@ -428,10 +451,6 @@ heaterstirrer.dalong:
type: string type: string
success: success:
type: boolean type: boolean
required:
- success
- message
- return_info
title: HeatChill_Result title: HeatChill_Result
type: object type: object
required: required:
@@ -440,42 +459,42 @@ heaterstirrer.dalong:
type: object type: object
type: HeatChill type: HeatChill
set_temp_target: set_temp_target:
feedback: {} feedback:
status: status
goal: goal:
command: temp command: command
temp: temp
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -484,42 +503,42 @@ heaterstirrer.dalong:
type: object type: object
type: SendCmd type: SendCmd
set_temp_warning: set_temp_warning:
feedback: {} feedback:
status: status
goal: goal:
command: temp command: command
temp: temp
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:
@@ -569,8 +588,8 @@ heaterstirrer.dalong:
- status - status
- stir_speed - stir_speed
- temp - temp
- temp_warning
- temp_target - temp_target
- temp_warning
type: object type: object
version: 1.0.0 version: 1.0.0
tempsensor: tempsensor:
@@ -691,42 +710,41 @@ tempsensor:
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
set_warning: set_warning:
feedback: {} feedback:
status: status
goal: goal:
command: command command: command
goal_default: goal_default:
command: '' command: ''
handles: {} handles: {}
placeholder_keys: {}
result: result:
return_info: return_info
success: success success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
additionalProperties: false
properties: properties:
status: status:
type: string type: string
required:
- status
title: SendCmd_Feedback title: SendCmd_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
command: command:
type: string type: string
required:
- command
title: SendCmd_Goal title: SendCmd_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: SendCmd_Result title: SendCmd_Result
type: object type: object
required: required:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -45,31 +45,6 @@ xrd_d7mate:
title: connect参数 title: connect参数
type: object type: object
type: UniLabJsonCommand 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: string
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-start_from_string: auto-start_from_string:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -85,11 +60,14 @@ xrd_d7mate:
goal: goal:
properties: properties:
params: params:
type: string anyOf:
- type: string
- type: object
required: required:
- params - params
type: object type: object
result: {} result:
type: object
required: required:
- goal - goal
title: start_from_string参数 title: start_from_string参数
@@ -105,21 +83,18 @@ xrd_d7mate:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -130,38 +105,38 @@ xrd_d7mate:
get_sample_down: get_sample_down:
feedback: {} feedback: {}
goal: goal:
sample_station: 1 int_input: int_input
sample_station: sample_station
goal_default: goal_default:
int_input: 0 int_input: 0
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: IntSingleInput_Feedback title: IntSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
int_input: int_input:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
type: integer type: integer
required:
- int_input
title: IntSingleInput_Goal title: IntSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: IntSingleInput_Result title: IntSingleInput_Result
type: object type: object
required: required:
@@ -179,21 +154,18 @@ xrd_d7mate:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -211,21 +183,18 @@ xrd_d7mate:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -238,26 +207,25 @@ xrd_d7mate:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -274,42 +242,35 @@ xrd_d7mate:
sample_id: '' sample_id: ''
start_theta: 10.0 start_theta: 10.0
goal_default: goal_default:
end_theta: 80.0 end_theta: null
exp_time: 0.5 exp_time: null
increment: 0.02 increment: null
sample_id: Sample001 sample_id: null
start_theta: 10.0 start_theta: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 送样完成后,发送样品信息和采集参数 description: 送样完成后,发送样品信息和采集参数
properties: properties:
feedback: feedback:
properties: {}
required: []
title: SampleReadyInput_Feedback title: SampleReadyInput_Feedback
type: object
goal: goal:
properties: properties:
end_theta: end_theta:
description: 结束角度≥5.5°且必须大于start_theta description: 结束角度≥5.5°且必须大于start_theta
minimum: 5.5
type: number type: number
exp_time: exp_time:
description: 曝光时间0.1-5.0秒) description: 曝光时间0.1-5.0秒)
maximum: 5.0
minimum: 0.1
type: number type: number
increment: increment:
description: 角度增量≥0.005 description: 角度增量≥0.005
minimum: 0.005
type: number type: number
sample_id: sample_id:
description: 样品标识符 description: 样品标识符
type: string type: string
start_theta: start_theta:
description: 起始角度≥5° description: 起始角度≥5°
minimum: 5.0
type: number type: number
required: required:
- sample_id - sample_id
@@ -320,19 +281,11 @@ xrd_d7mate:
title: SampleReadyInput_Goal title: SampleReadyInput_Goal
type: object type: object
result: result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: SampleReadyInput_Result title: SampleReadyInput_Result
type: object type: object
required: required:
- goal - goal
title: SampleReadyInput title: send_sample_ready参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
set_power_off: set_power_off:
@@ -340,26 +293,25 @@ xrd_d7mate:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -372,26 +324,25 @@ xrd_d7mate:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -405,18 +356,16 @@ xrd_d7mate:
current: 30.0 current: 30.0
voltage: 40.0 voltage: 40.0
goal_default: goal_default:
current: 30.0 current: null
voltage: 40.0 voltage: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 设置高压电源电压和电流 description: 设置高压电源电压和电流
properties: properties:
feedback: feedback:
properties: {}
required: []
title: VoltageCurrentInput_Feedback title: VoltageCurrentInput_Feedback
type: object
goal: goal:
properties: properties:
current: current:
@@ -431,19 +380,11 @@ xrd_d7mate:
title: VoltageCurrentInput_Goal title: VoltageCurrentInput_Goal
type: object type: object
result: result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: VoltageCurrentInput_Result title: VoltageCurrentInput_Result
type: object type: object
required: required:
- goal - goal
title: VoltageCurrentInput title: set_voltage_current参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
start: start:
@@ -453,11 +394,12 @@ xrd_d7mate:
end_theta: 80.0 end_theta: 80.0
exp_time: 0.1 exp_time: 0.1
increment: 0.05 increment: 0.05
sample_id: 样品名称 sample_id: ''
start_theta: 10.0 start_theta: 10.0
string: '' string: ''
wait_minutes: 3.0 wait_minutes: 3.0
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 启动自动模式→上样→等待→样品准备→监控→检测下样位→执行下样流程。 description: 启动自动模式→上样→等待→样品准备→监控→检测下样位→执行下样流程。
@@ -466,54 +408,42 @@ xrd_d7mate:
goal: goal:
properties: properties:
end_theta: end_theta:
default: 80.0
description: 结束角度≥5.5°且必须大于start_theta description: 结束角度≥5.5°且必须大于start_theta
minimum: 5.5 type: number
type: string
exp_time: exp_time:
default: 0.1
description: 曝光时间0.1-5.0秒) description: 曝光时间0.1-5.0秒)
maximum: 5.0 type: number
minimum: 0.1
type: string
increment: increment:
default: 0.05
description: 角度增量≥0.005 description: 角度增量≥0.005
minimum: 0.005 type: number
type: string
sample_id: sample_id:
default: ''
description: 样品标识符 description: 样品标识符
type: string type: string
start_theta: start_theta:
default: 10.0
description: 起始角度≥5° description: 起始角度≥5°
minimum: 5.0 type: number
type: string
string: string:
default: ''
description: 字符串格式的参数输入,如果提供则优先解析使用 description: 字符串格式的参数输入,如果提供则优先解析使用
type: string type: string
wait_minutes: wait_minutes:
default: 3.0
description: 允许上样后等待分钟数 description: 允许上样后等待分钟数
minimum: 0.0
type: number type: number
required: required: []
- sample_id
- start_theta
- end_theta
- increment
- exp_time
title: StartWorkflow_Goal title: StartWorkflow_Goal
type: object type: object
result: result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: StartWorkflow_Result title: StartWorkflow_Result
type: object type: object
required: required:
- goal - goal
title: StartWorkflow title: start参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
start_auto_mode: start_auto_mode:
@@ -521,17 +451,15 @@ xrd_d7mate:
goal: goal:
status: true status: true
goal_default: goal_default:
status: true status: null
handles: {} handles: {}
placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 启动或停止自动模式 description: 启动或停止自动模式
properties: properties:
feedback: feedback:
properties: {}
required: []
title: BoolSingleInput_Feedback title: BoolSingleInput_Feedback
type: object
goal: goal:
properties: properties:
status: status:
@@ -542,25 +470,16 @@ xrd_d7mate:
title: BoolSingleInput_Goal title: BoolSingleInput_Goal
type: object type: object
result: result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: BoolSingleInput_Result title: BoolSingleInput_Result
type: object type: object
required: required:
- goal - goal
title: BoolSingleInput title: start_auto_mode参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
module: unilabos.devices.xrd_d7mate.xrd_d7mate:XRDClient module: unilabos.devices.xrd_d7mate.xrd_d7mate:XRDClient
status_types: status_types:
current_acquire_data: dict current_acquire_data: dict
sample_down: dict
sample_request: dict sample_request: dict
sample_status: dict sample_status: dict
type: python type: python
@@ -586,16 +505,13 @@ xrd_d7mate:
properties: properties:
current_acquire_data: current_acquire_data:
type: object type: object
sample_down:
type: object
sample_request: sample_request:
type: object type: object
sample_status: sample_status:
type: object type: object
required: required:
- sample_request
- current_acquire_data - current_acquire_data
- sample_request
- sample_status - sample_status
- sample_down
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -8,26 +8,25 @@ zhida_gcms:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -77,31 +76,6 @@ zhida_gcms:
title: connect参数 title: connect参数
type: object type: object
type: UniLabJsonCommand 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: string
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
get_methods: get_methods:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -112,21 +86,18 @@ zhida_gcms:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -144,21 +115,18 @@ zhida_gcms:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -176,21 +144,18 @@ zhida_gcms:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -203,26 +168,25 @@ zhida_gcms:
goal: {} goal: {}
goal_default: {} goal_default: {}
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Feedback title: EmptyIn_Feedback
type: object type: object
goal: goal:
properties: {} additionalProperties: true
required: []
title: EmptyIn_Goal title: EmptyIn_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
required:
- return_info
title: EmptyIn_Result title: EmptyIn_Result
type: object type: object
required: required:
@@ -234,35 +198,35 @@ zhida_gcms:
feedback: {} feedback: {}
goal: goal:
string: string string: string
text: text
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:
@@ -273,36 +237,36 @@ zhida_gcms:
start_with_csv_file: start_with_csv_file:
feedback: {} feedback: {}
goal: goal:
csv_file_path: csv_file_path
string: string string: string
goal_default: goal_default:
string: '' string: ''
handles: {} handles: {}
result: {} placeholder_keys: {}
result:
return_info: return_info
success: success
schema: schema:
description: '' description: ''
properties: properties:
feedback: feedback:
properties: {} additionalProperties: true
required: []
title: StrSingleInput_Feedback title: StrSingleInput_Feedback
type: object type: object
goal: goal:
additionalProperties: false
properties: properties:
string: string:
type: string type: string
required:
- string
title: StrSingleInput_Goal title: StrSingleInput_Goal
type: object type: object
result: result:
additionalProperties: false
properties: properties:
return_info: return_info:
type: string type: string
success: success:
type: boolean type: boolean
required:
- return_info
- success
title: StrSingleInput_Result title: StrSingleInput_Result
type: object type: object
required: required:
@@ -343,8 +307,8 @@ zhida_gcms:
version: version:
type: object type: object
required: required:
- status
- methods - methods
- status
- version - version
type: object type: object
version: 1.0.0 version: 1.0.0

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,6 @@ YB_20ml_fenyeping:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_5ml_fenyeping: YB_5ml_fenyeping:
category: category:
@@ -22,7 +21,6 @@ YB_5ml_fenyeping:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_jia_yang_tou_da: YB_jia_yang_tou_da:
category: category:
@@ -35,7 +33,6 @@ YB_jia_yang_tou_da:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_pei_ye_da_Bottle: YB_pei_ye_da_Bottle:
category: category:
@@ -48,7 +45,6 @@ YB_pei_ye_da_Bottle:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_pei_ye_xiao_Bottle: YB_pei_ye_xiao_Bottle:
category: category:
@@ -61,7 +57,6 @@ YB_pei_ye_xiao_Bottle:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_qiang_tou: YB_qiang_tou:
category: category:
@@ -74,7 +69,6 @@ YB_qiang_tou:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_ye_Bottle: YB_ye_Bottle:
category: category:
@@ -88,5 +82,4 @@ YB_ye_Bottle:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -9,7 +9,6 @@ YB_100ml_yeti:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_20ml_fenyepingban: YB_20ml_fenyepingban:
category: category:
@@ -22,7 +21,6 @@ YB_20ml_fenyepingban:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_5ml_fenyepingban: YB_5ml_fenyepingban:
category: category:
@@ -35,7 +33,6 @@ YB_5ml_fenyepingban:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_6StockCarrier: YB_6StockCarrier:
category: category:
@@ -48,7 +45,6 @@ YB_6StockCarrier:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_6VialCarrier: YB_6VialCarrier:
category: category:
@@ -61,7 +57,6 @@ YB_6VialCarrier:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_gao_nian_ye_Bottle: YB_gao_nian_ye_Bottle:
category: category:
@@ -74,7 +69,6 @@ YB_gao_nian_ye_Bottle:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_gaonianye: YB_gaonianye:
category: category:
@@ -87,7 +81,6 @@ YB_gaonianye:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_jia_yang_tou_da_Carrier: YB_jia_yang_tou_da_Carrier:
category: category:
@@ -100,7 +93,6 @@ YB_jia_yang_tou_da_Carrier:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_peiyepingdaban: YB_peiyepingdaban:
category: category:
@@ -113,7 +105,6 @@ YB_peiyepingdaban:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_peiyepingxiaoban: YB_peiyepingxiaoban:
category: category:
@@ -126,7 +117,6 @@ YB_peiyepingxiaoban:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_qiang_tou_he: YB_qiang_tou_he:
category: category:
@@ -139,7 +129,6 @@ YB_qiang_tou_he:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_shi_pei_qi_kuai: YB_shi_pei_qi_kuai:
category: category:
@@ -152,7 +141,6 @@ YB_shi_pei_qi_kuai:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_ye: YB_ye:
category: category:
@@ -165,7 +153,6 @@ YB_ye:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
YB_ye_100ml_Bottle: YB_ye_100ml_Bottle:
category: category:
@@ -178,5 +165,4 @@ YB_ye_100ml_Bottle:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -8,7 +8,6 @@ BIOYOND_PolymerStation_1BottleCarrier:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
BIOYOND_PolymerStation_1FlaskCarrier: BIOYOND_PolymerStation_1FlaskCarrier:
category: category:
@@ -20,7 +19,6 @@ BIOYOND_PolymerStation_1FlaskCarrier:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
BIOYOND_PolymerStation_6StockCarrier: BIOYOND_PolymerStation_6StockCarrier:
category: category:
@@ -32,7 +30,6 @@ BIOYOND_PolymerStation_6StockCarrier:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
BIOYOND_PolymerStation_8StockCarrier: BIOYOND_PolymerStation_8StockCarrier:
category: category:
@@ -44,5 +41,4 @@ BIOYOND_PolymerStation_8StockCarrier:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -8,7 +8,6 @@ BIOYOND_PolymerPreparationStation_Deck:
handles: [] handles: []
icon: 配液站.webp icon: 配液站.webp
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
BIOYOND_PolymerReactionStation_Deck: BIOYOND_PolymerReactionStation_Deck:
category: category:
@@ -20,7 +19,6 @@ BIOYOND_PolymerReactionStation_Deck:
handles: [] handles: []
icon: 反应站.webp icon: 反应站.webp
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
BIOYOND_YB_Deck: BIOYOND_YB_Deck:
category: category:
@@ -32,7 +30,6 @@ BIOYOND_YB_Deck:
handles: [] handles: []
icon: 配液站.webp icon: 配液站.webp
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
CoincellDeck: CoincellDeck:
category: category:
@@ -44,5 +41,4 @@ CoincellDeck:
handles: [] handles: []
icon: koudian.webp icon: koudian.webp
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -1,24 +1,3 @@
disposal:
category:
- disposal
- waste
- resource_container
class:
module: unilabos.resources.disposal:Disposal
type: unilabos
description: 废料处理位置,用于处理实验废料
handles:
- data_key: disposal_access
data_source: handle
data_type: fluid
handler_key: access
io_type: target
label: access
side: NORTH
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
hplc_plate: hplc_plate:
category: category:
- resource_container - resource_container
@@ -40,56 +19,6 @@ hplc_plate:
- 3.1416 - 3.1416
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/hplc_plate/modal.xacro path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/hplc_plate/modal.xacro
type: resource type: resource
registry_type: resource
version: 1.0.0
maintenance:
category:
- maintenance
- position
- resource_container
class:
module: unilabos.resources.maintenance:Maintenance
type: unilabos
description: 维护位置,用于设备维护和校准
handles:
- data_key: maintenance_access
data_source: handle
data_type: mechanical
handler_key: access
io_type: target
label: access
side: NORTH
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
plate:
category:
- plate
- labware
- resource_container
class:
module: unilabos.resources.plate:Plate
type: unilabos
description: 实验板,用于放置样品和试剂
handles:
- data_key: plate_access
data_source: handle
data_type: mechanical
handler_key: access
io_type: target
label: access
side: NORTH
- data_key: sample_wells
data_source: handle
data_type: fluid
handler_key: wells
io_type: target
label: wells
side: CENTER
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
plate_96: plate_96:
category: category:
@@ -112,7 +41,6 @@ plate_96:
- 0 - 0
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96/modal.xacro path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96/modal.xacro
type: resource type: resource
registry_type: resource
version: 1.0.0 version: 1.0.0
plate_96_high: plate_96_high:
category: category:
@@ -135,35 +63,6 @@ plate_96_high:
- 1.5708 - 1.5708
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96_high/modal.xacro path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96_high/modal.xacro
type: resource type: resource
registry_type: resource
version: 1.0.0
tip_rack:
category:
- tip_rack
- labware
- resource_container
class:
module: unilabos.resources.tip_rack:TipRack
type: unilabos
description: 枪头架资源,用于存放和管理移液器枪头
handles:
- data_key: tip_access
data_source: handle
data_type: mechanical
handler_key: access
io_type: target
label: access
side: NORTH
- data_key: tip_pickup
data_source: handle
data_type: mechanical
handler_key: pickup
io_type: target
label: pickup
side: SOUTH
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
tiprack_96_high: tiprack_96_high:
category: category:
@@ -195,7 +94,6 @@ tiprack_96_high:
- 1.5708 - 1.5708
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_96_high/modal.xacro path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_96_high/modal.xacro
type: resource type: resource
registry_type: resource
version: 1.0.0 version: 1.0.0
tiprack_box: tiprack_box:
category: category:
@@ -227,5 +125,4 @@ tiprack_box:
- 0 - 0
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_box/modal.xacro path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_box/modal.xacro
type: resource type: resource
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -29,7 +29,6 @@ bottle_container:
- 0 - 0
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/bottle_container/modal.xacro path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/bottle_container/modal.xacro
type: resource type: resource
registry_type: resource
version: 1.0.0 version: 1.0.0
tube_container: tube_container:
category: category:
@@ -62,5 +61,4 @@ tube_container:
- 0 - 0
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tube_container/modal.xacro path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tube_container/modal.xacro
type: resource type: resource
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -12,5 +12,4 @@ TransformXYZDeck:
mesh: liquid_transform_xyz mesh: liquid_transform_xyz
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/liquid_transform_xyz/macro_device.xacro path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/liquid_transform_xyz/macro_device.xacro
type: device type: device
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -12,7 +12,6 @@ OTDeck:
mesh: opentrons_liquid_handler mesh: opentrons_liquid_handler
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/opentrons_liquid_handler/macro_device.xacro path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/opentrons_liquid_handler/macro_device.xacro
type: device type: device
registry_type: resource
version: 1.0.0 version: 1.0.0
hplc_station: hplc_station:
category: category:
@@ -28,5 +27,4 @@ hplc_station:
mesh: hplc_station mesh: hplc_station
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/hplc_station/macro_device.xacro path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/hplc_station/macro_device.xacro
type: device type: device
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -8,5 +8,4 @@ Opentrons_96_adapter_Vb:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -8,7 +8,6 @@ appliedbiosystemsmicroamp_384_wellplate_40ul:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
biorad_384_wellplate_50ul: biorad_384_wellplate_50ul:
category: category:
@@ -20,7 +19,6 @@ biorad_384_wellplate_50ul:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
biorad_96_wellplate_200ul_pcr: biorad_96_wellplate_200ul_pcr:
category: category:
@@ -32,7 +30,6 @@ biorad_96_wellplate_200ul_pcr:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
corning_12_wellplate_6point9ml_flat: corning_12_wellplate_6point9ml_flat:
category: category:
@@ -44,7 +41,6 @@ corning_12_wellplate_6point9ml_flat:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
corning_24_wellplate_3point4ml_flat: corning_24_wellplate_3point4ml_flat:
category: category:
@@ -56,7 +52,6 @@ corning_24_wellplate_3point4ml_flat:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
corning_384_wellplate_112ul_flat: corning_384_wellplate_112ul_flat:
category: category:
@@ -68,7 +63,6 @@ corning_384_wellplate_112ul_flat:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
corning_48_wellplate_1point6ml_flat: corning_48_wellplate_1point6ml_flat:
category: category:
@@ -80,7 +74,6 @@ corning_48_wellplate_1point6ml_flat:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
corning_6_wellplate_16point8ml_flat: corning_6_wellplate_16point8ml_flat:
category: category:
@@ -92,7 +85,6 @@ corning_6_wellplate_16point8ml_flat:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
corning_96_wellplate_360ul_flat: corning_96_wellplate_360ul_flat:
category: category:
@@ -104,7 +96,6 @@ corning_96_wellplate_360ul_flat:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
nest_96_wellplate_100ul_pcr_full_skirt: nest_96_wellplate_100ul_pcr_full_skirt:
category: category:
@@ -136,7 +127,6 @@ nest_96_wellplate_100ul_pcr_full_skirt:
- 1.5708 - 1.5708
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
type: resource type: resource
registry_type: resource
version: 1.0.0 version: 1.0.0
nest_96_wellplate_200ul_flat: nest_96_wellplate_200ul_flat:
category: category:
@@ -148,7 +138,6 @@ nest_96_wellplate_200ul_flat:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
nest_96_wellplate_2ml_deep: nest_96_wellplate_2ml_deep:
category: category:
@@ -171,7 +160,6 @@ nest_96_wellplate_2ml_deep:
- 1.5708 - 1.5708
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
type: resource type: resource
registry_type: resource
version: 1.0.0 version: 1.0.0
thermoscientificnunc_96_wellplate_1300ul: thermoscientificnunc_96_wellplate_1300ul:
category: category:
@@ -183,7 +171,6 @@ thermoscientificnunc_96_wellplate_1300ul:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
thermoscientificnunc_96_wellplate_2000ul: thermoscientificnunc_96_wellplate_2000ul:
category: category:
@@ -195,7 +182,6 @@ thermoscientificnunc_96_wellplate_2000ul:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
usascientific_96_wellplate_2point4ml_deep: usascientific_96_wellplate_2point4ml_deep:
category: category:
@@ -207,5 +193,4 @@ usascientific_96_wellplate_2point4ml_deep:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -8,7 +8,6 @@ agilent_1_reservoir_290ml:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
axygen_1_reservoir_90ml: axygen_1_reservoir_90ml:
category: category:
@@ -20,7 +19,6 @@ axygen_1_reservoir_90ml:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
nest_12_reservoir_15ml: nest_12_reservoir_15ml:
category: category:
@@ -32,7 +30,6 @@ nest_12_reservoir_15ml:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
nest_1_reservoir_195ml: nest_1_reservoir_195ml:
category: category:
@@ -44,7 +41,6 @@ nest_1_reservoir_195ml:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
nest_1_reservoir_290ml: nest_1_reservoir_290ml:
category: category:
@@ -56,7 +52,6 @@ nest_1_reservoir_290ml:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
usascientific_12_reservoir_22ml: usascientific_12_reservoir_22ml:
category: category:
@@ -68,5 +63,4 @@ usascientific_12_reservoir_22ml:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -8,7 +8,6 @@ eppendorf_96_tiprack_1000ul_eptips:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
eppendorf_96_tiprack_10ul_eptips: eppendorf_96_tiprack_10ul_eptips:
category: category:
@@ -20,7 +19,6 @@ eppendorf_96_tiprack_10ul_eptips:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
geb_96_tiprack_1000ul: geb_96_tiprack_1000ul:
category: category:
@@ -32,7 +30,6 @@ geb_96_tiprack_1000ul:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
geb_96_tiprack_10ul: geb_96_tiprack_10ul:
category: category:
@@ -44,7 +41,6 @@ geb_96_tiprack_10ul:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_96_filtertiprack_1000ul: opentrons_96_filtertiprack_1000ul:
category: category:
@@ -75,7 +71,6 @@ opentrons_96_filtertiprack_1000ul:
- 1.5708 - 1.5708
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
type: resource type: resource
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_96_filtertiprack_10ul: opentrons_96_filtertiprack_10ul:
category: category:
@@ -87,7 +82,6 @@ opentrons_96_filtertiprack_10ul:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_96_filtertiprack_200ul: opentrons_96_filtertiprack_200ul:
category: category:
@@ -99,7 +93,6 @@ opentrons_96_filtertiprack_200ul:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_96_filtertiprack_20ul: opentrons_96_filtertiprack_20ul:
category: category:
@@ -111,7 +104,6 @@ opentrons_96_filtertiprack_20ul:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_96_tiprack_1000ul: opentrons_96_tiprack_1000ul:
category: category:
@@ -123,7 +115,6 @@ opentrons_96_tiprack_1000ul:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_96_tiprack_10ul: opentrons_96_tiprack_10ul:
category: category:
@@ -135,7 +126,6 @@ opentrons_96_tiprack_10ul:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_96_tiprack_20ul: opentrons_96_tiprack_20ul:
category: category:
@@ -147,7 +137,6 @@ opentrons_96_tiprack_20ul:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_96_tiprack_300ul: opentrons_96_tiprack_300ul:
category: category:

View File

@@ -8,7 +8,6 @@ opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic: opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic:
category: category:
@@ -20,7 +19,6 @@ opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_10_tuberack_nest_4x50ml_6x15ml_conical: opentrons_10_tuberack_nest_4x50ml_6x15ml_conical:
category: category:
@@ -32,7 +30,6 @@ opentrons_10_tuberack_nest_4x50ml_6x15ml_conical:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_15_tuberack_falcon_15ml_conical: opentrons_15_tuberack_falcon_15ml_conical:
category: category:
@@ -44,7 +41,6 @@ opentrons_15_tuberack_falcon_15ml_conical:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_15_tuberack_nest_15ml_conical: opentrons_15_tuberack_nest_15ml_conical:
category: category:
@@ -56,7 +52,6 @@ opentrons_15_tuberack_nest_15ml_conical:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_24_aluminumblock_generic_2ml_screwcap: opentrons_24_aluminumblock_generic_2ml_screwcap:
category: category:
@@ -68,7 +63,6 @@ opentrons_24_aluminumblock_generic_2ml_screwcap:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_24_aluminumblock_nest_1point5ml_snapcap: opentrons_24_aluminumblock_nest_1point5ml_snapcap:
category: category:
@@ -80,7 +74,6 @@ opentrons_24_aluminumblock_nest_1point5ml_snapcap:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap: opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap:
category: category:
@@ -92,7 +85,6 @@ opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap: opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap:
category: category:
@@ -104,7 +96,6 @@ opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic: opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic:
category: category:
@@ -116,7 +107,6 @@ opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic: opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic:
category: category:
@@ -128,7 +118,6 @@ opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_24_tuberack_generic_2ml_screwcap: opentrons_24_tuberack_generic_2ml_screwcap:
category: category:
@@ -140,7 +129,6 @@ opentrons_24_tuberack_generic_2ml_screwcap:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_24_tuberack_nest_0point5ml_screwcap: opentrons_24_tuberack_nest_0point5ml_screwcap:
category: category:
@@ -152,7 +140,6 @@ opentrons_24_tuberack_nest_0point5ml_screwcap:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_24_tuberack_nest_1point5ml_screwcap: opentrons_24_tuberack_nest_1point5ml_screwcap:
category: category:
@@ -164,7 +151,6 @@ opentrons_24_tuberack_nest_1point5ml_screwcap:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_24_tuberack_nest_1point5ml_snapcap: opentrons_24_tuberack_nest_1point5ml_snapcap:
category: category:
@@ -176,7 +162,6 @@ opentrons_24_tuberack_nest_1point5ml_snapcap:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_24_tuberack_nest_2ml_screwcap: opentrons_24_tuberack_nest_2ml_screwcap:
category: category:
@@ -188,7 +173,6 @@ opentrons_24_tuberack_nest_2ml_screwcap:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_24_tuberack_nest_2ml_snapcap: opentrons_24_tuberack_nest_2ml_snapcap:
category: category:
@@ -200,7 +184,6 @@ opentrons_24_tuberack_nest_2ml_snapcap:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_6_tuberack_falcon_50ml_conical: opentrons_6_tuberack_falcon_50ml_conical:
category: category:
@@ -212,7 +195,6 @@ opentrons_6_tuberack_falcon_50ml_conical:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_6_tuberack_nest_50ml_conical: opentrons_6_tuberack_nest_50ml_conical:
category: category:
@@ -224,7 +206,6 @@ opentrons_6_tuberack_nest_50ml_conical:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
opentrons_96_well_aluminum_block: opentrons_96_well_aluminum_block:
category: category:
@@ -236,5 +217,4 @@ opentrons_96_well_aluminum_block:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -29,5 +29,4 @@ container:
side: WEST side: WEST
icon: Flask.webp icon: Flask.webp
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -8,7 +8,6 @@ POST_PROCESS_Raw_1BottleCarrier:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
POST_PROCESS_Reaction_1BottleCarrier: POST_PROCESS_Reaction_1BottleCarrier:
category: category:
@@ -20,5 +19,4 @@ POST_PROCESS_Reaction_1BottleCarrier:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -9,5 +9,4 @@ post_process_deck:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -9,7 +9,6 @@ PRCXI_30mm_Adapter:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_Adapter: PRCXI_Adapter:
category: category:
@@ -22,7 +21,6 @@ PRCXI_Adapter:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_Deep10_Adapter: PRCXI_Deep10_Adapter:
category: category:
@@ -35,7 +33,6 @@ PRCXI_Deep10_Adapter:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_Deep300_Adapter: PRCXI_Deep300_Adapter:
category: category:
@@ -48,7 +45,6 @@ PRCXI_Deep300_Adapter:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_PCR_Adapter: PRCXI_PCR_Adapter:
category: category:
@@ -61,7 +57,6 @@ PRCXI_PCR_Adapter:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_Reservoir_Adapter: PRCXI_Reservoir_Adapter:
category: category:
@@ -74,7 +69,6 @@ PRCXI_Reservoir_Adapter:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_Tip10_Adapter: PRCXI_Tip10_Adapter:
category: category:
@@ -87,7 +81,6 @@ PRCXI_Tip10_Adapter:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_Tip1250_Adapter: PRCXI_Tip1250_Adapter:
category: category:
@@ -100,7 +93,6 @@ PRCXI_Tip1250_Adapter:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_Tip300_Adapter: PRCXI_Tip300_Adapter:
category: category:
@@ -113,5 +105,4 @@ PRCXI_Tip300_Adapter:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -9,7 +9,6 @@ PRCXI_48_DeepWell:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_96_DeepWell: PRCXI_96_DeepWell:
category: category:
@@ -22,7 +21,6 @@ PRCXI_96_DeepWell:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_AGenBio_4_troughplate: PRCXI_AGenBio_4_troughplate:
category: category:
@@ -35,7 +33,6 @@ PRCXI_AGenBio_4_troughplate:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_BioER_96_wellplate: PRCXI_BioER_96_wellplate:
category: category:
@@ -48,7 +45,6 @@ PRCXI_BioER_96_wellplate:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_BioRad_384_wellplate: PRCXI_BioRad_384_wellplate:
category: category:
@@ -61,7 +57,6 @@ PRCXI_BioRad_384_wellplate:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_CellTreat_96_wellplate: PRCXI_CellTreat_96_wellplate:
category: category:
@@ -74,7 +69,6 @@ PRCXI_CellTreat_96_wellplate:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_PCR_Plate_200uL_nonskirted: PRCXI_PCR_Plate_200uL_nonskirted:
category: category:
@@ -87,7 +81,6 @@ PRCXI_PCR_Plate_200uL_nonskirted:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_PCR_Plate_200uL_semiskirted: PRCXI_PCR_Plate_200uL_semiskirted:
category: category:
@@ -100,7 +93,6 @@ PRCXI_PCR_Plate_200uL_semiskirted:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_PCR_Plate_200uL_skirted: PRCXI_PCR_Plate_200uL_skirted:
category: category:
@@ -113,7 +105,6 @@ PRCXI_PCR_Plate_200uL_skirted:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_nest_12_troughplate: PRCXI_nest_12_troughplate:
category: category:
@@ -126,7 +117,6 @@ PRCXI_nest_12_troughplate:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_nest_1_troughplate: PRCXI_nest_1_troughplate:
category: category:
@@ -139,5 +129,4 @@ PRCXI_nest_1_troughplate:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -9,7 +9,6 @@ PRCXI_1000uL_Tips:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_10uL_Tips: PRCXI_10uL_Tips:
category: category:
@@ -22,7 +21,6 @@ PRCXI_10uL_Tips:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_10ul_eTips: PRCXI_10ul_eTips:
category: category:
@@ -35,7 +33,6 @@ PRCXI_10ul_eTips:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_1250uL_Tips: PRCXI_1250uL_Tips:
category: category:
@@ -48,7 +45,6 @@ PRCXI_1250uL_Tips:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_200uL_Tips: PRCXI_200uL_Tips:
category: category:
@@ -61,7 +57,6 @@ PRCXI_200uL_Tips:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0
PRCXI_300ul_Tips: PRCXI_300ul_Tips:
category: category:
@@ -74,5 +69,4 @@ PRCXI_300ul_Tips:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -9,5 +9,4 @@ PRCXI_trash:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

View File

@@ -9,5 +9,4 @@ PRCXI_EP_Adapter:
handles: [] handles: []
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
registry_type: resource
version: 1.0.0 version: 1.0.0

724
unilabos/registry/utils.py Normal file
View File

@@ -0,0 +1,724 @@
"""
注册表工具函数
从 registry.py 中提取的纯工具函数,包括:
- docstring 解析
- 类型字符串 → JSON Schema 转换
- AST 类型节点解析
- TypedDict / Slot / Handle 等辅助检测
"""
import inspect
import logging
import re
import typing
from typing import Any, Dict, List, Optional, Tuple, Union
from msgcenterpy.instances.typed_dict_instance import TypedDictMessageInstance
from unilabos.utils.cls_creator import import_class
_logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# 异常
# ---------------------------------------------------------------------------
class ROSMsgNotFound(Exception):
pass
# ---------------------------------------------------------------------------
# Docstring 解析 (Google-style)
# ---------------------------------------------------------------------------
_SECTION_RE = re.compile(r"^(\w[\w\s]*):\s*$")
def parse_docstring(docstring: Optional[str]) -> Dict[str, Any]:
"""
解析 Google-style docstring提取描述和参数说明。
Returns:
{"description": "短描述", "params": {"param1": "参数1描述", ...}}
"""
result: Dict[str, Any] = {"description": "", "params": {}}
if not docstring:
return result
lines = docstring.strip().splitlines()
if not lines:
return result
result["description"] = lines[0].strip()
in_args = False
current_param: Optional[str] = None
current_desc_parts: list = []
for line in lines[1:]:
stripped = line.strip()
section_match = _SECTION_RE.match(stripped)
if section_match:
if current_param is not None:
result["params"][current_param] = "\n".join(current_desc_parts).strip()
current_param = None
current_desc_parts = []
section_name = section_match.group(1).lower()
in_args = section_name in ("args", "arguments", "parameters", "params")
continue
if not in_args:
continue
if ":" in stripped and not stripped.startswith(" "):
if current_param is not None:
result["params"][current_param] = "\n".join(current_desc_parts).strip()
param_part, _, desc_part = stripped.partition(":")
param_name = param_part.strip().split("(")[0].strip()
current_param = param_name
current_desc_parts = [desc_part.strip()]
elif current_param is not None:
aline = line
if aline.startswith(" "):
aline = aline[4:]
elif aline.startswith("\t"):
aline = aline[1:]
current_desc_parts.append(aline.strip())
if current_param is not None:
result["params"][current_param] = "\n".join(current_desc_parts).strip()
return result
# ---------------------------------------------------------------------------
# 类型常量
# ---------------------------------------------------------------------------
SIMPLE_TYPE_MAP = {
"str": "string",
"string": "string",
"int": "integer",
"integer": "integer",
"float": "number",
"number": "number",
"bool": "boolean",
"boolean": "boolean",
"list": "array",
"array": "array",
"dict": "object",
"object": "object",
}
ARRAY_TYPES = {"list", "List", "tuple", "Tuple", "set", "Set", "Sequence", "Iterable"}
OBJECT_TYPES = {"dict", "Dict", "Mapping"}
WRAPPER_TYPES = {"Optional"}
SLOT_TYPES = {"ResourceSlot", "DeviceSlot"}
# ---------------------------------------------------------------------------
# 简单类型映射
# ---------------------------------------------------------------------------
def get_json_schema_type(type_str: str) -> str:
"""简单类型名 -> JSON Schema type"""
return SIMPLE_TYPE_MAP.get(type_str.lower(), "string")
# ---------------------------------------------------------------------------
# AST 类型解析
# ---------------------------------------------------------------------------
def parse_type_node(type_str: str):
"""将类型注解字符串解析为 AST 节点,失败返回 None。"""
import ast as _ast
try:
return _ast.parse(type_str.strip(), mode="eval").body
except Exception:
return None
def _collect_bitor(node, out: list):
"""递归收集 X | Y | Z 的所有分支。"""
import ast as _ast
if isinstance(node, _ast.BinOp) and isinstance(node.op, _ast.BitOr):
_collect_bitor(node.left, out)
_collect_bitor(node.right, out)
else:
out.append(node)
def type_node_to_schema(
node,
import_map: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
"""将 AST 类型注解节点递归转换为 JSON Schema dict。
当提供 import_map 时,对于未知类名会尝试通过 import_map 解析模块路径,
然后 import 真实类型对象来生成 schema (支持 TypedDict 等)。
映射规则:
- Optional[X] → X 的 schema (剥掉 Optional)
- Union[X, Y] → {"anyOf": [X_schema, Y_schema]}
- List[X] / Tuple[X] / Set[X] → {"type": "array", "items": X_schema}
- Dict[K, V] → {"type": "object", "additionalProperties": V_schema}
- Literal["a", "b"] → {"type": "string", "enum": ["a", "b"]}
- TypedDict (via import_map) → {"type": "object", "properties": {...}}
- 基本类型 str/int/... → {"type": "string"/"integer"/...}
"""
import ast as _ast
# --- Name 节点: str / int / dict / ResourceSlot / 自定义类 ---
if isinstance(node, _ast.Name):
name = node.id
if name in SLOT_TYPES:
return {"$slot": name}
json_type = SIMPLE_TYPE_MAP.get(name.lower())
if json_type:
return {"type": json_type}
# 尝试通过 import_map 解析并 import 真实类型
if import_map and name in import_map:
type_obj = resolve_type_object(import_map[name])
if type_obj is not None:
return type_to_schema(type_obj)
# 未知类名 → 无法转 schema 的自定义类型默认当 object
return {"type": "object"}
if isinstance(node, _ast.Constant):
if isinstance(node.value, str):
return {"type": SIMPLE_TYPE_MAP.get(node.value.lower(), "string")}
return {"type": "string"}
# --- Subscript 节点: List[X], Dict[K,V], Optional[X], Literal[...] 等 ---
if isinstance(node, _ast.Subscript):
base_name = node.value.id if isinstance(node.value, _ast.Name) else ""
# Optional[X] → 剥掉
if base_name in WRAPPER_TYPES:
return type_node_to_schema(node.slice, import_map)
# Union[X, None] → 剥掉 None; Union[X, Y] → anyOf
if base_name == "Union":
elts = node.slice.elts if isinstance(node.slice, _ast.Tuple) else [node.slice]
non_none = [
e
for e in elts
if not (isinstance(e, _ast.Constant) and e.value is None)
and not (isinstance(e, _ast.Name) and e.id == "None")
]
if len(non_none) == 1:
return type_node_to_schema(non_none[0], import_map)
if len(non_none) > 1:
return {"anyOf": [type_node_to_schema(e, import_map) for e in non_none]}
return {"type": "string"}
# Literal["a", "b", 1] → enum
if base_name == "Literal":
elts = node.slice.elts if isinstance(node.slice, _ast.Tuple) else [node.slice]
values = []
for e in elts:
if isinstance(e, _ast.Constant):
values.append(e.value)
elif isinstance(e, _ast.Name):
values.append(e.id)
if values:
return {"type": "string", "enum": values}
return {"type": "string"}
# List / Tuple / Set → array
if base_name in ARRAY_TYPES:
if isinstance(node.slice, _ast.Tuple) and node.slice.elts:
inner_node = node.slice.elts[0]
else:
inner_node = node.slice
return {"type": "array", "items": type_node_to_schema(inner_node, import_map)}
# Dict → object
if base_name in OBJECT_TYPES:
schema: Dict[str, Any] = {"type": "object"}
if isinstance(node.slice, _ast.Tuple) and len(node.slice.elts) >= 2:
val_node = node.slice.elts[1]
# Dict[str, Any] → 不加 additionalProperties (Any 等同于无约束)
is_any = (isinstance(val_node, _ast.Name) and val_node.id == "Any") or (
isinstance(val_node, _ast.Constant) and val_node.value is None
)
if not is_any:
val_schema = type_node_to_schema(val_node, import_map)
schema["additionalProperties"] = val_schema
return schema
# --- BinOp: X | Y (Python 3.10+) → 当 Union 处理 ---
if isinstance(node, _ast.BinOp) and isinstance(node.op, _ast.BitOr):
parts: list = []
_collect_bitor(node, parts)
non_none = [
p
for p in parts
if not (isinstance(p, _ast.Constant) and p.value is None)
and not (isinstance(p, _ast.Name) and p.id == "None")
]
if len(non_none) == 1:
return type_node_to_schema(non_none[0], import_map)
if len(non_none) > 1:
return {"anyOf": [type_node_to_schema(p, import_map) for p in non_none]}
return {"type": "string"}
return {"type": "string"}
# ---------------------------------------------------------------------------
# 真实类型对象解析 (import-based)
# ---------------------------------------------------------------------------
def resolve_type_object(type_ref: str) -> Optional[Any]:
"""通过 'module.path:ClassName' 格式的引用 import 并返回真实类型对象。
对于 typing 内置名 (str, int, List 等) 直接返回 None (由 AST 路径处理)。
import 失败时静默返回 None。
"""
if ":" not in type_ref:
return None
try:
return import_class(type_ref)
except Exception:
return None
def is_typed_dict_class(obj: Any) -> bool:
"""检查对象是否是 TypedDict 类。"""
if obj is None:
return False
try:
from typing_extensions import is_typeddict
return is_typeddict(obj)
except ImportError:
if isinstance(obj, type):
return hasattr(obj, "__required_keys__") and hasattr(obj, "__optional_keys__")
return False
def type_to_schema(tp: Any) -> Dict[str, Any]:
"""将真实 typing 对象递归转换为 JSON Schema dict。
支持:
- 基本类型: str, int, float, bool → {"type": "string"/"integer"/...}
- typing 泛型: List[X], Dict[K,V], Optional[X], Union[X,Y], Literal[...]
- TypedDict → {"type": "object", "properties": {...}, "required": [...]}
- 自定义类 (ResourceSlot 等) → {"$slot": "..."} 或 {"type": "string"}
"""
origin = getattr(tp, "__origin__", None)
args = getattr(tp, "__args__", None)
# --- None / NoneType ---
if tp is type(None):
return {"type": "null"}
# --- 基本类型 ---
if tp is str:
return {"type": "string"}
if tp is int:
return {"type": "integer"}
if tp is float:
return {"type": "number"}
if tp is bool:
return {"type": "boolean"}
# --- TypedDict ---
if is_typed_dict_class(tp):
try:
return TypedDictMessageInstance.get_json_schema_from_typed_dict(tp)
except Exception:
return {"type": "object"}
# --- Literal ---
if origin is typing.Literal:
values = list(args) if args else []
return {"type": "string", "enum": values}
# --- Optional / Union ---
if origin is typing.Union:
non_none = [a for a in (args or ()) if a is not type(None)]
if len(non_none) == 1:
return type_to_schema(non_none[0])
if len(non_none) > 1:
return {"anyOf": [type_to_schema(a) for a in non_none]}
return {"type": "string"}
# --- List / Sequence / Set / Tuple / Iterable ---
if origin in (list, tuple, set, frozenset) or (
origin is not None
and getattr(origin, "__name__", "") in ("Sequence", "Iterable", "Iterator", "MutableSequence")
):
if args:
return {"type": "array", "items": type_to_schema(args[0])}
return {"type": "array"}
# --- Dict / Mapping ---
if origin in (dict,) or (origin is not None and getattr(origin, "__name__", "") in ("Mapping", "MutableMapping")):
schema: Dict[str, Any] = {"type": "object"}
if args and len(args) >= 2:
schema["additionalProperties"] = type_to_schema(args[1])
return schema
# --- Slot 类型 ---
if isinstance(tp, type):
name = tp.__name__
if name in SLOT_TYPES:
return {"$slot": name}
# --- 其他未知类型 fallback ---
if isinstance(tp, type):
return {"type": "object"}
return {"type": "string"}
# ---------------------------------------------------------------------------
# Slot / Placeholder 检测
# ---------------------------------------------------------------------------
def detect_slot_type(ptype) -> Tuple[Optional[str], bool]:
"""检测参数类型是否为 ResourceSlot / DeviceSlot。
兼容多种格式:
- runtime: "unilabos.registry.placeholder_type:ResourceSlot"
- runtime tuple: ("list", "unilabos.registry.placeholder_type:ResourceSlot")
- AST 裸名: "ResourceSlot", "List[ResourceSlot]", "Optional[ResourceSlot]"
Returns: (slot_name | None, is_list)
"""
ptype_str = str(ptype)
# 快速路径: 字符串里根本没有 Slot
if "ResourceSlot" not in ptype_str and "DeviceSlot" not in ptype_str:
return (None, False)
# runtime 格式: 完整模块路径
if isinstance(ptype, str):
if ptype.endswith(":ResourceSlot") or ptype == "ResourceSlot":
return ("ResourceSlot", False)
if ptype.endswith(":DeviceSlot") or ptype == "DeviceSlot":
return ("DeviceSlot", False)
# AST 复杂格式: List[ResourceSlot], Optional[ResourceSlot] 等
if "[" in ptype:
node = parse_type_node(ptype)
if node is not None:
schema = type_node_to_schema(node)
# 直接是 slot
if "$slot" in schema:
return (schema["$slot"], False)
# array 包裹 slot: {"type": "array", "items": {"$slot": "..."}}
items = schema.get("items", {})
if isinstance(items, dict) and "$slot" in items:
return (items["$slot"], True)
return (None, False)
# runtime tuple 格式
if isinstance(ptype, tuple) and len(ptype) == 2:
inner_str = str(ptype[1])
if "ResourceSlot" in inner_str:
return ("ResourceSlot", True)
if "DeviceSlot" in inner_str:
return ("DeviceSlot", True)
return (None, False)
def detect_placeholder_keys(params: list) -> Dict[str, str]:
"""Detect parameters that reference ResourceSlot or DeviceSlot."""
result: Dict[str, str] = {}
for p in params:
ptype = p.get("type", "")
if "ResourceSlot" in str(ptype):
result[p["name"]] = "unilabos_resources"
elif "DeviceSlot" in str(ptype):
result[p["name"]] = "unilabos_devices"
return result
# ---------------------------------------------------------------------------
# Handle 规范化
# ---------------------------------------------------------------------------
def normalize_ast_handles(handles_raw: Any) -> List[Dict[str, Any]]:
"""Convert AST-parsed handle structures to the standard registry format."""
if not handles_raw:
return []
# handle_type → io_type 映射 (AST 内部类名 → YAML 标准字段值)
_HANDLE_TYPE_TO_IO_TYPE = {
"input": "target",
"output": "source",
"action_input": "action_target",
"action_output": "action_source",
}
result: List[Dict[str, Any]] = []
for h in handles_raw:
if isinstance(h, dict):
call = h.get("_call", "")
if "InputHandle" in call:
handle_type = "input"
elif "OutputHandle" in call:
handle_type = "output"
elif "ActionInputHandle" in call:
handle_type = "action_input"
elif "ActionOutputHandle" in call:
handle_type = "action_output"
else:
handle_type = h.get("handle_type", "unknown")
io_type = _HANDLE_TYPE_TO_IO_TYPE.get(handle_type, handle_type)
entry: Dict[str, Any] = {
"handler_key": h.get("key", ""),
"data_type": h.get("data_type", ""),
"io_type": io_type,
}
side = h.get("side")
if side:
if isinstance(side, str) and "." in side:
val = side.rsplit(".", 1)[-1]
side = val.lower() if val in ("LEFT", "RIGHT", "TOP", "BOTTOM") else val
entry["side"] = side
label = h.get("label")
if label:
entry["label"] = label
data_key = h.get("data_key")
if data_key:
entry["data_key"] = data_key
data_source = h.get("data_source")
if data_source:
if isinstance(data_source, str) and "." in data_source:
val = data_source.rsplit(".", 1)[-1]
data_source = val.lower() if val in ("HANDLE", "EXECUTOR") else val
entry["data_source"] = data_source
description = h.get("description")
if description:
entry["description"] = description
result.append(entry)
return result
def normalize_ast_action_handles(handles_raw: Any) -> Dict[str, Any]:
"""Convert AST-parsed action handle list to {"input": [...], "output": [...]}.
Mirrors the runtime behavior of decorators._action_handles_to_dict:
- ActionInputHandle => grouped under "input"
- ActionOutputHandle => grouped under "output"
Field mapping: key -> handler_key (matches Pydantic serialization_alias).
"""
if not handles_raw or not isinstance(handles_raw, list):
return {}
input_list: List[Dict[str, Any]] = []
output_list: List[Dict[str, Any]] = []
for h in handles_raw:
if not isinstance(h, dict):
continue
call = h.get("_call", "")
is_input = "ActionInputHandle" in call or "InputHandle" in call
is_output = "ActionOutputHandle" in call or "OutputHandle" in call
entry: Dict[str, Any] = {
"handler_key": h.get("key", ""),
"data_type": h.get("data_type", ""),
"label": h.get("label", ""),
}
for opt_key in ("side", "data_key", "data_source", "description", "io_type"):
val = h.get(opt_key)
if val is not None:
# Only resolve enum-style refs (e.g. DataSource.HANDLE -> handle) for data_source/side
# data_key values like "wells.@flatten", "@this.0@@@plate" must be preserved as-is
if (
isinstance(val, str)
and "." in val
and opt_key not in ("io_type", "data_key")
):
val = val.rsplit(".", 1)[-1].lower()
entry[opt_key] = val
# io_type: only add when explicitly set; do not default output to "sink" (YAML convention omits it)
if "io_type" not in entry and is_input:
entry["io_type"] = "source"
if is_input:
input_list.append(entry)
elif is_output:
output_list.append(entry)
result: Dict[str, Any] = {}
if input_list:
result["input"] = input_list
# Always include output (empty list when no outputs) to match YAML
result["output"] = output_list
return result
# ---------------------------------------------------------------------------
# Schema 辅助
# ---------------------------------------------------------------------------
def wrap_action_schema(
goal_schema: Dict[str, Any],
action_name: str,
description: str = "",
result_schema: Optional[Dict[str, Any]] = None,
feedback_schema: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
将 goal 参数 schema 包装为标准的 action schema 格式:
{ "properties": { "goal": ..., "feedback": ..., "result": ... }, ... }
"""
# 去掉 auto- 前缀用于 title/description与 YAML 路径保持一致
display_name = action_name.removeprefix("auto-")
return {
"title": f"{display_name}参数",
"description": description or f"{display_name}的参数schema",
"type": "object",
"properties": {
"goal": goal_schema,
"feedback": feedback_schema or {},
"result": result_schema or {},
},
"required": ["goal"],
}
def preserve_field_descriptions(new_schema: Dict[str, Any], prev_schema: Dict[str, Any]):
"""递归保留之前 schema 中各字段的 description / title。
覆盖顶层以及嵌套 properties如 goal.properties.xxx.description
"""
if not prev_schema or not new_schema:
return
prev_props = prev_schema.get("properties", {})
new_props = new_schema.get("properties", {})
for field_name, prev_field in prev_props.items():
if field_name not in new_props:
continue
new_field = new_props[field_name]
if not isinstance(prev_field, dict) or not isinstance(new_field, dict):
continue
if "title" in prev_field:
new_field.setdefault("title", prev_field["title"])
if "description" in prev_field:
new_field.setdefault("description", prev_field["description"])
if "properties" in prev_field and "properties" in new_field:
preserve_field_descriptions(new_field, prev_field)
def strip_ros_descriptions(schema: Any):
"""递归清除 ROS schema 中自动生成的无意义 description含 rosidl_parser 内存地址)。"""
if isinstance(schema, dict):
desc = schema.get("description", "")
if isinstance(desc, str) and "rosidl_parser" in desc:
del schema["description"]
for v in schema.values():
strip_ros_descriptions(v)
elif isinstance(schema, list):
for item in schema:
strip_ros_descriptions(item)
# ---------------------------------------------------------------------------
# 深度对比
# ---------------------------------------------------------------------------
def _short(val, limit=120):
"""截断过长的值用于日志显示。"""
s = repr(val)
return s if len(s) <= limit else s[:limit] + "..."
def deep_diff(old, new, path="", max_depth=10) -> list:
"""递归对比两个对象,返回所有差异的描述列表。"""
diffs = []
if max_depth <= 0:
if old != new:
diffs.append(f"{path}: (达到最大深度) OLD≠NEW")
return diffs
if type(old) != type(new):
diffs.append(f"{path}: 类型不同 OLD={type(old).__name__}({_short(old)}) NEW={type(new).__name__}({_short(new)})")
return diffs
if isinstance(old, dict):
old_keys = set(old.keys())
new_keys = set(new.keys())
for k in sorted(new_keys - old_keys):
diffs.append(f"{path}.{k}: 新增字段 (AST有, YAML无) = {_short(new[k])}")
for k in sorted(old_keys - new_keys):
diffs.append(f"{path}.{k}: 缺失字段 (YAML有, AST无) = {_short(old[k])}")
for k in sorted(old_keys & new_keys):
diffs.extend(deep_diff(old[k], new[k], f"{path}.{k}", max_depth - 1))
elif isinstance(old, (list, tuple)):
if len(old) != len(new):
diffs.append(f"{path}: 列表长度不同 OLD={len(old)} NEW={len(new)}")
for i in range(min(len(old), len(new))):
diffs.extend(deep_diff(old[i], new[i], f"{path}[{i}]", max_depth - 1))
if len(new) > len(old):
for i in range(len(old), len(new)):
diffs.append(f"{path}[{i}]: 新增元素 = {_short(new[i])}")
elif len(old) > len(new):
for i in range(len(new), len(old)):
diffs.append(f"{path}[{i}]: 缺失元素 = {_short(old[i])}")
else:
if old != new:
diffs.append(f"{path}: OLD={_short(old)} NEW={_short(new)}")
return diffs
# ---------------------------------------------------------------------------
# MRO 方法参数解析
# ---------------------------------------------------------------------------
def resolve_method_params_via_import(module_str: str, method_name: str) -> Dict[str, str]:
"""当 AST 方法参数为空 (如 *args, **kwargs) 时, import class 并通过 MRO 获取真实方法参数.
返回 identity mapping {param_name: param_name}.
"""
if not module_str or ":" not in module_str:
return {}
try:
cls = import_class(module_str)
except Exception as e:
_logger.debug(f"[AST] resolve_method_params_via_import: import_class('{module_str}') failed: {e}")
return {}
try:
for base_cls in cls.__mro__:
if method_name not in base_cls.__dict__:
continue
method = base_cls.__dict__[method_name]
actual = getattr(method, "__wrapped__", method)
if isinstance(actual, (staticmethod, classmethod)):
actual = actual.__func__
if not callable(actual):
continue
sig = inspect.signature(actual, follow_wrapped=True)
params = [
p.name for p in sig.parameters.values()
if p.name not in ("self", "cls")
and p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
]
if params:
return {p: p for p in params}
except Exception as e:
_logger.debug(f"[AST] resolve_method_params_via_import: MRO walk for '{method_name}' failed: {e}")
return {}

View File

@@ -12,9 +12,11 @@ class RegularContainer(Container):
kwargs["size_y"] = 0 kwargs["size_y"] = 0
if "size_z" not in kwargs: if "size_z" not in kwargs:
kwargs["size_z"] = 0 kwargs["size_z"] = 0
if "category" not in kwargs:
kwargs["category"] = "container"
self.kwargs = kwargs self.kwargs = kwargs
super().__init__(*args, category="container", **kwargs) super().__init__(*args, **kwargs)
def load_state(self, state: Dict[str, Any]): def load_state(self, state: Dict[str, Any]):
super().load_state(state) super().load_state(state)

View File

@@ -76,7 +76,7 @@ def canonicalize_nodes_data(
if sample_id: if sample_id:
logger.error(f"{node}的sample_id参数已弃用sample_id: {sample_id}") logger.error(f"{node}的sample_id参数已弃用sample_id: {sample_id}")
for k in list(node.keys()): for k in list(node.keys()):
if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children", "pose", "extra"]: if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children", "pose", "extra", "machine_name"]:
v = node.pop(k) v = node.pop(k)
node["config"][k] = v node["config"][k] = v
if outer_host_node_id is not None: if outer_host_node_id is not None:
@@ -288,6 +288,15 @@ def read_node_link_json(
physical_setup_graph = nx.node_link_graph(graph_data, edges="links", multigraph=False) physical_setup_graph = nx.node_link_graph(graph_data, edges="links", multigraph=False)
handle_communications(physical_setup_graph) handle_communications(physical_setup_graph)
# Stamp machine_name on device trees only (resources are cloud-managed)
local_machine = BasicConfig.machine_name or "本地"
for tree in resource_tree_set.trees:
if tree.root_node.res_content.type != "device":
continue
for node in tree.get_all_nodes():
if not node.res_content.machine_name:
node.res_content.machine_name = local_machine
return physical_setup_graph, resource_tree_set, standardized_links return physical_setup_graph, resource_tree_set, standardized_links
@@ -372,6 +381,15 @@ def read_graphml(graphml_file: str) -> tuple[nx.Graph, ResourceTreeSet, List[Dic
physical_setup_graph = nx.node_link_graph(graph_data, link="links", multigraph=False) physical_setup_graph = nx.node_link_graph(graph_data, link="links", multigraph=False)
handle_communications(physical_setup_graph) handle_communications(physical_setup_graph)
# Stamp machine_name on device trees only (resources are cloud-managed)
local_machine = BasicConfig.machine_name or "本地"
for tree in resource_tree_set.trees:
if tree.root_node.res_content.type != "device":
continue
for node in tree.get_all_nodes():
if not node.res_content.machine_name:
node.res_content.machine_name = local_machine
return physical_setup_graph, resource_tree_set, standardized_links return physical_setup_graph, resource_tree_set, standardized_links

View File

@@ -120,6 +120,7 @@ class ResourceDictType(TypedDict):
config: Dict[str, Any] config: Dict[str, Any]
data: Dict[str, Any] data: Dict[str, Any]
extra: Dict[str, Any] extra: Dict[str, Any]
machine_name: str
# 统一的资源字典模型parent 自动序列化为 parent_uuidchildren 不序列化 # 统一的资源字典模型parent 自动序列化为 parent_uuidchildren 不序列化
@@ -141,6 +142,7 @@ class ResourceDict(BaseModel):
config: Dict[str, Any] = Field(description="Resource configuration") config: Dict[str, Any] = Field(description="Resource configuration")
data: Dict[str, Any] = Field(description="Resource data, eg: container liquid data") data: Dict[str, Any] = Field(description="Resource data, eg: container liquid data")
extra: Dict[str, Any] = Field(description="Extra data, eg: slot index") extra: Dict[str, Any] = Field(description="Extra data, eg: slot index")
machine_name: str = Field(description="Machine this resource belongs to", default="")
@field_serializer("parent_uuid") @field_serializer("parent_uuid")
def _serialize_parent(self, parent_uuid: Optional["ResourceDict"]): def _serialize_parent(self, parent_uuid: Optional["ResourceDict"]):
@@ -196,22 +198,30 @@ class ResourceDictInstance(object):
self.typ = "dict" self.typ = "dict"
@classmethod @classmethod
def get_resource_instance_from_dict(cls, content: Dict[str, Any]) -> "ResourceDictInstance": def get_resource_instance_from_dict(cls, content: ResourceDictType) -> "ResourceDictInstance":
"""从字典创建资源实例""" """从字典创建资源实例"""
if "id" not in content: if "id" not in content:
content["id"] = content["name"] content["id"] = content["name"]
if "uuid" not in content: if "uuid" not in content:
content["uuid"] = str(uuid.uuid4()) content["uuid"] = str(uuid.uuid4())
if "description" in content and content["description"] is None: if "description" in content and content["description"] is None:
# noinspection PyTypedDict
del content["description"] del content["description"]
if "model" in content and content["model"] is None: if "model" in content and content["model"] is None:
# noinspection PyTypedDict
del content["model"] del content["model"]
# noinspection PyTypedDict
if "schema" in content and content["schema"] is None: if "schema" in content and content["schema"] is None:
# noinspection PyTypedDict
del content["schema"] del content["schema"]
# noinspection PyTypedDict
if "x" in content.get("position", {}): if "x" in content.get("position", {}):
# 说明是老版本的position格式转换成新的 # 说明是老版本的position格式转换成新的
# noinspection PyTypedDict
content["position"] = {"position": content["position"]} content["position"] = {"position": content["position"]}
# noinspection PyTypedDict
if not content.get("class"): if not content.get("class"):
# noinspection PyTypedDict
content["class"] = "" content["class"] = ""
if not content.get("config"): # todo: 后续从后端保证字段非空 if not content.get("config"): # todo: 后续从后端保证字段非空
content["config"] = {} content["config"] = {}
@@ -222,16 +232,18 @@ class ResourceDictInstance(object):
if "position" in content: if "position" in content:
pose = content.get("pose", {}) pose = content.get("pose", {})
if "position" not in pose: if "position" not in pose:
# noinspection PyTypedDict
if "position" in content["position"]: if "position" in content["position"]:
# noinspection PyTypedDict
pose["position"] = content["position"]["position"] pose["position"] = content["position"]["position"]
else: else:
pose["position"] = {"x": 0, "y": 0, "z": 0} pose["position"] = ResourceDictPositionObjectType(x=0, y=0, z=0)
if "size" not in pose: if "size" not in pose:
pose["size"] = { pose["size"] = ResourceDictPositionSizeType(
"width": content["config"].get("size_x", 0), width= content["config"].get("size_x", 0),
"height": content["config"].get("size_y", 0), height= content["config"].get("size_y", 0),
"depth": content["config"].get("size_z", 0), depth= content["config"].get("size_z", 0),
} )
content["pose"] = pose content["pose"] = pose
try: try:
res_dict = ResourceDict.model_validate(content) res_dict = ResourceDict.model_validate(content)
@@ -399,7 +411,7 @@ class ResourceTreeSet(object):
) )
@classmethod @classmethod
def from_plr_resources(cls, resources: List["PLRResource"], known_newly_created=False) -> "ResourceTreeSet": def from_plr_resources(cls, resources: List["PLRResource"], known_newly_created=False, old_size=False) -> "ResourceTreeSet":
""" """
从plr资源创建ResourceTreeSet 从plr资源创建ResourceTreeSet
""" """
@@ -422,13 +434,20 @@ class ResourceTreeSet(object):
"resource_group": "resource_group", "resource_group": "resource_group",
"trash": "trash", "trash": "trash",
"plate_adapter": "plate_adapter", "plate_adapter": "plate_adapter",
"consumable": "consumable",
"tool": "tool",
"condenser": "condenser",
"crucible": "crucible",
"reagent_bottle": "reagent_bottle",
"flask": "flask",
"beaker": "beaker",
} }
if source in replace_info: if source in replace_info:
return replace_info[source] return replace_info[source]
elif source is None: elif source is None:
return "" return ""
else: else:
print("转换pylabrobot的时候出现未知类型", source) logger.trace(f"转换pylabrobot的时候出现未知类型 {source}")
return source return source
def build_uuid_mapping(res: "PLRResource", uuid_list: list, parent_uuid: Optional[str] = None): def build_uuid_mapping(res: "PLRResource", uuid_list: list, parent_uuid: Optional[str] = None):
@@ -483,7 +502,7 @@ class ResourceTreeSet(object):
k: v k: v
for k, v in d.items() for k, v in d.items()
if k if k
not in [ not in ([
"name", "name",
"children", "children",
"parent_name", "parent_name",
@@ -494,7 +513,15 @@ class ResourceTreeSet(object):
"size_z", "size_z",
"cross_section_type", "cross_section_type",
"bottom_type", "bottom_type",
] ] if not old_size else [
"name",
"children",
"parent_name",
"location",
"rotation",
"cross_section_type",
"bottom_type",
])
}, },
"data": states[d["name"]], "data": states[d["name"]],
"extra": extra, "extra": extra,
@@ -793,7 +820,8 @@ class ResourceTreeSet(object):
if remote_root_type == "device": if remote_root_type == "device":
# 情况1: 一级是 device # 情况1: 一级是 device
if remote_root_id not in local_device_map: if remote_root_id not in local_device_map:
logger.warning(f"Device '{remote_root_id}' 在本地不存在,跳过该 device 下的物料同步") if remote_root_id != "host_node":
logger.warning(f"Device '{remote_root_id}' 在本地不存在,跳过该 device 下的物料同步")
continue continue
local_device = local_device_map[remote_root_id] local_device = local_device_map[remote_root_id]
@@ -883,7 +911,7 @@ class ResourceTreeSet(object):
return self return self
def dump(self) -> List[List[Dict[str, Any]]]: def dump(self, old_position=False) -> List[List[Dict[str, Any]]]:
""" """
将 ResourceTreeSet 序列化为嵌套列表格式 将 ResourceTreeSet 序列化为嵌套列表格式
@@ -899,6 +927,10 @@ class ResourceTreeSet(object):
# 获取树的所有节点并序列化 # 获取树的所有节点并序列化
tree_nodes = [node.res_content.model_dump(by_alias=True) for node in tree.get_all_nodes()] tree_nodes = [node.res_content.model_dump(by_alias=True) for node in tree.get_all_nodes()]
result.append(tree_nodes) result.append(tree_nodes)
if old_position:
for r in result:
for rr in r:
rr["position"] = rr["pose"]["position"]
return result return result
@classmethod @classmethod

View File

@@ -11,6 +11,7 @@ from io import StringIO
from typing import Iterable, Any, Dict, Type, TypeVar, Union from typing import Iterable, Any, Dict, Type, TypeVar, Union
import yaml import yaml
from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
from pydantic import BaseModel from pydantic import BaseModel
from dataclasses import asdict, is_dataclass from dataclasses import asdict, is_dataclass
@@ -716,6 +717,19 @@ def ros_field_type_to_json_schema(
# return {'type': 'object', 'description': f'未知类型: {field_type}'} # return {'type': 'object', 'description': f'未知类型: {field_type}'}
def _strip_rosidl_descriptions(schema: Any) -> None:
"""递归清除 rosidl_parser 自动生成的无意义 description含内存地址"""
if isinstance(schema, dict):
desc = schema.get("description", "")
if isinstance(desc, str) and "rosidl_parser" in desc:
del schema["description"]
for v in schema.values():
_strip_rosidl_descriptions(v)
elif isinstance(schema, list):
for item in schema:
_strip_rosidl_descriptions(item)
def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any]: def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any]:
""" """
将 ROS 消息类转换为 JSON Schema 将 ROS 消息类转换为 JSON Schema
@@ -727,46 +741,10 @@ def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any
Returns: Returns:
对应的 JSON Schema 定义 对应的 JSON Schema 定义
""" """
schema = {"type": "object", "properties": {}, "required": []} schema = ROS2MessageInstance(msg_class()).get_json_schema()
# 优先使用字段名作为标题,否则使用类名
schema["title"] = field_name schema["title"] = field_name
schema.pop("description", None)
# 获取消息的字段和字段类型 _strip_rosidl_descriptions(schema)
try:
for ind, slot_info in enumerate(msg_class._fields_and_field_types.items()):
slot_name, slot_type = slot_info
type_info = msg_class.SLOT_TYPES[ind]
field_schema = ros_field_type_to_json_schema(type_info, slot_name)
schema["properties"][slot_name] = field_schema
schema["required"].append(slot_name)
# if hasattr(msg_class, 'get_fields_and_field_types'):
# fields_and_types = msg_class.get_fields_and_field_types()
#
# for field_name, field_type in fields_and_types.items():
# # 将 ROS 字段类型转换为 JSON Schema
# field_schema = ros_field_type_to_json_schema(field_type)
#
# schema['properties'][field_name] = field_schema
# schema['required'].append(field_name)
# elif hasattr(msg_class, '__slots__') and hasattr(msg_class, '_fields_and_field_types'):
# # 直接从实例属性获取
# for field_name in msg_class.__slots__:
# # 移除前导下划线(如果有)
# clean_name = field_name[1:] if field_name.startswith('_') else field_name
#
# # 从 _fields_and_field_types 获取类型
# if clean_name in msg_class._fields_and_field_types:
# field_type = msg_class._fields_and_field_types[clean_name]
# field_schema = ros_field_type_to_json_schema(field_type)
#
# schema['properties'][clean_name] = field_schema
# schema['required'].append(clean_name)
except Exception as e:
# 如果获取字段类型失败,添加错误信息
schema["description"] = f"解析消息字段时出错: {str(e)}"
logger.error(f"解析 {msg_class.__name__} 消息字段失败: {str(e)}")
return schema return schema
@@ -813,6 +791,8 @@ def ros_action_to_json_schema(
"required": ["goal"], "required": ["goal"],
} }
_strip_rosidl_descriptions(schema)
# 保留之前 schema 中 goal/feedback/result 下一级字段的 description # 保留之前 schema 中 goal/feedback/result 下一级字段的 description
if previous_schema: if previous_schema:
_preserve_field_descriptions(schema, previous_schema) _preserve_field_descriptions(schema, previous_schema)

View File

@@ -34,7 +34,8 @@ from unilabos_msgs.action import SendCmd
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
from unilabos.config.config import BasicConfig from unilabos.config.config import BasicConfig
from unilabos.utils.decorator import get_topic_config, get_all_subscriptions from unilabos.registry.decorators import get_topic_config
from unilabos.utils.decorator import get_all_subscriptions
from unilabos.resources.container import RegularContainer from unilabos.resources.container import RegularContainer
from unilabos.resources.graphio import ( from unilabos.resources.graphio import (
@@ -57,6 +58,7 @@ from unilabos_msgs.msg import Resource # type: ignore
from unilabos.resources.resource_tracker import ( from unilabos.resources.resource_tracker import (
DeviceNodeResourceTracker, DeviceNodeResourceTracker,
ResourceDictType,
ResourceTreeSet, ResourceTreeSet,
ResourceTreeInstance, ResourceTreeInstance,
ResourceDictInstance, ResourceDictInstance,
@@ -194,9 +196,9 @@ class PropertyPublisher:
self._value = None self._value = None
try: try:
self.publisher_ = node.create_publisher(msg_type, f"{name}", qos) self.publisher_ = node.create_publisher(msg_type, f"{name}", qos)
except AttributeError as ex: except Exception as e:
self.node.lab_logger().error( self.node.lab_logger().error(
f"创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {ex}\n{traceback.format_exc()}" f"StatusError, DeviceId: {self.node.device_id} 创建发布者 {name} 失败,可能由于注册表有误,类型: {msg_type},错误: {e}"
) )
self.timer = node.create_timer(self.timer_period, self.publish_property) self.timer = node.create_timer(self.timer_period, self.publish_property)
self.__loop = ROS2DeviceNode.get_asyncio_loop() self.__loop = ROS2DeviceNode.get_asyncio_loop()
@@ -569,9 +571,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
future.add_done_callback(done_cb) future.add_done_callback(done_cb)
except ImportError: except ImportError:
self.lab_logger().error("Host请求添加物料时本环境并不存在pylabrobot") self.lab_logger().error("Host请求添加物料时本环境并不存在pylabrobot")
res.response = get_result_info_str(traceback.format_exc(), False, {})
except Exception as e: except Exception as e:
self.lab_logger().error("Host请求添加物料时出错") self.lab_logger().error("Host请求添加物料时出错")
self.lab_logger().error(traceback.format_exc()) self.lab_logger().error(traceback.format_exc())
res.response = get_result_info_str(traceback.format_exc(), False, {})
return res return res
# noinspection PyTypeChecker # noinspection PyTypeChecker
@@ -594,6 +598,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
self.s2c_resource_tree, # type: ignore self.s2c_resource_tree, # type: ignore
callback_group=self.callback_group, callback_group=self.callback_group,
), ),
"s2c_device_manage": self.create_service(
SerialCommand,
f"/srv{self.namespace}/s2c_device_manage",
self.s2c_device_manage, # type: ignore
callback_group=self.callback_group,
),
} }
# 向全局在线设备注册表添加设备信息 # 向全局在线设备注册表添加设备信息
@@ -1062,6 +1072,48 @@ class BaseROS2DeviceNode(Node, Generic[T]):
return res return res
async def s2c_device_manage(self, req: SerialCommand_Request, res: SerialCommand_Response):
"""Handle add/remove device requests from HostNode via SerialCommand."""
try:
cmd = json.loads(req.command)
action = cmd.get("action", "")
data = cmd.get("data", {})
device_id = data.get("device_id", "")
if not device_id:
res.response = json.dumps({"success": False, "error": "device_id required"})
return res
if action == "add":
result = self.create_device(device_id, data)
elif action == "remove":
result = self.destroy_device(device_id)
else:
result = {"success": False, "error": f"Unknown action: {action}"}
res.response = json.dumps(result, ensure_ascii=False)
except NotImplementedError as e:
self.lab_logger().warning(f"[DeviceManage] {e}")
res.response = json.dumps({"success": False, "error": str(e)})
except Exception as e:
self.lab_logger().error(f"[DeviceManage] Error: {e}")
res.response = json.dumps({"success": False, "error": str(e)})
return res
def create_device(self, device_id: str, config: "ResourceDictType") -> dict:
"""Create a sub-device dynamically. Override in HostNode / WorkstationNode."""
raise NotImplementedError(
f"{self.__class__.__name__} does not support dynamic device creation"
)
def destroy_device(self, device_id: str) -> dict:
"""Destroy a sub-device dynamically. Override in HostNode / WorkstationNode."""
raise NotImplementedError(
f"{self.__class__.__name__} does not support dynamic device removal"
)
async def transfer_resource_to_another( async def transfer_resource_to_another(
self, self,
plr_resources: List["ResourcePLR"], plr_resources: List["ResourcePLR"],
@@ -1204,22 +1256,40 @@ class BaseROS2DeviceNode(Node, Generic[T]):
return self._lab_logger return self._lab_logger
def create_ros_publisher(self, attr_name, msg_type, initial_period=5.0): def create_ros_publisher(self, attr_name, msg_type, initial_period=5.0):
"""创建ROS发布者""" """创建ROS发布者,仅当方法/属性有 @topic_config 装饰器时才创建。"""
# 检测装饰器配置(支持 get_{attr_name} 方法和 @property # 检测 @topic_config 装饰器配置
topic_config = {} topic_config = {}
driver_class = type(self.driver_instance)
# 优先检测 get_{attr_name} 方法 # 区分 @property 和普通方法两种情况
if hasattr(self.driver_instance, f"get_{attr_name}"): is_prop = hasattr(driver_class, attr_name) and isinstance(
getter_method = getattr(self.driver_instance, f"get_{attr_name}") getattr(driver_class, attr_name), property
topic_config = get_topic_config(getter_method) )
# 如果没有配置,检测 @property 装饰的属性 if is_prop:
# @property: 检测 fget 上的 @topic_config
class_attr = getattr(driver_class, attr_name)
if class_attr.fget is not None:
topic_config = get_topic_config(class_attr.fget)
else:
# 普通方法: 直接检测 attr_name 方法上的 @topic_config
if hasattr(self.driver_instance, attr_name):
method = getattr(self.driver_instance, attr_name)
if callable(method):
topic_config = get_topic_config(method)
# 没有 @topic_config 装饰器则跳过发布
if not topic_config: if not topic_config:
driver_class = type(self.driver_instance) return
if hasattr(driver_class, attr_name):
class_attr = getattr(driver_class, attr_name) # 发布名称优先级: @topic_config(name=...) > get_ 前缀去除 > attr_name
if isinstance(class_attr, property) and class_attr.fget is not None: cfg_name = topic_config.get("name")
topic_config = get_topic_config(class_attr.fget) if cfg_name:
publish_name = cfg_name
elif attr_name.startswith("get_"):
publish_name = attr_name[4:]
else:
publish_name = attr_name
# 使用装饰器配置或默认值 # 使用装饰器配置或默认值
cfg_period = topic_config.get("period") cfg_period = topic_config.get("period")
@@ -1232,10 +1302,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# 获取属性值的方法 # 获取属性值的方法
def get_device_attr(): def get_device_attr():
try: try:
if hasattr(self.driver_instance, f"get_{attr_name}"): if is_prop:
return getattr(self.driver_instance, f"get_{attr_name}")()
else:
return getattr(self.driver_instance, attr_name) return getattr(self.driver_instance, attr_name)
else:
return getattr(self.driver_instance, attr_name)()
except AttributeError as ex: except AttributeError as ex:
if ex.args[0].startswith(f"AttributeError: '{self.driver_instance.__class__.__name__}' object"): if ex.args[0].startswith(f"AttributeError: '{self.driver_instance.__class__.__name__}' object"):
self.lab_logger().error( self.lab_logger().error(
@@ -1247,8 +1317,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
) )
self.lab_logger().error(traceback.format_exc()) self.lab_logger().error(traceback.format_exc())
self._property_publishers[attr_name] = PropertyPublisher( self._property_publishers[publish_name] = PropertyPublisher(
self, attr_name, get_device_attr, msg_type, period, print_publish, qos self, publish_name, get_device_attr, msg_type, period, print_publish, qos
) )
def create_ros_action_server(self, action_name, action_value_mapping): def create_ros_action_server(self, action_name, action_value_mapping):
@@ -1256,14 +1326,17 @@ class BaseROS2DeviceNode(Node, Generic[T]):
action_type = action_value_mapping["type"] action_type = action_value_mapping["type"]
str_action_type = str(action_type)[8:-2] str_action_type = str(action_type)[8:-2]
self._action_servers[action_name] = ActionServer( try:
self, self._action_servers[action_name] = ActionServer(
action_type, self,
action_name, action_type,
execute_callback=self._create_execute_callback(action_name, action_value_mapping), action_name,
callback_group=self.callback_group, execute_callback=self._create_execute_callback(action_name, action_value_mapping),
) callback_group=self.callback_group,
)
except Exception as e:
self.lab_logger().error(f"创建ActionServer失败Device: {self.device_id}, Action Name: {action_name}, Action Type: {action_type}, Error: {e}")
return
self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}") self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}")
def _setup_decorated_subscribers(self): def _setup_decorated_subscribers(self):
@@ -1811,7 +1884,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
continue continue
# 处理单个 ResourceSlot # 处理单个 ResourceSlot
if arg_type == "unilabos.registry.placeholder_type:ResourceSlot": _is_resource_slot = isinstance(arg_type, str) and arg_type.endswith(":ResourceSlot")
if _is_resource_slot:
resource_data = function_args[arg_name] resource_data = function_args[arg_name]
if isinstance(resource_data, dict) and "id" in resource_data: if isinstance(resource_data, dict) and "id" in resource_data:
try: try:
@@ -1825,8 +1899,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# 处理 ResourceSlot 列表 # 处理 ResourceSlot 列表
elif isinstance(arg_type, tuple) and len(arg_type) == 2: elif isinstance(arg_type, tuple) and len(arg_type) == 2:
resource_slot_type = "unilabos.registry.placeholder_type:ResourceSlot" if arg_type[0] == "list" and isinstance(arg_type[1], str) and arg_type[1].endswith(":ResourceSlot"):
if arg_type[0] == "list" and arg_type[1] == resource_slot_type:
resource_list = function_args[arg_name] resource_list = function_args[arg_name]
if isinstance(resource_list, list): if isinstance(resource_list, list):
try: try:

View File

@@ -4,7 +4,14 @@ import cv2
from sensor_msgs.msg import Image from sensor_msgs.msg import Image
from cv_bridge import CvBridge from cv_bridge import CvBridge
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker
from unilabos.registry.decorators import device
@device(
id="camera",
category=["camera"],
description="""VideoPublisher摄像头设备节点用于实时视频采集和流媒体发布。该设备通过OpenCV连接本地摄像头如USB摄像头、内置摄像头等定时采集视频帧并将其转换为ROS2的sensor_msgs/Image消息格式发布到视频话题。主要用于实验室自动化系统中的视觉监控、图像分析、实时观察等应用场景。支持可配置的摄像头索引、发布频率等参数。""",
)
class VideoPublisher(BaseROS2DeviceNode): class VideoPublisher(BaseROS2DeviceNode):
def __init__(self, device_id='video_publisher', registry_name="", device_uuid='', camera_index=0, period: float = 0.1, resource_tracker: DeviceNodeResourceTracker = None): def __init__(self, device_id='video_publisher', registry_name="", device_uuid='', camera_index=0, period: float = 0.1, resource_tracker: DeviceNodeResourceTracker = None):
# 初始化BaseROS2DeviceNode使用自身作为driver_instance # 初始化BaseROS2DeviceNode使用自身作为driver_instance

View File

@@ -12,6 +12,7 @@ from geometry_msgs.msg import Point
from rclpy.action import ActionClient, get_action_server_names_and_types_by_node from rclpy.action import ActionClient, get_action_server_names_and_types_by_node
from rclpy.service import Service from rclpy.service import Service
from typing_extensions import TypedDict from typing_extensions import TypedDict
from unilabos_msgs.action import EmptyIn, StrSingleInput, ResourceCreateFromOuterEasy, ResourceCreateFromOuter
from unilabos_msgs.msg import Resource # type: ignore from unilabos_msgs.msg import Resource # type: ignore
from unilabos_msgs.srv import ( from unilabos_msgs.srv import (
ResourceAdd, ResourceAdd,
@@ -23,6 +24,7 @@ from unilabos_msgs.srv import (
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
from unique_identifier_msgs.msg import UUID from unique_identifier_msgs.msg import UUID
from unilabos.registry.decorators import device
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
from unilabos.registry.registry import lab_registry from unilabos.registry.registry import lab_registry
from unilabos.resources.container import RegularContainer from unilabos.resources.container import RegularContainer
@@ -30,6 +32,7 @@ from unilabos.resources.graphio import initialize_resource
from unilabos.resources.registry import add_schema from unilabos.resources.registry import add_schema
from unilabos.resources.resource_tracker import ( from unilabos.resources.resource_tracker import (
ResourceDict, ResourceDict,
ResourceDictType,
ResourceDictInstance, ResourceDictInstance,
ResourceTreeSet, ResourceTreeSet,
ResourceTreeInstance, ResourceTreeInstance,
@@ -65,7 +68,13 @@ class DeviceActionStatus:
class TestResourceReturn(TypedDict): class TestResourceReturn(TypedDict):
resources: List[List[ResourceDict]] resources: List[List[ResourceDict]]
devices: List[Dict[str, Any]] devices: List[Dict[str, Any]]
unilabos_samples: List[LabSample] # unilabos_samples: List[LabSample]
class CreateResourceReturn(TypedDict):
created_resource_tree: List[List[ResourceDict]]
liquid_input_resource_tree: List[Dict[str, Any]]
# unilabos_samples: List[LabSample]
class TestLatencyReturn(TypedDict): class TestLatencyReturn(TypedDict):
@@ -80,6 +89,7 @@ class TestLatencyReturn(TypedDict):
status: str status: str
@device(id="host_node", category=[], description="Host Node", icon="icon_device.webp")
class HostNode(BaseROS2DeviceNode): class HostNode(BaseROS2DeviceNode):
""" """
主机节点类,负责管理设备、资源和控制器 主机节点类,负责管理设备、资源和控制器
@@ -268,44 +278,42 @@ class HostNode(BaseROS2DeviceNode):
self._action_clients: Dict[str, ActionClient] = { # 为了方便了解实际的数据类型host的默认写好 self._action_clients: Dict[str, ActionClient] = { # 为了方便了解实际的数据类型host的默认写好
"/devices/host_node/create_resource": ActionClient( "/devices/host_node/create_resource": ActionClient(
self, self,
lab_registry.ResourceCreateFromOuterEasy, ResourceCreateFromOuterEasy,
"/devices/host_node/create_resource", "/devices/host_node/create_resource",
callback_group=self.callback_group, callback_group=self.callback_group,
), ),
"/devices/host_node/create_resource_detailed": ActionClient( "/devices/host_node/create_resource_detailed": ActionClient(
self, self,
lab_registry.ResourceCreateFromOuter, ResourceCreateFromOuter,
"/devices/host_node/create_resource_detailed", "/devices/host_node/create_resource_detailed",
callback_group=self.callback_group, callback_group=self.callback_group,
), ),
"/devices/host_node/test_latency": ActionClient( "/devices/host_node/test_latency": ActionClient(
self, self,
lab_registry.EmptyIn, EmptyIn,
"/devices/host_node/test_latency", "/devices/host_node/test_latency",
callback_group=self.callback_group, callback_group=self.callback_group,
), ),
"/devices/host_node/test_resource": ActionClient( "/devices/host_node/test_resource": ActionClient(
self, self,
lab_registry.EmptyIn, EmptyIn,
"/devices/host_node/test_resource", "/devices/host_node/test_resource",
callback_group=self.callback_group, callback_group=self.callback_group,
), ),
"/devices/host_node/_execute_driver_command": ActionClient( "/devices/host_node/_execute_driver_command": ActionClient(
self, self,
lab_registry.StrSingleInput, StrSingleInput,
"/devices/host_node/_execute_driver_command", "/devices/host_node/_execute_driver_command",
callback_group=self.callback_group, callback_group=self.callback_group,
), ),
"/devices/host_node/_execute_driver_command_async": ActionClient( "/devices/host_node/_execute_driver_command_async": ActionClient(
self, self,
lab_registry.StrSingleInput, StrSingleInput,
"/devices/host_node/_execute_driver_command_async", "/devices/host_node/_execute_driver_command_async",
callback_group=self.callback_group, callback_group=self.callback_group,
), ),
} # 用来存储多个ActionClient实例 } # 用来存储多个ActionClient实例
self._action_value_mappings: Dict[str, Dict] = ( self._action_value_mappings: Dict[str, Dict] = {} # device_id -> action_value_mappings(本地+远程设备统一存储)
{}
) # device_id -> action_value_mappings(本地+远程设备统一存储)
self._slave_registry_configs: Dict[str, Dict] = {} # registry_name -> registry_config(含action_value_mappings) self._slave_registry_configs: Dict[str, Dict] = {} # registry_name -> registry_config(含action_value_mappings)
self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态 self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态
self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备 self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备
@@ -323,10 +331,18 @@ class HostNode(BaseROS2DeviceNode):
self._discover_devices() self._discover_devices()
# 初始化所有本机设备节点,多一次过滤,防止重复初始化 # 初始化所有本机设备节点,多一次过滤,防止重复初始化
local_machine = BasicConfig.machine_name
for device_config in devices_config.root_nodes: for device_config in devices_config.root_nodes:
device_id = device_config.res_content.id device_id = device_config.res_content.id
if device_config.res_content.type != "device": if device_config.res_content.type != "device":
continue continue
dev_machine = device_config.res_content.machine_name
if dev_machine and local_machine and dev_machine != local_machine:
self.lab_logger().info(
f"[Host Node] Device {device_id} belongs to machine '{dev_machine}', "
f"local is '{local_machine}', skipping initialization."
)
continue
if device_id not in self.devices_names: if device_id not in self.devices_names:
self.initialize_device(device_id, device_config) self.initialize_device(device_id, device_config)
else: else:
@@ -556,7 +572,7 @@ class HostNode(BaseROS2DeviceNode):
liquid_type: list[str] = [], liquid_type: list[str] = [],
liquid_volume: list[int] = [], liquid_volume: list[int] = [],
slot_on_deck: str = "", slot_on_deck: str = "",
): ) -> CreateResourceReturn:
# 暂不支持多对同名父子同时存在 # 暂不支持多对同名父子同时存在
res_creation_input = { res_creation_input = {
"id": res_id.split("/")[-1], "id": res_id.split("/")[-1],
@@ -609,6 +625,8 @@ class HostNode(BaseROS2DeviceNode):
assert len(response) == 1, "Create Resource应当只返回一个结果" assert len(response) == 1, "Create Resource应当只返回一个结果"
for i in response: for i in response:
res = json.loads(i) res = json.loads(i)
if "suc" in res:
raise ValueError(res.get("error"))
return res return res
except Exception as ex: except Exception as ex:
pass pass
@@ -650,7 +668,12 @@ class HostNode(BaseROS2DeviceNode):
action_id = f"/devices/{device_id}/{action_name}" action_id = f"/devices/{device_id}/{action_name}"
if action_id not in self._action_clients: if action_id not in self._action_clients:
action_type = action_value_mapping["type"] action_type = action_value_mapping["type"]
self._action_clients[action_id] = ActionClient(self, action_type, action_id) try:
self._action_clients[action_id] = ActionClient(self, action_type, action_id)
except Exception as e:
self.lab_logger().error(
f"创建ActionClient失败Device: {device_id}, Action Name: {action_name}, Action Type: {action_type}, Error: {e}")
continue
self.lab_logger().trace( self.lab_logger().trace(
f"[Host Node] Created ActionClient (Local): {action_id}" f"[Host Node] Created ActionClient (Local): {action_id}"
) # 子设备再创建用的是Discover发现的 ) # 子设备再创建用的是Discover发现的
@@ -1250,9 +1273,9 @@ class HostNode(BaseROS2DeviceNode):
# 用 registry_name 索引已存储的 registry_config,获取 action_value_mappings # 用 registry_name 索引已存储的 registry_config,获取 action_value_mappings
if registry_name and registry_name in self._slave_registry_configs: if registry_name and registry_name in self._slave_registry_configs:
action_mappings = self._slave_registry_configs[registry_name].get( action_mappings = (
"class", {} self._slave_registry_configs[registry_name].get("class", {}).get("action_value_mappings", {})
).get("action_value_mappings", {}) )
if action_mappings: if action_mappings:
self._action_value_mappings[edge_device_id] = action_mappings self._action_value_mappings[edge_device_id] = action_mappings
self.lab_logger().info( self.lab_logger().info(
@@ -1272,14 +1295,19 @@ class HostNode(BaseROS2DeviceNode):
# 解析 devices_config,建立 device_id -> action_value_mappings 映射 # 解析 devices_config,建立 device_id -> action_value_mappings 映射
if devices_config: if devices_config:
machine_name = info["machine_name"]
# Stamp machine_name on each device dict before parsing
for device_tree in devices_config: for device_tree in devices_config:
for device_dict in device_tree: for device_dict in device_tree:
device_dict["machine_name"] = machine_name
device_id = device_dict.get("id", "") device_id = device_dict.get("id", "")
class_name = device_dict.get("class", "") class_name = device_dict.get("class", "")
if device_id and class_name and class_name in self._slave_registry_configs: if device_id and class_name and class_name in self._slave_registry_configs:
action_mappings = self._slave_registry_configs[class_name].get( action_mappings = (
"class", {} self._slave_registry_configs[class_name]
).get("action_value_mappings", {}) .get("class", {})
.get("action_value_mappings", {})
)
if action_mappings: if action_mappings:
self._action_value_mappings[device_id] = action_mappings self._action_value_mappings[device_id] = action_mappings
self.lab_logger().info( self.lab_logger().info(
@@ -1287,6 +1315,18 @@ class HostNode(BaseROS2DeviceNode):
f"for remote device {device_id} (class: {class_name})" f"for remote device {device_id} (class: {class_name})"
) )
# Merge slave devices_config into self.devices_config tree
try:
slave_tree_set = ResourceTreeSet.load(devices_config) # slave一定是根节点的tree
for tree in slave_tree_set.trees:
self.devices_config.trees.append(tree)
self.lab_logger().info(
f"[Host Node] Merged {len(slave_tree_set.trees)} slave device trees "
f"(machine: {machine_name}) into devices_config"
)
except Exception as e:
self.lab_logger().error(f"[Host Node] Failed to merge slave devices_config: {e}")
self.lab_logger().debug(f"[Host Node] Node info update: {info}") self.lab_logger().debug(f"[Host Node] Node info update: {info}")
response.response = "OK" response.response = "OK"
except Exception as e: except Exception as e:
@@ -1695,3 +1735,177 @@ class HostNode(BaseROS2DeviceNode):
self.lab_logger().error(f"[Host Node-Resource] Error notifying resource tree update: {str(e)}") self.lab_logger().error(f"[Host Node-Resource] Error notifying resource tree update: {str(e)}")
self.lab_logger().error(traceback.format_exc()) self.lab_logger().error(traceback.format_exc())
return False return False
# ------------------------------------------------------------------
# Device lifecycle (add / remove) — pure forwarder
# ------------------------------------------------------------------
def notify_device_manage(self, target_node_id: str, action: str, config: ResourceDictType) -> bool:
"""Forward an add/remove device command to the target node via ROS2 SerialCommand.
The HostNode does NOT interpret the command; it simply resolves the
target namespace and forwards the request to ``s2c_device_manage``.
If *target_node_id* equals the HostNode's own device_id (i.e. the
command targets the host itself), we call our local ``create_device``
/ ``destroy_device`` directly instead of going through ROS2.
"""
try:
# If the target is the host itself, handle locally
device_id = config["id"]
if target_node_id == self.device_id:
if action == "add":
return self.create_device(device_id, config).get("success", False)
elif action == "remove":
return self.destroy_device(device_id).get("success", False)
if target_node_id not in self.devices_names:
self.lab_logger().error(
f"[Host Node-DeviceMgr] Target {target_node_id} not found in devices_names"
)
return False
namespace = self.devices_names[target_node_id]
device_key = f"{namespace}/{target_node_id}"
if device_key not in self._online_devices:
self.lab_logger().error(f"[Host Node-DeviceMgr] Target {device_key} is offline")
return False
srv_address = f"/srv{namespace}/s2c_device_manage"
self.lab_logger().info(
f"[Host Node-DeviceMgr] Forwarding {action}_device to {target_node_id} ({srv_address})"
)
sclient = self.create_client(SerialCommand, srv_address)
if not sclient.wait_for_service(timeout_sec=5.0):
self.lab_logger().error(f"[Host Node-DeviceMgr] Service {srv_address} not available")
return False
request = SerialCommand.Request()
request.command = json.dumps({"action": action, "data": config}, ensure_ascii=False)
future = sclient.call_async(request)
timeout = 30.0
start_time = time.time()
while not future.done():
if time.time() - start_time > timeout:
self.lab_logger().error(
f"[Host Node-DeviceMgr] Timeout waiting for {action}_device on {target_node_id}"
)
return False
time.sleep(0.05)
response = future.result()
self.lab_logger().info(
f"[Host Node-DeviceMgr] {action}_device on {target_node_id} completed"
)
return True
except Exception as e:
self.lab_logger().error(f"[Host Node-DeviceMgr] Error: {e}")
self.lab_logger().error(traceback.format_exc())
return False
def create_device(self, device_id: str, config: ResourceDictType) -> dict:
"""Dynamically create a root-level device on the host."""
if not device_id:
return {"success": False, "error": "device_id required"}
if device_id in self.devices_names:
return {"success": False, "error": f"Device {device_id} already exists"}
try:
config.setdefault("id", device_id)
config.setdefault("type", "device")
config.setdefault("machine_name", BasicConfig.machine_name or "本地")
res_dict = ResourceDictInstance.get_resource_instance_from_dict(config)
self.initialize_device(device_id, res_dict)
if device_id not in self.devices_names:
return {"success": False, "error": f"initialize_device failed for {device_id}"}
# Add to config tree (devices_config)
tree = ResourceTreeInstance(res_dict)
self.devices_config.trees.append(tree)
# Add to resource tracker so s2c_resource_tree can find it
try:
for plr_resource in ResourceTreeSet([tree]).to_plr_resources():
self._resource_tracker.add_resource(plr_resource)
except Exception as ex:
self.lab_logger().warning(f"[Host Node-DeviceMgr] PLR resource registration skipped: {ex}")
self.lab_logger().info(f"[Host Node-DeviceMgr] Device {device_id} created successfully")
return {"success": True, "device_id": device_id}
except Exception as e:
self.lab_logger().error(f"[Host Node-DeviceMgr] Failed to create {device_id}: {e}")
self.lab_logger().error(traceback.format_exc())
return {"success": False, "error": str(e)}
def destroy_device(self, device_id: str) -> dict:
"""Remove a root-level device from the host."""
if not device_id:
return {"success": False, "error": "device_id required"}
if device_id not in self.devices_names:
return {"success": False, "error": f"Device {device_id} not found"}
if device_id == self.device_id:
return {"success": False, "error": "Cannot destroy host_node itself"}
try:
namespace = self.devices_names[device_id]
device_key = f"{namespace}/{device_id}"
# Remove action clients
action_prefix = f"/devices/{device_id}/"
to_remove = [k for k in self._action_clients if k.startswith(action_prefix)]
for k in to_remove:
try:
self._action_clients[k].destroy()
except Exception:
pass
del self._action_clients[k]
# Remove from config tree (devices_config)
self.devices_config.trees = [
t for t in self.devices_config.trees
if t.root_node.res_content.id != device_id
]
# Remove from resource tracker
try:
tracked = self._resource_tracker.uuid_to_resources.copy()
for uid, res in tracked.items():
res_id = res.get("id") if isinstance(res, dict) else getattr(res, "name", None)
if res_id == device_id:
self._resource_tracker.remove_resource(res)
except Exception as ex:
self.lab_logger().warning(f"[Host Node-DeviceMgr] Resource tracker cleanup: {ex}")
# Clean internal state
self._online_devices.discard(device_key)
self.devices_names.pop(device_id, None)
self.device_machine_names.pop(device_id, None)
self._action_value_mappings.pop(device_id, None)
# Destroy the ROS2 node of the device
instance = self.devices_instances.pop(device_id, None)
if instance is not None:
try:
# noinspection PyProtectedMember
ros_node = getattr(instance, "_ros_node", None)
if ros_node is not None:
ros_node.destroy_node()
except Exception as e:
self.lab_logger().warning(f"[Host Node-DeviceMgr] Error destroying ROS node for {device_id}: {e}")
self.lab_logger().info(f"[Host Node-DeviceMgr] Device {device_id} destroyed")
return {"success": True, "device_id": device_id}
except Exception as e:
self.lab_logger().error(f"[Host Node-DeviceMgr] Failed to destroy {device_id}: {e}")
self.lab_logger().error(traceback.format_exc())
return {"success": False, "error": str(e)}

View File

@@ -20,7 +20,7 @@ from unilabos.ros.msgs.message_converter import (
convert_from_ros_msg_with_mapping, convert_from_ros_msg_with_mapping,
) )
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker, ROS2DeviceNode from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker, ROS2DeviceNode
from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDictInstance from unilabos.resources.resource_tracker import ResourceDictType, ResourceTreeSet, ResourceDictInstance
from unilabos.utils.type_check import get_result_info_str from unilabos.utils.type_check import get_result_info_str
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -177,6 +177,103 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
self.lab_logger().trace(f"为子设备 {device_id} 创建动作客户端: {action_name}") self.lab_logger().trace(f"为子设备 {device_id} 创建动作客户端: {action_name}")
return d return d
def create_device(self, device_id: str, config: ResourceDictType) -> dict:
"""Dynamically add a sub-device to this workstation."""
if not device_id:
return {"success": False, "error": "device_id required"}
if device_id in self.sub_devices:
return {"success": False, "error": f"Sub-device {device_id} already exists"}
try:
from unilabos.config.config import BasicConfig
config.setdefault("id", device_id)
config.setdefault("type", "device")
config.setdefault("machine_name", BasicConfig.machine_name or "本地")
res_dict = ResourceDictInstance.get_resource_instance_from_dict(config)
d = self.initialize_device(device_id, res_dict)
if d is None:
return {"success": False, "error": f"initialize_device returned None for {device_id}"}
# Add to children config list
self.children.append(res_dict)
# Add to resource tracker
try:
from unilabos.resources.resource_tracker import ResourceTreeInstance
tree = ResourceTreeInstance(res_dict)
for plr_resource in ResourceTreeSet([tree]).to_plr_resources():
self.resource_tracker.add_resource(plr_resource)
except Exception as ex:
self.lab_logger().warning(f"[Workstation-DeviceMgr] PLR resource registration skipped: {ex}")
self.lab_logger().info(f"[Workstation-DeviceMgr] Sub-device {device_id} created")
return {"success": True, "device_id": device_id}
except Exception as e:
self.lab_logger().error(f"[Workstation-DeviceMgr] Failed to create {device_id}: {e}")
self.lab_logger().error(traceback.format_exc())
return {"success": False, "error": str(e)}
def destroy_device(self, device_id: str) -> dict:
"""Dynamically remove a sub-device from this workstation."""
if not device_id:
return {"success": False, "error": "device_id required"}
if device_id not in self.sub_devices:
return {"success": False, "error": f"Sub-device {device_id} not found"}
try:
# Remove from children config list
self.children = [
c for c in self.children
if c.res_content.id != device_id
]
# Remove from resource tracker
try:
tracked = self.resource_tracker.uuid_to_resources.copy()
for uid, res in tracked.items():
res_id = res.get("id") if isinstance(res, dict) else getattr(res, "name", None)
if res_id == device_id:
self.resource_tracker.remove_resource(res)
except Exception as ex:
self.lab_logger().warning(f"[Workstation-DeviceMgr] Resource tracker cleanup: {ex}")
# Remove action clients for this sub-device
action_prefix = f"/devices/{device_id}/"
to_remove = [k for k in self._action_clients if k.startswith(action_prefix)]
for k in to_remove:
try:
self._action_clients[k].destroy()
except Exception:
pass
del self._action_clients[k]
# Destroy the ROS2 node
instance = self.sub_devices.pop(device_id, None)
if instance is not None:
ros_node = getattr(instance, "ros_node_instance", None)
if ros_node is not None:
try:
ros_node.destroy_node()
except Exception as e:
self.lab_logger().warning(
f"[Workstation-DeviceMgr] Error destroying ROS node for {device_id}: {e}"
)
# Remove from communication map if present
self.communication_node_id_to_instance.pop(device_id, None)
self.lab_logger().info(f"[Workstation-DeviceMgr] Sub-device {device_id} destroyed")
return {"success": True, "device_id": device_id}
except Exception as e:
self.lab_logger().error(f"[Workstation-DeviceMgr] Failed to destroy {device_id}: {e}")
self.lab_logger().error(traceback.format_exc())
return {"success": False, "error": str(e)}
def create_ros_action_server(self, action_name, action_value_mapping): def create_ros_action_server(self, action_name, action_value_mapping):
"""创建ROS动作服务器""" """创建ROS动作服务器"""
if action_name not in self.protocol_names: if action_name not in self.protocol_names:

View File

@@ -19,74 +19,6 @@ def singleton(cls):
return get_instance return get_instance
def topic_config(
period: Optional[float] = None,
print_publish: Optional[bool] = None,
qos: Optional[int] = None,
) -> Callable[[F], F]:
"""
Topic发布配置装饰器
用于装饰 get_{attr_name} 方法或 @property控制对应属性的ROS topic发布行为。
Args:
period: 发布周期。None 表示使用默认值 5.0
print_publish: 是否打印发布日志。None 表示使用节点默认配置
qos: QoS深度配置。None 表示使用默认值 10
Example:
class MyDriver:
# 方式1: 装饰 get_{attr_name} 方法
@topic_config(period=1.0, print_publish=False, qos=5)
def get_temperature(self):
return self._temperature
# 方式2: 与 @property 连用topic_config 放在下面)
@property
@topic_config(period=0.1)
def position(self):
return self._position
Note:
与 @property 连用时,@topic_config 必须放在 @property 下面,
这样装饰器执行顺序为:先 topic_config 添加配置,再 property 包装。
"""
def decorator(func: F) -> F:
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# 在函数上附加配置属性 (type: ignore 用于动态属性)
wrapper._topic_period = period # type: ignore[attr-defined]
wrapper._topic_print_publish = print_publish # type: ignore[attr-defined]
wrapper._topic_qos = qos # type: ignore[attr-defined]
wrapper._has_topic_config = True # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
return decorator
def get_topic_config(func) -> dict:
"""
获取函数上的topic配置
Args:
func: 被装饰的函数
Returns:
包含 period, print_publish, qos 的配置字典
"""
if hasattr(func, "_has_topic_config") and getattr(func, "_has_topic_config", False):
return {
"period": getattr(func, "_topic_period", None),
"print_publish": getattr(func, "_topic_print_publish", None),
"qos": getattr(func, "_topic_qos", None),
}
return {}
def subscribe( def subscribe(
topic: str, topic: str,
msg_type: Optional[type] = None, msg_type: Optional[type] = None,
@@ -104,24 +36,6 @@ def subscribe(
- {namespace}: 完整命名空间 (如 "/devices/pump_1") - {namespace}: 完整命名空间 (如 "/devices/pump_1")
msg_type: ROS 消息类型。如果为 None需要在回调函数的类型注解中指定 msg_type: ROS 消息类型。如果为 None需要在回调函数的类型注解中指定
qos: QoS 深度配置,默认为 10 qos: QoS 深度配置,默认为 10
Example:
from std_msgs.msg import String, Float64
class MyDriver:
@subscribe(topic="/devices/{device_id}/set_speed", msg_type=Float64)
def on_speed_update(self, msg: Float64):
self._speed = msg.data
print(f"Speed updated to: {self._speed}")
@subscribe(topic="{namespace}/command")
def on_command(self, msg: String):
# msg_type 可从类型注解推断
self.execute_command(msg.data)
Note:
- 回调方法的第一个参数是 self第二个参数是收到的 ROS 消息
- topic 中的占位符会在创建订阅时被实际值替换
""" """
def decorator(func: F) -> F: def decorator(func: F) -> F:
@@ -129,7 +43,6 @@ def subscribe(
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
return func(*args, **kwargs) return func(*args, **kwargs)
# 在函数上附加订阅配置
wrapper._subscribe_topic = topic # type: ignore[attr-defined] wrapper._subscribe_topic = topic # type: ignore[attr-defined]
wrapper._subscribe_msg_type = msg_type # type: ignore[attr-defined] wrapper._subscribe_msg_type = msg_type # type: ignore[attr-defined]
wrapper._subscribe_qos = qos # type: ignore[attr-defined] wrapper._subscribe_qos = qos # type: ignore[attr-defined]
@@ -141,15 +54,7 @@ def subscribe(
def get_subscribe_config(func) -> dict: def get_subscribe_config(func) -> dict:
""" """获取函数上的订阅配置 (topic, msg_type, qos)"""
获取函数上的订阅配置
Args:
func: 被装饰的函数
Returns:
包含 topic, msg_type, qos 的配置字典
"""
if hasattr(func, "_has_subscribe") and getattr(func, "_has_subscribe", False): if hasattr(func, "_has_subscribe") and getattr(func, "_has_subscribe", False):
return { return {
"topic": getattr(func, "_subscribe_topic", None), "topic": getattr(func, "_subscribe_topic", None),
@@ -163,9 +68,6 @@ def get_all_subscriptions(instance) -> list:
""" """
扫描实例的所有方法,获取带有 @subscribe 装饰器的方法及其配置 扫描实例的所有方法,获取带有 @subscribe 装饰器的方法及其配置
Args:
instance: 要扫描的实例
Returns: Returns:
包含 (method_name, method, config) 元组的列表 包含 (method_name, method, config) 元组的列表
""" """
@@ -184,92 +86,14 @@ def get_all_subscriptions(instance) -> list:
return subscriptions return subscriptions
def always_free(func: F) -> F: # ---------------------------------------------------------------------------
""" # 向后兼容重导出 -- 已迁移到 unilabos.registry.decorators
标记动作为永久闲置(不受busy队列限制)的装饰器 # ---------------------------------------------------------------------------
from unilabos.registry.decorators import ( # noqa: E402, F401
被此装饰器标记的 action 方法,在执行时不会受到设备级别的排队限制, topic_config,
任何时候请求都可以立即执行。适用于查询类、状态读取类等轻量级操作。 get_topic_config,
always_free,
Example: is_always_free,
class MyDriver: not_action,
@always_free is_not_action,
def query_status(self, param: str): )
# 这个动作可以随时执行,不需要排队
return self._status
def transfer(self, volume: float):
# 这个动作会按正常排队逻辑执行
pass
Note:
- 可以与其他装饰器组合使用,@always_free 应放在最外层
- 仅影响 WebSocket 调度层的 busy/free 判断,不影响 ROS2 层
"""
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper._is_always_free = True # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
def is_always_free(func) -> bool:
"""
检查函数是否被标记为永久闲置
Args:
func: 被检查的函数
Returns:
如果函数被 @always_free 装饰则返回 True否则返回 False
"""
return getattr(func, "_is_always_free", False)
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)

View File

@@ -22,6 +22,7 @@ class EnvironmentChecker:
# "pymodbus.framer.FramerType": "pymodbus==3.9.2", # "pymodbus.framer.FramerType": "pymodbus==3.9.2",
"websockets": "websockets", "websockets": "websockets",
"msgcenterpy": "msgcenterpy", "msgcenterpy": "msgcenterpy",
"orjson": "orjson",
"opentrons_shared_data": "opentrons_shared_data", "opentrons_shared_data": "opentrons_shared_data",
"typing_extensions": "typing_extensions", "typing_extensions": "typing_extensions",
"crcmod": "crcmod-plus", "crcmod": "crcmod-plus",
@@ -32,7 +33,7 @@ class EnvironmentChecker:
# 包版本要求(包名: 最低版本) # 包版本要求(包名: 最低版本)
self.version_requirements = { self.version_requirements = {
"msgcenterpy": "0.1.5", # msgcenterpy 最低版本要求 "msgcenterpy": "0.1.8", # msgcenterpy 最低版本要求
} }
self.missing_packages = [] self.missing_packages = []

View File

@@ -21,15 +21,11 @@ __all__ = [
"get_class", "get_class",
"get_module", "get_module",
"init_from_list", "init_from_list",
"get_class_info_static", "get_enhanced_class_info",
"get_registry_class_info",
] ]
from ast import Constant
from unilabos.resources.resource_tracker import PARAM_SAMPLE_UUIDS from unilabos.resources.resource_tracker import PARAM_SAMPLE_UUIDS
from unilabos.utils import logger from unilabos.utils import logger
from unilabos.utils.decorator import is_not_action, is_always_free
class ImportManager: class ImportManager:
@@ -45,6 +41,7 @@ class ImportManager:
self._modules: Dict[str, Any] = {} self._modules: Dict[str, Any] = {}
self._classes: Dict[str, Type] = {} self._classes: Dict[str, Type] = {}
self._functions: Dict[str, Callable] = {} self._functions: Dict[str, Callable] = {}
self._search_miss: set = set()
if module_list: if module_list:
for module_path in module_list: for module_path in module_list:
@@ -159,193 +156,113 @@ class ImportManager:
Returns: Returns:
找到的类对象如果未找到则返回None 找到的类对象如果未找到则返回None
""" """
# 如果cls_name是builtins中的关键字则返回对应类
if class_name in builtins.__dict__: if class_name in builtins.__dict__:
return builtins.__dict__[class_name] return builtins.__dict__[class_name]
# 首先在已索引的类中查找
if class_name in self._classes: if class_name in self._classes:
return self._classes[class_name] return self._classes[class_name]
cache_key = class_name.lower() if search_lower else class_name
if cache_key in self._search_miss:
return None
if search_lower: if search_lower:
classes = {name.lower(): obj for name, obj in self._classes.items()} classes = {name.lower(): obj for name, obj in self._classes.items()}
if class_name in classes: if class_name in classes:
return classes[class_name] return classes[class_name]
# 遍历所有已加载的模块进行搜索
for module_path, module in self._modules.items(): for module_path, module in self._modules.items():
for name, obj in inspect.getmembers(module): for name, obj in inspect.getmembers(module):
if inspect.isclass(obj) and ( if inspect.isclass(obj) and (
(name.lower() == class_name.lower()) if search_lower else (name == class_name) (name.lower() == class_name.lower()) if search_lower else (name == class_name)
): ):
# 将找到的类添加到索引中
self._classes[name] = obj self._classes[name] = obj
self._classes[f"{module_path}:{name}"] = obj self._classes[f"{module_path}:{name}"] = obj
return obj return obj
self._search_miss.add(cache_key)
return None return None
def get_enhanced_class_info(self, module_path: str, use_dynamic: bool = True) -> Dict[str, Any]: def get_enhanced_class_info(self, module_path: str, **_kwargs) -> Dict[str, Any]:
""" """通过 AST 分析获取类的增强信息。
获取增强的类信息,支持动态导入和静态分析
复用 ``ast_registry_scanner`` 的 ``_collect_imports`` / ``_extract_class_body``
与 AST 扫描注册表完全一致。
Args: Args:
module_path: 模块路径,格式为 "module.path""module.path:ClassName" module_path: 格式 ``"module.path:ClassName"``
use_dynamic: 是否优先使用动态导入
Returns: Returns:
包含详细类信息的字典 ``{"module_path", "ast_analysis_success", "import_map",
"init_params", "status_methods", "action_methods"}``
""" """
result = { from unilabos.registry.ast_registry_scanner import (
_collect_imports,
_extract_class_body,
_filepath_to_module,
)
result: Dict[str, Any] = {
"module_path": module_path, "module_path": module_path,
"dynamic_import_success": False, "ast_analysis_success": False,
"static_analysis_success": False, "import_map": {},
"init_params": {}, "init_params": [],
"status_methods": {}, # get_ 开头和 @property 方法
"action_methods": {}, # set_ 开头和其他非_开头方法
}
# 尝试动态导入
dynamic_info = None
static_info = None
if use_dynamic:
try:
dynamic_info = self._get_dynamic_class_info(module_path)
result["dynamic_import_success"] = True
logger.debug(f"[ImportManager] 动态导入类 {module_path} 成功")
except Exception as e:
logger.warning(
f"[UniLab Registry] 在补充注册表时,动态导入类 "
f"{module_path} 失败(将使用静态分析,"
f"建议修复导入错误,以实现更好的注册表识别效果!): {e}"
)
use_dynamic = False
if not use_dynamic:
# 尝试静态分析
try:
static_info = self._get_static_class_info(module_path)
result["static_analysis_success"] = True
logger.debug(f"[ImportManager] 静态分析类 {module_path} 成功")
except Exception as e:
logger.warning(f"[ImportManager] 静态分析类 {module_path} 失败: {e}")
# 合并信息(优先使用动态导入的信息)
if dynamic_info:
result.update(dynamic_info)
elif static_info:
result.update(static_info)
return result
def _get_dynamic_class_info(self, class_path: str) -> Dict[str, Any]:
"""使用inspect模块动态获取类信息"""
cls = get_class(class_path)
class_name = cls.__name__
result = {
"class_name": class_name,
"init_params": self._analyze_method_signature(cls.__init__)["args"],
"status_methods": {}, "status_methods": {},
"action_methods": {}, "action_methods": {},
} }
# 分析类的所有成员
for name, method in cls.__dict__.items():
if name.startswith("_"):
continue
# 检查是否是property
if isinstance(method, property):
# @property 装饰的方法
# noinspection PyTypeChecker
return_type = self._get_return_type_from_method(method.fget) if method.fget else "Any"
prop_info = {
"name": name,
"return_type": return_type,
}
result["status_methods"][name] = prop_info
# 检查是否有对应的setter
if method.fset:
setter_info = self._analyze_method_signature(method.fset)
result["action_methods"][name] = setter_info
elif inspect.ismethod(method) or inspect.isfunction(method):
if name.startswith("get_"):
actual_name = name[4:] # 去掉get_前缀
if actual_name in result["status_methods"]:
continue
# get_ 开头的方法归类为status
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)
# 检查是否被 @always_free 装饰器标记
if is_always_free(method):
method_info["always_free"] = True
result["action_methods"][name] = method_info
return result
def _get_static_class_info(self, module_path: str) -> Dict[str, Any]:
"""使用AST静态分析获取类信息"""
module_name, class_name = module_path.rsplit(":", 1) module_name, class_name = module_path.rsplit(":", 1)
# 将模块路径转换为文件路径
file_path = self._module_path_to_file_path(module_name) file_path = self._module_path_to_file_path(module_name)
if not file_path or not os.path.exists(file_path): if not file_path or not os.path.exists(file_path):
raise FileNotFoundError(f"找不到模块文件: {module_name} -> {file_path}") logger.warning(f"[ImportManager] 找不到模块文件: {module_name} -> {file_path}")
return result
with open(file_path, "r", encoding="utf-8") as f: try:
source_code = f.read() with open(file_path, "r", encoding="utf-8") as f:
tree = ast.parse(f.read(), filename=file_path)
except Exception as e:
logger.warning(f"[ImportManager] 解析文件 {file_path} 失败: {e}")
return result
tree = ast.parse(source_code) # 推导 module dotted path → 构建 import_map
python_path = Path(file_path)
for sp in sorted(sys.path, key=len, reverse=True):
try:
Path(file_path).relative_to(sp)
python_path = Path(sp)
break
except ValueError:
continue
module_dotted = _filepath_to_module(Path(file_path), python_path)
import_map = _collect_imports(tree, module_dotted)
result["import_map"] = import_map
# 查找目标类 # 定位目标类 AST 节点
target_class = None target_class = None
for node in ast.walk(tree): for node in ast.walk(tree):
if isinstance(node, ast.ClassDef): if isinstance(node, ast.ClassDef) and node.name == class_name:
if node.name == class_name: target_class = node
target_class = node break
break
if target_class is None: if target_class is None:
raise AttributeError(f"在文件 {file_path} 中找不到类 {class_name}") logger.warning(f"[ImportManager] 在文件 {file_path} 中找不到类 {class_name}")
return result
result = { body = _extract_class_body(target_class, import_map)
"class_name": class_name,
"init_params": {}, # 映射到统一字段名(与 registry.py complete_registry 消费端一致)
"status_methods": {}, result["init_params"] = body.get("init_params", [])
"action_methods": {}, result["status_methods"] = body.get("status_properties", {})
result["action_methods"] = {
k: {
"args": v.get("params", []),
"return_type": v.get("return_type", ""),
"is_async": v.get("is_async", False),
"always_free": v.get("always_free", False),
"docstring": v.get("docstring"),
}
for k, v in body.get("auto_methods", {}).items()
} }
result["ast_analysis_success"] = True
# 分析类的方法
for node in target_class.body:
if isinstance(node, ast.FunctionDef):
method_info = self._analyze_method_node(node)
method_name = node.name
if method_name == "__init__":
result["init_params"] = method_info["args"]
elif method_name.startswith("_"):
continue
elif self._is_property_method(node):
# @property 装饰的方法
result["status_methods"][method_name] = method_info
elif method_name.startswith("get_"):
# get_ 开头的方法归类为status
actual_name = method_name[4:] # 去掉get_前缀
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
# 检查是否被 @always_free 装饰器标记
if self._is_always_free_method(node):
method_info["always_free"] = True
result["action_methods"][method_name] = method_info
return result return result
def _analyze_method_signature(self, method, skip_unilabos_params: bool = True) -> Dict[str, Any]: def _analyze_method_signature(self, method, skip_unilabos_params: bool = True) -> Dict[str, Any]:
@@ -401,23 +318,26 @@ class ImportManager:
"name": method.__name__, "name": method.__name__,
"args": args, "args": args,
"return_type": self._get_type_string(signature.return_annotation), "return_type": self._get_type_string(signature.return_annotation),
"return_annotation": signature.return_annotation, # 保留原始类型注解用于TypedDict等特殊处理
"is_async": inspect.iscoroutinefunction(method), "is_async": inspect.iscoroutinefunction(method),
} }
def _get_return_type_from_method(self, method) -> str: def _get_return_type_from_method(self, method) -> Union[str, Tuple[str, Any]]:
"""从方法中获取返回类型""" """从方法中获取返回类型"""
signature = inspect.signature(method) signature = inspect.signature(method)
return self._get_type_string(signature.return_annotation) return self._get_type_string(signature.return_annotation)
def _get_type_string(self, annotation) -> Union[str, Tuple[str, Any]]: def _get_type_string(self, annotation) -> Union[str, Tuple[str, Any]]:
"""将类型注解转换为Class Library中可搜索的类名""" """将类型注解转换为类型字符串。
非内建类返回 ``module:ClassName`` 全路径(如
``"unilabos.registry.placeholder_type:ResourceSlot"``
避免短名冲突;内建类型直接返回短名(如 ``"str"``、``"int"``)。
"""
if annotation == inspect.Parameter.empty: if annotation == inspect.Parameter.empty:
return "Any" # 如果没有注解返回Any return "Any"
if annotation is None: if annotation is None:
return "None" # 明确的None类型 return "None"
if hasattr(annotation, "__origin__"): if hasattr(annotation, "__origin__"):
# 处理typing模块的类型
origin = annotation.__origin__ origin = annotation.__origin__
if origin in (list, set, tuple): if origin in (list, set, tuple):
if hasattr(annotation, "__args__") and annotation.__args__: if hasattr(annotation, "__args__") and annotation.__args__:
@@ -432,126 +352,26 @@ class ImportManager:
return "dict" return "dict"
elif origin is Optional: elif origin is Optional:
return "Unknown" return "Unknown"
return f"Unknown" return "Unknown"
annotation_str = str(annotation) annotation_str = str(annotation)
# 处理typing模块的复杂类型
if "typing." in annotation_str: if "typing." in annotation_str:
# 简化typing类型显示
return ( return (
annotation_str.replace("typing.", "") annotation_str.replace("typing.", "")
if getattr(annotation, "_name", None) is None if getattr(annotation, "_name", None) is None
else annotation._name.lower() else annotation._name.lower()
) )
# 如果是类型对象
if hasattr(annotation, "__name__"): if hasattr(annotation, "__name__"):
# 如果是内置类型 module = getattr(annotation, "__module__", None)
if annotation.__module__ == "builtins": if module and module != "builtins":
return annotation.__name__ return f"{module}:{annotation.__name__}"
else: return annotation.__name__
# 如果是自定义类,返回完整路径
return f"{annotation.__module__}:{annotation.__name__}"
# 如果是typing模块的类型
elif hasattr(annotation, "_name"): elif hasattr(annotation, "_name"):
return annotation._name return annotation._name
# 如果是字符串形式的类型注解
elif isinstance(annotation, str): elif isinstance(annotation, str):
return annotation return annotation
else: else:
return annotation_str return annotation_str
def _is_property_method(self, node: ast.FunctionDef) -> bool:
"""检查是否是@property装饰的方法"""
for decorator in node.decorator_list:
if isinstance(decorator, ast.Name) and decorator.id == "property":
return True
return False
def _is_setter_method(self, node: ast.FunctionDef) -> bool:
"""检查是否是@xxx.setter装饰的方法"""
for decorator in node.decorator_list:
if isinstance(decorator, ast.Attribute) and decorator.attr == "setter":
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 _is_always_free_method(self, node: ast.FunctionDef) -> bool:
"""检查是否是@always_free装饰的方法"""
for decorator in node.decorator_list:
if isinstance(decorator, ast.Name) and decorator.id == "always_free":
return True
return False
def _get_property_name_from_setter(self, node: ast.FunctionDef) -> str:
"""从setter装饰器中获取属性名"""
for decorator in node.decorator_list:
if isinstance(decorator, ast.Attribute) and decorator.attr == "setter":
if isinstance(decorator.value, ast.Name):
return decorator.value.id
return node.name
def get_class_info_static(self, module_class_path: str) -> Dict[str, Any]:
"""
静态分析获取类的方法信息,不需要实际导入模块
Args:
module_class_path: 格式为 "module.path:ClassName" 的字符串
Returns:
包含类方法信息的字典
"""
try:
if ":" not in module_class_path:
raise ValueError("module_class_path必须是 'module.path:ClassName' 格式")
module_path, class_name = module_class_path.rsplit(":", 1)
# 将模块路径转换为文件路径
file_path = self._module_path_to_file_path(module_path)
if not file_path or not os.path.exists(file_path):
logger.warning(f"找不到模块文件: {module_path} -> {file_path}")
return {}
# 解析源码
with open(file_path, "r", encoding="utf-8") as f:
source_code = f.read()
tree = ast.parse(source_code)
# 查找目标类
class_node = None
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef) and node.name == class_name:
class_node = node
break
if not class_node:
logger.warning(f"在模块 {module_path} 中找不到类 {class_name}")
return {}
# 分析类的方法
methods_info = {}
for node in class_node.body:
if isinstance(node, ast.FunctionDef):
method_info = self._analyze_method_node(node)
methods_info[node.name] = method_info
return {
"class_name": class_name,
"module_path": module_path,
"file_path": file_path,
"methods": methods_info,
}
except Exception as e:
logger.error(f"静态分析类 {module_class_path} 时出错: {str(e)}")
return {}
def _module_path_to_file_path(self, module_path: str) -> Optional[str]: def _module_path_to_file_path(self, module_path: str) -> Optional[str]:
for path in sys.path: for path in sys.path:
potential_path = Path(path) / module_path.replace(".", "/") potential_path = Path(path) / module_path.replace(".", "/")
@@ -566,222 +386,6 @@ class ImportManager:
return None return None
def _analyze_method_node(self, node: ast.FunctionDef) -> Dict[str, Any]:
"""分析方法节点,提取参数和返回类型信息"""
method_info = {
"name": node.name,
"args": [],
"return_type": None,
"is_async": isinstance(node, ast.AsyncFunctionDef),
}
# 获取默认值列表
defaults = node.args.defaults
num_defaults = len(defaults)
# 计算必需参数数量
total_args = len(node.args.args)
num_required = total_args - num_defaults
# 提取参数信息
for i, arg in enumerate(node.args.args):
if arg.arg == "self":
continue
# 跳过 sample_uuids 参数(由系统自动注入)
if arg.arg == PARAM_SAMPLE_UUIDS:
continue
arg_info = {
"name": arg.arg,
"type": None,
"default": None,
"required": i < num_required,
}
# 提取类型注解
if arg.annotation:
arg_info["type"] = ast.unparse(arg.annotation) if hasattr(ast, "unparse") else str(arg.annotation)
# 提取默认值并推断类型
if i >= num_required:
default_index = i - num_required
if default_index < len(defaults):
default_value: Constant = defaults[default_index] # type: ignore
assert isinstance(default_value, Constant), "暂不支持对非常量类型进行推断,可反馈开源仓库"
arg_info["default"] = default_value.value
# 如果没有类型注解,尝试从默认值推断类型
if not arg_info["type"]:
arg_info["type"] = self._get_type_string(type(arg_info["default"]))
method_info["args"].append(arg_info)
# 提取返回类型
if node.returns:
method_info["return_type"] = ast.unparse(node.returns) if hasattr(ast, "unparse") else str(node.returns)
return method_info
def _infer_type_from_default(self, node: ast.AST) -> Optional[str]:
"""从默认值推断参数类型"""
if isinstance(node, ast.Constant):
value = node.value
if isinstance(value, bool):
return "bool"
elif isinstance(value, int):
return "int"
elif isinstance(value, float):
return "float"
elif isinstance(value, str):
return "str"
elif value is None:
return "Optional[Any]"
elif isinstance(node, ast.List):
return "List"
elif isinstance(node, ast.Dict):
return "Dict"
elif isinstance(node, ast.Tuple):
return "Tuple"
elif isinstance(node, ast.Set):
return "Set"
elif isinstance(node, ast.Name):
# 常见的默认值模式
if node.id in ["None"]:
return "Optional[Any]"
elif node.id in ["True", "False"]:
return "bool"
return None
def _infer_types_from_docstring(self, method_info: Dict[str, Any]) -> None:
"""从docstring中推断参数类型"""
docstring = method_info.get("docstring", "")
if not docstring:
return
lines = docstring.split("\n")
in_args_section = False
for line in lines:
line = line.strip()
# 检测Args或Arguments段落
if line.lower().startswith(("args:", "arguments:")):
in_args_section = True
continue
elif line.startswith(("returns:", "return:", "yields:", "raises:")):
in_args_section = False
continue
elif not line or not in_args_section:
continue
# 解析参数行,格式通常是: param_name (type): description 或 param_name: description
if ":" in line:
parts = line.split(":", 1)
param_part = parts[0].strip()
# 提取参数名和类型
param_name = None
param_type = None
if "(" in param_part and ")" in param_part:
# 格式: param_name (type)
param_name = param_part.split("(")[0].strip()
type_part = param_part.split("(")[1].split(")")[0].strip()
param_type = type_part
else:
# 格式: param_name
param_name = param_part
# 更新对应参数的类型信息
if param_name:
for arg_info in method_info["args"]:
if arg_info["name"] == param_name and not arg_info["type"]:
if param_type:
arg_info["inferred_type"] = param_type
elif not arg_info["inferred_type"]:
# 从描述中推断类型
description = parts[1].strip().lower()
if any(word in description for word in ["path", "file", "directory", "filename"]):
arg_info["inferred_type"] = "str"
elif any(
word in description for word in ["port", "number", "count", "size", "length"]
):
arg_info["inferred_type"] = "int"
elif any(
word in description for word in ["rate", "ratio", "percentage", "temperature"]
):
arg_info["inferred_type"] = "float"
elif any(word in description for word in ["flag", "enable", "disable", "option"]):
arg_info["inferred_type"] = "bool"
def get_registry_class_info(self, module_class_path: str) -> Dict[str, Any]:
"""
获取适用于注册表的类信息,包含完整的类型推断
Args:
module_class_path: 格式为 "module.path:ClassName" 的字符串
Returns:
适用于注册表的类信息字典
"""
class_info = self.get_class_info_static(module_class_path)
if not class_info:
return {}
registry_info = {
"class_name": class_info["class_name"],
"module_path": class_info["module_path"],
"file_path": class_info["file_path"],
"methods": {},
"properties": [],
"init_params": {},
"action_methods": {},
}
for method_name, method_info in class_info["methods"].items():
# 分类处理不同类型的方法
if method_info["is_property"]:
registry_info["properties"].append(
{
"name": method_name,
"return_type": method_info.get("return_type"),
"docstring": method_info.get("docstring"),
}
)
elif method_name == "__init__":
# 处理初始化参数
init_params = {}
for arg in method_info["args"]:
if arg["name"] != "self":
param_info = {
"name": arg["name"],
"type": arg.get("type") or arg.get("inferred_type"),
"required": arg.get("is_required", True),
"default": arg.get("default"),
}
init_params[arg["name"]] = param_info
registry_info["init_params"] = init_params
elif not method_name.startswith("_"):
# 处理公共方法可能的action方法
action_info = {
"name": method_name,
"params": {},
"return_type": method_info.get("return_type"),
"docstring": method_info.get("docstring"),
"num_required": method_info.get("num_required", 0) - 1, # 减去self
"num_defaults": method_info.get("num_defaults", 0),
}
for arg in method_info["args"]:
if arg["name"] != "self":
param_info = {
"name": arg["name"],
"type": arg.get("type") or arg.get("inferred_type"),
"required": arg.get("is_required", True),
"default": arg.get("default"),
}
action_info["params"][arg["name"]] = param_info
registry_info["action_methods"][method_name] = action_info
return registry_info
# 全局实例,便于直接使用 # 全局实例,便于直接使用
@@ -809,16 +413,6 @@ def init_from_list(module_list: List[str]) -> None:
default_manager = ImportManager(module_list) default_manager = ImportManager(module_list)
def get_class_info_static(module_class_path: str) -> Dict[str, Any]: def get_enhanced_class_info(module_path: str, **kwargs) -> Dict[str, Any]:
"""静态分析获取类信息的便捷函数"""
return default_manager.get_class_info_static(module_class_path)
def get_registry_class_info(module_class_path: str) -> Dict[str, Any]:
"""获取适用于注册表的类信息的便捷函数"""
return default_manager.get_registry_class_info(module_class_path)
def get_enhanced_class_info(module_path: str, use_dynamic: bool = True) -> Dict[str, Any]:
"""获取增强的类信息的便捷函数""" """获取增强的类信息的便捷函数"""
return default_manager.get_enhanced_class_info(module_path, use_dynamic) return default_manager.get_enhanced_class_info(module_path, **kwargs)

View File

@@ -217,7 +217,6 @@ def configure_logger(loglevel=None, working_dir=None):
return log_filepath return log_filepath
# 配置日志系统 # 配置日志系统
configure_logger() configure_logger()

View File

@@ -1,7 +1,8 @@
networkx networkx
typing_extensions typing_extensions
websockets websockets
msgcenterpy>=0.1.5 msgcenterpy>=0.1.8
orjson>=3.11
opentrons_shared_data opentrons_shared_data
pint pint
fastapi fastapi

View File

@@ -1,3 +1,38 @@
import json
from unilabos.utils.type_check import TypeEncoder, json_default
try:
import orjson
def fast_dumps(obj, **kwargs) -> bytes:
"""JSON 序列化为 bytes优先使用 orjson。"""
return orjson.dumps(obj, option=orjson.OPT_NON_STR_KEYS, default=json_default)
def fast_dumps_pretty(obj, **kwargs) -> bytes:
"""JSON 序列化为 bytes带缩进优先使用 orjson。"""
return orjson.dumps(
obj,
option=orjson.OPT_NON_STR_KEYS | orjson.OPT_INDENT_2,
default=json_default,
)
def normalize_json(info: dict) -> dict:
"""经 JSON 序列化/反序列化一轮来清理非标准类型。"""
return orjson.loads(orjson.dumps(info, default=json_default))
except ImportError:
def fast_dumps(obj, **kwargs) -> bytes: # type: ignore[misc]
return json.dumps(obj, ensure_ascii=False, cls=TypeEncoder).encode("utf-8")
def fast_dumps_pretty(obj, **kwargs) -> bytes: # type: ignore[misc]
return json.dumps(obj, indent=2, ensure_ascii=False, cls=TypeEncoder).encode("utf-8")
def normalize_json(info: dict) -> dict: # type: ignore[misc]
return json.loads(json.dumps(info, ensure_ascii=False, cls=TypeEncoder))
# 辅助函数将UUID数组转换为字符串 # 辅助函数将UUID数组转换为字符串
def uuid_to_str(uuid_array) -> str: def uuid_to_str(uuid_array) -> str:
"""将UUID字节数组转换为十六进制字符串""" """将UUID字节数组转换为十六进制字符串"""

View File

@@ -15,14 +15,21 @@ def get_type_class(type_hint):
return final_type return final_type
def json_default(obj):
"""将 type 对象序列化为类名,其余 fallback 到 str()。"""
if isinstance(obj, type):
return str(obj)[8:-2]
return str(obj)
class TypeEncoder(json.JSONEncoder): class TypeEncoder(json.JSONEncoder):
"""自定义JSON编码器处理特殊类型""" """自定义JSON编码器处理特殊类型"""
def default(self, obj): def default(self, obj):
# 优先处理类型对象 try:
if isinstance(obj, type): return json_default(obj)
return str(obj)[8:-2] except Exception:
return super().default(obj) return super().default(obj)
class NoAliasDumper(yaml.SafeDumper): class NoAliasDumper(yaml.SafeDumper):
@@ -43,13 +50,10 @@ class ResultInfoEncoder(json.JSONEncoder):
"""专门用于处理任务执行结果信息的JSON编码器""" """专门用于处理任务执行结果信息的JSON编码器"""
def default(self, obj): def default(self, obj):
# 优先处理类型对象
if isinstance(obj, type): if isinstance(obj, type):
return str(obj)[8:-2] return json_default(obj)
# 对于无法序列化的对象,统一转换为字符串
try: try:
# 尝试调用 __dict__ 或者其他序列化方法
if hasattr(obj, "__dict__"): if hasattr(obj, "__dict__"):
return obj.__dict__ return obj.__dict__
elif hasattr(obj, "_asdict"): # namedtuple elif hasattr(obj, "_asdict"): # namedtuple
@@ -59,10 +63,8 @@ class ResultInfoEncoder(json.JSONEncoder):
elif hasattr(obj, "dict"): elif hasattr(obj, "dict"):
return obj.dict() return obj.dict()
else: else:
# 如果都不行,转换为字符串
return str(obj) return str(obj)
except Exception: except Exception:
# 如果转换失败,直接返回字符串表示
return str(obj) return str(obj)

View File

@@ -2,7 +2,7 @@
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3"> <package format="3">
<name>unilabos_msgs</name> <name>unilabos_msgs</name>
<version>0.10.18</version> <version>0.10.19</version>
<description>ROS2 Messages package for unilabos devices</description> <description>ROS2 Messages package for unilabos devices</description>
<maintainer email="changjh@pku.edu.cn">Junhan Chang</maintainer> <maintainer email="changjh@pku.edu.cn">Junhan Chang</maintainer>
<maintainer email="18435084+Xuwznln@users.noreply.github.com">Xuwznln</maintainer> <maintainer email="18435084+Xuwznln@users.noreply.github.com">Xuwznln</maintainer>