diff --git a/.conda/base/recipe.yaml b/.conda/base/recipe.yaml index cf908c9d..a63dda77 100644 --- a/.conda/base/recipe.yaml +++ b/.conda/base/recipe.yaml @@ -3,7 +3,7 @@ package: name: unilabos - version: 0.10.18 + version: 0.10.19 source: path: ../../unilabos @@ -54,7 +54,7 @@ requirements: - pymodbus - matplotlib - pylibftdi - - uni-lab::unilabos-env ==0.10.18 + - uni-lab::unilabos-env ==0.10.19 about: repository: https://github.com/deepmodeling/Uni-Lab-OS diff --git a/.conda/environment/recipe.yaml b/.conda/environment/recipe.yaml index 56ff44d4..e9fd3e24 100644 --- a/.conda/environment/recipe.yaml +++ b/.conda/environment/recipe.yaml @@ -2,7 +2,7 @@ package: name: unilabos-env - version: 0.10.18 + version: 0.10.19 build: noarch: generic diff --git a/.conda/full/recipe.yaml b/.conda/full/recipe.yaml index ff8b4824..ab0e0c9f 100644 --- a/.conda/full/recipe.yaml +++ b/.conda/full/recipe.yaml @@ -3,7 +3,7 @@ package: name: unilabos-full - version: 0.10.18 + version: 0.10.19 build: noarch: generic @@ -11,7 +11,7 @@ build: requirements: run: # Base unilabos package (includes unilabos-env) - - uni-lab::unilabos ==0.10.18 + - uni-lab::unilabos ==0.10.19 # Documentation tools - sphinx - sphinx_rtd_theme diff --git a/.cursor/skills/create-device-skill/SKILL.md b/.cursor/skills/create-device-skill/SKILL.md new file mode 100644 index 00000000..8f524141 --- /dev/null +++ b/.cursor/skills/create-device-skill/SKILL.md @@ -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": "", "class": { "module": "", "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 +# 或从 config.py 提取 +python ./scripts/gen_auth.py --config +``` + +认证算法:`base64(ak:sk)` → `Authorization: Lab ` + +#### 必备项 ②:--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` | `/unilabos_data/`(若子目录已存在则直接用) | +| 仅传了 `--config` | `/unilabos_data/` | +| 都没传 | `<当前工作目录>/unilabos_data/` | + +**按优先级搜索文件**: + +``` +<推断的 working_dir>/unilabos_data/req_device_registry_upload.json +<推断的 working_dir>/req_device_registry_upload.json +/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 +``` + +脚本输出包含每个设备的 **Python 源码路径**(从 `class.module` 转换),可用于后续阅读源码理解参数含义。 + +### Step 2 — 提取 Action Schema + +用户选择设备后,运行提取脚本: + +```bash +python ./scripts/extract_device_actions.py [--registry ] ./skills//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 +### `` + +<用途描述(一句话)> + +- **Schema**: [`actions/.json`](actions/.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 `(不要硬编码 `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.md # API 端点 + 渐进加载指引 +├── action-index.md # 动作索引:描述/用途/核心参数 +└── actions/ # 每个 action 的完整 JSON Schema + ├── action1.json + ├── action2.json + └── ... +``` diff --git a/.cursor/skills/create-device-skill/scripts/extract_device_actions.py b/.cursor/skills/create-device-skill/scripts/extract_device_actions.py new file mode 100644 index 00000000..c17f6102 --- /dev/null +++ b/.cursor/skills/create-device-skill/scripts/extract_device_actions.py @@ -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 + + # 提取指定设备的 action 到目录 + python extract_device_actions.py + python extract_device_actions.py --registry + +示例: + 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. /unilabos_data/req_device_registry_upload.json + 3. /req_device_registry_upload.json + 4. /../../.. (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 ") + print() + print("搜索过的路径:") + for p in [ + os.path.join("unilabos_data", REGISTRY_FILENAME), + REGISTRY_FILENAME, + os.path.join("", "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 ] # 列出设备") + print(" python extract_device_actions.py [--registry ] # 提取 actions") + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/.cursor/skills/create-device-skill/scripts/gen_auth.py b/.cursor/skills/create-device-skill/scripts/gen_auth.py new file mode 100644 index 00000000..f0cb9c9b --- /dev/null +++ b/.cursor/skills/create-device-skill/scripts/gen_auth.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +从 ak/sk 生成 UniLab API Authorization header。 + +算法: base64(ak:sk) → "Authorization: Lab " + +用法: + python gen_auth.py + python gen_auth.py --config + +示例: + 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 ") + print(" python gen_auth.py --config ") + 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() diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 57245d94..402edc26 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -49,7 +49,7 @@ jobs: uv pip uninstall enum34 || echo enum34 not installed, skipping uv pip install . - - name: Run check mode (complete_registry) + - name: Run check mode (AST registry validation) run: | call conda activate check-env echo Running check mode... diff --git a/.gitignore b/.gitignore index 838331e3..12b344d6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ output/ unilabos_data/ pyrightconfig.json .cursorignore +device_package*/ ## Python # Byte-compiled / optimized / DLL files diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..2f9efa06 --- /dev/null +++ b/AGENTS.md @@ -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 --config --backend ros +unilab --graph --config --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 -n --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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..bd5ce566 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,4 @@ + +Please follow the rules defined in: + +@AGENTS.md diff --git a/docs/developer_guide/add_device.md b/docs/developer_guide/add_device.md index dc95274f..15ba4e08 100644 --- a/docs/developer_guide/add_device.md +++ b/docs/developer_guide/add_device.md @@ -15,6 +15,9 @@ Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 中使用, **示例:** ```python +from unilabos.registry.decorators import device, topic_config + +@device(id="mock_gripper", category=["gripper"], description="Mock Gripper") class MockGripper: def __init__(self): self._position: float = 0.0 @@ -23,19 +26,23 @@ class MockGripper: self._status = "Idle" @property + @topic_config() # 添加 @topic_config 才会定时广播 def position(self) -> float: return self._position @property + @topic_config() def velocity(self) -> float: return self._velocity @property + @topic_config() def torque(self) -> float: return self._torque - # 会被自动识别的设备属性,接入 Uni-Lab 时会定时对外广播 + # 使用 @topic_config 装饰的属性,接入 Uni-Lab 时会定时对外广播 @property + @topic_config(period=2.0) # 可自定义发布周期 def status(self) -> str: return self._status @@ -149,7 +156,7 @@ my_device: # 设备唯一标识符 系统会自动分析您的 Python 驱动类并生成: -- `status_types`:从 `@property` 装饰的方法自动识别状态属性 +- `status_types`:从 `@topic_config` 装饰的 `@property` 或方法自动识别状态属性 - `action_value_mappings`:从类方法自动生成动作映射 - `init_param_schema`:从 `__init__` 方法分析初始化参数 - `schema`:前端显示用的属性类型定义 @@ -179,7 +186,9 @@ Uni-Lab 设备驱动是一个 Python 类,需要遵循以下结构: ```python from typing import Dict, Any +from unilabos.registry.decorators import device, topic_config +@device(id="my_device", category=["general"], description="My Device") class MyDevice: """设备类文档字符串 @@ -198,8 +207,9 @@ class MyDevice: # 初始化硬件连接 @property + @topic_config() # 必须添加 @topic_config 才会广播 def status(self) -> str: - """设备状态(会自动广播)""" + """设备状态(通过 @topic_config 广播)""" return self._status def my_action(self, param: float) -> Dict[str, Any]: @@ -217,34 +227,61 @@ class MyDevice: ## 状态属性 vs 动作方法 -### 状态属性(@property) +### 状态属性(@property + @topic_config) -状态属性会被自动识别并定期广播: +状态属性需要同时使用 `@property` 和 `@topic_config` 装饰器才会被识别并定期广播: ```python +from unilabos.registry.decorators import topic_config + @property +@topic_config() # 必须添加,否则不会广播 def temperature(self) -> float: """当前温度""" return self._read_temperature() @property +@topic_config(period=2.0) # 可自定义发布周期(秒) def status(self) -> str: """设备状态: idle, running, error""" return self._status @property +@topic_config(name="ready") # 可自定义发布名称 def is_ready(self) -> bool: """设备是否就绪""" 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`装饰器 -- 只读,不能有参数 -- 自动添加到注册表的`status_types` +- 必须使用 `@topic_config` 装饰器 +- 支持 `@property` 和普通方法 +- 添加到注册表的 `status_types` - 定期发布到 ROS2 topic +> **⚠️ 重要:** 仅有 `@property` 装饰器而没有 `@topic_config` 的属性**不会**被广播。这是一个 Breaking Change。 + ### 动作方法 动作方法是设备可以执行的操作: @@ -497,6 +534,7 @@ class LiquidHandler: self._status = "idle" @property + @topic_config() def status(self) -> str: 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 from typing import Dict, Any, Optional, List @@ -901,7 +984,7 @@ def method( pass ``` -### 2. 文档字符串 +### 4. 文档字符串 ```python def method(self, param: float) -> Dict[str, Any]: @@ -923,7 +1006,7 @@ def method(self, param: float) -> Dict[str, Any]: pass ``` -### 3. 配置验证 +### 5. 配置验证 ```python def __init__(self, config: Dict[str, Any]): @@ -937,7 +1020,7 @@ def __init__(self, config: Dict[str, Any]): self.baudrate = config['baudrate'] ``` -### 4. 资源清理 +### 6. 资源清理 ```python def __del__(self): @@ -946,7 +1029,7 @@ def __del__(self): self.connection.close() ``` -### 5. 设计前端友好的返回值 +### 7. 设计前端友好的返回值 **记住:返回值会直接显示在 Web 界面** diff --git a/docs/developer_guide/add_registry.md b/docs/developer_guide/add_registry.md index 36caa943..38d3f893 100644 --- a/docs/developer_guide/add_registry.md +++ b/docs/developer_guide/add_registry.md @@ -422,18 +422,20 @@ placeholder_keys: ### status_types -系统会扫描你的 Python 类,从状态方法(property 或 get\_方法)自动生成这部分: +系统会扫描你的 Python 类,从带有 `@topic_config` 装饰器的 `@property` 或方法自动生成这部分: ```yaml status_types: - current_temperature: float # 从 get_current_temperature() 或 @property current_temperature - is_heating: bool # 从 get_is_heating() 或 @property is_heating - status: str # 从 get_status() 或 @property status + current_temperature: float # 从 @topic_config 装饰的 @property 或方法 + is_heating: bool + status: str ``` **注意事项**: -- 系统会查找所有 `get_` 开头的方法和 `@property` 装饰的属性 +- 仅有带 `@topic_config` 装饰器的 `@property` 或方法才会被识别为状态属性 +- 没有 `@topic_config` 的 `@property` 不会生成 status_types,也不会广播 +- `get_` 前缀的方法名会自动去除前缀(如 `get_temperature` → `temperature`) - 类型会自动转成相应的类型(如 `str`、`float`、`bool`) - 如果类型是 `Any`、`None` 或未知的,默认使用 `String` @@ -537,11 +539,13 @@ class AdvancedLiquidHandler: self._temperature = 25.0 @property + @topic_config() def status(self) -> str: """设备状态""" return self._status @property + @topic_config() def temperature(self) -> float: """当前温度""" return self._temperature @@ -809,21 +813,23 @@ my_temperature_controller: 你的设备类需要符合以下要求: ```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): """初始化,参数会自动分析到 init_param_schema.config""" - super().__init__(config) self.port = config.get('port', '/dev/ttyUSB0') - # 状态方法(会自动生成到 status_types) + # 状态方法(必须添加 @topic_config 才会生成到 status_types 并广播) @property + @topic_config() def status(self): """返回设备状态""" return "idle" @property + @topic_config() def temperature(self): """返回当前温度""" 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 # ✓ 好 @@ -1051,7 +1084,7 @@ def method(self, resource, device): pass ``` -2. **提供有意义的参数名** +4. **提供有意义的参数名** ```python # ✓ 好 - 清晰的参数名 @@ -1063,7 +1096,7 @@ def transfer(self, r1: ResourceSlot, r2: ResourceSlot): pass ``` -3. **使用 Optional 表示可选参数** +5. **使用 Optional 表示可选参数** ```python from typing import Optional @@ -1076,7 +1109,7 @@ def method( pass ``` -4. **添加详细的文档字符串** +6. **添加详细的文档字符串** ```python def method( @@ -1096,13 +1129,13 @@ def method( pass ``` -5. **方法命名规范** +7. **方法命名规范** - - 状态方法使用 `@property` 装饰器或 `get_` 前缀 + - 状态方法使用 `@property` + `@topic_config` 装饰器,或普通方法 + `@topic_config` - 动作方法使用动词开头 - 保持命名清晰、一致 -6. **完善的错误处理** +8. **完善的错误处理** - 实现完善的错误处理 - 添加日志记录 - 提供有意义的错误信息 diff --git a/docs/developer_guide/networking_overview.md b/docs/developer_guide/networking_overview.md index 40b308d3..19f16312 100644 --- a/docs/developer_guide/networking_overview.md +++ b/docs/developer_guide/networking_overview.md @@ -221,10 +221,10 @@ Laboratory A Laboratory B ```bash # 实验室A -unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource +unilab --ak your_ak --sk your_sk --upload_registry # 实验室B -unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource +unilab --ak your_ak --sk your_sk --upload_registry ``` --- diff --git a/docs/user_guide/launch.md b/docs/user_guide/launch.md index 402e39aa..34caa5b9 100644 --- a/docs/user_guide/launch.md +++ b/docs/user_guide/launch.md @@ -22,7 +22,6 @@ options: --is_slave Run the backend as slave node (without host privileges). --slave_no_host Skip waiting for host service in slave mode --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 --port PORT Port for web service information page --disable_browser Disable opening information page on startup @@ -85,7 +84,7 @@ Uni-Lab 的启动过程分为以下几个阶段: 支持两种方式: - **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式) -- **远程资源**:使用 `--use_remote_resource` 从云端获取 +- **远程资源**:不指定本地文件即可 ### 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 --use_remote_resource +unilab --ak your_ak --sk your_sk # 更新注册表 unilab --ak your_ak --sk your_sk --complete_registry diff --git a/recipes/msgs/recipe.yaml b/recipes/msgs/recipe.yaml index a3c2d2bd..fc8a5ccf 100644 --- a/recipes/msgs/recipe.yaml +++ b/recipes/msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.10.18 + version: 0.10.19 source: path: ../../unilabos_msgs target_directory: src diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index aeb76a0c..91e07b24 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.10.18" + version: "0.10.19" source: path: ../.. diff --git a/setup.py b/setup.py index dc7bbc73..7ca06f2e 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ package_name = 'unilabos' setup( name=package_name, - version='0.10.18', + version='0.10.19', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/unilabos/__init__.py b/unilabos/__init__.py index 63e3face..eebdd757 100644 --- a/unilabos/__init__.py +++ b/unilabos/__init__.py @@ -1 +1 @@ -__version__ = "0.10.18" +__version__ = "0.10.19" diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 93751262..fa7bc35d 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -4,6 +4,7 @@ import os import platform import shutil import signal +import subprocess import sys import threading import time @@ -25,6 +26,84 @@ from unilabos.config.config import load_config, BasicConfig, HTTPConfig _restart_requested: bool = False _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): if config_path is None: @@ -66,6 +145,13 @@ def parse_args(): action="append", 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( "--working_dir", type=str, @@ -155,18 +241,18 @@ def parse_args(): action="store_true", help="Skip environment dependency check on startup", ) - parser.add_argument( - "--complete_registry", - action="store_true", - default=False, - help="Complete registry information", - ) parser.add_argument( "--check_mode", action="store_true", default=False, 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( "--no_update_feedback", action="store_true", @@ -178,6 +264,24 @@ def parse_args(): default=False, 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_parser = subparsers.add_parser( "workflow_upload", @@ -228,6 +332,11 @@ def main(): args = parser.parse_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) check_mode = args_dict.get("check_mode", False) @@ -358,6 +467,9 @@ def main(): BasicConfig.test_mode = args_dict.get("test_mode", False) if BasicConfig.test_mode: 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" machine_name = platform.node() 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) - # 注册表 - 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 - 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: - 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) + # Step 1: 上传全部注册表到服务端,同步保存到 unilabos_data if BasicConfig.upload_registry: - # 设备注册到服务端 - 需要 ak 和 sk if BasicConfig.ak and BasicConfig.sk: - print_status("开始注册设备到服务端...", "info") + # print_status("开始注册设备到服务端...", "info") try: register_devices_and_resources(lab_registry) - print_status("设备注册完成", "info") + # print_status("设备注册完成", "info") except Exception as e: print_status(f"设备注册失败: {e}", "error") else: @@ -482,7 +604,7 @@ def main(): 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") remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"]) resource_tree_set.merge_remote_resources(remote_tree_set) @@ -579,6 +701,10 @@ def main(): open_browser=not args_dict["disable_browser"], 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__": diff --git a/unilabos/app/register.py b/unilabos/app/register.py index 5918b43a..5940364e 100644 --- a/unilabos/app/register.py +++ b/unilabos/app/register.py @@ -1,9 +1,8 @@ -import json 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.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]]]: @@ -11,50 +10,63 @@ def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[ 注册设备和资源到服务器(仅支持HTTP) """ - # 注册资源信息 - 使用HTTP方式 from unilabos.app.web.client import http_client logger.info("[UniLab Register] 开始注册设备和资源...") - # 注册设备信息 devices_to_register = {} for device_info in lab_registry.obtain_registry_device_info(): - devices_to_register[device_info["id"]] = json.loads( - json.dumps(device_info, ensure_ascii=False, cls=TypeEncoder) - ) - logger.debug(f"[UniLab Register] 收集设备: {device_info['id']}") + devices_to_register[device_info["id"]] = _normalize_device(device_info) + logger.trace(f"[UniLab Register] 收集设备: {device_info['id']}") resources_to_register = {} for resource_info in lab_registry.obtain_registry_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: return devices_to_register, resources_to_register - # 注册设备 + if devices_to_register: try: 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 - if response.status_code in [200, 201]: - logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}s") + res_data = response.json() if response.status_code == 200 else {} + 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: - 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: logger.error(f"[UniLab Register] 设备注册异常: {e}") - # 注册资源 if resources_to_register: try: 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 - if response.status_code in [200, 201]: - logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}s") + res_data = response.json() if response.status_code == 200 else {} + 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: - 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: logger.error(f"[UniLab Register] 资源注册异常: {e}") - - logger.info("[UniLab Register] 设备和资源注册完成.") diff --git a/unilabos/app/web/api.py b/unilabos/app/web/api.py index 0f6077c8..99981f77 100644 --- a/unilabos/app/web/api.py +++ b/unilabos/app/web/api.py @@ -1052,7 +1052,7 @@ async def handle_file_import(websocket: WebSocket, request_data: dict): "result": {}, "schema": lab_registry._generate_unilab_json_command_schema(v["args"], k), "goal_default": {i["name"]: i["default"] for i in v["args"]}, - "handles": [], + "handles": {}, } # 不生成已配置action的动作 for k, v in enhanced_info["action_methods"].items() @@ -1340,5 +1340,5 @@ def setup_api_routes(app): # 启动广播任务 @app.on_event("startup") async def startup_event(): - asyncio.create_task(broadcast_device_status()) - asyncio.create_task(broadcast_status_page_data()) + asyncio.create_task(broadcast_device_status(), name="web-api-startup-device") + asyncio.create_task(broadcast_status_page_data(), name="web-api-startup-status") diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index b43b0f44..b1cc67eb 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -3,11 +3,13 @@ HTTP客户端模块 提供与远程服务器通信的客户端功能,只有host需要用 """ - +import gzip import json import os 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 from unilabos.resources.resource_tracker import ResourceTreeSet from unilabos.utils.log import info @@ -280,22 +282,54 @@ class HTTPClient: ) 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: registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}] + tag: 保存文件的标签后缀 (如 "device_registry" / "resource_registry") Returns: Response: API响应对象 """ + # 序列化一次,同时用于保存和发送 + 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( f"{self.remote_addr}/lab/resource", - json=registry_data, - headers={"Authorization": f"Lab {self.auth}"}, + data=compressed_body, + headers=headers, 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]: logger.error(f"注册资源失败: {response.status_code}, {response.text}") if response.status_code == 200: diff --git a/unilabos/app/web/server.py b/unilabos/app/web/server.py index 8d090162..981edeca 100644 --- a/unilabos/app/web/server.py +++ b/unilabos/app/web/server.py @@ -86,7 +86,7 @@ def setup_server() -> FastAPI: # 设置页面路由 try: setup_web_pages(pages) - info("[Web] 已加载Web UI模块") + # info("[Web] 已加载Web UI模块") except ImportError as e: info(f"[Web] 未找到Web页面模块: {str(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.start() - info("[Web] Server started, monitoring for restart requests...") + # info("[Web] Server started, monitoring for restart requests...") # 监控重启标志 import unilabos.app.main as main_module diff --git a/unilabos/app/ws_client.py b/unilabos/app/ws_client.py index 2a7f9b15..cbbb58ef 100644 --- a/unilabos/app/ws_client.py +++ b/unilabos/app/ws_client.py @@ -26,6 +26,7 @@ from enum import Enum from typing_extensions import TypedDict from unilabos.app.model import JobAddReq +from unilabos.resources.resource_tracker import ResourceDictType from unilabos.ros.nodes.presets.host_node import HostNode from unilabos.utils.type_check import serialize_result_info from unilabos.app.communication import BaseCommunicationClient @@ -408,6 +409,7 @@ class MessageProcessor: # 线程控制 self.is_running = False self.thread = None + self._loop = None # asyncio event loop引用,用于外部关闭websocket self.reconnect_count = 0 logger.info(f"[MessageProcessor] Initialized for URL: {websocket_url}") @@ -434,22 +436,31 @@ class MessageProcessor: def stop(self) -> None: """停止消息处理线程""" 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(): self.thread.join(timeout=2) logger.info("[MessageProcessor] Stopped") def _run(self): """运行消息处理主循环""" - loop = asyncio.new_event_loop() + self._loop = asyncio.new_event_loop() try: - asyncio.set_event_loop(loop) - loop.run_until_complete(self._connection_handler()) + asyncio.set_event_loop(self._loop) + self._loop.run_until_complete(self._connection_handler()) except Exception as e: logger.error(f"[MessageProcessor] Thread error: {str(e)}") logger.error(traceback.format_exc()) finally: - if loop: - loop.close() + if self._loop: + self._loop.close() + self._loop = None async def _connection_handler(self): """处理WebSocket连接和重连逻辑""" @@ -466,8 +477,10 @@ class MessageProcessor: async with websockets.connect( self.websocket_url, ssl=ssl_context, + open_timeout=20, ping_interval=WSConfig.ping_interval, ping_timeout=10, + close_timeout=5, additional_headers={ "Authorization": f"Lab {BasicConfig.auth_secret()}", "EdgeSession": f"{self.session_id}", @@ -478,85 +491,98 @@ class MessageProcessor: self.connected = True self.reconnect_count = 0 - logger.info(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: # 接收消息循环 await self._message_handler() finally: + # 必须在 async with __aexit__ 之前停止 send_task, + # 否则 send_task 会在关闭握手期间继续发送数据, + # 干扰 websockets 库的内部清理,导致 task 泄漏。 + self.connected = False send_task.cancel() try: await send_task except asyncio.CancelledError: pass - self.connected = False except websockets.exceptions.ConnectionClosed: - logger.warning("[MessageProcessor] Connection closed") - self.connected = False + logger.warning("[MessageProcessor] 与服务端连接中断") + except TimeoutError: + logger.warning( + f"[MessageProcessor] 与服务端连接通信超时 (已尝试 {self.reconnect_count + 1} 次),请检查您的网络状况" + ) + except websockets.exceptions.InvalidStatus as e: + logger.warning( + f"[MessageProcessor] 收到服务端注册码 {e.response.status_code}, 上一进程可能还未退出" + ) except Exception as e: - logger.error(f"[MessageProcessor] Connection error: {str(e)}") logger.error(traceback.format_exc()) - self.connected = False + logger.error(f"[MessageProcessor] 尝试重连时出错 {str(e)}") finally: + self.connected = False self.websocket = None # 重连逻辑 - if self.is_running and self.reconnect_count < WSConfig.max_reconnect_attempts: + if not self.is_running: + break + if self.reconnect_count < WSConfig.max_reconnect_attempts: self.reconnect_count += 1 + backoff = WSConfig.reconnect_interval logger.info( - f"[MessageProcessor] Reconnecting in {WSConfig.reconnect_interval}s " - f"(attempt {self.reconnect_count}/{WSConfig.max_reconnect_attempts})" + f"[MessageProcessor] 即将在 {backoff} 秒后重连 (已尝试 {self.reconnect_count}/{WSConfig.max_reconnect_attempts})" ) - await asyncio.sleep(WSConfig.reconnect_interval) - elif self.reconnect_count >= WSConfig.max_reconnect_attempts: + await asyncio.sleep(backoff) + else: logger.error("[MessageProcessor] Max reconnection attempts reached") break - else: - self.reconnect_count -= 1 async def _message_handler(self): - """处理接收到的消息""" + """处理接收到的消息。 + + ConnectionClosed 不在此处捕获,让其向上传播到 _connection_handler, + 以便 async with websockets.connect() 的 __aexit__ 能感知连接已断, + 正确清理内部 task,避免 task 泄漏。 + """ if not self.websocket: logger.error("[MessageProcessor] WebSocket connection is None") return - try: - async for message in self.websocket: - try: - data = json.loads(message) - message_type = data.get("action", "") - message_data = data.get("data") - if self.session_id and self.session_id == data.get("edge_session"): - await self._process_message(message_type, message_data) + async for message in self.websocket: + try: + data = json.loads(message) + message_type = data.get("action", "") + message_data = data.get("data") + if self.session_id and self.session_id == data.get("edge_session"): + 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: - 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: - 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()) + 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()) async def _send_handler(self): """处理发送队列中的消息""" - logger.debug("[MessageProcessor] Send handler started") + logger.trace("[MessageProcessor] Send handler started") try: while self.connected and self.websocket: @@ -601,6 +627,7 @@ class MessageProcessor: except asyncio.CancelledError: logger.debug("[MessageProcessor] Send handler cancelled") + raise except Exception as e: logger.error(f"[MessageProcessor] Fatal error in send handler: {str(e)}") logger.error(traceback.format_exc()) @@ -632,6 +659,10 @@ class MessageProcessor: # elif message_type == "session_id": # self.session_id = message_data.get("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": await self._handle_request_restart(message_data) else: @@ -968,6 +999,37 @@ class MessageProcessor: ) 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]): """ 处理重启请求 @@ -979,10 +1041,9 @@ class MessageProcessor: logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s") # 发送确认消息 - if self.websocket_client: - await self.websocket_client.send_message( - {"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}} - ) + self.send_message( + {"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}} + ) # 设置全局重启标志 import unilabos.app.main as main_module @@ -1084,13 +1145,14 @@ class QueueProcessor: def stop(self) -> None: """停止队列处理线程""" self.is_running = False + self.queue_update_event.set() # 立即唤醒等待中的线程 if self.thread and self.thread.is_alive(): self.thread.join(timeout=2) logger.info("[QueueProcessor] Stopped") def _run(self): """运行队列处理主循环""" - logger.debug("[QueueProcessor] Queue processor started") + logger.trace("[QueueProcessor] Queue processor started") while self.is_running: try: @@ -1305,7 +1367,6 @@ class WebSocketClient(BaseCommunicationClient): else: url = f"{scheme}://{parsed.netloc}/api/v1/ws/schedule" - logger.debug(f"[WebSocketClient] URL: {url}") return url def start(self) -> None: @@ -1318,13 +1379,11 @@ class WebSocketClient(BaseCommunicationClient): logger.error("[WebSocketClient] WebSocket URL not configured") return - logger.info(f"[WebSocketClient] Starting connection to {self.websocket_url}") - # 启动两个核心线程 self.message_processor.start() self.queue_processor.start() - logger.info("[WebSocketClient] All threads started") + logger.trace("[WebSocketClient] All threads started") def stop(self) -> None: """停止WebSocket客户端""" @@ -1340,8 +1399,8 @@ class WebSocketClient(BaseCommunicationClient): message = {"action": "normal_exit", "data": {"session_id": session_id}} self.message_processor.send_message(message) logger.info(f"[WebSocketClient] Sent normal_exit message with session_id: {session_id}") - # 给一点时间让消息发送出去 - time.sleep(1) + # send_handler 每100ms检查一次队列,等300ms足以让消息发出 + time.sleep(0.3) except Exception as e: logger.warning(f"[WebSocketClient] Failed to send normal_exit message: {str(e)}") diff --git a/unilabos/config/config.py b/unilabos/config/config.py index 4b7d91a4..b80d3b60 100644 --- a/unilabos/config/config.py +++ b/unilabos/config/config.py @@ -24,6 +24,7 @@ class BasicConfig: port = 8002 # 本地HTTP服务 check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性 test_mode = False # 测试模式,所有动作不实际执行,返回模拟结果 + extra_resource = False # 是否加载lab_开头的额外资源 # 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL' log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG" @@ -40,7 +41,7 @@ class BasicConfig: class WSConfig: reconnect_interval = 5 # 重连间隔(秒) max_reconnect_attempts = 999 # 最大重连次数 - ping_interval = 30 # ping间隔(秒) + ping_interval = 20 # ping间隔(秒) # HTTP配置 diff --git a/unilabos/device_comms/universal_driver.py b/unilabos/device_comms/universal_driver.py index 281e0cd9..0ff41805 100644 --- a/unilabos/device_comms/universal_driver.py +++ b/unilabos/device_comms/universal_driver.py @@ -1,4 +1,3 @@ - from abc import abstractmethod from functools import wraps import inspect diff --git a/unilabos/devices/liquid_handling/prcxi/prcxi.py b/unilabos/devices/liquid_handling/prcxi/prcxi.py index f34583fe..47b213ad 100644 --- a/unilabos/devices/liquid_handling/prcxi/prcxi.py +++ b/unilabos/devices/liquid_handling/prcxi/prcxi.py @@ -634,7 +634,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract): def __init__( self, - deck: Deck, + deck: PRCXI9300Deck, host: str, port: int, timeout: float, @@ -648,11 +648,11 @@ class PRCXI9300Handler(LiquidHandlerAbstract): is_9320=False, ): tablets_info = [] - count = 0 - for child in deck.children: + for site_id in range(len(deck.sites)): + child = deck._get_site_resource(site_id) # 如果放其他类型的物料,是不可以的 if hasattr(child, "_unilabos_state") and "Material" in child._unilabos_state: - number = int(child.name.replace("T", "")) + number = site_id + 1 tablets_info.append( WorkTablets( Number=number, Code=f"T{number}", Material=child._unilabos_state["Material"] diff --git a/unilabos/devices/virtual/workbench.py b/unilabos/devices/virtual/workbench.py index f5fae47e..d67db398 100644 --- a/unilabos/devices/virtual/workbench.py +++ b/unilabos/devices/virtual/workbench.py @@ -1,15 +1,15 @@ """ Virtual Workbench Device - 模拟工作台设备 -包含: +包含: - 1个机械臂 (每次操作3s, 独占锁) - 3个加热台 (每次加热10s, 可并行) -工作流程: -1. A1-A5 物料同时启动,竞争机械臂 +工作流程: +1. A1-A5 物料同时启动, 竞争机械臂 2. 机械臂将物料移动到空闲加热台 -3. 加热完成后,机械臂将物料移动到C1-C5 +3. 加热完成后, 机械臂将物料移动到C1-C5 -注意:调用来自线程池,使用 threading.Lock 进行同步 +注意: 调用来自线程池, 使用 threading.Lock 进行同步 """ import logging @@ -21,9 +21,11 @@ from threading import Lock, RLock 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.utils.decorator import not_action, always_free -from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, RETURN_UNILABOS_SAMPLES +from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample # ============ TypedDict 返回类型定义 ============ @@ -57,6 +59,8 @@ class MoveToOutputResult(TypedDict): success: bool station_id: int material_id: str + output_position: str + message: str unilabos_samples: List[LabSample] @@ -81,9 +85,9 @@ class HeatingStationState(Enum): """加热台状态枚举""" IDLE = "idle" # 空闲 - OCCUPIED = "occupied" # 已放置物料,等待加热 + OCCUPIED = "occupied" # 已放置物料, 等待加热 HEATING = "heating" # 加热中 - COMPLETED = "completed" # 加热完成,等待取走 + COMPLETED = "completed" # 加热完成, 等待取走 class ArmState(Enum): @@ -105,19 +109,24 @@ class HeatingStation: 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: """ Virtual Workbench Device - 虚拟工作台设备 模拟一个包含1个机械臂和3个加热台的工作站 - - 机械臂操作耗时3秒,同一时间只能执行一个操作 - - 加热台加热耗时10秒,3个加热台可并行工作 + - 机械臂操作耗时3秒, 同一时间只能执行一个操作 + - 加热台加热耗时10秒, 3个加热台可并行工作 工作流: - 1. 物料A1-A5并发启动(线程池),竞争机械臂使用权 - 2. 获取机械臂后,查找空闲加热台 - 3. 机械臂将物料放入加热台,开始加热 - 4. 加热完成后,机械臂将物料移动到目标位置Cn + 1. 物料A1-A5并发启动(线程池), 竞争机械臂使用权 + 2. 获取机械臂后, 查找空闲加热台 + 3. 机械臂将物料放入加热台, 开始加热 + 4. 加热完成后, 机械臂将物料移动到目标位置Cn """ _ros_node: BaseROS2DeviceNode @@ -145,19 +154,19 @@ class VirtualWorkbench: 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)) - # 机械臂状态和锁 (使用threading.Lock) + # 机械臂状态和锁 self._arm_lock = Lock() self._arm_state = ArmState.IDLE self._arm_current_task: Optional[str] = None - # 加热台状态 (station_id -> HeatingStation) - 立即初始化,不依赖initialize() + # 加热台状态 self._heating_stations: Dict[int, HeatingStation] = { 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() # 处理其他kwargs参数 @@ -183,7 +192,6 @@ class VirtualWorkbench: """初始化虚拟工作台""" self.logger.info(f"初始化虚拟工作台 {self.device_id}") - # 重置加热台状态 (已在__init__中创建,这里重置为初始状态) with self._stations_lock: for station in self._heating_stations.values(): station.state = HeatingStationState.IDLE @@ -191,7 +199,6 @@ class VirtualWorkbench: station.material_number = None station.heating_progress = 0.0 - # 初始化状态 self.data.update( { "status": "Ready", @@ -257,11 +264,7 @@ class VirtualWorkbench: self.data["message"] = message def _find_available_heating_station(self) -> Optional[int]: - """查找空闲的加热台 - - Returns: - 空闲加热台ID,如果没有则返回None - """ + """查找空闲的加热台""" with self._stations_lock: for station_id, station in self._heating_stations.items(): if station.state == HeatingStationState.IDLE: @@ -269,23 +272,12 @@ class VirtualWorkbench: return None def _acquire_arm(self, task_description: str) -> bool: - """获取机械臂使用权(阻塞直到获取) - - Args: - task_description: 任务描述,用于日志 - - Returns: - 是否成功获取 - """ + """获取机械臂使用权(阻塞直到获取)""" self.logger.info(f"[{task_description}] 等待获取机械臂...") - - # 阻塞等待获取锁 self._arm_lock.acquire() - self._arm_state = ArmState.BUSY self._arm_current_task = task_description self._update_data_status(f"机械臂执行: {task_description}") - self.logger.info(f"[{task_description}] 成功获取机械臂使用权") return True @@ -298,6 +290,22 @@ class VirtualWorkbench: self._update_data_status(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( self, sample_uuids: SampleUUIDsType, @@ -306,19 +314,14 @@ class VirtualWorkbench: """ 批量准备物料 - 虚拟起始节点 - 作为工作流的起始节点,生成指定数量的物料编号供后续节点使用。 - 输出5个handle (material_1 ~ material_5),分别对应实验1~5。 - - Args: - count: 待生成的物料数量,默认5 (生成 A1-A5) - - Returns: - PrepareMaterialsResult: 包含 material_1 ~ material_5 用于传递给 move_to_heating_station + 作为工作流的起始节点, 生成指定数量的物料编号供后续节点使用。 + 输出5个handle (material_1 ~ material_5), 分别对应实验1~5。 """ - # 生成物料列表 A1 - A{count} 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 { "success": True, @@ -329,9 +332,28 @@ class VirtualWorkbench: "material_4": materials[3] if len(materials) > 3 else 0, "material_5": materials[4] if len(materials) > 4 else 0, "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( self, sample_uuids: SampleUUIDsType, @@ -340,20 +362,12 @@ class VirtualWorkbench: """ 将物料从An位置移动到加热台 - 多线程并发调用时,会竞争机械臂使用权,并自动查找空闲加热台 - - Args: - material_number: 物料编号 (1-5) - - Returns: - MoveToHeatingStationResult: 包含 station_id, material_number 等用于传递给下一个节点 + 多线程并发调用时, 会竞争机械臂使用权, 并自动查找空闲加热台 """ - # 根据物料编号生成物料ID material_id = f"A{material_number}" task_desc = f"移动{material_id}到加热台" self.logger.info(f"[任务] {task_desc} - 开始执行") - # 记录任务 with self._tasks_lock: self._active_tasks[material_id] = { "status": "waiting_for_arm", @@ -361,33 +375,27 @@ class VirtualWorkbench: } try: - # 步骤1: 等待获取机械臂使用权(竞争) with self._tasks_lock: self._active_tasks[material_id]["status"] = "waiting_for_arm" self._acquire_arm(task_desc) - # 步骤2: 查找空闲加热台 with self._tasks_lock: self._active_tasks[material_id]["status"] = "finding_station" station_id = None - # 循环等待直到找到空闲加热台 while station_id is None: station_id = self._find_available_heating_station() if station_id is None: - self.logger.info(f"[{material_id}] 没有空闲加热台,等待中...") - # 释放机械臂,等待后重试 + self.logger.info(f"[{material_id}] 没有空闲加热台, 等待中...") self._release_arm() time.sleep(0.5) self._acquire_arm(task_desc) - # 步骤3: 占用加热台 - 立即标记为OCCUPIED,防止其他任务选择同一加热台 with self._stations_lock: self._heating_stations[station_id].state = HeatingStationState.OCCUPIED self._heating_stations[station_id].current_material = material_id self._heating_stations[station_id].material_number = material_number - # 步骤4: 模拟机械臂移动操作 (3秒) with self._tasks_lock: self._active_tasks[material_id]["status"] = "arm_moving" self._active_tasks[material_id]["assigned_station"] = station_id @@ -395,11 +403,11 @@ class VirtualWorkbench: time.sleep(self.ARM_OPERATION_TIME) - # 步骤5: 放入加热台完成 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() with self._tasks_lock: @@ -412,8 +420,16 @@ class VirtualWorkbench: "material_number": material_number, "message": f"{material_id}已成功移动到加热台{station_id}", "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()] + 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() + ], } except Exception as e: @@ -427,11 +443,33 @@ class VirtualWorkbench: "material_number": material_number, "message": f"移动失败: {str(e)}", "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()] + 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() + ], } - @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( self, sample_uuids: SampleUUIDsType, @@ -440,13 +478,6 @@ class VirtualWorkbench: ) -> 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}] 开始加热") @@ -458,8 +489,16 @@ class VirtualWorkbench: "material_number": material_number, "message": f"无效的加热台ID: {station_id}", "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()] + 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() + ], } with self._stations_lock: @@ -473,8 +512,16 @@ class VirtualWorkbench: "material_number": material_number, "message": f"加热台{station_id}上没有物料", "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()] + 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() + ], } if station.state == HeatingStationState.HEATING: @@ -485,13 +532,20 @@ class VirtualWorkbench: "material_number": material_number, "message": f"加热台{station_id}已经在加热中", "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()] + 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() + ], } material_id = station.current_material - # 开始加热 station.state = HeatingStationState.HEATING station.heating_start_time = time.time() station.heating_progress = 0.0 @@ -502,7 +556,6 @@ class VirtualWorkbench: self._update_data_status(f"加热台{station_id}开始加热{material_id}") - # 打印当前所有正在加热的台位 with self._stations_lock: heating_list = [ f"加热台{sid}:{s.current_material}" @@ -511,7 +564,6 @@ class VirtualWorkbench: ] self.logger.info(f"[并行加热] 当前同时加热中: {', '.join(heating_list)}") - # 模拟加热过程 start_time = time.time() last_countdown_log = start_time while True: @@ -524,7 +576,6 @@ class VirtualWorkbench: self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%") - # 每5秒打印一次倒计时 if time.time() - last_countdown_log >= 5.0: self.logger.info(f"[加热台{station_id}] {material_id} 剩余 {remaining:.1f}s") last_countdown_log = time.time() @@ -534,7 +585,6 @@ class VirtualWorkbench: time.sleep(1.0) - # 加热完成 with self._stations_lock: self._heating_stations[station_id].state = HeatingStationState.COMPLETED self._heating_stations[station_id].heating_progress = 100.0 @@ -553,10 +603,28 @@ class VirtualWorkbench: "material_number": material_number, "message": f"加热台{station_id}加热完成", "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()] + 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="将物料从加热台移动到输出位置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( self, sample_uuids: SampleUUIDsType, @@ -565,15 +633,8 @@ class VirtualWorkbench: ) -> MoveToOutputResult: """ 将物料从加热台移动到输出位置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: return { @@ -583,8 +644,16 @@ class VirtualWorkbench: "output_position": f"C{output_number}", "message": f"无效的加热台ID: {station_id}", "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()] + 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() + ], } with self._stations_lock: @@ -599,8 +668,16 @@ class VirtualWorkbench: "output_position": f"C{output_number}", "message": f"加热台{station_id}上没有物料", "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()] + 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() + ], } if station.state != HeatingStationState.COMPLETED: @@ -611,8 +688,16 @@ class VirtualWorkbench: "output_position": f"C{output_number}", "message": f"加热台{station_id}尚未完成加热 (当前状态: {station.state.value})", "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()] + 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() + ], } output_position = f"C{output_number}" @@ -624,18 +709,17 @@ class VirtualWorkbench: if material_id in self._active_tasks: self._active_tasks[material_id]["status"] = "waiting_for_arm_output" - # 获取机械臂 self._acquire_arm(task_desc) with self._tasks_lock: if material_id in self._active_tasks: self._active_tasks[material_id]["status"] = "arm_moving_to_output" - # 模拟机械臂操作 (3秒) - self.logger.info(f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}...") + self.logger.info( + f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}..." + ) time.sleep(self.ARM_OPERATION_TIME) - # 清空加热台 with self._stations_lock: self._heating_stations[station_id].state = HeatingStationState.IDLE 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_start_time = None - # 释放机械臂 self._release_arm() - # 任务完成 with self._tasks_lock: if material_id in self._active_tasks: self._active_tasks[material_id]["status"] = "completed" self._active_tasks[material_id]["end_time"] = time.time() 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 { "success": True, @@ -662,8 +746,17 @@ class VirtualWorkbench: "output_position": output_position, "message": f"{material_id}已成功移动到{output_position}", "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()] + LabSample( + 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: @@ -677,83 +770,105 @@ class VirtualWorkbench: "output_position": output_position, "message": f"移动失败: {str(e)}", "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()] + 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() + ], } # ============ 状态属性 ============ @property + @topic_config() def status(self) -> str: return self.data.get("status", "Unknown") @property + @topic_config() def arm_state(self) -> str: return self._arm_state.value @property + @topic_config() def arm_current_task(self) -> str: return self._arm_current_task or "" @property + @topic_config() def heating_station_1_state(self) -> str: with self._stations_lock: station = self._heating_stations.get(1) return station.state.value if station else "unknown" @property + @topic_config() def heating_station_1_material(self) -> str: with self._stations_lock: station = self._heating_stations.get(1) return station.current_material or "" if station else "" @property + @topic_config() def heating_station_1_progress(self) -> float: with self._stations_lock: station = self._heating_stations.get(1) return station.heating_progress if station else 0.0 @property + @topic_config() def heating_station_2_state(self) -> str: with self._stations_lock: station = self._heating_stations.get(2) return station.state.value if station else "unknown" @property + @topic_config() def heating_station_2_material(self) -> str: with self._stations_lock: station = self._heating_stations.get(2) return station.current_material or "" if station else "" @property + @topic_config() def heating_station_2_progress(self) -> float: with self._stations_lock: station = self._heating_stations.get(2) return station.heating_progress if station else 0.0 @property + @topic_config() def heating_station_3_state(self) -> str: with self._stations_lock: station = self._heating_stations.get(3) return station.state.value if station else "unknown" @property + @topic_config() def heating_station_3_material(self) -> str: with self._stations_lock: station = self._heating_stations.get(3) return station.current_material or "" if station else "" @property + @topic_config() def heating_station_3_progress(self) -> float: with self._stations_lock: station = self._heating_stations.get(3) return station.heating_progress if station else 0.0 @property + @topic_config() def active_tasks_count(self) -> int: with self._tasks_lock: return len(self._active_tasks) @property + @topic_config() def message(self) -> str: return self.data.get("message", "") diff --git a/unilabos/registry/ast_registry_scanner.py b/unilabos/registry/ast_registry_scanner.py new file mode 100644 index 00000000..86c3602e --- /dev/null +++ b/unilabos/registry/ast_registry_scanner.py @@ -0,0 +1,1037 @@ +""" +AST-based Registry Scanner + +Statically parse Python files to extract @device, @action, @topic_config, @resource +decorator metadata without importing any modules. This is ~100x faster than importlib +since it only reads and parses text files. + +Includes a file-level cache: each file's MD5 hash, size and mtime are tracked so +unchanged files skip AST parsing entirely. The cache is persisted as JSON in the +working directory (``unilabos_data/ast_scan_cache.json``). + +Usage: + from unilabos.registry.ast_registry_scanner import scan_directory + + # Scan all device and resource files under a package directory + result = scan_directory("unilabos", python_path="/project") + # => {"devices": {device_id: {...}, ...}, "resources": {resource_id: {...}, ...}} +""" + +import ast +import hashlib +import json +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple, Union + + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +MAX_SCAN_DEPTH = 10 # 最大目录递归深度 +MAX_SCAN_FILES = 1000 # 最大扫描文件数量 +_CACHE_VERSION = 1 # 缓存格式版本号,格式变更时递增 + +# 合法的装饰器来源模块 +_REGISTRY_DECORATOR_MODULE = "unilabos.registry.decorators" + + +# --------------------------------------------------------------------------- +# File-level cache helpers +# --------------------------------------------------------------------------- + + +def _file_fingerprint(filepath: Path) -> Dict[str, Any]: + """Return size, mtime and MD5 hash for *filepath*.""" + stat = filepath.stat() + md5 = hashlib.md5(filepath.read_bytes()).hexdigest() + return {"size": stat.st_size, "mtime": stat.st_mtime, "md5": md5} + + +def load_scan_cache(cache_path: Optional[Path]) -> Dict[str, Any]: + """Load the AST scan cache from *cache_path*. Returns empty structure on any error.""" + if cache_path is None or not cache_path.is_file(): + return {"version": _CACHE_VERSION, "files": {}} + try: + raw = cache_path.read_text(encoding="utf-8") + data = json.loads(raw) + if data.get("version") != _CACHE_VERSION: + return {"version": _CACHE_VERSION, "files": {}} + return data + except Exception: + return {"version": _CACHE_VERSION, "files": {}} + + +def save_scan_cache(cache_path: Optional[Path], cache: Dict[str, Any]) -> None: + """Persist *cache* to *cache_path* (atomic-ish via temp file).""" + if cache_path is None: + return + try: + cache_path.parent.mkdir(parents=True, exist_ok=True) + tmp = cache_path.with_suffix(".tmp") + tmp.write_text(json.dumps(cache, ensure_ascii=False, indent=1), encoding="utf-8") + tmp.replace(cache_path) + except Exception: + pass + + +def _is_cache_hit(entry: Dict[str, Any], fp: Dict[str, Any]) -> bool: + """Check if a cache entry matches the current file fingerprint.""" + return ( + entry.get("md5") == fp["md5"] + and entry.get("size") == fp["size"] + ) + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def _collect_py_files( + root_dir: Path, + max_depth: int = MAX_SCAN_DEPTH, + max_files: int = MAX_SCAN_FILES, + exclude_files: Optional[set] = None, +) -> List[Path]: + """ + 收集 root_dir 下的 .py 文件,限制最大递归深度和文件数量。 + + Args: + root_dir: 扫描根目录 + max_depth: 最大递归深度 (默认 10 层) + max_files: 最大文件数量 (默认 1000 个) + exclude_files: 要排除的文件名集合 (如 {"lab_resources.py"}) + + Returns: + 排序后的 .py 文件路径列表 + """ + result: List[Path] = [] + _exclude = exclude_files or set() + + def _walk(dir_path: Path, depth: int): + if depth > max_depth or len(result) >= max_files: + return + try: + entries = sorted(dir_path.iterdir()) + except (PermissionError, OSError): + return + for entry in entries: + if len(result) >= max_files: + return + if entry.is_file() and entry.suffix == ".py" and not entry.name.startswith("__"): + if entry.name not in _exclude: + result.append(entry) + elif entry.is_dir() and not entry.name.startswith(("__", ".")): + _walk(entry, depth + 1) + + _walk(root_dir, 0) + return result + + +def scan_directory( + root_dir: Union[str, Path], + python_path: Union[str, Path] = "", + max_depth: int = MAX_SCAN_DEPTH, + max_files: int = MAX_SCAN_FILES, + executor: ThreadPoolExecutor = None, + exclude_files: Optional[set] = None, + cache: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """ + Recursively scan .py files under *root_dir* for @device and @resource + decorated classes/functions. + + Uses a thread pool to parse files in parallel for faster I/O. + When *cache* is provided, files whose fingerprint (MD5 + size) hasn't + changed since the last scan are served from cache without re-parsing. + + Returns: + {"devices": {device_id: meta, ...}, "resources": {resource_id: meta, ...}} + + Args: + root_dir: Directory to scan (e.g. "unilabos/devices"). + python_path: The directory that should be on sys.path, i.e. the parent + of the top-level package. Module paths are derived as + filepath relative to this directory. If empty, defaults to + root_dir's parent. + max_depth: Maximum directory recursion depth (default 10). + max_files: Maximum number of .py files to scan (default 1000). + executor: Shared ThreadPoolExecutor (required). The caller manages its + lifecycle. + exclude_files: 要排除的文件名集合 (如 {"lab_resources.py"}) + cache: Mutable cache dict (``load_scan_cache()`` result). Hits are read + from here; misses are written back so the caller can persist later. + """ + if executor is None: + raise ValueError("executor is required and must not be None") + + root_dir = Path(root_dir).resolve() + if not python_path: + python_path = root_dir.parent + else: + python_path = Path(python_path).resolve() + + # --- Collect files (depth/count limited) --- + py_files = _collect_py_files(root_dir, max_depth=max_depth, max_files=max_files, exclude_files=exclude_files) + + cache_files: Dict[str, Any] = cache.get("files", {}) if cache else {} + + # --- Parallel scan (with cache fast-path) --- + devices: Dict[str, dict] = {} + resources: Dict[str, dict] = {} + cache_hits = 0 + cache_misses = 0 + + def _parse_one_cached(py_file: Path) -> Tuple[List[dict], List[dict], bool]: + """Returns (devices, resources, was_cache_hit).""" + key = str(py_file) + try: + fp = _file_fingerprint(py_file) + except OSError: + return [], [], False + + cached_entry = cache_files.get(key) + if cached_entry and _is_cache_hit(cached_entry, fp): + return cached_entry.get("devices", []), cached_entry.get("resources", []), True + + try: + devs, ress = _parse_file(py_file, python_path) + except (SyntaxError, Exception): + devs, ress = [], [] + + cache_files[key] = { + "md5": fp["md5"], + "size": fp["size"], + "mtime": fp["mtime"], + "devices": devs, + "resources": ress, + } + return devs, ress, False + + def _collect_results(futures_dict: Dict): + nonlocal cache_hits, cache_misses + for future in as_completed(futures_dict): + devs, ress, hit = future.result() + if hit: + cache_hits += 1 + else: + cache_misses += 1 + for dev in devs: + device_id = dev.get("device_id") + if device_id: + if device_id in devices: + existing = devices[device_id].get("file_path", "?") + new_file = dev.get("file_path", "?") + raise ValueError( + f"@device id 重复: '{device_id}' 同时出现在 {existing} 和 {new_file}" + ) + devices[device_id] = dev + for res in ress: + resource_id = res.get("resource_id") + if resource_id: + if resource_id in resources: + existing = resources[resource_id].get("file_path", "?") + new_file = res.get("file_path", "?") + raise ValueError( + f"@resource id 重复: '{resource_id}' 同时出现在 {existing} 和 {new_file}" + ) + resources[resource_id] = res + + futures = {executor.submit(_parse_one_cached, f): f for f in py_files} + _collect_results(futures) + + if cache is not None: + cache["files"] = cache_files + + return { + "devices": devices, + "resources": resources, + "_cache_stats": {"hits": cache_hits, "misses": cache_misses, "total": len(py_files)}, + } + + + + +# --------------------------------------------------------------------------- +# File-level parsing +# --------------------------------------------------------------------------- + +# 已知继承自 rclpy.node.Node 的基类名 (用于 AST 静态检测) +_KNOWN_ROS2_BASE_CLASSES = {"Node", "BaseROS2DeviceNode"} +_KNOWN_ROS2_MODULES = {"rclpy", "rclpy.node"} + + +def _detect_class_type(cls_node: ast.ClassDef, import_map: Dict[str, str]) -> str: + """ + 检测类是否继承自 rclpy Node,返回 'ros2' 或 'python'。 + + 通过检查类的基类名称和 import_map 中的模块路径来判断: + 1. 基类名在已知 ROS2 基类集合中 + 2. 基类在 import_map 中解析到 rclpy 相关模块 + 3. 基类在 import_map 中解析到 BaseROS2DeviceNode + """ + for base in cls_node.bases: + base_name = "" + if isinstance(base, ast.Name): + base_name = base.id + elif isinstance(base, ast.Attribute): + base_name = base.attr + elif isinstance(base, ast.Subscript) and isinstance(base.value, ast.Name): + # Generic[T] 形式,如 BaseROS2DeviceNode[SomeType] + base_name = base.value.id + + if not base_name: + continue + + # 直接匹配已知 ROS2 基类名 + if base_name in _KNOWN_ROS2_BASE_CLASSES: + return "ros2" + + # 通过 import_map 检查模块路径 + module_path = import_map.get(base_name, "") + if any(mod in module_path for mod in _KNOWN_ROS2_MODULES): + return "ros2" + if "BaseROS2DeviceNode" in module_path: + return "ros2" + + return "python" + + +def _parse_file( + filepath: Path, + python_path: Path, +) -> Tuple[List[dict], List[dict]]: + """ + Parse a single .py file using ast and extract all @device-decorated classes + and @resource-decorated functions/classes. + + Returns: + (devices, resources) -- two lists of metadata dicts. + """ + source = filepath.read_text(encoding="utf-8", errors="replace") + tree = ast.parse(source, filename=str(filepath)) + + # Derive module path from file path + module_path = _filepath_to_module(filepath, python_path) + + # Build import map from the file (includes same-file class defs) + import_map = _collect_imports(tree, module_path) + + devices: List[dict] = [] + resources: List[dict] = [] + + for node in ast.iter_child_nodes(tree): + # --- @device on classes --- + if isinstance(node, ast.ClassDef): + device_decorator = _find_decorator(node, "device") + if device_decorator is not None and _is_registry_decorator("device", import_map): + device_args = _extract_decorator_args(device_decorator, import_map) + class_body = _extract_class_body(node, import_map) + + # Support ids + id_meta (multi-device) or id (single device) + device_ids: List[str] = [] + if device_args.get("ids") is not None: + device_ids = list(device_args["ids"]) + else: + did = device_args.get("id") or device_args.get("device_id") + device_ids = [did] if did else [f"{module_path}:{node.name}"] + + id_meta = device_args.get("id_meta") or {} + base_meta = { + "class_name": node.name, + "module": f"{module_path}:{node.name}", + "file_path": str(filepath).replace("\\", "/"), + "category": device_args.get("category", []), + "description": device_args.get("description", ""), + "display_name": device_args.get("display_name", ""), + "icon": device_args.get("icon", ""), + "version": device_args.get("version", "1.0.0"), + "device_type": _detect_class_type(node, import_map), + "handles": device_args.get("handles", []), + "model": device_args.get("model"), + "hardware_interface": device_args.get("hardware_interface"), + "actions": class_body.get("actions", {}), + "status_properties": class_body.get("status_properties", {}), + "init_params": class_body.get("init_params", []), + "auto_methods": class_body.get("auto_methods", {}), + "import_map": import_map, + } + for did in device_ids: + meta = dict(base_meta) + meta["device_id"] = did + overrides = id_meta.get(did, {}) + for key in ("handles", "description", "icon", "model", "hardware_interface"): + if key in overrides: + meta[key] = overrides[key] + devices.append(meta) + + # --- @resource on classes --- + resource_decorator = _find_decorator(node, "resource") + if resource_decorator is not None and _is_registry_decorator("resource", import_map): + res_meta = _extract_resource_meta( + resource_decorator, node.name, module_path, filepath, import_map, + is_function=False, + init_node=_find_init_in_class(node), + ) + resources.append(res_meta) + + # --- @resource on module-level functions --- + elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + resource_decorator = _find_method_decorator(node, "resource") + if resource_decorator is not None and _is_registry_decorator("resource", import_map): + res_meta = _extract_resource_meta( + resource_decorator, node.name, module_path, filepath, import_map, + is_function=True, + func_node=node, + ) + resources.append(res_meta) + + return devices, resources + + +def _find_init_in_class(cls_node: ast.ClassDef) -> Optional[ast.FunctionDef]: + """Find __init__ method in a class.""" + for item in cls_node.body: + if isinstance(item, ast.FunctionDef) and item.name == "__init__": + return item + return None + + +def _extract_resource_meta( + decorator_node: Union[ast.Call, ast.Name], + name: str, + module_path: str, + filepath: Path, + import_map: Dict[str, str], + is_function: bool = False, + func_node: Optional[Union[ast.FunctionDef, ast.AsyncFunctionDef]] = None, + init_node: Optional[ast.FunctionDef] = None, +) -> dict: + """ + Extract resource metadata from a @resource decorator on a function or class. + """ + res_args = _extract_decorator_args(decorator_node, import_map) + + resource_id = res_args.get("id") or res_args.get("resource_id") + if resource_id is None: + resource_id = f"{module_path}:{name}" + + # Extract init/function params + init_params: List[dict] = [] + if is_function and func_node is not None: + init_params = _extract_method_params(func_node, import_map) + elif not is_function and init_node is not None: + init_params = _extract_method_params(init_node, import_map) + + return { + "resource_id": resource_id, + "name": name, + "module": f"{module_path}:{name}", + "file_path": str(filepath).replace("\\", "/"), + "is_function": is_function, + "category": res_args.get("category", []), + "description": res_args.get("description", ""), + "icon": res_args.get("icon", ""), + "version": res_args.get("version", "1.0.0"), + "class_type": res_args.get("class_type", "pylabrobot"), + "handles": res_args.get("handles", []), + "model": res_args.get("model"), + "init_params": init_params, + } + + +# --------------------------------------------------------------------------- +# Import map collection +# --------------------------------------------------------------------------- + + +def _collect_imports(tree: ast.Module, module_path: str = "") -> Dict[str, str]: + """ + Walk all Import/ImportFrom nodes in the AST tree, build a mapping from + local name to fully-qualified import path. + + Also includes top-level class/function definitions from the same file, + so that same-file TypedDict / Enum / dataclass references can be resolved. + + Returns: + {"SendCmd": "unilabos_msgs.action:SendCmd", + "StrSingleInput": "unilabos_msgs.action:StrSingleInput", + "InputHandle": "unilabos.registry.decorators:InputHandle", + "SetLiquidReturn": "unilabos.devices.liquid_handling.liquid_handler_abstract:SetLiquidReturn", + ...} + """ + import_map: Dict[str, str] = {} + + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom): + module = node.module or "" + for alias in node.names: + local_name = alias.asname if alias.asname else alias.name + import_map[local_name] = f"{module}:{alias.name}" + elif isinstance(node, ast.Import): + for alias in node.names: + local_name = alias.asname if alias.asname else alias.name + import_map[local_name] = alias.name + + # 同文件顶层 class / function 定义 + if module_path: + for node in tree.body: + if isinstance(node, ast.ClassDef): + import_map.setdefault(node.name, f"{module_path}:{node.name}") + elif isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef): + import_map.setdefault(node.name, f"{module_path}:{node.name}") + elif isinstance(node, ast.Assign): + # 顶层赋值 (如 MotorAxis = Enum(...)) + for target in node.targets: + if isinstance(target, ast.Name): + import_map.setdefault(target.id, f"{module_path}:{target.id}") + + return import_map + + + +# --------------------------------------------------------------------------- +# Decorator finding & argument extraction +# --------------------------------------------------------------------------- + + +def _find_decorator( + node: Union[ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef], + decorator_name: str, +) -> Optional[ast.Call]: + """ + Find a specific decorator call on a class or function definition. + + Handles both: + - @device(...) -> ast.Call with func=ast.Name(id="device") + - @module.device(...) -> ast.Call with func=ast.Attribute(attr="device") + """ + for dec in node.decorator_list: + if isinstance(dec, ast.Call): + if isinstance(dec.func, ast.Name) and dec.func.id == decorator_name: + return dec + if isinstance(dec.func, ast.Attribute) and dec.func.attr == decorator_name: + return dec + elif isinstance(dec, ast.Name) and dec.id == decorator_name: + # @device without parens (unlikely but handle it) + return None # Can't extract args from bare decorator + return None + + +def _find_method_decorator(func_node: ast.FunctionDef, decorator_name: str) -> Optional[Union[ast.Call, ast.Name]]: + """Find a decorator on a method.""" + for dec in func_node.decorator_list: + if isinstance(dec, ast.Call): + if isinstance(dec.func, ast.Name) and dec.func.id == decorator_name: + return dec + if isinstance(dec.func, ast.Attribute) and dec.func.attr == decorator_name: + return dec + elif isinstance(dec, ast.Name) and dec.id == decorator_name: + # @action without parens, or @topic_config without parens + return dec + return None + + +def _has_decorator(func_node: ast.FunctionDef, decorator_name: str) -> bool: + """Check if a method has a specific decorator (with or without call).""" + for dec in func_node.decorator_list: + if isinstance(dec, ast.Call): + if isinstance(dec.func, ast.Name) and dec.func.id == decorator_name: + return True + if isinstance(dec.func, ast.Attribute) and dec.func.attr == decorator_name: + return True + elif isinstance(dec, ast.Name) and dec.id == decorator_name: + return True + return False + + +def _is_registry_decorator(name: str, import_map: Dict[str, str]) -> bool: + """Check that *name* was imported from ``unilabos.registry.decorators``.""" + source = import_map.get(name, "") + return _REGISTRY_DECORATOR_MODULE in source + + +def _extract_decorator_args( + node: Union[ast.Call, ast.Name], + import_map: Dict[str, str], +) -> dict: + """ + Extract keyword arguments from a decorator call AST node. + + Resolves Name references (e.g. SendCmd, Side.NORTH) via import_map. + Handles literal values (strings, ints, bools, lists, dicts, None). + """ + if isinstance(node, ast.Name): + return {} # Bare decorator, no args + if not isinstance(node, ast.Call): + return {} + + result: dict = {} + + for kw in node.keywords: + if kw.arg is None: + continue # **kwargs, skip + result[kw.arg] = _ast_node_to_value(kw.value, import_map) + + return result + + +# --------------------------------------------------------------------------- +# AST node value conversion +# --------------------------------------------------------------------------- + + +def _ast_node_to_value(node: ast.expr, import_map: Dict[str, str]) -> Any: + """ + Convert an AST expression node to a Python value. + + Handles: + - Literals (str, int, float, bool, None) + - Lists, Tuples, Dicts, Sets + - Name references (e.g. SendCmd -> resolved via import_map) + - Attribute access (e.g. Side.NORTH -> resolved) + - Function/class calls (e.g. InputHandle(...) -> structured dict) + - Unary operators (e.g. -1) + """ + # --- Constant (str, int, float, bool, None) --- + if isinstance(node, ast.Constant): + return node.value + + # --- Name (e.g. SendCmd, True, False, None) --- + if isinstance(node, ast.Name): + return _resolve_name(node.id, import_map) + + # --- Attribute (e.g. Side.NORTH, DataSource.HANDLE) --- + if isinstance(node, ast.Attribute): + return _resolve_attribute(node, import_map) + + # --- List --- + if isinstance(node, ast.List): + return [_ast_node_to_value(elt, import_map) for elt in node.elts] + + # --- Tuple --- + if isinstance(node, ast.Tuple): + return [_ast_node_to_value(elt, import_map) for elt in node.elts] + + # --- Dict --- + if isinstance(node, ast.Dict): + result = {} + for k, v in zip(node.keys, node.values): + if k is None: + continue # **kwargs spread + key = _ast_node_to_value(k, import_map) + val = _ast_node_to_value(v, import_map) + result[key] = val + return result + + # --- Set --- + if isinstance(node, ast.Set): + return [_ast_node_to_value(elt, import_map) for elt in node.elts] + + # --- Call (e.g. InputHandle(...), OutputHandle(...)) --- + if isinstance(node, ast.Call): + return _ast_call_to_value(node, import_map) + + # --- UnaryOp (e.g. -1, -0.5) --- + if isinstance(node, ast.UnaryOp): + if isinstance(node.op, ast.USub): + operand = _ast_node_to_value(node.operand, import_map) + if isinstance(operand, (int, float)): + return -operand + elif isinstance(node.op, ast.Not): + operand = _ast_node_to_value(node.operand, import_map) + return not operand + + # --- BinOp (e.g. "a" + "b") --- + if isinstance(node, ast.BinOp): + if isinstance(node.op, ast.Add): + left = _ast_node_to_value(node.left, import_map) + right = _ast_node_to_value(node.right, import_map) + if isinstance(left, str) and isinstance(right, str): + return left + right + + # --- JoinedStr (f-string) --- + if isinstance(node, ast.JoinedStr): + return "" + + # Fallback: return the AST dump as a string marker + return f"" + + +def _resolve_name(name: str, import_map: Dict[str, str]) -> str: + """ + Resolve a bare Name reference via import_map. + + E.g. "SendCmd" -> "unilabos_msgs.action:SendCmd" + "True" -> True (handled by ast.Constant in Python 3.8+) + """ + if name in import_map: + return import_map[name] + # Fallback: return the name as-is + return name + + +def _resolve_attribute(node: ast.Attribute, import_map: Dict[str, str]) -> str: + """ + Resolve an attribute access like Side.NORTH or DataSource.HANDLE. + + Returns a string like "NORTH" for enum values, or + "module.path:Class.attr" for imported references. + """ + # Get the full dotted path + parts = [] + current = node + while isinstance(current, ast.Attribute): + parts.append(current.attr) + current = current.value + if isinstance(current, ast.Name): + parts.append(current.id) + + parts.reverse() + # parts = ["Side", "NORTH"] or ["DataSource", "HANDLE"] + + if len(parts) >= 2: + base = parts[0] + attr = ".".join(parts[1:]) + + # If the base is an imported name, resolve it + if base in import_map: + return f"{import_map[base]}.{attr}" + + # For known enum-like patterns, return just the value + # e.g. Side.NORTH -> "NORTH" + if base in ("Side", "DataSource"): + return parts[-1] + + return ".".join(parts) + + +def _ast_call_to_value(node: ast.Call, import_map: Dict[str, str]) -> dict: + """ + Convert a function/class call like InputHandle(key="in", ...) to a structured dict. + + Returns: + {"_call": "unilabos.registry.decorators:InputHandle", + "key": "in", "data_type": "fluid", ...} + """ + # Resolve the call target + if isinstance(node.func, ast.Name): + call_name = _resolve_name(node.func.id, import_map) + elif isinstance(node.func, ast.Attribute): + call_name = _resolve_attribute(node.func, import_map) + else: + call_name = "" + + result: dict = {"_call": call_name} + + # Positional args + for i, arg in enumerate(node.args): + result[f"_pos_{i}"] = _ast_node_to_value(arg, import_map) + + # Keyword args + for kw in node.keywords: + if kw.arg is None: + continue + result[kw.arg] = _ast_node_to_value(kw.value, import_map) + + return result + + +# --------------------------------------------------------------------------- +# Class body extraction +# --------------------------------------------------------------------------- + + +def _extract_class_body( + cls_node: ast.ClassDef, + import_map: Dict[str, str], +) -> dict: + """ + Walk the class body to extract: + - @action-decorated methods + - @property with @topic_config (status properties) + - get_* methods with @topic_config + - __init__ parameters + - Public methods without @action (auto-actions) + """ + result: dict = { + "actions": {}, # method_name -> action_info + "status_properties": {}, # prop_name -> status_info + "init_params": [], # [{"name": ..., "type": ..., "default": ...}, ...] + "auto_methods": {}, # method_name -> method_info (no @action decorator) + } + + for item in cls_node.body: + if not isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + + method_name = item.name + + # --- __init__ --- + if method_name == "__init__": + result["init_params"] = _extract_method_params(item, import_map) + continue + + # --- Skip private/dunder --- + if method_name.startswith("_"): + continue + + # --- Check for @property or @topic_config → status property --- + is_property = _has_decorator(item, "property") + has_topic = ( + _has_decorator(item, "topic_config") + and _is_registry_decorator("topic_config", import_map) + ) + + if is_property or has_topic: + topic_args = {} + topic_dec = _find_method_decorator(item, "topic_config") + if topic_dec is not None: + topic_args = _extract_decorator_args(topic_dec, import_map) + + return_type = _get_annotation_str(item.returns, import_map) + # 非 @property 的 @topic_config 方法,用去掉 get_ 前缀的名称 + prop_name = method_name[4:] if method_name.startswith("get_") and not is_property else method_name + + result["status_properties"][prop_name] = { + "name": prop_name, + "return_type": return_type, + "is_property": is_property, + "topic_config": topic_args if topic_args else None, + } + continue + + # --- Check for @action --- + action_dec = _find_method_decorator(item, "action") + if action_dec is not None and _is_registry_decorator("action", import_map): + action_args = _extract_decorator_args(action_dec, import_map) + # 补全 @action 装饰器的默认值(与 decorators.py 中 action() 签名一致) + action_args.setdefault("action_type", None) + action_args.setdefault("goal", {}) + action_args.setdefault("feedback", {}) + action_args.setdefault("result", {}) + action_args.setdefault("handles", {}) + action_args.setdefault("goal_default", {}) + action_args.setdefault("placeholder_keys", {}) + action_args.setdefault("always_free", False) + action_args.setdefault("is_protocol", False) + action_args.setdefault("description", "") + action_args.setdefault("auto_prefix", False) + action_args.setdefault("parent", False) + method_params = _extract_method_params(item, import_map) + return_type = _get_annotation_str(item.returns, import_map) + is_async = isinstance(item, ast.AsyncFunctionDef) + method_doc = ast.get_docstring(item) + + result["actions"][method_name] = { + "action_args": action_args, + "params": method_params, + "return_type": return_type, + "is_async": is_async, + "docstring": method_doc, + } + continue + + # --- Check for @not_action --- + if _has_decorator(item, "not_action") and _is_registry_decorator("not_action", import_map): + continue + + # --- get_ 前缀且无额外参数(仅 self)→ status property --- + if method_name.startswith("get_"): + real_args = [a for a in item.args.args if a.arg != "self"] + if len(real_args) == 0: + prop_name = method_name[4:] + return_type = _get_annotation_str(item.returns, import_map) + if prop_name not in result["status_properties"]: + result["status_properties"][prop_name] = { + "name": prop_name, + "return_type": return_type, + "is_property": False, + "topic_config": None, + } + continue + + # --- Public method without @action => auto-action --- + if method_name in ("post_init", "__str__", "__repr__"): + continue + + method_params = _extract_method_params(item, import_map) + return_type = _get_annotation_str(item.returns, import_map) + is_async = isinstance(item, ast.AsyncFunctionDef) + method_doc = ast.get_docstring(item) + + auto_entry: dict = { + "params": method_params, + "return_type": return_type, + "is_async": is_async, + "docstring": method_doc, + } + if _has_decorator(item, "always_free") and _is_registry_decorator("always_free", import_map): + auto_entry["always_free"] = True + result["auto_methods"][method_name] = auto_entry + + return result + + +# --------------------------------------------------------------------------- +# Method parameter extraction +# --------------------------------------------------------------------------- + + +_PARAM_SKIP_NAMES = frozenset({"sample_uuids"}) + + +def _extract_method_params( + func_node: Union[ast.FunctionDef, ast.AsyncFunctionDef], + import_map: Optional[Dict[str, str]] = None, +) -> List[dict]: + """ + Extract parameters from a class method definition. + + Automatically skips the first positional argument (self / cls) and any + domain-specific names listed in ``_PARAM_SKIP_NAMES``. + + Returns: + [{"name": "position", "type": "str", "default": None, "required": True}, ...] + """ + if import_map is None: + import_map = {} + params: List[dict] = [] + + args = func_node.args + + # Skip the first positional arg (self/cls) -- always present for class methods + # noinspection PyUnresolvedReferences + positional_args = args.args[1:] if args.args else [] + + # defaults align to the *end* of the args list; offset must account for the skipped arg + num_args = len(args.args) + num_defaults = len(args.defaults) + first_default_idx = num_args - num_defaults + + for i, arg in enumerate(positional_args, start=1): + name = arg.arg + if name in _PARAM_SKIP_NAMES: + continue + + param_info: dict = {"name": name} + + # Type annotation + if arg.annotation: + param_info["type"] = _get_annotation_str(arg.annotation, import_map) + else: + param_info["type"] = "" + + # Default value + default_idx = i - first_default_idx + if 0 <= default_idx < len(args.defaults): + default_val = _ast_node_to_value(args.defaults[default_idx], import_map) + param_info["default"] = default_val + param_info["required"] = False + else: + param_info["default"] = None + param_info["required"] = True + + params.append(param_info) + + # Keyword-only arguments (self/cls never appear here) + for i, arg in enumerate(args.kwonlyargs): + name = arg.arg + if name in _PARAM_SKIP_NAMES: + continue + + param_info: dict = {"name": name} + + if arg.annotation: + param_info["type"] = _get_annotation_str(arg.annotation, import_map) + else: + param_info["type"] = "" + + if i < len(args.kw_defaults) and args.kw_defaults[i] is not None: + param_info["default"] = _ast_node_to_value(args.kw_defaults[i], import_map) + param_info["required"] = False + else: + param_info["default"] = None + param_info["required"] = True + + params.append(param_info) + + return params + + +def _get_annotation_str(node: Optional[ast.expr], import_map: Dict[str, str]) -> str: + """Convert a type annotation AST node to a string representation. + + 保持类型字符串为合法 Python 表达式 (可被 ast.parse 解析)。 + 不在此处做 import_map 替换 — 由上层在需要时通过 import_map 解析。 + """ + if node is None: + return "" + + if isinstance(node, ast.Constant): + return str(node.value) + + if isinstance(node, ast.Name): + return node.id + + if isinstance(node, ast.Attribute): + parts = [] + current = node + while isinstance(current, ast.Attribute): + parts.append(current.attr) + current = current.value + if isinstance(current, ast.Name): + parts.append(current.id) + parts.reverse() + return ".".join(parts) + + # Handle subscript types like List[str], Dict[str, int], Optional[str] + if isinstance(node, ast.Subscript): + base = _get_annotation_str(node.value, import_map) + if isinstance(node.slice, ast.Tuple): + args = ", ".join(_get_annotation_str(elt, import_map) for elt in node.slice.elts) + else: + args = _get_annotation_str(node.slice, import_map) + return f"{base}[{args}]" + + # Handle Union types (X | Y in Python 3.10+) + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr): + left = _get_annotation_str(node.left, import_map) + right = _get_annotation_str(node.right, import_map) + return f"Union[{left}, {right}]" + + return ast.dump(node) + + +# --------------------------------------------------------------------------- +# Module path derivation +# --------------------------------------------------------------------------- + + +def _filepath_to_module(filepath: Path, python_path: Path) -> str: + """ + 通过 *python_path*(sys.path 中的根目录)推导 Python 模块路径。 + + 做法:取 filepath 相对于 python_path 的路径,将目录分隔符替换为 '.'。 + + E.g. filepath = "/project/unilabos/devices/pump/valve.py" + python_path = "/project" + => "unilabos.devices.pump.valve" + """ + try: + relative = filepath.relative_to(python_path) + except ValueError: + return str(filepath) + + parts = list(relative.parts) + # 去掉 .py 后缀 + if parts and parts[-1].endswith(".py"): + parts[-1] = parts[-1][:-3] + # 去掉 __init__ + if parts and parts[-1] == "__init__": + parts.pop() + + return ".".join(parts) diff --git a/unilabos/registry/decorators.py b/unilabos/registry/decorators.py new file mode 100644 index 00000000..e8c65ac8 --- /dev/null +++ b/unilabos/registry/decorators.py @@ -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) diff --git a/unilabos/registry/devices/Qone_nmr.yaml b/unilabos/registry/devices/Qone_nmr.yaml index fa182c77..5c5f1f8a 100644 --- a/unilabos/registry/devices/Qone_nmr.yaml +++ b/unilabos/registry/devices/Qone_nmr.yaml @@ -13,21 +13,18 @@ Qone_nmr: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -71,31 +68,6 @@ Qone_nmr: title: monitor_folder_for_new_content参数 type: object 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: feedback: {} goal: {} @@ -138,21 +110,18 @@ Qone_nmr: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -167,32 +136,31 @@ Qone_nmr: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: diff --git a/unilabos/registry/devices/bioyond_cell.yaml b/unilabos/registry/devices/bioyond_cell.yaml index fc4b75cb..f57cd35c 100644 --- a/unilabos/registry/devices/bioyond_cell.yaml +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -22,7 +22,8 @@ bioyond_cell: required: - xlsx_path type: object - result: {} + result: + type: object required: - goal title: auto_batch_outbound_from_xlsx参数 @@ -490,7 +491,9 @@ bioyond_cell: goal: properties: material_names: - type: string + items: + type: string + type: array type_id: default: 3a190ca0-b2f6-9aeb-8067-547e72c11469 type: string @@ -499,7 +502,8 @@ bioyond_cell: type: string required: [] type: object - result: {} + result: + type: object required: - goal title: create_and_inbound_materials参数 @@ -535,7 +539,8 @@ bioyond_cell: - type_id - warehouse_name type: object - result: {} + result: + type: object required: - goal title: create_material参数 @@ -556,11 +561,16 @@ bioyond_cell: goal: properties: mappings: + additionalProperties: + type: object type: object required: - mappings type: object - result: {} + result: + items: + type: object + type: array required: - goal title: create_materials参数 @@ -592,7 +602,8 @@ bioyond_cell: required: - xlsx_path type: object - result: {} + result: + type: object required: - goal title: create_orders参数 @@ -624,7 +635,8 @@ bioyond_cell: required: - xlsx_path type: object - result: {} + result: + type: object required: - goal title: create_orders_v2参数 @@ -665,7 +677,8 @@ bioyond_cell: - bottle_type - location_code type: object - result: {} + result: + type: object required: - goal title: create_sample参数 @@ -718,7 +731,8 @@ bioyond_cell: type: string required: [] type: object - result: {} + result: + type: object required: - goal title: order_list_v2参数 @@ -821,7 +835,8 @@ bioyond_cell: required: - material_obj type: object - result: {} + result: + type: object required: - goal title: report_material_change参数 @@ -875,7 +890,8 @@ bioyond_cell: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: scheduler_continue参数 @@ -896,7 +912,8 @@ bioyond_cell: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: scheduler_reset参数 @@ -917,7 +934,8 @@ bioyond_cell: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: scheduler_start参数 @@ -1362,7 +1380,8 @@ bioyond_cell: type: string required: [] type: object - result: {} + result: + type: object required: - goal title: scheduler_start_and_auto_feeding参数 @@ -1807,7 +1826,8 @@ bioyond_cell: type: string required: [] type: object - result: {} + result: + type: object required: - goal title: scheduler_start_and_auto_feeding_v2参数 @@ -1828,7 +1848,8 @@ bioyond_cell: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: scheduler_stop参数 @@ -1850,12 +1871,15 @@ bioyond_cell: properties: items: items: + additionalProperties: + type: string type: object type: array required: - items type: object - result: {} + result: + type: object required: - goal title: storage_batch_inbound参数 @@ -1884,7 +1908,8 @@ bioyond_cell: - material_id - location_id type: object - result: {} + result: + type: object required: - goal title: storage_inbound参数 @@ -1905,7 +1930,8 @@ bioyond_cell: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: transfer_1_to_2参数 @@ -1946,7 +1972,8 @@ bioyond_cell: type: integer required: [] type: object - result: {} + result: + type: object required: - goal title: transfer_3_to_2参数 @@ -1983,7 +2010,8 @@ bioyond_cell: type: integer required: [] type: object - result: {} + result: + type: object required: - goal title: transfer_3_to_2_to_1参数 @@ -2007,10 +2035,11 @@ bioyond_cell: ip: type: string port: - type: string + type: integer required: [] type: object - result: {} + result: + type: object required: - goal title: update_push_ip参数 @@ -2039,7 +2068,8 @@ bioyond_cell: required: - order_code type: object - result: {} + result: + type: object required: - goal title: wait_for_order_finish参数 @@ -2072,7 +2102,8 @@ bioyond_cell: required: - order_code type: object - result: {} + result: + type: object required: - goal title: wait_for_order_finish_polling参数 @@ -2104,7 +2135,8 @@ bioyond_cell: type: integer required: [] type: object - result: {} + result: + type: boolean required: - goal title: wait_for_transfer_task参数 @@ -2112,8 +2144,7 @@ bioyond_cell: type: UniLabJsonCommand module: unilabos.devices.workstation.bioyond_studio.bioyond_cell.bioyond_cell_workstation:BioyondCellWorkstation status_types: - device_id: String - material_info: dict + device_id: '' type: python config_info: [] description: '' @@ -2134,11 +2165,7 @@ bioyond_cell: properties: device_id: type: string - material_info: - type: object required: - device_id - - material_info type: object - registry_type: device version: 1.0.0 diff --git a/unilabos/registry/devices/bioyond_dispensing_station.yaml b/unilabos/registry/devices/bioyond_dispensing_station.yaml index 7b9ebc90..547b54ff 100644 --- a/unilabos/registry/devices/bioyond_dispensing_station.yaml +++ b/unilabos/registry/devices/bioyond_dispensing_station.yaml @@ -24,7 +24,8 @@ bioyond_dispensing_station: required: - data type: object - result: {} + result: + type: object required: - goal title: brief_step_parameters参数 @@ -53,7 +54,8 @@ bioyond_dispensing_station: - report_request - used_materials type: object - result: {} + result: + type: object required: - goal title: process_order_finish_report参数 @@ -78,7 +80,8 @@ bioyond_dispensing_station: required: - order_id type: object - result: {} + result: + type: object required: - goal title: project_order_report参数 @@ -128,7 +131,8 @@ bioyond_dispensing_station: required: - workflow_id type: object - result: {} + result: + type: object required: - goal title: workflow_sample_locations参数 @@ -144,12 +148,12 @@ bioyond_dispensing_station: temperature: temperature titration: titration goal_default: - delay_time: '600' - hold_m_name: '' + delay_time: null + hold_m_name: null liquid_material_name: NMP - speed: '400' - temperature: '40' - titration: '' + speed: null + temperature: null + titration: null handles: input: - data_key: titration @@ -165,20 +169,16 @@ bioyond_dispensing_station: handler_key: BATCH_CREATE_RESULT io_type: sink label: Complete Batch Create Result JSON (contains order_codes and order_ids) - result: - return_info: return_info + placeholder_keys: {} + result: {} schema: description: 批量创建90%10%小瓶投料任务。从计算节点接收titration数据,包含物料名称、主称固体质量、滴定固体质量和滴定溶剂体积。返回的return_info中包含order_codes和order_ids列表。 properties: feedback: - properties: {} - required: [] title: BatchCreate9010VialFeedingTasks_Feedback - type: object goal: properties: delay_time: - default: '600' description: 延迟时间(秒),默认600 type: string hold_m_name: @@ -189,11 +189,9 @@ bioyond_dispensing_station: description: 10%物料的液体物料名称,默认为"NMP" type: string speed: - default: '400' description: 搅拌速度,默认400 type: string temperature: - default: '40' description: 温度(℃),默认40 type: string titration: @@ -202,21 +200,14 @@ bioyond_dispensing_station: type: string required: - titration - - hold_m_name title: BatchCreate9010VialFeedingTasks_Goal type: object result: - properties: - return_info: - description: 批量任务创建结果汇总JSON字符串,包含total(总数)、success(成功数)、failed(失败数)、order_codes(任务编码数组)、order_ids(任务ID数组)、details(每个任务的详细信息) - type: string - required: - - return_info title: BatchCreate9010VialFeedingTasks_Result - type: object + type: string required: - goal - title: BatchCreate9010VialFeedingTasks + title: batch_create_90_10_vial_feeding_tasks参数 type: object type: UniLabJsonCommand batch_create_diamine_solution_tasks: @@ -228,11 +219,11 @@ bioyond_dispensing_station: speed: speed temperature: temperature goal_default: - delay_time: '600' + delay_time: null liquid_material_name: NMP - solutions: '' - speed: '400' - temperature: '20' + solutions: null + speed: null + temperature: null handles: input: - data_key: solutions @@ -248,20 +239,16 @@ bioyond_dispensing_station: handler_key: BATCH_CREATE_RESULT io_type: sink label: Complete Batch Create Result JSON (contains order_codes and order_ids) - result: - return_info: return_info + placeholder_keys: {} + result: {} schema: description: 批量创建二胺溶液配置任务。自动为多个二胺样品创建溶液配置任务,每个任务包含固体物料称量、溶剂添加、搅拌混合等步骤。返回的return_info中包含order_codes和order_ids列表。 properties: feedback: - properties: {} - required: [] title: BatchCreateDiamineSolutionTasks_Feedback - type: object goal: properties: delay_time: - default: '600' description: 溶液配置完成后的延迟时间(秒),用于充分混合和溶解,默认600秒 type: string liquid_material_name: @@ -275,11 +262,9 @@ bioyond_dispensing_station: 4.5, "solvent_volume": 18}]' type: string speed: - default: '400' description: 搅拌速度(rpm),用于混合溶液,默认400转/分钟 type: string temperature: - default: '20' description: 配置温度(℃),溶液配置过程的目标温度,默认20℃(室温) type: string required: @@ -287,17 +272,11 @@ bioyond_dispensing_station: title: BatchCreateDiamineSolutionTasks_Goal type: object result: - properties: - return_info: - description: 批量任务创建结果汇总JSON字符串,包含total(总数)、success(成功数)、failed(失败数)、order_codes(任务编码数组)、order_ids(任务ID数组)、details(每个任务的详细信息) - type: string - required: - - return_info title: BatchCreateDiamineSolutionTasks_Result - type: object + type: string required: - goal - title: BatchCreateDiamineSolutionTasks + title: batch_create_diamine_solution_tasks参数 type: object type: UniLabJsonCommand compute_experiment_design: @@ -309,7 +288,7 @@ bioyond_dispensing_station: wt_percent: wt_percent goal_default: m_tot: '70' - ratio: '' + ratio: null titration_percent: '0.03' wt_percent: '0.25' handles: @@ -338,12 +317,8 @@ bioyond_dispensing_station: handler_key: feeding_order io_type: sink label: Feeding Order Data From Calculation Node - result: - feeding_order: feeding_order - return_info: return_info - solutions: solutions - solvents: solvents - titration: titration + placeholder_keys: {} + result: {} schema: description: 计算实验设计,输出solutions/titration/solvents/feeding_order用于后续节点。 properties: @@ -356,7 +331,7 @@ bioyond_dispensing_station: type: string ratio: description: 组分摩尔比的对象,保持输入顺序,如{"MDA":1,"BTDA":1} - type: string + type: object titration_percent: default: '0.03' description: 滴定比例(10%部分) @@ -371,14 +346,23 @@ bioyond_dispensing_station: result: properties: feeding_order: + items: {} + title: Feeding Order type: array return_info: + title: Return Info type: string solutions: + items: {} + title: Solutions type: array solvents: + additionalProperties: true + title: Solvents type: object titration: + additionalProperties: true + title: Titration type: object required: - solutions @@ -386,11 +370,11 @@ bioyond_dispensing_station: - solvents - feeding_order - return_info - title: ComputeExperimentDesign_Result + title: ComputeExperimentDesignReturn type: object required: - goal - title: ComputeExperimentDesign + title: compute_experiment_design参数 type: object type: UniLabJsonCommand create_90_10_vial_feeding_task: @@ -444,17 +428,18 @@ bioyond_dispensing_station: speed: '' temperature: '' handles: {} + placeholder_keys: {} result: return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: DispenStationVialFeed_Feedback type: object goal: + additionalProperties: false properties: delay_time: type: string @@ -502,38 +487,13 @@ bioyond_dispensing_station: type: string temperature: 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 type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: DispenStationVialFeed_Result type: object required: @@ -564,17 +524,18 @@ bioyond_dispensing_station: temperature: '' volume: '' handles: {} + placeholder_keys: {} result: return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: DispenStationSolnPrep_Feedback type: object goal: + additionalProperties: false properties: delay_time: type: string @@ -594,24 +555,13 @@ bioyond_dispensing_station: type: string volume: type: string - required: - - order_name - - material_name - - target_weigh - - volume - - liquid_material_name - - speed - - temperature - - delay_time - - hold_m_name title: DispenStationSolnPrep_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: DispenStationSolnPrep_Result type: object required: @@ -624,8 +574,8 @@ bioyond_dispensing_station: goal: {} goal_default: {} handles: {} - result: - return_info: return_info + placeholder_keys: {} + result: {} schema: description: 启动调度器 - 启动Bioyond配液站的任务调度器,开始执行队列中的任务 properties: @@ -635,12 +585,6 @@ bioyond_dispensing_station: required: [] type: object result: - properties: - return_info: - description: 调度器启动结果,成功返回1,失败返回0 - type: integer - required: - - return_info title: scheduler_start结果 type: object required: @@ -654,8 +598,8 @@ bioyond_dispensing_station: target_device_id: target_device_id transfer_groups: transfer_groups goal_default: - target_device_id: '' - transfer_groups: '' + target_device_id: null + transfer_groups: null handles: {} placeholder_keys: target_device_id: unilabos_devices @@ -671,32 +615,13 @@ bioyond_dispensing_station: type: string transfer_groups: 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 required: - target_device_id - transfer_groups type: object - result: {} + result: + type: object required: - goal title: transfer_materials_to_reaction_station参数 @@ -709,9 +634,9 @@ bioyond_dispensing_station: check_interval: check_interval timeout: timeout goal_default: - batch_create_result: '' - check_interval: '10' - timeout: '7200' + batch_create_result: null + check_interval: 10 + timeout: 7200 handles: input: - data_key: batch_create_result @@ -727,47 +652,35 @@ bioyond_dispensing_station: handler_key: batch_reports_result io_type: sink label: Batch Order Completion Reports - result: - return_info: return_info + placeholder_keys: {} + result: {} schema: description: 同时等待多个任务完成并获取所有实验报告。从上游batch_create任务接收包含order_codes和order_ids的结果对象,并行监控所有任务状态并返回每个任务的报告。 properties: feedback: - properties: {} - required: [] title: WaitForMultipleOrdersAndGetReports_Feedback - type: object goal: properties: batch_create_result: description: 批量创建任务的返回结果对象,包含order_codes和order_ids数组。从上游batch_create节点通过handle传递 type: string check_interval: - default: '10' + default: 10 description: 检查任务状态的时间间隔(秒),默认每10秒检查一次所有待完成任务 - type: string + type: integer timeout: - default: '7200' + default: 7200 description: 等待超时时间(秒),默认7200秒(2小时)。超过此时间未完成的任务将标记为timeout - type: string - required: - - batch_create_result + type: integer + required: [] title: WaitForMultipleOrdersAndGetReports_Goal type: object 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 type: object required: - goal - title: WaitForMultipleOrdersAndGetReports + title: wait_for_multiple_orders_and_get_reports参数 type: object type: UniLabJsonCommand module: unilabos.devices.workstation.bioyond_studio.dispensing_station.dispensing_station:BioyondDispensingStation diff --git a/unilabos/registry/devices/camera.yaml b/unilabos/registry/devices/camera.yaml deleted file mode 100644 index c8b9d944..00000000 --- a/unilabos/registry/devices/camera.yaml +++ /dev/null @@ -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 diff --git a/unilabos/registry/devices/cameraSII.yaml b/unilabos/registry/devices/cameraSII.yaml index ad2df955..446357d0 100644 --- a/unilabos/registry/devices/cameraSII.yaml +++ b/unilabos/registry/devices/cameraSII.yaml @@ -18,7 +18,7 @@ cameracontroller_device: goal: properties: config: - type: string + type: object required: [] type: object result: {} @@ -42,7 +42,8 @@ cameracontroller_device: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: stop参数 @@ -50,7 +51,7 @@ cameracontroller_device: type: UniLabJsonCommand module: unilabos.devices.cameraSII.cameraUSB:CameraController status_types: - status: dict + status: Dict[str, Any] type: python config_info: [] description: Uni-Lab-OS 摄像头驱动(Linux USB 摄像头版,无 PTZ) @@ -103,5 +104,4 @@ cameracontroller_device: required: - status type: object - registry_type: device version: 1.0.0 diff --git a/unilabos/registry/devices/characterization_chromatic.yaml b/unilabos/registry/devices/characterization_chromatic.yaml index f3059b58..1b33b9e2 100644 --- a/unilabos/registry/devices/characterization_chromatic.yaml +++ b/unilabos/registry/devices/characterization_chromatic.yaml @@ -141,30 +141,26 @@ hplc.agilent: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -175,7 +171,6 @@ hplc.agilent: module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver status_types: could_run: bool - data_file: String device_status: str driver_init_ok: bool finish_status: str @@ -199,10 +194,6 @@ hplc.agilent: properties: could_run: type: boolean - data_file: - items: - type: string - type: array device_status: type: string driver_init_ok: @@ -216,14 +207,13 @@ hplc.agilent: success: type: boolean required: - - status_text - - device_status - could_run + - device_status - driver_init_ok - - is_running - - success - finish_status - - data_file + - is_running + - status_text + - success type: object version: 1.0.0 hplc.agilent-zhida: @@ -236,26 +226,25 @@ hplc.agilent-zhida: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -315,21 +304,18 @@ hplc.agilent-zhida: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -341,35 +327,35 @@ hplc.agilent-zhida: feedback: {} goal: string: string + text: text goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -407,7 +393,7 @@ hplc.agilent-zhida: status: type: object required: - - status - methods + - status type: object version: 1.0.0 diff --git a/unilabos/registry/devices/characterization_optic.yaml b/unilabos/registry/devices/characterization_optic.yaml index 80dcf93d..a7c0e98d 100644 --- a/unilabos/registry/devices/characterization_optic.yaml +++ b/unilabos/registry/devices/characterization_optic.yaml @@ -120,42 +120,41 @@ raman.home_made: type: object type: UniLabJsonCommand raman_cmd: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: diff --git a/unilabos/registry/devices/chinwe.yaml b/unilabos/registry/devices/chinwe.yaml index 2078d0f0..ac4d00bb 100644 --- a/unilabos/registry/devices/chinwe.yaml +++ b/unilabos/registry/devices/chinwe.yaml @@ -19,7 +19,8 @@ separator.chinwe: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: connect参数 @@ -65,135 +66,145 @@ separator.chinwe: required: - command_dict type: object - result: {} + result: + type: boolean required: - goal title: execute_command_from_outer参数 type: object type: UniLabJsonCommand motor_rotate_quarter: + feedback: {} goal: direction: 顺时针 motor_id: 4 speed: 60 + goal_default: + direction: 顺时针 + motor_id: null + speed: 60 handles: {} + placeholder_keys: {} + result: {} schema: description: 电机旋转 1/4 圈 properties: + feedback: {} goal: properties: direction: default: 顺时针 description: 旋转方向 - enum: - - 顺时针 - - 逆时针 type: string motor_id: - default: '4' description: 选择电机 (4:搅拌, 5:旋钮) - enum: - - '4' - - '5' - type: string + type: integer speed: default: 60 description: 速度 (RPM) type: integer required: - motor_id - - speed type: object + result: {} + required: + - goal + title: motor_rotate_quarter参数 + type: object type: UniLabJsonCommand motor_run_continuous: + feedback: {} goal: direction: 顺时针 motor_id: 4 speed: 60 + goal_default: + direction: 顺时针 + motor_id: null + speed: null handles: {} + placeholder_keys: {} + result: {} schema: description: 电机一直旋转 (速度模式) properties: + feedback: {} goal: properties: direction: default: 顺时针 description: 旋转方向 - enum: - - 顺时针 - - 逆时针 type: string motor_id: - default: '4' description: 选择电机 (4:搅拌, 5:旋钮) - enum: - - '4' - - '5' - type: string + type: integer speed: - default: 60 description: 速度 (RPM) type: integer required: - motor_id - speed type: object + result: {} + required: + - goal + title: motor_run_continuous参数 + type: object type: UniLabJsonCommand motor_stop: + feedback: {} goal: motor_id: 4 + goal_default: + motor_id: null handles: {} + placeholder_keys: {} + result: {} schema: description: 停止指定步进电机 properties: + feedback: {} goal: properties: motor_id: - default: '4' description: 选择电机 - enum: - - '4' - - '5' title: '注: 4=搅拌, 5=旋钮' - type: string + type: integer required: - motor_id type: object + result: {} + required: + - goal + title: motor_stop参数 + type: object type: UniLabJsonCommand pump_aspirate: + feedback: {} goal: pump_id: 1 valve_port: 1 volume: 1000 + goal_default: + pump_id: null + valve_port: null + volume: null handles: {} + placeholder_keys: {} + result: {} schema: description: 注射泵吸液 properties: + feedback: {} goal: properties: pump_id: - default: '1' description: 选择泵 - enum: - - '1' - - '2' - - '3' - type: string + type: integer valve_port: - default: '1' description: 阀门端口 - enum: - - '1' - - '2' - - '3' - - '4' - - '5' - - '6' - - '7' - - '8' - type: string + type: integer volume: - default: 1000 description: 吸液步数 type: integer required: @@ -201,41 +212,38 @@ separator.chinwe: - volume - valve_port type: object + result: {} + required: + - goal + title: pump_aspirate参数 + type: object type: UniLabJsonCommand pump_dispense: + feedback: {} goal: pump_id: 1 valve_port: 1 volume: 1000 + goal_default: + pump_id: null + valve_port: null + volume: null handles: {} + placeholder_keys: {} + result: {} schema: description: 注射泵排液 properties: + feedback: {} goal: properties: pump_id: - default: '1' description: 选择泵 - enum: - - '1' - - '2' - - '3' - type: string + type: integer valve_port: - default: '1' description: 阀门端口 - enum: - - '1' - - '2' - - '3' - - '4' - - '5' - - '6' - - '7' - - '8' - type: string + type: integer volume: - default: 1000 description: 排液步数 type: integer required: @@ -243,121 +251,152 @@ separator.chinwe: - volume - valve_port type: object + result: {} + required: + - goal + title: pump_dispense参数 + type: object type: UniLabJsonCommand pump_initialize: + feedback: {} goal: drain_port: 0 output_port: 0 pump_id: 1 speed: 10 + goal_default: + drain_port: 0 + output_port: 0 + pump_id: null + speed: 10 handles: {} + placeholder_keys: {} + result: {} schema: description: 初始化指定注射泵 properties: + feedback: {} goal: properties: drain_port: default: 0 description: 排液口索引 - type: integer + type: string output_port: default: 0 description: 输出口索引 - type: integer - pump_id: - default: '1' - description: 选择泵 - enum: - - '1' - - '2' - - '3' - title: '注: 1号泵, 2号泵, 3号泵' type: string + pump_id: + description: 选择泵 + title: '注: 1号泵, 2号泵, 3号泵' + type: integer speed: default: 10 description: 运动速度 - type: integer + type: string required: - pump_id type: object + result: {} + required: + - goal + title: pump_initialize参数 + type: object type: UniLabJsonCommand pump_valve: + feedback: {} goal: port: 1 pump_id: 1 + goal_default: + port: null + pump_id: null handles: {} + placeholder_keys: {} + result: {} schema: description: 切换指定泵的阀门端口 properties: + feedback: {} goal: properties: port: - default: '1' description: 阀门端口号 (1-8) - enum: - - '1' - - '2' - - '3' - - '4' - - '5' - - '6' - - '7' - - '8' - type: string + type: integer pump_id: - default: '1' description: 选择泵 - enum: - - '1' - - '2' - - '3' - type: string + type: integer required: - pump_id - port type: object + result: {} + required: + - goal + title: pump_valve参数 + type: object type: UniLabJsonCommand wait_sensor_level: + feedback: {} goal: target_state: 有液 timeout: 30 + goal_default: + target_state: 有液 + timeout: 30 handles: {} + placeholder_keys: {} + result: {} schema: description: 等待传感器液位条件 properties: + feedback: {} goal: properties: target_state: default: 有液 description: 目标液位状态 - enum: - - 有液 - - 无液 type: string timeout: default: 30 description: 超时时间 (秒) type: integer - required: - - target_state + required: [] type: object + result: + type: boolean + required: + - goal + title: wait_sensor_level参数 + type: object type: UniLabJsonCommand wait_time: + feedback: {} goal: duration: 10 + goal_default: + duration: null handles: {} + placeholder_keys: {} + result: {} schema: description: 等待指定时间 properties: + feedback: {} goal: properties: duration: - default: 10 description: 等待时间 (秒) type: integer required: - duration type: object + result: + type: boolean + required: + - goal + title: wait_time参数 + type: object type: UniLabJsonCommand module: unilabos.devices.separator.chinwe:ChinweDevice status_types: @@ -406,8 +445,8 @@ separator.chinwe: sensor_rssi: type: integer required: + - is_connected - sensor_level - sensor_rssi - - is_connected type: object version: 2.1.0 diff --git a/unilabos/registry/devices/coin_cell_workstation.yaml b/unilabos/registry/devices/coin_cell_workstation.yaml index 2e9f6073..df5a3508 100644 --- a/unilabos/registry/devices/coin_cell_workstation.yaml +++ b/unilabos/registry/devices/coin_cell_workstation.yaml @@ -64,7 +64,8 @@ coincellassemblyworkstation_device: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: fun_wuliao_test参数 @@ -109,7 +110,8 @@ coincellassemblyworkstation_device: - elec_num - elec_use_num type: object - result: {} + result: + type: object required: - goal title: func_allpack_cmd参数 @@ -220,7 +222,8 @@ coincellassemblyworkstation_device: - elec_num - elec_use_num type: object - result: {} + result: + type: object required: - goal title: func_allpack_cmd_simp参数 @@ -309,7 +312,8 @@ coincellassemblyworkstation_device: type: boolean required: [] type: object - result: {} + result: + type: boolean required: - goal title: func_pack_device_init_auto_start_combined参数 @@ -351,7 +355,8 @@ coincellassemblyworkstation_device: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: func_pack_device_stop参数 @@ -376,7 +381,8 @@ coincellassemblyworkstation_device: type: string required: [] type: object - result: {} + result: + type: boolean required: - goal title: func_pack_get_msg_cmd参数 @@ -430,7 +436,8 @@ coincellassemblyworkstation_device: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: func_pack_send_finished_cmd参数 @@ -467,7 +474,8 @@ coincellassemblyworkstation_device: - assembly_type - assembly_pressure type: object - result: {} + result: + type: boolean required: - goal title: func_pack_send_msg_cmd参数 @@ -611,7 +619,8 @@ coincellassemblyworkstation_device: - elec_num - elec_use_num type: object - result: {} + result: + type: object required: - goal title: func_sendbottle_allpack_multi参数 @@ -663,31 +672,6 @@ coincellassemblyworkstation_device: title: modify_deck_name参数 type: object 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: feedback: {} goal: {} @@ -735,7 +719,8 @@ coincellassemblyworkstation_device: required: - fujipian_panshu type: object - result: {} + result: + type: boolean required: - goal title: qiming_coin_cell_code参数 @@ -826,25 +811,24 @@ coincellassemblyworkstation_device: sys_status: type: string required: - - sys_status - - sys_mode - - request_rec_msg_status - - request_send_msg_status - data_assembly_coin_cell_num + - data_assembly_pressure - data_assembly_time - - data_open_circuit_voltage - data_axis_x_pos - data_axis_y_pos - data_axis_z_pos - - data_pole_weight - - data_assembly_pressure - - data_electrolyte_volume - - data_coin_num - data_coin_cell_code + - data_coin_num - data_electrolyte_code - - data_glove_box_pressure + - data_electrolyte_volume - data_glove_box_o2_content + - data_glove_box_pressure - 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 - registry_type: device version: 1.0.0 diff --git a/unilabos/registry/devices/gas_handler.yaml b/unilabos/registry/devices/gas_handler.yaml index 65218619..ad212575 100644 --- a/unilabos/registry/devices/gas_handler.yaml +++ b/unilabos/registry/devices/gas_handler.yaml @@ -50,26 +50,25 @@ gas_source.mock: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -82,26 +81,25 @@ gas_source.mock: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -116,32 +114,31 @@ gas_source.mock: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -232,26 +229,25 @@ vacuum_pump.mock: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -264,26 +260,25 @@ vacuum_pump.mock: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -298,32 +293,31 @@ vacuum_pump.mock: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: diff --git a/unilabos/registry/devices/hotel.yaml b/unilabos/registry/devices/hotel.yaml index 3fd0ea5b..fdcc89dd 100644 --- a/unilabos/registry/devices/hotel.yaml +++ b/unilabos/registry/devices/hotel.yaml @@ -5,7 +5,7 @@ hotel.thermo_orbitor_rs2_hotel: action_value_mappings: {} module: unilabos.devices.resource_container.container:HotelContainer status_types: - rotation: String + rotation: '' type: python config_info: [] description: Thermo Orbitor RS2 Hotel容器设备,用于实验室样品的存储和管理。该设备通过HotelContainer类实现容器的旋转控制和状态监控,主要用于存储实验样品、试剂瓶或其他实验器具,支持旋转功能以便于样品的自动化存取。适用于需要有序存储和快速访问大量样品的实验室自动化场景。 diff --git a/unilabos/registry/devices/laiyu_liquid_test.yaml b/unilabos/registry/devices/laiyu_liquid_test.yaml index dcaa9818..6d87f429 100644 --- a/unilabos/registry/devices/laiyu_liquid_test.yaml +++ b/unilabos/registry/devices/laiyu_liquid_test.yaml @@ -22,7 +22,8 @@ xyz_stepper_controller: required: - degrees type: object - result: {} + result: + type: integer required: - goal title: degrees_to_steps参数 @@ -47,7 +48,8 @@ xyz_stepper_controller: required: - axis type: object - result: {} + result: + type: boolean required: - goal title: emergency_stop参数 @@ -72,7 +74,10 @@ xyz_stepper_controller: type: boolean required: [] type: object - result: {} + result: + additionalProperties: + type: boolean + type: object required: - goal title: enable_all_axes参数 @@ -101,7 +106,8 @@ xyz_stepper_controller: required: - axis type: object - result: {} + result: + type: boolean required: - goal title: enable_motor参数 @@ -122,7 +128,10 @@ xyz_stepper_controller: properties: {} required: [] type: object - result: {} + result: + additionalProperties: + type: boolean + type: object required: - goal title: home_all_axes参数 @@ -147,7 +156,8 @@ xyz_stepper_controller: required: - axis type: object - result: {} + result: + type: boolean required: - goal title: home_axis参数 @@ -188,7 +198,8 @@ xyz_stepper_controller: - axis - position type: object - result: {} + result: + type: boolean required: - goal title: move_to_position参数 @@ -229,7 +240,8 @@ xyz_stepper_controller: - axis - degrees type: object - result: {} + result: + type: boolean required: - goal title: move_to_position_degrees参数 @@ -270,7 +282,8 @@ xyz_stepper_controller: - axis - revolutions type: object - result: {} + result: + type: boolean required: - goal title: move_to_position_revolutions参数 @@ -301,14 +314,17 @@ xyz_stepper_controller: default: 5000 type: integer x: - type: string + type: integer y: - type: string + type: integer z: - type: string + type: integer required: [] type: object - result: {} + result: + additionalProperties: + type: boolean + type: object required: - goal title: move_xyz参数 @@ -339,14 +355,17 @@ xyz_stepper_controller: default: 5000 type: integer x_deg: - type: string + type: number y_deg: - type: string + type: number z_deg: - type: string + type: number required: [] type: object - result: {} + result: + additionalProperties: + type: boolean + type: object required: - goal title: move_xyz_degrees参数 @@ -377,14 +396,17 @@ xyz_stepper_controller: default: 5000 type: integer x_rev: - type: string + type: number y_rev: - type: string + type: number z_rev: - type: string + type: number required: [] type: object - result: {} + result: + additionalProperties: + type: boolean + type: object required: - goal title: move_xyz_revolutions参数 @@ -409,7 +431,8 @@ xyz_stepper_controller: required: - revolutions type: object - result: {} + result: + type: integer required: - goal title: revolutions_to_steps参数 @@ -442,7 +465,8 @@ xyz_stepper_controller: - axis - speed type: object - result: {} + result: + type: boolean required: - goal title: set_speed_mode参数 @@ -467,7 +491,8 @@ xyz_stepper_controller: required: - steps type: object - result: {} + result: + type: number required: - goal title: steps_to_degrees参数 @@ -492,7 +517,8 @@ xyz_stepper_controller: required: - steps type: object - result: {} + result: + type: number required: - goal title: steps_to_revolutions参数 @@ -513,7 +539,10 @@ xyz_stepper_controller: properties: {} required: [] type: object - result: {} + result: + additionalProperties: + type: boolean + type: object required: - goal title: stop_all_axes参数 @@ -542,7 +571,8 @@ xyz_stepper_controller: required: - axis type: object - result: {} + result: + type: boolean required: - goal title: wait_for_completion参数 @@ -550,8 +580,7 @@ xyz_stepper_controller: type: UniLabJsonCommand module: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:XYZStepperController status_types: - all_positions: dict - motor_status: unilabos.devices.liquid_handling.laiyu.drivers.xyz_stepper_driver:MotorPosition + all_positions: Dict[MotorAxis, MotorPosition] type: python config_info: [] description: 新XYZ控制器 @@ -574,12 +603,10 @@ xyz_stepper_controller: data: properties: all_positions: - type: object - motor_status: + additionalProperties: + type: object type: object required: - - motor_status - all_positions type: object - registry_type: device version: 1.0.0 diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index b04d6317..4d2f7288 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -8,6 +8,7 @@ liquid_handler: goal: asp_vols: asp_vols blow_out_air_volume: blow_out_air_volume + delays: delays dis_vols: dis_vols flow_rates: flow_rates is_96_well: is_96_well @@ -23,84 +24,38 @@ liquid_handler: targets: targets use_channels: use_channels goal_default: - asp_vols: - - 0.0 - blow_out_air_volume: - - 0.0 - dis_vols: - - 0.0 - flow_rates: - - 0.0 + asp_vols: [] + blow_out_air_volume: [] + dis_vols: [] + flow_rates: [] is_96_well: false - liquid_height: - - 0.0 + liquid_height: [] mix_liquid_height: 0.0 mix_rate: 0 mix_time: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - reagent_sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + reagent_sources: [] spread: '' - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + targets: [] + use_channels: [] handles: {} placeholder_keys: reagent_sources: unilabos_resources targets: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerAdd_Feedback type: object goal: + additionalProperties: false properties: asp_vols: items: @@ -125,6 +80,8 @@ liquid_handler: type: number type: array mix_liquid_height: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -155,7 +112,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array reagent_sources: @@ -230,7 +186,6 @@ liquid_handler: - pose - config - data - title: reagent_sources type: object type: array spread: @@ -307,43 +262,21 @@ liquid_handler: - pose - config - data - title: targets type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - asp_vols - - dis_vols - - reagent_sources - - targets - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread - - is_96_well - - mix_time - - mix_vol - - mix_rate - - mix_liquid_height - - none_keys title: LiquidHandlerAdd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerAdd_Result type: object required: @@ -362,41 +295,14 @@ liquid_handler: use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: - - 0.0 - flow_rates: - - 0.0 - liquid_height: - - 0.0 - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - resources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + blow_out_air_volume: [] + flow_rates: [] + liquid_height: [] + offsets: [] + resources: [] spread: '' - use_channels: - - 0 - vols: - - 0.0 + use_channels: [] + vols: [] handles: {} result: name: name @@ -404,11 +310,11 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerAspirate_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: @@ -435,7 +341,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array resources: @@ -510,41 +415,27 @@ liquid_handler: - pose - config - data - title: resources type: object type: array spread: type: string use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: items: type: number type: array - required: - - resources - - vols - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread title: LiquidHandlerAspirate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerAspirate_Result type: object required: @@ -574,7 +465,9 @@ liquid_handler: properties: none_keys: default: [] - type: string + items: + type: string + type: array protocol_author: type: string protocol_date: @@ -644,41 +537,19 @@ liquid_handler: goal: properties: tip_racks: - type: string + items: + type: object + type: array required: - tip_racks type: object - result: {} + result: + type: string required: - goal title: iter_tips参数 type: object 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-set_group: feedback: {} goal: {} @@ -698,9 +569,13 @@ liquid_handler: group_name: type: string volumes: - type: string + items: + type: number + type: array wells: - type: string + items: + type: object + type: array required: - group_name - wells @@ -712,6 +587,259 @@ liquid_handler: title: set_group参数 type: object type: UniLabJsonCommand + auto-set_liquid: + feedback: {} + goal: {} + goal_default: + liquid_names: null + volumes: null + wells: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: set_liquid的参数schema + properties: + feedback: {} + goal: + properties: + liquid_names: + items: + type: string + type: array + volumes: + items: + type: number + type: array + wells: + items: + type: object + type: array + required: + - wells + - liquid_names + - volumes + type: object + result: + $defs: + ResourceDict: + properties: + class: + description: Resource class name + title: Class + type: string + config: + additionalProperties: true + description: Resource configuration + title: Config + type: object + data: + additionalProperties: true + description: 'Resource data, eg: container liquid data' + title: Data + type: object + description: + default: '' + description: Resource description + title: Description + type: string + extra: + additionalProperties: true + description: 'Extra data, eg: slot index' + title: Extra + type: object + icon: + default: '' + description: Resource icon + title: Icon + type: string + id: + description: Resource ID + title: Id + type: string + machine_name: + default: '' + description: Machine this resource belongs to + title: Machine Name + type: string + model: + additionalProperties: true + description: Resource model + title: Model + type: object + name: + description: Resource name + title: Name + type: string + parent: + anyOf: + - $ref: '#/$defs/ResourceDict' + - type: 'null' + default: null + description: Parent resource object + parent_uuid: + anyOf: + - type: string + - type: 'null' + default: null + description: Parent resource uuid + title: Parent Uuid + pose: + $ref: '#/$defs/ResourceDictPosition' + description: Resource position + schema: + additionalProperties: true + description: Resource schema + title: Schema + type: object + type: + anyOf: + - const: device + type: string + - type: string + description: Resource type + title: Type + uuid: + description: Resource UUID + title: Uuid + type: string + required: + - id + - uuid + - name + - type + - class + - config + - data + - extra + title: ResourceDict + type: object + ResourceDictPosition: + properties: + cross_section_type: + default: rectangle + description: Cross section type + enum: + - rectangle + - circle + - rounded_rectangle + title: Cross Section Type + type: string + extra: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + default: null + description: Extra data + title: Extra + layout: + default: x-y + description: Resource layout + enum: + - 2d + - x-y + - z-y + - x-z + title: Layout + type: string + position: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource position + position3d: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource position in 3D space + rotation: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource rotation + scale: + $ref: '#/$defs/ResourceDictPositionScale' + description: Resource scale + size: + $ref: '#/$defs/ResourceDictPositionSize' + description: Resource size + title: ResourceDictPosition + type: object + ResourceDictPositionObject: + properties: + x: + default: 0.0 + description: X coordinate + title: X + type: number + y: + default: 0.0 + description: Y coordinate + title: Y + type: number + z: + default: 0.0 + description: Z coordinate + title: Z + type: number + title: ResourceDictPositionObject + type: object + ResourceDictPositionScale: + properties: + x: + default: 0.0 + description: x scale + title: X + type: number + y: + default: 0.0 + description: y scale + title: Y + type: number + z: + default: 0.0 + description: z scale + title: Z + type: number + title: ResourceDictPositionScale + type: object + ResourceDictPositionSize: + properties: + depth: + default: 0.0 + description: Depth + title: Depth + type: number + height: + default: 0.0 + description: Height + title: Height + type: number + width: + default: 0.0 + description: Width + title: Width + type: number + title: ResourceDictPositionSize + type: object + properties: + volumes: + items: + type: number + title: Volumes + type: array + wells: + items: + items: + $ref: '#/$defs/ResourceDict' + type: array + title: Wells + type: array + required: + - wells + - volumes + title: SetLiquidReturn + type: object + required: + - goal + title: set_liquid参数 + type: object + type: UniLabJsonCommand auto-set_liquid_from_plate: feedback: {} goal: {} @@ -721,7 +849,8 @@ liquid_handler: volumes: null well_names: null handles: {} - placeholder_keys: {} + placeholder_keys: + plate: unilabos_resources result: {} schema: description: '' @@ -730,20 +859,326 @@ liquid_handler: goal: properties: liquid_names: - type: string + items: + type: string + type: array plate: - type: string + additionalProperties: false + properties: + category: + type: string + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + additionalProperties: false + properties: + orientation: + additionalProperties: false + properties: + w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + additionalProperties: false + properties: + x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + title: plate + type: object volumes: - type: string + items: + type: number + type: array well_names: - type: string + items: + type: string + type: array required: - plate - well_names - liquid_names - volumes type: object - result: {} + result: + $defs: + ResourceDict: + properties: + class: + description: Resource class name + title: Class + type: string + config: + additionalProperties: true + description: Resource configuration + title: Config + type: object + data: + additionalProperties: true + description: 'Resource data, eg: container liquid data' + title: Data + type: object + description: + default: '' + description: Resource description + title: Description + type: string + extra: + additionalProperties: true + description: 'Extra data, eg: slot index' + title: Extra + type: object + icon: + default: '' + description: Resource icon + title: Icon + type: string + id: + description: Resource ID + title: Id + type: string + machine_name: + default: '' + description: Machine this resource belongs to + title: Machine Name + type: string + model: + additionalProperties: true + description: Resource model + title: Model + type: object + name: + description: Resource name + title: Name + type: string + parent: + anyOf: + - $ref: '#/$defs/ResourceDict' + - type: 'null' + default: null + description: Parent resource object + parent_uuid: + anyOf: + - type: string + - type: 'null' + default: null + description: Parent resource uuid + title: Parent Uuid + pose: + $ref: '#/$defs/ResourceDictPosition' + description: Resource position + schema: + additionalProperties: true + description: Resource schema + title: Schema + type: object + type: + anyOf: + - const: device + type: string + - type: string + description: Resource type + title: Type + uuid: + description: Resource UUID + title: Uuid + type: string + required: + - id + - uuid + - name + - type + - class + - config + - data + - extra + title: ResourceDict + type: object + ResourceDictPosition: + properties: + cross_section_type: + default: rectangle + description: Cross section type + enum: + - rectangle + - circle + - rounded_rectangle + title: Cross Section Type + type: string + extra: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + default: null + description: Extra data + title: Extra + layout: + default: x-y + description: Resource layout + enum: + - 2d + - x-y + - z-y + - x-z + title: Layout + type: string + position: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource position + position3d: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource position in 3D space + rotation: + $ref: '#/$defs/ResourceDictPositionObject' + description: Resource rotation + scale: + $ref: '#/$defs/ResourceDictPositionScale' + description: Resource scale + size: + $ref: '#/$defs/ResourceDictPositionSize' + description: Resource size + title: ResourceDictPosition + type: object + ResourceDictPositionObject: + properties: + x: + default: 0.0 + description: X coordinate + title: X + type: number + y: + default: 0.0 + description: Y coordinate + title: Y + type: number + z: + default: 0.0 + description: Z coordinate + title: Z + type: number + title: ResourceDictPositionObject + type: object + ResourceDictPositionScale: + properties: + x: + default: 0.0 + description: x scale + title: X + type: number + y: + default: 0.0 + description: y scale + title: Y + type: number + z: + default: 0.0 + description: z scale + title: Z + type: number + title: ResourceDictPositionScale + type: object + ResourceDictPositionSize: + properties: + depth: + default: 0.0 + description: Depth + title: Depth + type: number + height: + default: 0.0 + description: Height + title: Height + type: number + width: + default: 0.0 + description: Width + title: Width + type: number + title: ResourceDictPositionSize + type: object + properties: + plate: + items: + items: + $ref: '#/$defs/ResourceDict' + type: array + title: Plate + type: array + volumes: + items: + type: number + title: Volumes + type: array + wells: + items: + items: + $ref: '#/$defs/ResourceDict' + type: array + title: Wells + type: array + required: + - plate + - wells + - volumes + title: SetLiquidFromPlateReturn + type: object required: - goal title: set_liquid_from_plate参数 @@ -764,7 +1199,9 @@ liquid_handler: goal: properties: tip_racks: - type: string + items: + type: object + type: array required: - tip_racks type: object @@ -789,7 +1226,9 @@ liquid_handler: goal: properties: targets: - type: string + items: + type: object + type: array required: - targets type: object @@ -837,8 +1276,7 @@ liquid_handler: goal: use_channels: use_channels goal_default: - use_channels: - - 0 + use_channels: [] handles: {} result: name: name @@ -846,31 +1284,25 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDiscardTips_Feedback type: object goal: + additionalProperties: false properties: use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - use_channels title: LiquidHandlerDiscardTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDiscardTips_Result type: object required: @@ -889,39 +1321,13 @@ liquid_handler: use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: - - 0 - flow_rates: - - 0.0 - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - resources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + blow_out_air_volume: [] + flow_rates: [] + offsets: [] + resources: [] spread: '' - use_channels: - - 0 - vols: - - 0.0 + use_channels: [] + vols: [] handles: {} result: name: name @@ -929,16 +1335,14 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDispense_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array flow_rates: @@ -958,7 +1362,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array resources: @@ -1033,40 +1436,27 @@ liquid_handler: - pose - config - data - title: resources type: object type: array spread: type: string use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: items: type: number type: array - required: - - resources - - vols - - use_channels - - flow_rates - - offsets - - blow_out_air_volume - - spread title: LiquidHandlerDispense_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDispense_Result type: object required: @@ -1083,32 +1473,9 @@ liquid_handler: use_channels: use_channels goal_default: allow_nonzero_volume: false - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - tip_spots: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + offsets: [] + tip_spots: [] + use_channels: [] handles: {} placeholder_keys: tip_spots: unilabos_resources @@ -1118,11 +1485,11 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDropTips_Feedback type: object goal: + additionalProperties: false properties: allow_nonzero_volume: type: boolean @@ -1139,7 +1506,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array tip_spots: @@ -1214,31 +1580,21 @@ liquid_handler: - pose - config - data - title: tip_spots type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - tip_spots - - use_channels - - offsets - - allow_nonzero_volume title: LiquidHandlerDropTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDropTips_Result type: object required: @@ -1285,21 +1641,28 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDropTips96_Feedback type: object goal: + additionalProperties: false properties: allow_nonzero_volume: type: boolean offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1308,6 +1671,7 @@ liquid_handler: title: offset type: object tip_rack: + additionalProperties: false properties: category: type: string @@ -1326,16 +1690,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1345,12 +1719,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1380,21 +1761,15 @@ liquid_handler: - data title: tip_rack type: object - required: - - tip_rack - - offset - - allow_nonzero_volume title: LiquidHandlerDropTips96_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDropTips96_Result type: object required: @@ -1417,47 +1792,31 @@ liquid_handler: mix_rate: 0.0 mix_time: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + targets: [] handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMix_Feedback type: object goal: + additionalProperties: false properties: height_to_bottom: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_time: maximum: 2147483647 @@ -1484,7 +1843,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array targets: @@ -1559,28 +1917,17 @@ liquid_handler: - pose - config - data - title: targets type: object type: array - required: - - targets - - mix_time - - mix_vol - - height_to_bottom - - offsets - - mix_rate - - none_keys title: LiquidHandlerMix_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMix_Result type: object required: @@ -1608,10 +1955,7 @@ liquid_handler: z: 0.0 drop_direction: '' get_direction: '' - intermediate_locations: - - x: 0.0 - y: 0.0 - z: 0.0 + intermediate_locations: [] lid: category: '' children: [] @@ -1666,19 +2010,26 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMoveLid_Feedback type: object goal: + additionalProperties: false properties: destination_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1703,10 +2054,10 @@ liquid_handler: - x - y - z - title: intermediate_locations type: object type: array lid: + additionalProperties: false properties: category: type: string @@ -1725,16 +2076,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1744,12 +2105,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1782,16 +2150,25 @@ liquid_handler: pickup_direction: type: string pickup_distance_from_top: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number put_direction: type: string resource_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1800,6 +2177,7 @@ liquid_handler: title: resource_offset type: object to: + additionalProperties: false properties: category: type: string @@ -1818,16 +2196,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1837,12 +2225,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1872,28 +2267,15 @@ liquid_handler: - data title: to type: object - required: - - lid - - to - - intermediate_locations - - resource_offset - - destination_offset - - pickup_direction - - drop_direction - - get_direction - - put_direction - - pickup_distance_from_top title: LiquidHandlerMoveLid_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMoveLid_Result type: object required: @@ -1921,10 +2303,7 @@ liquid_handler: z: 0.0 drop_direction: '' get_direction: '' - intermediate_locations: - - x: 0.0 - y: 0.0 - z: 0.0 + intermediate_locations: [] pickup_direction: '' pickup_distance_from_top: 0.0 pickup_offset: @@ -1983,19 +2362,26 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMovePlate_Feedback type: object goal: + additionalProperties: false properties: destination_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2020,20 +2406,28 @@ liquid_handler: - x - y - z - title: intermediate_locations type: object type: array pickup_direction: type: string pickup_distance_from_top: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number pickup_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2042,6 +2436,7 @@ liquid_handler: title: pickup_offset type: object plate: + additionalProperties: false properties: category: type: string @@ -2060,16 +2455,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2079,12 +2484,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2117,12 +2529,19 @@ liquid_handler: put_direction: type: string resource_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2131,6 +2550,7 @@ liquid_handler: title: resource_offset type: object to: + additionalProperties: false properties: category: type: string @@ -2149,16 +2569,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2168,12 +2598,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2203,29 +2640,15 @@ liquid_handler: - data title: to type: object - required: - - plate - - to - - intermediate_locations - - resource_offset - - pickup_offset - - destination_offset - - pickup_direction - - drop_direction - - get_direction - - put_direction - - pickup_distance_from_top title: LiquidHandlerMovePlate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMovePlate_Result type: object required: @@ -2253,10 +2676,7 @@ liquid_handler: z: 0.0 drop_direction: '' get_direction: '' - intermediate_locations: - - x: 0.0 - y: 0.0 - z: 0.0 + intermediate_locations: [] pickup_direction: '' pickup_distance_from_top: 0.0 put_direction: '' @@ -2295,19 +2715,26 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMoveResource_Feedback type: object goal: + additionalProperties: false properties: destination_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2332,16 +2759,18 @@ liquid_handler: - x - y - z - title: intermediate_locations type: object type: array pickup_direction: type: string pickup_distance_from_top: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number put_direction: type: string resource: + additionalProperties: false properties: category: type: string @@ -2360,16 +2789,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2379,12 +2818,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2415,12 +2861,19 @@ liquid_handler: title: resource type: object resource_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2429,12 +2882,19 @@ liquid_handler: title: resource_offset type: object to: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2442,28 +2902,15 @@ liquid_handler: - z title: to type: object - required: - - resource - - to - - intermediate_locations - - resource_offset - - destination_offset - - pickup_distance_from_top - - pickup_direction - - drop_direction - - get_direction - - put_direction title: LiquidHandlerMoveResource_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMoveResource_Result type: object required: @@ -2501,24 +2948,30 @@ liquid_handler: sample_id: '' type: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMoveTo_Feedback type: object goal: + additionalProperties: false properties: channel: maximum: 2147483647 minimum: -2147483648 type: integer dis_to_top: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number well: + additionalProperties: false properties: category: type: string @@ -2537,16 +2990,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2556,12 +3019,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2591,21 +3061,15 @@ liquid_handler: - data title: well type: object - required: - - well - - dis_to_top - - channel title: LiquidHandlerMoveTo_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMoveTo_Result type: object required: @@ -2620,32 +3084,9 @@ liquid_handler: tip_spots: tip_spots use_channels: use_channels goal_default: - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - tip_spots: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + offsets: [] + tip_spots: [] + use_channels: [] handles: {} result: name: name @@ -2653,11 +3094,11 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerPickUpTips_Feedback type: object goal: + additionalProperties: false properties: offsets: items: @@ -2672,7 +3113,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array tip_spots: @@ -2747,30 +3187,21 @@ liquid_handler: - pose - config - data - title: tip_spots type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - tip_spots - - use_channels - - offsets title: LiquidHandlerPickUpTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerPickUpTips_Result type: object required: @@ -2815,19 +3246,26 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerPickUpTips96_Feedback type: object goal: + additionalProperties: false properties: offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2836,6 +3274,7 @@ liquid_handler: title: offset type: object tip_rack: + additionalProperties: false properties: category: type: string @@ -2854,16 +3293,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2873,12 +3322,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2908,20 +3364,15 @@ liquid_handler: - data title: tip_rack type: object - required: - - tip_rack - - offset title: LiquidHandlerPickUpTips96_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerPickUpTips96_Result type: object required: @@ -2946,48 +3397,18 @@ liquid_handler: vols: vols waste_liquid: waste_liquid goal_default: - blow_out_air_volume: - - 0.0 - delays: - - 0 - flow_rates: - - 0.0 + blow_out_air_volume: [] + delays: [] + flow_rates: [] is_96_well: false - liquid_height: - - 0.0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + liquid_height: [] + none_keys: [] + offsets: [] + sources: [] spread: '' - top: - - 0.0 - use_channels: - - 0 - vols: - - 0.0 + top: [] + use_channels: [] + vols: [] waste_liquid: category: '' children: [] @@ -3014,11 +3435,11 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerRemove_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: @@ -3026,8 +3447,6 @@ liquid_handler: type: array delays: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array flow_rates: @@ -3057,7 +3476,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array sources: @@ -3132,7 +3550,6 @@ liquid_handler: - pose - config - data - title: sources type: object type: array spread: @@ -3143,8 +3560,6 @@ liquid_handler: type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: @@ -3152,6 +3567,7 @@ liquid_handler: type: number type: array waste_liquid: + additionalProperties: false properties: category: type: string @@ -3170,16 +3586,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3189,12 +3615,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3224,31 +3657,15 @@ liquid_handler: - data title: waste_liquid type: object - required: - - vols - - sources - - waste_liquid - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread - - delays - - is_96_well - - top - - none_keys title: LiquidHandlerRemove_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerRemove_Result type: object required: @@ -3273,48 +3690,18 @@ liquid_handler: vols: vols waste_liquid: waste_liquid goal_default: - blow_out_air_volume: - - 0.0 - delays: - - 0 - flow_rates: - - 0.0 + blow_out_air_volume: [] + delays: [] + flow_rates: [] is_96_well: false - liquid_height: - - 0.0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + liquid_height: [] + none_keys: [] + offsets: [] + sources: [] spread: '' - top: - - 0.0 - use_channels: - - 0 - vols: - - 0.0 + top: [] + use_channels: [] + vols: [] waste_liquid: category: '' children: [] @@ -3339,16 +3726,18 @@ liquid_handler: placeholder_keys: sources: unilabos_resources waste_liquid: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerRemove_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: @@ -3356,8 +3745,6 @@ liquid_handler: type: array delays: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array flow_rates: @@ -3387,7 +3774,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array sources: @@ -3462,7 +3848,6 @@ liquid_handler: - pose - config - data - title: sources type: object type: array spread: @@ -3473,8 +3858,6 @@ liquid_handler: type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: @@ -3482,6 +3865,7 @@ liquid_handler: type: number type: array waste_liquid: + additionalProperties: false properties: category: type: string @@ -3500,16 +3884,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3519,12 +3913,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3554,31 +3955,15 @@ liquid_handler: - data title: waste_liquid type: object - required: - - vols - - sources - - waste_liquid - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread - - delays - - is_96_well - - top - - none_keys title: LiquidHandlerRemove_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerRemove_Result type: object required: @@ -3593,8 +3978,7 @@ liquid_handler: use_channels: use_channels goal_default: allow_nonzero_volume: false - use_channels: - - 0 + use_channels: [] handles: {} result: name: name @@ -3602,34 +3986,27 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerReturnTips_Feedback type: object goal: + additionalProperties: false properties: allow_nonzero_volume: type: boolean use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - use_channels - - allow_nonzero_volume title: LiquidHandlerReturnTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerReturnTips_Result type: object required: @@ -3650,27 +4027,23 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerReturnTips96_Feedback type: object goal: + additionalProperties: false properties: allow_nonzero_volume: type: boolean - required: - - allow_nonzero_volume title: LiquidHandlerReturnTips96_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerReturnTips96_Result type: object required: @@ -3737,17 +4110,22 @@ liquid_handler: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerStamp_Feedback type: object goal: + additionalProperties: false properties: aspiration_flow_rate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number dispense_flow_rate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number source: + additionalProperties: false properties: category: type: string @@ -3766,16 +4144,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3785,12 +4173,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3821,6 +4216,7 @@ liquid_handler: title: source type: object target: + additionalProperties: false properties: category: type: string @@ -3839,16 +4235,26 @@ liquid_handler: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3858,12 +4264,19 @@ liquid_handler: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3894,24 +4307,18 @@ liquid_handler: title: target type: object volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - source - - target - - volume - - aspiration_flow_rate - - dispense_flow_rate title: LiquidHandlerStamp_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerStamp_Result type: object required: @@ -3944,20 +4351,22 @@ liquid_handler: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number transferred_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - transferred_volume - - current_status title: Transfer_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -3970,31 +4379,27 @@ liquid_handler: rinsing_solvent: type: string rinsing_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solid: type: boolean time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number to_vessel: type: string viscous: type: boolean volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - from_vessel - - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid title: Transfer_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -4002,10 +4407,6 @@ liquid_handler: type: string success: type: boolean - required: - - success - - message - - return_info title: Transfer_Result type: object required: @@ -4038,96 +4439,27 @@ liquid_handler: touch_tip: touch_tip use_channels: use_channels goal_default: - asp_flow_rates: - - 0.0 - asp_vols: - - 0.0 - blow_out_air_volume: - - 0.0 - delays: - - 0 - dis_flow_rates: - - 0.0 - dis_vols: - - 0.0 + asp_flow_rates: [] + asp_vols: [] + blow_out_air_volume: [] + delays: [] + dis_flow_rates: [] + dis_vols: [] is_96_well: false - liquid_height: - - 0.0 + liquid_height: [] mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' mix_times: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + sources: [] spread: '' - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - tip_racks: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + targets: [] + tip_racks: [] touch_tip: false - use_channels: - - 0 + use_channels: [] handles: input: - data_key: sources @@ -4160,16 +4492,18 @@ liquid_handler: sources: unilabos_resources targets: unilabos_resources tip_racks: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerTransfer_Feedback type: object goal: + additionalProperties: false properties: asp_flow_rates: items: @@ -4185,8 +4519,6 @@ liquid_handler: type: array delays: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array dis_flow_rates: @@ -4204,6 +4536,8 @@ liquid_handler: type: number type: array mix_liquid_height: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -4236,7 +4570,6 @@ liquid_handler: - x - y - z - title: offsets type: object type: array sources: @@ -4311,7 +4644,6 @@ liquid_handler: - pose - config - data - title: sources type: object type: array spread: @@ -4388,7 +4720,6 @@ liquid_handler: - pose - config - data - title: targets type: object type: array tip_racks: @@ -4463,50 +4794,23 @@ liquid_handler: - pose - config - data - title: tip_racks type: object type: array touch_tip: type: boolean use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - asp_vols - - dis_vols - - sources - - targets - - tip_racks - - use_channels - - asp_flow_rates - - dis_flow_rates - - offsets - - touch_tip - - liquid_height - - blow_out_air_volume - - spread - - is_96_well - - mix_stage - - mix_times - - mix_vol - - mix_rate - - mix_liquid_height - - delays - - none_keys title: LiquidHandlerTransfer_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerTransfer_Result type: object required: @@ -4525,12 +4829,12 @@ liquid_handler: config: properties: backend: - type: string + type: object channel_num: default: 8 type: integer deck: - type: string + type: object simulator: default: false type: boolean @@ -4573,6 +4877,8 @@ liquid_handler.biomek: goal: properties: bind_location: + additionalProperties: + type: number type: object bind_parent_id: type: string @@ -4612,6 +4918,36 @@ liquid_handler.biomek: title: create_resource参数 type: object type: UniLabJsonCommand + auto-deserialize: + feedback: {} + goal: {} + goal_default: + allow_marshal: false + data: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: deserialize的参数schema + properties: + feedback: {} + goal: + properties: + allow_marshal: + default: false + type: boolean + data: + type: object + required: + - data + type: object + result: + type: object + required: + - goal + title: deserialize参数 + type: object + type: UniLabJsonCommand auto-instrument_setup_biomek: feedback: {} goal: {} @@ -4678,8 +5014,7 @@ liquid_handler.biomek: protocol_type: protocol_type protocol_version: protocol_version goal_default: - none_keys: - - '' + none_keys: [] protocol_author: '' protocol_date: '' protocol_description: '' @@ -4687,16 +5022,18 @@ liquid_handler.biomek: protocol_type: '' protocol_version: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerProtocolCreation_Feedback type: object goal: + additionalProperties: false properties: none_keys: items: @@ -4714,22 +5051,13 @@ liquid_handler.biomek: type: string protocol_version: type: string - required: - - protocol_name - - protocol_description - - protocol_version - - protocol_author - - protocol_date - - protocol_type - - none_keys title: LiquidHandlerProtocolCreation_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: LiquidHandlerProtocolCreation_Result type: object required: @@ -4756,34 +5084,33 @@ liquid_handler.biomek: data_type: resource handler_key: plate_out label: plate - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerIncubateBiomek_Feedback type: object goal: + additionalProperties: false properties: time: maximum: 2147483647 minimum: -2147483648 type: integer - required: - - time title: LiquidHandlerIncubateBiomek_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerIncubateBiomek_Result type: object required: @@ -4794,8 +5121,10 @@ liquid_handler.biomek: move_biomek: feedback: {} goal: - source: sources - target: targets + source: source + sources: sources + target: target + targets: targets goal_default: sources: '' targets: '' @@ -4812,36 +5141,33 @@ liquid_handler.biomek: data_type: resource handler_key: targets label: targets + placeholder_keys: {} result: - name: name + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMoveBiomek_Feedback type: object goal: + additionalProperties: false properties: sources: type: string targets: type: string - required: - - sources - - targets title: LiquidHandlerMoveBiomek_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMoveBiomek_Result type: object required: @@ -4870,16 +5196,19 @@ liquid_handler.biomek: data_type: resource handler_key: plate_out label: plate - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerOscillateBiomek_Feedback type: object goal: + additionalProperties: false properties: rpm: maximum: 2147483647 @@ -4889,20 +5218,15 @@ liquid_handler.biomek: maximum: 2147483647 minimum: -2147483648 type: integer - required: - - rpm - - time title: LiquidHandlerOscillateBiomek_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerOscillateBiomek_Result type: object required: @@ -4915,26 +5239,25 @@ liquid_handler.biomek: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -4945,9 +5268,13 @@ liquid_handler.biomek: transfer_biomek: feedback: {} goal: + aspirate_technique: aspirate_technique aspirate_techniques: aspirate_techniques + dispense_technique: dispense_technique dispense_techniques: dispense_techniques + source: source sources: sources + target: target targets: targets tip_rack: tip_rack volume: volume @@ -4986,16 +5313,19 @@ liquid_handler.biomek: data_type: resource handler_key: targets_out label: targets - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerTransferBiomek_Feedback type: object goal: + additionalProperties: false properties: aspirate_technique: type: string @@ -5008,25 +5338,18 @@ liquid_handler.biomek: tip_rack: type: string volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - sources - - targets - - tip_rack - - volume - - aspirate_technique - - dispense_technique title: LiquidHandlerTransferBiomek_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerTransferBiomek_Result type: object required: @@ -5059,96 +5382,27 @@ liquid_handler.biomek: touch_tip: touch_tip use_channels: use_channels goal_default: - asp_flow_rates: - - 0.0 - asp_vols: - - 0.0 - blow_out_air_volume: - - 0.0 - delays: - - 0 - dis_flow_rates: - - 0.0 - dis_vols: - - 0.0 + asp_flow_rates: [] + asp_vols: [] + blow_out_air_volume: [] + delays: [] + dis_flow_rates: [] + dis_vols: [] is_96_well: false - liquid_height: - - 0.0 + liquid_height: [] mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' mix_times: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + sources: [] spread: '' - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - tip_racks: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + targets: [] + tip_racks: [] touch_tip: false - use_channels: - - 0 + use_channels: [] handles: input: - data_key: sources @@ -5183,16 +5437,18 @@ liquid_handler.biomek: sources: unilabos_resources targets: unilabos_resources tip_racks: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerTransfer_Feedback type: object goal: + additionalProperties: false properties: asp_flow_rates: items: @@ -5208,8 +5464,6 @@ liquid_handler.biomek: type: array delays: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array dis_flow_rates: @@ -5227,6 +5481,8 @@ liquid_handler.biomek: type: number type: array mix_liquid_height: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -5259,7 +5515,6 @@ liquid_handler.biomek: - x - y - z - title: offsets type: object type: array sources: @@ -5334,7 +5589,6 @@ liquid_handler.biomek: - pose - config - data - title: sources type: object type: array spread: @@ -5411,7 +5665,6 @@ liquid_handler.biomek: - pose - config - data - title: targets type: object type: array tip_racks: @@ -5486,50 +5739,23 @@ liquid_handler.biomek: - pose - config - data - title: tip_racks type: object type: array touch_tip: type: boolean use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - asp_vols - - dis_vols - - sources - - targets - - tip_racks - - use_channels - - asp_flow_rates - - dis_flow_rates - - offsets - - touch_tip - - liquid_height - - blow_out_air_volume - - spread - - is_96_well - - mix_stage - - mix_times - - mix_vol - - mix_rate - - mix_liquid_height - - delays - - none_keys title: LiquidHandlerTransfer_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerTransfer_Result type: object required: @@ -5539,7 +5765,7 @@ liquid_handler.biomek: type: LiquidHandlerTransfer module: unilabos.devices.liquid_handling.biomek:LiquidHandlerBiomek status_types: - success: String + success: '' type: python config_info: [] description: Biomek液体处理器设备,基于pylabrobot控制 @@ -5568,6 +5794,7 @@ liquid_handler.laiyu: goal: asp_vols: asp_vols blow_out_air_volume: blow_out_air_volume + delays: delays dis_vols: dis_vols flow_rates: flow_rates is_96_well: is_96_well @@ -5583,84 +5810,38 @@ liquid_handler.laiyu: targets: targets use_channels: use_channels goal_default: - asp_vols: - - 0.0 - blow_out_air_volume: - - 0.0 - dis_vols: - - 0.0 - flow_rates: - - 0.0 + asp_vols: [] + blow_out_air_volume: [] + dis_vols: [] + flow_rates: [] is_96_well: false - liquid_height: - - 0.0 + liquid_height: [] mix_liquid_height: 0.0 mix_rate: 0 mix_time: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - reagent_sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + reagent_sources: [] spread: '' - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + targets: [] + use_channels: [] handles: {} placeholder_keys: reagent_sources: unilabos_resources targets: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerAdd_Feedback type: object goal: + additionalProperties: false properties: asp_vols: items: @@ -5685,6 +5866,8 @@ liquid_handler.laiyu: type: number type: array mix_liquid_height: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -5715,7 +5898,6 @@ liquid_handler.laiyu: - x - y - z - title: offsets type: object type: array reagent_sources: @@ -5790,7 +5972,6 @@ liquid_handler.laiyu: - pose - config - data - title: reagent_sources type: object type: array spread: @@ -5867,43 +6048,21 @@ liquid_handler.laiyu: - pose - config - data - title: targets type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - asp_vols - - dis_vols - - reagent_sources - - targets - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread - - is_96_well - - mix_time - - mix_vol - - mix_rate - - mix_liquid_height - - none_keys title: LiquidHandlerAdd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerAdd_Result type: object required: @@ -5919,57 +6078,33 @@ liquid_handler.laiyu: liquid_height: liquid_height offsets: offsets resources: resources + spread: spread use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: - - 0.0 - flow_rates: - - 0.0 - liquid_height: - - 0.0 - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - resources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + blow_out_air_volume: [] + flow_rates: [] + liquid_height: [] + offsets: [] + resources: [] spread: '' - use_channels: - - 0 - vols: - - 0.0 + use_channels: [] + vols: [] handles: {} placeholder_keys: resources: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerAspirate_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: @@ -5996,7 +6131,6 @@ liquid_handler.laiyu: - x - y - z - title: offsets type: object type: array resources: @@ -6071,41 +6205,27 @@ liquid_handler.laiyu: - pose - config - data - title: resources type: object type: array spread: type: string use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: items: type: number type: array - required: - - resources - - vols - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread title: LiquidHandlerAspirate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerAspirate_Result type: object required: @@ -6148,54 +6268,93 @@ liquid_handler.laiyu: goal: properties: asp_flow_rates: - type: string + items: + type: number + type: array asp_vols: - type: string + anyOf: + - items: + type: number + type: array + - type: number blow_out_air_volume: - type: string + items: + type: number + type: array delays: - type: string + items: + type: integer + type: array dis_flow_rates: - type: string + items: + type: number + type: array dis_vols: - type: string + anyOf: + - items: + type: number + type: array + - type: number is_96_well: default: false type: boolean liquid_height: - type: string + items: + type: number + type: array mix_liquid_height: - type: string + type: number mix_rate: - type: string + type: integer mix_stage: default: none + enum: + - none + - before + - after + - both type: string mix_times: - type: string + items: + type: integer + type: array mix_vol: - type: string + type: integer none_keys: default: [] items: type: string type: array offsets: - type: string + items: + type: object + type: array sources: - type: string + items: + type: object + type: array spread: default: wide + enum: + - wide + - tight + - custom type: string targets: - type: string + items: + type: object + type: array tip_racks: - type: string + items: + type: object + type: array touch_tip: default: false type: boolean use_channels: - type: string + items: + type: integer + type: array required: - sources - targets @@ -6217,60 +6376,35 @@ liquid_handler.laiyu: liquid_height: liquid_height offsets: offsets resources: resources + spread: spread use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: - - 0 - flow_rates: - - 0.0 - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - resources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + blow_out_air_volume: [] + flow_rates: [] + offsets: [] + resources: [] spread: '' - use_channels: - - 0 - vols: - - 0.0 + use_channels: [] + vols: [] handles: {} placeholder_keys: resources: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDispense_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array flow_rates: @@ -6290,7 +6424,6 @@ liquid_handler.laiyu: - x - y - z - title: offsets type: object type: array resources: @@ -6365,40 +6498,27 @@ liquid_handler.laiyu: - pose - config - data - title: resources type: object type: array spread: type: string use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: items: type: number type: array - required: - - resources - - vols - - use_channels - - flow_rates - - offsets - - blow_out_air_volume - - spread title: LiquidHandlerDispense_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDispense_Result type: object required: @@ -6415,45 +6535,24 @@ liquid_handler.laiyu: use_channels: use_channels goal_default: allow_nonzero_volume: false - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - tip_spots: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + offsets: [] + tip_spots: [] + use_channels: [] handles: {} placeholder_keys: tip_spots: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDropTips_Feedback type: object goal: + additionalProperties: false properties: allow_nonzero_volume: type: boolean @@ -6470,7 +6569,6 @@ liquid_handler.laiyu: - x - y - z - title: offsets type: object type: array tip_spots: @@ -6545,31 +6643,21 @@ liquid_handler.laiyu: - pose - config - data - title: tip_spots type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - tip_spots - - use_channels - - offsets - - allow_nonzero_volume title: LiquidHandlerDropTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDropTips_Result type: object required: @@ -6592,49 +6680,32 @@ liquid_handler.laiyu: mix_rate: 0.0 mix_time: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + targets: [] handles: {} placeholder_keys: targets: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMix_Feedback type: object goal: + additionalProperties: false properties: height_to_bottom: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_time: maximum: 2147483647 @@ -6661,7 +6732,6 @@ liquid_handler.laiyu: - x - y - z - title: offsets type: object type: array targets: @@ -6736,28 +6806,17 @@ liquid_handler.laiyu: - pose - config - data - title: targets type: object type: array - required: - - targets - - mix_time - - mix_vol - - height_to_bottom - - offsets - - mix_rate - - none_keys title: LiquidHandlerMix_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMix_Result type: object required: @@ -6772,45 +6831,24 @@ liquid_handler.laiyu: tip_spots: tip_spots use_channels: use_channels goal_default: - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - tip_spots: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + offsets: [] + tip_spots: [] + use_channels: [] handles: {} placeholder_keys: tip_spots: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerPickUpTips_Feedback type: object goal: + additionalProperties: false properties: offsets: items: @@ -6825,7 +6863,6 @@ liquid_handler.laiyu: - x - y - z - title: offsets type: object type: array tip_spots: @@ -6900,30 +6937,21 @@ liquid_handler.laiyu: - pose - config - data - title: tip_spots type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - tip_spots - - use_channels - - offsets title: LiquidHandlerPickUpTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerPickUpTips_Result type: object required: @@ -6978,6 +7006,7 @@ liquid_handler.prcxi: goal: asp_vols: asp_vols blow_out_air_volume: blow_out_air_volume + delays: delays dis_vols: dis_vols flow_rates: flow_rates is_96_well: is_96_well @@ -6993,84 +7022,38 @@ liquid_handler.prcxi: targets: targets use_channels: use_channels goal_default: - asp_vols: - - 0.0 - blow_out_air_volume: - - 0.0 - dis_vols: - - 0.0 - flow_rates: - - 0.0 + asp_vols: [] + blow_out_air_volume: [] + dis_vols: [] + flow_rates: [] is_96_well: false - liquid_height: - - 0.0 + liquid_height: [] mix_liquid_height: 0.0 mix_rate: 0 mix_time: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - reagent_sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + reagent_sources: [] spread: '' - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + targets: [] + use_channels: [] handles: {} placeholder_keys: reagent_sources: unilabos_resources targets: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerAdd_Feedback type: object goal: + additionalProperties: false properties: asp_vols: items: @@ -7095,6 +7078,8 @@ liquid_handler.prcxi: type: number type: array mix_liquid_height: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -7125,7 +7110,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array reagent_sources: @@ -7200,7 +7184,6 @@ liquid_handler.prcxi: - pose - config - data - title: reagent_sources type: object type: array spread: @@ -7277,43 +7260,21 @@ liquid_handler.prcxi: - pose - config - data - title: targets type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - asp_vols - - dis_vols - - reagent_sources - - targets - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread - - is_96_well - - mix_time - - mix_vol - - mix_rate - - mix_liquid_height - - none_keys title: LiquidHandlerAdd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerAdd_Result type: object required: @@ -7329,57 +7290,33 @@ liquid_handler.prcxi: liquid_height: liquid_height offsets: offsets resources: resources + spread: spread use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: - - 0.0 - flow_rates: - - 0.0 - liquid_height: - - 0.0 - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - resources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + blow_out_air_volume: [] + flow_rates: [] + liquid_height: [] + offsets: [] + resources: [] spread: '' - use_channels: - - 0 - vols: - - 0.0 + use_channels: [] + vols: [] handles: {} placeholder_keys: resources: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerAspirate_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: @@ -7406,7 +7343,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array resources: @@ -7481,41 +7417,27 @@ liquid_handler.prcxi: - pose - config - data - title: resources type: object type: array spread: type: string use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: items: type: number type: array - required: - - resources - - vols - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread title: LiquidHandlerAspirate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerAspirate_Result type: object required: @@ -7646,11 +7568,14 @@ liquid_handler.prcxi: goal: properties: tip_racks: - type: string + items: + type: object + type: array required: - tip_racks type: object - result: {} + result: + type: string required: - goal title: iter_tips参数 @@ -7689,31 +7614,6 @@ liquid_handler.prcxi: title: move_to参数 type: object type: UniLabJsonCommandAsync - 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-run_protocol: feedback: {} goal: {} @@ -7824,7 +7724,9 @@ liquid_handler.prcxi: goal: properties: targets: - type: string + items: + type: object + type: array required: - targets type: object @@ -7870,41 +7772,39 @@ liquid_handler.prcxi: discard_tips: feedback: {} goal: + allow_nonzero_volume: allow_nonzero_volume + offsets: offsets use_channels: use_channels goal_default: - use_channels: - - 0 + use_channels: [] handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDiscardTips_Feedback type: object goal: + additionalProperties: false properties: use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - use_channels title: LiquidHandlerDiscardTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDiscardTips_Result type: object required: @@ -7917,63 +7817,38 @@ liquid_handler.prcxi: goal: blow_out_air_volume: blow_out_air_volume flow_rates: flow_rates + liquid_height: liquid_height offsets: offsets resources: resources spread: spread use_channels: use_channels vols: vols goal_default: - blow_out_air_volume: - - 0 - flow_rates: - - 0.0 - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - resources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + blow_out_air_volume: [] + flow_rates: [] + offsets: [] + resources: [] spread: '' - use_channels: - - 0 - vols: - - 0.0 + use_channels: [] + vols: [] handles: {} placeholder_keys: resources: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDispense_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array flow_rates: @@ -7993,7 +7868,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array resources: @@ -8068,40 +7942,27 @@ liquid_handler.prcxi: - pose - config - data - title: resources type: object type: array spread: type: string use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: items: type: number type: array - required: - - resources - - vols - - use_channels - - flow_rates - - offsets - - blow_out_air_volume - - spread title: LiquidHandlerDispense_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDispense_Result type: object required: @@ -8118,45 +7979,24 @@ liquid_handler.prcxi: use_channels: use_channels goal_default: allow_nonzero_volume: false - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - tip_spots: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + offsets: [] + tip_spots: [] + use_channels: [] handles: {} placeholder_keys: tip_spots: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerDropTips_Feedback type: object goal: + additionalProperties: false properties: allow_nonzero_volume: type: boolean @@ -8173,7 +8013,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array tip_spots: @@ -8248,31 +8087,21 @@ liquid_handler.prcxi: - pose - config - data - title: tip_spots type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - tip_spots - - use_channels - - offsets - - allow_nonzero_volume title: LiquidHandlerDropTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerDropTips_Result type: object required: @@ -8295,49 +8124,32 @@ liquid_handler.prcxi: mix_rate: 0.0 mix_time: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + targets: [] handles: {} placeholder_keys: targets: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMix_Feedback type: object goal: + additionalProperties: false properties: height_to_bottom: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_time: maximum: 2147483647 @@ -8364,7 +8176,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array targets: @@ -8439,28 +8250,17 @@ liquid_handler.prcxi: - pose - config - data - title: targets type: object type: array - required: - - targets - - mix_time - - mix_vol - - height_to_bottom - - offsets - - mix_rate - - none_keys title: LiquidHandlerMix_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMix_Result type: object required: @@ -8476,6 +8276,7 @@ liquid_handler.prcxi: get_direction: get_direction intermediate_locations: intermediate_locations pickup_direction: pickup_direction + pickup_distance_from_top: pickup_distance_from_top pickup_offset: pickup_offset plate: plate put_direction: put_direction @@ -8488,10 +8289,7 @@ liquid_handler.prcxi: z: 0.0 drop_direction: '' get_direction: '' - intermediate_locations: - - x: 0.0 - y: 0.0 - z: 0.0 + intermediate_locations: [] pickup_direction: '' pickup_distance_from_top: 0.0 pickup_offset: @@ -8548,24 +8346,32 @@ liquid_handler.prcxi: plate: unilabos_resources to: unilabos_resources result: - name: name + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerMovePlate_Feedback type: object goal: + additionalProperties: false properties: destination_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8590,20 +8396,28 @@ liquid_handler.prcxi: - x - y - z - title: intermediate_locations type: object type: array pickup_direction: type: string pickup_distance_from_top: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number pickup_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8612,6 +8426,7 @@ liquid_handler.prcxi: title: pickup_offset type: object plate: + additionalProperties: false properties: category: type: string @@ -8630,16 +8445,26 @@ liquid_handler.prcxi: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8649,12 +8474,19 @@ liquid_handler.prcxi: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8687,12 +8519,19 @@ liquid_handler.prcxi: put_direction: type: string resource_offset: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8701,6 +8540,7 @@ liquid_handler.prcxi: title: resource_offset type: object to: + additionalProperties: false properties: category: type: string @@ -8719,16 +8559,26 @@ liquid_handler.prcxi: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8738,12 +8588,19 @@ liquid_handler.prcxi: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -8773,29 +8630,15 @@ liquid_handler.prcxi: - data title: to type: object - required: - - plate - - to - - intermediate_locations - - resource_offset - - pickup_offset - - destination_offset - - pickup_direction - - drop_direction - - get_direction - - put_direction - - pickup_distance_from_top title: LiquidHandlerMovePlate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerMovePlate_Result type: object required: @@ -8810,45 +8653,24 @@ liquid_handler.prcxi: tip_spots: tip_spots use_channels: use_channels goal_default: - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - tip_spots: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - use_channels: - - 0 + offsets: [] + tip_spots: [] + use_channels: [] handles: {} placeholder_keys: tip_spots: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerPickUpTips_Feedback type: object goal: + additionalProperties: false properties: offsets: items: @@ -8863,7 +8685,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array tip_spots: @@ -8938,30 +8759,21 @@ liquid_handler.prcxi: - pose - config - data - title: tip_spots type: object type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - tip_spots - - use_channels - - offsets title: LiquidHandlerPickUpTips_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerPickUpTips_Result type: object required: @@ -8986,48 +8798,18 @@ liquid_handler.prcxi: vols: vols waste_liquid: waste_liquid goal_default: - blow_out_air_volume: - - 0.0 - delays: - - 0 - flow_rates: - - 0.0 + blow_out_air_volume: [] + delays: [] + flow_rates: [] is_96_well: false - liquid_height: - - 0.0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + liquid_height: [] + none_keys: [] + offsets: [] + sources: [] spread: '' - top: - - 0.0 - use_channels: - - 0 - vols: - - 0.0 + top: [] + use_channels: [] + vols: [] waste_liquid: category: '' children: [] @@ -9052,16 +8834,18 @@ liquid_handler.prcxi: placeholder_keys: sources: unilabos_resources waste_liquid: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerRemove_Feedback type: object goal: + additionalProperties: false properties: blow_out_air_volume: items: @@ -9069,8 +8853,6 @@ liquid_handler.prcxi: type: array delays: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array flow_rates: @@ -9100,7 +8882,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array sources: @@ -9175,7 +8956,6 @@ liquid_handler.prcxi: - pose - config - data - title: sources type: object type: array spread: @@ -9186,8 +8966,6 @@ liquid_handler.prcxi: type: array use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array vols: @@ -9195,6 +8973,7 @@ liquid_handler.prcxi: type: number type: array waste_liquid: + additionalProperties: false properties: category: type: string @@ -9213,16 +8992,26 @@ liquid_handler.prcxi: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -9232,12 +9021,19 @@ liquid_handler.prcxi: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -9267,31 +9063,15 @@ liquid_handler.prcxi: - data title: waste_liquid type: object - required: - - vols - - sources - - waste_liquid - - use_channels - - flow_rates - - offsets - - liquid_height - - blow_out_air_volume - - spread - - delays - - is_96_well - - top - - none_keys title: LiquidHandlerRemove_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerRemove_Result type: object required: @@ -9306,30 +9086,9 @@ liquid_handler.prcxi: volumes: volumes wells: wells goal_default: - liquid_names: - - '' - volumes: - - 0.0 - wells: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + liquid_names: [] + volumes: [] + wells: [] handles: input: - data_key: wells @@ -9345,16 +9104,17 @@ liquid_handler.prcxi: label: 已设定液体孔 placeholder_keys: wells: unilabos_resources - result: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerSetLiquid_Feedback type: object goal: + additionalProperties: false properties: liquid_names: items: @@ -9436,21 +9196,15 @@ liquid_handler.prcxi: - pose - config - data - title: wells type: object type: array - required: - - wells - - liquid_names - - volumes title: LiquidHandlerSetLiquid_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: LiquidHandlerSetLiquid_Result type: object required: @@ -9503,6 +9257,7 @@ liquid_handler.prcxi: type: string type: array plate: + additionalProperties: false properties: category: type: string @@ -9521,16 +9276,26 @@ liquid_handler.prcxi: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -9540,12 +9305,19 @@ liquid_handler.prcxi: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -9562,17 +9334,6 @@ liquid_handler.prcxi: type: string type: type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data title: plate type: object volumes: @@ -9626,6 +9387,11 @@ liquid_handler.prcxi: description: Resource ID title: Id type: string + machine_name: + default: '' + description: Machine this resource belongs to + title: Machine Name + type: string model: additionalProperties: true description: Resource model @@ -9689,6 +9455,14 @@ liquid_handler.prcxi: - rounded_rectangle title: Cross Section Type type: string + extra: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + default: null + description: Extra data + title: Extra layout: default: x-y description: Resource layout @@ -9809,39 +9583,22 @@ liquid_handler.prcxi: goal: tip_racks: tip_racks goal_default: - tip_racks: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + tip_racks: [] handles: {} placeholder_keys: tip_racks: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerSetTipRack_Feedback type: object goal: + additionalProperties: false properties: tip_racks: items: @@ -9915,22 +9672,17 @@ liquid_handler.prcxi: - pose - config - data - title: tip_racks type: object type: array - required: - - tip_racks title: LiquidHandlerSetTipRack_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerSetTipRack_Result type: object required: @@ -9963,20 +9715,22 @@ liquid_handler.prcxi: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number transferred_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - transferred_volume - - current_status title: Transfer_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -9989,31 +9743,27 @@ liquid_handler.prcxi: rinsing_solvent: type: string rinsing_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solid: type: boolean time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number to_vessel: type: string viscous: type: boolean volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - from_vessel - - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid title: Transfer_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -10021,10 +9771,6 @@ liquid_handler.prcxi: type: string success: type: boolean - required: - - success - - message - - return_info title: Transfer_Result type: object required: @@ -10057,96 +9803,27 @@ liquid_handler.prcxi: touch_tip: touch_tip use_channels: use_channels goal_default: - asp_flow_rates: - - 0.0 - asp_vols: - - 0.0 - blow_out_air_volume: - - 0.0 - delays: - - 0 - dis_flow_rates: - - 0.0 - dis_vols: - - 0.0 + asp_flow_rates: [] + asp_vols: [] + blow_out_air_volume: [] + delays: [] + dis_flow_rates: [] + dis_vols: [] is_96_well: false - liquid_height: - - 0.0 + liquid_height: [] mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' mix_times: 0 mix_vol: 0 - none_keys: - - '' - offsets: - - x: 0.0 - y: 0.0 - z: 0.0 - sources: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + none_keys: [] + offsets: [] + sources: [] spread: '' - targets: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - tip_racks: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' + targets: [] + tip_racks: [] touch_tip: false - use_channels: - - 0 + use_channels: [] handles: input: - data_key: sources @@ -10179,16 +9856,18 @@ liquid_handler.prcxi: sources: unilabos_resources targets: unilabos_resources tip_racks: unilabos_resources - result: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: LiquidHandlerTransfer_Feedback type: object goal: + additionalProperties: false properties: asp_flow_rates: items: @@ -10204,8 +9883,6 @@ liquid_handler.prcxi: type: array delays: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array dis_flow_rates: @@ -10223,6 +9900,8 @@ liquid_handler.prcxi: type: number type: array mix_liquid_height: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number mix_rate: maximum: 2147483647 @@ -10255,7 +9934,6 @@ liquid_handler.prcxi: - x - y - z - title: offsets type: object type: array sources: @@ -10330,7 +10008,6 @@ liquid_handler.prcxi: - pose - config - data - title: sources type: object type: array spread: @@ -10407,7 +10084,6 @@ liquid_handler.prcxi: - pose - config - data - title: targets type: object type: array tip_racks: @@ -10482,50 +10158,23 @@ liquid_handler.prcxi: - pose - config - data - title: tip_racks type: object type: array touch_tip: type: boolean use_channels: items: - maximum: 2147483647 - minimum: -2147483648 type: integer type: array - required: - - asp_vols - - dis_vols - - sources - - targets - - tip_racks - - use_channels - - asp_flow_rates - - dis_flow_rates - - offsets - - touch_tip - - liquid_height - - blow_out_air_volume - - spread - - is_96_well - - mix_stage - - mix_times - - mix_vol - - mix_rate - - mix_liquid_height - - delays - - none_keys title: LiquidHandlerTransfer_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: LiquidHandlerTransfer_Result type: object required: @@ -10597,11 +10246,13 @@ liquid_handler.revvity: action_value_mappings: run: feedback: + gantt: gantt status: status goal: + file_path: file_path params: params resource: resource - wf_name: file_path + wf_name: wf_name goal_default: params: '' resource: @@ -10626,27 +10277,29 @@ liquid_handler.revvity: type: '' wf_name: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: gantt: type: string status: type: string - required: - - status - - gantt title: WorkStationRun_Feedback type: object goal: + additionalProperties: false properties: params: type: string resource: + additionalProperties: false properties: category: type: string @@ -10665,16 +10318,26 @@ liquid_handler.revvity: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -10684,12 +10347,19 @@ liquid_handler.revvity: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -10721,21 +10391,15 @@ liquid_handler.revvity: type: object wf_name: type: string - required: - - wf_name - - params - - resource title: WorkStationRun_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: WorkStationRun_Result type: object required: @@ -10764,7 +10428,7 @@ liquid_handler.revvity: success: type: boolean required: - - success - status + - success type: object version: 1.0.0 diff --git a/unilabos/registry/devices/neware_battery_test_system.yaml b/unilabos/registry/devices/neware_battery_test_system.yaml index ea6bedc4..4f3b972a 100644 --- a/unilabos/registry/devices/neware_battery_test_system.yaml +++ b/unilabos/registry/devices/neware_battery_test_system.yaml @@ -5,31 +5,6 @@ neware_battery_test_system: - battery_test class: 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: feedback: {} goal: {} @@ -66,7 +41,8 @@ neware_battery_test_system: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: test_connection参数 @@ -77,9 +53,8 @@ neware_battery_test_system: goal: {} goal_default: {} handles: {} - result: - return_info: return_info - success: success + placeholder_keys: {} + result: {} schema: description: 调试方法:显示所有资源的实际名称 properties: @@ -89,19 +64,10 @@ neware_battery_test_system: required: [] type: object result: - properties: - return_info: - description: 资源调试信息 - type: string - success: - description: 是否成功 - type: boolean - required: - - return_info - - success type: object required: - goal + title: debug_resource_names参数 type: object type: UniLabJsonCommand export_status_json: @@ -111,9 +77,8 @@ neware_battery_test_system: goal_default: filepath: bts_status.json handles: {} - result: - return_info: return_info - success: success + placeholder_keys: {} + result: {} schema: description: 导出当前状态数据到JSON文件 properties: @@ -127,19 +92,10 @@ neware_battery_test_system: required: [] type: object result: - properties: - return_info: - description: 导出操作结果信息 - type: string - success: - description: 导出是否成功 - type: boolean - required: - - return_info - - success type: object required: - goal + title: export_status_json参数 type: object type: UniLabJsonCommand get_device_summary: @@ -181,10 +137,8 @@ neware_battery_test_system: goal_default: plate_num: null handles: {} - result: - plate_data: plate_data - return_info: return_info - success: success + placeholder_keys: {} + result: {} schema: description: 获取指定盘或所有盘的状态信息 properties: @@ -193,29 +147,14 @@ neware_battery_test_system: properties: plate_num: description: 盘号 (1 或 2),如果为null则返回所有盘的状态 - maximum: 2 - minimum: 1 type: integer required: [] type: object result: - properties: - plate_data: - description: 盘状态数据(单盘或所有盘) - type: object - return_info: - description: 操作结果信息 - type: string - success: - description: 查询是否成功 - type: boolean - required: - - return_info - - success - - plate_data type: object required: - goal + title: get_plate_status参数 type: object type: UniLabJsonCommand print_status_summary_action: @@ -223,9 +162,8 @@ neware_battery_test_system: goal: {} goal_default: {} handles: {} - result: - return_info: return_info - success: success + placeholder_keys: {} + result: {} schema: description: 打印通道状态摘要信息到控制台 properties: @@ -235,28 +173,21 @@ neware_battery_test_system: required: [] type: object result: - properties: - return_info: - description: 打印操作结果信息 - type: string - success: - description: 打印是否成功 - type: boolean - required: - - return_info - - success type: object required: - goal + title: print_status_summary_action参数 type: object type: UniLabJsonCommand query_plate_action: feedback: {} goal: - string: plate_id + plate_id: plate_id + string: string goal_default: string: '' handles: {} + placeholder_keys: {} result: return_info: return_info success: success @@ -264,27 +195,23 @@ neware_battery_test_system: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -298,13 +225,11 @@ neware_battery_test_system: csv_path: string output_dir: string goal_default: - csv_path: '' + csv_path: null output_dir: . handles: {} - result: - return_info: return_info - submitted_count: submitted_count - success: success + placeholder_keys: {} + result: {} schema: description: 从CSV文件批量提交Neware测试任务 properties: @@ -315,31 +240,17 @@ neware_battery_test_system: description: 输入CSV文件的绝对路径 type: string output_dir: + default: . description: 输出目录(用于存储XML和备份文件),默认当前目录 type: string required: - csv_path type: object 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 required: - goal + title: submit_from_csv参数 type: object type: UniLabJsonCommand test_connection_action: @@ -347,9 +258,8 @@ neware_battery_test_system: goal: {} goal_default: {} handles: {} - result: - return_info: return_info - success: success + placeholder_keys: {} + result: {} schema: description: 测试与电池测试系统的TCP连接 properties: @@ -359,19 +269,10 @@ neware_battery_test_system: required: [] type: object result: - properties: - return_info: - description: 连接测试结果信息 - type: string - success: - description: 连接测试是否成功 - type: boolean - required: - - return_info - - success type: object required: - goal + title: test_connection_action参数 type: object type: UniLabJsonCommand upload_backup_to_oss: @@ -392,12 +293,8 @@ neware_battery_test_system: handler_key: uploaded_files io_type: sink label: Uploaded Files (with standard flow info) - result: - failed_files: failed_files - return_info: return_info - success: success - total_count: total_count - uploaded_count: uploaded_count + placeholder_keys: {} + result: {} schema: description: 上传备份文件到阿里云OSS properties: @@ -417,65 +314,17 @@ neware_battery_test_system: required: [] type: object 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 required: - goal + title: upload_backup_to_oss参数 type: object type: UniLabJsonCommand module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem status_types: - channel_status: dict - connection_info: dict + channel_status: Dict[int, Dict] + connection_info: Dict[str, str] device_summary: dict - plate_status: dict status: str total_channels: int type: python @@ -517,23 +366,24 @@ neware_battery_test_system: data: properties: channel_status: + additionalProperties: + type: object type: object connection_info: + additionalProperties: + type: string type: object device_summary: type: object - plate_status: - type: object status: type: string total_channels: type: integer required: - - status - channel_status - connection_info - - total_channels - - plate_status - device_summary + - status + - total_channels type: object version: 1.0.0 diff --git a/unilabos/registry/devices/opcua_example.yaml b/unilabos/registry/devices/opcua_example.yaml index a7e6b4e3..271fd682 100644 --- a/unilabos/registry/devices/opcua_example.yaml +++ b/unilabos/registry/devices/opcua_example.yaml @@ -142,8 +142,7 @@ opcua_example: type: object type: UniLabJsonCommand module: unilabos.device_comms.opcua_client.client:OpcUaClient - status_types: - node_value: String + status_types: {} type: python config_info: [] description: null @@ -167,10 +166,7 @@ opcua_example: - url type: object data: - properties: - node_value: - type: string - required: - - node_value + properties: {} + required: [] type: object version: 1.0.0 diff --git a/unilabos/registry/devices/opsky_ATR30007.yaml b/unilabos/registry/devices/opsky_ATR30007.yaml index ee8b8871..a3fa7df8 100644 --- a/unilabos/registry/devices/opsky_ATR30007.yaml +++ b/unilabos/registry/devices/opsky_ATR30007.yaml @@ -80,7 +80,8 @@ opsky_ATR30007: type: string required: [] type: object - result: {} + result: + type: object required: - goal title: run_once参数 diff --git a/unilabos/registry/devices/organic_miscellaneous.yaml b/unilabos/registry/devices/organic_miscellaneous.yaml index 3085c823..c1290bea 100644 --- a/unilabos/registry/devices/organic_miscellaneous.yaml +++ b/unilabos/registry/devices/organic_miscellaneous.yaml @@ -100,42 +100,41 @@ rotavap.one: type: object type: UniLabJsonCommand set_timer: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -250,9 +249,13 @@ separator.homemade: feedback: status: status goal: + event: event settling_time: settling_time stir_speed: stir_speed - stir_time: stir_time, + stir_time: stir_time + time: time + time_spec: time_spec + vessel: vessel goal_default: event: '' settling_time: '' @@ -281,34 +284,42 @@ separator.homemade: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: Stir_Feedback type: object goal: + additionalProperties: false properties: event: type: string settling_time: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number stir_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string time_spec: type: string vessel: + additionalProperties: false properties: category: type: string @@ -327,16 +338,26 @@ separator.homemade: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -346,12 +367,19 @@ separator.homemade: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -381,17 +409,10 @@ separator.homemade: - data title: vessel type: object - required: - - vessel - - time - - event - - time_spec - - stir_time - - stir_speed - - settling_time title: Stir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -399,10 +420,6 @@ separator.homemade: type: string success: type: boolean - required: - - success - - message - - return_info title: Stir_Result type: object required: @@ -418,36 +435,34 @@ separator.homemade: goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: diff --git a/unilabos/registry/devices/post_process_station.yaml b/unilabos/registry/devices/post_process_station.yaml index be42bad4..1614a2c3 100644 --- a/unilabos/registry/devices/post_process_station.yaml +++ b/unilabos/registry/devices/post_process_station.yaml @@ -28,31 +28,6 @@ post_process_station: title: load_config参数 type: object 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: feedback: {} goal: {} @@ -104,42 +79,41 @@ post_process_station: type: object type: UniLabJsonCommand disconnect: - feedback: {} + feedback: + status: status goal: - command: {} + command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -149,42 +123,41 @@ post_process_station: type: SendCmd read_node: feedback: - result: result + status: status goal: - command: node_name + command: command + node_name: node_name goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -283,17 +256,19 @@ post_process_station: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: PostProcessTriggerClean_Feedback type: object goal: + additionalProperties: false properties: acetone_inner_wall_cleaning_count: maximum: 2147483647 minimum: -2147483648 type: integer acetone_inner_wall_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number acetone_inner_wall_cleaning_waste_time: maximum: 2147483647 @@ -304,6 +279,8 @@ post_process_station: minimum: -2147483648 type: integer acetone_outer_wall_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number acetone_outer_wall_cleaning_wait_time: maximum: 2147483647 @@ -322,6 +299,8 @@ post_process_station: minimum: -2147483648 type: integer acetone_stirrer_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number acetone_stirrer_cleaning_wait_time: maximum: 2147483647 @@ -348,6 +327,8 @@ post_process_station: minimum: -2147483648 type: integer nmp_inner_wall_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number nmp_inner_wall_cleaning_waste_time: maximum: 2147483647 @@ -358,6 +339,8 @@ post_process_station: minimum: -2147483648 type: integer nmp_outer_wall_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number nmp_outer_wall_cleaning_wait_time: maximum: 2147483647 @@ -376,6 +359,8 @@ post_process_station: minimum: -2147483648 type: integer nmp_stirrer_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number nmp_stirrer_cleaning_wait_time: maximum: 2147483647 @@ -394,6 +379,8 @@ post_process_station: minimum: -2147483648 type: integer water_inner_wall_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number water_inner_wall_cleaning_waste_time: maximum: 2147483647 @@ -404,6 +391,8 @@ post_process_station: minimum: -2147483648 type: integer water_outer_wall_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number water_outer_wall_cleaning_wait_time: maximum: 2147483647 @@ -422,6 +411,8 @@ post_process_station: minimum: -2147483648 type: integer water_stirrer_cleaning_injection: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number water_stirrer_cleaning_wait_time: maximum: 2147483647 @@ -431,55 +422,13 @@ post_process_station: maximum: 2147483647 minimum: -2147483648 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 type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: PostProcessTriggerClean_Result type: object required: @@ -502,11 +451,11 @@ post_process_station: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: PostProcessGrab_Feedback type: object goal: + additionalProperties: false properties: raw_tank_number: maximum: 2147483647 @@ -516,17 +465,13 @@ post_process_station: maximum: 2147483647 minimum: -2147483648 type: integer - required: - - reaction_tank_number - - raw_tank_number title: PostProcessGrab_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: PostProcessGrab_Result type: object required: @@ -573,13 +518,15 @@ post_process_station: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: PostProcessTriggerPostPro_Feedback type: object goal: + additionalProperties: false properties: atomization_fast_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number atomization_pressure_kpa: maximum: 2147483647 @@ -594,8 +541,12 @@ post_process_station: minimum: -2147483648 type: integer first_wash_water_amount: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number initial_water_amount: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number injection_pump_push_speed: maximum: 2147483647 @@ -622,32 +573,20 @@ post_process_station: minimum: -2147483648 type: integer second_wash_water_amount: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number wash_slow_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 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 type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: PostProcessTriggerPostPro_Result type: object required: @@ -669,30 +608,26 @@ post_process_station: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -702,8 +637,7 @@ post_process_station: type: SendCmd module: unilabos.devices.workstation.post_process.post_process:OpcUaClient status_types: - cache_stats: dict - node_value: String + cache_stats: Dict[str, Any] type: python config_info: [] description: 后处理站 @@ -718,7 +652,9 @@ post_process_station: config_path: type: string deck: - type: string + anyOf: + - type: object + - type: object password: type: string subscription_interval: @@ -738,10 +674,7 @@ post_process_station: properties: cache_stats: type: object - node_value: - type: string required: - - node_value - cache_stats type: object version: 1.0.0 diff --git a/unilabos/registry/devices/pump_and_valve.yaml b/unilabos/registry/devices/pump_and_valve.yaml index 40fd9d3e..95a082d5 100644 --- a/unilabos/registry/devices/pump_and_valve.yaml +++ b/unilabos/registry/devices/pump_and_valve.yaml @@ -136,36 +136,36 @@ solenoid_valve: set_valve_position: feedback: {} goal: - string: position + position: position + string: string goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -278,26 +278,25 @@ solenoid_valve.mock: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -310,26 +309,25 @@ solenoid_valve.mock: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -422,6 +420,27 @@ syringe_pump_with_valve.runze.SY03B-T06: title: initialize参数 type: object 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: feedback: {} goal: {} @@ -695,7 +714,10 @@ syringe_pump_with_valve.runze.SY03B-T06: goal: properties: position: - type: string + anyOf: + - type: integer + - type: string + - type: number required: - position type: object @@ -720,7 +742,9 @@ syringe_pump_with_valve.runze.SY03B-T06: goal: properties: velocity: - type: string + anyOf: + - type: integer + - type: string required: - velocity type: object @@ -780,13 +804,13 @@ syringe_pump_with_valve.runze.SY03B-T06: status_types: max_velocity: float mode: int - plunger_position: String + plunger_position: '' position: float status: str valve_position: str - velocity_end: String - velocity_grade: String - velocity_init: String + velocity_end: '' + velocity_grade: '' + velocity_init: '' type: python config_info: [] description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。 @@ -885,15 +909,15 @@ syringe_pump_with_valve.runze.SY03B-T06: velocity_init: type: string required: - - status - - mode - max_velocity + - mode + - plunger_position + - position + - status + - valve_position + - velocity_end - velocity_grade - velocity_init - - velocity_end - - valve_position - - position - - plunger_position type: object version: 1.0.0 syringe_pump_with_valve.runze.SY03B-T08: @@ -943,6 +967,27 @@ syringe_pump_with_valve.runze.SY03B-T08: title: initialize参数 type: object 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: feedback: {} goal: {} @@ -1216,7 +1261,10 @@ syringe_pump_with_valve.runze.SY03B-T08: goal: properties: position: - type: string + anyOf: + - type: integer + - type: string + - type: number required: - position type: object @@ -1241,7 +1289,9 @@ syringe_pump_with_valve.runze.SY03B-T08: goal: properties: velocity: - type: string + anyOf: + - type: integer + - type: string required: - velocity type: object @@ -1301,13 +1351,13 @@ syringe_pump_with_valve.runze.SY03B-T08: status_types: max_velocity: float mode: int - plunger_position: String + plunger_position: '' position: float status: str valve_position: str - velocity_end: String - velocity_grade: String - velocity_init: String + velocity_end: '' + velocity_grade: '' + velocity_init: '' type: python config_info: [] description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。 @@ -1422,14 +1472,14 @@ syringe_pump_with_valve.runze.SY03B-T08: velocity_init: type: string required: - - status - - mode - max_velocity + - mode + - plunger_position + - position + - status + - valve_position + - velocity_end - velocity_grade - velocity_init - - velocity_end - - valve_position - - position - - plunger_position type: object version: 1.0.0 diff --git a/unilabos/registry/devices/reaction_station_bioyond.yaml b/unilabos/registry/devices/reaction_station_bioyond.yaml index 8b4622dc..1372140d 100644 --- a/unilabos/registry/devices/reaction_station_bioyond.yaml +++ b/unilabos/registry/devices/reaction_station_bioyond.yaml @@ -13,12 +13,13 @@ reaction_station.bioyond: start_point: start_point start_step_key: start_step_key goal_default: - duration: 0 + duration: null end_point: 0 end_step_key: '' start_point: 0 start_step_key: '' handles: {} + placeholder_keys: {} result: {} schema: description: 添加时间约束 - 在两个工作流之间添加时间约束 @@ -30,23 +31,19 @@ reaction_station.bioyond: description: 时间(秒) type: integer end_point: - default: Start + default: 0 description: 终点计时点 (Start=开始前, End=结束后) - enum: - - Start - - End - type: string + type: integer end_step_key: + default: '' description: 终点步骤Key (可选, 默认为空则自动选择) type: string start_point: - default: Start + default: 0 description: 起点计时点 (Start=开始前, End=结束后) - enum: - - Start - - End - type: string + type: integer start_step_key: + default: '' description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择) type: string required: @@ -98,7 +95,8 @@ reaction_station.bioyond: required: - json_str type: object - result: {} + result: + type: object required: - goal title: create_order参数 @@ -125,7 +123,8 @@ reaction_station.bioyond: required: - workflow_ids type: object - result: {} + result: + type: object required: - goal title: hard_delete_merged_workflows参数 @@ -150,7 +149,8 @@ reaction_station.bioyond: required: - json_str type: object - result: {} + result: + type: object required: - goal title: merge_workflow_with_parameters参数 @@ -175,7 +175,8 @@ reaction_station.bioyond: required: - report_request type: object - result: {} + result: + type: object required: - goal title: process_temperature_cutoff_report参数 @@ -200,7 +201,12 @@ reaction_station.bioyond: required: - web_workflow_json type: object - result: {} + result: + items: + additionalProperties: + type: string + type: object + type: array required: - goal title: process_web_workflows参数 @@ -229,7 +235,8 @@ reaction_station.bioyond: - reactor_id - temperature type: object - result: {} + result: + type: string required: - goal title: set_reactor_temperature参数 @@ -254,7 +261,8 @@ reaction_station.bioyond: required: - preintake_id type: object - result: {} + result: + type: object required: - goal title: skip_titration_steps参数 @@ -275,7 +283,8 @@ reaction_station.bioyond: properties: {} required: [] type: object - result: {} + result: + type: object required: - goal title: sync_workflow_sequence_from_bioyond参数 @@ -307,7 +316,8 @@ reaction_station.bioyond: type: integer required: [] type: object - result: {} + result: + type: object required: - goal title: wait_for_multiple_orders_and_get_reports参数 @@ -359,7 +369,8 @@ reaction_station.bioyond: required: - workflow_id type: object - result: {} + result: + type: object required: - goal title: workflow_step_query参数 @@ -370,9 +381,8 @@ reaction_station.bioyond: goal: {} goal_default: {} handles: {} - result: - code: code - message: message + placeholder_keys: {} + result: {} schema: description: 清空服务端所有非核心工作流 (保留核心流程) properties: @@ -382,13 +392,6 @@ reaction_station.bioyond: required: [] type: object result: - properties: - code: - description: 操作结果代码(1表示成功) - type: integer - message: - description: 结果描述 - type: string type: object required: - goal @@ -405,13 +408,14 @@ reaction_station.bioyond: torque_variation: torque_variation volume: volume goal_default: - assign_material_name: '' - temperature: '' - time: '' - titration_type: '' - torque_variation: '' - volume: '' + assign_material_name: null + temperature: 25.0 + time: '90' + titration_type: '1' + torque_variation: 2 + volume: null handles: {} + placeholder_keys: {} result: {} schema: description: 滴回去 @@ -423,33 +427,27 @@ reaction_station.bioyond: description: 物料名称(不能为空) type: string temperature: + default: 25.0 description: 温度设定(°C) - type: string + type: number time: + default: '90' description: 观察时间(分钟) type: string titration_type: + default: '1' description: 是否滴定(NO=否, YES=是) - enum: - - 'NO' - - 'YES' type: string torque_variation: + default: 2 description: 是否观察 (NO=否, YES=是) - enum: - - 'NO' - - 'YES' - type: string + type: integer volume: description: 分液公式(mL) type: string required: - - volume - assign_material_name - - time - - torque_variation - - titration_type - - temperature + - volume type: object result: {} required: @@ -462,7 +460,7 @@ reaction_station.bioyond: goal: batch_reports_result: batch_reports_result goal_default: - batch_reports_result: '' + batch_reports_result: null handles: input: - data_key: batch_reports_result @@ -478,8 +476,8 @@ reaction_station.bioyond: handler_key: ACTUALS_EXTRACTED io_type: sink label: Extracted Actuals - result: - return_info: return_info + placeholder_keys: {} + result: {} schema: description: 从批量任务完成报告中提取每个订单的实际加料量,输出extracted列表。 properties: @@ -493,13 +491,6 @@ reaction_station.bioyond: - batch_reports_result type: object result: - properties: - return_info: - description: JSON字符串,包含actuals数组,每项含order_code, order_id, actualTargetWeigh, - actualVolume - type: string - required: - - return_info title: extract_actuals_from_batch_reports结果 type: object required: @@ -517,13 +508,14 @@ reaction_station.bioyond: torque_variation: torque_variation volume: volume goal_default: - assign_material_name: '' - temperature: '' - time: '' - titration_type: '' - torque_variation: '' - volume: '' + assign_material_name: BAPP + temperature: 25.0 + time: '0' + titration_type: '1' + torque_variation: 1 + volume: '350' handles: {} + placeholder_keys: {} result: {} schema: description: 液体进料烧杯 @@ -532,36 +524,30 @@ reaction_station.bioyond: goal: properties: assign_material_name: + default: BAPP description: 物料名称 type: string temperature: + default: 25.0 description: 温度设定(°C) - type: string + type: number time: + default: '0' description: 观察时间(分钟) type: string titration_type: + default: '1' description: 是否滴定(NO=否, YES=是) - enum: - - 'NO' - - 'YES' type: string torque_variation: + default: 1 description: 是否观察 (NO=否, YES=是) - enum: - - 'NO' - - 'YES' - type: string + type: integer volume: + default: '350' description: 分液公式(mL) type: string - required: - - volume - - assign_material_name - - time - - torque_variation - - titration_type - - temperature + required: [] type: object result: {} required: @@ -580,13 +566,13 @@ reaction_station.bioyond: torque_variation: torque_variation volume: volume goal_default: - assign_material_name: '' - solvents: '' - temperature: '25.00' + assign_material_name: null + solvents: null + temperature: 25.0 time: '360' titration_type: '1' - torque_variation: '2' - volume: '' + torque_variation: 2 + volume: null handles: input: - data_key: solvents @@ -595,6 +581,7 @@ reaction_station.bioyond: handler_key: solvents io_type: source label: Solvents Data From Calculation Node + placeholder_keys: {} result: {} schema: description: 液体投料-溶剂。可以直接提供volume(mL),或通过solvents对象自动从additional_solvent(mL)计算volume。 @@ -609,27 +596,21 @@ reaction_station.bioyond: description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume' type: string temperature: - default: '25.00' + default: 25.0 description: 温度设定(°C),默认25.00 - type: string + type: number time: default: '360' description: 观察时间(分钟),默认360 type: string titration_type: - default: 'NO' + default: '1' description: 是否滴定(NO=否, YES=是),默认NO - enum: - - 'NO' - - 'YES' type: string torque_variation: - default: 'YES' + default: 2 description: 是否观察 (NO=否, YES=是),默认YES - enum: - - 'NO' - - 'YES' - type: string + type: integer volume: description: 分液量(mL)。可直接提供,或通过solvents参数自动计算 type: string @@ -655,15 +636,15 @@ reaction_station.bioyond: volume_formula: volume_formula x_value: x_value goal_default: - assign_material_name: '' - extracted_actuals: '' - feeding_order_data: '' - temperature: '25.00' + assign_material_name: null + extracted_actuals: null + feeding_order_data: null + temperature: 25.0 time: '90' titration_type: '2' - torque_variation: '2' - volume_formula: '' - x_value: '' + torque_variation: 2 + volume_formula: null + x_value: null handles: input: - data_key: extracted_actuals @@ -678,6 +659,7 @@ reaction_station.bioyond: handler_key: feeding_order io_type: source label: Feeding Order Data From Calculation Node + placeholder_keys: {} result: {} schema: 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}]}' type: string temperature: - default: '25.00' + default: 25.0 description: 温度设定(°C),默认25.00 - type: string + type: number time: default: '90' description: 观察时间(分钟),默认90 type: string titration_type: - default: 'YES' + default: '2' description: 是否滴定(NO=否, YES=是),默认YES - enum: - - 'NO' - - 'YES' type: string torque_variation: - default: 'YES' + default: 2 description: 是否观察 (NO=否, YES=是),默认YES - enum: - - 'NO' - - 'YES' - type: string + type: integer volume_formula: description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成 type: string @@ -742,13 +718,14 @@ reaction_station.bioyond: torque_variation: torque_variation volume_formula: volume_formula goal_default: - assign_material_name: '' - temperature: '' - time: '' - titration_type: '' - torque_variation: '' - volume_formula: '' + assign_material_name: null + temperature: 25.0 + time: '0' + titration_type: '1' + torque_variation: 1 + volume_formula: null handles: {} + placeholder_keys: {} result: {} schema: description: 液体进料小瓶(非滴定) @@ -760,33 +737,27 @@ reaction_station.bioyond: description: 物料名称 type: string temperature: + default: 25.0 description: 温度设定(°C) - type: string + type: number time: + default: '0' description: 观察时间(分钟) type: string titration_type: + default: '1' description: 是否滴定(NO=否, YES=是) - enum: - - 'NO' - - 'YES' type: string torque_variation: + default: 1 description: 是否观察 (NO=否, YES=是) - enum: - - 'NO' - - 'YES' - type: string + type: integer volume_formula: description: 分液公式(mL) type: string required: - volume_formula - assign_material_name - - time - - torque_variation - - titration_type - - temperature type: object result: {} required: @@ -800,9 +771,10 @@ reaction_station.bioyond: task_name: task_name workflow_name: workflow_name goal_default: - task_name: '' - workflow_name: '' + task_name: null + workflow_name: null handles: {} + placeholder_keys: {} result: {} schema: description: 处理并执行工作流 @@ -820,7 +792,8 @@ reaction_station.bioyond: - workflow_name - task_name type: object - result: {} + result: + type: object required: - goal title: process_and_execute_workflow参数 @@ -833,10 +806,11 @@ reaction_station.bioyond: cutoff: cutoff temperature: temperature goal_default: - assign_material_name: '' - cutoff: '' - temperature: '' + assign_material_name: null + cutoff: '900000' + temperature: -10.0 handles: {} + placeholder_keys: {} result: {} schema: description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数 @@ -848,14 +822,14 @@ reaction_station.bioyond: description: 物料名称 type: string cutoff: + default: '900000' description: 粘度上限 type: string temperature: + default: -10.0 description: 温度设定(°C) - type: string + type: number required: - - cutoff - - temperature - assign_material_name type: object result: {} @@ -869,6 +843,7 @@ reaction_station.bioyond: goal: {} goal_default: {} handles: {} + placeholder_keys: {} result: {} schema: description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作 @@ -878,15 +853,7 @@ reaction_station.bioyond: properties: {} required: [] type: object - result: - properties: - code: - description: 操作结果代码(1表示成功,0表示失败) - type: integer - return_info: - description: 操作结果详细信息 - type: string - type: object + result: {} required: - goal title: reactor_taken_out参数 @@ -897,8 +864,8 @@ reaction_station.bioyond: goal: {} goal_default: {} handles: {} - result: - return_info: return_info + placeholder_keys: {} + result: {} schema: description: 启动调度器 - 启动Bioyond工作站的任务调度器,开始执行队列中的任务 properties: @@ -908,12 +875,6 @@ reaction_station.bioyond: required: [] type: object result: - properties: - return_info: - description: 调度器启动结果,成功返回1,失败返回0 - type: integer - required: - - return_info title: scheduler_start结果 type: object required: @@ -930,12 +891,13 @@ reaction_station.bioyond: time: time torque_variation: torque_variation goal_default: - assign_material_name: '' - material_id: '' - temperature: '' - time: '' - torque_variation: '' + assign_material_name: null + material_id: null + temperature: 25.0 + time: '0' + torque_variation: 1 handles: {} + placeholder_keys: {} result: {} schema: description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA) @@ -948,29 +910,21 @@ reaction_station.bioyond: type: string material_id: description: 粉末类型ID,Salt=盐(21分钟),Flour=面粉(27分钟),BTDA=BTDA(38分钟) - enum: - - Salt - - Flour - - BTDA type: string temperature: + default: 25.0 description: 温度设定(°C) - type: string + type: number time: + default: '0' description: 观察时间(分钟) type: string torque_variation: + default: 1 description: 是否观察 (NO=否, YES=是) - enum: - - 'NO' - - 'YES' - type: string + type: integer required: - - assign_material_name - material_id - - time - - torque_variation - - temperature type: object result: {} required: diff --git a/unilabos/registry/devices/robot_agv.yaml b/unilabos/registry/devices/robot_agv.yaml index 9f45bd5e..b37a0c46 100644 --- a/unilabos/registry/devices/robot_agv.yaml +++ b/unilabos/registry/devices/robot_agv.yaml @@ -37,42 +37,41 @@ agv.SEER: type: object type: UniLabJsonCommand send_nav_task: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: diff --git a/unilabos/registry/devices/robot_arm.yaml b/unilabos/registry/devices/robot_arm.yaml index 147eab4d..ff357ad4 100644 --- a/unilabos/registry/devices/robot_arm.yaml +++ b/unilabos/registry/devices/robot_arm.yaml @@ -122,31 +122,6 @@ robotic_arm.SCARA_with_slider.moveit.virtual: title: moveit_task参数 type: object 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: feedback: {} goal: {} @@ -198,41 +173,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual: type: object type: UniLabJsonCommand pick_and_place: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -241,41 +216,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual: type: object type: SendCmd set_position: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -284,41 +259,41 @@ robotic_arm.SCARA_with_slider.moveit.virtual: type: object type: SendCmd set_status: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -455,42 +430,41 @@ robotic_arm.UR: type: object type: UniLabJsonCommand move_pos_task: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -532,8 +506,8 @@ robotic_arm.UR: type: string required: - arm_pose - - gripper_pose - arm_status + - gripper_pose - gripper_status type: object version: 1.0.0 @@ -726,41 +700,41 @@ robotic_arm.elite: type: object type: UniLabJsonCommand modbus_task_cmd: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -770,8 +744,8 @@ robotic_arm.elite: type: SendCmd module: unilabos.devices.arm.elite_robot:EliteRobot status_types: - actual_joint_positions: String - arm_pose: String + actual_joint_positions: '' + arm_pose: list[float] type: python config_info: [] description: Elite robot arm @@ -797,8 +771,8 @@ robotic_arm.elite: type: number type: array required: - - arm_pose - actual_joint_positions + - arm_pose type: object model: mesh: elite_robot diff --git a/unilabos/registry/devices/robot_gripper.yaml b/unilabos/registry/devices/robot_gripper.yaml index 295c48a0..4f579e24 100644 --- a/unilabos/registry/devices/robot_gripper.yaml +++ b/unilabos/registry/devices/robot_gripper.yaml @@ -114,11 +114,12 @@ gripper.misumi_rz: goal: properties: data: - type: string + type: object required: - data type: object - result: {} + result: + type: object required: - goal title: modbus_crc参数 @@ -398,30 +399,26 @@ gripper.misumi_rz: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -504,71 +501,82 @@ gripper.mock: type: UniLabJsonCommand push_to: feedback: - effort: torque + effort: effort position: position + reached_goal: reached_goal + stalled: stalled goal: - command.max_effort: torque - command.position: position + command: command + position: position + torque: torque + velocity: velocity goal_default: command: max_effort: 0.0 position: 0.0 handles: {} + placeholder_keys: {} result: - effort: torque + effort: effort position: position + reached_goal: reached_goal + stalled: stalled schema: description: '' properties: feedback: + additionalProperties: false properties: effort: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number reached_goal: type: boolean stalled: type: boolean - required: - - position - - effort - - stalled - - reached_goal title: GripperCommand_Feedback type: object goal: + additionalProperties: false properties: command: + additionalProperties: false properties: max_effort: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - position - max_effort title: command type: object - required: - - command title: GripperCommand_Goal type: object result: + additionalProperties: false properties: effort: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number reached_goal: type: boolean stalled: type: boolean - required: - - position - - effort - - stalled - - reached_goal title: GripperCommand_Result type: object required: @@ -604,8 +612,8 @@ gripper.mock: type: number required: - position - - velocity - - torque - status + - torque + - velocity type: object version: 1.0.0 diff --git a/unilabos/registry/devices/robot_linear_motion.yaml b/unilabos/registry/devices/robot_linear_motion.yaml index 0f8506e9..74b01e80 100644 --- a/unilabos/registry/devices/robot_linear_motion.yaml +++ b/unilabos/registry/devices/robot_linear_motion.yaml @@ -24,6 +24,27 @@ linear_motion.grbl: title: initialize参数 type: object 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: feedback: {} goal: {} @@ -93,44 +114,39 @@ linear_motion.grbl: type: UniLabJsonCommandAsync move_through_points: feedback: - current_pose.pose.position: position - estimated_time_remaining.sec: time_remaining - navigation_time.sec: time_spent - number_of_poses_remaining: pose_number_remaining + current_pose: current_pose + distance_remaining: distance_remaining + estimated_time_remaining: estimated_time_remaining + navigation_time: navigation_time + number_of_poses_remaining: number_of_poses_remaining + number_of_recoveries: number_of_recoveries goal: - poses[].pose.position: positions[] + behavior_tree: behavior_tree + poses: poses + positions: positions goal_default: behavior_tree: '' - 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 + poses: [] handles: {} - result: {} + placeholder_keys: {} + result: + result: result schema: description: '' properties: feedback: + additionalProperties: false properties: current_pose: + additionalProperties: false properties: header: + additionalProperties: false properties: frame_id: type: string stamp: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -151,16 +167,26 @@ linear_motion.grbl: title: header type: object pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -170,12 +196,19 @@ linear_motion.grbl: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -194,8 +227,11 @@ linear_motion.grbl: title: current_pose type: object distance_remaining: + maximum: 3.4028235e+38 + minimum: -3.4028235e+38 type: number estimated_time_remaining: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -211,6 +247,7 @@ linear_motion.grbl: title: estimated_time_remaining type: object navigation_time: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -233,16 +270,10 @@ linear_motion.grbl: maximum: 32767 minimum: -32768 type: integer - required: - - current_pose - - navigation_time - - estimated_time_remaining - - number_of_recoveries - - distance_remaining - - number_of_poses_remaining title: NavigateThroughPoses_Feedback type: object goal: + additionalProperties: false properties: behavior_tree: type: string @@ -256,12 +287,8 @@ linear_motion.grbl: stamp: properties: nanosec: - maximum: 4294967295 - minimum: 0 type: integer sec: - maximum: 2147483647 - minimum: -2147483648 type: integer required: - sec @@ -314,23 +341,17 @@ linear_motion.grbl: required: - header - pose - title: poses type: object type: array - required: - - poses - - behavior_tree title: NavigateThroughPoses_Goal type: object result: + additionalProperties: false properties: result: - properties: {} - required: [] + additionalProperties: true title: result type: object - required: - - result title: NavigateThroughPoses_Result type: object required: @@ -340,9 +361,15 @@ linear_motion.grbl: type: NavigateThroughPoses set_spindle_speed: feedback: - position: spindle_speed + error: error + header: header + position: position + velocity: velocity goal: - position: spindle_speed + max_velocity: max_velocity + min_duration: min_duration + position: position + spindle_speed: spindle_speed goal_default: max_velocity: 0.0 min_duration: @@ -350,19 +377,25 @@ linear_motion.grbl: sec: 0 position: 0.0 handles: {} + placeholder_keys: {} result: {} schema: description: '' properties: feedback: + additionalProperties: false properties: error: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number header: + additionalProperties: false properties: frame_id: type: string stamp: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -383,21 +416,24 @@ linear_motion.grbl: title: header type: object position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number velocity: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - header - - position - - velocity - - error title: SingleJointPosition_Feedback type: object goal: + additionalProperties: false properties: max_velocity: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number min_duration: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -413,16 +449,13 @@ linear_motion.grbl: title: min_duration type: object position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - position - - min_duration - - max_velocity title: SingleJointPosition_Goal type: object result: - properties: {} - required: [] + additionalProperties: true title: SingleJointPosition_Result type: object required: @@ -432,7 +465,7 @@ linear_motion.grbl: type: SingleJointPosition module: unilabos.devices.cnc.grbl_sync:GrblCNC status_types: - position: unilabos.messages:Point3D + position: Point3D spindle_speed: float status: str type: python @@ -471,9 +504,9 @@ linear_motion.grbl: status: type: string required: - - status - position - spindle_speed + - status type: object version: 1.0.0 linear_motion.toyo_xyz.sim: @@ -600,31 +633,6 @@ linear_motion.toyo_xyz.sim: title: moveit_task参数 type: object 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: feedback: {} goal: {} @@ -676,41 +684,41 @@ linear_motion.toyo_xyz.sim: type: object type: UniLabJsonCommand pick_and_place: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -719,41 +727,41 @@ linear_motion.toyo_xyz.sim: type: object type: SendCmd set_position: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -762,41 +770,41 @@ linear_motion.toyo_xyz.sim: type: object type: SendCmd set_status: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -939,30 +947,26 @@ motor.iCL42: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -1000,8 +1004,8 @@ motor.iCL42: success: type: boolean required: - - motor_position - is_executing_run + - motor_position - success type: object version: 1.0.0 diff --git a/unilabos/registry/devices/solid_dispenser.yaml b/unilabos/registry/devices/solid_dispenser.yaml index 9bceb54b..46280631 100644 --- a/unilabos/registry/devices/solid_dispenser.yaml +++ b/unilabos/registry/devices/solid_dispenser.yaml @@ -14,19 +14,24 @@ solid_dispenser.laiyu: powder_tube_number: 0 target_tube_position: '' handles: {} + placeholder_keys: {} result: actual_mass_mg: actual_mass_mg + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: SolidDispenseAddPowderTube_Feedback type: object goal: + additionalProperties: false properties: compound_mass: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number powder_tube_number: maximum: 2147483647 @@ -34,24 +39,19 @@ solid_dispenser.laiyu: type: integer target_tube_position: type: string - required: - - powder_tube_number - - target_tube_position - - compound_mass title: SolidDispenseAddPowderTube_Goal type: object result: + additionalProperties: false properties: actual_mass_mg: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number return_info: type: string success: type: boolean - required: - - return_info - - actual_mass_mg - - success title: SolidDispenseAddPowderTube_Result type: object required: @@ -74,11 +74,12 @@ solid_dispenser.laiyu: goal: properties: data: - type: string + type: object required: - data type: object - result: {} + result: + type: object required: - goal title: calculate_crc参数 @@ -99,11 +100,12 @@ solid_dispenser.laiyu: goal: properties: command: - type: string + type: object required: - command type: object - result: {} + result: + type: object required: - goal title: send_command参数 @@ -112,36 +114,37 @@ solid_dispenser.laiyu: discharge: feedback: {} goal: - float_input: float_input + float_in: float_in goal_default: float_in: 0.0 handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: FloatSingleInput_Feedback type: object goal: + additionalProperties: false properties: float_in: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - float_in title: FloatSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: FloatSingleInput_Result type: object required: @@ -156,32 +159,31 @@ solid_dispenser.laiyu: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -200,38 +202,41 @@ solid_dispenser.laiyu: y: 0.0 z: 0.0 handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: Point3DSeparateInput_Feedback type: object goal: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - x - - y - - z title: Point3DSeparateInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: Point3DSeparateInput_Result type: object required: @@ -246,34 +251,33 @@ solid_dispenser.laiyu: goal_default: int_input: 0 handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: IntSingleInput_Feedback type: object goal: + additionalProperties: false properties: int_input: maximum: 2147483647 minimum: -2147483648 type: integer - required: - - int_input title: IntSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: IntSingleInput_Result type: object required: @@ -288,34 +292,33 @@ solid_dispenser.laiyu: goal_default: int_input: 0 handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: IntSingleInput_Feedback type: object goal: + additionalProperties: false properties: int_input: maximum: 2147483647 minimum: -2147483648 type: integer - required: - - int_input title: IntSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: IntSingleInput_Result type: object required: @@ -328,26 +331,25 @@ solid_dispenser.laiyu: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: diff --git a/unilabos/registry/devices/temperature.yaml b/unilabos/registry/devices/temperature.yaml index 874fe517..9e60adb1 100644 --- a/unilabos/registry/devices/temperature.yaml +++ b/unilabos/registry/devices/temperature.yaml @@ -34,7 +34,8 @@ chiller: - register_address - value type: object - result: {} + result: + type: object required: - goal title: build_modbus_frame参数 @@ -63,7 +64,8 @@ chiller: required: - temperature type: object - result: {} + result: + type: integer required: - goal title: convert_temperature_to_modbus_value参数 @@ -84,11 +86,12 @@ chiller: goal: properties: data: - type: string + type: object required: - data type: object - result: {} + result: + type: object required: - goal title: modbus_crc参数 @@ -116,42 +119,41 @@ chiller: type: object type: UniLabJsonCommand set_temperature: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -266,9 +268,15 @@ heaterstirrer.dalong: feedback: status: status goal: + pressure: pressure purpose: purpose + reflux_solvent: reflux_solvent + stir: stir + stir_speed: stir_speed temp: temp + temp_spec: temp_spec time: time + time_spec: time_spec vessel: vessel goal_default: pressure: '' @@ -301,20 +309,23 @@ heaterstirrer.dalong: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChill_Feedback type: object goal: + additionalProperties: false properties: pressure: type: string @@ -325,8 +336,12 @@ heaterstirrer.dalong: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp_spec: type: string @@ -335,6 +350,7 @@ heaterstirrer.dalong: time_spec: type: string vessel: + additionalProperties: false properties: category: type: string @@ -353,16 +369,26 @@ heaterstirrer.dalong: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -372,12 +398,19 @@ heaterstirrer.dalong: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -407,20 +440,10 @@ heaterstirrer.dalong: - data title: vessel type: object - required: - - vessel - - temp - - time - - temp_spec - - time_spec - - pressure - - reflux_solvent - - stir - - stir_speed - - purpose title: HeatChill_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -428,10 +451,6 @@ heaterstirrer.dalong: type: string success: type: boolean - required: - - success - - message - - return_info title: HeatChill_Result type: object required: @@ -440,42 +459,42 @@ heaterstirrer.dalong: type: object type: HeatChill set_temp_target: - feedback: {} + feedback: + status: status goal: - command: temp + command: command + temp: temp goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -484,42 +503,42 @@ heaterstirrer.dalong: type: object type: SendCmd set_temp_warning: - feedback: {} + feedback: + status: status goal: - command: temp + command: command + temp: temp goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -569,8 +588,8 @@ heaterstirrer.dalong: - status - stir_speed - temp - - temp_warning - temp_target + - temp_warning type: object version: 1.0.0 tempsensor: @@ -691,42 +710,41 @@ tempsensor: type: object type: UniLabJsonCommand set_warning: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index f0635755..67560f2f 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -18,7 +18,8 @@ virtual_centrifuge: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -39,41 +40,17 @@ virtual_centrifuge: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - 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 centrifuge: feedback: current_speed: current_speed - current_status: status + current_status: current_status current_temp: current_temp progress: progress goal: @@ -106,38 +83,50 @@ virtual_centrifuge: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number current_status: type: string current_temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_speed - - current_temp - - current_status title: Centrifuge_Feedback type: object goal: + additionalProperties: false properties: speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -156,16 +145,26 @@ virtual_centrifuge: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -175,12 +174,19 @@ virtual_centrifuge: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -210,14 +216,10 @@ virtual_centrifuge: - data title: vessel type: object - required: - - vessel - - speed - - time - - temp title: Centrifuge_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -225,10 +227,6 @@ virtual_centrifuge: type: string success: type: boolean - required: - - success - - message - - return_info title: Centrifuge_Result type: object required: @@ -267,7 +265,7 @@ virtual_centrifuge: config: properties: config: - type: string + type: object device_id: type: string required: [] @@ -299,18 +297,18 @@ virtual_centrifuge: time_remaining: type: number required: - - status - centrifuge_state - current_speed - - target_speed - current_temp - - target_temp - max_speed - max_temp - - min_temp - - time_remaining - - progress - message + - min_temp + - progress + - status + - target_speed + - target_temp + - time_remaining type: object version: 1.0.0 virtual_column: @@ -333,7 +331,8 @@ virtual_column: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -354,45 +353,26 @@ virtual_column: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - 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 run_column: feedback: - current_status: current_status - processed_volume: processed_volume progress: progress + status: status goal: column: column from_vessel: from_vessel + pct1: pct1 + pct2: pct2 + ratio: ratio + rf: rf + solvent1: solvent1 + solvent2: solvent2 to_vessel: to_vessel goal_default: column: '' @@ -443,29 +423,32 @@ virtual_column: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: - message: current_status - return_info: current_status + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: RunColumn_Feedback type: object goal: + additionalProperties: false properties: column: type: string from_vessel: + additionalProperties: false properties: category: type: string @@ -484,16 +467,26 @@ virtual_column: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -503,12 +496,19 @@ virtual_column: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -551,6 +551,7 @@ virtual_column: solvent2: type: string to_vessel: + additionalProperties: false properties: category: type: string @@ -569,16 +570,26 @@ virtual_column: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -588,12 +599,19 @@ virtual_column: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -623,19 +641,10 @@ virtual_column: - data title: to_vessel type: object - required: - - from_vessel - - to_vessel - - column - - rf - - pct1 - - pct2 - - solvent1 - - solvent2 - - ratio title: RunColumn_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -643,10 +652,6 @@ virtual_column: type: string success: type: boolean - required: - - success - - message - - return_info title: RunColumn_Result type: object required: @@ -722,17 +727,17 @@ virtual_column: status: type: string required: - - status + - column_diameter + - column_length - column_state - current_flow_rate + - current_phase + - current_status + - final_volume - max_flow_rate - - column_length - - column_diameter - processed_volume - progress - - current_status - - current_phase - - final_volume + - status type: object version: 1.0.0 virtual_filter: @@ -755,7 +760,8 @@ virtual_filter: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -776,37 +782,13 @@ virtual_filter: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - 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 filter: feedback: current_status: current_status @@ -868,35 +850,40 @@ virtual_filter: type: '' volume: 0.0 handles: {} + placeholder_keys: {} result: message: message - return_info: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string current_temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number filtered_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_temp - - filtered_volume - - current_status title: Filter_Feedback type: object goal: + additionalProperties: false properties: continue_heatchill: type: boolean filtrate_vessel: + additionalProperties: false properties: category: type: string @@ -915,16 +902,26 @@ virtual_filter: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -934,12 +931,19 @@ virtual_filter: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -972,10 +976,15 @@ virtual_filter: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -994,16 +1003,26 @@ virtual_filter: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1013,12 +1032,19 @@ virtual_filter: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1049,18 +1075,13 @@ virtual_filter: title: vessel type: object volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - vessel - - filtrate_vessel - - stir - - stir_speed - - temp - - continue_heatchill - - volume title: Filter_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -1068,10 +1089,6 @@ virtual_filter: type: string success: type: boolean - required: - - success - - message - - return_info title: Filter_Result type: object required: @@ -1123,7 +1140,7 @@ virtual_filter: config: properties: config: - type: string + type: object device_id: type: string required: [] @@ -1149,15 +1166,15 @@ virtual_filter: status: type: string required: - - status - - progress - - current_temp - current_status + - current_temp - filtered_volume - - message - - max_temp - max_stir_speed + - max_temp - max_volume + - message + - progress + - status type: object version: 1.0.0 virtual_gas_source: @@ -1180,7 +1197,8 @@ virtual_gas_source: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -1201,7 +1219,8 @@ virtual_gas_source: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 @@ -1254,26 +1273,25 @@ virtual_gas_source: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -1286,26 +1304,25 @@ virtual_gas_source: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -1320,32 +1337,31 @@ virtual_gas_source: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -1373,7 +1389,7 @@ virtual_gas_source: config: properties: config: - type: string + type: object device_id: type: string required: [] @@ -1406,7 +1422,8 @@ virtual_heatchill: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -1427,46 +1444,26 @@ virtual_heatchill: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - 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 heat_chill: feedback: status: status goal: + pressure: pressure purpose: purpose + reflux_solvent: reflux_solvent stir: stir stir_speed: stir_speed temp: temp + temp_spec: temp_spec time: time + time_spec: time_spec vessel: vessel goal_default: pressure: '' @@ -1499,20 +1496,23 @@ virtual_heatchill: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChill_Feedback type: object goal: + additionalProperties: false properties: pressure: type: string @@ -1523,8 +1523,12 @@ virtual_heatchill: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp_spec: type: string @@ -1533,6 +1537,7 @@ virtual_heatchill: time_spec: type: string vessel: + additionalProperties: false properties: category: type: string @@ -1551,16 +1556,26 @@ virtual_heatchill: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1570,12 +1585,19 @@ virtual_heatchill: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1605,20 +1627,10 @@ virtual_heatchill: - data title: vessel type: object - required: - - vessel - - temp - - time - - temp_spec - - time_spec - - pressure - - reflux_solvent - - stir - - stir_speed - - purpose title: HeatChill_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -1626,10 +1638,6 @@ virtual_heatchill: type: string success: type: boolean - required: - - success - - message - - return_info title: HeatChill_Result type: object required: @@ -1668,26 +1676,31 @@ virtual_heatchill: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChillStart_Feedback type: object goal: + additionalProperties: false properties: purpose: type: string temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -1706,16 +1719,26 @@ virtual_heatchill: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1725,12 +1748,19 @@ virtual_heatchill: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1760,21 +1790,15 @@ virtual_heatchill: - data title: vessel type: object - required: - - vessel - - temp - - purpose title: HeatChillStart_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: HeatChillStart_Result type: object required: @@ -1809,22 +1833,25 @@ virtual_heatchill: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChillStop_Feedback type: object goal: + additionalProperties: false properties: vessel: + additionalProperties: false properties: category: type: string @@ -1843,16 +1870,26 @@ virtual_heatchill: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1862,12 +1899,19 @@ virtual_heatchill: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1897,19 +1941,15 @@ virtual_heatchill: - data title: vessel type: object - required: - - vessel title: HeatChillStop_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: HeatChillStop_Result type: object required: @@ -1971,15 +2011,15 @@ virtual_heatchill: stir_speed: type: number required: - - status - - operation_mode - is_stirring - - stir_speed - - remaining_time - - progress + - max_stir_speed - max_temp - min_temp - - max_stir_speed + - operation_mode + - progress + - remaining_time + - status + - stir_speed type: object version: 1.0.0 virtual_multiway_valve: @@ -2027,7 +2067,8 @@ virtual_multiway_valve: required: - port_number type: object - result: {} + result: + type: boolean required: - goal title: is_at_port参数 @@ -2052,7 +2093,8 @@ virtual_multiway_valve: required: - position type: object - result: {} + result: + type: boolean required: - goal title: is_at_position参数 @@ -2073,7 +2115,8 @@ virtual_multiway_valve: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: is_at_pump_position参数 @@ -2193,42 +2236,41 @@ virtual_multiway_valve: type: object type: UniLabJsonCommand set_position: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -2237,42 +2279,41 @@ virtual_multiway_valve: type: object type: SendCmd set_valve_position: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -2402,13 +2443,13 @@ virtual_multiway_valve: valve_state: type: string required: - - status - - valve_state - - current_position - - target_position - current_port - - valve_position + - current_position - flow_path + - status + - target_position + - valve_position + - valve_state type: object version: 1.0.0 virtual_rotavap: @@ -2431,7 +2472,8 @@ virtual_rotavap: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -2452,43 +2494,22 @@ virtual_rotavap: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - 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 evaporate: feedback: current_device: current_device status: status + time_remaining: time_remaining + time_spent: time_spent goal: pressure: pressure + solvent: solvent stir_speed: stir_speed temp: temp time: time @@ -2520,19 +2541,22 @@ virtual_rotavap: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: - message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_device: type: string status: type: string time_remaining: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -2548,6 +2572,7 @@ virtual_rotavap: title: time_remaining type: object time_spent: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -2562,26 +2587,29 @@ virtual_rotavap: - nanosec title: time_spent type: object - required: - - status - - current_device - - time_spent - - time_remaining title: Evaporate_Feedback type: object goal: + additionalProperties: false properties: pressure: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solvent: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -2600,16 +2628,26 @@ virtual_rotavap: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2619,12 +2657,19 @@ virtual_rotavap: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2654,24 +2699,15 @@ virtual_rotavap: - data title: vessel type: object - required: - - vessel - - pressure - - temp - - time - - stir_speed - - solvent title: Evaporate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: Evaporate_Result type: object required: @@ -2725,7 +2761,7 @@ virtual_rotavap: config: properties: config: - type: string + type: object device_id: type: string required: [] @@ -2755,17 +2791,17 @@ virtual_rotavap: vacuum_pressure: type: number required: - - status - - rotavap_state - current_temp - - rotation_speed - - vacuum_pressure - evaporated_volume - - progress - - message - - max_temp - max_rotation_speed + - max_temp + - message + - progress - remaining_time + - rotation_speed + - rotavap_state + - status + - vacuum_pressure type: object version: 1.0.0 virtual_separator: @@ -2788,7 +2824,8 @@ virtual_separator: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -2809,44 +2846,21 @@ virtual_separator: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - 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 separate: feedback: - current_status: status progress: progress + status: status goal: from_vessel: from_vessel product_phase: product_phase + product_vessel: product_vessel purpose: purpose repeats: repeats separation_vessel: separation_vessel @@ -2857,7 +2871,10 @@ virtual_separator: stir_time: stir_time through: through to_vessel: to_vessel + vessel: vessel + volume: volume waste_phase_to_vessel: waste_phase_to_vessel + waste_vessel: waste_vessel goal_default: from_vessel: category: '' @@ -3010,26 +3027,30 @@ virtual_separator: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: Separate_Feedback type: object goal: + additionalProperties: false properties: from_vessel: + additionalProperties: false properties: category: type: string @@ -3048,16 +3069,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3067,12 +3098,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3105,6 +3143,7 @@ virtual_separator: product_phase: type: string product_vessel: + additionalProperties: false properties: category: type: string @@ -3123,16 +3162,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3142,12 +3191,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3184,6 +3240,7 @@ virtual_separator: minimum: -2147483648 type: integer separation_vessel: + additionalProperties: false properties: category: type: string @@ -3202,16 +3259,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3221,12 +3288,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3257,18 +3331,25 @@ virtual_separator: title: separation_vessel type: object settling_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solvent: type: string solvent_volume: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number stir_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number through: type: string to_vessel: + additionalProperties: false properties: category: type: string @@ -3287,16 +3368,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3306,12 +3397,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3342,6 +3440,7 @@ virtual_separator: title: to_vessel type: object vessel: + additionalProperties: false properties: category: type: string @@ -3360,16 +3459,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3379,12 +3488,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3417,6 +3533,7 @@ virtual_separator: volume: type: string waste_phase_to_vessel: + additionalProperties: false properties: category: type: string @@ -3435,16 +3552,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3454,12 +3581,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3490,6 +3624,7 @@ virtual_separator: title: waste_phase_to_vessel type: object waste_vessel: + additionalProperties: false properties: category: type: string @@ -3508,16 +3643,26 @@ virtual_separator: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3527,12 +3672,19 @@ virtual_separator: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3562,27 +3714,10 @@ virtual_separator: - data title: waste_vessel type: object - required: - - vessel - - purpose - - product_phase - - from_vessel - - separation_vessel - - to_vessel - - waste_phase_to_vessel - - product_vessel - - waste_vessel - - solvent - - solvent_volume - - volume - - through - - repeats - - stir_time - - stir_speed - - settling_time title: Separate_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -3590,10 +3725,6 @@ virtual_separator: type: string success: type: boolean - required: - - success - - message - - return_info title: Separate_Result type: object required: @@ -3645,7 +3776,7 @@ virtual_separator: config: properties: config: - type: string + type: object device_id: type: string required: [] @@ -3671,15 +3802,15 @@ virtual_separator: volume: type: number required: - - status - - separator_state - - volume - has_phases - - phase_separation - - stir_speed - - settling_time - - progress - message + - phase_separation + - progress + - separator_state + - settling_time + - status + - stir_speed + - volume type: object version: 1.0.0 virtual_solenoid_valve: @@ -3702,7 +3833,8 @@ virtual_solenoid_valve: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -3723,7 +3855,8 @@ virtual_solenoid_valve: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 @@ -3744,37 +3877,13 @@ virtual_solenoid_valve: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: is_closed参数 type: object 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-reset: feedback: {} goal: {} @@ -3819,31 +3928,28 @@ virtual_solenoid_valve: type: UniLabJsonCommand close: feedback: {} - goal: - command: CLOSED + goal: {} goal_default: {} handles: {} + placeholder_keys: {} result: - success: success + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -3856,26 +3962,25 @@ virtual_solenoid_valve: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -3890,32 +3995,31 @@ virtual_solenoid_valve: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -3924,44 +4028,41 @@ virtual_solenoid_valve: type: object type: StrSingleInput set_valve_position: - feedback: {} + feedback: + status: status goal: command: command goal_default: command: '' handles: {} + placeholder_keys: {} result: - message: message + return_info: return_info success: success - valve_position: valve_position schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: SendCmd_Feedback type: object goal: + additionalProperties: false properties: command: type: string - required: - - command title: SendCmd_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: SendCmd_Result type: object required: @@ -4016,10 +4117,10 @@ virtual_solenoid_valve: valve_state: type: string required: - - status - - valve_state - is_open + - status - valve_position + - valve_state type: object version: 1.0.0 virtual_solid_dispenser: @@ -4029,9 +4130,10 @@ virtual_solid_dispenser: action_value_mappings: add_solid: feedback: - current_status: status + current_status: current_status progress: progress goal: + amount: amount equiv: equiv event: event mass: mass @@ -4040,7 +4142,12 @@ virtual_solid_dispenser: rate_spec: rate_spec ratio: ratio reagent: reagent + stir: stir + stir_speed: stir_speed + time: time vessel: vessel + viscous: viscous + volume: volume goal_default: amount: '' equiv: '' @@ -4077,6 +4184,7 @@ virtual_solid_dispenser: viscous: false volume: '' handles: {} + placeholder_keys: {} result: message: message return_info: return_info @@ -4085,17 +4193,18 @@ virtual_solid_dispenser: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_status title: Add_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -4118,10 +4227,13 @@ virtual_solid_dispenser: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -4140,16 +4252,26 @@ virtual_solid_dispenser: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4159,12 +4281,19 @@ virtual_solid_dispenser: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4198,25 +4327,10 @@ virtual_solid_dispenser: type: boolean volume: type: string - required: - - vessel - - reagent - - volume - - mass - - amount - - time - - stir - - stir_speed - - viscous - - purpose - - event - - mol - - rate_spec - - equiv - - ratio title: Add_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -4224,10 +4338,6 @@ virtual_solid_dispenser: type: string success: type: boolean - required: - - success - - message - - return_info title: Add_Result type: object required: @@ -4250,7 +4360,8 @@ virtual_solid_dispenser: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -4275,7 +4386,8 @@ virtual_solid_dispenser: required: - reagent_name type: object - result: {} + result: + type: string required: - goal title: find_solid_reagent_bottle参数 @@ -4296,7 +4408,8 @@ virtual_solid_dispenser: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 @@ -4321,7 +4434,8 @@ virtual_solid_dispenser: required: - mass_str type: object - result: {} + result: + type: number required: - goal title: parse_mass_string参数 @@ -4346,37 +4460,13 @@ virtual_solid_dispenser: required: - mol_str type: object - result: {} + result: + type: number required: - goal title: parse_mol_string参数 type: object 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 module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser status_types: current_reagent: str @@ -4425,9 +4515,9 @@ virtual_solid_dispenser: total_operations: type: integer required: - - status - current_reagent - dispensed_amount + - status - total_operations type: object version: 1.0.0 @@ -4451,7 +4541,8 @@ virtual_stirrer: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -4472,40 +4563,18 @@ virtual_stirrer: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 type: object type: UniLabJsonCommandAsync - 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 start_stir: feedback: - status: status + current_speed: current_speed + current_status: current_status + progress: progress goal: purpose: purpose stir_speed: stir_speed @@ -4534,32 +4603,40 @@ virtual_stirrer: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_speed - - current_status title: StartStir_Feedback type: object goal: + additionalProperties: false properties: purpose: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -4578,16 +4655,26 @@ virtual_stirrer: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4597,12 +4684,19 @@ virtual_stirrer: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4632,13 +4726,10 @@ virtual_stirrer: - data title: vessel type: object - required: - - vessel - - stir_speed - - purpose title: StartStir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -4646,10 +4737,6 @@ virtual_stirrer: type: string success: type: boolean - required: - - success - - message - - return_info title: StartStir_Result type: object required: @@ -4661,9 +4748,13 @@ virtual_stirrer: feedback: status: status goal: + event: event settling_time: settling_time stir_speed: stir_speed stir_time: stir_time + time: time + time_spec: time_spec + vessel: vessel goal_default: event: '' settling_time: '' @@ -4692,34 +4783,42 @@ virtual_stirrer: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: Stir_Feedback type: object goal: + additionalProperties: false properties: event: type: string settling_time: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number stir_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string time_spec: type: string vessel: + additionalProperties: false properties: category: type: string @@ -4738,16 +4837,26 @@ virtual_stirrer: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4757,12 +4866,19 @@ virtual_stirrer: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4792,17 +4908,10 @@ virtual_stirrer: - data title: vessel type: object - required: - - vessel - - time - - event - - time_spec - - stir_time - - stir_speed - - settling_time title: Stir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -4810,10 +4919,6 @@ virtual_stirrer: type: string success: type: boolean - required: - - success - - message - - return_info title: Stir_Result type: object required: @@ -4823,7 +4928,8 @@ virtual_stirrer: type: Stir stop_stir: feedback: - status: status + current_status: current_status + progress: progress goal: vessel: vessel goal_default: @@ -4848,25 +4954,30 @@ virtual_stirrer: sample_id: '' type: '' handles: {} + placeholder_keys: {} result: + message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_status title: StopStir_Feedback type: object goal: + additionalProperties: false properties: vessel: + additionalProperties: false properties: category: type: string @@ -4885,16 +4996,26 @@ virtual_stirrer: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4904,12 +5025,19 @@ virtual_stirrer: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4939,11 +5067,10 @@ virtual_stirrer: - data title: vessel type: object - required: - - vessel title: StopStir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -4951,10 +5078,6 @@ virtual_stirrer: type: string success: type: boolean - required: - - success - - message - - return_info title: StopStir_Result type: object required: @@ -4966,7 +5089,7 @@ virtual_stirrer: status_types: current_speed: float current_vessel: str - device_info: dict + device_info: Dict[str, Any] is_stirring: bool max_speed: float min_speed: float @@ -5016,15 +5139,15 @@ virtual_stirrer: status: type: string required: - - status - - operation_mode - - current_vessel - current_speed + - current_vessel + - device_info - is_stirring - - remaining_time - max_speed - min_speed - - device_info + - operation_mode + - remaining_time + - status type: object version: 1.0.0 virtual_transfer_pump: @@ -5075,7 +5198,8 @@ virtual_transfer_pump: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -5172,7 +5296,8 @@ virtual_transfer_pump: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 @@ -5193,7 +5318,8 @@ virtual_transfer_pump: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: is_empty参数 @@ -5214,37 +5340,13 @@ virtual_transfer_pump: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: is_full参数 type: object 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-pull_plunger: feedback: {} goal: {} @@ -5359,38 +5461,44 @@ virtual_transfer_pump: max_velocity: 0.0 position: 0.0 handles: {} + placeholder_keys: {} result: message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - current_position - - progress title: SetPumpPosition_Feedback type: object goal: + additionalProperties: false properties: max_velocity: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number position: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - position - - max_velocity title: SetPumpPosition_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5398,10 +5506,6 @@ virtual_transfer_pump: type: string success: type: boolean - required: - - return_info - - success - - message title: SetPumpPosition_Result type: object required: @@ -5416,6 +5520,8 @@ virtual_transfer_pump: transferred_volume: transferred_volume goal: amount: amount + aspirate_velocity: aspirate_velocity + dispense_velocity: dispense_velocity from_vessel: from_vessel rinsing_repeats: rinsing_repeats rinsing_solvent: rinsing_solvent @@ -5437,27 +5543,31 @@ virtual_transfer_pump: viscous: false volume: 0.0 handles: {} + placeholder_keys: {} result: message: message + return_info: return_info success: success schema: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number transferred_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - transferred_volume - - current_status title: Transfer_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -5470,31 +5580,27 @@ virtual_transfer_pump: rinsing_solvent: type: string rinsing_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solid: type: boolean time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number to_vessel: type: string viscous: type: boolean volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - from_vessel - - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid title: Transfer_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5502,10 +5608,6 @@ virtual_transfer_pump: type: string success: type: boolean - required: - - success - - message - - return_info title: Transfer_Result type: object required: @@ -5558,12 +5660,12 @@ virtual_transfer_pump: transfer_rate: type: number required: - - status - - position - current_volume - max_velocity - - transfer_rate + - position - remaining_capacity + - status + - transfer_rate type: object version: 1.0.0 virtual_vacuum_pump: @@ -5586,7 +5688,8 @@ virtual_vacuum_pump: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: cleanup参数 @@ -5607,7 +5710,8 @@ virtual_vacuum_pump: properties: {} required: [] type: object - result: {} + result: + type: boolean required: - goal title: initialize参数 @@ -5660,26 +5764,25 @@ virtual_vacuum_pump: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -5692,26 +5795,25 @@ virtual_vacuum_pump: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -5726,32 +5828,31 @@ virtual_vacuum_pump: goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -5779,7 +5880,7 @@ virtual_vacuum_pump: config: properties: config: - type: string + type: object device_id: type: string required: [] @@ -5792,482 +5893,3 @@ virtual_vacuum_pump: - status type: object version: 1.0.0 -virtual_workbench: - category: - - virtual_device - class: - action_value_mappings: - auto-move_to_heating_station: - feedback: {} - goal: {} - goal_default: - material_number: null - handles: - input: - - data_key: material_number - data_source: handle - data_type: workbench_material - handler_key: material_input - label: 物料编号 - output: - - data_key: station_id - data_source: executor - data_type: workbench_station - handler_key: heating_station_output - label: 加热台ID - - data_key: material_number - data_source: executor - data_type: workbench_material - handler_key: material_number_output - label: 物料编号 - placeholder_keys: {} - result: {} - schema: - description: 将物料从An位置移动到空闲加热台,返回分配的加热台ID - properties: - feedback: {} - goal: - properties: - material_number: - description: 物料编号,1-5,物料ID自动生成为A{n} - type: integer - required: - - material_number - type: object - result: - $defs: - LabSample: - properties: - extra: - additionalProperties: true - title: Extra - type: object - oss_path: - title: Oss Path - type: string - sample_uuid: - title: Sample Uuid - type: string - required: - - sample_uuid - - oss_path - - extra - title: LabSample - type: object - description: move_to_heating_station 返回类型 - properties: - material_id: - title: Material Id - type: string - material_number: - title: Material Number - type: integer - message: - title: Message - type: string - station_id: - description: 分配的加热台ID - title: Station Id - type: integer - success: - title: Success - type: boolean - unilabos_samples: - items: - $ref: '#/$defs/LabSample' - title: Unilabos Samples - type: array - required: - - success - - station_id - - material_id - - material_number - - message - - unilabos_samples - title: MoveToHeatingStationResult - type: object - required: - - goal - title: move_to_heating_station参数 - type: object - type: UniLabJsonCommand - auto-move_to_output: - feedback: {} - goal: {} - goal_default: - material_number: null - station_id: null - handles: - input: - - data_key: station_id - data_source: handle - data_type: workbench_station - handler_key: output_station_input - label: 加热台ID - - data_key: material_number - data_source: handle - data_type: workbench_material - handler_key: output_material_input - label: 物料编号 - placeholder_keys: {} - result: {} - schema: - description: 将物料从加热台移动到输出位置Cn - properties: - feedback: {} - goal: - properties: - material_number: - description: 物料编号,用于确定输出位置Cn - type: integer - station_id: - description: 加热台ID,1-3,从上一节点传入 - type: integer - required: - - station_id - - material_number - type: object - result: - $defs: - LabSample: - properties: - extra: - additionalProperties: true - title: Extra - type: object - oss_path: - title: Oss Path - type: string - sample_uuid: - title: Sample Uuid - type: string - required: - - sample_uuid - - oss_path - - extra - title: LabSample - type: object - description: move_to_output 返回类型 - properties: - material_id: - title: Material Id - type: string - station_id: - title: Station Id - type: integer - success: - title: Success - type: boolean - unilabos_samples: - items: - $ref: '#/$defs/LabSample' - title: Unilabos Samples - type: array - required: - - success - - station_id - - material_id - - unilabos_samples - title: MoveToOutputResult - type: object - required: - - goal - title: move_to_output参数 - type: object - type: UniLabJsonCommand - auto-prepare_materials: - feedback: {} - goal: {} - goal_default: - count: 5 - handles: - output: - - data_key: material_1 - data_source: executor - data_type: workbench_material - handler_key: channel_1 - label: 实验1 - - data_key: material_2 - data_source: executor - data_type: workbench_material - handler_key: channel_2 - label: 实验2 - - data_key: material_3 - data_source: executor - data_type: workbench_material - handler_key: channel_3 - label: 实验3 - - data_key: material_4 - data_source: executor - data_type: workbench_material - handler_key: channel_4 - label: 实验4 - - data_key: material_5 - data_source: executor - data_type: workbench_material - handler_key: channel_5 - label: 实验5 - placeholder_keys: {} - result: {} - schema: - description: 批量准备物料 - 虚拟起始节点,生成A1-A5物料,输出5个handle供后续节点使用 - properties: - feedback: {} - goal: - properties: - count: - default: 5 - description: 待生成的物料数量,默认5 (生成 A1-A5) - type: integer - required: [] - type: object - result: - $defs: - LabSample: - properties: - extra: - additionalProperties: true - title: Extra - type: object - oss_path: - title: Oss Path - type: string - sample_uuid: - title: Sample Uuid - type: string - required: - - sample_uuid - - oss_path - - extra - title: LabSample - type: object - description: prepare_materials 返回类型 - 批量准备物料 - properties: - count: - title: Count - type: integer - material_1: - title: Material 1 - type: integer - material_2: - title: Material 2 - type: integer - material_3: - title: Material 3 - type: integer - material_4: - title: Material 4 - type: integer - material_5: - title: Material 5 - type: integer - message: - title: Message - type: string - success: - title: Success - type: boolean - unilabos_samples: - items: - $ref: '#/$defs/LabSample' - title: Unilabos Samples - type: array - required: - - success - - count - - material_1 - - material_2 - - material_3 - - material_4 - - material_5 - - message - - unilabos_samples - title: PrepareMaterialsResult - type: object - required: - - goal - title: prepare_materials参数 - type: object - type: UniLabJsonCommand - auto-start_heating: - always_free: true - feedback: {} - goal: {} - goal_default: - material_number: null - station_id: null - handles: - input: - - data_key: station_id - data_source: handle - data_type: workbench_station - handler_key: station_id_input - label: 加热台ID - - data_key: material_number - data_source: handle - data_type: workbench_material - handler_key: material_number_input - label: 物料编号 - output: - - data_key: station_id - data_source: executor - data_type: workbench_station - handler_key: heating_done_station - label: 加热完成-加热台ID - - data_key: material_number - data_source: executor - data_type: workbench_material - handler_key: heating_done_material - label: 加热完成-物料编号 - placeholder_keys: {} - result: {} - schema: - description: 启动指定加热台的加热程序 - properties: - feedback: {} - goal: - properties: - material_number: - description: 物料编号,从上一节点传入 - type: integer - station_id: - description: 加热台ID,1-3,从上一节点传入 - type: integer - required: - - station_id - - material_number - type: object - result: - $defs: - LabSample: - properties: - extra: - additionalProperties: true - title: Extra - type: object - oss_path: - title: Oss Path - type: string - sample_uuid: - title: Sample Uuid - type: string - required: - - sample_uuid - - oss_path - - extra - title: LabSample - type: object - description: start_heating 返回类型 - properties: - material_id: - title: Material Id - type: string - material_number: - title: Material Number - type: integer - message: - title: Message - type: string - station_id: - title: Station Id - type: integer - success: - title: Success - type: boolean - unilabos_samples: - items: - $ref: '#/$defs/LabSample' - title: Unilabos Samples - type: array - required: - - success - - station_id - - material_id - - material_number - - message - - unilabos_samples - title: StartHeatingResult - type: object - required: - - goal - title: start_heating参数 - type: object - type: UniLabJsonCommand - module: unilabos.devices.virtual.workbench:VirtualWorkbench - status_types: - active_tasks_count: int - arm_current_task: str - arm_state: str - heating_station_1_material: str - heating_station_1_progress: float - heating_station_1_state: str - heating_station_2_material: str - heating_station_2_progress: float - heating_station_2_state: str - heating_station_3_material: str - heating_station_3_progress: float - heating_station_3_state: str - message: str - status: str - type: python - config_info: [] - description: Virtual Workbench with 1 robotic arm and 3 heating stations for concurrent - material processing - handles: [] - icon: '' - init_param_schema: - config: - properties: - config: - type: string - device_id: - type: string - required: [] - type: object - data: - properties: - active_tasks_count: - type: integer - arm_current_task: - type: string - arm_state: - type: string - heating_station_1_material: - type: string - heating_station_1_progress: - type: number - heating_station_1_state: - type: string - heating_station_2_material: - type: string - heating_station_2_progress: - type: number - heating_station_2_state: - type: string - heating_station_3_material: - type: string - heating_station_3_progress: - type: number - heating_station_3_state: - type: string - message: - type: string - status: - type: string - required: - - status - - arm_state - - arm_current_task - - heating_station_1_state - - heating_station_1_material - - heating_station_1_progress - - heating_station_2_state - - heating_station_2_material - - heating_station_2_progress - - heating_station_3_state - - heating_station_3_material - - heating_station_3_progress - - active_tasks_count - - message - type: object - version: 1.0.0 diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index e1be7f3d..87a2fabe 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -59,16 +59,17 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: AGVTransfer_Feedback type: object goal: + additionalProperties: false properties: from_repo: + additionalProperties: false properties: category: type: string @@ -87,16 +88,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -106,12 +117,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -144,6 +162,7 @@ workstation: from_repo_position: type: string to_repo: + additionalProperties: false properties: category: type: string @@ -162,16 +181,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -181,12 +210,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -218,22 +254,15 @@ workstation: type: object to_repo_position: type: string - required: - - from_repo - - from_repo_position - - to_repo - - to_repo_position title: AGVTransfer_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: AGVTransfer_Result type: object required: @@ -319,17 +348,18 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_status title: Add_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -352,10 +382,13 @@ workstation: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -374,16 +407,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -393,12 +436,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -432,25 +482,10 @@ workstation: type: boolean volume: type: string - required: - - vessel - - reagent - - volume - - mass - - amount - - time - - stir - - stir_speed - - viscous - - purpose - - event - - mol - - rate_spec - - equiv - - ratio title: Add_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -458,10 +493,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Add_Result type: object required: @@ -528,23 +559,27 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: AdjustPH_Feedback type: object goal: + additionalProperties: false properties: ph_value: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number reagent: type: string vessel: + additionalProperties: false properties: category: type: string @@ -563,16 +598,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -582,12 +627,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -617,13 +669,10 @@ workstation: - data title: vessel type: object - required: - - vessel - - ph_value - - reagent title: AdjustPH_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -631,10 +680,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: AdjustPH_Result type: object required: @@ -693,31 +738,41 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number current_status: type: string current_temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_speed - - current_temp - - current_status title: Centrifuge_Feedback type: object goal: + additionalProperties: false properties: speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -736,16 +791,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -755,12 +820,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -790,14 +862,10 @@ workstation: - data title: vessel type: object - required: - - vessel - - speed - - time - - temp title: Centrifuge_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -805,10 +873,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Centrifuge_Result type: object required: @@ -874,12 +938,14 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_device: type: string status: type: string time_remaining: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -895,6 +961,7 @@ workstation: title: time_remaining type: object time_spent: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -909,14 +976,10 @@ workstation: - nanosec title: time_spent type: object - required: - - status - - current_device - - time_spent - - time_remaining title: Clean_Feedback type: object goal: + additionalProperties: false properties: repeats: maximum: 2147483647 @@ -925,8 +988,11 @@ workstation: solvent: type: string temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -945,16 +1011,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -964,12 +1040,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1000,24 +1083,18 @@ workstation: title: vessel type: object volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - vessel - - solvent - - volume - - temp - - repeats title: Clean_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: Clean_Result type: object required: @@ -1083,17 +1160,18 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: CleanVessel_Feedback type: object goal: + additionalProperties: false properties: repeats: maximum: 2147483647 @@ -1102,8 +1180,11 @@ workstation: solvent: type: string temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -1122,16 +1203,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1141,12 +1232,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1177,16 +1275,13 @@ workstation: title: vessel type: object volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - vessel - - solvent - - volume - - temp - - repeats title: CleanVessel_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -1194,10 +1289,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: CleanVessel_Result type: object required: @@ -1280,17 +1371,18 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: Dissolve_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -1305,12 +1397,15 @@ workstation: solvent: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: type: string time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -1329,16 +1424,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1348,12 +1453,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1385,21 +1497,10 @@ workstation: type: object volume: type: string - required: - - vessel - - solvent - - volume - - amount - - temp - - time - - stir_speed - - mass - - mol - - reagent - - event title: Dissolve_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -1407,10 +1508,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Dissolve_Result type: object required: @@ -1465,21 +1562,23 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: Dry_Feedback type: object goal: + additionalProperties: false properties: compound: type: string vessel: + additionalProperties: false properties: category: type: string @@ -1498,16 +1597,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1517,12 +1626,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1552,12 +1668,10 @@ workstation: - data title: vessel type: object - required: - - compound - - vessel title: Dry_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -1565,10 +1679,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Dry_Result type: object required: @@ -1623,12 +1733,14 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_device: type: string status: type: string time_remaining: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -1644,6 +1756,7 @@ workstation: title: time_remaining type: object time_spent: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -1658,18 +1771,15 @@ workstation: - nanosec title: time_spent type: object - required: - - status - - current_device - - time_spent - - time_remaining title: EvacuateAndRefill_Feedback type: object goal: + additionalProperties: false properties: gas: type: string vessel: + additionalProperties: false properties: category: type: string @@ -1688,16 +1798,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1707,12 +1827,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1742,20 +1869,15 @@ workstation: - data title: vessel type: object - required: - - vessel - - gas title: EvacuateAndRefill_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: EvacuateAndRefill_Result type: object required: @@ -1823,12 +1945,14 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_device: type: string status: type: string time_remaining: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -1844,6 +1968,7 @@ workstation: title: time_remaining type: object time_spent: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -1858,26 +1983,29 @@ workstation: - nanosec title: time_spent type: object - required: - - status - - current_device - - time_spent - - time_remaining title: Evaporate_Feedback type: object goal: + additionalProperties: false properties: pressure: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solvent: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -1896,16 +2024,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1915,12 +2053,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -1950,24 +2095,15 @@ workstation: - data title: vessel type: object - required: - - vessel - - pressure - - temp - - time - - stir_speed - - solvent title: Evaporate_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: Evaporate_Result type: object required: @@ -2062,27 +2198,31 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string current_temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number filtered_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_temp - - filtered_volume - - current_status title: Filter_Feedback type: object goal: + additionalProperties: false properties: continue_heatchill: type: boolean filtrate_vessel: + additionalProperties: false properties: category: type: string @@ -2101,16 +2241,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2120,12 +2270,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2158,10 +2315,15 @@ workstation: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -2180,16 +2342,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2199,12 +2371,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2235,18 +2414,13 @@ workstation: title: vessel type: object volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - vessel - - filtrate_vessel - - stir - - stir_speed - - temp - - continue_heatchill - - volume title: Filter_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -2254,10 +2428,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Filter_Result type: object required: @@ -2376,17 +2546,18 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: FilterThrough_Feedback type: object goal: + additionalProperties: false properties: eluting_repeats: maximum: 2147483647 @@ -2395,8 +2566,11 @@ workstation: eluting_solvent: type: string eluting_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number filter_through: + additionalProperties: false properties: category: type: string @@ -2415,16 +2589,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2434,12 +2618,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2470,6 +2661,7 @@ workstation: title: filter_through type: object from_vessel: + additionalProperties: false properties: category: type: string @@ -2488,16 +2680,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2507,12 +2709,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2543,8 +2752,11 @@ workstation: title: from_vessel type: object residence_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number to_vessel: + additionalProperties: false properties: category: type: string @@ -2563,16 +2775,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2582,12 +2804,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2617,17 +2846,10 @@ workstation: - data title: to_vessel type: object - required: - - from_vessel - - to_vessel - - filter_through - - eluting_solvent - - eluting_volume - - eluting_repeats - - residence_time title: FilterThrough_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -2635,10 +2857,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: FilterThrough_Result type: object required: @@ -2709,14 +2927,14 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChill_Feedback type: object goal: + additionalProperties: false properties: pressure: type: string @@ -2727,8 +2945,12 @@ workstation: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp_spec: type: string @@ -2737,6 +2959,7 @@ workstation: time_spec: type: string vessel: + additionalProperties: false properties: category: type: string @@ -2755,16 +2978,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2774,12 +3007,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2809,20 +3049,10 @@ workstation: - data title: vessel type: object - required: - - vessel - - temp - - time - - temp_spec - - time_spec - - pressure - - reflux_solvent - - stir - - stir_speed - - purpose title: HeatChill_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -2830,10 +3060,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: HeatChill_Result type: object required: @@ -2890,20 +3116,23 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChillStart_Feedback type: object goal: + additionalProperties: false properties: purpose: type: string temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -2922,16 +3151,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2941,12 +3180,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -2976,21 +3222,15 @@ workstation: - data title: vessel type: object - required: - - vessel - - temp - - purpose title: HeatChillStart_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: HeatChillStart_Result type: object required: @@ -3043,16 +3283,17 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: HeatChillStop_Feedback type: object goal: + additionalProperties: false properties: vessel: + additionalProperties: false properties: category: type: string @@ -3071,16 +3312,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3090,12 +3341,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3125,19 +3383,15 @@ workstation: - data title: vessel type: object - required: - - vessel title: HeatChillStop_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: HeatChillStop_Result type: object required: @@ -3194,23 +3448,25 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: Hydrogenate_Feedback type: object goal: + additionalProperties: false properties: temp: type: string time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -3229,16 +3485,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3248,12 +3514,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3283,13 +3556,10 @@ workstation: - data title: vessel type: object - required: - - temp - - time - - vessel title: Hydrogenate_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -3297,10 +3567,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Hydrogenate_Result type: object required: @@ -3416,12 +3682,14 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_device: type: string status: type: string time_remaining: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -3437,6 +3705,7 @@ workstation: title: time_remaining type: object time_spent: + additionalProperties: false properties: nanosec: maximum: 4294967295 @@ -3451,22 +3720,21 @@ workstation: - nanosec title: time_spent type: object - required: - - status - - current_device - - time_spent - - time_remaining title: PumpTransfer_Feedback type: object goal: + additionalProperties: false properties: amount: type: string event: type: string flowrate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number from_vessel: + additionalProperties: false properties: category: type: string @@ -3485,16 +3753,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3504,12 +3782,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3548,14 +3833,19 @@ workstation: rinsing_solvent: type: string rinsing_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solid: type: boolean through: type: string time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number to_vessel: + additionalProperties: false properties: category: type: string @@ -3574,16 +3864,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3593,12 +3893,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3629,38 +3936,24 @@ workstation: title: to_vessel type: object transfer_flowrate: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number viscous: type: boolean volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - from_vessel - - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid - - flowrate - - transfer_flowrate - - rate_spec - - event - - through title: PumpTransfer_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: PumpTransfer_Result type: object required: @@ -3731,17 +4024,18 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: Recrystallize_Feedback type: object goal: + additionalProperties: false properties: ratio: type: string @@ -3750,6 +4044,7 @@ workstation: solvent2: type: string vessel: + additionalProperties: false properties: category: type: string @@ -3768,16 +4063,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3787,12 +4092,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3824,15 +4136,10 @@ workstation: type: object volume: type: string - required: - - ratio - - solvent1 - - solvent2 - - vessel - - volume title: Recrystallize_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -3840,10 +4147,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Recrystallize_Result type: object required: @@ -3890,21 +4193,23 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: ResetHandling_Feedback type: object goal: + additionalProperties: false properties: solvent: type: string vessel: + additionalProperties: false properties: category: type: string @@ -3923,16 +4228,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3942,12 +4257,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -3977,12 +4299,10 @@ workstation: - data title: vessel type: object - required: - - solvent - - vessel title: ResetHandling_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -3990,10 +4310,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: ResetHandling_Result type: object required: @@ -4087,21 +4403,23 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: RunColumn_Feedback type: object goal: + additionalProperties: false properties: column: type: string from_vessel: + additionalProperties: false properties: category: type: string @@ -4120,16 +4438,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4139,12 +4467,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4187,6 +4522,7 @@ workstation: solvent2: type: string to_vessel: + additionalProperties: false properties: category: type: string @@ -4205,16 +4541,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4224,12 +4570,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4259,19 +4612,10 @@ workstation: - data title: to_vessel type: object - required: - - from_vessel - - to_vessel - - column - - rf - - pct1 - - pct2 - - solvent1 - - solvent2 - - ratio title: RunColumn_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -4279,10 +4623,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: RunColumn_Result type: object required: @@ -4495,19 +4835,21 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: Separate_Feedback type: object goal: + additionalProperties: false properties: from_vessel: + additionalProperties: false properties: category: type: string @@ -4526,16 +4868,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4545,12 +4897,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4583,6 +4942,7 @@ workstation: product_phase: type: string product_vessel: + additionalProperties: false properties: category: type: string @@ -4601,16 +4961,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4620,12 +4990,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4662,6 +5039,7 @@ workstation: minimum: -2147483648 type: integer separation_vessel: + additionalProperties: false properties: category: type: string @@ -4680,16 +5058,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4699,12 +5087,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4735,18 +5130,25 @@ workstation: title: separation_vessel type: object settling_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solvent: type: string solvent_volume: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number stir_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number through: type: string to_vessel: + additionalProperties: false properties: category: type: string @@ -4765,16 +5167,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4784,12 +5196,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4820,6 +5239,7 @@ workstation: title: to_vessel type: object vessel: + additionalProperties: false properties: category: type: string @@ -4838,16 +5258,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4857,12 +5287,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4895,6 +5332,7 @@ workstation: volume: type: string waste_phase_to_vessel: + additionalProperties: false properties: category: type: string @@ -4913,16 +5351,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4932,12 +5380,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -4968,6 +5423,7 @@ workstation: title: waste_phase_to_vessel type: object waste_vessel: + additionalProperties: false properties: category: type: string @@ -4986,16 +5442,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5005,12 +5471,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5040,27 +5513,10 @@ workstation: - data title: waste_vessel type: object - required: - - vessel - - purpose - - product_phase - - from_vessel - - separation_vessel - - to_vessel - - waste_phase_to_vessel - - product_vessel - - waste_vessel - - solvent - - solvent_volume - - volume - - through - - repeats - - stir_time - - stir_speed - - settling_time title: Separate_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5068,10 +5524,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Separate_Result type: object required: @@ -5128,26 +5580,31 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_speed - - current_status title: StartStir_Feedback type: object goal: + additionalProperties: false properties: purpose: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number vessel: + additionalProperties: false properties: category: type: string @@ -5166,16 +5623,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5185,12 +5652,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5220,13 +5694,10 @@ workstation: - data title: vessel type: object - required: - - vessel - - stir_speed - - purpose title: StartStir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5234,10 +5705,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: StartStir_Result type: object required: @@ -5302,28 +5769,33 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: status: type: string - required: - - status title: Stir_Feedback type: object goal: + additionalProperties: false properties: event: type: string settling_time: type: string stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number stir_time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string time_spec: type: string vessel: + additionalProperties: false properties: category: type: string @@ -5342,16 +5814,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5361,12 +5843,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5396,17 +5885,10 @@ workstation: - data title: vessel type: object - required: - - vessel - - time - - event - - time_spec - - stir_time - - stir_speed - - settling_time title: Stir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5414,10 +5896,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Stir_Result type: object required: @@ -5470,19 +5948,21 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - current_status title: StopStir_Feedback type: object goal: + additionalProperties: false properties: vessel: + additionalProperties: false properties: category: type: string @@ -5501,16 +5981,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5520,12 +6010,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5555,11 +6052,10 @@ workstation: - data title: vessel type: object - required: - - vessel title: StopStir_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5567,10 +6063,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: StopStir_Result type: object required: @@ -5638,20 +6130,22 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: current_status: type: string progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number transferred_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - progress - - transferred_volume - - current_status title: Transfer_Feedback type: object goal: + additionalProperties: false properties: amount: type: string @@ -5664,31 +6158,27 @@ workstation: rinsing_solvent: type: string rinsing_volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number solid: type: boolean time: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number to_vessel: type: string viscous: type: boolean volume: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number - required: - - from_vessel - - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid title: Transfer_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -5696,10 +6186,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: Transfer_Result type: object required: @@ -5807,21 +6293,23 @@ workstation: description: '' properties: feedback: + additionalProperties: false properties: progress: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number status: type: string - required: - - status - - progress title: WashSolid_Feedback type: object goal: + additionalProperties: false properties: event: type: string filtrate_vessel: + additionalProperties: false properties: category: type: string @@ -5840,16 +6328,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5859,12 +6357,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5907,12 +6412,17 @@ workstation: stir: type: boolean stir_speed: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number temp: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number time: type: string vessel: + additionalProperties: false properties: category: type: string @@ -5931,16 +6441,26 @@ workstation: parent: type: string pose: + additionalProperties: false properties: orientation: + additionalProperties: false properties: w: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5950,12 +6470,19 @@ workstation: title: orientation type: object position: + additionalProperties: false properties: x: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number y: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number z: + maximum: 1.7976931348623157e+308 + minimum: -1.7976931348623157e+308 type: number required: - x @@ -5989,23 +6516,10 @@ workstation: type: string volume_spec: type: string - required: - - vessel - - solvent - - volume - - filtrate_vessel - - temp - - stir - - stir_speed - - time - - repeats - - volume_spec - - repeats_spec - - mass - - event title: WashSolid_Goal type: object result: + additionalProperties: false properties: message: type: string @@ -6013,10 +6527,6 @@ workstation: type: string success: type: boolean - required: - - success - - message - - return_info title: WashSolid_Result type: object required: @@ -6035,7 +6545,7 @@ workstation: config: properties: deck: - type: string + type: object protocol_type: items: type: string diff --git a/unilabos/registry/devices/xrd_d7mate.yaml b/unilabos/registry/devices/xrd_d7mate.yaml index cbdf8aa8..2b49ae55 100644 --- a/unilabos/registry/devices/xrd_d7mate.yaml +++ b/unilabos/registry/devices/xrd_d7mate.yaml @@ -45,31 +45,6 @@ xrd_d7mate: title: connect参数 type: object 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: feedback: {} goal: {} @@ -85,11 +60,14 @@ xrd_d7mate: goal: properties: params: - type: string + anyOf: + - type: string + - type: object required: - params type: object - result: {} + result: + type: object required: - goal title: start_from_string参数 @@ -105,21 +83,18 @@ xrd_d7mate: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -130,38 +105,38 @@ xrd_d7mate: get_sample_down: feedback: {} goal: - sample_station: 1 + int_input: int_input + sample_station: sample_station goal_default: int_input: 0 handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: IntSingleInput_Feedback type: object goal: + additionalProperties: false properties: int_input: maximum: 2147483647 minimum: -2147483648 type: integer - required: - - int_input title: IntSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: IntSingleInput_Result type: object required: @@ -179,21 +154,18 @@ xrd_d7mate: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -211,21 +183,18 @@ xrd_d7mate: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -238,26 +207,25 @@ xrd_d7mate: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -274,42 +242,35 @@ xrd_d7mate: sample_id: '' start_theta: 10.0 goal_default: - end_theta: 80.0 - exp_time: 0.5 - increment: 0.02 - sample_id: Sample001 - start_theta: 10.0 + end_theta: null + exp_time: null + increment: null + sample_id: null + start_theta: null handles: {} + placeholder_keys: {} result: {} schema: description: 送样完成后,发送样品信息和采集参数 properties: feedback: - properties: {} - required: [] title: SampleReadyInput_Feedback - type: object goal: properties: end_theta: description: 结束角度(≥5.5°,且必须大于start_theta) - minimum: 5.5 type: number exp_time: description: 曝光时间(0.1-5.0秒) - maximum: 5.0 - minimum: 0.1 type: number increment: description: 角度增量(≥0.005) - minimum: 0.005 type: number sample_id: description: 样品标识符 type: string start_theta: description: 起始角度(≥5°) - minimum: 5.0 type: number required: - sample_id @@ -320,19 +281,11 @@ xrd_d7mate: title: SampleReadyInput_Goal type: object result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success title: SampleReadyInput_Result type: object required: - goal - title: SampleReadyInput + title: send_sample_ready参数 type: object type: UniLabJsonCommand set_power_off: @@ -340,26 +293,25 @@ xrd_d7mate: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -372,26 +324,25 @@ xrd_d7mate: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -405,18 +356,16 @@ xrd_d7mate: current: 30.0 voltage: 40.0 goal_default: - current: 30.0 - voltage: 40.0 + current: null + voltage: null handles: {} + placeholder_keys: {} result: {} schema: description: 设置高压电源电压和电流 properties: feedback: - properties: {} - required: [] title: VoltageCurrentInput_Feedback - type: object goal: properties: current: @@ -431,19 +380,11 @@ xrd_d7mate: title: VoltageCurrentInput_Goal type: object result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success title: VoltageCurrentInput_Result type: object required: - goal - title: VoltageCurrentInput + title: set_voltage_current参数 type: object type: UniLabJsonCommand start: @@ -453,11 +394,12 @@ xrd_d7mate: end_theta: 80.0 exp_time: 0.1 increment: 0.05 - sample_id: 样品名称 + sample_id: '' start_theta: 10.0 string: '' wait_minutes: 3.0 handles: {} + placeholder_keys: {} result: {} schema: description: 启动自动模式→上样→等待→样品准备→监控→检测下样位→执行下样流程。 @@ -466,54 +408,42 @@ xrd_d7mate: goal: properties: end_theta: + default: 80.0 description: 结束角度(≥5.5°,且必须大于start_theta) - minimum: 5.5 - type: string + type: number exp_time: + default: 0.1 description: 曝光时间(0.1-5.0秒) - maximum: 5.0 - minimum: 0.1 - type: string + type: number increment: + default: 0.05 description: 角度增量(≥0.005) - minimum: 0.005 - type: string + type: number sample_id: + default: '' description: 样品标识符 type: string start_theta: + default: 10.0 description: 起始角度(≥5°) - minimum: 5.0 - type: string + type: number string: + default: '' description: 字符串格式的参数输入,如果提供则优先解析使用 type: string wait_minutes: + default: 3.0 description: 允许上样后等待分钟数 - minimum: 0.0 type: number - required: - - sample_id - - start_theta - - end_theta - - increment - - exp_time + required: [] title: StartWorkflow_Goal type: object result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success title: StartWorkflow_Result type: object required: - goal - title: StartWorkflow + title: start参数 type: object type: UniLabJsonCommand start_auto_mode: @@ -521,17 +451,15 @@ xrd_d7mate: goal: status: true goal_default: - status: true + status: null handles: {} + placeholder_keys: {} result: {} schema: description: 启动或停止自动模式 properties: feedback: - properties: {} - required: [] title: BoolSingleInput_Feedback - type: object goal: properties: status: @@ -542,25 +470,16 @@ xrd_d7mate: title: BoolSingleInput_Goal type: object result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success title: BoolSingleInput_Result type: object required: - goal - title: BoolSingleInput + title: start_auto_mode参数 type: object type: UniLabJsonCommand module: unilabos.devices.xrd_d7mate.xrd_d7mate:XRDClient status_types: current_acquire_data: dict - sample_down: dict sample_request: dict sample_status: dict type: python @@ -586,16 +505,13 @@ xrd_d7mate: properties: current_acquire_data: type: object - sample_down: - type: object sample_request: type: object sample_status: type: object required: - - sample_request - current_acquire_data + - sample_request - sample_status - - sample_down type: object version: 1.0.0 diff --git a/unilabos/registry/devices/zhida_gcms.yaml b/unilabos/registry/devices/zhida_gcms.yaml index 607af9b9..37adbd79 100644 --- a/unilabos/registry/devices/zhida_gcms.yaml +++ b/unilabos/registry/devices/zhida_gcms.yaml @@ -8,26 +8,25 @@ zhida_gcms: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -77,31 +76,6 @@ zhida_gcms: title: connect参数 type: object 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: feedback: {} goal: {} @@ -112,21 +86,18 @@ zhida_gcms: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -144,21 +115,18 @@ zhida_gcms: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -176,21 +144,18 @@ zhida_gcms: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -203,26 +168,25 @@ zhida_gcms: goal: {} goal_default: {} handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Feedback type: object goal: - properties: {} - required: [] + additionalProperties: true title: EmptyIn_Goal type: object result: + additionalProperties: false properties: return_info: type: string - required: - - return_info title: EmptyIn_Result type: object required: @@ -234,35 +198,35 @@ zhida_gcms: feedback: {} goal: string: string + text: text goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -273,36 +237,36 @@ zhida_gcms: start_with_csv_file: feedback: {} goal: + csv_file_path: csv_file_path string: string goal_default: string: '' handles: {} - result: {} + placeholder_keys: {} + result: + return_info: return_info + success: success schema: description: '' properties: feedback: - properties: {} - required: [] + additionalProperties: true title: StrSingleInput_Feedback type: object goal: + additionalProperties: false properties: string: type: string - required: - - string title: StrSingleInput_Goal type: object result: + additionalProperties: false properties: return_info: type: string success: type: boolean - required: - - return_info - - success title: StrSingleInput_Result type: object required: @@ -343,8 +307,8 @@ zhida_gcms: version: type: object required: - - status - methods + - status - version type: object version: 1.0.0 diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index 02d80cca..8841764c 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -1,20 +1,58 @@ +""" +统一注册表系统 + +合并了原 Registry (YAML 加载) 和 DecoratorRegistry (装饰器/AST 扫描) 的功能, +提供单一入口来构建、验证和查询设备/资源注册表。 +""" + import copy +import importlib +import inspect import io import os import sys -import inspect -import importlib import threading +import time import traceback from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path -from typing import Any, Dict, List, Union, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union import yaml +from unilabos_msgs.action import EmptyIn, ResourceCreateFromOuter, ResourceCreateFromOuterEasy from unilabos_msgs.msg import Resource from unilabos.config.config import BasicConfig +from unilabos.registry.decorators import ( + get_device_meta, + get_action_meta, + get_resource_meta, + has_action_decorator, + get_all_registered_devices, + get_all_registered_resources, + is_not_action, + is_always_free, + get_topic_config, +) +from unilabos.registry.utils import ( + ROSMsgNotFound, + parse_docstring, + get_json_schema_type, + parse_type_node, + type_node_to_schema, + resolve_type_object, + type_to_schema, + detect_slot_type, + detect_placeholder_keys, + normalize_ast_handles, + normalize_ast_action_handles, + wrap_action_schema, + preserve_field_descriptions, + resolve_method_params_via_import, + SIMPLE_TYPE_MAP, +) from unilabos.resources.graphio import resource_plr_to_ulab, tree_to_list +from unilabos.resources.resource_tracker import ResourceTreeSet from unilabos.ros.msgs.message_converter import ( msg_converter_manager, ros_action_to_json_schema, @@ -23,234 +61,1427 @@ from unilabos.ros.msgs.message_converter import ( ) from unilabos.utils import logger from unilabos.utils.decorator import singleton -from unilabos.utils.import_manager import get_enhanced_class_info, get_class +from unilabos.utils.cls_creator import import_class +from unilabos.utils.import_manager import get_enhanced_class_info from unilabos.utils.type_check import NoAliasDumper +from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance +from msgcenterpy.instances.ros2_instance import ROS2MessageInstance -DEFAULT_PATHS = [Path(__file__).absolute().parent] - - -class ROSMsgNotFound(Exception): - pass +_module_hash_cache: Dict[str, Optional[str]] = {} @singleton class Registry: + """ + 统一注册表。 + + 核心流程: + 1. AST 静态扫描 @device/@resource 装饰器 (快速, 无需 import) + 2. 加载 YAML 注册表 (兼容旧格式) + 3. 设置 host_node 内置设备 + 4. verify & resolve (实际 import 验证 + 类型解析) + """ + def __init__(self, registry_paths=None): import ctypes try: + # noinspection PyUnusedImports import unilabos_msgs except ImportError: logger.error("[UniLab Registry] unilabos_msgs模块未找到,请确保已根据官方文档安装unilabos_msgs包。") sys.exit(1) try: ctypes.CDLL(str(Path(unilabos_msgs.__file__).parent / "unilabos_msgs_s__rosidl_typesupport_c.pyd")) - except OSError as e: + except OSError: pass - self.registry_paths = DEFAULT_PATHS.copy() # 使用copy避免修改默认值 + self.registry_paths = [Path(__file__).absolute().parent] if registry_paths: self.registry_paths.extend(registry_paths) - self.ResourceCreateFromOuter = self._replace_type_with_class( - "ResourceCreateFromOuter", "host_node", f"动作 create_resource_detailed" - ) - self.ResourceCreateFromOuterEasy = self._replace_type_with_class( - "ResourceCreateFromOuterEasy", "host_node", f"动作 create_resource" - ) - self.EmptyIn = self._replace_type_with_class("EmptyIn", "host_node", f"") - self.StrSingleInput = self._replace_type_with_class("StrSingleInput", "host_node", f"") - self.device_type_registry = {} - self.device_module_to_registry = {} - self.resource_type_registry = {} - self._setup_called = False # 跟踪setup是否已调用 - self._registry_lock = threading.Lock() # 多线程加载时的锁 - # 其他状态变量 - # self.is_host_mode = False # 移至BasicConfig中 + logger.debug(f"[UniLab Registry] registry_paths: {self.registry_paths}") - def setup(self, complete_registry=False, upload_registry=False): - # 检查是否已调用过setup + self.device_type_registry: Dict[str, Any] = {} + self.resource_type_registry: Dict[str, Any] = {} + self._type_resolve_cache: Dict[str, Any] = {} + + self._setup_called = False + self._startup_executor: Optional[ThreadPoolExecutor] = None + + # ------------------------------------------------------------------ + # 统一入口 + # ------------------------------------------------------------------ + + def setup(self, devices_dirs=None, upload_registry=False, complete_registry=False): + """统一构建注册表入口。""" if self._setup_called: logger.critical("[UniLab Registry] setup方法已被调用过,不允许多次调用") return - from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type - - # 获取 HostNode 类的增强信息,用于自动生成 action schema - host_node_enhanced_info = get_enhanced_class_info( - "unilabos.ros.nodes.presets.host_node:HostNode", use_dynamic=True + self._startup_executor = ThreadPoolExecutor( + max_workers=8, thread_name_prefix="RegistryStartup" ) - # 为 test_latency 生成 schema,保留原有 description - test_latency_method_info = host_node_enhanced_info.get("action_methods", {}).get("test_latency", {}) - test_latency_schema = self._generate_unilab_json_command_schema( - test_latency_method_info.get("args", []), - "test_latency", - test_latency_method_info.get("return_annotation"), - ) - test_latency_schema["description"] = "用于测试延迟的动作,返回延迟时间和时间差。" + # 1. AST 静态扫描 (快速, 无需 import) + self._run_ast_scan(devices_dirs, upload_registry=upload_registry) - test_resource_method_info = host_node_enhanced_info.get("action_methods", {}).get("test_resource", {}) - test_resource_schema = self._generate_unilab_json_command_schema( - test_resource_method_info.get("args", []), - "test_resource", - test_resource_method_info.get("return_annotation"), - ) - test_resource_schema["description"] = "用于测试物料、设备和样本。" + # 2. Host node 内置设备 + self._setup_host_node() - self.device_type_registry.update( - { - "host_node": { - "description": "UniLabOS主机节点", - "class": { - "module": "unilabos.ros.nodes.presets.host_node", - "type": "python", - "status_types": {}, - "action_value_mappings": { - "create_resource_detailed": { - "type": self.ResourceCreateFromOuter, - "goal": { - "resources": "resources", - "device_ids": "device_ids", - "bind_parent_ids": "bind_parent_ids", - "bind_locations": "bind_locations", - "other_calling_params": "other_calling_params", - }, - "feedback": {}, - "result": {"success": "success"}, - "schema": ros_action_to_json_schema( - self.ResourceCreateFromOuter, "用于创建或更新物料资源,每次传入多个物料信息。" - ), - "goal_default": yaml.safe_load( - io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuter.Goal)) - ), - "handles": {}, - }, - "create_resource": { - "type": self.ResourceCreateFromOuterEasy, - "goal": { - "res_id": "res_id", - "class_name": "class_name", - "parent": "parent", - "device_id": "device_id", - "bind_locations": "bind_locations", - "liquid_input_slot": "liquid_input_slot[]", - "liquid_type": "liquid_type[]", - "liquid_volume": "liquid_volume[]", - "slot_on_deck": "slot_on_deck", - }, - "feedback": {}, - "result": {"success": "success"}, - "schema": ros_action_to_json_schema( - self.ResourceCreateFromOuterEasy, "用于创建或更新物料资源,每次传入一个物料信息。" - ), - "goal_default": yaml.safe_load( - io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuterEasy.Goal)) - ), - "handles": { - "output": [ - { - "handler_key": "labware", - "data_type": "resource", - "label": "Labware", - "data_source": "executor", - "data_key": "created_resource_tree.@flatten", - }, - { - "handler_key": "liquid_slots", - "data_type": "resource", - "label": "LiquidSlots", - "data_source": "executor", - "data_key": "liquid_input_resource_tree.@flatten", - }, - { - "handler_key": "materials", - "data_type": "resource", - "label": "AllMaterials", - "data_source": "executor", - "data_key": "[created_resource_tree,liquid_input_resource_tree].@flatten.@flatten", - }, - ] - }, - "placeholder_keys": { - "res_id": "unilabos_resources", # 将当前实验室的全部物料id作为下拉框可选择 - "device_id": "unilabos_devices", # 将当前实验室的全部设备id作为下拉框可选择 - "parent": "unilabos_nodes", # 将当前实验室的设备/物料作为下拉框可选择 - "class_name": "unilabos_class", # 当前实验室物料的class name - "slot_on_deck": "unilabos_resource_slot:parent", # 勾选的parent的config中的sites的name,展示name,参数对应slot(index) - }, - }, - "test_latency": { - "type": ( - "UniLabJsonCommandAsync" - if test_latency_method_info.get("is_async", False) - else "UniLabJsonCommand" - ), - "goal": {}, - "feedback": {}, - "result": {}, - "schema": test_latency_schema, - "goal_default": { - arg["name"]: arg["default"] for arg in test_latency_method_info.get("args", []) - }, - "handles": {}, - }, - "auto-test_resource": { - "type": "UniLabJsonCommand", - "goal": {}, - "feedback": {}, - "result": {}, - "schema": test_resource_schema, - "placeholder_keys": { - "device": "unilabos_devices", - "devices": "unilabos_devices", - "resource": "unilabos_resources", - "resources": "unilabos_resources", - }, - "goal_default": {}, - "handles": { - "input": [ - { - "handler_key": "input_resources", - "data_type": "resource", - "label": "InputResources", - "data_source": "handle", - "data_key": "resources", # 不为空 - }, - ] - }, - }, - }, - }, - "version": "1.0.0", - "category": [], - "config_info": [], - "icon": "icon_device.webp", - "registry_type": "device", - "handles": [], # virtue采用了不同的handle - "init_param_schema": {}, - "file_path": "/", - } - } - ) - # 为host_node添加内置的驱动命令动作 - self._add_builtin_actions(self.device_type_registry["host_node"], "host_node") - logger.trace(f"[UniLab Registry] ----------Setup----------") + # 3. YAML 注册表加载 (兼容旧格式) self.registry_paths = [Path(path).absolute() for path in self.registry_paths] for i, path in enumerate(self.registry_paths): sys_path = path.parent logger.trace(f"[UniLab Registry] Path {i+1}/{len(self.registry_paths)}: {sys_path}") sys.path.append(str(sys_path)) - self.load_device_types(path, complete_registry) + self.load_device_types(path, complete_registry=complete_registry) if BasicConfig.enable_resource_load: - self.load_resource_types(path, complete_registry, upload_registry) + self.load_resource_types(path, upload_registry, complete_registry=complete_registry) else: - logger.warning("跳过了资源注册表加载!") - logger.info("[UniLab Registry] 注册表设置完成") - # 标记setup已被调用 + logger.warning( + "[UniLab Registry] 资源加载已禁用 (enable_resource_load=False),跳过资源注册表加载" + ) + self._startup_executor.shutdown(wait=True) + self._startup_executor = None self._setup_called = True + logger.trace(f"[UniLab Registry] ----------Setup Complete----------") + + # ------------------------------------------------------------------ + # Host node 设置 + # ------------------------------------------------------------------ + + def _setup_host_node(self): + """设置 host_node 内置设备 — 基于 _run_ast_scan 已扫描的结果进行覆写。""" + # 从 AST 扫描结果中取出 host_node 的 action_value_mappings + ast_entry = self.device_type_registry.get("host_node", {}) + ast_actions = ast_entry.get("class", {}).get("action_value_mappings", {}) + + # 取出 AST 生成的 auto-method entries, 补充特定覆写 + test_latency_action = ast_actions.get("auto-test_latency", {}) + test_resource_action = ast_actions.get("auto-test_resource", {}) + test_resource_action["handles"] = { + "input": [ + { + "handler_key": "input_resources", + "data_type": "resource", + "label": "InputResources", + "data_source": "handle", + "data_key": "resources", + }, + ] + } + + create_resource_action = ast_actions.get("auto-create_resource", {}) + raw_create_resource_schema = ros_action_to_json_schema( + ResourceCreateFromOuterEasy, "用于创建或更新物料资源,每次传入一个物料信息。" + ) + raw_create_resource_schema["properties"]["result"] = create_resource_action["schema"]["properties"]["result"] + + # 覆写: 保留硬编码的 ROS2 action + AST 生成的 auto-method + self.device_type_registry["host_node"] = { + "class": { + "module": "unilabos.ros.nodes.presets.host_node:HostNode", + "status_types": {}, + "action_value_mappings": { + "create_resource": { + "type": ResourceCreateFromOuterEasy, + "goal": { + "res_id": "res_id", + "class_name": "class_name", + "parent": "parent", + "device_id": "device_id", + "bind_locations": "bind_locations", + "liquid_input_slot": "liquid_input_slot[]", + "liquid_type": "liquid_type[]", + "liquid_volume": "liquid_volume[]", + "slot_on_deck": "slot_on_deck", + }, + "feedback": {}, + "result": {"success": "success"}, + "schema": raw_create_resource_schema, + "goal_default": ROS2MessageInstance(ResourceCreateFromOuterEasy.Goal()).get_python_dict(), + "handles": { + "output": [ + { + "handler_key": "labware", + "data_type": "resource", + "label": "Labware", + "data_source": "executor", + "data_key": "created_resource_tree.@flatten", + }, + { + "handler_key": "liquid_slots", + "data_type": "resource", + "label": "LiquidSlots", + "data_source": "executor", + "data_key": "liquid_input_resource_tree.@flatten", + }, + { + "handler_key": "materials", + "data_type": "resource", + "label": "AllMaterials", + "data_source": "executor", + "data_key": "[created_resource_tree,liquid_input_resource_tree].@flatten.@flatten", + }, + ] + }, + "placeholder_keys": { + "res_id": "unilabos_resources", + "device_id": "unilabos_devices", + "parent": "unilabos_nodes", + "class_name": "unilabos_class", + }, + }, + "test_latency": test_latency_action, + "auto-test_resource": test_resource_action, + }, + "init_params": {}, + }, + "version": "1.0.0", + "category": [], + "config_info": [], + "icon": "icon_device.webp", + "registry_type": "device", + "description": "Host Node", + "handles": [], + "init_param_schema": {}, + "file_path": "/", + } + self._add_builtin_actions(self.device_type_registry["host_node"], "host_node") + + # ------------------------------------------------------------------ + # AST 静态扫描 + # ------------------------------------------------------------------ + + def _run_ast_scan(self, devices_dirs=None, upload_registry=False): + """ + 执行 AST 静态扫描,从 Python 代码中提取 @device / @resource 装饰器元数据。 + 无需 import 任何驱动模块,速度极快。 + + 所有缓存(AST 扫描 / build 结果 / config_info)统一存放在 + registry_cache.pkl 一个文件中,删除即可完全重置。 + """ + import time as _time + from unilabos.registry.ast_registry_scanner import scan_directory + + scan_t0 = _time.perf_counter() + + # 确保 executor 存在 + own_executor = False + if self._startup_executor is None: + self._startup_executor = ThreadPoolExecutor( + max_workers=8, thread_name_prefix="RegistryStartup" + ) + own_executor = True + + # ---- 统一缓存:一个 pkl 包含所有数据 ---- + unified_cache = self._load_config_cache() + ast_cache = unified_cache.setdefault("_ast_scan", {"files": {}}) + + # 默认:扫描 unilabos 包所在的父目录 + pkg_root = Path(__file__).resolve().parent.parent # .../unilabos + python_path = pkg_root.parent # .../Uni-Lab-OS + scan_root = pkg_root # 扫描 unilabos/ 整个包 + + # 额外的 --devices 目录:把它们的父目录加入 sys.path + extra_dirs: list[Path] = [] + if devices_dirs: + for d in devices_dirs: + d_path = Path(d).resolve() + if not d_path.is_dir(): + logger.warning(f"[UniLab Registry] --devices 路径不存在或不是目录: {d_path}") + continue + parent_dir = str(d_path.parent) + if parent_dir not in sys.path: + sys.path.insert(0, parent_dir) + logger.info(f"[UniLab Registry] 添加 Python 路径: {parent_dir}") + extra_dirs.append(d_path) + + # 主扫描 + exclude_files = {"lab_resources.py"} if not BasicConfig.extra_resource else None + scan_result = scan_directory( + scan_root, python_path=python_path, executor=self._startup_executor, + exclude_files=exclude_files, cache=ast_cache, + ) + if exclude_files: + logger.info( + f"[UniLab Registry] 排除扫描文件: {exclude_files} " + f"(可通过 --extra_resource 启用加载)" + ) + + # 合并缓存统计 + total_stats = scan_result.pop("_cache_stats", {"hits": 0, "misses": 0, "total": 0}) + + # 额外目录逐个扫描并合并 + for d_path in extra_dirs: + extra_result = scan_directory( + d_path, python_path=str(d_path.parent), executor=self._startup_executor, + cache=ast_cache, + ) + extra_stats = extra_result.pop("_cache_stats", {"hits": 0, "misses": 0, "total": 0}) + total_stats["hits"] += extra_stats["hits"] + total_stats["misses"] += extra_stats["misses"] + total_stats["total"] += extra_stats["total"] + + for did, dmeta in extra_result.get("devices", {}).items(): + if did in scan_result.get("devices", {}): + existing = scan_result["devices"][did].get("file_path", "?") + new_file = dmeta.get("file_path", "?") + raise ValueError( + f"@device id 重复: '{did}' 同时出现在 {existing} 和 {new_file}" + ) + scan_result.setdefault("devices", {})[did] = dmeta + for rid, rmeta in extra_result.get("resources", {}).items(): + if rid in scan_result.get("resources", {}): + existing = scan_result["resources"][rid].get("file_path", "?") + new_file = rmeta.get("file_path", "?") + raise ValueError( + f"@resource id 重复: '{rid}' 同时出现在 {existing} 和 {new_file}" + ) + scan_result.setdefault("resources", {})[rid] = rmeta + + # 缓存命中统计 + if total_stats["total"] > 0: + logger.info( + f"[UniLab Registry] AST 缓存统计: " + f"{total_stats['hits']}/{total_stats['total']} 命中, " + f"{total_stats['misses']} 重新解析" + ) + + ast_devices = scan_result.get("devices", {}) + ast_resources = scan_result.get("resources", {}) + + # build 结果缓存:当所有 AST 文件命中时跳过 _build_*_entry_from_ast + all_ast_hit = total_stats["misses"] == 0 and total_stats["total"] > 0 + cached_build = unified_cache.get("_build_results") if all_ast_hit else None + + if cached_build: + cached_devices = cached_build.get("devices", {}) + cached_resources = cached_build.get("resources", {}) + if set(cached_devices) == set(ast_devices) and set(cached_resources) == set(ast_resources): + self.device_type_registry.update(cached_devices) + self.resource_type_registry.update(cached_resources) + logger.info( + f"[UniLab Registry] build 缓存命中: 跳过 {len(cached_devices)} 设备 + " + f"{len(cached_resources)} 资源的 entry 构建" + ) + else: + cached_build = None + + if not cached_build: + build_t0 = _time.perf_counter() + + for device_id, ast_meta in ast_devices.items(): + entry = self._build_device_entry_from_ast(device_id, ast_meta) + if entry: + self.device_type_registry[device_id] = entry + + for resource_id, ast_meta in ast_resources.items(): + entry = self._build_resource_entry_from_ast(resource_id, ast_meta) + if entry: + self.resource_type_registry[resource_id] = entry + + build_elapsed = _time.perf_counter() - build_t0 + logger.info(f"[UniLab Registry] entry 构建耗时: {build_elapsed:.2f}s") + + unified_cache["_build_results"] = { + "devices": {k: v for k, v in self.device_type_registry.items() if k in ast_devices}, + "resources": {k: v for k, v in self.resource_type_registry.items() if k in ast_resources}, + } + + # upload 模式下,利用线程池并行 import pylabrobot 资源并生成 config_info + if upload_registry: + self._populate_resource_config_info(config_cache=unified_cache) + + # 统一保存一次 + self._save_config_cache(unified_cache) + + ast_device_count = len(ast_devices) + ast_resource_count = len(ast_resources) + scan_elapsed = _time.perf_counter() - scan_t0 + if ast_device_count > 0 or ast_resource_count > 0: + logger.info( + f"[UniLab Registry] AST 扫描完成: {ast_device_count} 设备, " + f"{ast_resource_count} 资源 (耗时 {scan_elapsed:.2f}s)" + ) + + if own_executor: + self._startup_executor.shutdown(wait=False) + self._startup_executor = None + + # ------------------------------------------------------------------ + # 类型辅助 (共享, 去重后的单一实现) + # ------------------------------------------------------------------ + + def _replace_type_with_class(self, type_name: str, device_id: str, field_name: str) -> Any: + """将类型名称替换为实际的 ROS 消息类对象(带缓存)""" + if not type_name or type_name == "": + return type_name + + cached = self._type_resolve_cache.get(type_name) + if cached is not None: + return cached + + result = self._resolve_type_uncached(type_name, device_id, field_name) + self._type_resolve_cache[type_name] = result + return result + + def _resolve_type_uncached(self, type_name: str, device_id: str, field_name: str) -> Any: + """实际的类型解析逻辑(无缓存)""" + # 泛型类型映射 + if "[" in type_name: + generic_mapping = { + "List[int]": "Int64MultiArray", + "list[int]": "Int64MultiArray", + "List[float]": "Float64MultiArray", + "list[float]": "Float64MultiArray", + "List[bool]": "Int8MultiArray", + "list[bool]": "Int8MultiArray", + } + mapped = generic_mapping.get(type_name) + if mapped: + cls = msg_converter_manager.search_class(mapped) + if cls: + return cls + logger.debug( + f"[Registry] 设备 {device_id} 的 {field_name} " + f"泛型类型 '{type_name}' 映射为 String" + ) + return String + + convert_manager = { + "str": "String", + "bool": "Bool", + "int": "Int64", + "float": "Float64", + } + type_name = convert_manager.get(type_name, type_name) + if ":" in type_name: + type_class = msg_converter_manager.get_class(type_name) + else: + type_class = msg_converter_manager.search_class(type_name) + if type_class: + return type_class + else: + logger.trace( + f"[Registry] 类型 '{type_name}' 非 ROS2 消息类型 (设备 {device_id} {field_name}),映射为 String" + ) + return String + + # ---- 类型字符串 -> JSON Schema type ---- + # (常量和工具函数已移至 unilabos.registry.utils) + + def _generate_schema_from_info( + self, param_name: str, param_type: Union[str, Tuple[str]], param_default: Any, + import_map: Optional[Dict[str, str]] = None, + ) -> Dict[str, Any]: + """根据参数信息生成 JSON Schema。 + 支持复杂类型字符串如 'Optional[Dict[str, Any]]'、'List[int]' 等。 + 当提供 import_map 时,可解析 TypedDict 等自定义类型。""" + + prop_schema: Dict[str, Any] = {} + + if isinstance(param_type, str) and ("[" in param_type or "|" in param_type): + # 复杂泛型 — ast.parse 解析结构,递归生成 schema + node = parse_type_node(param_type) + if node is not None: + prop_schema = type_node_to_schema(node, import_map) + # slot 标记 fallback(正常不应走到这里,上层会拦截) + if "$slot" in prop_schema: + prop_schema = {"type": "object"} + else: + prop_schema["type"] = "string" + elif isinstance(param_type, str): + # 简单类型名,但可能是 import_map 中的自定义类型 + json_type = SIMPLE_TYPE_MAP.get(param_type.lower()) + if json_type: + prop_schema["type"] = json_type + elif ":" in param_type: + type_obj = resolve_type_object(param_type) + if type_obj is not None: + prop_schema = type_to_schema(type_obj) + else: + prop_schema["type"] = "object" + elif import_map and param_type in import_map: + type_obj = resolve_type_object(import_map[param_type]) + if type_obj is not None: + prop_schema = type_to_schema(type_obj) + else: + prop_schema["type"] = "object" + else: + json_type = get_json_schema_type(param_type) + if json_type == "string" and param_type and param_type.lower() not in SIMPLE_TYPE_MAP: + prop_schema["type"] = "object" + else: + prop_schema["type"] = json_type + elif isinstance(param_type, tuple): + if len(param_type) == 2: + outer_type, inner_type = param_type + outer_json_type = get_json_schema_type(outer_type) + prop_schema["type"] = outer_json_type + # Any 值类型不加 additionalProperties/items (等同于无约束) + if isinstance(inner_type, str) and inner_type in ("Any", "None", "Unknown"): + pass + else: + inner_json_type = get_json_schema_type(inner_type) + if outer_json_type == "array": + prop_schema["items"] = {"type": inner_json_type} + elif outer_json_type == "object": + prop_schema["additionalProperties"] = {"type": inner_json_type} + else: + prop_schema["type"] = "string" + else: + prop_schema["type"] = get_json_schema_type(param_type) + + if param_default is not None: + prop_schema["default"] = param_default + + return prop_schema + + def _generate_unilab_json_command_schema( + self, method_args: list, docstring: Optional[str] = None, + import_map: Optional[Dict[str, str]] = None, + ) -> Dict[str, Any]: + """根据方法参数和 docstring 生成 UniLabJsonCommand schema""" + doc_info = parse_docstring(docstring) + param_descs = doc_info.get("params", {}) + + schema = { + "type": "object", + "properties": {}, + "required": [], + } + for arg_info in method_args: + param_name = arg_info.get("name", "") + param_type = arg_info.get("type", "") + param_default = arg_info.get("default") + param_required = arg_info.get("required", True) + + is_slot, is_list_slot = detect_slot_type(param_type) + if is_slot == "ResourceSlot": + if is_list_slot: + schema["properties"][param_name] = { + "items": ros_message_to_json_schema(Resource, param_name), + "type": "array", + } + else: + schema["properties"][param_name] = ros_message_to_json_schema( + Resource, param_name + ) + elif is_slot == "DeviceSlot": + schema["properties"][param_name] = {"type": "string", "description": "device reference"} + else: + schema["properties"][param_name] = self._generate_schema_from_info( + param_name, param_type, param_default, import_map=import_map + ) + + if param_name in param_descs: + schema["properties"][param_name]["description"] = param_descs[param_name] + + if param_required: + schema["required"].append(param_name) + + return schema + + def _generate_status_types_schema(self, status_methods: Dict[str, Any]) -> Dict[str, Any]: + """根据 status 方法信息生成 status_types schema""" + status_schema: Dict[str, Any] = { + "type": "object", + "properties": {}, + "required": [], + } + for status_name, status_info in status_methods.items(): + return_type = status_info.get("return_type", "str") + status_schema["properties"][status_name] = self._generate_schema_from_info( + status_name, return_type, None + ) + status_schema["required"].append(status_name) + return status_schema + + # ------------------------------------------------------------------ + # 方法签名分析 -- 委托给 ImportManager + # ------------------------------------------------------------------ + + @staticmethod + def _analyze_method_signature(method) -> Dict[str, Any]: + """分析方法签名,提取参数信息""" + from unilabos.utils.import_manager import default_manager + try: + return default_manager._analyze_method_signature(method) + except (ValueError, TypeError): + return {"args": [], "is_async": inspect.iscoroutinefunction(method)} + + @staticmethod + def _get_return_type_from_method(method) -> str: + """获取方法的返回类型字符串""" + from unilabos.utils.import_manager import default_manager + return default_manager._get_return_type_from_method(method) + + # ------------------------------------------------------------------ + # 动态类信息提取 (import-based) + # ------------------------------------------------------------------ + + def _extract_class_info(self, cls: type) -> Dict[str, Any]: + """ + 从类中提取 init 参数、状态方法和动作方法信息。 + """ + result = { + "class_name": cls.__name__, + "init_params": self._analyze_method_signature(cls.__init__)["args"], + "status_methods": {}, + "action_methods": {}, + "explicit_actions": {}, + "decorated_no_type_actions": {}, + } + + for name, method in cls.__dict__.items(): + if name.startswith("_"): + continue + + # property => status + if isinstance(method, property): + return_type = self._get_return_type_from_method(method.fget) if method.fget else "Any" + status_entry = { + "name": name, + "return_type": return_type, + } + if method.fget: + tc = get_topic_config(method.fget) + if tc: + status_entry["topic_config"] = tc + result["status_methods"][name] = status_entry + + if method.fset: + setter_info = self._analyze_method_signature(method.fset) + action_meta = get_action_meta(method.fset) + if action_meta and action_meta.get("action_type") is not None: + result["explicit_actions"][name] = { + "method_info": setter_info, + "action_meta": action_meta, + } + continue + + if not callable(method): + continue + + if is_not_action(method): + continue + + # @topic_config 装饰的非 property 方法视为状态方法,不作为 action + tc = get_topic_config(method) + if tc: + return_type = self._get_return_type_from_method(method) + prop_name = name[4:] if name.startswith("get_") else name + result["status_methods"][prop_name] = { + "name": prop_name, + "return_type": return_type, + "topic_config": tc, + } + continue + + method_info = self._analyze_method_signature(method) + action_meta = get_action_meta(method) + + if action_meta: + action_type = action_meta.get("action_type") + if action_type is not None: + result["explicit_actions"][name] = { + "method_info": method_info, + "action_meta": action_meta, + } + else: + result["decorated_no_type_actions"][name] = { + "method_info": method_info, + "action_meta": action_meta, + } + elif has_action_decorator(method): + result["explicit_actions"][name] = { + "method_info": method_info, + "action_meta": action_meta or {}, + } + else: + result["action_methods"][name] = method_info + + return result + + # ------------------------------------------------------------------ + # 内置动作 + # ------------------------------------------------------------------ + + def _add_builtin_actions(self, device_config: Dict[str, Any], device_id: str): + """为设备添加内置的驱动命令动作(运行时需要,上报注册表时会过滤掉)""" + str_single_input = self._replace_type_with_class("StrSingleInput", device_id, "内置动作") + for additional_action in ["_execute_driver_command", "_execute_driver_command_async"]: + try: + goal_default = ROS2MessageInstance(str_single_input.Goal()).get_python_dict() + except Exception: + goal_default = {"string": ""} + + device_config["class"]["action_value_mappings"][additional_action] = { + "type": str_single_input, + "goal": {"string": "string"}, + "feedback": {}, + "result": {}, + "schema": ros_action_to_json_schema(str_single_input), + "goal_default": goal_default, + "handles": {}, + } + + # ------------------------------------------------------------------ + # AST-based 注册表条目构建 + # ------------------------------------------------------------------ + + def _build_device_entry_from_ast(self, device_id: str, ast_meta: dict) -> Dict[str, Any]: + """ + Build a device registry entry from AST-scanned metadata. + Uses only string types -- no module imports required (except for TypedDict resolution). + """ + module_str = ast_meta.get("module", "") + file_path = ast_meta.get("file_path", "") + imap = ast_meta.get("import_map") or {} + + # --- status_types (string version) --- + status_types_str: Dict[str, str] = {} + for name, info in ast_meta.get("status_properties", {}).items(): + ret_type = info.get("return_type", "str") + if not ret_type or ret_type in ("Any", "None", "Unknown", ""): + ret_type = "String" + # 归一化泛型容器类型: Dict[str, Any] → dict, List[int] → list 等 + elif "[" in ret_type: + base = ret_type.split("[", 1)[0].strip() + base_lower = base.lower() + if base_lower in ("dict", "mapping", "ordereddict"): + ret_type = "dict" + elif base_lower in ("list", "tuple", "set", "sequence", "iterable"): + ret_type = "list" + elif base_lower == "optional": + # Optional[X] → 取内部类型再归一化 + inner = ret_type.split("[", 1)[1].rsplit("]", 1)[0].strip() + inner_lower = inner.lower() + if inner_lower in ("dict", "mapping"): + ret_type = "dict" + elif inner_lower in ("list", "tuple", "set"): + ret_type = "list" + else: + ret_type = inner + status_types_str[name] = ret_type + status_types_str = dict(sorted(status_types_str.items())) + + # --- action_value_mappings --- + action_value_mappings: Dict[str, Any] = {} + + def _build_json_command_entry(method_name, method_info, action_args=None): + """构建 UniLabJsonCommand 类型的 action entry""" + is_async = method_info.get("is_async", False) + type_str = "UniLabJsonCommandAsync" if is_async else "UniLabJsonCommand" + params = method_info.get("params", []) + method_doc = method_info.get("docstring") + goal_schema = self._generate_schema_from_ast_params(params, method_name, method_doc, imap) + + if action_args is not None: + action_name = action_args.get("action_name", method_name) + if action_args.get("auto_prefix"): + action_name = f"auto-{action_name}" + else: + action_name = f"auto-{method_name}" + + # Source C: 从 schema 生成类型默认值 + goal_default = JSONSchemaMessageInstance.generate_default_from_schema(goal_schema) + # Source B: method param 显式 default 覆盖 Source C + for p in params: + if p.get("default") is not None: + goal_default[p["name"]] = p["default"] + # goal 为 identity mapping {param_name: param_name}, 默认值只放在 goal_default + goal = {p["name"]: p["name"] for p in params} + + # @action 中的显式 goal/goal_default 覆盖 + goal_override = dict((action_args or {}).get("goal", {})) + goal_default_override = dict((action_args or {}).get("goal_default", {})) + if goal_override: + override_values = set(goal_override.values()) + goal = {k: v for k, v in goal.items() if not (k == v and v in override_values)} + goal.update(goal_override) + goal_default.update(goal_default_override) + + # action handles: 从 @action(handles=[...]) 提取并转换为标准格式 + raw_handles = (action_args or {}).get("handles") + handles = normalize_ast_action_handles(raw_handles) if isinstance(raw_handles, list) else (raw_handles or {}) + + # placeholder_keys: 优先用装饰器显式配置,否则从参数类型检测 + pk = (action_args or {}).get("placeholder_keys") or detect_placeholder_keys(params) + + # 从方法返回值类型生成 result schema + result_schema = None + ret_type_str = method_info.get("return_type", "") + if ret_type_str and ret_type_str not in ("None", "Any", ""): + result_schema = self._generate_schema_from_info( + "result", ret_type_str, None, imap + ) + + entry = { + "type": type_str, + "goal": goal, + "feedback": (action_args or {}).get("feedback") or {}, + "result": (action_args or {}).get("result") or {}, + "schema": wrap_action_schema(goal_schema, action_name, result_schema=result_schema), + "goal_default": goal_default, + "handles": handles, + "placeholder_keys": pk, + } + if (action_args or {}).get("always_free") or method_info.get("always_free"): + entry["always_free"] = True + return action_name, entry + + # 1) auto- actions + for method_name, method_info in ast_meta.get("auto_methods", {}).items(): + action_name, action_entry = _build_json_command_entry(method_name, method_info) + action_value_mappings[action_name] = action_entry + + # 2) @action() without action_type + for method_name, method_info in ast_meta.get("actions", {}).items(): + action_args = method_info.get("action_args", {}) + if action_args.get("action_type"): + continue + action_name, action_entry = _build_json_command_entry(method_name, method_info, action_args) + action_value_mappings[action_name] = action_entry + + # 3) @action(action_type=X) + for method_name, method_info in ast_meta.get("actions", {}).items(): + action_args = method_info.get("action_args", {}) + action_type = action_args.get("action_type") + if not action_type: + continue + + action_name = action_args.get("action_name", method_name) + if action_args.get("auto_prefix"): + action_name = f"auto-{action_name}" + + raw_handles = action_args.get("handles") + handles = normalize_ast_action_handles(raw_handles) if isinstance(raw_handles, list) else (raw_handles or {}) + + method_params = method_info.get("params", []) + + # goal/feedback/result: 字段映射 + # parent=True 时直接通过 import class + MRO 获取; 否则从 AST 方法参数获取, 最后从 ROS2 Goal 获取 + # feedback/result 从 ROS2 获取; 默认 identity mapping {k: k}, 再用 @action 参数 update + goal_override = dict(action_args.get("goal", {})) + feedback_override = dict(action_args.get("feedback", {})) + result_override = dict(action_args.get("result", {})) + goal_default_override = dict(action_args.get("goal_default", {})) + + if action_args.get("parent"): + # @action(parent=True): 直接通过 import class + MRO 获取父类方法签名 + goal = resolve_method_params_via_import(module_str, method_name) + else: + # 从 AST 方法参数构建 goal identity mapping + real_params = [p for p in method_params if p["name"] not in ("self", "cls")] + goal = {p["name"]: p["name"] for p in real_params} + + feedback = {} + result = {} + schema = {} + goal_default = {} + + # 尝试 import ROS2 action type 获取 feedback/result/schema/goal_default, 以及 goal fallback + if ":" not in action_type: + action_type = imap.get(action_type, action_type) + action_type_obj = resolve_type_object(action_type) if ":" in action_type else None + if action_type_obj is None: + logger.warning( + f"[AST] device action '{action_name}': resolve_type_object('{action_type}') returned None" + ) + if action_type_obj is not None: + # 始终从 ROS2 Goal 获取字段作为基础, 再用方法参数覆盖 + try: + if hasattr(action_type_obj, "Goal"): + goal_fields = action_type_obj.Goal.get_fields_and_field_types() + ros2_goal = {k: k for k in goal_fields} + ros2_goal.update(goal) + goal = ros2_goal + except Exception as e: + logger.debug(f"[AST] device action '{action_name}': Goal enrichment from ROS2 failed: {e}") + try: + if hasattr(action_type_obj, "Feedback"): + fb_fields = action_type_obj.Feedback.get_fields_and_field_types() + feedback = {k: k for k in fb_fields} + except Exception as e: + logger.debug(f"[AST] device action '{action_name}': Feedback enrichment failed: {e}") + try: + if hasattr(action_type_obj, "Result"): + res_fields = action_type_obj.Result.get_fields_and_field_types() + result = {k: k for k in res_fields} + except Exception as e: + logger.debug(f"[AST] device action '{action_name}': Result enrichment failed: {e}") + try: + schema = ros_action_to_json_schema(action_type_obj) + except Exception: + pass + # 直接从 ROS2 Goal 实例获取默认值 (msgcenterpy) + try: + goal_default = ROS2MessageInstance(action_type_obj.Goal()).get_python_dict() + except Exception: + pass + + # 如果 ROS2 action type 未提供 result schema, 用方法返回值类型生成 fallback + if not schema.get("properties", {}).get("result"): + ret_type_str = method_info.get("return_type", "") + if ret_type_str and ret_type_str not in ("None", "Any", ""): + ret_schema = self._generate_schema_from_info( + "result", ret_type_str, None, imap + ) + if ret_schema: + schema.setdefault("properties", {})["result"] = ret_schema + + # @action 中的显式 goal/feedback/result/goal_default 覆盖默认值 + # 移除被 override 取代的 identity 条目 (如 {source: source} 被 {sources: source} 取代) + if goal_override: + override_values = set(goal_override.values()) + goal = {k: v for k, v in goal.items() if not (k == v and v in override_values)} + goal.update(goal_override) + feedback.update(feedback_override) + result.update(result_override) + goal_default.update(goal_default_override) + + action_entry = { + "type": action_type.split(":")[-1], + "goal": goal, + "feedback": feedback, + "result": result, + "schema": schema, + "goal_default": goal_default, + "handles": handles, + "placeholder_keys": action_args.get("placeholder_keys") or detect_placeholder_keys(method_params), + } + if action_args.get("always_free") or method_info.get("always_free"): + action_entry["always_free"] = True + action_value_mappings[action_name] = action_entry + + action_value_mappings = dict(sorted(action_value_mappings.items())) + + # --- init_param_schema = { config: , data: } --- + init_params = ast_meta.get("init_params", []) + config_schema = self._generate_schema_from_ast_params(init_params, "__init__", import_map=imap) + data_schema = self._generate_status_schema_from_ast( + ast_meta.get("status_properties", {}), imap + ) + init_schema: Dict[str, Any] = { + "config": config_schema, + "data": data_schema, + } + + # --- handles --- + handles_raw = ast_meta.get("handles", []) + handles = normalize_ast_handles(handles_raw) + + entry: Dict[str, Any] = { + "category": ast_meta.get("category", []), + "class": { + "module": module_str, + "status_types": status_types_str, + "action_value_mappings": action_value_mappings, + "type": ast_meta.get("device_type", "python"), + }, + "config_info": [], + "description": ast_meta.get("description", ""), + "handles": handles, + "icon": ast_meta.get("icon", ""), + "init_param_schema": init_schema, + "version": ast_meta.get("version", "1.0.0"), + "registry_type": "device", + "file_path": file_path, + } + model = ast_meta.get("model") + if model is not None: + entry["model"] = model + hardware_interface = ast_meta.get("hardware_interface") + if hardware_interface is not None: + # AST 解析 HardwareInterface(...) 得到 {"_call": "...", "name": ..., "read": ..., "write": ...} + # 归一化为 YAML 格式,去掉 _call + if isinstance(hardware_interface, dict) and "_call" in hardware_interface: + hardware_interface = {k: v for k, v in hardware_interface.items() if k != "_call"} + entry["class"]["hardware_interface"] = hardware_interface + return entry + + def _generate_schema_from_ast_params( + self, params: list, method_name: str, docstring: Optional[str] = None, + import_map: Optional[Dict[str, str]] = None, + ) -> Dict[str, Any]: + """Generate JSON Schema from AST-extracted parameter list.""" + doc_info = parse_docstring(docstring) + param_descs = doc_info.get("params", {}) + + schema: Dict[str, Any] = { + "type": "object", + "properties": {}, + "required": [], + } + for p in params: + pname = p.get("name", "") + ptype = p.get("type", "") + pdefault = p.get("default") + prequired = p.get("required", True) + + # --- 检测 ResourceSlot / DeviceSlot (兼容 runtime 和 AST 两种格式) --- + is_slot, is_list_slot = detect_slot_type(ptype) + if is_slot == "ResourceSlot": + if is_list_slot: + schema["properties"][pname] = { + "items": ros_message_to_json_schema(Resource, pname), + "type": "array", + } + else: + schema["properties"][pname] = ros_message_to_json_schema(Resource, pname) + elif is_slot == "DeviceSlot": + schema["properties"][pname] = {"type": "string", "description": "device reference"} + else: + schema["properties"][pname] = self._generate_schema_from_info( + pname, ptype, pdefault, import_map + ) + + if pname in param_descs: + schema["properties"][pname]["description"] = param_descs[pname] + + if prequired: + schema["required"].append(pname) + + return schema + + def _generate_status_schema_from_ast( + self, status_properties: Dict[str, Any], + import_map: Optional[Dict[str, str]] = None, + ) -> Dict[str, Any]: + """Generate status_types schema from AST-extracted status properties.""" + schema: Dict[str, Any] = { + "type": "object", + "properties": {}, + "required": [], + } + for name, info in status_properties.items(): + ret_type = info.get("return_type", "str") + schema["properties"][name] = self._generate_schema_from_info( + name, ret_type, None, import_map + ) + schema["required"].append(name) + return schema + + def _build_resource_entry_from_ast(self, resource_id: str, ast_meta: dict) -> Dict[str, Any]: + """Build a resource registry entry from AST-scanned metadata.""" + module_str = ast_meta.get("module", "") + file_path = ast_meta.get("file_path", "") + + handles_raw = ast_meta.get("handles", []) + handles = normalize_ast_handles(handles_raw) + + entry: Dict[str, Any] = { + "category": ast_meta.get("category", []), + "class": { + "module": module_str, + "type": ast_meta.get("class_type", "python"), + }, + "config_info": [], + "description": ast_meta.get("description", ""), + "handles": handles, + "icon": ast_meta.get("icon", ""), + "init_param_schema": {}, + "version": ast_meta.get("version", "1.0.0"), + "registry_type": "resource", + "file_path": file_path, + } + + if ast_meta.get("model"): + entry["model"] = ast_meta["model"] + + return entry + + # ------------------------------------------------------------------ + # 定向 AST 扫描(供 complete_registry Case 1 使用) + # ------------------------------------------------------------------ + + def _ast_scan_module(self, module_str: str) -> Optional[Dict[str, Any]]: + """对单个 module_str 做定向 AST 扫描,返回 ast_meta 或 None。 + + 用于 complete_registry 模式下 YAML 中存在但 AST 全量扫描未覆盖的设备/资源。 + 仅做文件定位 + AST 解析,不实例化类。 + """ + from unilabos.registry.ast_registry_scanner import _parse_file + + mod_part = module_str.split(":")[0] + try: + mod = importlib.import_module(mod_part) + src_file = Path(inspect.getfile(mod)) + except Exception: + return None + + python_path = Path(__file__).resolve().parent.parent.parent + try: + devs, ress = _parse_file(src_file, python_path) + except Exception: + return None + + for d in devs: + if d.get("module") == module_str: + return d + for r in ress: + if r.get("module") == module_str: + return r + return None + + # ------------------------------------------------------------------ + # config_info 缓存 (pickle 格式,比 JSON 快 ~10x,debug 模式下差异更大) + # ------------------------------------------------------------------ + + @staticmethod + def _get_config_cache_path() -> Optional[Path]: + if BasicConfig.working_dir: + return Path(BasicConfig.working_dir) / "registry_cache.pkl" + return None + + _CACHE_VERSION = 3 + + def _load_config_cache(self) -> dict: + import pickle + cache_path = self._get_config_cache_path() + if cache_path is None or not cache_path.is_file(): + return {} + try: + data = pickle.loads(cache_path.read_bytes()) + if not isinstance(data, dict) or data.get("_version") != self._CACHE_VERSION: + return {} + return data + except Exception: + return {} + + def _save_config_cache(self, cache: dict) -> None: + import pickle + cache_path = self._get_config_cache_path() + if cache_path is None: + return + try: + cache["_version"] = self._CACHE_VERSION + cache_path.parent.mkdir(parents=True, exist_ok=True) + tmp = cache_path.with_suffix(".tmp") + tmp.write_bytes(pickle.dumps(cache, protocol=pickle.HIGHEST_PROTOCOL)) + tmp.replace(cache_path) + except Exception as e: + logger.debug(f"[UniLab Registry] 缓存保存失败: {e}") + + @staticmethod + def _module_source_hash(module_str: str) -> Optional[str]: + """Fast MD5 of the source file backing *module_str*. Results are + cached for the process lifetime so the same file is never read twice.""" + if module_str in _module_hash_cache: + return _module_hash_cache[module_str] + + import hashlib + import importlib.util + mod_part = module_str.split(":")[0] if ":" in module_str else module_str + result = None + try: + spec = importlib.util.find_spec(mod_part) + if spec and spec.origin and os.path.isfile(spec.origin): + result = hashlib.md5(open(spec.origin, "rb").read()).hexdigest() + except Exception: + pass + _module_hash_cache[module_str] = result + return result + + def _populate_resource_config_info(self, config_cache: Optional[dict] = None): + """ + 利用线程池并行 import pylabrobot 资源类,生成 config_info。 + 仅在 upload_registry=True 时调用。 + + 启用缓存:以 module_str 为 key,记录源文件 MD5。若源文件未变则 + 直接复用上次的 config_info,跳过 import + 实例化 + dump。 + + Args: + config_cache: 共享的缓存 dict。未提供时自行加载/保存; + 由 load_resource_types 传入时由调用方统一保存。 + """ + import time as _time + + executor = self._startup_executor + if executor is None: + return + + # 筛选需要 import 的 pylabrobot 资源(跳过已有 config_info 的缓存条目) + pylabrobot_entries = { + rid: entry + for rid, entry in self.resource_type_registry.items() + if entry.get("class", {}).get("type") == "pylabrobot" + and entry.get("class", {}).get("module") + and not entry.get("config_info") + } + if not pylabrobot_entries: + return + + t0 = _time.perf_counter() + own_cache = config_cache is None + if own_cache: + config_cache = self._load_config_cache() + cache_hits = 0 + cache_misses = 0 + + def _import_and_dump(resource_id: str, module_str: str): + """Import class, create instance, dump tree. Returns (rid, config_info).""" + try: + res_class = import_class(module_str) + if callable(res_class) and not isinstance(res_class, type): + res_instance = res_class(res_class.__name__) + tree_set = ResourceTreeSet.from_plr_resources([res_instance], known_newly_created=True, old_size=True) + dumped = tree_set.dump(old_position=True) + return resource_id, dumped[0] if dumped else [] + except Exception as e: + logger.warning(f"[UniLab Registry] 资源 {resource_id} config_info 生成失败: {e}") + return resource_id, [] + + # Separate into cache-hit vs cache-miss + need_generate: dict = {} # rid -> module_str + for rid, entry in pylabrobot_entries.items(): + module_str = entry["class"]["module"] + cached = config_cache.get(module_str) + if cached and isinstance(cached, dict) and "config_info" in cached: + src_hash = self._module_source_hash(module_str) + if src_hash is not None and cached.get("src_hash") == src_hash: + self.resource_type_registry[rid]["config_info"] = cached["config_info"] + cache_hits += 1 + continue + need_generate[rid] = module_str + + cache_misses = len(need_generate) + + if need_generate: + future_to_rid = { + executor.submit(_import_and_dump, rid, mod): rid + for rid, mod in need_generate.items() + } + for future in as_completed(future_to_rid): + try: + resource_id, config_info = future.result() + self.resource_type_registry[resource_id]["config_info"] = config_info + module_str = need_generate[resource_id] + src_hash = self._module_source_hash(module_str) + config_cache[module_str] = { + "src_hash": src_hash, + "config_info": config_info, + } + except Exception as e: + rid = future_to_rid[future] + logger.warning(f"[UniLab Registry] 资源 {rid} config_info 线程异常: {e}") + + if own_cache: + self._save_config_cache(config_cache) + + elapsed = _time.perf_counter() - t0 + total = cache_hits + cache_misses + logger.info( + f"[UniLab Registry] config_info 缓存统计: " + f"{cache_hits}/{total} 命中, {cache_misses} 重新生成 " + f"(耗时 {elapsed:.2f}s)" + ) + + # ------------------------------------------------------------------ + # Verify & Resolve (实际 import 验证) + # ------------------------------------------------------------------ + + def verify_and_resolve_registry(self): + """ + 对 AST 扫描得到的注册表执行实际 import 验证(使用共享线程池并行)。 + """ + errors = [] + import_success_count = 0 + resolved_count = 0 + total_items = len(self.device_type_registry) + len(self.resource_type_registry) + + lock = threading.Lock() + + def _verify_device(device_id: str, entry: dict): + nonlocal import_success_count, resolved_count + module_str = entry.get("class", {}).get("module", "") + if not module_str or ":" not in module_str: + with lock: + import_success_count += 1 + return None + + try: + cls = import_class(module_str) + with lock: + import_success_count += 1 + resolved_count += 1 + + # 尝试用动态信息增强注册表 + try: + self.resolve_types_for_device(device_id, cls) + except Exception as e: + logger.debug(f"[UniLab Registry/Verify] 设备 {device_id} 类型解析失败: {e}") + + return None + except Exception as e: + logger.warning( + f"[UniLab Registry/Verify] 设备 {device_id}: " + f"导入模块 {module_str} 失败: {e}" + ) + return f"device:{device_id}: {e}" + + def _verify_resource(resource_id: str, entry: dict): + nonlocal import_success_count + module_str = entry.get("class", {}).get("module", "") + if not module_str or ":" not in module_str: + with lock: + import_success_count += 1 + return None + + try: + import_class(module_str) + with lock: + import_success_count += 1 + return None + except Exception as e: + logger.warning( + f"[UniLab Registry/Verify] 资源 {resource_id}: " + f"导入模块 {module_str} 失败: {e}" + ) + return f"resource:{resource_id}: {e}" + + executor = self._startup_executor or ThreadPoolExecutor(max_workers=8) + try: + device_futures = {} + resource_futures = {} + + for device_id, entry in list(self.device_type_registry.items()): + fut = executor.submit(_verify_device, device_id, entry) + device_futures[fut] = device_id + + for resource_id, entry in list(self.resource_type_registry.items()): + fut = executor.submit(_verify_resource, resource_id, entry) + resource_futures[fut] = resource_id + + for future in as_completed(device_futures): + result = future.result() + if result: + errors.append(result) + + for future in as_completed(resource_futures): + result = future.result() + if result: + errors.append(result) + finally: + if self._startup_executor is None: + executor.shutdown(wait=True) + + if errors: + logger.warning( + f"[UniLab Registry/Verify] 验证完成: {import_success_count}/{total_items} 成功, " + f"{len(errors)} 个错误" + ) + else: + logger.info( + f"[UniLab Registry/Verify] 验证完成: {import_success_count}/{total_items} 全部通过, " + f"{resolved_count} 设备类型已解析" + ) + + return errors + + def resolve_types_for_device(self, device_id: str, cls=None): + """ + 将 AST 扫描得到的字符串类型引用替换为实际的 ROS 消息类对象。 + """ + entry = self.device_type_registry.get(device_id) + if not entry: + return + + class_info = entry.get("class", {}) + + # 解析 status_types + status_types = class_info.get("status_types", {}) + resolved_status = {} + for name, type_ref in status_types.items(): + if isinstance(type_ref, str): + resolved = self._replace_type_with_class(type_ref, device_id, f"状态 {name}") + if resolved: + resolved_status[name] = resolved + else: + resolved_status[name] = type_ref + else: + resolved_status[name] = type_ref + class_info["status_types"] = resolved_status + + # 解析 action_value_mappings + _KEEP_AS_STRING = {"UniLabJsonCommand", "UniLabJsonCommandAsync"} + action_mappings = class_info.get("action_value_mappings", {}) + for action_name, action_config in action_mappings.items(): + type_ref = action_config.get("type", "") + if isinstance(type_ref, str) and type_ref and type_ref not in _KEEP_AS_STRING: + resolved = self._replace_type_with_class(type_ref, device_id, f"动作 {action_name}") + if resolved: + action_config["type"] = resolved + if not action_config.get("schema"): + try: + action_config["schema"] = ros_action_to_json_schema(resolved) + except Exception: + pass + if not action_config.get("goal_default"): + try: + action_config["goal_default"] = ROS2MessageInstance(resolved.Goal()).get_python_dict() + except Exception: + pass + + # 如果提供了类,用动态信息增强 + if cls is not None: + try: + dynamic_info = self._extract_class_info(cls) + + for name, info in dynamic_info.get("status_methods", {}).items(): + if name not in resolved_status: + ret_type = info.get("return_type", "str") + resolved = self._replace_type_with_class(ret_type, device_id, f"状态 {name}") + if resolved: + class_info["status_types"][name] = resolved + + for action_name_key, action_config in action_mappings.items(): + type_obj = action_config.get("type") + if isinstance(type_obj, str) and type_obj in ( + "UniLabJsonCommand", "UniLabJsonCommandAsync" + ): + method_name = action_name_key + if method_name.startswith("auto-"): + method_name = method_name[5:] + + actual_method = getattr(cls, method_name, None) + if actual_method: + method_info = self._analyze_method_signature(actual_method) + schema = self._generate_unilab_json_command_schema( + method_info["args"], + docstring=getattr(actual_method, "__doc__", None), + ) + action_config["schema"] = schema + except Exception as e: + logger.debug(f"[Registry] 设备 {device_id} 动态增强失败: {e}") + + # 添加内置动作 + self._add_builtin_actions(entry, device_id) + + def resolve_all_types(self): + """将所有注册表条目中的字符串类型引用替换为实际的 ROS2 消息类对象。 + + 仅做 ROS2 消息类型查找,不 import 任何设备模块,速度快且无副作用。 + """ + t0 = time.time() + for device_id in list(self.device_type_registry): + try: + self.resolve_types_for_device(device_id) + except Exception as e: + logger.debug(f"[Registry] 设备 {device_id} 类型解析失败: {e}") + logger.info( + f"[UniLab Registry] 类型解析完成: {len(self.device_type_registry)} 设备 " + f"(耗时 {time.time() - t0:.2f}s)" + ) + + # ------------------------------------------------------------------ + # YAML 注册表加载 (兼容旧格式) + # ------------------------------------------------------------------ def _load_single_resource_file( - self, file: Path, complete_registry: bool, upload_registry: bool + self, file: Path, complete_registry: bool ) -> Tuple[Dict[str, Any], Dict[str, Any], bool]: """ 加载单个资源文件 (线程安全) @@ -269,7 +1500,20 @@ class Registry: return {}, {}, False complete_data = {} + skip_ids = set() for resource_id, resource_info in data.items(): + if not isinstance(resource_info, dict): + continue + + # AST 已有该资源 → 跳过,提示冗余 + if self.resource_type_registry.get(resource_id): + logger.warning( + f"[UniLab Registry] 资源 '{resource_id}' 已由 AST 扫描注册," + f"YAML 定义冗余,跳过 YAML 处理" + ) + skip_ids.add(resource_id) + continue + if "version" not in resource_info: resource_info["version"] = "1.0.0" if "category" not in resource_info: @@ -291,426 +1535,134 @@ class Registry: if "file_path" in resource_info: del resource_info["file_path"] complete_data[resource_id] = copy.deepcopy(dict(sorted(resource_info.items()))) - if upload_registry: - class_info = resource_info.get("class", {}) - if len(class_info) and "module" in class_info: - if class_info.get("type") == "pylabrobot": - res_class = get_class(class_info["module"]) - if callable(res_class) and not isinstance(res_class, type): - res_instance = res_class(res_class.__name__) - res_ulr = tree_to_list([resource_plr_to_ulab(res_instance)]) - resource_info["config_info"] = res_ulr resource_info["registry_type"] = "resource" resource_info["file_path"] = str(file.absolute()).replace("\\", "/") + for rid in skip_ids: + data.pop(rid, None) + complete_data = dict(sorted(complete_data.items())) - complete_data = copy.deepcopy(complete_data) if complete_registry: + write_data = copy.deepcopy(complete_data) + for res_id, res_cfg in write_data.items(): + res_cfg.pop("file_path", None) + res_cfg.pop("registry_type", None) try: with open(file, "w", encoding="utf-8") as f: - yaml.dump(complete_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper) + yaml.dump(write_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper) except Exception as e: logger.warning(f"[UniLab Registry] 写入资源文件失败: {file}, 错误: {e}") return data, complete_data, True - def load_resource_types(self, path: os.PathLike, complete_registry: bool, upload_registry: bool): + def load_resource_types(self, path: os.PathLike, upload_registry: bool, complete_registry: bool = False): abs_path = Path(path).absolute() - resource_path = abs_path / "resources" - files = list(resource_path.glob("*/*.yaml")) - logger.debug(f"[UniLab Registry] resources: {resource_path.exists()}, total: {len(files)}") + resources_path = abs_path / "resources" + files = list(resources_path.rglob("*.yaml")) + logger.trace( + f"[UniLab Registry] resources: {resources_path.exists()}, total: {len(files)}" + ) if not files: return - # 使用线程池并行加载 - max_workers = min(8, len(files)) - results = [] + import hashlib as _hl - with ThreadPoolExecutor(max_workers=max_workers) as executor: - future_to_file = { - executor.submit(self._load_single_resource_file, file, complete_registry, upload_registry): file - for file in files - } - for future in as_completed(future_to_file): - file = future_to_file[future] - try: - data, complete_data, is_valid = future.result() - if is_valid: - results.append((file, data)) - except Exception as e: - logger.warning(f"[UniLab Registry] 处理资源文件异常: {file}, 错误: {e}") + # --- YAML-level cache: per-file entries with config_info --- + config_cache = self._load_config_cache() if upload_registry else None + yaml_cache: dict = config_cache.get("_yaml_resources", {}) if config_cache else {} + yaml_cache_hits = 0 + yaml_cache_misses = 0 + uncached_files: list[Path] = [] + yaml_file_rids: dict[str, list[str]] = {} - # 线程安全地更新注册表 - current_resource_number = len(self.resource_type_registry) + 1 - with self._registry_lock: - for i, (file, data) in enumerate(results): - self.resource_type_registry.update(data) - logger.trace( - f"[UniLab Registry] Resource-{current_resource_number} File-{i+1}/{len(results)} " - + f"Add {list(data.keys())}" - ) - current_resource_number += 1 - - # 记录无效文件 - valid_files = {r[0] for r in results} - for file in files: - if file not in valid_files: - logger.debug(f"[UniLab Registry] Res File Not Valid YAML File: {file.absolute()}") - - def _extract_class_docstrings(self, module_string: str) -> Dict[str, str]: - """ - 从模块字符串中提取类和方法的docstring信息 - - Args: - module_string: 模块字符串,格式为 "module.path:ClassName" - - Returns: - 包含类和方法docstring信息的字典 - """ - docstrings = {"class_docstring": "", "methods": {}} - - if not module_string or ":" not in module_string: - return docstrings - - try: - module_path, class_name = module_string.split(":", 1) - - # 动态导入模块 - module = importlib.import_module(module_path) - - # 获取类 - if hasattr(module, class_name): - cls = getattr(module, class_name) - - # 获取类的docstring - class_doc = inspect.getdoc(cls) - if class_doc: - docstrings["class_docstring"] = class_doc.strip() - - # 获取所有方法的docstring - for method_name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - method_doc = inspect.getdoc(method) - if method_doc: - docstrings["methods"][method_name] = method_doc.strip() - - # 也获取属性方法的docstring - for method_name, method in inspect.getmembers(cls, predicate=lambda x: isinstance(x, property)): - if hasattr(method, "fget") and method.fget: - method_doc = inspect.getdoc(method.fget) - if method_doc: - docstrings["methods"][method_name] = method_doc.strip() - - except Exception as e: - logger.warning(f"[UniLab Registry] 无法提取docstring信息,模块: {module_string}, 错误: {str(e)}") - - return docstrings - - def _replace_type_with_class(self, type_name: str, device_id: str, field_name: str) -> Any: - """ - 将类型名称替换为实际的类对象 - - Args: - type_name: 类型名称 - device_id: 设备ID,用于错误信息 - field_name: 字段名称,用于错误信息 - - Returns: - 找到的类对象或原始字符串 - - Raises: - SystemExit: 如果找不到类型则终止程序 - """ - # 如果类型名为空,跳过替换 - if not type_name or type_name == "": - logger.warning(f"[UniLab Registry] 设备 {device_id} 的 {field_name} 类型为空,跳过替换") - return type_name - convert_manager = { # 将python基本对象转为ros2基本对象 - "str": "String", - "bool": "Bool", - "int": "Int64", - "float": "Float64", - } - type_name = convert_manager.get(type_name, type_name) # 替换为ROS2类型 - if ":" in type_name: - type_class = msg_converter_manager.get_class(type_name) + if complete_registry: + uncached_files = files + yaml_cache_misses = len(files) else: - type_class = msg_converter_manager.search_class(type_name) - if type_class: - return type_class - else: - logger.error(f"[UniLab Registry] 无法找到类型 '{type_name}' 用于设备 {device_id} 的 {field_name}") - raise ROSMsgNotFound(f"类型 '{type_name}' 未找到,用于设备 {device_id} 的 {field_name}") + for file in files: + file_key = str(file.absolute()).replace("\\", "/") + if upload_registry and yaml_cache: + try: + yaml_md5 = _hl.md5(file.read_bytes()).hexdigest() + except OSError: + uncached_files.append(file) + yaml_cache_misses += 1 + continue + cached = yaml_cache.get(file_key) + if cached and cached.get("yaml_md5") == yaml_md5: + module_hashes: dict = cached.get("module_hashes", {}) + all_ok = all( + self._module_source_hash(m) == h + for m, h in module_hashes.items() + ) if module_hashes else True + if all_ok and cached.get("entries"): + for rid, entry in cached["entries"].items(): + self.resource_type_registry[rid] = entry + yaml_cache_hits += 1 + continue + uncached_files.append(file) + yaml_cache_misses += 1 - def _get_json_schema_type(self, type_str: str) -> str: - """ - 根据类型字符串返回对应的JSON Schema类型 - - Args: - type_str: 类型字符串 - - Returns: - JSON Schema类型字符串 - """ - type_lower = type_str.lower() - type_mapping = { - ("str", "string"): "string", - ("int", "integer"): "integer", - ("float", "number"): "number", - ("bool", "boolean"): "boolean", - ("list", "array"): "array", - ("dict", "object"): "object", + # Process uncached YAML files with thread pool + executor = self._startup_executor + future_to_file = { + executor.submit(self._load_single_resource_file, file, complete_registry): file + for file in uncached_files } - # 遍历映射找到匹配的类型 - for type_variants, json_type in type_mapping.items(): - if type_lower in type_variants: - return json_type + for future in as_completed(future_to_file): + file = future_to_file[future] + try: + data, complete_data, is_valid = future.result() + if is_valid: + self.resource_type_registry.update(complete_data) + file_key = str(file.absolute()).replace("\\", "/") + yaml_file_rids[file_key] = list(complete_data.keys()) + except Exception as e: + logger.warning(f"[UniLab Registry] 加载资源文件失败: {file}, 错误: {e}") - # 特殊处理包含冒号的类型(如ROS消息类型) - if ":" in type_lower: - return "object" + # upload 模式下,统一利用线程池为 pylabrobot 资源生成 config_info + if upload_registry: + self._populate_resource_config_info(config_cache=config_cache) - # 默认返回字符串类型 - return "string" + # Update YAML cache for newly processed files (entries now have config_info) + if yaml_file_rids and config_cache is not None: + for file_key, rids in yaml_file_rids.items(): + entries = {} + module_hashes = {} + for rid in rids: + entry = self.resource_type_registry.get(rid) + if entry: + entries[rid] = copy.deepcopy(entry) + mod_str = entry.get("class", {}).get("module", "") + if mod_str and mod_str not in module_hashes: + src_h = self._module_source_hash(mod_str) + if src_h: + module_hashes[mod_str] = src_h + try: + yaml_md5 = _hl.md5(Path(file_key).read_bytes()).hexdigest() + except OSError: + continue + yaml_cache[file_key] = { + "yaml_md5": yaml_md5, + "module_hashes": module_hashes, + "entries": entries, + } + config_cache["_yaml_resources"] = yaml_cache + self._save_config_cache(config_cache) - def _generate_schema_from_info( - self, - param_name: str, - param_type: Union[str, Tuple[str]], - param_default: Any, - ) -> Dict[str, Any]: - """ - 根据参数信息生成JSON Schema - """ - prop_schema = {} - - # 处理嵌套类型(Tuple[str]) - if isinstance(param_type, tuple): - if len(param_type) == 2: - outer_type, inner_type = param_type - outer_json_type = self._get_json_schema_type(outer_type) - inner_json_type = self._get_json_schema_type(inner_type) - - prop_schema["type"] = outer_json_type - - # 根据外层类型设置内层类型信息 - if outer_json_type == "array": - prop_schema["items"] = {"type": inner_json_type} - elif outer_json_type == "object": - prop_schema["additionalProperties"] = {"type": inner_json_type} - else: - # 不是标准的嵌套类型,默认为字符串 - prop_schema["type"] = "string" - else: - # 处理非嵌套类型 - if param_type: - prop_schema["type"] = self._get_json_schema_type(param_type) - else: - # 如果没有类型信息,默认为字符串 - prop_schema["type"] = "string" - - # 设置默认值 - if param_default is not None: - prop_schema["default"] = param_default - - return prop_schema - - def _generate_status_types_schema(self, status_types: Dict[str, Any]) -> Dict[str, Any]: - """ - 根据状态类型生成JSON Schema - """ - status_schema = { - "type": "object", - "properties": {}, - "required": [], - } - for status_name, status_type in status_types.items(): - status_schema["properties"][status_name] = self._generate_schema_from_info( - status_name, status_type["return_type"], None + total_yaml = yaml_cache_hits + yaml_cache_misses + if upload_registry and total_yaml > 0: + logger.info( + f"[UniLab Registry] YAML 资源缓存: " + f"{yaml_cache_hits}/{total_yaml} 文件命中, " + f"{yaml_cache_misses} 重新加载" ) - status_schema["required"].append(status_name) - return status_schema - - def _generate_unilab_json_command_schema( - self, - method_args: List[Dict[str, Any]], - method_name: str, - return_annotation: Any = None, - previous_schema: Dict[str, Any] | None = None, - ) -> Dict[str, Any]: - """ - 根据UniLabJsonCommand方法信息生成JSON Schema,暂不支持嵌套类型 - - Args: - method_args: 方法信息字典,包含args等 - method_name: 方法名称 - return_annotation: 返回类型注解,用于生成result schema(仅支持TypedDict) - previous_schema: 之前的 schema,用于保留 goal/feedback/result 下一级字段的 description - - Returns: - JSON Schema格式的参数schema - """ - schema = { - "type": "object", - "properties": {}, - "required": [], - } - for arg_info in method_args: - param_name = arg_info.get("name", "") - param_type = arg_info.get("type", "") - param_default = arg_info.get("default") - param_required = arg_info.get("required", True) - if param_type == "unilabos.registry.placeholder_type:ResourceSlot": - schema["properties"][param_name] = ros_message_to_json_schema(Resource, param_name) - elif param_type == ("list", "unilabos.registry.placeholder_type:ResourceSlot"): - schema["properties"][param_name] = { - "items": ros_message_to_json_schema(Resource, param_name), - "type": "array", - } - else: - schema["properties"][param_name] = self._generate_schema_from_info( - param_name, param_type, param_default - ) - if param_required: - schema["required"].append(param_name) - - # 生成result schema(仅当return_annotation是TypedDict时) - result_schema = {} - if return_annotation is not None and self._is_typed_dict(return_annotation): - result_schema = self._generate_typed_dict_result_schema(return_annotation) - - final_schema = { - "title": f"{method_name}参数", - "description": f"", - "type": "object", - "properties": {"goal": schema, "feedback": {}, "result": result_schema}, - "required": ["goal"], - } - - # 保留之前 schema 中 goal/feedback/result 下一级字段的 description - if previous_schema: - self._preserve_field_descriptions(final_schema, previous_schema) - - return final_schema - - def _preserve_field_descriptions(self, new_schema: Dict[str, Any], previous_schema: Dict[str, Any]) -> None: - """ - 保留之前 schema 中 goal/feedback/result 下一级字段的 description 和 title - - Args: - new_schema: 新生成的 schema(会被修改) - previous_schema: 之前的 schema - """ - for section in ["goal", "feedback", "result"]: - new_section = new_schema.get("properties", {}).get(section, {}) - prev_section = previous_schema.get("properties", {}).get(section, {}) - - if not new_section or not prev_section: - continue - - new_props = new_section.get("properties", {}) - prev_props = prev_section.get("properties", {}) - - for field_name, field_schema in new_props.items(): - if field_name in prev_props: - prev_field = prev_props[field_name] - # 保留字段的 description - if "description" in prev_field and prev_field["description"]: - field_schema["description"] = prev_field["description"] - # 保留字段的 title(用户自定义的中文名) - if "title" in prev_field and prev_field["title"]: - field_schema["title"] = prev_field["title"] - - def _is_typed_dict(self, annotation: Any) -> bool: - """ - 检查类型注解是否是TypedDict - - Args: - annotation: 类型注解对象 - - Returns: - 是否为TypedDict - """ - if annotation is None or annotation == inspect.Parameter.empty: - return False - - # 使用 typing_extensions.is_typeddict 进行检查(Python < 3.12 兼容) - try: - from typing_extensions import is_typeddict - - return is_typeddict(annotation) - except ImportError: - # 回退方案:检查 TypedDict 特有的属性 - if isinstance(annotation, type): - return hasattr(annotation, "__required_keys__") and hasattr(annotation, "__optional_keys__") - return False - - def _generate_typed_dict_result_schema(self, return_annotation: Any) -> Dict[str, Any]: - """ - 根据TypedDict类型生成result的JSON Schema - - Args: - return_annotation: TypedDict类型注解 - - Returns: - JSON Schema格式的result schema - """ - if not self._is_typed_dict(return_annotation): - return {} - - try: - from msgcenterpy.instances.typed_dict_instance import TypedDictMessageInstance - - result_schema = TypedDictMessageInstance.get_json_schema_from_typed_dict(return_annotation) - return result_schema - except ImportError: - logger.warning("[UniLab Registry] msgcenterpy未安装,无法生成TypedDict的result schema") - return {} - except Exception as e: - logger.warning(f"[UniLab Registry] 生成TypedDict result schema失败: {e}") - return {} - - def _add_builtin_actions(self, device_config: Dict[str, Any], device_id: str): - """ - 为设备配置添加内置的执行驱动命令动作 - - Args: - device_config: 设备配置字典 - device_id: 设备ID - """ - from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type - - if "class" not in device_config: - return - - if "action_value_mappings" not in device_config["class"]: - device_config["class"]["action_value_mappings"] = {} - - for additional_action in ["_execute_driver_command", "_execute_driver_command_async"]: - device_config["class"]["action_value_mappings"][additional_action] = { - "type": self._replace_type_with_class("StrSingleInput", device_id, f"动作 {additional_action}"), - "goal": {"string": "string"}, - "feedback": {}, - "result": {}, - "schema": ros_action_to_json_schema( - self._replace_type_with_class("StrSingleInput", device_id, f"动作 {additional_action}") - ), - "goal_default": yaml.safe_load( - io.StringIO( - get_yaml_from_goal_type( - self._replace_type_with_class( - "StrSingleInput", device_id, f"动作 {additional_action}" - ).Goal - ) - ) - ), - "handles": {}, - } def _load_single_device_file( - self, file: Path, complete_registry: bool, get_yaml_from_goal_type + self, file: Path, complete_registry: bool ) -> Tuple[Dict[str, Any], Dict[str, Any], bool, List[str]]: """ 加载单个设备文件 (线程安全) @@ -736,7 +1688,12 @@ class Registry: status_str_type_mapping = {} device_ids = [] + skip_ids = set() for device_id, device_config in data.items(): + if not isinstance(device_config, dict): + continue + + # 补全默认字段 if "version" not in device_config: device_config["version"] = "1.0.0" if "category" not in device_config: @@ -753,7 +1710,18 @@ class Registry: device_config["handles"] = [] if "init_param_schema" not in device_config: device_config["init_param_schema"] = {} + if "class" in device_config: + # --- AST 已有该设备 → 跳过,提示冗余 --- + if self.device_type_registry.get(device_id): + logger.warning( + f"[UniLab Registry] 设备 '{device_id}' 已由 AST 扫描注册," + f"YAML 定义冗余,跳过 YAML 处理" + ) + skip_ids.add(device_id) + continue + + # --- 正常 YAML 处理 --- if "status_types" not in device_config["class"] or device_config["class"]["status_types"] is None: device_config["class"]["status_types"] = {} if ( @@ -761,15 +1729,21 @@ class Registry: or device_config["class"]["action_value_mappings"] is None ): device_config["class"]["action_value_mappings"] = {} + enhanced_info = {} + enhanced_import_map: Dict[str, str] = {} if complete_registry: + original_status_keys = set(device_config["class"]["status_types"].keys()) device_config["class"]["status_types"].clear() - enhanced_info = get_enhanced_class_info(device_config["class"]["module"], use_dynamic=True) - if not enhanced_info.get("dynamic_import_success", False): + enhanced_info = get_enhanced_class_info(device_config["class"]["module"]) + if not enhanced_info.get("ast_analysis_success", False): continue - device_config["class"]["status_types"].update( - {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()} - ) + enhanced_import_map = enhanced_info.get("import_map", {}) + for st_k, st_v in enhanced_info["status_methods"].items(): + if st_k in original_status_keys: + device_config["class"]["status_types"][st_k] = st_v["return_type"] + + # --- status_types: 字符串 → class 映射 --- for status_name, status_type in device_config["class"]["status_types"].items(): if isinstance(status_type, tuple) or status_type in ["Any", "None", "Unknown"]: status_type = "String" @@ -782,68 +1756,137 @@ class Registry: target_type = String status_str_type_mapping[status_type] = target_type device_config["class"]["status_types"] = dict(sorted(device_config["class"]["status_types"].items())) + if complete_registry: - old_action_configs = {} - for action_name, action_config in device_config["class"]["action_value_mappings"].items(): - old_action_configs[action_name] = action_config + old_action_configs = dict(device_config["class"]["action_value_mappings"]) device_config["class"]["action_value_mappings"] = { k: v for k, v in device_config["class"]["action_value_mappings"].items() if not k.startswith("auto-") } - device_config["class"]["action_value_mappings"].update( - { - f"auto-{k}": { - "type": "UniLabJsonCommandAsync" if v["is_async"] else "UniLabJsonCommand", - "goal": {}, - "feedback": {}, - "result": {}, - "schema": self._generate_unilab_json_command_schema( - v["args"], - k, - v.get("return_annotation"), - old_action_configs.get(f"auto-{k}", {}).get("schema"), - ), - "goal_default": {i["name"]: i["default"] for i in v["args"]}, - "handles": old_action_configs.get(f"auto-{k}", {}).get("handles", []), - "placeholder_keys": { - i["name"]: ( - "unilabos_resources" - if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot" - or i["type"] == ("list", "unilabos.registry.placeholder_type:ResourceSlot") - else "unilabos_devices" - ) - for i in v["args"] - if i.get("type", "") - in [ - "unilabos.registry.placeholder_type:ResourceSlot", - "unilabos.registry.placeholder_type:DeviceSlot", - ("list", "unilabos.registry.placeholder_type:ResourceSlot"), - ("list", "unilabos.registry.placeholder_type:DeviceSlot"), - ] - }, - **({"always_free": True} if v.get("always_free") else {}), - } - for k, v in enhanced_info["action_methods"].items() - if k not in device_config["class"]["action_value_mappings"] - } - ) - for action_name, old_config in old_action_configs.items(): - if action_name in device_config["class"]["action_value_mappings"]: - old_schema = old_config.get("schema", {}) - if "description" in old_schema and old_schema["description"]: - device_config["class"]["action_value_mappings"][action_name]["schema"][ - "description" - ] = old_schema["description"] - device_config["init_param_schema"] = {} - device_config["init_param_schema"]["config"] = self._generate_unilab_json_command_schema( - enhanced_info["init_params"], "__init__" - )["properties"]["goal"] - device_config["init_param_schema"]["data"] = self._generate_status_types_schema( - enhanced_info["status_methods"] - ) + for k, v in enhanced_info["action_methods"].items(): + if k in device_config["class"]["action_value_mappings"]: + action_key = k + elif k.startswith("get_"): + continue + else: + action_key = f"auto-{k}" + goal_schema = self._generate_unilab_json_command_schema( + v["args"], import_map=enhanced_import_map + ) + ret_type = v.get("return_type", "") + result_schema = None + if ret_type and ret_type not in ("None", "Any", ""): + result_schema = self._generate_schema_from_info( + "result", ret_type, None, import_map=enhanced_import_map + ) + old_cfg = old_action_configs.get(action_key) or old_action_configs.get(f"auto-{k}", {}) + new_schema = wrap_action_schema(goal_schema, action_key, result_schema=result_schema) + old_schema = old_cfg.get("schema", {}) + if old_schema: + preserve_field_descriptions(new_schema, old_schema) + if "description" in old_schema: + new_schema["description"] = old_schema["description"] + new_schema.setdefault("description", "") + old_type = old_cfg.get("type", "") + entry_goal = old_cfg.get("goal", {}) + entry_feedback = {} + entry_result = {} + entry_schema = new_schema + entry_goal_default = {i["name"]: i.get("default") for i in v["args"]} + + if old_type and not old_type.startswith("UniLabJsonCommand"): + entry_type = old_type + try: + action_type_obj = self._replace_type_with_class( + old_type, device_id, f"动作 {action_key}" + ) + except ROSMsgNotFound: + action_type_obj = None + if action_type_obj is not None and not isinstance(action_type_obj, str): + real_params = [p for p in v["args"]] + ros_goal = {p["name"]: p["name"] for p in real_params} + try: + if hasattr(action_type_obj, "Goal"): + goal_fields = action_type_obj.Goal.get_fields_and_field_types() + ros2_goal = {f: f for f in goal_fields} + ros2_goal.update(ros_goal) + entry_goal = ros2_goal + except Exception: + pass + try: + if hasattr(action_type_obj, "Feedback"): + fb_fields = action_type_obj.Feedback.get_fields_and_field_types() + entry_feedback = {f: f for f in fb_fields} + except Exception: + pass + try: + if hasattr(action_type_obj, "Result"): + res_fields = action_type_obj.Result.get_fields_and_field_types() + entry_result = {f: f for f in res_fields} + except Exception: + pass + try: + entry_schema = ros_action_to_json_schema(action_type_obj) + if old_schema: + preserve_field_descriptions(entry_schema, old_schema) + if "description" in old_schema: + entry_schema["description"] = old_schema["description"] + entry_schema.setdefault("description", "") + except Exception: + pass + try: + entry_goal_default = ROS2MessageInstance( + action_type_obj.Goal() + ).get_python_dict() + except Exception: + entry_goal_default = old_cfg.get("goal_default", {}) + else: + entry_type = "UniLabJsonCommandAsync" if v["is_async"] else "UniLabJsonCommand" + + merged_pk = dict(old_cfg.get("placeholder_keys", {})) + merged_pk.update(detect_placeholder_keys(v["args"])) + + entry = { + "type": entry_type, + "goal": entry_goal, + "feedback": entry_feedback, + "result": entry_result, + "schema": entry_schema, + "goal_default": entry_goal_default, + "handles": old_cfg.get("handles", []), + "placeholder_keys": merged_pk, + } + if v.get("always_free"): + entry["always_free"] = True + device_config["class"]["action_value_mappings"][action_key] = entry + + device_config["init_param_schema"] = {} + init_schema = self._generate_unilab_json_command_schema( + enhanced_info["init_params"], "__init__", + import_map=enhanced_import_map, + ) + device_config["init_param_schema"]["config"] = init_schema + + data_schema: Dict[str, Any] = { + "type": "object", + "properties": {}, + "required": [], + } + for st_name in device_config["class"]["status_types"]: + st_type_str = device_config["class"]["status_types"][st_name] + if isinstance(st_type_str, str): + data_schema["properties"][st_name] = self._generate_schema_from_info( + st_name, st_type_str, None, import_map=enhanced_import_map + ) + else: + data_schema["properties"][st_name] = {"type": "string"} + data_schema["required"].append(st_name) + device_config["init_param_schema"]["data"] = data_schema + + # --- action_value_mappings: 处理非 UniLabJsonCommand 类型 --- device_config.pop("schema", None) device_config["class"]["action_value_mappings"] = dict( sorted(device_config["class"]["action_value_mappings"].items()) @@ -868,37 +1911,82 @@ class Registry: continue action_str_type_mapping[action_type_str] = target_type if target_type is not None: - action_config["goal_default"] = yaml.safe_load( - io.StringIO(get_yaml_from_goal_type(target_type.Goal)) - ) + try: + action_config["goal_default"] = ROS2MessageInstance(target_type.Goal()).get_python_dict() + except Exception: + action_config["goal_default"] = {} + prev_schema = action_config.get("schema", {}) action_config["schema"] = ros_action_to_json_schema(target_type) + if prev_schema: + preserve_field_descriptions(action_config["schema"], prev_schema) + if "description" in prev_schema: + action_config["schema"]["description"] = prev_schema["description"] + action_config["schema"].setdefault("description", "") else: logger.warning( f"[UniLab Registry] 设备 {device_id} 的动作 {action_name} 类型为空,跳过替换" ) + + # deepcopy 保存可序列化的 complete_data(此时 type 字段仍为字符串) + device_config["file_path"] = str(file.absolute()).replace("\\", "/") + device_config["registry_type"] = "device" complete_data[device_id] = copy.deepcopy(dict(sorted(device_config.items()))) + + # 之后才把 type 字符串替换为 class 对象(仅用于运行时 data) for status_name, status_type in device_config["class"]["status_types"].items(): - device_config["class"]["status_types"][status_name] = status_str_type_mapping[status_type] + if status_type in status_str_type_mapping: + device_config["class"]["status_types"][status_name] = status_str_type_mapping[status_type] for action_name, action_config in device_config["class"]["action_value_mappings"].items(): - if action_config["type"] not in action_str_type_mapping: - continue - action_config["type"] = action_str_type_mapping[action_config["type"]] + if action_config.get("type") in action_str_type_mapping: + action_config["type"] = action_str_type_mapping[action_config["type"]] + self._add_builtin_actions(device_config, device_id) - device_config["file_path"] = str(file.absolute()).replace("\\", "/") - device_config["registry_type"] = "device" + device_ids.append(device_id) + for did in skip_ids: + data.pop(did, None) + complete_data = dict(sorted(complete_data.items())) complete_data = copy.deepcopy(complete_data) - try: - with open(file, "w", encoding="utf-8") as f: - yaml.dump(complete_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper) - except Exception as e: - logger.warning(f"[UniLab Registry] 写入设备文件失败: {file}, 错误: {e}") + if complete_registry: + write_data = copy.deepcopy(complete_data) + for dev_id, dev_cfg in write_data.items(): + dev_cfg.pop("file_path", None) + dev_cfg.pop("registry_type", None) + try: + with open(file, "w", encoding="utf-8") as f: + yaml.dump(write_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper) + except Exception as e: + logger.warning(f"[UniLab Registry] 写入设备文件失败: {file}, 错误: {e}") return data, complete_data, True, device_ids - def load_device_types(self, path: os.PathLike, complete_registry: bool): + def _rebuild_device_runtime_data(self, complete_data: Dict[str, Any]) -> Dict[str, Any]: + """从 complete_data(纯字符串)重建运行时数据(type 字段替换为 class 对象)。""" + data = copy.deepcopy(complete_data) + for device_id, device_config in data.items(): + if "class" not in device_config: + continue + # status_types: str → class + for st_name, st_type in device_config["class"].get("status_types", {}).items(): + if isinstance(st_type, str): + device_config["class"]["status_types"][st_name] = self._replace_type_with_class( + st_type, device_id, f"状态 {st_name}" + ) + # action type: str → class (non-UniLabJsonCommand only) + for _act_name, act_cfg in device_config["class"].get("action_value_mappings", {}).items(): + t_ref = act_cfg.get("type", "") + if isinstance(t_ref, str) and t_ref and not t_ref.startswith("UniLabJsonCommand"): + resolved = self._replace_type_with_class(t_ref, device_id, f"动作 {_act_name}") + if resolved: + act_cfg["type"] = resolved + self._add_builtin_actions(device_config, device_id) + return data + + def load_device_types(self, path: os.PathLike, complete_registry: bool = False): + import hashlib as _hl + t0 = time.time() abs_path = Path(path).absolute() devices_path = abs_path / "devices" device_comms_path = abs_path / "device_comms" @@ -911,44 +1999,80 @@ class Registry: if not files: return - from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type + config_cache = self._load_config_cache() + yaml_dev_cache: dict = config_cache.get("_yaml_devices", {}) + cache_hits = 0 + uncached_files: list[Path] = [] - # 使用线程池并行加载 - max_workers = min(8, len(files)) - results = [] - - with ThreadPoolExecutor(max_workers=max_workers) as executor: - future_to_file = { - executor.submit(self._load_single_device_file, file, complete_registry, get_yaml_from_goal_type): file - for file in files - } - for future in as_completed(future_to_file): - file = future_to_file[future] + if complete_registry: + uncached_files = files + else: + for file in files: + file_key = str(file.absolute()).replace("\\", "/") try: - data, complete_data, is_valid, device_ids = future.result() - if is_valid: - results.append((file, data, device_ids)) - except Exception as e: - traceback.print_exc() - logger.warning(f"[UniLab Registry] 处理设备文件异常: {file}, 错误: {e}") + yaml_md5 = _hl.md5(file.read_bytes()).hexdigest() + except OSError: + uncached_files.append(file) + continue + cached = yaml_dev_cache.get(file_key) + if cached and cached.get("yaml_md5") == yaml_md5 and cached.get("entries"): + complete_data = cached["entries"] + # 过滤掉 AST 已有的设备 + complete_data = { + did: cfg for did, cfg in complete_data.items() + if not self.device_type_registry.get(did) + } + runtime_data = self._rebuild_device_runtime_data(complete_data) + self.device_type_registry.update(runtime_data) + cache_hits += 1 + continue + uncached_files.append(file) - # 线程安全地更新注册表 - current_device_number = len(self.device_type_registry) + 1 - with self._registry_lock: - for file, data, device_ids in results: - self.device_type_registry.update(data) - for device_id in device_ids: - logger.trace( - f"[UniLab Registry] Device-{current_device_number} Add {device_id} " - + f"[{data[device_id].get('name', '未命名设备')}]" - ) - current_device_number += 1 + executor = self._startup_executor + future_to_file = { + executor.submit( + self._load_single_device_file, file, complete_registry + ): file + for file in uncached_files + } - # 记录无效文件 - valid_files = {r[0] for r in results} - for file in files: - if file not in valid_files: - logger.debug(f"[UniLab Registry] Device File Not Valid YAML File: {file.absolute()}") + for future in as_completed(future_to_file): + file = future_to_file[future] + try: + data, _complete_data, is_valid, device_ids = future.result() + if is_valid: + runtime_data = {did: data[did] for did in device_ids if did in data} + self.device_type_registry.update(runtime_data) + # 写入缓存 + file_key = str(file.absolute()).replace("\\", "/") + try: + yaml_md5 = _hl.md5(file.read_bytes()).hexdigest() + yaml_dev_cache[file_key] = { + "yaml_md5": yaml_md5, + "entries": _complete_data, + } + except OSError: + pass + except Exception as e: + logger.warning(f"[UniLab Registry] 加载设备文件失败: {file}, 错误: {e}") + + if uncached_files and yaml_dev_cache: + latest_cache = self._load_config_cache() + latest_cache["_yaml_devices"] = yaml_dev_cache + self._save_config_cache(latest_cache) + + total = len(files) + extra = " (complete_registry 跳过缓存)" if complete_registry else "" + logger.info( + f"[UniLab Registry] YAML 设备加载: " + f"{cache_hits}/{total} 缓存命中, " + f"{len(uncached_files)} 重新加载 " + f"(耗时 {time.time() - t0:.2f}s){extra}" + ) + + # ------------------------------------------------------------------ + # 注册表信息输出 + # ------------------------------------------------------------------ def obtain_registry_device_info(self): devices = [] @@ -956,7 +2080,6 @@ class Registry: device_info_copy = copy.deepcopy(device_info) if "class" in device_info_copy and "action_value_mappings" in device_info_copy["class"]: action_mappings = device_info_copy["class"]["action_value_mappings"] - # 过滤掉内置的驱动命令动作 builtin_actions = ["_execute_driver_command", "_execute_driver_command_async"] filtered_action_mappings = { action_name: action_config @@ -966,6 +2089,9 @@ class Registry: device_info_copy["class"]["action_value_mappings"] = filtered_action_mappings for action_name, action_config in filtered_action_mappings.items(): + type_obj = action_config.get("type") + if hasattr(type_obj, "__name__"): + action_config["type"] = type_obj.__name__ if "schema" in action_config and action_config["schema"]: schema = action_config["schema"] # 确保schema结构存在 @@ -989,6 +2115,10 @@ class Registry: action_config["schema"]["properties"]["goal"]["_unilabos_placeholder_info"] = action_config[ "placeholder_keys" ] + status_types = device_info_copy["class"].get("status_types", {}) + for status_name, status_type in status_types.items(): + if hasattr(status_type, "__name__"): + status_types[status_name] = status_type.__name__ msg = {"id": device_id, **device_info_copy} devices.append(msg) @@ -1001,35 +2131,76 @@ class Registry: resources.append(msg) return resources + def get_yaml_output(self, device_id: str) -> str: + """将指定设备的注册表条目导出为 YAML 字符串。""" + entry = self.device_type_registry.get(device_id) + if not entry: + return "" + + entry = copy.deepcopy(entry) + + if "class" in entry: + status_types = entry["class"].get("status_types", {}) + for name, type_obj in status_types.items(): + if hasattr(type_obj, "__name__"): + status_types[name] = type_obj.__name__ + + for action_name, action_config in entry["class"].get("action_value_mappings", {}).items(): + type_obj = action_config.get("type") + if hasattr(type_obj, "__name__"): + action_config["type"] = type_obj.__name__ + + entry.pop("registry_type", None) + entry.pop("file_path", None) + + if "class" in entry and "action_value_mappings" in entry["class"]: + entry["class"]["action_value_mappings"] = { + k: v + for k, v in entry["class"]["action_value_mappings"].items() + if not k.startswith("_execute_driver_command") + } + + return yaml.dump( + {device_id: entry}, + allow_unicode=True, + default_flow_style=False, + Dumper=NoAliasDumper, + ) + + +# --------------------------------------------------------------------------- +# 全局单例实例 & 构建入口 +# --------------------------------------------------------------------------- -# 全局单例实例 lab_registry = Registry() -def build_registry(registry_paths=None, complete_registry=False, upload_registry=False): +def build_registry(registry_paths=None, devices_dirs=None, upload_registry=False, check_mode=False, complete_registry=False): """ 构建或获取Registry单例实例 - - Args: - registry_paths: 额外的注册表路径列表 - - Returns: - Registry实例 """ logger.info("[UniLab Registry] 构建注册表实例") - # 由于使用了单例,这里不需要重新创建实例 global lab_registry - # 如果有额外路径,添加到registry_paths if registry_paths: current_paths = lab_registry.registry_paths.copy() - # 检查是否有新路径需要添加 for path in registry_paths: if path not in current_paths: lab_registry.registry_paths.append(path) - # 初始化注册表 - lab_registry.setup(complete_registry, upload_registry) + lab_registry.setup(devices_dirs=devices_dirs, upload_registry=upload_registry, complete_registry=complete_registry) + + # 将 AST 扫描的字符串类型替换为实际 ROS2 消息类(仅查找 ROS2 类型,不 import 设备模块) + lab_registry.resolve_all_types() + + if check_mode: + lab_registry.verify_and_resolve_registry() + + # noinspection PyProtectedMember + if lab_registry._startup_executor is not None: + # noinspection PyProtectedMember + lab_registry._startup_executor.shutdown(wait=False) + lab_registry._startup_executor = None return lab_registry diff --git a/unilabos/registry/resources/bioyond/YB_bottle.yaml b/unilabos/registry/resources/bioyond/YB_bottle.yaml index f8e17261..19917372 100644 --- a/unilabos/registry/resources/bioyond/YB_bottle.yaml +++ b/unilabos/registry/resources/bioyond/YB_bottle.yaml @@ -9,7 +9,6 @@ YB_20ml_fenyeping: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_5ml_fenyeping: category: @@ -22,7 +21,6 @@ YB_5ml_fenyeping: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_jia_yang_tou_da: category: @@ -35,7 +33,6 @@ YB_jia_yang_tou_da: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_pei_ye_da_Bottle: category: @@ -48,7 +45,6 @@ YB_pei_ye_da_Bottle: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_pei_ye_xiao_Bottle: category: @@ -61,7 +57,6 @@ YB_pei_ye_xiao_Bottle: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_qiang_tou: category: @@ -74,7 +69,6 @@ YB_qiang_tou: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_ye_Bottle: category: @@ -88,5 +82,4 @@ YB_ye_Bottle: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml b/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml index 4698a266..76b6b938 100644 --- a/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml +++ b/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml @@ -9,7 +9,6 @@ YB_100ml_yeti: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_20ml_fenyepingban: category: @@ -22,7 +21,6 @@ YB_20ml_fenyepingban: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_5ml_fenyepingban: category: @@ -35,7 +33,6 @@ YB_5ml_fenyepingban: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_6StockCarrier: category: @@ -48,7 +45,6 @@ YB_6StockCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_6VialCarrier: category: @@ -61,7 +57,6 @@ YB_6VialCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_gao_nian_ye_Bottle: category: @@ -74,7 +69,6 @@ YB_gao_nian_ye_Bottle: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_gaonianye: category: @@ -87,7 +81,6 @@ YB_gaonianye: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_jia_yang_tou_da_Carrier: category: @@ -100,7 +93,6 @@ YB_jia_yang_tou_da_Carrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_peiyepingdaban: category: @@ -113,7 +105,6 @@ YB_peiyepingdaban: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_peiyepingxiaoban: category: @@ -126,7 +117,6 @@ YB_peiyepingxiaoban: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_qiang_tou_he: category: @@ -139,7 +129,6 @@ YB_qiang_tou_he: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_shi_pei_qi_kuai: category: @@ -152,7 +141,6 @@ YB_shi_pei_qi_kuai: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_ye: category: @@ -165,7 +153,6 @@ YB_ye: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 YB_ye_100ml_Bottle: category: @@ -178,5 +165,4 @@ YB_ye_100ml_Bottle: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/bioyond/bottle_carriers.yaml b/unilabos/registry/resources/bioyond/bottle_carriers.yaml index 764a8aa5..f72cc10d 100644 --- a/unilabos/registry/resources/bioyond/bottle_carriers.yaml +++ b/unilabos/registry/resources/bioyond/bottle_carriers.yaml @@ -8,7 +8,6 @@ BIOYOND_PolymerStation_1BottleCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 BIOYOND_PolymerStation_1FlaskCarrier: category: @@ -20,7 +19,6 @@ BIOYOND_PolymerStation_1FlaskCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 BIOYOND_PolymerStation_6StockCarrier: category: @@ -32,7 +30,6 @@ BIOYOND_PolymerStation_6StockCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 BIOYOND_PolymerStation_8StockCarrier: category: @@ -44,5 +41,4 @@ BIOYOND_PolymerStation_8StockCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/bioyond/deck.yaml b/unilabos/registry/resources/bioyond/deck.yaml index 8d6993b1..5770a2d1 100644 --- a/unilabos/registry/resources/bioyond/deck.yaml +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -8,7 +8,6 @@ BIOYOND_PolymerPreparationStation_Deck: handles: [] icon: 配液站.webp init_param_schema: {} - registry_type: resource version: 1.0.0 BIOYOND_PolymerReactionStation_Deck: category: @@ -20,7 +19,6 @@ BIOYOND_PolymerReactionStation_Deck: handles: [] icon: 反应站.webp init_param_schema: {} - registry_type: resource version: 1.0.0 BIOYOND_YB_Deck: category: @@ -32,7 +30,6 @@ BIOYOND_YB_Deck: handles: [] icon: 配液站.webp init_param_schema: {} - registry_type: resource version: 1.0.0 CoincellDeck: category: @@ -44,5 +41,4 @@ CoincellDeck: handles: [] icon: koudian.webp init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/common/resource_container.yaml b/unilabos/registry/resources/common/resource_container.yaml index 48dcab59..3f0aa9d2 100644 --- a/unilabos/registry/resources/common/resource_container.yaml +++ b/unilabos/registry/resources/common/resource_container.yaml @@ -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: category: - resource_container @@ -40,56 +19,6 @@ hplc_plate: - 3.1416 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/hplc_plate/modal.xacro 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 plate_96: category: @@ -112,7 +41,6 @@ plate_96: - 0 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96/modal.xacro type: resource - registry_type: resource version: 1.0.0 plate_96_high: category: @@ -135,35 +63,6 @@ plate_96_high: - 1.5708 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96_high/modal.xacro 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 tiprack_96_high: category: @@ -195,7 +94,6 @@ tiprack_96_high: - 1.5708 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_96_high/modal.xacro type: resource - registry_type: resource version: 1.0.0 tiprack_box: category: @@ -227,5 +125,4 @@ tiprack_box: - 0 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_box/modal.xacro type: resource - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/laiyu/container.yaml b/unilabos/registry/resources/laiyu/container.yaml index 1652956e..586e3cfe 100644 --- a/unilabos/registry/resources/laiyu/container.yaml +++ b/unilabos/registry/resources/laiyu/container.yaml @@ -29,7 +29,6 @@ bottle_container: - 0 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/bottle_container/modal.xacro type: resource - registry_type: resource version: 1.0.0 tube_container: category: @@ -62,5 +61,4 @@ tube_container: - 0 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tube_container/modal.xacro type: resource - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/laiyu/deck.yaml b/unilabos/registry/resources/laiyu/deck.yaml index e6d930a5..85da0ca7 100644 --- a/unilabos/registry/resources/laiyu/deck.yaml +++ b/unilabos/registry/resources/laiyu/deck.yaml @@ -12,5 +12,4 @@ TransformXYZDeck: mesh: liquid_transform_xyz path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/liquid_transform_xyz/macro_device.xacro type: device - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/opentrons/deck.yaml b/unilabos/registry/resources/opentrons/deck.yaml index 8fa35ee5..10e91cef 100644 --- a/unilabos/registry/resources/opentrons/deck.yaml +++ b/unilabos/registry/resources/opentrons/deck.yaml @@ -12,7 +12,6 @@ OTDeck: mesh: opentrons_liquid_handler path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/opentrons_liquid_handler/macro_device.xacro type: device - registry_type: resource version: 1.0.0 hplc_station: category: @@ -28,5 +27,4 @@ hplc_station: mesh: hplc_station path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/hplc_station/macro_device.xacro type: device - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/opentrons/plate_adapters.yaml b/unilabos/registry/resources/opentrons/plate_adapters.yaml index d2942d46..d09bf784 100644 --- a/unilabos/registry/resources/opentrons/plate_adapters.yaml +++ b/unilabos/registry/resources/opentrons/plate_adapters.yaml @@ -8,5 +8,4 @@ Opentrons_96_adapter_Vb: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/opentrons/plates.yaml b/unilabos/registry/resources/opentrons/plates.yaml index 02267ae0..20a71995 100644 --- a/unilabos/registry/resources/opentrons/plates.yaml +++ b/unilabos/registry/resources/opentrons/plates.yaml @@ -8,7 +8,6 @@ appliedbiosystemsmicroamp_384_wellplate_40ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 biorad_384_wellplate_50ul: category: @@ -20,7 +19,6 @@ biorad_384_wellplate_50ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 biorad_96_wellplate_200ul_pcr: category: @@ -32,7 +30,6 @@ biorad_96_wellplate_200ul_pcr: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 corning_12_wellplate_6point9ml_flat: category: @@ -44,7 +41,6 @@ corning_12_wellplate_6point9ml_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 corning_24_wellplate_3point4ml_flat: category: @@ -56,7 +52,6 @@ corning_24_wellplate_3point4ml_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 corning_384_wellplate_112ul_flat: category: @@ -68,7 +63,6 @@ corning_384_wellplate_112ul_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 corning_48_wellplate_1point6ml_flat: category: @@ -80,7 +74,6 @@ corning_48_wellplate_1point6ml_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 corning_6_wellplate_16point8ml_flat: category: @@ -92,7 +85,6 @@ corning_6_wellplate_16point8ml_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 corning_96_wellplate_360ul_flat: category: @@ -104,7 +96,6 @@ corning_96_wellplate_360ul_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 nest_96_wellplate_100ul_pcr_full_skirt: category: @@ -136,7 +127,6 @@ nest_96_wellplate_100ul_pcr_full_skirt: - 1.5708 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro type: resource - registry_type: resource version: 1.0.0 nest_96_wellplate_200ul_flat: category: @@ -148,7 +138,6 @@ nest_96_wellplate_200ul_flat: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 nest_96_wellplate_2ml_deep: category: @@ -171,7 +160,6 @@ nest_96_wellplate_2ml_deep: - 1.5708 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro type: resource - registry_type: resource version: 1.0.0 thermoscientificnunc_96_wellplate_1300ul: category: @@ -183,7 +171,6 @@ thermoscientificnunc_96_wellplate_1300ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 thermoscientificnunc_96_wellplate_2000ul: category: @@ -195,7 +182,6 @@ thermoscientificnunc_96_wellplate_2000ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 usascientific_96_wellplate_2point4ml_deep: category: @@ -207,5 +193,4 @@ usascientific_96_wellplate_2point4ml_deep: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/opentrons/reservoirs.yaml b/unilabos/registry/resources/opentrons/reservoirs.yaml index b2f7857b..6b2033d9 100644 --- a/unilabos/registry/resources/opentrons/reservoirs.yaml +++ b/unilabos/registry/resources/opentrons/reservoirs.yaml @@ -8,7 +8,6 @@ agilent_1_reservoir_290ml: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 axygen_1_reservoir_90ml: category: @@ -20,7 +19,6 @@ axygen_1_reservoir_90ml: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 nest_12_reservoir_15ml: category: @@ -32,7 +30,6 @@ nest_12_reservoir_15ml: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 nest_1_reservoir_195ml: category: @@ -44,7 +41,6 @@ nest_1_reservoir_195ml: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 nest_1_reservoir_290ml: category: @@ -56,7 +52,6 @@ nest_1_reservoir_290ml: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 usascientific_12_reservoir_22ml: category: @@ -68,5 +63,4 @@ usascientific_12_reservoir_22ml: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/opentrons/tip_racks.yaml b/unilabos/registry/resources/opentrons/tip_racks.yaml index cbc7d6f1..d1682b2a 100644 --- a/unilabos/registry/resources/opentrons/tip_racks.yaml +++ b/unilabos/registry/resources/opentrons/tip_racks.yaml @@ -8,7 +8,6 @@ eppendorf_96_tiprack_1000ul_eptips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 eppendorf_96_tiprack_10ul_eptips: category: @@ -20,7 +19,6 @@ eppendorf_96_tiprack_10ul_eptips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 geb_96_tiprack_1000ul: category: @@ -32,7 +30,6 @@ geb_96_tiprack_1000ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 geb_96_tiprack_10ul: category: @@ -44,7 +41,6 @@ geb_96_tiprack_10ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_filtertiprack_1000ul: category: @@ -75,7 +71,6 @@ opentrons_96_filtertiprack_1000ul: - 1.5708 path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro type: resource - registry_type: resource version: 1.0.0 opentrons_96_filtertiprack_10ul: category: @@ -87,7 +82,6 @@ opentrons_96_filtertiprack_10ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_filtertiprack_200ul: category: @@ -99,7 +93,6 @@ opentrons_96_filtertiprack_200ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_filtertiprack_20ul: category: @@ -111,7 +104,6 @@ opentrons_96_filtertiprack_20ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_tiprack_1000ul: category: @@ -123,7 +115,6 @@ opentrons_96_tiprack_1000ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_tiprack_10ul: category: @@ -135,7 +126,6 @@ opentrons_96_tiprack_10ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_tiprack_20ul: category: @@ -147,7 +137,6 @@ opentrons_96_tiprack_20ul: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_tiprack_300ul: category: diff --git a/unilabos/registry/resources/opentrons/tube_racks.yaml b/unilabos/registry/resources/opentrons/tube_racks.yaml index 32bf3e36..33ec5dc9 100644 --- a/unilabos/registry/resources/opentrons/tube_racks.yaml +++ b/unilabos/registry/resources/opentrons/tube_racks.yaml @@ -8,7 +8,6 @@ opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic: category: @@ -20,7 +19,6 @@ opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_10_tuberack_nest_4x50ml_6x15ml_conical: category: @@ -32,7 +30,6 @@ opentrons_10_tuberack_nest_4x50ml_6x15ml_conical: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_15_tuberack_falcon_15ml_conical: category: @@ -44,7 +41,6 @@ opentrons_15_tuberack_falcon_15ml_conical: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_15_tuberack_nest_15ml_conical: category: @@ -56,7 +52,6 @@ opentrons_15_tuberack_nest_15ml_conical: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_aluminumblock_generic_2ml_screwcap: category: @@ -68,7 +63,6 @@ opentrons_24_aluminumblock_generic_2ml_screwcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_aluminumblock_nest_1point5ml_snapcap: category: @@ -80,7 +74,6 @@ opentrons_24_aluminumblock_nest_1point5ml_snapcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap: category: @@ -92,7 +85,6 @@ opentrons_24_tuberack_eppendorf_1point5ml_safelock_snapcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap: category: @@ -104,7 +96,6 @@ opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic: category: @@ -116,7 +107,6 @@ opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap_acrylic: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic: category: @@ -128,7 +118,6 @@ opentrons_24_tuberack_generic_0point75ml_snapcap_acrylic: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_generic_2ml_screwcap: category: @@ -140,7 +129,6 @@ opentrons_24_tuberack_generic_2ml_screwcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_nest_0point5ml_screwcap: category: @@ -152,7 +140,6 @@ opentrons_24_tuberack_nest_0point5ml_screwcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_nest_1point5ml_screwcap: category: @@ -164,7 +151,6 @@ opentrons_24_tuberack_nest_1point5ml_screwcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_nest_1point5ml_snapcap: category: @@ -176,7 +162,6 @@ opentrons_24_tuberack_nest_1point5ml_snapcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_nest_2ml_screwcap: category: @@ -188,7 +173,6 @@ opentrons_24_tuberack_nest_2ml_screwcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_24_tuberack_nest_2ml_snapcap: category: @@ -200,7 +184,6 @@ opentrons_24_tuberack_nest_2ml_snapcap: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_6_tuberack_falcon_50ml_conical: category: @@ -212,7 +195,6 @@ opentrons_6_tuberack_falcon_50ml_conical: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_6_tuberack_nest_50ml_conical: category: @@ -224,7 +206,6 @@ opentrons_6_tuberack_nest_50ml_conical: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 opentrons_96_well_aluminum_block: category: @@ -236,5 +217,4 @@ opentrons_96_well_aluminum_block: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/organic/container.yaml b/unilabos/registry/resources/organic/container.yaml index a8fb9b6c..240cdf79 100644 --- a/unilabos/registry/resources/organic/container.yaml +++ b/unilabos/registry/resources/organic/container.yaml @@ -29,5 +29,4 @@ container: side: WEST icon: Flask.webp init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/post_process/bottle_carriers.yaml b/unilabos/registry/resources/post_process/bottle_carriers.yaml index ea30cb7d..45054311 100644 --- a/unilabos/registry/resources/post_process/bottle_carriers.yaml +++ b/unilabos/registry/resources/post_process/bottle_carriers.yaml @@ -8,7 +8,6 @@ POST_PROCESS_Raw_1BottleCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 POST_PROCESS_Reaction_1BottleCarrier: category: @@ -20,5 +19,4 @@ POST_PROCESS_Reaction_1BottleCarrier: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/post_process/deck.yaml b/unilabos/registry/resources/post_process/deck.yaml index 621cafc6..e5d4cc8d 100644 --- a/unilabos/registry/resources/post_process/deck.yaml +++ b/unilabos/registry/resources/post_process/deck.yaml @@ -9,5 +9,4 @@ post_process_deck: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/prcxi/plate_adapters.yaml b/unilabos/registry/resources/prcxi/plate_adapters.yaml index a769fee3..3e960f2e 100644 --- a/unilabos/registry/resources/prcxi/plate_adapters.yaml +++ b/unilabos/registry/resources/prcxi/plate_adapters.yaml @@ -9,7 +9,6 @@ PRCXI_30mm_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Adapter: category: @@ -22,7 +21,6 @@ PRCXI_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Deep10_Adapter: category: @@ -35,7 +33,6 @@ PRCXI_Deep10_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Deep300_Adapter: category: @@ -48,7 +45,6 @@ PRCXI_Deep300_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_PCR_Adapter: category: @@ -61,7 +57,6 @@ PRCXI_PCR_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Reservoir_Adapter: category: @@ -74,7 +69,6 @@ PRCXI_Reservoir_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Tip10_Adapter: category: @@ -87,7 +81,6 @@ PRCXI_Tip10_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Tip1250_Adapter: category: @@ -100,7 +93,6 @@ PRCXI_Tip1250_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_Tip300_Adapter: category: @@ -113,5 +105,4 @@ PRCXI_Tip300_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/prcxi/plates.yaml b/unilabos/registry/resources/prcxi/plates.yaml index 81e2ae96..b8527dbf 100644 --- a/unilabos/registry/resources/prcxi/plates.yaml +++ b/unilabos/registry/resources/prcxi/plates.yaml @@ -9,7 +9,6 @@ PRCXI_48_DeepWell: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_96_DeepWell: category: @@ -22,7 +21,6 @@ PRCXI_96_DeepWell: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_AGenBio_4_troughplate: category: @@ -35,7 +33,6 @@ PRCXI_AGenBio_4_troughplate: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_BioER_96_wellplate: category: @@ -48,7 +45,6 @@ PRCXI_BioER_96_wellplate: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_BioRad_384_wellplate: category: @@ -61,7 +57,6 @@ PRCXI_BioRad_384_wellplate: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_CellTreat_96_wellplate: category: @@ -74,7 +69,6 @@ PRCXI_CellTreat_96_wellplate: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_PCR_Plate_200uL_nonskirted: category: @@ -87,7 +81,6 @@ PRCXI_PCR_Plate_200uL_nonskirted: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_PCR_Plate_200uL_semiskirted: category: @@ -100,7 +93,6 @@ PRCXI_PCR_Plate_200uL_semiskirted: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_PCR_Plate_200uL_skirted: category: @@ -113,7 +105,6 @@ PRCXI_PCR_Plate_200uL_skirted: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_nest_12_troughplate: category: @@ -126,7 +117,6 @@ PRCXI_nest_12_troughplate: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_nest_1_troughplate: category: @@ -139,5 +129,4 @@ PRCXI_nest_1_troughplate: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/prcxi/tip_racks.yaml b/unilabos/registry/resources/prcxi/tip_racks.yaml index 56a16db8..f6d2e7f0 100644 --- a/unilabos/registry/resources/prcxi/tip_racks.yaml +++ b/unilabos/registry/resources/prcxi/tip_racks.yaml @@ -9,7 +9,6 @@ PRCXI_1000uL_Tips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_10uL_Tips: category: @@ -22,7 +21,6 @@ PRCXI_10uL_Tips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_10ul_eTips: category: @@ -35,7 +33,6 @@ PRCXI_10ul_eTips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_1250uL_Tips: category: @@ -48,7 +45,6 @@ PRCXI_1250uL_Tips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_200uL_Tips: category: @@ -61,7 +57,6 @@ PRCXI_200uL_Tips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 PRCXI_300ul_Tips: category: @@ -74,5 +69,4 @@ PRCXI_300ul_Tips: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/prcxi/trash.yaml b/unilabos/registry/resources/prcxi/trash.yaml index f87a7624..952a832b 100644 --- a/unilabos/registry/resources/prcxi/trash.yaml +++ b/unilabos/registry/resources/prcxi/trash.yaml @@ -9,5 +9,4 @@ PRCXI_trash: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/resources/prcxi/tube_racks.yaml b/unilabos/registry/resources/prcxi/tube_racks.yaml index 0b1e07c6..6510c16c 100644 --- a/unilabos/registry/resources/prcxi/tube_racks.yaml +++ b/unilabos/registry/resources/prcxi/tube_racks.yaml @@ -9,5 +9,4 @@ PRCXI_EP_Adapter: handles: [] icon: '' init_param_schema: {} - registry_type: resource version: 1.0.0 diff --git a/unilabos/registry/utils.py b/unilabos/registry/utils.py new file mode 100644 index 00000000..1ab7dd2c --- /dev/null +++ b/unilabos/registry/utils.py @@ -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 {} diff --git a/unilabos/resources/container.py b/unilabos/resources/container.py index ed3871d3..08d40af0 100644 --- a/unilabos/resources/container.py +++ b/unilabos/resources/container.py @@ -12,9 +12,11 @@ class RegularContainer(Container): kwargs["size_y"] = 0 if "size_z" not in kwargs: kwargs["size_z"] = 0 + if "category" not in kwargs: + kwargs["category"] = "container" self.kwargs = kwargs - super().__init__(*args, category="container", **kwargs) + super().__init__(*args, **kwargs) def load_state(self, state: Dict[str, Any]): super().load_state(state) diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 17eaa68a..5a37c4c7 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -76,7 +76,7 @@ def canonicalize_nodes_data( if sample_id: logger.error(f"{node}的sample_id参数已弃用,sample_id: {sample_id}") 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) node["config"][k] = v 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) 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 @@ -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) 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 diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index baf0ed16..3fb945b6 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -75,14 +75,6 @@ class ResourceDictPositionObject(BaseModel): z: float = Field(description="Z coordinate", default=0.0) -class ResourceDictPoseExtraObjectType(BaseModel): - z_index: int - - -class ResourceDictPoseExtraObject(BaseModel): - z_index: Optional[int] = Field(alias="zIndex", default=None) - - class ResourceDictPositionType(TypedDict): size: ResourceDictPositionSizeType scale: ResourceDictPositionScaleType @@ -109,7 +101,7 @@ class ResourceDictPosition(BaseModel): cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field( description="Cross section type", default="rectangle" ) - extra: Optional[ResourceDictPoseExtraObject] = Field(description="Extra data", default=None) + extra: Optional[Dict[str, Any]] = Field(description="Extra data", default=None) class ResourceDictType(TypedDict): @@ -128,6 +120,7 @@ class ResourceDictType(TypedDict): config: Dict[str, Any] data: Dict[str, Any] extra: Dict[str, Any] + machine_name: str # 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化 @@ -147,8 +140,9 @@ class ResourceDict(BaseModel): klass: str = Field(alias="class", description="Resource class name") pose: ResourceDictPosition = Field(description="Resource position", default_factory=ResourceDictPosition) config: Dict[str, Any] = Field(description="Resource configuration") - data: Dict[str, Any] = Field(description="Resource data") - extra: Dict[str, Any] = Field(description="Extra data") + data: Dict[str, Any] = Field(description="Resource data, eg: container liquid data") + 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") def _serialize_parent(self, parent_uuid: Optional["ResourceDict"]): @@ -204,22 +198,30 @@ class ResourceDictInstance(object): self.typ = "dict" @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: content["id"] = content["name"] if "uuid" not in content: content["uuid"] = str(uuid.uuid4()) if "description" in content and content["description"] is None: + # noinspection PyTypedDict del content["description"] if "model" in content and content["model"] is None: + # noinspection PyTypedDict del content["model"] + # noinspection PyTypedDict if "schema" in content and content["schema"] is None: + # noinspection PyTypedDict del content["schema"] + # noinspection PyTypedDict if "x" in content.get("position", {}): # 说明是老版本的position格式,转换成新的 + # noinspection PyTypedDict content["position"] = {"position": content["position"]} + # noinspection PyTypedDict if not content.get("class"): + # noinspection PyTypedDict content["class"] = "" if not content.get("config"): # todo: 后续从后端保证字段非空 content["config"] = {} @@ -230,16 +232,18 @@ class ResourceDictInstance(object): if "position" in content: pose = content.get("pose", {}) if "position" not in pose: + # noinspection PyTypedDict if "position" in content["position"]: + # noinspection PyTypedDict pose["position"] = content["position"]["position"] else: - pose["position"] = {"x": 0, "y": 0, "z": 0} + pose["position"] = ResourceDictPositionObjectType(x=0, y=0, z=0) if "size" not in pose: - pose["size"] = { - "width": content["config"].get("size_x", 0), - "height": content["config"].get("size_y", 0), - "depth": content["config"].get("size_z", 0), - } + pose["size"] = ResourceDictPositionSizeType( + width= content["config"].get("size_x", 0), + height= content["config"].get("size_y", 0), + depth= content["config"].get("size_z", 0), + ) content["pose"] = pose try: res_dict = ResourceDict.model_validate(content) @@ -407,7 +411,7 @@ class ResourceTreeSet(object): ) @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 """ @@ -430,13 +434,20 @@ class ResourceTreeSet(object): "resource_group": "resource_group", "trash": "trash", "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: return replace_info[source] elif source is None: return "" else: - print("转换pylabrobot的时候,出现未知类型", source) + logger.trace(f"转换pylabrobot的时候,出现未知类型 {source}") return source def build_uuid_mapping(res: "PLRResource", uuid_list: list, parent_uuid: Optional[str] = None): @@ -491,7 +502,7 @@ class ResourceTreeSet(object): k: v for k, v in d.items() if k - not in [ + not in ([ "name", "children", "parent_name", @@ -502,7 +513,15 @@ class ResourceTreeSet(object): "size_z", "cross_section_type", "bottom_type", - ] + ] if not old_size else [ + "name", + "children", + "parent_name", + "location", + "rotation", + "cross_section_type", + "bottom_type", + ]) }, "data": states[d["name"]], "extra": extra, @@ -801,7 +820,8 @@ class ResourceTreeSet(object): if remote_root_type == "device": # 情况1: 一级是 device 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 local_device = local_device_map[remote_root_id] @@ -848,14 +868,27 @@ class ResourceTreeSet(object): f"从远端同步了 {added_count} 个物料子树" ) else: - # 情况2: 二级是物料(不是 device) - if remote_child_name not in local_children_map: - # 引入整个子树 - remote_child.res_content.parent = local_device.res_content - local_device.children.append(remote_child) - logger.info(f"Device '{remote_root_id}': 从远端同步物料子树 '{remote_child_name}'") - else: - logger.info(f"物料 '{remote_root_id}/{remote_child_name}' 已存在,跳过") + # 二级物料已存在,比较三级子节点是否缺失 + local_material = local_children_map[remote_child_name] + local_material_children_map = {child.res_content.name: child for child in + local_material.children} + added_count = 0 + for remote_sub in remote_child.children: + remote_sub_name = remote_sub.res_content.name + if remote_sub_name not in local_material_children_map: + remote_sub.res_content.parent = local_material.res_content + local_material.children.append(remote_sub) + added_count += 1 + else: + logger.info( + f"物料 '{remote_root_id}/{remote_child_name}/{remote_sub_name}' " + f"已存在,跳过" + ) + if added_count > 0: + logger.info( + f"物料 '{remote_root_id}/{remote_child_name}': " + f"从远端同步了 {added_count} 个子物料" + ) else: # 情况1: 一级节点是物料(不是 device) # 检查是否已存在 @@ -878,7 +911,7 @@ class ResourceTreeSet(object): return self - def dump(self) -> List[List[Dict[str, Any]]]: + def dump(self, old_position=False) -> List[List[Dict[str, Any]]]: """ 将 ResourceTreeSet 序列化为嵌套列表格式 @@ -894,6 +927,10 @@ class ResourceTreeSet(object): # 获取树的所有节点并序列化 tree_nodes = [node.res_content.model_dump(by_alias=True) for node in tree.get_all_nodes()] result.append(tree_nodes) + if old_position: + for r in result: + for rr in r: + rr["position"] = rr["pose"]["position"] return result @classmethod diff --git a/unilabos/ros/msgs/message_converter.py b/unilabos/ros/msgs/message_converter.py index b526d5f5..83e6f456 100644 --- a/unilabos/ros/msgs/message_converter.py +++ b/unilabos/ros/msgs/message_converter.py @@ -11,6 +11,7 @@ from io import StringIO from typing import Iterable, Any, Dict, Type, TypeVar, Union import yaml +from msgcenterpy.instances.ros2_instance import ROS2MessageInstance from pydantic import BaseModel from dataclasses import asdict, is_dataclass @@ -716,6 +717,19 @@ def ros_field_type_to_json_schema( # 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]: """ 将 ROS 消息类转换为 JSON Schema @@ -727,46 +741,10 @@ def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any Returns: 对应的 JSON Schema 定义 """ - schema = {"type": "object", "properties": {}, "required": []} - - # 优先使用字段名作为标题,否则使用类名 + schema = ROS2MessageInstance(msg_class()).get_json_schema() schema["title"] = field_name - - # 获取消息的字段和字段类型 - 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)}") - + schema.pop("description", None) + _strip_rosidl_descriptions(schema) return schema @@ -813,6 +791,8 @@ def ros_action_to_json_schema( "required": ["goal"], } + _strip_rosidl_descriptions(schema) + # 保留之前 schema 中 goal/feedback/result 下一级字段的 description if previous_schema: _preserve_field_descriptions(schema, previous_schema) diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 6ff8cc57..ffc106c7 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -34,7 +34,8 @@ from unilabos_msgs.action import SendCmd from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response 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.graphio import ( @@ -57,6 +58,7 @@ from unilabos_msgs.msg import Resource # type: ignore from unilabos.resources.resource_tracker import ( DeviceNodeResourceTracker, + ResourceDictType, ResourceTreeSet, ResourceTreeInstance, ResourceDictInstance, @@ -194,9 +196,9 @@ class PropertyPublisher: self._value = None try: self.publisher_ = node.create_publisher(msg_type, f"{name}", qos) - except AttributeError as ex: + except Exception as e: 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.__loop = ROS2DeviceNode.get_asyncio_loop() @@ -569,9 +571,11 @@ class BaseROS2DeviceNode(Node, Generic[T]): future.add_done_callback(done_cb) except ImportError: self.lab_logger().error("Host请求添加物料时,本环境并不存在pylabrobot") + res.response = get_result_info_str(traceback.format_exc(), False, {}) except Exception as e: self.lab_logger().error("Host请求添加物料时出错") self.lab_logger().error(traceback.format_exc()) + res.response = get_result_info_str(traceback.format_exc(), False, {}) return res # noinspection PyTypeChecker @@ -594,6 +598,12 @@ class BaseROS2DeviceNode(Node, Generic[T]): self.s2c_resource_tree, # type: ignore 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 + 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( self, plr_resources: List["ResourcePLR"], @@ -1204,22 +1256,40 @@ class BaseROS2DeviceNode(Node, Generic[T]): return self._lab_logger def create_ros_publisher(self, attr_name, msg_type, initial_period=5.0): - """创建ROS发布者""" - # 检测装饰器配置(支持 get_{attr_name} 方法和 @property) + """创建ROS发布者,仅当方法/属性有 @topic_config 装饰器时才创建。""" + # 检测 @topic_config 装饰器配置 topic_config = {} + driver_class = type(self.driver_instance) - # 优先检测 get_{attr_name} 方法 - if hasattr(self.driver_instance, f"get_{attr_name}"): - getter_method = getattr(self.driver_instance, f"get_{attr_name}") - topic_config = get_topic_config(getter_method) + # 区分 @property 和普通方法两种情况 + is_prop = hasattr(driver_class, attr_name) and isinstance( + getattr(driver_class, attr_name), property + ) - # 如果没有配置,检测 @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: - driver_class = type(self.driver_instance) - if hasattr(driver_class, attr_name): - class_attr = getattr(driver_class, attr_name) - if isinstance(class_attr, property) and class_attr.fget is not None: - topic_config = get_topic_config(class_attr.fget) + return + + # 发布名称优先级: @topic_config(name=...) > get_ 前缀去除 > attr_name + cfg_name = topic_config.get("name") + 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") @@ -1232,10 +1302,10 @@ class BaseROS2DeviceNode(Node, Generic[T]): # 获取属性值的方法 def get_device_attr(): try: - if hasattr(self.driver_instance, f"get_{attr_name}"): - return getattr(self.driver_instance, f"get_{attr_name}")() - else: + if is_prop: return getattr(self.driver_instance, attr_name) + else: + return getattr(self.driver_instance, attr_name)() except AttributeError as ex: if ex.args[0].startswith(f"AttributeError: '{self.driver_instance.__class__.__name__}' object"): self.lab_logger().error( @@ -1247,8 +1317,8 @@ class BaseROS2DeviceNode(Node, Generic[T]): ) self.lab_logger().error(traceback.format_exc()) - self._property_publishers[attr_name] = PropertyPublisher( - self, attr_name, get_device_attr, msg_type, period, print_publish, qos + self._property_publishers[publish_name] = PropertyPublisher( + self, publish_name, get_device_attr, msg_type, period, print_publish, qos ) 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"] str_action_type = str(action_type)[8:-2] - self._action_servers[action_name] = ActionServer( - self, - action_type, - action_name, - execute_callback=self._create_execute_callback(action_name, action_value_mapping), - callback_group=self.callback_group, - ) - + try: + self._action_servers[action_name] = ActionServer( + self, + action_type, + action_name, + 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}") def _setup_decorated_subscribers(self): @@ -1811,7 +1884,8 @@ class BaseROS2DeviceNode(Node, Generic[T]): continue # 处理单个 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] if isinstance(resource_data, dict) and "id" in resource_data: try: @@ -1825,8 +1899,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): # 处理 ResourceSlot 列表 elif isinstance(arg_type, tuple) and len(arg_type) == 2: - resource_slot_type = "unilabos.registry.placeholder_type:ResourceSlot" - if arg_type[0] == "list" and arg_type[1] == resource_slot_type: + if arg_type[0] == "list" and isinstance(arg_type[1], str) and arg_type[1].endswith(":ResourceSlot"): resource_list = function_args[arg_name] if isinstance(resource_list, list): try: diff --git a/unilabos/ros/nodes/presets/camera.py b/unilabos/ros/nodes/presets/camera.py index 2267f676..e94f001f 100644 --- a/unilabos/ros/nodes/presets/camera.py +++ b/unilabos/ros/nodes/presets/camera.py @@ -4,7 +4,14 @@ import cv2 from sensor_msgs.msg import Image from cv_bridge import CvBridge 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): 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 diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 4a868523..eb139f1f 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -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.service import Service 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.srv import ( ResourceAdd, @@ -23,6 +24,7 @@ from unilabos_msgs.srv import ( from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response from unique_identifier_msgs.msg import UUID +from unilabos.registry.decorators import device from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot from unilabos.registry.registry import lab_registry 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.resource_tracker import ( ResourceDict, + ResourceDictType, ResourceDictInstance, ResourceTreeSet, ResourceTreeInstance, @@ -65,7 +68,13 @@ class DeviceActionStatus: class TestResourceReturn(TypedDict): resources: List[List[ResourceDict]] 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): @@ -80,6 +89,7 @@ class TestLatencyReturn(TypedDict): status: str +@device(id="host_node", category=[], description="Host Node", icon="icon_device.webp") class HostNode(BaseROS2DeviceNode): """ 主机节点类,负责管理设备、资源和控制器 @@ -268,44 +278,42 @@ class HostNode(BaseROS2DeviceNode): self._action_clients: Dict[str, ActionClient] = { # 为了方便了解实际的数据类型,host的默认写好 "/devices/host_node/create_resource": ActionClient( self, - lab_registry.ResourceCreateFromOuterEasy, + ResourceCreateFromOuterEasy, "/devices/host_node/create_resource", callback_group=self.callback_group, ), "/devices/host_node/create_resource_detailed": ActionClient( self, - lab_registry.ResourceCreateFromOuter, + ResourceCreateFromOuter, "/devices/host_node/create_resource_detailed", callback_group=self.callback_group, ), "/devices/host_node/test_latency": ActionClient( self, - lab_registry.EmptyIn, + EmptyIn, "/devices/host_node/test_latency", callback_group=self.callback_group, ), "/devices/host_node/test_resource": ActionClient( self, - lab_registry.EmptyIn, + EmptyIn, "/devices/host_node/test_resource", callback_group=self.callback_group, ), "/devices/host_node/_execute_driver_command": ActionClient( self, - lab_registry.StrSingleInput, + StrSingleInput, "/devices/host_node/_execute_driver_command", callback_group=self.callback_group, ), "/devices/host_node/_execute_driver_command_async": ActionClient( self, - lab_registry.StrSingleInput, + StrSingleInput, "/devices/host_node/_execute_driver_command_async", callback_group=self.callback_group, ), } # 用来存储多个ActionClient实例 - self._action_value_mappings: Dict[str, Dict] = ( - {} - ) # device_id -> action_value_mappings(本地+远程设备统一存储) + self._action_value_mappings: Dict[str, Dict] = {} # device_id -> action_value_mappings(本地+远程设备统一存储) self._slave_registry_configs: Dict[str, Dict] = {} # registry_name -> registry_config(含action_value_mappings) self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态 self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备 @@ -323,10 +331,18 @@ class HostNode(BaseROS2DeviceNode): self._discover_devices() # 初始化所有本机设备节点,多一次过滤,防止重复初始化 + local_machine = BasicConfig.machine_name for device_config in devices_config.root_nodes: device_id = device_config.res_content.id if device_config.res_content.type != "device": 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: self.initialize_device(device_id, device_config) else: @@ -556,7 +572,7 @@ class HostNode(BaseROS2DeviceNode): liquid_type: list[str] = [], liquid_volume: list[int] = [], slot_on_deck: str = "", - ): + ) -> CreateResourceReturn: # 暂不支持多对同名父子同时存在 res_creation_input = { "id": res_id.split("/")[-1], @@ -609,6 +625,8 @@ class HostNode(BaseROS2DeviceNode): assert len(response) == 1, "Create Resource应当只返回一个结果" for i in response: res = json.loads(i) + if "suc" in res: + raise ValueError(res.get("error")) return res except Exception as ex: pass @@ -650,7 +668,12 @@ class HostNode(BaseROS2DeviceNode): action_id = f"/devices/{device_id}/{action_name}" if action_id not in self._action_clients: 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( f"[Host Node] Created ActionClient (Local): {action_id}" ) # 子设备再创建用的是Discover发现的 @@ -1250,9 +1273,9 @@ class HostNode(BaseROS2DeviceNode): # 用 registry_name 索引已存储的 registry_config,获取 action_value_mappings if registry_name and registry_name in self._slave_registry_configs: - action_mappings = self._slave_registry_configs[registry_name].get( - "class", {} - ).get("action_value_mappings", {}) + action_mappings = ( + self._slave_registry_configs[registry_name].get("class", {}).get("action_value_mappings", {}) + ) if action_mappings: self._action_value_mappings[edge_device_id] = action_mappings self.lab_logger().info( @@ -1272,14 +1295,19 @@ class HostNode(BaseROS2DeviceNode): # 解析 devices_config,建立 device_id -> action_value_mappings 映射 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_dict in device_tree: + device_dict["machine_name"] = machine_name device_id = device_dict.get("id", "") class_name = device_dict.get("class", "") if device_id and class_name and class_name in self._slave_registry_configs: - action_mappings = self._slave_registry_configs[class_name].get( - "class", {} - ).get("action_value_mappings", {}) + action_mappings = ( + self._slave_registry_configs[class_name] + .get("class", {}) + .get("action_value_mappings", {}) + ) if action_mappings: self._action_value_mappings[device_id] = action_mappings self.lab_logger().info( @@ -1287,6 +1315,18 @@ class HostNode(BaseROS2DeviceNode): 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}") response.response = "OK" 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(traceback.format_exc()) 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)} diff --git a/unilabos/ros/nodes/presets/workstation.py b/unilabos/ros/nodes/presets/workstation.py index 902e2967..7f9f2aed 100644 --- a/unilabos/ros/nodes/presets/workstation.py +++ b/unilabos/ros/nodes/presets/workstation.py @@ -20,7 +20,7 @@ from unilabos.ros.msgs.message_converter import ( convert_from_ros_msg_with_mapping, ) 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 if TYPE_CHECKING: @@ -177,6 +177,103 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): self.lab_logger().trace(f"为子设备 {device_id} 创建动作客户端: {action_name}") 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): """创建ROS动作服务器""" if action_name not in self.protocol_names: diff --git a/unilabos/utils/decorator.py b/unilabos/utils/decorator.py index 22a90736..15793b14 100644 --- a/unilabos/utils/decorator.py +++ b/unilabos/utils/decorator.py @@ -19,74 +19,6 @@ def singleton(cls): 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( topic: str, msg_type: Optional[type] = None, @@ -104,24 +36,6 @@ def subscribe( - {namespace}: 完整命名空间 (如 "/devices/pump_1") msg_type: ROS 消息类型。如果为 None,需要在回调函数的类型注解中指定 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: @@ -129,7 +43,6 @@ def subscribe( def wrapper(*args, **kwargs): return func(*args, **kwargs) - # 在函数上附加订阅配置 wrapper._subscribe_topic = topic # type: ignore[attr-defined] wrapper._subscribe_msg_type = msg_type # type: ignore[attr-defined] wrapper._subscribe_qos = qos # type: ignore[attr-defined] @@ -141,15 +54,7 @@ def subscribe( def get_subscribe_config(func) -> dict: - """ - 获取函数上的订阅配置 - - Args: - func: 被装饰的函数 - - Returns: - 包含 topic, msg_type, qos 的配置字典 - """ + """获取函数上的订阅配置 (topic, msg_type, qos)""" if hasattr(func, "_has_subscribe") and getattr(func, "_has_subscribe", False): return { "topic": getattr(func, "_subscribe_topic", None), @@ -163,9 +68,6 @@ def get_all_subscriptions(instance) -> list: """ 扫描实例的所有方法,获取带有 @subscribe 装饰器的方法及其配置 - Args: - instance: 要扫描的实例 - Returns: 包含 (method_name, method, config) 元组的列表 """ @@ -184,92 +86,14 @@ def get_all_subscriptions(instance) -> list: return subscriptions -def always_free(func: F) -> F: - """ - 标记动作为永久闲置(不受busy队列限制)的装饰器 - - 被此装饰器标记的 action 方法,在执行时不会受到设备级别的排队限制, - 任何时候请求都可以立即执行。适用于查询类、状态读取类等轻量级操作。 - - Example: - class MyDriver: - @always_free - 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) +# --------------------------------------------------------------------------- +# 向后兼容重导出 -- 已迁移到 unilabos.registry.decorators +# --------------------------------------------------------------------------- +from unilabos.registry.decorators import ( # noqa: E402, F401 + topic_config, + get_topic_config, + always_free, + is_always_free, + not_action, + is_not_action, +) diff --git a/unilabos/utils/environment_check.py b/unilabos/utils/environment_check.py index 73c0b10b..a2bbd262 100644 --- a/unilabos/utils/environment_check.py +++ b/unilabos/utils/environment_check.py @@ -22,6 +22,7 @@ class EnvironmentChecker: # "pymodbus.framer.FramerType": "pymodbus==3.9.2", "websockets": "websockets", "msgcenterpy": "msgcenterpy", + "orjson": "orjson", "opentrons_shared_data": "opentrons_shared_data", "typing_extensions": "typing_extensions", "crcmod": "crcmod-plus", @@ -32,7 +33,7 @@ class EnvironmentChecker: # 包版本要求(包名: 最低版本) self.version_requirements = { - "msgcenterpy": "0.1.5", # msgcenterpy 最低版本要求 + "msgcenterpy": "0.1.8", # msgcenterpy 最低版本要求 } self.missing_packages = [] diff --git a/unilabos/utils/import_manager.py b/unilabos/utils/import_manager.py index dabbe1a7..7fe2f501 100644 --- a/unilabos/utils/import_manager.py +++ b/unilabos/utils/import_manager.py @@ -21,15 +21,11 @@ __all__ = [ "get_class", "get_module", "init_from_list", - "get_class_info_static", - "get_registry_class_info", + "get_enhanced_class_info", ] -from ast import Constant - from unilabos.resources.resource_tracker import PARAM_SAMPLE_UUIDS from unilabos.utils import logger -from unilabos.utils.decorator import is_not_action, is_always_free class ImportManager: @@ -45,6 +41,7 @@ class ImportManager: self._modules: Dict[str, Any] = {} self._classes: Dict[str, Type] = {} self._functions: Dict[str, Callable] = {} + self._search_miss: set = set() if module_list: for module_path in module_list: @@ -159,193 +156,113 @@ class ImportManager: Returns: 找到的类对象,如果未找到则返回None """ - # 如果cls_name是builtins中的关键字,则返回对应类 if class_name in builtins.__dict__: return builtins.__dict__[class_name] - # 首先在已索引的类中查找 if class_name in self._classes: 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: classes = {name.lower(): obj for name, obj in self._classes.items()} if class_name in classes: return classes[class_name] - # 遍历所有已加载的模块进行搜索 for module_path, module in self._modules.items(): for name, obj in inspect.getmembers(module): if inspect.isclass(obj) and ( (name.lower() == class_name.lower()) if search_lower else (name == class_name) ): - # 将找到的类添加到索引中 self._classes[name] = obj self._classes[f"{module_path}:{name}"] = obj return obj + self._search_miss.add(cache_key) 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: - module_path: 模块路径,格式为 "module.path" 或 "module.path:ClassName" - use_dynamic: 是否优先使用动态导入 + module_path: 格式 ``"module.path:ClassName"`` 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, - "dynamic_import_success": False, - "static_analysis_success": False, - "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"], + "ast_analysis_success": False, + "import_map": {}, + "init_params": [], "status_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) - # 将模块路径转换为文件路径 file_path = self._module_path_to_file_path(module_name) 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: - source_code = f.read() + try: + 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 for node in ast.walk(tree): - if isinstance(node, ast.ClassDef): - if node.name == class_name: - target_class = node - break + if isinstance(node, ast.ClassDef) and node.name == class_name: + target_class = node + break if target_class is None: - raise AttributeError(f"在文件 {file_path} 中找不到类 {class_name}") + logger.warning(f"[ImportManager] 在文件 {file_path} 中找不到类 {class_name}") + return result - result = { - "class_name": class_name, - "init_params": {}, - "status_methods": {}, - "action_methods": {}, + body = _extract_class_body(target_class, import_map) + + # 映射到统一字段名(与 registry.py complete_registry 消费端一致) + result["init_params"] = body.get("init_params", []) + 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() } - - # 分析类的方法 - 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 + result["ast_analysis_success"] = True return result def _analyze_method_signature(self, method, skip_unilabos_params: bool = True) -> Dict[str, Any]: @@ -401,23 +318,26 @@ class ImportManager: "name": method.__name__, "args": args, "return_type": self._get_type_string(signature.return_annotation), - "return_annotation": signature.return_annotation, # 保留原始类型注解,用于TypedDict等特殊处理 "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) return self._get_type_string(signature.return_annotation) 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: - return "Any" # 如果没有注解,返回Any + return "Any" if annotation is None: - return "None" # 明确的None类型 + return "None" if hasattr(annotation, "__origin__"): - # 处理typing模块的类型 origin = annotation.__origin__ if origin in (list, set, tuple): if hasattr(annotation, "__args__") and annotation.__args__: @@ -432,126 +352,26 @@ class ImportManager: return "dict" elif origin is Optional: return "Unknown" - return f"Unknown" + return "Unknown" annotation_str = str(annotation) - # 处理typing模块的复杂类型 if "typing." in annotation_str: - # 简化typing类型显示 return ( annotation_str.replace("typing.", "") if getattr(annotation, "_name", None) is None else annotation._name.lower() ) - # 如果是类型对象 if hasattr(annotation, "__name__"): - # 如果是内置类型 - if annotation.__module__ == "builtins": - return annotation.__name__ - else: - # 如果是自定义类,返回完整路径 - return f"{annotation.__module__}:{annotation.__name__}" - # 如果是typing模块的类型 + module = getattr(annotation, "__module__", None) + if module and module != "builtins": + return f"{module}:{annotation.__name__}" + return annotation.__name__ elif hasattr(annotation, "_name"): return annotation._name - # 如果是字符串形式的类型注解 elif isinstance(annotation, str): return annotation else: 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]: for path in sys.path: potential_path = Path(path) / module_path.replace(".", "/") @@ -566,222 +386,6 @@ class ImportManager: 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) -def get_class_info_static(module_class_path: str) -> 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]: +def get_enhanced_class_info(module_path: str, **kwargs) -> Dict[str, Any]: """获取增强的类信息的便捷函数""" - return default_manager.get_enhanced_class_info(module_path, use_dynamic) + return default_manager.get_enhanced_class_info(module_path, **kwargs) diff --git a/unilabos/utils/log.py b/unilabos/utils/log.py index be5d8c31..da085f14 100644 --- a/unilabos/utils/log.py +++ b/unilabos/utils/log.py @@ -217,7 +217,6 @@ def configure_logger(loglevel=None, working_dir=None): return log_filepath - # 配置日志系统 configure_logger() diff --git a/unilabos/utils/requirements.txt b/unilabos/utils/requirements.txt index 65d724fc..105d387d 100644 --- a/unilabos/utils/requirements.txt +++ b/unilabos/utils/requirements.txt @@ -1,7 +1,8 @@ networkx typing_extensions websockets -msgcenterpy>=0.1.5 +msgcenterpy>=0.1.8 +orjson>=3.11 opentrons_shared_data pint fastapi diff --git a/unilabos/utils/tools.py b/unilabos/utils/tools.py index 89195cbd..3c7b742e 100644 --- a/unilabos/utils/tools.py +++ b/unilabos/utils/tools.py @@ -1,4 +1,39 @@ +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数组转换为字符串 def uuid_to_str(uuid_array) -> str: """将UUID字节数组转换为十六进制字符串""" - return "".join(format(byte, "02x") for byte in uuid_array) \ No newline at end of file + return "".join(format(byte, "02x") for byte in uuid_array) diff --git a/unilabos/utils/type_check.py b/unilabos/utils/type_check.py index 64001e56..e3df2dc2 100644 --- a/unilabos/utils/type_check.py +++ b/unilabos/utils/type_check.py @@ -15,14 +15,21 @@ def get_type_class(type_hint): 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): """自定义JSON编码器处理特殊类型""" def default(self, obj): - # 优先处理类型对象 - if isinstance(obj, type): - return str(obj)[8:-2] - return super().default(obj) + try: + return json_default(obj) + except Exception: + return super().default(obj) class NoAliasDumper(yaml.SafeDumper): @@ -43,13 +50,10 @@ class ResultInfoEncoder(json.JSONEncoder): """专门用于处理任务执行结果信息的JSON编码器""" def default(self, obj): - # 优先处理类型对象 if isinstance(obj, type): - return str(obj)[8:-2] + return json_default(obj) - # 对于无法序列化的对象,统一转换为字符串 try: - # 尝试调用 __dict__ 或者其他序列化方法 if hasattr(obj, "__dict__"): return obj.__dict__ elif hasattr(obj, "_asdict"): # namedtuple @@ -59,10 +63,8 @@ class ResultInfoEncoder(json.JSONEncoder): elif hasattr(obj, "dict"): return obj.dict() else: - # 如果都不行,转换为字符串 return str(obj) except Exception: - # 如果转换失败,直接返回字符串表示 return str(obj) diff --git a/unilabos_msgs/package.xml b/unilabos_msgs/package.xml index 6957f7bf..ead5eded 100644 --- a/unilabos_msgs/package.xml +++ b/unilabos_msgs/package.xml @@ -2,7 +2,7 @@ unilabos_msgs - 0.10.18 + 0.10.19 ROS2 Messages package for unilabos devices Junhan Chang Xuwznln