mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-25 09:59:16 +00:00
Compare commits
33 Commits
prcix9320
...
d2f204c5b0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2f204c5b0 | ||
|
|
d8922884b1 | ||
|
|
427afe83d4 | ||
|
|
23c2e3b2f7 | ||
|
|
59c26265e9 | ||
|
|
4c2adea55a | ||
|
|
0f6264503a | ||
|
|
2c554182d3 | ||
|
|
6d319d91ff | ||
|
|
3155b2f97e | ||
|
|
e5e30a1c7d | ||
|
|
4e82f62327 | ||
|
|
95d3456214 | ||
|
|
38bf95b13c | ||
|
|
f2c0bec02c | ||
|
|
e0394bf414 | ||
|
|
975a56415a | ||
|
|
cadbe87e3f | ||
|
|
b993c1f590 | ||
|
|
e0fae94c10 | ||
|
|
b5cd181ac1 | ||
|
|
5c047beb83 | ||
|
|
b40c087143 | ||
|
|
7f1cc3b2a5 | ||
|
|
3f160c2049 | ||
|
|
a54e7c0f23 | ||
|
|
e5015cd5e0 | ||
|
|
514373c164 | ||
|
|
fcea02585a | ||
|
|
07cf690897 | ||
|
|
cfea27460a | ||
|
|
b7d3e980a9 | ||
|
|
f9ed6cb3fb |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
package:
|
package:
|
||||||
name: unilabos-env
|
name: unilabos-env
|
||||||
version: 0.10.17
|
version: 0.10.19
|
||||||
|
|
||||||
build:
|
build:
|
||||||
noarch: generic
|
noarch: generic
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
@echo off
|
|
||||||
setlocal enabledelayedexpansion
|
|
||||||
|
|
||||||
REM upgrade pip
|
|
||||||
"%PREFIX%\python.exe" -m pip install --upgrade pip
|
|
||||||
|
|
||||||
REM install extra deps
|
|
||||||
"%PREFIX%\python.exe" -m pip install paho-mqtt opentrons_shared_data
|
|
||||||
"%PREFIX%\python.exe" -m pip install git+https://github.com/Xuwznln/pylabrobot.git
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euxo pipefail
|
|
||||||
|
|
||||||
# make sure pip is available
|
|
||||||
"$PREFIX/bin/python" -m pip install --upgrade pip
|
|
||||||
|
|
||||||
# install extra deps
|
|
||||||
"$PREFIX/bin/python" -m pip install paho-mqtt opentrons_shared_data
|
|
||||||
"$PREFIX/bin/python" -m pip install git+https://github.com/Xuwznln/pylabrobot.git
|
|
||||||
328
.cursor/skills/create-device-skill/SKILL.md
Normal file
328
.cursor/skills/create-device-skill/SKILL.md
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
---
|
||||||
|
name: create-device-skill
|
||||||
|
description: Create a skill for any Uni-Lab device by extracting action schemas from the device registry. Use when the user wants to create a new device skill, add device API documentation, or set up action schemas for a device.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 创建设备 Skill 指南
|
||||||
|
|
||||||
|
本 meta-skill 教你如何为任意 Uni-Lab-OS 设备创建完整的 API 操作技能(参考 `unilab-device-api` 的成功案例)。
|
||||||
|
|
||||||
|
## 数据源
|
||||||
|
|
||||||
|
- **设备注册表**: `unilabos_data/req_device_registry_upload.json`
|
||||||
|
- **结构**: `{ "resources": [{ "id": "<device_id>", "class": { "module": "<python_module:ClassName>", "action_value_mappings": { ... } } }] }`
|
||||||
|
- **生成时机**: `unilab` 启动并完成注册表上传后自动生成
|
||||||
|
- **module 字段**: 格式 `unilabos.devices.xxx.yyy:ClassName`,可转为源码路径 `unilabos/devices/xxx/yyy.py`,阅读源码可了解参数含义和设备行为
|
||||||
|
|
||||||
|
## 创建流程
|
||||||
|
|
||||||
|
### Step 0 — 收集必备信息(缺一不可,否则询问后终止)
|
||||||
|
|
||||||
|
开始前**必须**确认以下 4 项信息全部就绪。如果用户未提供任何一项,**立即询问并终止当前流程**,等用户补齐后再继续。
|
||||||
|
|
||||||
|
向用户提问:「请提供你的 unilab 启动参数,我需要以下信息:」
|
||||||
|
|
||||||
|
#### 必备项 ①:ak / sk(认证凭据)
|
||||||
|
|
||||||
|
来源:启动命令的 `--ak` `--sk` 参数,或 config.py 中的 `ak = "..."` `sk = "..."`。
|
||||||
|
|
||||||
|
获取后立即生成 AUTH token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python ./scripts/gen_auth.py <ak> <sk>
|
||||||
|
# 或从 config.py 提取
|
||||||
|
python ./scripts/gen_auth.py --config <config.py>
|
||||||
|
```
|
||||||
|
|
||||||
|
认证算法:`base64(ak:sk)` → `Authorization: Lab <token>`
|
||||||
|
|
||||||
|
#### 必备项 ②:--addr(目标环境)
|
||||||
|
|
||||||
|
决定 API 请求发往哪个服务器。从启动命令的 `--addr` 参数获取:
|
||||||
|
|
||||||
|
| `--addr` 值 | BASE URL |
|
||||||
|
|-------------|----------|
|
||||||
|
| `test` | `https://uni-lab.test.bohrium.com` |
|
||||||
|
| `uat` | `https://uni-lab.uat.bohrium.com` |
|
||||||
|
| `local` | `http://127.0.0.1:48197` |
|
||||||
|
| 不传(默认) | `https://uni-lab.bohrium.com` |
|
||||||
|
| 其他自定义 URL | 直接使用该 URL |
|
||||||
|
|
||||||
|
#### 必备项 ③:req_device_registry_upload.json(设备注册表)
|
||||||
|
|
||||||
|
数据文件由 `unilab` 启动时自动生成,需要定位它:
|
||||||
|
|
||||||
|
**推断 working_dir**(即 `unilabos_data` 所在目录):
|
||||||
|
|
||||||
|
| 条件 | working_dir 取值 |
|
||||||
|
|------|------------------|
|
||||||
|
| 传了 `--working_dir` | `<working_dir>/unilabos_data/`(若子目录已存在则直接用) |
|
||||||
|
| 仅传了 `--config` | `<config 文件所在目录>/unilabos_data/` |
|
||||||
|
| 都没传 | `<当前工作目录>/unilabos_data/` |
|
||||||
|
|
||||||
|
**按优先级搜索文件**:
|
||||||
|
|
||||||
|
```
|
||||||
|
<推断的 working_dir>/unilabos_data/req_device_registry_upload.json
|
||||||
|
<推断的 working_dir>/req_device_registry_upload.json
|
||||||
|
<workspace 根目录>/unilabos_data/req_device_registry_upload.json
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以直接 Glob 搜索:`**/req_device_registry_upload.json`
|
||||||
|
|
||||||
|
找到后**必须检查文件修改时间**并告知用户:「找到注册表文件 `<路径>`,生成于 `<时间>`。请确认这是最近一次启动生成的。」超过 1 天提醒用户是否需要重新启动 `unilab`。
|
||||||
|
|
||||||
|
**如果文件不存在** → 告知用户先运行 `unilab` 启动命令,等日志出现 `注册表响应数据已保存` 后再执行本流程。**终止。**
|
||||||
|
|
||||||
|
#### 必备项 ④:目标设备
|
||||||
|
|
||||||
|
用户需要明确要为哪个设备创建 skill。可以是设备名称(如「PRCXI 移液站」)或 device_id(如 `liquid_handler.prcxi`)。
|
||||||
|
|
||||||
|
如果用户不确定,运行提取脚本列出所有设备供选择:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python ./scripts/extract_device_actions.py --registry <找到的文件路径>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 完整示例
|
||||||
|
|
||||||
|
用户提供:
|
||||||
|
|
||||||
|
```
|
||||||
|
--ak a1fd9d4e-xxxx-xxxx-xxxx-d9a69c09f0fd
|
||||||
|
--sk 136ff5c6-xxxx-xxxx-xxxx-a03e301f827b
|
||||||
|
--addr test
|
||||||
|
--port 8003
|
||||||
|
--disable_browser
|
||||||
|
```
|
||||||
|
|
||||||
|
从中提取:
|
||||||
|
- ✅ ak/sk → 运行 `gen_auth.py` 得到 `AUTH="Authorization: Lab YTFmZDlk..."`
|
||||||
|
- ✅ addr=test → `BASE=https://uni-lab.test.bohrium.com`
|
||||||
|
- ✅ 搜索 `unilabos_data/req_device_registry_upload.json` → 找到并确认时间
|
||||||
|
- ✅ 用户指明目标设备 → 如 `liquid_handler.prcxi`
|
||||||
|
|
||||||
|
**四项全部就绪后才进入 Step 1。**
|
||||||
|
|
||||||
|
### Step 1 — 列出可用设备
|
||||||
|
|
||||||
|
运行提取脚本,列出所有设备及 action 数量和 Python 源码路径,让用户选择:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 自动搜索(默认在 unilabos_data/ 和当前目录查找)
|
||||||
|
python ./scripts/extract_device_actions.py
|
||||||
|
|
||||||
|
# 指定注册表文件路径
|
||||||
|
python ./scripts/extract_device_actions.py --registry <path/to/req_device_registry_upload.json>
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本输出包含每个设备的 **Python 源码路径**(从 `class.module` 转换),可用于后续阅读源码理解参数含义。
|
||||||
|
|
||||||
|
### Step 2 — 提取 Action Schema
|
||||||
|
|
||||||
|
用户选择设备后,运行提取脚本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python ./scripts/extract_device_actions.py [--registry <path>] <device_id> ./skills/<skill-name>/actions/
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会显示设备的 Python 源码路径和类名,方便阅读源码了解参数含义。
|
||||||
|
|
||||||
|
每个 action 生成一个 JSON 文件,包含:
|
||||||
|
- `type` — 作为 API 调用的 `action_type`
|
||||||
|
- `schema` — 完整 JSON Schema(含 `properties.goal.properties` 参数定义)
|
||||||
|
- `goal` — goal 字段映射(含占位符 `$placeholder`)
|
||||||
|
- `goal_default` — 默认值
|
||||||
|
|
||||||
|
### Step 3 — 写 action-index.md
|
||||||
|
|
||||||
|
按模板为每个 action 写条目:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### `<action_name>`
|
||||||
|
|
||||||
|
<用途描述(一句话)>
|
||||||
|
|
||||||
|
- **Schema**: [`actions/<filename>.json`](actions/<filename>.json)
|
||||||
|
- **核心参数**: `param1`, `param2`(从 schema.required 获取)
|
||||||
|
- **可选参数**: `param3`, `param4`
|
||||||
|
- **占位符字段**: `field`(需填入物料信息,值以 `$` 开头)
|
||||||
|
```
|
||||||
|
|
||||||
|
描述规则:
|
||||||
|
- 从 `schema.properties` 读参数列表(schema 已提升为 goal 内容)
|
||||||
|
- 从 `schema.required` 区分核心/可选参数
|
||||||
|
- 按功能分类(移液、枪头、外设等)
|
||||||
|
- 标注 `placeholder_keys` 中的字段类型:
|
||||||
|
- `unilabos_resources` → **ResourceSlot**,填入 `{id, name, uuid}`(id 是路径格式,从资源树取物料节点)
|
||||||
|
- `unilabos_devices` → **DeviceSlot**,填入路径字符串如 `"/host_node"`(从资源树筛选 type=device)
|
||||||
|
- `unilabos_nodes` → **NodeSlot**,填入路径字符串如 `"/PRCXI/PRCXI_Deck"`(资源树中任意节点)
|
||||||
|
- `unilabos_class` → **ClassSlot**,填入类名字符串如 `"container"`(从注册表查找)
|
||||||
|
- array 类型字段 → `[{id, name, uuid}, ...]`
|
||||||
|
- 特殊:`create_resource` 的 `res_id`(ResourceSlot)可填不存在的路径
|
||||||
|
|
||||||
|
### Step 4 — 写 SKILL.md
|
||||||
|
|
||||||
|
直接复用 `unilab-device-api` 的 API 模板(10 个 endpoint),修改:
|
||||||
|
- 设备名称
|
||||||
|
- Action 数量
|
||||||
|
- 目录列表
|
||||||
|
- Session state 中的 `device_name`
|
||||||
|
- **AUTH 头** — 使用 Step 0 中 `gen_auth.py` 生成的 `Authorization: Lab <token>`(不要硬编码 `Api` 类型的 key)
|
||||||
|
- **Python 源码路径** — 在 SKILL.md 开头注明设备对应的源码文件,方便参考参数含义
|
||||||
|
- **Slot 字段表** — 列出本设备哪些 action 的哪些字段需要填入 Slot(物料/设备/节点/类名)
|
||||||
|
|
||||||
|
API 模板结构:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 设备信息
|
||||||
|
- device_id, Python 源码路径, 设备类名
|
||||||
|
|
||||||
|
## 前置条件(缺一不可)
|
||||||
|
- ak/sk → AUTH, --addr → BASE URL
|
||||||
|
|
||||||
|
## Session State
|
||||||
|
- lab_uuid(通过 API #1 自动匹配,不要问用户), device_name
|
||||||
|
|
||||||
|
## API Endpoints (10 个)
|
||||||
|
# 注意:
|
||||||
|
# - #1 获取 lab 列表 + 自动匹配 lab_uuid(遍历 is_admin 的 lab,
|
||||||
|
# 调用 /lab/info/{uuid} 比对 access_key == ak)
|
||||||
|
# - #2 创建工作流用 POST /lab/workflow
|
||||||
|
# - #10 获取资源树路径含 lab_uuid: /lab/material/download/{lab_uuid}
|
||||||
|
|
||||||
|
## Placeholder Slot 填写规则
|
||||||
|
- unilabos_resources → ResourceSlot → {"id":"/path/name","name":"name","uuid":"xxx"}
|
||||||
|
- unilabos_devices → DeviceSlot → "/parent/device" 路径字符串
|
||||||
|
- unilabos_nodes → NodeSlot → "/parent/node" 路径字符串
|
||||||
|
- unilabos_class → ClassSlot → "class_name" 字符串
|
||||||
|
- 特例:create_resource 的 res_id 允许填不存在的路径
|
||||||
|
- 列出本设备所有 Slot 字段、类型及含义
|
||||||
|
|
||||||
|
## 渐进加载策略
|
||||||
|
## 完整工作流 Checklist
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5 — 验证
|
||||||
|
|
||||||
|
检查文件完整性:
|
||||||
|
- [ ] `SKILL.md` 包含 10 个 API endpoint
|
||||||
|
- [ ] `SKILL.md` 包含 Placeholder Slot 填写规则(ResourceSlot / DeviceSlot / NodeSlot / ClassSlot + create_resource 特例)和本设备的 Slot 字段表
|
||||||
|
- [ ] `action-index.md` 列出所有 action 并有描述
|
||||||
|
- [ ] `actions/` 目录中每个 action 有对应 JSON 文件
|
||||||
|
- [ ] JSON 文件包含 `type`, `schema`(已提升为 goal 内容), `goal`, `goal_default`, `placeholder_keys` 字段
|
||||||
|
- [ ] 描述能让 agent 判断该用哪个 action
|
||||||
|
|
||||||
|
## Action JSON 文件结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "LiquidHandlerTransfer", // → API 的 action_type
|
||||||
|
"goal": { // goal 字段映射
|
||||||
|
"sources": "sources",
|
||||||
|
"targets": "targets",
|
||||||
|
"tip_racks": "tip_racks",
|
||||||
|
"asp_vols": "asp_vols"
|
||||||
|
},
|
||||||
|
"schema": { // ← 直接是 goal 的 schema(已提升)
|
||||||
|
"type": "object",
|
||||||
|
"properties": { // 参数定义(即请求中 goal 的字段)
|
||||||
|
"sources": { "type": "array", "items": { "type": "object" } },
|
||||||
|
"targets": { "type": "array", "items": { "type": "object" } },
|
||||||
|
"asp_vols": { "type": "array", "items": { "type": "number" } }
|
||||||
|
},
|
||||||
|
"required": [...],
|
||||||
|
"_unilabos_placeholder_info": { // ← Slot 类型标记
|
||||||
|
"sources": "unilabos_resources",
|
||||||
|
"targets": "unilabos_resources",
|
||||||
|
"tip_racks": "unilabos_resources"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"goal_default": { ... }, // 默认值
|
||||||
|
"placeholder_keys": { // ← 汇总所有 Slot 字段
|
||||||
|
"sources": "unilabos_resources", // ResourceSlot
|
||||||
|
"targets": "unilabos_resources",
|
||||||
|
"tip_racks": "unilabos_resources",
|
||||||
|
"target_device_id": "unilabos_devices" // DeviceSlot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**:`schema` 已由脚本从原始 `schema.properties.goal` 提升为顶层,直接包含参数定义。
|
||||||
|
> `schema.properties` 中的字段即为 API 请求 `param.goal` 中的字段。
|
||||||
|
|
||||||
|
## Placeholder Slot 类型体系
|
||||||
|
|
||||||
|
`placeholder_keys` / `_unilabos_placeholder_info` 中有 4 种值,对应不同的填写方式:
|
||||||
|
|
||||||
|
| placeholder 值 | Slot 类型 | 填写格式 | 选取范围 |
|
||||||
|
|---------------|-----------|---------|---------|
|
||||||
|
| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` | 仅**物料**节点(不含设备) |
|
||||||
|
| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` | 仅**设备**节点(type=device),路径字符串 |
|
||||||
|
| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` | **设备 + 物料**,即所有节点,路径字符串 |
|
||||||
|
| `unilabos_class` | ClassSlot | `"class_name"` | 注册表中已上报的资源类 name |
|
||||||
|
|
||||||
|
### ResourceSlot(`unilabos_resources`)
|
||||||
|
|
||||||
|
最常见的类型。从资源树中选取**物料**节点(孔板、枪头盒、试剂槽等):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"id": "/workstation/container1", "name": "container1", "uuid": "ff149a9a-2cb8-419d-8db5-d3ba056fb3c2"}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 单个(schema type=object):`{"id": "/path/name", "name": "name", "uuid": "xxx"}`
|
||||||
|
- 数组(schema type=array):`[{"id": "/path/a", "name": "a", "uuid": "xxx"}, ...]`
|
||||||
|
- `id` 本身是从 parent 计算的路径格式
|
||||||
|
- 根据 action 语义选择正确的物料(如 `sources` = 液体来源,`targets` = 目标位置)
|
||||||
|
|
||||||
|
> **特例**:`create_resource` 的 `res_id` 字段,目标物料可能**尚不存在**,此时直接填写期望的路径(如 `"/workstation/container1"`),不需要 uuid。
|
||||||
|
|
||||||
|
### DeviceSlot(`unilabos_devices`)
|
||||||
|
|
||||||
|
填写**设备路径字符串**。从资源树中筛选 type=device 的节点,从 parent 计算路径:
|
||||||
|
|
||||||
|
```
|
||||||
|
"/host_node"
|
||||||
|
"/bioyond_cell/reaction_station"
|
||||||
|
```
|
||||||
|
|
||||||
|
- 只填路径字符串,不需要 `{id, uuid}` 对象
|
||||||
|
- 根据 action 语义选择正确的设备(如 `target_device_id` = 目标设备)
|
||||||
|
|
||||||
|
### NodeSlot(`unilabos_nodes`)
|
||||||
|
|
||||||
|
范围 = 设备 + 物料。即资源树中**所有节点**都可以选,填写**路径字符串**:
|
||||||
|
|
||||||
|
```
|
||||||
|
"/PRCXI/PRCXI_Deck"
|
||||||
|
```
|
||||||
|
|
||||||
|
- 使用场景:当参数既可能指向物料也可能指向设备时(如 `PumpTransferProtocol` 的 `from_vessel`/`to_vessel`,`create_resource` 的 `parent`)
|
||||||
|
|
||||||
|
### ClassSlot(`unilabos_class`)
|
||||||
|
|
||||||
|
填写注册表中已上报的**资源类 name**。从本地 `req_resource_registry_upload.json` 中查找:
|
||||||
|
|
||||||
|
```
|
||||||
|
"container"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通过 API #10 获取资源树
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH"
|
||||||
|
```
|
||||||
|
|
||||||
|
注意 `lab_uuid` 在路径中(不是查询参数)。资源树返回所有节点,每个节点包含 `id`(路径格式)、`name`、`uuid`、`type`、`parent` 等字段。填写 Slot 时需根据 placeholder 类型筛选正确的节点。
|
||||||
|
|
||||||
|
## 最终目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
./<skill-name>/
|
||||||
|
├── SKILL.md # API 端点 + 渐进加载指引
|
||||||
|
├── action-index.md # 动作索引:描述/用途/核心参数
|
||||||
|
└── actions/ # 每个 action 的完整 JSON Schema
|
||||||
|
├── action1.json
|
||||||
|
├── action2.json
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
从 req_device_registry_upload.json 中提取指定设备的 action schema。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
# 列出所有设备及 action 数量(自动搜索注册表文件)
|
||||||
|
python extract_device_actions.py
|
||||||
|
|
||||||
|
# 指定注册表文件路径
|
||||||
|
python extract_device_actions.py --registry <path/to/req_device_registry_upload.json>
|
||||||
|
|
||||||
|
# 提取指定设备的 action 到目录
|
||||||
|
python extract_device_actions.py <device_id> <output_dir>
|
||||||
|
python extract_device_actions.py --registry <path> <device_id> <output_dir>
|
||||||
|
|
||||||
|
示例:
|
||||||
|
python extract_device_actions.py --registry unilabos_data/req_device_registry_upload.json
|
||||||
|
python extract_device_actions.py liquid_handler.prcxi .cursor/skills/unilab-device-api/actions/
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
REGISTRY_FILENAME = "req_device_registry_upload.json"
|
||||||
|
|
||||||
|
def find_registry(explicit_path=None):
|
||||||
|
"""
|
||||||
|
查找 req_device_registry_upload.json 文件。
|
||||||
|
|
||||||
|
搜索优先级:
|
||||||
|
1. 用户通过 --registry 显式指定的路径
|
||||||
|
2. <cwd>/unilabos_data/req_device_registry_upload.json
|
||||||
|
3. <cwd>/req_device_registry_upload.json
|
||||||
|
4. <script所在目录>/../../.. (workspace根) 下的 unilabos_data/
|
||||||
|
5. 向上逐级搜索父目录(最多 5 层)
|
||||||
|
"""
|
||||||
|
if explicit_path:
|
||||||
|
if os.path.isfile(explicit_path):
|
||||||
|
return explicit_path
|
||||||
|
if os.path.isdir(explicit_path):
|
||||||
|
fp = os.path.join(explicit_path, REGISTRY_FILENAME)
|
||||||
|
if os.path.isfile(fp):
|
||||||
|
return fp
|
||||||
|
print(f"警告: 指定的路径不存在: {explicit_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
candidates = [
|
||||||
|
os.path.join("unilabos_data", REGISTRY_FILENAME),
|
||||||
|
REGISTRY_FILENAME,
|
||||||
|
]
|
||||||
|
|
||||||
|
for c in candidates:
|
||||||
|
if os.path.isfile(c):
|
||||||
|
return c
|
||||||
|
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
workspace_root = os.path.normpath(os.path.join(script_dir, "..", "..", ".."))
|
||||||
|
for c in candidates:
|
||||||
|
path = os.path.join(workspace_root, c)
|
||||||
|
if os.path.isfile(path):
|
||||||
|
return path
|
||||||
|
|
||||||
|
cwd = os.getcwd()
|
||||||
|
for _ in range(5):
|
||||||
|
parent = os.path.dirname(cwd)
|
||||||
|
if parent == cwd:
|
||||||
|
break
|
||||||
|
cwd = parent
|
||||||
|
for c in candidates:
|
||||||
|
path = os.path.join(cwd, c)
|
||||||
|
if os.path.isfile(path):
|
||||||
|
return path
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def load_registry(path):
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
def list_devices(data):
|
||||||
|
"""列出所有包含 action_value_mappings 的设备,同时返回 module 路径"""
|
||||||
|
resources = data.get('resources', [])
|
||||||
|
devices = []
|
||||||
|
for res in resources:
|
||||||
|
rid = res.get('id', '')
|
||||||
|
cls = res.get('class', {})
|
||||||
|
avm = cls.get('action_value_mappings', {})
|
||||||
|
module = cls.get('module', '')
|
||||||
|
if avm:
|
||||||
|
devices.append((rid, len(avm), module))
|
||||||
|
return devices
|
||||||
|
|
||||||
|
def flatten_schema_to_goal(action_data):
|
||||||
|
"""将 schema 中嵌套的 goal 内容提升为顶层 schema,去掉 feedback/result 包装"""
|
||||||
|
schema = action_data.get('schema', {})
|
||||||
|
goal_schema = schema.get('properties', {}).get('goal', {})
|
||||||
|
if goal_schema:
|
||||||
|
action_data = dict(action_data)
|
||||||
|
action_data['schema'] = goal_schema
|
||||||
|
return action_data
|
||||||
|
|
||||||
|
|
||||||
|
def extract_actions(data, device_id, output_dir):
|
||||||
|
"""提取指定设备的 action schema 到独立 JSON 文件"""
|
||||||
|
resources = data.get('resources', [])
|
||||||
|
for res in resources:
|
||||||
|
if res.get('id') == device_id:
|
||||||
|
cls = res.get('class', {})
|
||||||
|
module = cls.get('module', '')
|
||||||
|
avm = cls.get('action_value_mappings', {})
|
||||||
|
if not avm:
|
||||||
|
print(f"设备 {device_id} 没有 action_value_mappings")
|
||||||
|
return []
|
||||||
|
|
||||||
|
if module:
|
||||||
|
py_path = module.split(":")[0].replace(".", "/") + ".py"
|
||||||
|
class_name = module.split(":")[-1] if ":" in module else ""
|
||||||
|
print(f"Python 源码: {py_path}")
|
||||||
|
if class_name:
|
||||||
|
print(f"设备类: {class_name}")
|
||||||
|
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
written = []
|
||||||
|
for action_name in sorted(avm.keys()):
|
||||||
|
action_data = flatten_schema_to_goal(avm[action_name])
|
||||||
|
filename = action_name.replace('-', '_') + '.json'
|
||||||
|
filepath = os.path.join(output_dir, filename)
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(action_data, f, indent=2, ensure_ascii=False)
|
||||||
|
written.append(filename)
|
||||||
|
print(f" {filepath}")
|
||||||
|
return written
|
||||||
|
|
||||||
|
print(f"设备 {device_id} 未找到")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
explicit_registry = None
|
||||||
|
|
||||||
|
if "--registry" in args:
|
||||||
|
idx = args.index("--registry")
|
||||||
|
if idx + 1 < len(args):
|
||||||
|
explicit_registry = args[idx + 1]
|
||||||
|
args = args[:idx] + args[idx + 2:]
|
||||||
|
else:
|
||||||
|
print("错误: --registry 需要指定路径")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
registry_path = find_registry(explicit_registry)
|
||||||
|
if not registry_path:
|
||||||
|
print(f"错误: 找不到 {REGISTRY_FILENAME}")
|
||||||
|
print()
|
||||||
|
print("解决方法:")
|
||||||
|
print(" 1. 先运行 unilab 启动命令,等待注册表生成")
|
||||||
|
print(" 2. 用 --registry 指定文件路径:")
|
||||||
|
print(f" python {sys.argv[0]} --registry <path/to/{REGISTRY_FILENAME}>")
|
||||||
|
print()
|
||||||
|
print("搜索过的路径:")
|
||||||
|
for p in [
|
||||||
|
os.path.join("unilabos_data", REGISTRY_FILENAME),
|
||||||
|
REGISTRY_FILENAME,
|
||||||
|
os.path.join("<workspace_root>", "unilabos_data", REGISTRY_FILENAME),
|
||||||
|
]:
|
||||||
|
print(f" - {p}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"注册表: {registry_path}")
|
||||||
|
mtime = os.path.getmtime(registry_path)
|
||||||
|
gen_time = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
size_mb = os.path.getsize(registry_path) / (1024 * 1024)
|
||||||
|
print(f"生成时间: {gen_time} (文件大小: {size_mb:.1f} MB)")
|
||||||
|
data = load_registry(registry_path)
|
||||||
|
|
||||||
|
if len(args) == 0:
|
||||||
|
devices = list_devices(data)
|
||||||
|
print(f"\n找到 {len(devices)} 个设备:")
|
||||||
|
print(f"{'设备 ID':<50} {'Actions':>7} {'Python 模块'}")
|
||||||
|
print("-" * 120)
|
||||||
|
for did, count, module in sorted(devices, key=lambda x: x[0]):
|
||||||
|
py_path = module.split(":")[0].replace(".", "/") + ".py" if module else ""
|
||||||
|
print(f"{did:<50} {count:>7} {py_path}")
|
||||||
|
|
||||||
|
elif len(args) == 2:
|
||||||
|
device_id = args[0]
|
||||||
|
output_dir = args[1]
|
||||||
|
print(f"\n提取 {device_id} 的 actions 到 {output_dir}/")
|
||||||
|
written = extract_actions(data, device_id, output_dir)
|
||||||
|
if written:
|
||||||
|
print(f"\n共写入 {len(written)} 个 action 文件")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("用法:")
|
||||||
|
print(" python extract_device_actions.py [--registry <path>] # 列出设备")
|
||||||
|
print(" python extract_device_actions.py [--registry <path>] <device_id> <dir> # 提取 actions")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
69
.cursor/skills/create-device-skill/scripts/gen_auth.py
Normal file
69
.cursor/skills/create-device-skill/scripts/gen_auth.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
从 ak/sk 生成 UniLab API Authorization header。
|
||||||
|
|
||||||
|
算法: base64(ak:sk) → "Authorization: Lab <token>"
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python gen_auth.py <ak> <sk>
|
||||||
|
python gen_auth.py --config <config.py>
|
||||||
|
|
||||||
|
示例:
|
||||||
|
python gen_auth.py myak mysk
|
||||||
|
python gen_auth.py --config experiments/config.py
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def gen_auth(ak: str, sk: str) -> str:
|
||||||
|
token = base64.b64encode(f"{ak}:{sk}".encode("utf-8")).decode("utf-8")
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def extract_from_config(config_path: str) -> tuple:
|
||||||
|
"""从 config.py 中提取 ak 和 sk"""
|
||||||
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
ak_match = re.search(r'''ak\s*=\s*["']([^"']+)["']''', content)
|
||||||
|
sk_match = re.search(r'''sk\s*=\s*["']([^"']+)["']''', content)
|
||||||
|
if not ak_match or not sk_match:
|
||||||
|
return None, None
|
||||||
|
return ak_match.group(1), sk_match.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = sys.argv[1:]
|
||||||
|
|
||||||
|
if len(args) == 2 and args[0] == "--config":
|
||||||
|
ak, sk = extract_from_config(args[1])
|
||||||
|
if not ak or not sk:
|
||||||
|
print(f"错误: 在 {args[1]} 中未找到 ak/sk 配置")
|
||||||
|
print("期望格式: ak = \"xxx\" sk = \"xxx\"")
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"配置文件: {args[1]}")
|
||||||
|
elif len(args) == 2:
|
||||||
|
ak, sk = args
|
||||||
|
else:
|
||||||
|
print("用法:")
|
||||||
|
print(" python gen_auth.py <ak> <sk>")
|
||||||
|
print(" python gen_auth.py --config <config.py>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
token = gen_auth(ak, sk)
|
||||||
|
print(f"ak: {ak}")
|
||||||
|
print(f"sk: {sk}")
|
||||||
|
print()
|
||||||
|
print(f"Authorization header:")
|
||||||
|
print(f" Authorization: Lab {token}")
|
||||||
|
print()
|
||||||
|
print(f"curl 用法:")
|
||||||
|
print(f' curl -H "Authorization: Lab {token}" ...')
|
||||||
|
print()
|
||||||
|
print(f"Shell 变量:")
|
||||||
|
print(f' AUTH="Authorization: Lab {token}"')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
.conda
|
|
||||||
# .github
|
|
||||||
.idea
|
|
||||||
# .vscode
|
|
||||||
output
|
|
||||||
pylabrobot_repo
|
|
||||||
recipes
|
|
||||||
scripts
|
|
||||||
service
|
|
||||||
temp
|
|
||||||
# unilabos/test
|
|
||||||
# unilabos/app/web
|
|
||||||
unilabos/device_mesh
|
|
||||||
unilabos_data
|
|
||||||
unilabos_msgs
|
|
||||||
unilabos.egg-info
|
|
||||||
CONTRIBUTORS
|
|
||||||
# LICENSE
|
|
||||||
MANIFEST.in
|
|
||||||
pyrightconfig.json
|
|
||||||
# README.md
|
|
||||||
# README_zh.md
|
|
||||||
setup.py
|
|
||||||
setup.cfg
|
|
||||||
.gitattrubutes
|
|
||||||
**/__pycache__
|
|
||||||
19
.github/dependabot.yml
vendored
Normal file
19
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
target-branch: "dev"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "06:00"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
reviewers:
|
||||||
|
- "msgcenterpy-team"
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "github-actions"
|
||||||
|
commit-message:
|
||||||
|
prefix: "ci"
|
||||||
|
include: "scope"
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -252,9 +252,3 @@ ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2
|
|||||||
test_config.py
|
test_config.py
|
||||||
|
|
||||||
|
|
||||||
/.claude
|
|
||||||
/.conda
|
|
||||||
/.cursor
|
|
||||||
/.github
|
|
||||||
/.conda/base
|
|
||||||
.conda/base/recipe.yaml
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
# Liquid handling 集成测试
|
|
||||||
|
|
||||||
`test_transfer_liquid.py` 现在会调用 PRCXI 的 RViz 仿真 backend,运行前请确保:
|
|
||||||
|
|
||||||
1. 已安装包含 `pylabrobot`、`rclpy` 的运行环境;
|
|
||||||
2. 启动 ROS 依赖(`rviz` 可选,但是 `rviz_backend` 会创建 ROS 节点);
|
|
||||||
3. 在 shell 中设置 `UNILAB_SIM_TEST=1`,否则 pytest 会自动跳过这些慢速用例:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export UNILAB_SIM_TEST=1
|
|
||||||
pytest tests/devices/liquid_handling/test_transfer_liquid.py -m slow
|
|
||||||
```
|
|
||||||
|
|
||||||
如果只需验证逻辑层(不依赖仿真),可以直接运行 `tests/devices/liquid_handling/unit_test.py`,该文件使用 Fake backend,适合作为 CI 的快速测试。***
|
|
||||||
|
|
||||||
@@ -1,547 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Iterable, List, Optional, Sequence, Tuple
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class DummyContainer:
|
|
||||||
name: str
|
|
||||||
|
|
||||||
def __repr__(self) -> str: # pragma: no cover
|
|
||||||
return f"DummyContainer({self.name})"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class DummyTipSpot:
|
|
||||||
name: str
|
|
||||||
|
|
||||||
def __repr__(self) -> str: # pragma: no cover
|
|
||||||
return f"DummyTipSpot({self.name})"
|
|
||||||
|
|
||||||
|
|
||||||
def make_tip_iter(n: int = 256) -> Iterable[List[DummyTipSpot]]:
|
|
||||||
"""Yield lists so code can safely call `tip.extend(next(self.current_tip))`."""
|
|
||||||
for i in range(n):
|
|
||||||
yield [DummyTipSpot(f"tip_{i}")]
|
|
||||||
|
|
||||||
|
|
||||||
class FakeLiquidHandler(LiquidHandlerAbstract):
|
|
||||||
"""不初始化真实 backend/deck;仅用来记录 transfer_liquid 内部调用序列。"""
|
|
||||||
|
|
||||||
def __init__(self, channel_num: int = 8):
|
|
||||||
# 不调用 super().__init__,避免真实硬件/后端依赖
|
|
||||||
self.channel_num = channel_num
|
|
||||||
self.support_touch_tip = True
|
|
||||||
self.current_tip = iter(make_tip_iter())
|
|
||||||
self.calls: List[Tuple[str, Any]] = []
|
|
||||||
|
|
||||||
async def pick_up_tips(self, tip_spots, use_channels=None, offsets=None, **backend_kwargs):
|
|
||||||
self.calls.append(("pick_up_tips", {"tips": list(tip_spots), "use_channels": use_channels}))
|
|
||||||
|
|
||||||
async def aspirate(
|
|
||||||
self,
|
|
||||||
resources: Sequence[Any],
|
|
||||||
vols: List[float],
|
|
||||||
use_channels: Optional[List[int]] = None,
|
|
||||||
flow_rates: Optional[List[Optional[float]]] = None,
|
|
||||||
offsets: Any = None,
|
|
||||||
liquid_height: Any = None,
|
|
||||||
blow_out_air_volume: Any = None,
|
|
||||||
spread: str = "wide",
|
|
||||||
**backend_kwargs,
|
|
||||||
):
|
|
||||||
self.calls.append(
|
|
||||||
(
|
|
||||||
"aspirate",
|
|
||||||
{
|
|
||||||
"resources": list(resources),
|
|
||||||
"vols": list(vols),
|
|
||||||
"use_channels": list(use_channels) if use_channels is not None else None,
|
|
||||||
"flow_rates": list(flow_rates) if flow_rates is not None else None,
|
|
||||||
"offsets": list(offsets) if offsets is not None else None,
|
|
||||||
"liquid_height": list(liquid_height) if liquid_height is not None else None,
|
|
||||||
"blow_out_air_volume": list(blow_out_air_volume) if blow_out_air_volume is not None else None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def dispense(
|
|
||||||
self,
|
|
||||||
resources: Sequence[Any],
|
|
||||||
vols: List[float],
|
|
||||||
use_channels: Optional[List[int]] = None,
|
|
||||||
flow_rates: Optional[List[Optional[float]]] = None,
|
|
||||||
offsets: Any = None,
|
|
||||||
liquid_height: Any = None,
|
|
||||||
blow_out_air_volume: Any = None,
|
|
||||||
spread: str = "wide",
|
|
||||||
**backend_kwargs,
|
|
||||||
):
|
|
||||||
self.calls.append(
|
|
||||||
(
|
|
||||||
"dispense",
|
|
||||||
{
|
|
||||||
"resources": list(resources),
|
|
||||||
"vols": list(vols),
|
|
||||||
"use_channels": list(use_channels) if use_channels is not None else None,
|
|
||||||
"flow_rates": list(flow_rates) if flow_rates is not None else None,
|
|
||||||
"offsets": list(offsets) if offsets is not None else None,
|
|
||||||
"liquid_height": list(liquid_height) if liquid_height is not None else None,
|
|
||||||
"blow_out_air_volume": list(blow_out_air_volume) if blow_out_air_volume is not None else None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def discard_tips(self, use_channels=None, *args, **kwargs):
|
|
||||||
# 有的分支是 discard_tips(use_channels=[0]),有的分支是 discard_tips([0..7])(位置参数)
|
|
||||||
self.calls.append(("discard_tips", {"use_channels": list(use_channels) if use_channels is not None else None}))
|
|
||||||
|
|
||||||
async def custom_delay(self, seconds=0, msg=None):
|
|
||||||
self.calls.append(("custom_delay", {"seconds": seconds, "msg": msg}))
|
|
||||||
|
|
||||||
async def touch_tip(self, targets):
|
|
||||||
# 原实现会访问 targets.get_size_x() 等;测试里只记录调用
|
|
||||||
self.calls.append(("touch_tip", {"targets": targets}))
|
|
||||||
|
|
||||||
def run(coro):
|
|
||||||
return asyncio.run(coro)
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_one_single_channel_basic_calls():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
lh.current_tip = iter(make_tip_iter(64))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(3)]
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(3)]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=[1, 2, 3],
|
|
||||||
dis_vols=[4, 5, 6],
|
|
||||||
mix_times=None, # 应该仍能执行(不 mix)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert [c[0] for c in lh.calls].count("pick_up_tips") == 3
|
|
||||||
assert [c[0] for c in lh.calls].count("aspirate") == 3
|
|
||||||
assert [c[0] for c in lh.calls].count("dispense") == 3
|
|
||||||
assert [c[0] for c in lh.calls].count("discard_tips") == 3
|
|
||||||
|
|
||||||
# 每次 aspirate/dispense 都是单孔列表
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
assert aspirates[0]["resources"] == [sources[0]]
|
|
||||||
assert aspirates[0]["vols"] == [1.0]
|
|
||||||
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert dispenses[2]["resources"] == [targets[2]]
|
|
||||||
assert dispenses[2]["vols"] == [6.0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_one_single_channel_before_stage_mixes_prior_to_aspirate():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
lh.current_tip = iter(make_tip_iter(16))
|
|
||||||
|
|
||||||
source = DummyContainer("S0")
|
|
||||||
target = DummyContainer("T0")
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=[source],
|
|
||||||
targets=[target],
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=[5],
|
|
||||||
dis_vols=[5],
|
|
||||||
mix_stage="before",
|
|
||||||
mix_times=1,
|
|
||||||
mix_vol=3,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirate_calls = [(idx, payload) for idx, (name, payload) in enumerate(lh.calls) if name == "aspirate"]
|
|
||||||
assert len(aspirate_calls) >= 2
|
|
||||||
mix_idx, mix_payload = aspirate_calls[0]
|
|
||||||
assert mix_payload["resources"] == [target]
|
|
||||||
assert mix_payload["vols"] == [3]
|
|
||||||
transfer_idx, transfer_payload = aspirate_calls[1]
|
|
||||||
assert transfer_payload["resources"] == [source]
|
|
||||||
assert mix_idx < transfer_idx
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_one_eight_channel_groups_by_8():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(256))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(16)]
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(16)]
|
|
||||||
asp_vols = list(range(1, 17))
|
|
||||||
dis_vols = list(range(101, 117))
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=list(range(8)),
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=dis_vols,
|
|
||||||
mix_times=0, # 触发逻辑但不 mix
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 16 个任务 -> 2 组,每组 8 通道一起做
|
|
||||||
assert [c[0] for c in lh.calls].count("pick_up_tips") == 2
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert len(aspirates) == 2
|
|
||||||
assert len(dispenses) == 2
|
|
||||||
|
|
||||||
assert aspirates[0]["resources"] == sources[0:8]
|
|
||||||
assert aspirates[0]["vols"] == [float(v) for v in asp_vols[0:8]]
|
|
||||||
assert dispenses[1]["resources"] == targets[8:16]
|
|
||||||
assert dispenses[1]["vols"] == [float(v) for v in dis_vols[8:16]]
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_one_eight_channel_requires_multiple_of_8_targets():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(64))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(9)]
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(9)]
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="multiple of 8"):
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=list(range(8)),
|
|
||||||
asp_vols=[1] * 9,
|
|
||||||
dis_vols=[1] * 9,
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_one_eight_channel_parameter_lists_are_chunked_per_8():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(512))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(16)]
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(16)]
|
|
||||||
asp_vols = [i + 1 for i in range(16)]
|
|
||||||
dis_vols = [200 + i for i in range(16)]
|
|
||||||
asp_flow_rates = [0.1 * (i + 1) for i in range(16)]
|
|
||||||
dis_flow_rates = [0.2 * (i + 1) for i in range(16)]
|
|
||||||
offsets = [f"offset_{i}" for i in range(16)]
|
|
||||||
liquid_heights = [i * 0.5 for i in range(16)]
|
|
||||||
blow_out_air_volume = [i + 0.05 for i in range(16)]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=list(range(8)),
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=dis_vols,
|
|
||||||
asp_flow_rates=asp_flow_rates,
|
|
||||||
dis_flow_rates=dis_flow_rates,
|
|
||||||
offsets=offsets,
|
|
||||||
liquid_height=liquid_heights,
|
|
||||||
blow_out_air_volume=blow_out_air_volume,
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert len(aspirates) == len(dispenses) == 2
|
|
||||||
|
|
||||||
for batch_idx in range(2):
|
|
||||||
start = batch_idx * 8
|
|
||||||
end = start + 8
|
|
||||||
asp_call = aspirates[batch_idx]
|
|
||||||
dis_call = dispenses[batch_idx]
|
|
||||||
assert asp_call["resources"] == sources[start:end]
|
|
||||||
assert asp_call["flow_rates"] == asp_flow_rates[start:end]
|
|
||||||
assert asp_call["offsets"] == offsets[start:end]
|
|
||||||
assert asp_call["liquid_height"] == liquid_heights[start:end]
|
|
||||||
assert asp_call["blow_out_air_volume"] == blow_out_air_volume[start:end]
|
|
||||||
assert dis_call["flow_rates"] == dis_flow_rates[start:end]
|
|
||||||
assert dis_call["offsets"] == offsets[start:end]
|
|
||||||
assert dis_call["liquid_height"] == liquid_heights[start:end]
|
|
||||||
assert dis_call["blow_out_air_volume"] == blow_out_air_volume[start:end]
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_one_eight_channel_handles_32_tasks_four_batches():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(1024))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(32)]
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(32)]
|
|
||||||
asp_vols = [i + 1 for i in range(32)]
|
|
||||||
dis_vols = [300 + i for i in range(32)]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=list(range(8)),
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=dis_vols,
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
pick_calls = [name for name, _ in lh.calls if name == "pick_up_tips"]
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert len(pick_calls) == 4
|
|
||||||
assert len(aspirates) == len(dispenses) == 4
|
|
||||||
assert aspirates[0]["resources"] == sources[0:8]
|
|
||||||
assert aspirates[-1]["resources"] == sources[24:32]
|
|
||||||
assert dispenses[0]["resources"] == targets[0:8]
|
|
||||||
assert dispenses[-1]["resources"] == targets[24:32]
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_many_single_channel_aspirates_total_when_asp_vol_too_small():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
lh.current_tip = iter(make_tip_iter(64))
|
|
||||||
|
|
||||||
source = DummyContainer("SRC")
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(3)]
|
|
||||||
dis_vols = [10, 20, 30] # sum=60
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=[source],
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=10, # 小于 sum(dis_vols) -> 应吸 60
|
|
||||||
dis_vols=dis_vols,
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
assert len(aspirates) == 1
|
|
||||||
assert aspirates[0]["resources"] == [source]
|
|
||||||
assert aspirates[0]["vols"] == [60.0]
|
|
||||||
assert aspirates[0]["use_channels"] == [0]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert [d["vols"][0] for d in dispenses] == [10.0, 20.0, 30.0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_one_to_many_eight_channel_basic():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(128))
|
|
||||||
|
|
||||||
source = DummyContainer("SRC")
|
|
||||||
targets = [DummyContainer(f"T{i}") for i in range(8)]
|
|
||||||
dis_vols = [i + 1 for i in range(8)]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=[source],
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=list(range(8)),
|
|
||||||
asp_vols=999, # one-to-many 8ch 会按 dis_vols 吸(每通道各自)
|
|
||||||
dis_vols=dis_vols,
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
assert aspirates[0]["resources"] == [source] * 8
|
|
||||||
assert aspirates[0]["vols"] == [float(v) for v in dis_vols]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert dispenses[0]["resources"] == targets
|
|
||||||
assert dispenses[0]["vols"] == [float(v) for v in dis_vols]
|
|
||||||
|
|
||||||
|
|
||||||
def test_many_to_one_single_channel_standard_dispense_equals_asp_by_default():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
lh.current_tip = iter(make_tip_iter(128))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(3)]
|
|
||||||
target = DummyContainer("T")
|
|
||||||
asp_vols = [5, 6, 7]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=[target],
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=1, # many-to-one 允许标量;非比例模式下实际每次分液=对应 asp_vol
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert [d["vols"][0] for d in dispenses] == [float(v) for v in asp_vols]
|
|
||||||
assert all(d["resources"] == [target] for d in dispenses)
|
|
||||||
|
|
||||||
|
|
||||||
def test_many_to_one_single_channel_before_stage_mixes_target_once():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
lh.current_tip = iter(make_tip_iter(128))
|
|
||||||
|
|
||||||
sources = [DummyContainer("S0"), DummyContainer("S1")]
|
|
||||||
target = DummyContainer("T")
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=[target],
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=[5, 6],
|
|
||||||
dis_vols=1,
|
|
||||||
mix_stage="before",
|
|
||||||
mix_times=2,
|
|
||||||
mix_vol=4,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirate_calls = [(idx, payload) for idx, (name, payload) in enumerate(lh.calls) if name == "aspirate"]
|
|
||||||
assert len(aspirate_calls) >= 1
|
|
||||||
mix_idx, mix_payload = aspirate_calls[0]
|
|
||||||
assert mix_payload["resources"] == [target]
|
|
||||||
assert mix_payload["vols"] == [4]
|
|
||||||
# 第一個 mix 之後會真正開始吸 source
|
|
||||||
assert any(call["resources"] == [sources[0]] for _, call in aspirate_calls[1:])
|
|
||||||
|
|
||||||
|
|
||||||
def test_many_to_one_single_channel_proportional_mixing_uses_dis_vols_per_source():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
lh.current_tip = iter(make_tip_iter(128))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(3)]
|
|
||||||
target = DummyContainer("T")
|
|
||||||
asp_vols = [5, 6, 7]
|
|
||||||
dis_vols = [1, 2, 3]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=[target],
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=dis_vols, # 比例模式
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert [d["vols"][0] for d in dispenses] == [float(v) for v in dis_vols]
|
|
||||||
|
|
||||||
|
|
||||||
def test_many_to_one_eight_channel_basic():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(256))
|
|
||||||
|
|
||||||
sources = [DummyContainer(f"S{i}") for i in range(8)]
|
|
||||||
target = DummyContainer("T")
|
|
||||||
asp_vols = [10 + i for i in range(8)]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=[target],
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=list(range(8)),
|
|
||||||
asp_vols=asp_vols,
|
|
||||||
dis_vols=999, # 非比例模式下每通道分液=对应 asp_vol
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert aspirates[0]["resources"] == sources
|
|
||||||
assert aspirates[0]["vols"] == [float(v) for v in asp_vols]
|
|
||||||
assert dispenses[0]["resources"] == [target] * 8
|
|
||||||
assert dispenses[0]["vols"] == [float(v) for v in asp_vols]
|
|
||||||
|
|
||||||
|
|
||||||
def test_transfer_liquid_mode_detection_unsupported_shape_raises():
|
|
||||||
lh = FakeLiquidHandler(channel_num=8)
|
|
||||||
lh.current_tip = iter(make_tip_iter(64))
|
|
||||||
|
|
||||||
sources = [DummyContainer("S0"), DummyContainer("S1")]
|
|
||||||
targets = [DummyContainer("T0"), DummyContainer("T1"), DummyContainer("T2")]
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Unsupported transfer mode"):
|
|
||||||
run(
|
|
||||||
lh.transfer_liquid(
|
|
||||||
sources=sources,
|
|
||||||
targets=targets,
|
|
||||||
tip_racks=[],
|
|
||||||
use_channels=[0],
|
|
||||||
asp_vols=[1, 1],
|
|
||||||
dis_vols=[1, 1, 1],
|
|
||||||
mix_times=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_mix_single_target_produces_matching_cycles():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
target = DummyContainer("T_mix")
|
|
||||||
|
|
||||||
run(lh.mix(targets=[target], mix_time=2, mix_vol=5))
|
|
||||||
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
|
||||||
assert len(aspirates) == len(dispenses) == 2
|
|
||||||
assert all(call["resources"] == [target] for call in aspirates)
|
|
||||||
assert all(call["vols"] == [5] for call in aspirates)
|
|
||||||
assert all(call["resources"] == [target] for call in dispenses)
|
|
||||||
assert all(call["vols"] == [5] for call in dispenses)
|
|
||||||
|
|
||||||
|
|
||||||
def test_mix_multiple_targets_supports_per_target_offsets():
|
|
||||||
lh = FakeLiquidHandler(channel_num=1)
|
|
||||||
targets = [DummyContainer("T0"), DummyContainer("T1")]
|
|
||||||
offsets = ["left", "right"]
|
|
||||||
heights = [0.1, 0.2]
|
|
||||||
rates = [0.5, 1.0]
|
|
||||||
|
|
||||||
run(
|
|
||||||
lh.mix(
|
|
||||||
targets=targets,
|
|
||||||
mix_time=1,
|
|
||||||
mix_vol=3,
|
|
||||||
offsets=offsets,
|
|
||||||
height_to_bottom=heights,
|
|
||||||
mix_rate=rates,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
|
||||||
assert len(aspirates) == 2
|
|
||||||
assert aspirates[0]["resources"] == [targets[0]]
|
|
||||||
assert aspirates[0]["offsets"] == [offsets[0]]
|
|
||||||
assert aspirates[0]["liquid_height"] == [heights[0]]
|
|
||||||
assert aspirates[0]["flow_rates"] == [rates[0]]
|
|
||||||
assert aspirates[1]["resources"] == [targets[1]]
|
|
||||||
assert aspirates[1]["offsets"] == [offsets[1]]
|
|
||||||
assert aspirates[1]["liquid_height"] == [heights[1]]
|
|
||||||
assert aspirates[1]["flow_rates"] == [rates[1]]
|
|
||||||
|
|
||||||
|
|
||||||
6
unilabos/__main__.py
Normal file
6
unilabos/__main__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
"""Entry point for `python -m unilabos`."""
|
||||||
|
|
||||||
|
from unilabos.app.main import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -264,12 +264,6 @@ def parse_args():
|
|||||||
default=False,
|
default=False,
|
||||||
help="Test mode: all actions simulate execution and return mock results without running real hardware",
|
help="Test mode: all actions simulate execution and return mock results without running real hardware",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--external_devices_only",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Only load external device packages (--devices), skip built-in unilabos/devices/ scanning and YAML device registry",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--extra_resource",
|
"--extra_resource",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -348,18 +342,11 @@ def main():
|
|||||||
check_mode = args_dict.get("check_mode", False)
|
check_mode = args_dict.get("check_mode", False)
|
||||||
|
|
||||||
if not skip_env_check:
|
if not skip_env_check:
|
||||||
from unilabos.utils.environment_check import check_environment, check_device_package_requirements
|
from unilabos.utils.environment_check import check_environment
|
||||||
|
|
||||||
if not check_environment(auto_install=True):
|
if not check_environment(auto_install=True):
|
||||||
print_status("环境检查失败,程序退出", "error")
|
print_status("环境检查失败,程序退出", "error")
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
|
||||||
# 第一次设备包依赖检查:build_registry 之前,确保 import map 可用
|
|
||||||
devices_dirs_for_req = args_dict.get("devices", None)
|
|
||||||
if devices_dirs_for_req:
|
|
||||||
if not check_device_package_requirements(devices_dirs_for_req):
|
|
||||||
print_status("设备包依赖检查失败,程序退出", "error")
|
|
||||||
os._exit(1)
|
|
||||||
else:
|
else:
|
||||||
print_status("跳过环境依赖检查", "warning")
|
print_status("跳过环境依赖检查", "warning")
|
||||||
|
|
||||||
@@ -490,7 +477,19 @@ def main():
|
|||||||
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
|
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
|
||||||
BasicConfig.check_mode = check_mode
|
BasicConfig.check_mode = check_mode
|
||||||
|
|
||||||
|
from unilabos.resources.graphio import (
|
||||||
|
read_node_link_json,
|
||||||
|
read_graphml,
|
||||||
|
dict_from_graph,
|
||||||
|
)
|
||||||
|
from unilabos.app.communication import get_communication_client
|
||||||
from unilabos.registry.registry import build_registry
|
from unilabos.registry.registry import build_registry
|
||||||
|
from unilabos.app.backend import start_backend
|
||||||
|
from unilabos.app.web import http_client
|
||||||
|
from unilabos.app.web import start_server
|
||||||
|
from unilabos.app.register import register_devices_and_resources
|
||||||
|
from unilabos.resources.graphio import modify_to_backend_format
|
||||||
|
from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDict
|
||||||
|
|
||||||
# 显示启动横幅
|
# 显示启动横幅
|
||||||
print_unilab_banner(args_dict)
|
print_unilab_banner(args_dict)
|
||||||
@@ -499,14 +498,12 @@ def main():
|
|||||||
# check_mode 和 upload_registry 都会执行实际 import 验证
|
# check_mode 和 upload_registry 都会执行实际 import 验证
|
||||||
devices_dirs = args_dict.get("devices", None)
|
devices_dirs = args_dict.get("devices", None)
|
||||||
complete_registry = args_dict.get("complete_registry", False) or check_mode
|
complete_registry = args_dict.get("complete_registry", False) or check_mode
|
||||||
external_only = args_dict.get("external_devices_only", False)
|
|
||||||
lab_registry = build_registry(
|
lab_registry = build_registry(
|
||||||
registry_paths=args_dict["registry_path"],
|
registry_paths=args_dict["registry_path"],
|
||||||
devices_dirs=devices_dirs,
|
devices_dirs=devices_dirs,
|
||||||
upload_registry=BasicConfig.upload_registry,
|
upload_registry=BasicConfig.upload_registry,
|
||||||
check_mode=check_mode,
|
check_mode=check_mode,
|
||||||
complete_registry=complete_registry,
|
complete_registry=complete_registry,
|
||||||
external_only=external_only,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check mode: 注册表验证完成后直接退出
|
# Check mode: 注册表验证完成后直接退出
|
||||||
@@ -516,20 +513,6 @@ def main():
|
|||||||
print_status(f"Check mode: 注册表验证完成 ({device_count} 设备, {resource_count} 资源),退出", "info")
|
print_status(f"Check mode: 注册表验证完成 ({device_count} 设备, {resource_count} 资源),退出", "info")
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
# 以下导入依赖 ROS2 环境,check_mode 已退出不需要
|
|
||||||
from unilabos.resources.graphio import (
|
|
||||||
read_node_link_json,
|
|
||||||
read_graphml,
|
|
||||||
dict_from_graph,
|
|
||||||
modify_to_backend_format,
|
|
||||||
)
|
|
||||||
from unilabos.app.communication import get_communication_client
|
|
||||||
from unilabos.app.backend import start_backend
|
|
||||||
from unilabos.app.web import http_client
|
|
||||||
from unilabos.app.web import start_server
|
|
||||||
from unilabos.app.register import register_devices_and_resources
|
|
||||||
from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDict
|
|
||||||
|
|
||||||
# Step 1: 上传全部注册表到服务端,同步保存到 unilabos_data
|
# Step 1: 上传全部注册表到服务端,同步保存到 unilabos_data
|
||||||
if BasicConfig.upload_registry:
|
if BasicConfig.upload_registry:
|
||||||
if BasicConfig.ak and BasicConfig.sk:
|
if BasicConfig.ak and BasicConfig.sk:
|
||||||
@@ -627,10 +610,6 @@ def main():
|
|||||||
resource_tree_set.merge_remote_resources(remote_tree_set)
|
resource_tree_set.merge_remote_resources(remote_tree_set)
|
||||||
print_status("远端物料同步完成", "info")
|
print_status("远端物料同步完成", "info")
|
||||||
|
|
||||||
# 第二次设备包依赖检查:云端物料同步后,community 包可能引入新的 requirements
|
|
||||||
# TODO: 当 community device package 功能上线后,在这里调用
|
|
||||||
# install_requirements_txt(community_pkg_path / "requirements.txt", label="community.xxx")
|
|
||||||
|
|
||||||
# 使用 ResourceTreeSet 代替 list
|
# 使用 ResourceTreeSet 代替 list
|
||||||
args_dict["resources_config"] = resource_tree_set
|
args_dict["resources_config"] = resource_tree_set
|
||||||
args_dict["devices_config"] = resource_tree_set
|
args_dict["devices_config"] = resource_tree_set
|
||||||
|
|||||||
@@ -58,14 +58,14 @@ class JobResultStore:
|
|||||||
feedback=feedback or {},
|
feedback=feedback or {},
|
||||||
timestamp=time.time(),
|
timestamp=time.time(),
|
||||||
)
|
)
|
||||||
logger.debug(f"[JobResultStore] Stored result for job {job_id[:8]}, status={status}")
|
logger.trace(f"[JobResultStore] Stored result for job {job_id[:8]}, status={status}")
|
||||||
|
|
||||||
def get_and_remove(self, job_id: str) -> Optional[JobResult]:
|
def get_and_remove(self, job_id: str) -> Optional[JobResult]:
|
||||||
"""获取并删除任务结果"""
|
"""获取并删除任务结果"""
|
||||||
with self._results_lock:
|
with self._results_lock:
|
||||||
result = self._results.pop(job_id, None)
|
result = self._results.pop(job_id, None)
|
||||||
if result:
|
if result:
|
||||||
logger.debug(f"[JobResultStore] Retrieved and removed result for job {job_id[:8]}")
|
logger.trace(f"[JobResultStore] Retrieved and removed result for job {job_id[:8]}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_result(self, job_id: str) -> Optional[JobResult]:
|
def get_result(self, job_id: str) -> Optional[JobResult]:
|
||||||
|
|||||||
@@ -754,32 +754,6 @@ class MessageProcessor:
|
|||||||
req = JobAddReq(**data)
|
req = JobAddReq(**data)
|
||||||
|
|
||||||
job_log = format_job_log(req.job_id, req.task_id, req.device_id, req.action)
|
job_log = format_job_log(req.job_id, req.task_id, req.device_id, req.action)
|
||||||
|
|
||||||
# 服务端对always_free动作可能跳过query_action_state直接发job_start,
|
|
||||||
# 此时job尚未注册,需要自动补注册
|
|
||||||
existing_job = self.device_manager.get_job_info(req.job_id)
|
|
||||||
if not existing_job:
|
|
||||||
action_name = req.action
|
|
||||||
device_action_key = f"/devices/{req.device_id}/{action_name}"
|
|
||||||
action_always_free = self._check_action_always_free(req.device_id, action_name)
|
|
||||||
|
|
||||||
if action_always_free:
|
|
||||||
job_info = JobInfo(
|
|
||||||
job_id=req.job_id,
|
|
||||||
task_id=req.task_id,
|
|
||||||
device_id=req.device_id,
|
|
||||||
action_name=action_name,
|
|
||||||
device_action_key=device_action_key,
|
|
||||||
status=JobStatus.QUEUE,
|
|
||||||
start_time=time.time(),
|
|
||||||
always_free=True,
|
|
||||||
)
|
|
||||||
self.device_manager.add_queue_request(job_info)
|
|
||||||
logger.info(f"[MessageProcessor] Job {job_log} always_free, auto-registered from direct job_start")
|
|
||||||
else:
|
|
||||||
logger.error(f"[MessageProcessor] Job {job_log} not registered (missing query_action_state)")
|
|
||||||
return
|
|
||||||
|
|
||||||
success = self.device_manager.start_job(req.job_id)
|
success = self.device_manager.start_job(req.job_id)
|
||||||
if not success:
|
if not success:
|
||||||
logger.error(f"[MessageProcessor] Failed to start job {job_log}")
|
logger.error(f"[MessageProcessor] Failed to start job {job_log}")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -208,8 +208,7 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
spread: Literal["wide", "tight", "custom"] = "wide",
|
spread: Literal["wide", "tight", "custom"] = "wide",
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
):
|
):
|
||||||
if spread == "":
|
|
||||||
spread = "wide"
|
|
||||||
if self._simulator:
|
if self._simulator:
|
||||||
return await self._simulate_handler.aspirate(
|
return await self._simulate_handler.aspirate(
|
||||||
resources,
|
resources,
|
||||||
@@ -222,46 +221,23 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
spread,
|
spread,
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
)
|
)
|
||||||
try:
|
await super().aspirate(
|
||||||
await super().aspirate(
|
resources,
|
||||||
resources,
|
vols,
|
||||||
vols,
|
use_channels,
|
||||||
use_channels,
|
flow_rates,
|
||||||
flow_rates,
|
offsets,
|
||||||
offsets,
|
liquid_height,
|
||||||
liquid_height,
|
blow_out_air_volume,
|
||||||
blow_out_air_volume,
|
spread,
|
||||||
spread,
|
**backend_kwargs,
|
||||||
**backend_kwargs,
|
)
|
||||||
)
|
|
||||||
except ValueError as e:
|
|
||||||
if "Resource is too small to space channels" in str(e) and spread != "custom":
|
|
||||||
await super().aspirate(
|
|
||||||
resources,
|
|
||||||
vols,
|
|
||||||
use_channels,
|
|
||||||
flow_rates,
|
|
||||||
offsets,
|
|
||||||
liquid_height,
|
|
||||||
blow_out_air_volume,
|
|
||||||
spread="custom",
|
|
||||||
**backend_kwargs,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
res_samples = []
|
res_samples = []
|
||||||
res_volumes = []
|
res_volumes = []
|
||||||
# 处理 use_channels 为 None 的情况(通常用于单通道操作)
|
for resource, volume, channel in zip(resources, vols, use_channels):
|
||||||
if use_channels is None:
|
sample_uuid_value = resource.unilabos_extra.get(EXTRA_SAMPLE_UUID, None)
|
||||||
# 对于单通道操作,推断通道为 [0]
|
res_samples.append({"name": resource.name, EXTRA_SAMPLE_UUID: sample_uuid_value})
|
||||||
channels_to_use = [0] * len(resources)
|
|
||||||
else:
|
|
||||||
channels_to_use = use_channels
|
|
||||||
|
|
||||||
for resource, volume, channel in zip(resources, vols, channels_to_use):
|
|
||||||
sample_uuid_value = getattr(resource, "unilabos_extra", {}).get(EXTRA_SAMPLE_UUID, None)
|
|
||||||
res_samples.append({"name": resource.name, "sample_uuid": sample_uuid_value})
|
|
||||||
res_volumes.append(volume)
|
res_volumes.append(volume)
|
||||||
self.pending_liquids_dict[channel] = {
|
self.pending_liquids_dict[channel] = {
|
||||||
EXTRA_SAMPLE_UUID: sample_uuid_value,
|
EXTRA_SAMPLE_UUID: sample_uuid_value,
|
||||||
@@ -281,8 +257,6 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
spread: Literal["wide", "tight", "custom"] = "wide",
|
spread: Literal["wide", "tight", "custom"] = "wide",
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
) -> SimpleReturn:
|
) -> SimpleReturn:
|
||||||
if spread == "":
|
|
||||||
spread = "wide"
|
|
||||||
if self._simulator:
|
if self._simulator:
|
||||||
return await self._simulate_handler.dispense(
|
return await self._simulate_handler.dispense(
|
||||||
resources,
|
resources,
|
||||||
@@ -295,33 +269,16 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
spread,
|
spread,
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
)
|
)
|
||||||
try:
|
await super().dispense(
|
||||||
await super().dispense(
|
resources,
|
||||||
resources,
|
vols,
|
||||||
vols,
|
use_channels,
|
||||||
use_channels,
|
flow_rates,
|
||||||
flow_rates,
|
offsets,
|
||||||
offsets,
|
liquid_height,
|
||||||
liquid_height,
|
blow_out_air_volume,
|
||||||
blow_out_air_volume,
|
**backend_kwargs,
|
||||||
spread,
|
)
|
||||||
**backend_kwargs,
|
|
||||||
)
|
|
||||||
except ValueError as e:
|
|
||||||
if "Resource is too small to space channels" in str(e) and spread != "custom":
|
|
||||||
await super().dispense(
|
|
||||||
resources,
|
|
||||||
vols,
|
|
||||||
use_channels,
|
|
||||||
flow_rates,
|
|
||||||
offsets,
|
|
||||||
liquid_height,
|
|
||||||
blow_out_air_volume,
|
|
||||||
"custom",
|
|
||||||
**backend_kwargs,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
res_samples = []
|
res_samples = []
|
||||||
res_volumes = []
|
res_volumes = []
|
||||||
for resource, volume, channel in zip(resources, vols, use_channels):
|
for resource, volume, channel in zip(resources, vols, use_channels):
|
||||||
@@ -891,7 +848,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for _ in range(len(sources)):
|
for _ in range(len(sources)):
|
||||||
tip = []
|
tip = []
|
||||||
for __ in range(len(use_channels)):
|
for __ in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.extend(next(self.current_tip))
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
resources=[sources[_]],
|
resources=[sources[_]],
|
||||||
@@ -931,7 +888,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(sources), 8):
|
for i in range(0, len(sources), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.extend(next(self.current_tip))
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
current_targets = waste_liquid[i : i + 8]
|
current_targets = waste_liquid[i : i + 8]
|
||||||
current_reagent_sources = sources[i : i + 8]
|
current_reagent_sources = sources[i : i + 8]
|
||||||
@@ -1025,7 +982,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for _ in range(len(targets)):
|
for _ in range(len(targets)):
|
||||||
tip = []
|
tip = []
|
||||||
for x in range(len(use_channels)):
|
for x in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.extend(next(self.current_tip))
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
@@ -1077,7 +1034,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(targets), 8):
|
for i in range(0, len(targets), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.extend(next(self.current_tip))
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
current_targets = targets[i : i + 8]
|
current_targets = targets[i : i + 8]
|
||||||
current_reagent_sources = reagent_sources[i : i + 8]
|
current_reagent_sources = reagent_sources[i : i + 8]
|
||||||
@@ -1202,19 +1159,11 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
Number of mix cycles. If *None* (default) no mixing occurs regardless of
|
Number of mix cycles. If *None* (default) no mixing occurs regardless of
|
||||||
mix_stage.
|
mix_stage.
|
||||||
"""
|
"""
|
||||||
num_sources = len(sources)
|
|
||||||
num_targets = len(targets)
|
|
||||||
len_asp_vols = len(asp_vols)
|
|
||||||
len_dis_vols = len(dis_vols)
|
|
||||||
# 确保 use_channels 有默认值
|
# 确保 use_channels 有默认值
|
||||||
if use_channels is None:
|
if use_channels is None:
|
||||||
# 默认使用设备所有通道(例如 8 通道移液站默认就是 0-7)
|
# 默认使用设备所有通道(例如 8 通道移液站默认就是 0-7)
|
||||||
use_channels = list(range(self.channel_num)) if self.channel_num == 8 else [0]
|
use_channels = list(range(self.channel_num)) if self.channel_num > 0 else [0]
|
||||||
elif len(use_channels) == 8:
|
|
||||||
if self.channel_num != 8:
|
|
||||||
raise ValueError(f"if channel_num is 8, use_channels length must be 8, but got {len(use_channels)}")
|
|
||||||
if num_sources%8 != 0 or num_targets%8 != 0 or len_asp_vols%8 != 0 or len_dis_vols%8 != 0:
|
|
||||||
raise ValueError(f"if channel_num is 8, sources, targets, asp_vols, and dis_vols length must be divisible by 8, but got {num_sources}, {num_targets}, {len_asp_vols}, and {len_dis_vols}")
|
|
||||||
|
|
||||||
if is_96_well:
|
if is_96_well:
|
||||||
pass # This mode is not verified.
|
pass # This mode is not verified.
|
||||||
@@ -1242,233 +1191,89 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
if mix_times is not None:
|
if mix_times is not None:
|
||||||
mix_times = int(mix_times)
|
mix_times = int(mix_times)
|
||||||
|
|
||||||
# 设置tip racks
|
|
||||||
self.set_tiprack(tip_racks)
|
|
||||||
|
|
||||||
# 识别传输模式(mix_times 为 None 也应该能正常移液,只是不做 mix)
|
# 识别传输模式(mix_times 为 None 也应该能正常移液,只是不做 mix)
|
||||||
num_sources = len(sources)
|
num_sources = len(sources)
|
||||||
num_targets = len(targets)
|
num_targets = len(targets)
|
||||||
len_asp_vols = len(asp_vols)
|
|
||||||
len_dis_vols = len(dis_vols)
|
|
||||||
|
|
||||||
# if num_targets != 1 and num_sources != 1:
|
if num_sources == 1 and num_targets > 1:
|
||||||
# if len_asp_vols != num_sources and len_asp_vols != num_targets:
|
# 模式1: 一对多 (1 source -> N targets)
|
||||||
# raise ValueError(f"asp_vols length must be equal to sources or targets length, but got {len_asp_vols} and {num_sources} and {num_targets}")
|
await self._transfer_one_to_many(
|
||||||
# if len_dis_vols != num_sources and len_dis_vols != num_targets:
|
sources[0],
|
||||||
# raise ValueError(f"dis_vols length must be equal to sources or targets length, but got {len_dis_vols} and {num_sources} and {num_targets}")
|
targets,
|
||||||
|
tip_racks,
|
||||||
if len(use_channels) != 8:
|
use_channels,
|
||||||
max_len = max(num_sources, num_targets)
|
asp_vols,
|
||||||
for i in range(max_len):
|
dis_vols,
|
||||||
|
asp_flow_rates,
|
||||||
# 辅助函数:安全地从列表中获取元素,如果列表为空则返回None
|
dis_flow_rates,
|
||||||
def safe_get(lst, idx, default=None):
|
offsets,
|
||||||
return [lst[idx]] if lst else default
|
touch_tip,
|
||||||
|
liquid_height,
|
||||||
# 动态构建参数字典,只传递实际提供的参数
|
blow_out_air_volume,
|
||||||
kwargs = {
|
spread,
|
||||||
'sources': [sources[i%num_sources]],
|
mix_stage,
|
||||||
'targets': [targets[i%num_targets]],
|
mix_times,
|
||||||
'tip_racks': tip_racks,
|
mix_vol,
|
||||||
'use_channels': use_channels,
|
mix_rate,
|
||||||
'asp_vols': [asp_vols[i%len_asp_vols]],
|
mix_liquid_height,
|
||||||
'dis_vols': [dis_vols[i%len_dis_vols]],
|
delays,
|
||||||
}
|
)
|
||||||
|
elif num_sources > 1 and num_targets == 1:
|
||||||
# 条件性添加可选参数
|
# 模式2: 多对一 (N sources -> 1 target)
|
||||||
if asp_flow_rates is not None:
|
await self._transfer_many_to_one(
|
||||||
kwargs['asp_flow_rates'] = [asp_flow_rates[i%len_asp_vols]]
|
sources,
|
||||||
if dis_flow_rates is not None:
|
targets[0],
|
||||||
kwargs['dis_flow_rates'] = [dis_flow_rates[i%len_dis_vols]]
|
tip_racks,
|
||||||
if offsets is not None:
|
use_channels,
|
||||||
kwargs['offsets'] = safe_get(offsets, i)
|
asp_vols,
|
||||||
if touch_tip is not None:
|
dis_vols,
|
||||||
kwargs['touch_tip'] = touch_tip if touch_tip else False
|
asp_flow_rates,
|
||||||
if liquid_height is not None:
|
dis_flow_rates,
|
||||||
kwargs['liquid_height'] = safe_get(liquid_height, i)
|
offsets,
|
||||||
if blow_out_air_volume is not None:
|
touch_tip,
|
||||||
kwargs['blow_out_air_volume'] = safe_get(blow_out_air_volume, i)
|
liquid_height,
|
||||||
if spread is not None:
|
blow_out_air_volume,
|
||||||
kwargs['spread'] = spread
|
spread,
|
||||||
if mix_stage is not None:
|
mix_stage,
|
||||||
kwargs['mix_stage'] = safe_get(mix_stage, i)
|
mix_times,
|
||||||
if mix_times is not None:
|
mix_vol,
|
||||||
kwargs['mix_times'] = safe_get(mix_times, i)
|
mix_rate,
|
||||||
if mix_vol is not None:
|
mix_liquid_height,
|
||||||
kwargs['mix_vol'] = safe_get(mix_vol, i)
|
delays,
|
||||||
if mix_rate is not None:
|
)
|
||||||
kwargs['mix_rate'] = safe_get(mix_rate, i)
|
elif num_sources == num_targets:
|
||||||
if mix_liquid_height is not None:
|
# 模式3: 一对一 (N sources -> N targets)
|
||||||
kwargs['mix_liquid_height'] = safe_get(mix_liquid_height, i)
|
await self._transfer_one_to_one(
|
||||||
if delays is not None:
|
sources,
|
||||||
kwargs['delays'] = safe_get(delays, i)
|
targets,
|
||||||
|
tip_racks,
|
||||||
await self._transfer_base_method(**kwargs)
|
use_channels,
|
||||||
|
asp_vols,
|
||||||
|
dis_vols,
|
||||||
|
asp_flow_rates,
|
||||||
# if num_sources == 1 and num_targets > 1:
|
dis_flow_rates,
|
||||||
# # 模式1: 一对多 (1 source -> N targets)
|
offsets,
|
||||||
# await self._transfer_one_to_many(
|
touch_tip,
|
||||||
# sources,
|
liquid_height,
|
||||||
# targets,
|
blow_out_air_volume,
|
||||||
# tip_racks,
|
spread,
|
||||||
# use_channels,
|
mix_stage,
|
||||||
# asp_vols,
|
mix_times,
|
||||||
# dis_vols,
|
mix_vol,
|
||||||
# asp_flow_rates,
|
mix_rate,
|
||||||
# dis_flow_rates,
|
mix_liquid_height,
|
||||||
# offsets,
|
delays,
|
||||||
# touch_tip,
|
)
|
||||||
# liquid_height,
|
else:
|
||||||
# blow_out_air_volume,
|
raise ValueError(
|
||||||
# spread,
|
f"Unsupported transfer mode: {num_sources} sources -> {num_targets} targets. "
|
||||||
# mix_stage,
|
"Supported modes: 1->N, N->1, or N->N."
|
||||||
# mix_times,
|
)
|
||||||
# mix_vol,
|
|
||||||
# mix_rate,
|
|
||||||
# mix_liquid_height,
|
|
||||||
# delays,
|
|
||||||
# )
|
|
||||||
# elif num_sources > 1 and num_targets == 1:
|
|
||||||
# # 模式2: 多对一 (N sources -> 1 target)
|
|
||||||
# await self._transfer_many_to_one(
|
|
||||||
# sources,
|
|
||||||
# targets[0],
|
|
||||||
# tip_racks,
|
|
||||||
# use_channels,
|
|
||||||
# asp_vols,
|
|
||||||
# dis_vols,
|
|
||||||
# asp_flow_rates,
|
|
||||||
# dis_flow_rates,
|
|
||||||
# offsets,
|
|
||||||
# touch_tip,
|
|
||||||
# liquid_height,
|
|
||||||
# blow_out_air_volume,
|
|
||||||
# spread,
|
|
||||||
# mix_stage,
|
|
||||||
# mix_times,
|
|
||||||
# mix_vol,
|
|
||||||
# mix_rate,
|
|
||||||
# mix_liquid_height,
|
|
||||||
# delays,
|
|
||||||
# )
|
|
||||||
# elif num_sources == num_targets:
|
|
||||||
# # 模式3: 一对一 (N sources -> N targets)
|
|
||||||
# await self._transfer_one_to_one(
|
|
||||||
# sources,
|
|
||||||
# targets,
|
|
||||||
# tip_racks,
|
|
||||||
# use_channels,
|
|
||||||
# asp_vols,
|
|
||||||
# dis_vols,
|
|
||||||
# asp_flow_rates,
|
|
||||||
# dis_flow_rates,
|
|
||||||
# offsets,
|
|
||||||
# touch_tip,
|
|
||||||
# liquid_height,
|
|
||||||
# blow_out_air_volume,
|
|
||||||
# spread,
|
|
||||||
# mix_stage,
|
|
||||||
# mix_times,
|
|
||||||
# mix_vol,
|
|
||||||
# mix_rate,
|
|
||||||
# mix_liquid_height,
|
|
||||||
# delays,
|
|
||||||
# )
|
|
||||||
# else:
|
|
||||||
# raise ValueError(
|
|
||||||
# f"Unsupported transfer mode: {num_sources} sources -> {num_targets} targets. "
|
|
||||||
# "Supported modes: 1->N, N->1, or N->N."
|
|
||||||
# )
|
|
||||||
|
|
||||||
return TransferLiquidReturn(
|
return TransferLiquidReturn(
|
||||||
sources=ResourceTreeSet.from_plr_resources(list(sources), known_newly_created=False).dump(), # type: ignore
|
sources=ResourceTreeSet.from_plr_resources(list(sources), known_newly_created=False).dump(), # type: ignore
|
||||||
targets=ResourceTreeSet.from_plr_resources(list(targets), known_newly_created=False).dump(), # type: ignore
|
targets=ResourceTreeSet.from_plr_resources(list(targets), known_newly_created=False).dump(), # type: ignore
|
||||||
)
|
)
|
||||||
async def _transfer_base_method(
|
|
||||||
self,
|
|
||||||
sources: Sequence[Container],
|
|
||||||
targets: Sequence[Container],
|
|
||||||
tip_racks: Sequence[TipRack],
|
|
||||||
use_channels: List[int],
|
|
||||||
asp_vols: List[float],
|
|
||||||
dis_vols: List[float],
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
|
|
||||||
# 从kwargs中提取参数,提供默认值
|
|
||||||
asp_flow_rates = kwargs.get('asp_flow_rates')
|
|
||||||
dis_flow_rates = kwargs.get('dis_flow_rates')
|
|
||||||
offsets = kwargs.get('offsets')
|
|
||||||
touch_tip = kwargs.get('touch_tip', False)
|
|
||||||
liquid_height = kwargs.get('liquid_height')
|
|
||||||
blow_out_air_volume = kwargs.get('blow_out_air_volume')
|
|
||||||
spread = kwargs.get('spread', 'wide')
|
|
||||||
mix_stage = kwargs.get('mix_stage')
|
|
||||||
mix_times = kwargs.get('mix_times')
|
|
||||||
mix_vol = kwargs.get('mix_vol')
|
|
||||||
mix_rate = kwargs.get('mix_rate')
|
|
||||||
mix_liquid_height = kwargs.get('mix_liquid_height')
|
|
||||||
delays = kwargs.get('delays')
|
|
||||||
|
|
||||||
tip = []
|
|
||||||
tip.extend(self._get_next_tip())
|
|
||||||
await self.pick_up_tips(tip)
|
|
||||||
|
|
||||||
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
|
||||||
await self.mix(
|
|
||||||
targets=[targets[0]],
|
|
||||||
mix_time=mix_times,
|
|
||||||
mix_vol=mix_vol,
|
|
||||||
offsets=offsets if offsets else None,
|
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.aspirate(
|
|
||||||
resources=[sources[0]],
|
|
||||||
vols=[asp_vols[0]],
|
|
||||||
use_channels=use_channels,
|
|
||||||
flow_rates=[asp_flow_rates[0]] if asp_flow_rates and len(asp_flow_rates) > 0 else None,
|
|
||||||
offsets=[offsets[0]] if offsets and len(offsets) > 0 else None,
|
|
||||||
liquid_height=[liquid_height[0]] if liquid_height and len(liquid_height) > 0 else None,
|
|
||||||
blow_out_air_volume=(
|
|
||||||
[blow_out_air_volume[0]] if blow_out_air_volume and len(blow_out_air_volume) > 0 else None
|
|
||||||
),
|
|
||||||
spread=spread,
|
|
||||||
)
|
|
||||||
if delays is not None:
|
|
||||||
await self.custom_delay(seconds=delays[0])
|
|
||||||
await self.dispense(
|
|
||||||
resources=[targets[0]],
|
|
||||||
vols=[dis_vols[0]],
|
|
||||||
use_channels=use_channels,
|
|
||||||
flow_rates=[dis_flow_rates[0]] if dis_flow_rates and len(dis_flow_rates) > 0 else None,
|
|
||||||
offsets=[offsets[0]] if offsets and len(offsets) > 0 else None,
|
|
||||||
blow_out_air_volume=(
|
|
||||||
[blow_out_air_volume[0]] if blow_out_air_volume and len(blow_out_air_volume) > 0 else None
|
|
||||||
),
|
|
||||||
liquid_height=[liquid_height[0]] if liquid_height and len(liquid_height) > 0 else None,
|
|
||||||
spread=spread,
|
|
||||||
)
|
|
||||||
if delays is not None and len(delays) > 1:
|
|
||||||
await self.custom_delay(seconds=delays[1])
|
|
||||||
if mix_stage in ["after", "both"] and mix_times is not None and mix_times > 0:
|
|
||||||
await self.mix(
|
|
||||||
targets=[targets[0]],
|
|
||||||
mix_time=mix_times,
|
|
||||||
mix_vol=mix_vol,
|
|
||||||
offsets=offsets if offsets else None,
|
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
|
||||||
if delays is not None and len(delays) > 1:
|
|
||||||
await self.custom_delay(seconds=delays[0])
|
|
||||||
await self.touch_tip(targets[0])
|
|
||||||
await self.discard_tips(use_channels=use_channels)
|
|
||||||
|
|
||||||
async def _transfer_one_to_one(
|
async def _transfer_one_to_one(
|
||||||
self,
|
self,
|
||||||
@@ -1495,23 +1300,17 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
"""一对一传输模式:N sources -> N targets"""
|
"""一对一传输模式:N sources -> N targets"""
|
||||||
# 验证参数长度
|
# 验证参数长度
|
||||||
if len(asp_vols) != len(targets):
|
if len(asp_vols) != len(targets):
|
||||||
if len(asp_vols) == 1:
|
raise ValueError(f"Length of `asp_vols` {len(asp_vols)} must match `targets` {len(targets)}.")
|
||||||
asp_vols = [asp_vols[0]] * len(targets)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Length of `asp_vols` {len(asp_vols)} must match `targets` {len(targets)}.")
|
|
||||||
if len(dis_vols) != len(targets):
|
if len(dis_vols) != len(targets):
|
||||||
if len(dis_vols) == 1:
|
raise ValueError(f"Length of `dis_vols` {len(dis_vols)} must match `targets` {len(targets)}.")
|
||||||
dis_vols = [dis_vols[0]] * len(targets)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Length of `dis_vols` {len(dis_vols)} must match `targets` {len(targets)}.")
|
|
||||||
if len(sources) != len(targets):
|
if len(sources) != len(targets):
|
||||||
raise ValueError(f"Length of `sources` {len(sources)} must match `targets` {len(targets)}.")
|
raise ValueError(f"Length of `sources` {len(sources)} must match `targets` {len(targets)}.")
|
||||||
|
|
||||||
if len(use_channels) != 1:
|
if len(use_channels) == 1:
|
||||||
for _ in range(len(targets)):
|
for _ in range(len(targets)):
|
||||||
tip = []
|
tip = []
|
||||||
for ___ in range(len(use_channels)):
|
for ___ in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.extend(next(self.current_tip))
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
@@ -1522,7 +1321,6 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
offsets=offsets if offsets else None,
|
offsets=offsets if offsets else None,
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
@@ -1561,7 +1359,6 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
offsets=offsets if offsets else None,
|
offsets=offsets if offsets else None,
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
)
|
||||||
if delays is not None and len(delays) > 1:
|
if delays is not None and len(delays) > 1:
|
||||||
await self.custom_delay(seconds=delays[1])
|
await self.custom_delay(seconds=delays[1])
|
||||||
@@ -1575,7 +1372,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(targets), 8):
|
for i in range(0, len(targets), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.extend(next(self.current_tip))
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
current_targets = targets[i : i + 8]
|
current_targets = targets[i : i + 8]
|
||||||
current_reagent_sources = sources[i : i + 8]
|
current_reagent_sources = sources[i : i + 8]
|
||||||
@@ -1598,7 +1395,6 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
offsets=offsets if offsets else None,
|
offsets=offsets if offsets else None,
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
@@ -1635,7 +1431,6 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
offsets=offsets if offsets else None,
|
offsets=offsets if offsets else None,
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
)
|
||||||
if delays is not None and len(delays) > 1:
|
if delays is not None and len(delays) > 1:
|
||||||
await self.custom_delay(seconds=delays[1])
|
await self.custom_delay(seconds=delays[1])
|
||||||
@@ -1680,7 +1475,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
# 单通道模式:一次吸液,多次分液
|
# 单通道模式:一次吸液,多次分液
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.extend(next(self.current_tip))
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
@@ -1692,7 +1487,6 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
offsets=offsets[idx : idx + 1] if offsets and len(offsets) > idx else None,
|
offsets=offsets[idx : idx + 1] if offsets and len(offsets) > idx else None,
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 从源容器吸液(总体积)
|
# 从源容器吸液(总体积)
|
||||||
@@ -1736,7 +1530,6 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
offsets=offsets[idx : idx + 1] if offsets else None,
|
offsets=offsets[idx : idx + 1] if offsets else None,
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
)
|
||||||
if touch_tip:
|
if touch_tip:
|
||||||
await self.touch_tip([target])
|
await self.touch_tip([target])
|
||||||
@@ -1752,7 +1545,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
for i in range(0, len(targets), 8):
|
for i in range(0, len(targets), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.extend(next(self.current_tip))
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
current_targets = targets[i : i + 8]
|
current_targets = targets[i : i + 8]
|
||||||
@@ -1780,7 +1573,6 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
offsets=offsets[i : i + 8] if offsets else None,
|
offsets=offsets[i : i + 8] if offsets else None,
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 从源容器吸液(8个通道都从同一个源,但每个通道的吸液体积不同)
|
# 从源容器吸液(8个通道都从同一个源,但每个通道的吸液体积不同)
|
||||||
@@ -1826,7 +1618,6 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
offsets=offsets if offsets else None,
|
offsets=offsets if offsets else None,
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if touch_tip:
|
if touch_tip:
|
||||||
@@ -1859,10 +1650,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
"""多对一传输模式:N sources -> 1 target(汇总/混合)"""
|
"""多对一传输模式:N sources -> 1 target(汇总/混合)"""
|
||||||
# 验证和扩展体积参数
|
# 验证和扩展体积参数
|
||||||
if len(asp_vols) != len(sources):
|
if len(asp_vols) != len(sources):
|
||||||
if len(asp_vols) == 1:
|
raise ValueError(f"Length of `asp_vols` {len(asp_vols)} must match `sources` {len(sources)}.")
|
||||||
asp_vols = [asp_vols[0]] * len(sources)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Length of `asp_vols` {len(asp_vols)} must match `sources` {len(sources)}.")
|
|
||||||
|
|
||||||
# 支持两种模式:
|
# 支持两种模式:
|
||||||
# 1. dis_vols 为单个值:所有源汇总,使用总吸液体积或指定分液体积
|
# 1. dis_vols 为单个值:所有源汇总,使用总吸液体积或指定分液体积
|
||||||
@@ -1881,19 +1669,10 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
f"(matching `asp_vols`). Got length {len(dis_vols)}."
|
f"(matching `asp_vols`). Got length {len(dis_vols)}."
|
||||||
)
|
)
|
||||||
|
|
||||||
need_mix_after = mix_stage in ["after", "both"] and mix_times is not None and mix_times > 0
|
|
||||||
defer_final_discard = need_mix_after or touch_tip
|
|
||||||
|
|
||||||
if len(use_channels) == 1:
|
if len(use_channels) == 1:
|
||||||
# 单通道模式:多次吸液,一次分液
|
# 单通道模式:多次吸液,一次分液
|
||||||
|
# 先混合前(如果需要)
|
||||||
# 如果需要 before mix,先 pick up tip 并执行 mix
|
|
||||||
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
tip = []
|
|
||||||
for _ in range(len(use_channels)):
|
|
||||||
tip.extend(self._get_next_tip())
|
|
||||||
await self.pick_up_tips(tip)
|
|
||||||
|
|
||||||
await self.mix(
|
await self.mix(
|
||||||
targets=[target],
|
targets=[target],
|
||||||
mix_time=mix_times,
|
mix_time=mix_times,
|
||||||
@@ -1901,16 +1680,13 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
offsets=offsets[0:1] if offsets else None,
|
offsets=offsets[0:1] if offsets else None,
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.discard_tips(use_channels=use_channels)
|
|
||||||
|
|
||||||
# 从每个源容器吸液并分液到目标容器
|
# 从每个源容器吸液并分液到目标容器
|
||||||
for idx, source in enumerate(sources):
|
for idx, source in enumerate(sources):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.extend(next(self.current_tip))
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
await self.aspirate(
|
await self.aspirate(
|
||||||
@@ -1963,11 +1739,10 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
if delays is not None and len(delays) > 1:
|
if delays is not None and len(delays) > 1:
|
||||||
await self.custom_delay(seconds=delays[1])
|
await self.custom_delay(seconds=delays[1])
|
||||||
|
|
||||||
if not (defer_final_discard and idx == len(sources) - 1):
|
await self.discard_tips(use_channels=use_channels)
|
||||||
await self.discard_tips(use_channels=use_channels)
|
|
||||||
|
|
||||||
# 最后在目标容器中混合(如果需要)
|
# 最后在目标容器中混合(如果需要)
|
||||||
if need_mix_after:
|
if mix_stage in ["after", "both"] and mix_times is not None and mix_times > 0:
|
||||||
await self.mix(
|
await self.mix(
|
||||||
targets=[target],
|
targets=[target],
|
||||||
mix_time=mix_times,
|
mix_time=mix_times,
|
||||||
@@ -1975,15 +1750,11 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
offsets=offsets[0:1] if offsets else None,
|
offsets=offsets[0:1] if offsets else None,
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if touch_tip:
|
if touch_tip:
|
||||||
await self.touch_tip([target])
|
await self.touch_tip([target])
|
||||||
|
|
||||||
if defer_final_discard:
|
|
||||||
await self.discard_tips(use_channels=use_channels)
|
|
||||||
|
|
||||||
elif len(use_channels) == 8:
|
elif len(use_channels) == 8:
|
||||||
# 8通道模式:需要确保源数量是8的倍数
|
# 8通道模式:需要确保源数量是8的倍数
|
||||||
if len(sources) % 8 != 0:
|
if len(sources) % 8 != 0:
|
||||||
@@ -1991,11 +1762,6 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
|
|
||||||
# 每次处理8个源
|
# 每次处理8个源
|
||||||
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
if mix_stage in ["before", "both"] and mix_times is not None and mix_times > 0:
|
||||||
tip = []
|
|
||||||
for _ in range(len(use_channels)):
|
|
||||||
tip.extend(self._get_next_tip())
|
|
||||||
await self.pick_up_tips(tip)
|
|
||||||
|
|
||||||
await self.mix(
|
await self.mix(
|
||||||
targets=[target],
|
targets=[target],
|
||||||
mix_time=mix_times,
|
mix_time=mix_times,
|
||||||
@@ -2003,15 +1769,12 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
offsets=offsets[0:1] if offsets else None,
|
offsets=offsets[0:1] if offsets else None,
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.discard_tips([0,1,2,3,4,5,6,7])
|
|
||||||
|
|
||||||
for i in range(0, len(sources), 8):
|
for i in range(0, len(sources), 8):
|
||||||
tip = []
|
tip = []
|
||||||
for _ in range(len(use_channels)):
|
for _ in range(len(use_channels)):
|
||||||
tip.extend(self._get_next_tip())
|
tip.extend(next(self.current_tip))
|
||||||
await self.pick_up_tips(tip)
|
await self.pick_up_tips(tip)
|
||||||
|
|
||||||
current_sources = sources[i : i + 8]
|
current_sources = sources[i : i + 8]
|
||||||
@@ -2070,11 +1833,10 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
if delays is not None and len(delays) > 1:
|
if delays is not None and len(delays) > 1:
|
||||||
await self.custom_delay(seconds=delays[1])
|
await self.custom_delay(seconds=delays[1])
|
||||||
|
|
||||||
if not (defer_final_discard and i + 8 >= len(sources)):
|
await self.discard_tips([0, 1, 2, 3, 4, 5, 6, 7])
|
||||||
await self.discard_tips([0,1,2,3,4,5,6,7])
|
|
||||||
|
|
||||||
# 最后在目标容器中混合(如果需要)
|
# 最后在目标容器中混合(如果需要)
|
||||||
if need_mix_after:
|
if mix_stage in ["after", "both"] and mix_times is not None and mix_times > 0:
|
||||||
await self.mix(
|
await self.mix(
|
||||||
targets=[target],
|
targets=[target],
|
||||||
mix_time=mix_times,
|
mix_time=mix_times,
|
||||||
@@ -2082,15 +1844,11 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
offsets=offsets[0:1] if offsets else None,
|
offsets=offsets[0:1] if offsets else None,
|
||||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||||
mix_rate=mix_rate if mix_rate else None,
|
mix_rate=mix_rate if mix_rate else None,
|
||||||
use_channels=use_channels,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if touch_tip:
|
if touch_tip:
|
||||||
await self.touch_tip([target])
|
await self.touch_tip([target])
|
||||||
|
|
||||||
if defer_final_discard:
|
|
||||||
await self.discard_tips([0,1,2,3,4,5,6,7])
|
|
||||||
|
|
||||||
# except Exception as e:
|
# except Exception as e:
|
||||||
# traceback.print_exc()
|
# traceback.print_exc()
|
||||||
# raise RuntimeError(f"Liquid addition failed: {e}") from e
|
# raise RuntimeError(f"Liquid addition failed: {e}") from e
|
||||||
@@ -2109,12 +1867,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
print(f"Waiting time: {msg}")
|
print(f"Waiting time: {msg}")
|
||||||
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
||||||
print(f"Time to finish: {time.strftime('%H:%M:%S', time.localtime(time.time() + seconds))}")
|
print(f"Time to finish: {time.strftime('%H:%M:%S', time.localtime(time.time() + seconds))}")
|
||||||
# Use ROS node sleep if available, otherwise use asyncio.sleep
|
await self._ros_node.sleep(seconds)
|
||||||
if hasattr(self, '_ros_node') and self._ros_node is not None:
|
|
||||||
await self._ros_node.sleep(seconds)
|
|
||||||
else:
|
|
||||||
import asyncio
|
|
||||||
await asyncio.sleep(seconds)
|
|
||||||
if msg:
|
if msg:
|
||||||
print(f"Done: {msg}")
|
print(f"Done: {msg}")
|
||||||
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
||||||
@@ -2152,73 +1905,34 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
height_to_bottom: Optional[float] = None,
|
height_to_bottom: Optional[float] = None,
|
||||||
offsets: Optional[Coordinate] = None,
|
offsets: Optional[Coordinate] = None,
|
||||||
mix_rate: Optional[float] = None,
|
mix_rate: Optional[float] = None,
|
||||||
use_channels: Optional[List[int]] = None,
|
|
||||||
none_keys: List[str] = [],
|
none_keys: List[str] = [],
|
||||||
):
|
):
|
||||||
if mix_time is None or mix_time <= 0: # No mixing required
|
if mix_time is None: # No mixing required
|
||||||
return
|
return
|
||||||
"""Mix the liquid in the target wells."""
|
"""Mix the liquid in the target wells."""
|
||||||
if mix_vol is None:
|
|
||||||
raise ValueError("`mix_vol` must be provided when `mix_time` is set.")
|
|
||||||
|
|
||||||
targets_list: List[Container] = list(targets)
|
|
||||||
if len(targets_list) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
def _expand(value, count: int):
|
|
||||||
if value is None:
|
|
||||||
return [None] * count
|
|
||||||
if isinstance(value, (list, tuple)):
|
|
||||||
if len(value) != count:
|
|
||||||
raise ValueError("Length of per-target parameters must match targets.")
|
|
||||||
return list(value)
|
|
||||||
return [value] * count
|
|
||||||
|
|
||||||
offsets_list = _expand(offsets, len(targets_list))
|
|
||||||
heights_list = _expand(height_to_bottom, len(targets_list))
|
|
||||||
rates_list = _expand(mix_rate, len(targets_list))
|
|
||||||
|
|
||||||
for _ in range(mix_time):
|
for _ in range(mix_time):
|
||||||
for idx, target in enumerate(targets_list):
|
await self.aspirate(
|
||||||
offset_arg = (
|
resources=[targets],
|
||||||
[offsets_list[idx]] if offsets_list[idx] is not None else None
|
vols=[mix_vol],
|
||||||
)
|
flow_rates=[mix_rate] if mix_rate else None,
|
||||||
height_arg = (
|
offsets=[offsets] if offsets else None,
|
||||||
[heights_list[idx]] if heights_list[idx] is not None else None
|
liquid_height=[height_to_bottom] if height_to_bottom else None,
|
||||||
)
|
)
|
||||||
rate_arg = [rates_list[idx]] if rates_list[idx] is not None else None
|
await self.custom_delay(seconds=1)
|
||||||
|
await self.dispense(
|
||||||
await self.aspirate(
|
resources=[targets],
|
||||||
resources=[target],
|
vols=[mix_vol],
|
||||||
vols=[mix_vol],
|
flow_rates=[mix_rate] if mix_rate else None,
|
||||||
use_channels=use_channels,
|
offsets=[offsets] if offsets else None,
|
||||||
flow_rates=rate_arg,
|
liquid_height=[height_to_bottom] if height_to_bottom else None,
|
||||||
offsets=offset_arg,
|
)
|
||||||
liquid_height=height_arg,
|
|
||||||
)
|
|
||||||
await self.custom_delay(seconds=1)
|
|
||||||
await self.dispense(
|
|
||||||
resources=[target],
|
|
||||||
vols=[mix_vol],
|
|
||||||
use_channels=use_channels,
|
|
||||||
flow_rates=rate_arg,
|
|
||||||
offsets=offset_arg,
|
|
||||||
liquid_height=height_arg,
|
|
||||||
)
|
|
||||||
|
|
||||||
def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]:
|
def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]:
|
||||||
"""Yield tips from a list of TipRacks one-by-one until depleted."""
|
"""Yield tips from a list of TipRacks one-by-one until depleted."""
|
||||||
for rack in tip_racks:
|
for rack in tip_racks:
|
||||||
for tip in rack:
|
for tip in rack:
|
||||||
yield tip
|
yield tip
|
||||||
# raise RuntimeError("Out of tips!")
|
raise RuntimeError("Out of tips!")
|
||||||
|
|
||||||
def _get_next_tip(self):
|
|
||||||
"""从 current_tip 迭代器获取下一个 tip,耗尽时抛出明确错误而非 StopIteration"""
|
|
||||||
try:
|
|
||||||
return next(self.current_tip)
|
|
||||||
except StopIteration as e:
|
|
||||||
raise RuntimeError("Tip rack exhausted: no more tips available for transfer") from e
|
|
||||||
|
|
||||||
def set_tiprack(self, tip_racks: Sequence[TipRack]):
|
def set_tiprack(self, tip_racks: Sequence[TipRack]):
|
||||||
"""Set the tip racks for the liquid handler."""
|
"""Set the tip racks for the liquid handler."""
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
"""虚拟样品演示设备 — 用于前端 sample tracking 功能的极简 demo"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import random
|
|
||||||
import time
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class VirtualSampleDemo:
|
|
||||||
"""虚拟样品追踪演示设备,提供两种典型返回模式:
|
|
||||||
- measure_samples: 等长输入输出 (前端按 index 自动对齐)
|
|
||||||
- split_and_measure: 输出比输入长,附带 samples 列标注归属
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
|
||||||
if device_id is None and "id" in kwargs:
|
|
||||||
device_id = kwargs.pop("id")
|
|
||||||
if config is None and "config" in kwargs:
|
|
||||||
config = kwargs.pop("config")
|
|
||||||
|
|
||||||
self.device_id = device_id or "unknown_sample_demo"
|
|
||||||
self.config = config or {}
|
|
||||||
self.logger = logging.getLogger(f"VirtualSampleDemo.{self.device_id}")
|
|
||||||
self.data: Dict[str, Any] = {"status": "Idle"}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Action 1: 等长输入输出,无 samples 列
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
async def measure_samples(self, concentrations: List[float]) -> Dict[str, Any]:
|
|
||||||
"""模拟光度测量。absorbance = concentration * 0.05 + noise
|
|
||||||
|
|
||||||
入参和出参 list 长度相等,前端按 index 自动对齐。
|
|
||||||
"""
|
|
||||||
self.logger.info(f"measure_samples: concentrations={concentrations}")
|
|
||||||
absorbance = [round(c * 0.05 + random.gauss(0, 0.005), 4) for c in concentrations]
|
|
||||||
return {"concentrations": concentrations, "absorbance": absorbance}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Action 2: 输出比输入长,带 samples 列
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
async def split_and_measure(self, volumes: List[float], split_count: int = 3) -> Dict[str, Any]:
|
|
||||||
"""将每个样品均分为 split_count 份后逐份测量。
|
|
||||||
|
|
||||||
返回的 list 长度 = len(volumes) * split_count,
|
|
||||||
附带 samples 列标注每行属于第几个输入样品 (0-based index)。
|
|
||||||
"""
|
|
||||||
self.logger.info(f"split_and_measure: volumes={volumes}, split_count={split_count}")
|
|
||||||
out_volumes: List[float] = []
|
|
||||||
readings: List[float] = []
|
|
||||||
samples: List[int] = []
|
|
||||||
|
|
||||||
for idx, vol in enumerate(volumes):
|
|
||||||
split_vol = round(vol / split_count, 2)
|
|
||||||
for _ in range(split_count):
|
|
||||||
out_volumes.append(split_vol)
|
|
||||||
readings.append(round(random.uniform(0.1, 1.0), 4))
|
|
||||||
samples.append(idx)
|
|
||||||
|
|
||||||
return {"volumes": out_volumes, "readings": readings, "unilabos_samples": samples}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Action 3: 入参和出参都带 samples 列(不等长)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
async def analyze_readings(self, readings: List[float], samples: List[int]) -> Dict[str, Any]:
|
|
||||||
"""对 split_and_measure 的输出做二次分析。
|
|
||||||
|
|
||||||
入参 readings/samples 长度相同但 > 原始样品数,
|
|
||||||
出参同样带 samples 列,长度与入参一致。
|
|
||||||
"""
|
|
||||||
self.logger.info(f"analyze_readings: readings={readings}, samples={samples}")
|
|
||||||
scores: List[float] = []
|
|
||||||
passed: List[bool] = []
|
|
||||||
threshold = 0.4
|
|
||||||
|
|
||||||
for r in readings:
|
|
||||||
score = round(r * 100 + random.gauss(0, 2), 2)
|
|
||||||
scores.append(score)
|
|
||||||
passed.append(r >= threshold)
|
|
||||||
|
|
||||||
return {"scores": scores, "passed": passed, "unilabos_samples": samples}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# 状态属性
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
return self.data.get("status", "Idle")
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
# 工作站抽象基类物料系统架构说明
|
# 工作站抽象基类物料系统架构说明
|
||||||
|
|
||||||
## 设计理念
|
|
||||||
|
|
||||||
基于用户需求"请你帮我系统思考一下,工作站抽象基类的物料系统基类该如何构建",我们最终确定了一个**PyLabRobot Deck为中心**的简化架构。
|
|
||||||
|
|
||||||
### 核心原则
|
### 核心原则
|
||||||
|
|
||||||
1. **PyLabRobot为物料管理核心**:使用PyLabRobot的Deck系统作为物料管理的基础,利用其成熟的Resource体系
|
1. **PyLabRobot为物料管理核心**:使用PyLabRobot的Deck系统作为物料管理的基础,利用其成熟的Resource体系
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Bioyond Cell 工作站 - 多订单返回示例
|
||||||
|
|
||||||
|
本文档说明了 `create_orders` 函数如何收集并返回所有订单的完成报文。
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
之前的实现只会等待并返回第一个订单的完成报文,如果有多个订单(例如从 Excel 解析出 3 个订单),只能得到第一个订单的推送信息。
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
修改后的 `create_orders` 函数现在会:
|
||||||
|
|
||||||
|
1. **提取所有 orderCode**:从 LIMS 接口返回的 `data` 列表中提取所有订单编号
|
||||||
|
2. **逐个等待完成**:遍历所有 orderCode,调用 `wait_for_order_finish` 等待每个订单完成
|
||||||
|
3. **收集所有报文**:将每个订单的完成报文存入 `all_reports` 列表
|
||||||
|
4. **统一返回**:返回包含所有订单报文的 JSON 格式数据
|
||||||
|
|
||||||
|
## 返回格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "all_completed",
|
||||||
|
"total_orders": 3,
|
||||||
|
"reports": [
|
||||||
|
{
|
||||||
|
"token": "",
|
||||||
|
"request_time": "2025-12-24T15:32:09.2148671+08:00",
|
||||||
|
"data": {
|
||||||
|
"orderId": "3a1e614d-a082-c44a-60be-68647a35e6f1",
|
||||||
|
"orderCode": "BSO2025122400024",
|
||||||
|
"orderName": "DP20251224001",
|
||||||
|
"status": "30",
|
||||||
|
"workflowStatus": "completed",
|
||||||
|
"usedMaterials": [...]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"token": "",
|
||||||
|
"request_time": "2025-12-24T15:32:09.9999039+08:00",
|
||||||
|
"data": {
|
||||||
|
"orderId": "3a1e614d-a0a2-f7a9-9360-610021c9479d",
|
||||||
|
"orderCode": "BSO2025122400025",
|
||||||
|
"orderName": "DP20251224002",
|
||||||
|
"status": "30",
|
||||||
|
"workflowStatus": "completed",
|
||||||
|
"usedMaterials": [...]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"token": "",
|
||||||
|
"request_time": "2025-12-24T15:34:00.4139986+08:00",
|
||||||
|
"data": {
|
||||||
|
"orderId": "3a1e614d-a0cd-81ca-9f7f-2f4e93af01cd",
|
||||||
|
"orderCode": "BSO2025122400026",
|
||||||
|
"orderName": "DP20251224003",
|
||||||
|
"status": "30",
|
||||||
|
"workflowStatus": "completed",
|
||||||
|
"usedMaterials": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"original_response": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 调用 create_orders
|
||||||
|
result = workstation.create_orders("20251224.xlsx")
|
||||||
|
|
||||||
|
# 访问返回数据
|
||||||
|
print(f"总订单数: {result['total_orders']}")
|
||||||
|
print(f"状态: {result['status']}")
|
||||||
|
|
||||||
|
# 遍历所有订单的报文
|
||||||
|
for i, report in enumerate(result['reports'], 1):
|
||||||
|
order_data = report.get('data', {})
|
||||||
|
print(f"\n订单 {i}:")
|
||||||
|
print(f" orderCode: {order_data.get('orderCode')}")
|
||||||
|
print(f" orderName: {order_data.get('orderName')}")
|
||||||
|
print(f" status: {order_data.get('status')}")
|
||||||
|
print(f" 使用物料数: {len(order_data.get('usedMaterials', []))}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 控制台输出示例
|
||||||
|
|
||||||
|
```
|
||||||
|
[create_orders] 即将提交订单数量: 3
|
||||||
|
[create_orders] 接口返回: {...}
|
||||||
|
[create_orders] 等待 3 个订单完成: ['BSO2025122400024', 'BSO2025122400025', 'BSO2025122400026']
|
||||||
|
[create_orders] 正在等待第 1/3 个订单: BSO2025122400024
|
||||||
|
[create_orders] ✓ 订单 BSO2025122400024 完成
|
||||||
|
[create_orders] 正在等待第 2/3 个订单: BSO2025122400025
|
||||||
|
[create_orders] ✓ 订单 BSO2025122400025 完成
|
||||||
|
[create_orders] 正在等待第 3/3 个订单: BSO2025122400026
|
||||||
|
[create_orders] ✓ 订单 BSO2025122400026 完成
|
||||||
|
[create_orders] 所有订单已完成,共收集 3 个报文
|
||||||
|
实验记录本========================create_orders========================
|
||||||
|
返回报文数量: 3
|
||||||
|
报文 1: orderCode=BSO2025122400024, status=30
|
||||||
|
报文 2: orderCode=BSO2025122400025, status=30
|
||||||
|
报文 3: orderCode=BSO2025122400026, status=30
|
||||||
|
========================
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键改进
|
||||||
|
|
||||||
|
1. ✅ **等待所有订单**:不再只等待第一个订单,而是遍历所有 orderCode
|
||||||
|
2. ✅ **收集完整报文**:每个订单的完整推送报文都被保存在 `reports` 数组中
|
||||||
|
3. ✅ **详细日志**:清晰显示正在等待哪个订单,以及完成情况
|
||||||
|
4. ✅ **错误处理**:即使某个订单失败,也会记录其状态信息
|
||||||
|
5. ✅ **统一格式**:返回的 JSON 格式便于后续处理和分析
|
||||||
Binary file not shown.
@@ -0,0 +1,204 @@
|
|||||||
|
# BioyondCellWorkstation JSON 配置迁移经验总结
|
||||||
|
|
||||||
|
**日期**: 2026-01-13
|
||||||
|
**目的**: 从 `config.py` 迁移到 JSON 配置文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题背景
|
||||||
|
|
||||||
|
原系统通过 `config.py` 管理配置,导致:
|
||||||
|
1. HTTP 服务重复启动(父类 `BioyondWorkstation` 和子类都启动)
|
||||||
|
2. 配置分散在代码中,不便于管理
|
||||||
|
3. 无法通过 JSON 统一配置所有参数
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 解决方案:嵌套配置结构
|
||||||
|
|
||||||
|
### JSON 结构设计
|
||||||
|
|
||||||
|
**正确示例** (嵌套在 `config` 中):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [{
|
||||||
|
"id": "bioyond_cell_workstation",
|
||||||
|
"config": {
|
||||||
|
"deck": {...},
|
||||||
|
"protocol_type": [],
|
||||||
|
"bioyond_config": {
|
||||||
|
"api_host": "http://172.16.11.219:44388",
|
||||||
|
"api_key": "8A819E5C",
|
||||||
|
"timeout": 30,
|
||||||
|
"HTTP_host": "172.16.11.206",
|
||||||
|
"HTTP_port": 8080,
|
||||||
|
"debug_mode": false,
|
||||||
|
"material_type_mappings": {...},
|
||||||
|
"warehouse_mapping": {...},
|
||||||
|
"solid_liquid_mappings": {...}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- ✅ `bioyond_config` 放在 `config` 中(会传递到 `__init__`)
|
||||||
|
- ❌ **不要**放在 `data` 中(`data` 是运行时状态,不会传递)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Python 代码适配
|
||||||
|
|
||||||
|
### 1. 修改 `BioyondCellWorkstation.__init__` 签名
|
||||||
|
|
||||||
|
**文件**: `bioyond_cell_workstation.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def __init__(self, bioyond_config: dict = None, deck=None, protocol_type=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
bioyond_config: 从 JSON 加载的配置字典
|
||||||
|
deck: Deck 配置
|
||||||
|
protocol_type: 协议类型
|
||||||
|
"""
|
||||||
|
# 验证配置
|
||||||
|
if bioyond_config is None:
|
||||||
|
raise ValueError("需要 bioyond_config 参数")
|
||||||
|
|
||||||
|
# 保存配置
|
||||||
|
self.bioyond_config = bioyond_config
|
||||||
|
|
||||||
|
# 设置 HTTP 服务去重标志
|
||||||
|
self.bioyond_config["_disable_auto_http_service"] = True
|
||||||
|
|
||||||
|
# 调用父类
|
||||||
|
super().__init__(bioyond_config=self.bioyond_config, deck=deck, **kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 替换全局变量引用
|
||||||
|
|
||||||
|
**修改前**(使用全局变量):
|
||||||
|
```python
|
||||||
|
from config import MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING
|
||||||
|
|
||||||
|
def create_sample(self, board_type, ...):
|
||||||
|
carrier_type_id = MATERIAL_TYPE_MAPPINGS[board_type][1]
|
||||||
|
location_id = WAREHOUSE_MAPPING[warehouse_name]["site_uuids"][location_code]
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改后**(从配置读取):
|
||||||
|
```python
|
||||||
|
def create_sample(self, board_type, ...):
|
||||||
|
carrier_type_id = self.bioyond_config['material_type_mappings'][board_type][1]
|
||||||
|
location_id = self.bioyond_config['warehouse_mapping'][warehouse_name]["site_uuids"][location_code]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 修复父类配置访问
|
||||||
|
|
||||||
|
在 `station.py` 中安全访问配置默认值:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 修改前(会 KeyError)
|
||||||
|
self._http_service_config = {
|
||||||
|
"host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG["http_service_host"])
|
||||||
|
}
|
||||||
|
|
||||||
|
# 修改后(安全访问)
|
||||||
|
self._http_service_config = {
|
||||||
|
"host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG.get("http_service_host", ""))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见陷阱
|
||||||
|
|
||||||
|
### ❌ 错误1:将配置放在 `data` 字段
|
||||||
|
```json
|
||||||
|
"config": {"deck": {...}},
|
||||||
|
"data": {"bioyond_config": {...}} // ❌ 不会传递到 __init__
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ 错误2:扁平化配置(已废弃方案)
|
||||||
|
虽然扁平化也能工作,但不推荐:
|
||||||
|
```json
|
||||||
|
"config": {
|
||||||
|
"deck": {...},
|
||||||
|
"api_host": "...", // ❌ 不够清晰
|
||||||
|
"api_key": "...",
|
||||||
|
"HTTP_host": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ 错误3:忘记替换全局变量引用
|
||||||
|
代码中直接使用 `MATERIAL_TYPE_MAPPINGS` 等全局变量会导致 `NameError`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 云端同步注意事项
|
||||||
|
|
||||||
|
使用 `--upload_registry` 时,云端配置可能覆盖本地配置:
|
||||||
|
- 首次上传时确保 JSON 完整
|
||||||
|
- 或使用新的 `ak/sk` 避免旧配置干扰
|
||||||
|
- 调试时可暂时移除 `--upload_registry` 参数
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
|
||||||
|
启动成功后应看到:
|
||||||
|
```
|
||||||
|
✅ 从 JSON 配置加载 bioyond_config 成功
|
||||||
|
API Host: http://...
|
||||||
|
HTTP Service: ...
|
||||||
|
✅ BioyondCellWorkstation 初始化完成
|
||||||
|
Loaded ResourceTreeSet with ... nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
运行时不应出现:
|
||||||
|
- ❌ `NameError: name 'MATERIAL_TYPE_MAPPINGS' is not defined`
|
||||||
|
- ❌ `KeyError: 'http_service_host'`
|
||||||
|
- ❌ `bioyond_config 缺少必需参数`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 调试经验
|
||||||
|
|
||||||
|
1. **添加调试日志**查看参数传递链路:
|
||||||
|
- `graphio.py`: JSON 加载后的 config 内容
|
||||||
|
- `initialize_device.py`: `device_config.res_content.config` 的键
|
||||||
|
- `bioyond_cell_workstation.py`: `__init__` 接收到的参数
|
||||||
|
|
||||||
|
2. **config vs data 区别**:
|
||||||
|
- `config`: 初始化参数,传递给 `__init__`
|
||||||
|
- `data`: 运行时状态,不传递给 `__init__`
|
||||||
|
|
||||||
|
3. **参数名必须匹配**:
|
||||||
|
- JSON 中的键名必须与 `__init__` 参数名完全一致
|
||||||
|
|
||||||
|
4. **调试代码清理**:完成后记得删除调试日志(🔍 DEBUG 标记)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 修改文件清单
|
||||||
|
|
||||||
|
| 文件 | 修改内容 |
|
||||||
|
|------|----------|
|
||||||
|
| `yibin_electrolyte_config.json` | 创建嵌套 `config.bioyond_config` 结构 |
|
||||||
|
| `bioyond_cell_workstation.py` | 修改 `__init__` 接收 `bioyond_config`,替换所有全局变量引用 |
|
||||||
|
| `station.py` | 安全访问 `HTTP_SERVICE_CONFIG` 默认值 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考代码位置
|
||||||
|
|
||||||
|
- JSON 配置示例: `yibin_electrolyte_config.json` L12-L353
|
||||||
|
- `__init__` 实现: `bioyond_cell_workstation.py` L39-L94
|
||||||
|
- 全局变量替换示例: `bioyond_cell_workstation.py` L2005, L1863, L1966
|
||||||
|
- HTTP 服务配置: `station.py` L629-L634
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**总结**: 使用嵌套结构将所有配置放在 `config.bioyond_config` 中,修改 `__init__` 直接接收该参数,并替换所有全局变量引用为 `self.bioyond_config` 访问。
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
# BioyondCell 配置迁移修改总结
|
||||||
|
|
||||||
|
**日期**: 2026-01-13
|
||||||
|
**目标**: 从 `config.py` 完全迁移到 JSON 配置,消除所有全局变量依赖
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 修改概览
|
||||||
|
|
||||||
|
本次修改完成了 BioyondCell 模块从 Python 配置文件到 JSON 配置的完整迁移,并清理了所有对 `config.py` 全局变量的依赖。
|
||||||
|
|
||||||
|
### 核心成果
|
||||||
|
|
||||||
|
- ✅ 完全移除对 `config.py` 的导入依赖
|
||||||
|
- ✅ 使用嵌套 JSON 结构 `config.bioyond_config`
|
||||||
|
- ✅ 修复 7 处 `bioyond_cell_workstation.py` 中的全局变量引用
|
||||||
|
- ✅ 修复 3 处其他文件中的全局变量引用
|
||||||
|
- ✅ HTTP 服务去重机制完善
|
||||||
|
- ✅ 系统成功启动并正常运行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 修改文件清单
|
||||||
|
|
||||||
|
### 1. JSON 配置文件
|
||||||
|
|
||||||
|
**文件**: `yibin_electrolyte_config.json`
|
||||||
|
|
||||||
|
**修改**:
|
||||||
|
- 采用嵌套结构将所有配置放在 `config.bioyond_config` 中
|
||||||
|
- 包含:`api_host`, `api_key`, `HTTP_host`, `HTTP_port`, `material_type_mappings`, `warehouse_mapping`, `solid_liquid_mappings` 等
|
||||||
|
|
||||||
|
**示例结构**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [{
|
||||||
|
"id": "bioyond_cell_workstation",
|
||||||
|
"config": {
|
||||||
|
"deck": {...},
|
||||||
|
"protocol_type": [],
|
||||||
|
"bioyond_config": {
|
||||||
|
"api_host": "http://172.16.11.219:44388",
|
||||||
|
"api_key": "8A819E5C",
|
||||||
|
"HTTP_host": "172.16.11.206",
|
||||||
|
"HTTP_port": 8080,
|
||||||
|
"material_type_mappings": {...},
|
||||||
|
"warehouse_mapping": {...},
|
||||||
|
"solid_liquid_mappings": {...}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. bioyond_cell_workstation.py
|
||||||
|
|
||||||
|
**位置**: `unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`
|
||||||
|
|
||||||
|
#### 修改 A: `__init__` 方法签名 (L39-99)
|
||||||
|
|
||||||
|
**修改前**:
|
||||||
|
```python
|
||||||
|
def __init__(self, deck=None, protocol_type=None, **kwargs):
|
||||||
|
# 从 kwargs 收集配置字段
|
||||||
|
self.bioyond_config = {}
|
||||||
|
for field in bioyond_field_names:
|
||||||
|
if field in kwargs:
|
||||||
|
self.bioyond_config[field] = kwargs.pop(field)
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改后**:
|
||||||
|
```python
|
||||||
|
def __init__(self, bioyond_config: dict = None, deck=None, protocol_type=None, **kwargs):
|
||||||
|
"""直接接收 bioyond_config 参数"""
|
||||||
|
if bioyond_config is None:
|
||||||
|
raise ValueError("需要 bioyond_config 参数")
|
||||||
|
|
||||||
|
self.bioyond_config = bioyond_config
|
||||||
|
|
||||||
|
# 设置 HTTP 服务去重标志
|
||||||
|
self.bioyond_config["_disable_auto_http_service"] = True
|
||||||
|
|
||||||
|
super().__init__(bioyond_config=self.bioyond_config, deck=deck, **kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 修改 B: 替换全局变量引用 (7 处)
|
||||||
|
|
||||||
|
| 位置 | 原代码 | 修改后 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| L2005 | `MATERIAL_TYPE_MAPPINGS[board_type][1]` | `self.bioyond_config['material_type_mappings'][board_type][1]` |
|
||||||
|
| L2006 | `MATERIAL_TYPE_MAPPINGS[bottle_type][1]` | `self.bioyond_config['material_type_mappings'][bottle_type][1]` |
|
||||||
|
| L2009 | `WAREHOUSE_MAPPING` | `self.bioyond_config['warehouse_mapping']` |
|
||||||
|
| L2013 | `WAREHOUSE_MAPPING[warehouse_name]` | `self.bioyond_config['warehouse_mapping'][warehouse_name]` |
|
||||||
|
| L2017 | `WAREHOUSE_MAPPING[warehouse_name]["site_uuids"]` | `self.bioyond_config['warehouse_mapping'][warehouse_name]["site_uuids"]` |
|
||||||
|
| L1863 | `SOLID_LIQUID_MAPPINGS.get(material_name)` | `self.bioyond_config.get('solid_liquid_mappings', {}).get(material_name)` |
|
||||||
|
| L1966, L1976 | `MATERIAL_TYPE_MAPPINGS.items()` | `self.bioyond_config['material_type_mappings'].items()` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. station.py
|
||||||
|
|
||||||
|
**位置**: `unilabos/devices/workstation/bioyond_studio/station.py`
|
||||||
|
|
||||||
|
#### 修改 A: 删除 config 导入 (L26-28)
|
||||||
|
|
||||||
|
**修改前**:
|
||||||
|
```python
|
||||||
|
from unilabos.devices.workstation.bioyond_studio.config import (
|
||||||
|
API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING, HTTP_SERVICE_CONFIG
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改后**:
|
||||||
|
```python
|
||||||
|
# 已删除此导入
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 修改 B: `_create_communication_module` 方法 (L691-702)
|
||||||
|
|
||||||
|
**修改前**:
|
||||||
|
```python
|
||||||
|
def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None:
|
||||||
|
default_config = {
|
||||||
|
**API_CONFIG,
|
||||||
|
"workflow_mappings": WORKFLOW_MAPPINGS,
|
||||||
|
"material_type_mappings": MATERIAL_TYPE_MAPPINGS,
|
||||||
|
"warehouse_mapping": WAREHOUSE_MAPPING
|
||||||
|
}
|
||||||
|
if config:
|
||||||
|
self.bioyond_config = {**default_config, **config}
|
||||||
|
else:
|
||||||
|
self.bioyond_config = default_config
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改后**:
|
||||||
|
```python
|
||||||
|
def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None:
|
||||||
|
"""创建Bioyond通信模块"""
|
||||||
|
# 使用传入的 config 参数(来自 bioyond_config)
|
||||||
|
# 不再依赖全局变量 API_CONFIG 等
|
||||||
|
if config:
|
||||||
|
self.bioyond_config = config
|
||||||
|
else:
|
||||||
|
# 如果没有传入配置,创建空配置(用于测试或兼容性)
|
||||||
|
self.bioyond_config = {}
|
||||||
|
|
||||||
|
self.hardware_interface = BioyondV1RPC(self.bioyond_config)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 修改 C: HTTP 服务配置 (L627-632)
|
||||||
|
|
||||||
|
**修改前**:
|
||||||
|
```python
|
||||||
|
self._http_service_config = {
|
||||||
|
"host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG.get("http_service_host", "")),
|
||||||
|
"port": bioyond_config.get("http_service_port", HTTP_SERVICE_CONFIG.get("http_service_port", 0))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改后**:
|
||||||
|
```python
|
||||||
|
self._http_service_config = {
|
||||||
|
"host": bioyond_config.get("http_service_host", bioyond_config.get("HTTP_host", "")),
|
||||||
|
"port": bioyond_config.get("http_service_port", bioyond_config.get("HTTP_port", 0))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. bioyond_rpc.py
|
||||||
|
|
||||||
|
**位置**: `unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py`
|
||||||
|
|
||||||
|
#### 修改 A: 删除 config 导入 (L12)
|
||||||
|
|
||||||
|
**修改前**:
|
||||||
|
```python
|
||||||
|
from unilabos.devices.workstation.bioyond_studio.config import LOCATION_MAPPING
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改后**:
|
||||||
|
```python
|
||||||
|
# 已删除此导入
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 修改 B: `material_outbound` 方法 (L278-280)
|
||||||
|
|
||||||
|
**修改前**:
|
||||||
|
```python
|
||||||
|
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
||||||
|
"""指定库位出库物料(通过库位名称)"""
|
||||||
|
location_id = LOCATION_MAPPING.get(location_name, location_name)
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改后**:
|
||||||
|
```python
|
||||||
|
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
||||||
|
"""指定库位出库物料(通过库位名称)"""
|
||||||
|
# location_name 参数实际上应该直接是 location_id (UUID)
|
||||||
|
location_id = location_name
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**: `LOCATION_MAPPING` 在 `config-0113.py` 中本来就是空字典 `{}`,所以直接使用 `location_name` 逻辑等价。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 关键设计决策
|
||||||
|
|
||||||
|
### 1. 嵌套 vs 扁平配置
|
||||||
|
|
||||||
|
**选择**: 嵌套结构 `config.bioyond_config`
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- ✅ 语义清晰,配置分组明确
|
||||||
|
- ✅ 参数传递直观,直接对应 `__init__` 参数
|
||||||
|
- ✅ 易于维护,不需要硬编码字段列表
|
||||||
|
- ✅ 符合 UniLab 设计模式
|
||||||
|
|
||||||
|
### 2. HTTP 服务去重
|
||||||
|
|
||||||
|
**实现**: 子类设置 `_disable_auto_http_service` 标志
|
||||||
|
|
||||||
|
```python
|
||||||
|
# bioyond_cell_workstation.py
|
||||||
|
self.bioyond_config["_disable_auto_http_service"] = True
|
||||||
|
|
||||||
|
# station.py (post_init)
|
||||||
|
if self.bioyond_config.get("_disable_auto_http_service"):
|
||||||
|
logger.info("子类已自行管理HTTP服务,跳过自动启动")
|
||||||
|
return
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 全局变量替换策略
|
||||||
|
|
||||||
|
**原则**: 所有配置从 `self.bioyond_config` 获取
|
||||||
|
|
||||||
|
**模式**:
|
||||||
|
```python
|
||||||
|
# 修改前
|
||||||
|
from config import MATERIAL_TYPE_MAPPINGS
|
||||||
|
carrier_type_id = MATERIAL_TYPE_MAPPINGS[board_type][1]
|
||||||
|
|
||||||
|
# 修改后
|
||||||
|
carrier_type_id = self.bioyond_config['material_type_mappings'][board_type][1]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 验证结果
|
||||||
|
|
||||||
|
### 启动成功日志
|
||||||
|
```
|
||||||
|
✅ 从 JSON 配置加载 bioyond_config 成功
|
||||||
|
API Host: http://172.16.11.219:44388
|
||||||
|
HTTP Service: 172.16.11.206:8080
|
||||||
|
🔧 已设置 _disable_auto_http_service 标志,防止 HTTP 服务重复启动
|
||||||
|
✅ BioyondCellWorkstation 初始化完成
|
||||||
|
Loaded ResourceTreeSet with 1 trees, 1785 total nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
### 功能验证
|
||||||
|
- ✅ 订单创建 (`create_orders_v2`)
|
||||||
|
- ✅ 质量比计算
|
||||||
|
- ✅ 物料转移 (`transfer_3_to_2_to_1`)
|
||||||
|
- ✅ HTTP 报送接收 (step_finish, sample_finish, order_finish)
|
||||||
|
- ✅ 等待机制 (`wait_for_order_finish`)
|
||||||
|
- ✅ 仓库 UUID 映射
|
||||||
|
- ✅ 物料类型映射
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 相关文档
|
||||||
|
|
||||||
|
- **配置迁移经验**: `2026-01-13_JSON配置迁移经验.md`
|
||||||
|
- **任务清单**: `C:\Users\AndyXie\.gemini\antigravity\brain\...\task.md`
|
||||||
|
- **实施计划**: `C:\Users\AndyXie\.gemini\antigravity\brain\...\implementation_plan.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
### 其他工作站模块
|
||||||
|
|
||||||
|
以下文件仍在使用 `config.py` 全局变量(未包含在本次修改中):
|
||||||
|
- `reaction_station.py` - 使用 `API_CONFIG`
|
||||||
|
- `experiment.py` - 使用 `API_CONFIG`, `WORKFLOW_MAPPINGS`, `MATERIAL_TYPE_MAPPINGS`
|
||||||
|
- `dispensing_station.py` - 使用 `API_CONFIG`, `WAREHOUSE_MAPPING`
|
||||||
|
- `station.py` L176, L177, L529, L530 - 动态导入 `WAREHOUSE_MAPPING`
|
||||||
|
|
||||||
|
**建议**: 后续可以统一迁移这些模块到 JSON 配置。
|
||||||
|
|
||||||
|
### config.py 文件
|
||||||
|
|
||||||
|
`config.py` 文件已恢复但**不再被 bioyond_cell 使用**。可以:
|
||||||
|
- 保留作为其他模块的参考
|
||||||
|
- 或者完全删除(如果其他模块也迁移完成)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 下一步建议
|
||||||
|
|
||||||
|
1. **清理调试代码** ✅ (已完成)
|
||||||
|
2. **提交代码到 Git**
|
||||||
|
3. **迁移其他工作站模块** (可选)
|
||||||
|
4. **更新文档和启动脚本**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**修改完成日期**: 2026-01-13
|
||||||
|
**系统状态**: ✅ 稳定运行
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,157 @@
|
|||||||
|
# 批量出库 Excel 模板使用说明
|
||||||
|
|
||||||
|
**文件**: `outbound_template.xlsx`
|
||||||
|
**用途**: 配合 `auto_batch_outbound_from_xlsx()` 方法进行批量出库操作
|
||||||
|
**API 端点**: `/api/lims/storage/auto-batch-out-bound`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Excel 列说明
|
||||||
|
|
||||||
|
| 列名 | 说明 | 示例 | 必填 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `locationId` | **库位 ID(UUID)** | `3a19da43-57b5-294f-d663-154a1cc32270` | ✅ 是 |
|
||||||
|
| `warehouseId` | **仓库 ID 或名称** | `配液站内试剂仓库` | ✅ 是 |
|
||||||
|
| `quantity` | **出库数量** | `1.0`, `2.0` | ✅ 是 |
|
||||||
|
| `x` | **X 坐标(库位横向位置)** | `1`, `2`, `3` | ✅ 是 |
|
||||||
|
| `y` | **Y 坐标(库位纵向位置)** | `1`, `2`, `3` | ✅ 是 |
|
||||||
|
| `z` | **Z 坐标(库位层数/高度)** | `1`, `2`, `3` | ✅ 是 |
|
||||||
|
| `备注说明` | 可选备注信息 | `配液站内试剂仓库-A01` | ❌ 否 |
|
||||||
|
|
||||||
|
### 📐 坐标说明
|
||||||
|
|
||||||
|
**x, y, z** 是库位在仓库内的**三维坐标**:
|
||||||
|
|
||||||
|
```
|
||||||
|
仓库(例如 WH4)
|
||||||
|
├── Z=1(第1层/加样头面)
|
||||||
|
│ ├── X=1, Y=1(位置 A)
|
||||||
|
│ ├── X=2, Y=1(位置 B)
|
||||||
|
│ ├── X=3, Y=1(位置 C)
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
└── Z=2(第2层/原液瓶面)
|
||||||
|
├── X=1, Y=1(位置 A)
|
||||||
|
├── X=2, Y=1(位置 B)
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
- **warehouseId**: 指定哪个仓库(WH3, WH4, 配液站等)
|
||||||
|
- **x, y, z**: 在该仓库内的三维坐标
|
||||||
|
- **locationId**: 该坐标位置的唯一 UUID
|
||||||
|
|
||||||
|
### 🎯 起点与终点
|
||||||
|
|
||||||
|
**重要说明**:批量出库模板**只规定了出库的"起点"**(从哪里取物料),**没有指定"终点"**(放到哪里)。
|
||||||
|
|
||||||
|
```
|
||||||
|
出库流程:
|
||||||
|
起点(Excel 指定) → ?终点(LIMS/工作流决定)
|
||||||
|
↓
|
||||||
|
locationId, x, y, z → 由 LIMS 系统或当前工作流自动分配
|
||||||
|
```
|
||||||
|
|
||||||
|
**终点由以下方式确定:**
|
||||||
|
- **LIMS 系统自动分配**:根据当前任务自动规划目标位置
|
||||||
|
- **工作流预定义**:在创建出库任务时已绑定目标位置
|
||||||
|
- **暂存区**:默认放到出库暂存区,等待下一步操作
|
||||||
|
|
||||||
|
💡 **对比**:上料操作(`auto_feeding4to3`)则有 `targetWH` 参数可以指定目标仓库
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 如何获取 UUID?
|
||||||
|
|
||||||
|
### 方法 1:从配置文件获取
|
||||||
|
|
||||||
|
参考 `yibin_electrolyte_config.json` 中的 `warehouse_mapping`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"warehouse_mapping": {
|
||||||
|
"配液站内试剂仓库": {
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a19da43-57b5-294f-d663-154a1cc32270",
|
||||||
|
"B01": "3a19da43-57b5-7394-5f49-54efe2c9bef2",
|
||||||
|
"C01": "3a19da43-57b5-5e75-552f-8dbd0ad1075f"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"手动堆栈": {
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea",
|
||||||
|
"A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 2:通过 API 查询
|
||||||
|
|
||||||
|
```python
|
||||||
|
material_info = hardware_interface.material_id_query(workflow_id)
|
||||||
|
locations = material_info.get("locations", [])
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 填写示例
|
||||||
|
|
||||||
|
### 示例 1:从配液站内试剂仓库出库
|
||||||
|
|
||||||
|
| locationId | warehouseId | quantity | x | y | z | 备注说明 |
|
||||||
|
|------------|-------------|----------|---|---|---|----------|
|
||||||
|
| `3a19da43-57b5-294f-d663-154a1cc32270` | 配液站内试剂仓库 | 1 | 1 | 1 | 1 | A01 位置 |
|
||||||
|
| `3a19da43-57b5-7394-5f49-54efe2c9bef2` | 配液站内试剂仓库 | 2 | 2 | 1 | 1 | B01 位置 |
|
||||||
|
|
||||||
|
### 示例 2:从手动堆栈出库
|
||||||
|
|
||||||
|
| locationId | warehouseId | quantity | x | y | z | 备注说明 |
|
||||||
|
|------------|-------------|----------|---|---|---|----------|
|
||||||
|
| `3a19deae-2c7a-36f5-5e41-02c5b66feaea` | 手动堆栈 | 1 | 1 | 1 | 1 | A01 |
|
||||||
|
| `3a19deae-2c7a-dc6d-c41e-ef285d946cfe` | 手动堆栈 | 1 | 1 | 2 | 1 | A02 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 使用方法
|
||||||
|
|
||||||
|
```python
|
||||||
|
from bioyond_cell_workstation import BioyondCellWorkstation
|
||||||
|
|
||||||
|
# 初始化工作站
|
||||||
|
workstation = BioyondCellWorkstation(config=config, deck=deck)
|
||||||
|
|
||||||
|
# 调用批量出库方法
|
||||||
|
result = workstation.auto_batch_outbound_from_xlsx(
|
||||||
|
xlsx_path="outbound_template.xlsx"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
1. **locationId 必须是有效的 UUID**,不能使用库位名称
|
||||||
|
2. **x, y, z 坐标必须与 locationId 对应**,表示该库位在仓库内的位置
|
||||||
|
3. **quantity 必须是数字**,可以是整数或浮点数
|
||||||
|
4. Excel 文件必须包含表头行
|
||||||
|
5. 空行会被自动跳过
|
||||||
|
6. 确保 UUID 与实际库位对应,否则 API 会报错
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 相关文件
|
||||||
|
|
||||||
|
- **配置文件**: `yibin_electrolyte_config.json`
|
||||||
|
- **Python 代码**: `bioyond_cell_workstation.py` (L630-695)
|
||||||
|
- **生成脚本**: `create_outbound_template.py`
|
||||||
|
- **上料模板**: `material_template.xlsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 重新生成模板
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda activate newunilab
|
||||||
|
python create_outbound_template.py
|
||||||
|
```
|
||||||
@@ -9,7 +9,7 @@ from datetime import datetime, timezone
|
|||||||
from unilabos.device_comms.rpc import BaseRequest
|
from unilabos.device_comms.rpc import BaseRequest
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
import json
|
import json
|
||||||
from unilabos.devices.workstation.bioyond_studio.config import LOCATION_MAPPING
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleLogger:
|
class SimpleLogger:
|
||||||
@@ -49,6 +49,14 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.api_key = config["api_key"]
|
self.api_key = config["api_key"]
|
||||||
self.host = config["api_host"]
|
self.host = config["api_host"]
|
||||||
|
|
||||||
|
# 初始化 location_mapping
|
||||||
|
# 直接从 warehouse_mapping 构建,确保数据源所谓的单一和结构化
|
||||||
|
self.location_mapping = {}
|
||||||
|
warehouse_mapping = self.config.get("warehouse_mapping", {})
|
||||||
|
for warehouse_name, warehouse_config in warehouse_mapping.items():
|
||||||
|
if "site_uuids" in warehouse_config:
|
||||||
|
self.location_mapping.update(warehouse_config["site_uuids"])
|
||||||
self._logger = SimpleLogger()
|
self._logger = SimpleLogger()
|
||||||
self.material_cache = {}
|
self.material_cache = {}
|
||||||
self._load_material_cache()
|
self._load_material_cache()
|
||||||
@@ -176,7 +184,40 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
print(f"add material data: {response['data']}")
|
print(f"add material data: {response['data']}")
|
||||||
return response.get("data", {})
|
|
||||||
|
# 自动更新缓存
|
||||||
|
data = response.get("data", {})
|
||||||
|
if data:
|
||||||
|
if isinstance(data, str):
|
||||||
|
# 如果返回的是字符串,通常是ID
|
||||||
|
mat_id = data
|
||||||
|
name = params.get("name")
|
||||||
|
else:
|
||||||
|
# 如果返回的是字典,尝试获取name和id
|
||||||
|
name = data.get("name") or params.get("name")
|
||||||
|
mat_id = data.get("id")
|
||||||
|
|
||||||
|
if name and mat_id:
|
||||||
|
self.material_cache[name] = mat_id
|
||||||
|
print(f"已自动更新缓存: {name} -> {mat_id}")
|
||||||
|
|
||||||
|
# 处理返回数据中的 details (如果有)
|
||||||
|
# 有些 API 返回结构可能直接包含 details,或者在 data 字段中
|
||||||
|
details = data.get("details", []) if isinstance(data, dict) else []
|
||||||
|
if not details and isinstance(data, dict):
|
||||||
|
details = data.get("detail", [])
|
||||||
|
|
||||||
|
if details:
|
||||||
|
for detail in details:
|
||||||
|
d_name = detail.get("name")
|
||||||
|
# 尝试从不同字段获取 ID
|
||||||
|
d_id = detail.get("id") or detail.get("detailMaterialId")
|
||||||
|
|
||||||
|
if d_name and d_id:
|
||||||
|
self.material_cache[d_name] = d_id
|
||||||
|
print(f"已自动更新 detail 缓存: {d_name} -> {d_id}")
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def query_matial_type_id(self, data) -> list:
|
def query_matial_type_id(self, data) -> list:
|
||||||
"""查找物料typeid"""
|
"""查找物料typeid"""
|
||||||
@@ -203,7 +244,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
params={
|
params={
|
||||||
"apiKey": self.api_key,
|
"apiKey": self.api_key,
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
"data": {},
|
"data": 0,
|
||||||
})
|
})
|
||||||
if not response or response['code'] != 1:
|
if not response or response['code'] != 1:
|
||||||
return []
|
return []
|
||||||
@@ -273,11 +314,19 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
|
|
||||||
if not response or response['code'] != 1:
|
if not response or response['code'] != 1:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
# 自动更新缓存 - 移除被删除的物料
|
||||||
|
for name, mid in list(self.material_cache.items()):
|
||||||
|
if mid == material_id:
|
||||||
|
del self.material_cache[name]
|
||||||
|
print(f"已从缓存移除物料: {name}")
|
||||||
|
break
|
||||||
|
|
||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
||||||
"""指定库位出库物料(通过库位名称)"""
|
"""指定库位出库物料(通过库位名称)"""
|
||||||
location_id = LOCATION_MAPPING.get(location_name, location_name)
|
location_id = self.location_mapping.get(location_name, location_name)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"materialId": material_id,
|
"materialId": material_id,
|
||||||
@@ -1103,6 +1152,10 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
for detail_material in detail_materials:
|
for detail_material in detail_materials:
|
||||||
detail_name = detail_material.get("name")
|
detail_name = detail_material.get("name")
|
||||||
detail_id = detail_material.get("detailMaterialId")
|
detail_id = detail_material.get("detailMaterialId")
|
||||||
|
if not detail_id:
|
||||||
|
# 尝试其他可能的字段
|
||||||
|
detail_id = detail_material.get("id")
|
||||||
|
|
||||||
if detail_name and detail_id:
|
if detail_name and detail_id:
|
||||||
self.material_cache[detail_name] = detail_id
|
self.material_cache[detail_name] = detail_id
|
||||||
print(f"加载detail材料: {detail_name} -> ID: {detail_id}")
|
print(f"加载detail材料: {detail_name} -> ID: {detail_id}")
|
||||||
@@ -1123,6 +1176,14 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
print(f"从缓存找到材料: {material_name_or_id} -> ID: {material_id}")
|
print(f"从缓存找到材料: {material_name_or_id} -> ID: {material_id}")
|
||||||
return material_id
|
return material_id
|
||||||
|
|
||||||
|
# 如果缓存中没有,尝试刷新缓存
|
||||||
|
print(f"缓存中未找到材料 '{material_name_or_id}',尝试刷新缓存...")
|
||||||
|
self.refresh_material_cache()
|
||||||
|
if material_name_or_id in self.material_cache:
|
||||||
|
material_id = self.material_cache[material_name_or_id]
|
||||||
|
print(f"刷新缓存后找到材料: {material_name_or_id} -> ID: {material_id}")
|
||||||
|
return material_id
|
||||||
|
|
||||||
print(f"警告: 未在缓存中找到材料名称 '{material_name_or_id}',将使用原值")
|
print(f"警告: 未在缓存中找到材料名称 '{material_name_or_id}',将使用原值")
|
||||||
return material_name_or_id
|
return material_name_or_id
|
||||||
|
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
# config.py
|
|
||||||
"""
|
|
||||||
配置文件 - 包含所有配置信息和映射关系
|
|
||||||
"""
|
|
||||||
|
|
||||||
# API配置
|
|
||||||
API_CONFIG = {
|
|
||||||
"api_key": "",
|
|
||||||
"api_host": ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# 工作流映射配置
|
|
||||||
WORKFLOW_MAPPINGS = {
|
|
||||||
"reactor_taken_out": "",
|
|
||||||
"reactor_taken_in": "",
|
|
||||||
"Solid_feeding_vials": "",
|
|
||||||
"Liquid_feeding_vials(non-titration)": "",
|
|
||||||
"Liquid_feeding_solvents": "",
|
|
||||||
"Liquid_feeding(titration)": "",
|
|
||||||
"liquid_feeding_beaker": "",
|
|
||||||
"Drip_back": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 工作流名称到DisplaySectionName的映射
|
|
||||||
WORKFLOW_TO_SECTION_MAP = {
|
|
||||||
'reactor_taken_in': '反应器放入',
|
|
||||||
'liquid_feeding_beaker': '液体投料-烧杯',
|
|
||||||
'Liquid_feeding_vials(non-titration)': '液体投料-小瓶(非滴定)',
|
|
||||||
'Liquid_feeding_solvents': '液体投料-溶剂',
|
|
||||||
'Solid_feeding_vials': '固体投料-小瓶',
|
|
||||||
'Liquid_feeding(titration)': '液体投料-滴定',
|
|
||||||
'reactor_taken_out': '反应器取出'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 库位映射配置
|
|
||||||
WAREHOUSE_MAPPING = {
|
|
||||||
"粉末堆栈": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
# 样品板
|
|
||||||
"A1": "3a14198e-6929-31f0-8a22-0f98f72260df",
|
|
||||||
"A2": "3a14198e-6929-4379-affa-9a2935c17f99",
|
|
||||||
"A3": "3a14198e-6929-56da-9a1c-7f5fbd4ae8af",
|
|
||||||
"A4": "3a14198e-6929-5e99-2b79-80720f7cfb54",
|
|
||||||
"B1": "3a14198e-6929-f525-9a1b-1857552b28ee",
|
|
||||||
"B2": "3a14198e-6929-bf98-0fd5-26e1d68bf62d",
|
|
||||||
"B3": "3a14198e-6929-2d86-a468-602175a2b5aa",
|
|
||||||
"B4": "3a14198e-6929-1a98-ae57-e97660c489ad",
|
|
||||||
# 分装板
|
|
||||||
"C1": "3a14198e-6929-46fe-841e-03dd753f1e4a",
|
|
||||||
"C2": "3a14198e-6929-1bc9-a9bd-3b7ca66e7f95",
|
|
||||||
"C3": "3a14198e-6929-72ac-32ce-9b50245682b8",
|
|
||||||
"C4": "3a14198e-6929-3bd8-e6c7-4a9fd93be118",
|
|
||||||
"D1": "3a14198e-6929-8a0b-b686-6f4a2955c4e2",
|
|
||||||
"D2": "3a14198e-6929-dde1-fc78-34a84b71afdf",
|
|
||||||
"D3": "3a14198e-6929-a0ec-5f15-c0f9f339f963",
|
|
||||||
"D4": "3a14198e-6929-7ac8-915a-fea51cb2e884"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"溶液堆栈": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A1": "3a14198e-d724-e036-afdc-2ae39a7f3383",
|
|
||||||
"A2": "3a14198e-d724-afa4-fc82-0ac8a9016791",
|
|
||||||
"A3": "3a14198e-d724-ca48-bb9e-7e85751e55b6",
|
|
||||||
"A4": "3a14198e-d724-df6d-5e32-5483b3cab583",
|
|
||||||
"B1": "3a14198e-d724-d818-6d4f-5725191a24b5",
|
|
||||||
"B2": "3a14198e-d724-be8a-5e0b-012675e195c6",
|
|
||||||
"B3": "3a14198e-d724-cc1e-5c2c-228a130f40a8",
|
|
||||||
"B4": "3a14198e-d724-1e28-c885-574c3df468d0",
|
|
||||||
"C1": "3a14198e-d724-b5bb-adf3-4c5a0da6fb31",
|
|
||||||
"C2": "3a14198e-d724-ab4e-48cb-817c3c146707",
|
|
||||||
"C3": "3a14198e-d724-7f18-1853-39d0c62e1d33",
|
|
||||||
"C4": "3a14198e-d724-28a2-a760-baa896f46b66",
|
|
||||||
"D1": "3a14198e-d724-d378-d266-2508a224a19f",
|
|
||||||
"D2": "3a14198e-d724-f56e-468b-0110a8feb36a",
|
|
||||||
"D3": "3a14198e-d724-0cf1-dea9-a1f40fe7e13c",
|
|
||||||
"D4": "3a14198e-d724-0ddd-9654-f9352a421de9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"试剂堆栈": {
|
|
||||||
"uuid": "",
|
|
||||||
"site_uuids": {
|
|
||||||
"A1": "3a14198c-c2cf-8b40-af28-b467808f1c36",
|
|
||||||
"A2": "3a14198c-c2d0-f3e7-871a-e470d144296f",
|
|
||||||
"A3": "3a14198c-c2d0-dc7d-b8d0-e1d88cee3094",
|
|
||||||
"A4": "3a14198c-c2d0-2070-efc8-44e245f10c6f",
|
|
||||||
"B1": "3a14198c-c2d0-354f-39ad-642e1a72fcb8",
|
|
||||||
"B2": "3a14198c-c2d0-1559-105d-0ea30682cab4",
|
|
||||||
"B3": "3a14198c-c2d0-725e-523d-34c037ac2440",
|
|
||||||
"B4": "3a14198c-c2d0-efce-0939-69ca5a7dfd39"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 物料类型配置
|
|
||||||
MATERIAL_TYPE_MAPPINGS = {
|
|
||||||
"烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
|
|
||||||
"试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
|
|
||||||
"样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
|
|
||||||
"分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
|
|
||||||
"样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
|
|
||||||
"90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
|
|
||||||
"10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
|
|
||||||
}
|
|
||||||
|
|
||||||
# 步骤参数配置(各工作流的步骤UUID)
|
|
||||||
WORKFLOW_STEP_IDS = {
|
|
||||||
"reactor_taken_in": {
|
|
||||||
"config": ""
|
|
||||||
},
|
|
||||||
"liquid_feeding_beaker": {
|
|
||||||
"liquid": "",
|
|
||||||
"observe": ""
|
|
||||||
},
|
|
||||||
"liquid_feeding_vials_non_titration": {
|
|
||||||
"liquid": "",
|
|
||||||
"observe": ""
|
|
||||||
},
|
|
||||||
"liquid_feeding_solvents": {
|
|
||||||
"liquid": "",
|
|
||||||
"observe": ""
|
|
||||||
},
|
|
||||||
"solid_feeding_vials": {
|
|
||||||
"feeding": "",
|
|
||||||
"observe": ""
|
|
||||||
},
|
|
||||||
"liquid_feeding_titration": {
|
|
||||||
"liquid": "",
|
|
||||||
"observe": ""
|
|
||||||
},
|
|
||||||
"drip_back": {
|
|
||||||
"liquid": "",
|
|
||||||
"observe": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOCATION_MAPPING = {}
|
|
||||||
|
|
||||||
ACTION_NAMES = {}
|
|
||||||
|
|
||||||
HTTP_SERVICE_CONFIG = {}
|
|
||||||
329
unilabos/devices/workstation/bioyond_studio/config.py.deprecated
Normal file
329
unilabos/devices/workstation/bioyond_studio/config.py.deprecated
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
# config.py
|
||||||
|
"""
|
||||||
|
Bioyond工作站配置文件
|
||||||
|
包含API配置、工作流映射、物料类型映射、仓库库位映射等所有配置信息
|
||||||
|
"""
|
||||||
|
|
||||||
|
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 基础配置
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# API配置
|
||||||
|
API_CONFIG = {
|
||||||
|
"api_key": "DE9BDDA0",
|
||||||
|
"api_host": "http://192.168.1.200:44402"
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP 报送服务配置
|
||||||
|
HTTP_SERVICE_CONFIG = {
|
||||||
|
"http_service_host": "127.0.0.1", # 监听地址
|
||||||
|
"http_service_port": 8080, # 监听端口
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deck配置 - 反应站工作台配置
|
||||||
|
DECK_CONFIG = BIOYOND_PolymerReactionStation_Deck(setup=True)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 工作流配置
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 工作流ID映射
|
||||||
|
WORKFLOW_MAPPINGS = {
|
||||||
|
"reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1",
|
||||||
|
"reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6",
|
||||||
|
"Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6",
|
||||||
|
"Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47",
|
||||||
|
"Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046",
|
||||||
|
"Liquid_feeding(titration)": "3a16082a-96ac-0449-446a-4ed39f3365b6",
|
||||||
|
"liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
|
||||||
|
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 工作流名称到显示名称的映射
|
||||||
|
WORKFLOW_TO_SECTION_MAP = {
|
||||||
|
'reactor_taken_in': '反应器放入',
|
||||||
|
'reactor_taken_out': '反应器取出',
|
||||||
|
'Solid_feeding_vials': '固体投料-小瓶',
|
||||||
|
'Liquid_feeding_vials(non-titration)': '液体投料-小瓶(非滴定)',
|
||||||
|
'Liquid_feeding_solvents': '液体投料-溶剂',
|
||||||
|
'Liquid_feeding(titration)': '液体投料-滴定',
|
||||||
|
'liquid_feeding_beaker': '液体投料-烧杯',
|
||||||
|
'Drip_back': '液体回滴'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 工作流步骤ID配置
|
||||||
|
WORKFLOW_STEP_IDS = {
|
||||||
|
"reactor_taken_in": {
|
||||||
|
"config": "60a06f85-c5b3-29eb-180f-4f62dd7e2154"
|
||||||
|
},
|
||||||
|
"liquid_feeding_beaker": {
|
||||||
|
"liquid": "6808cda7-fee7-4092-97f0-5f9c2ffa60e3",
|
||||||
|
"observe": "1753c0de-dffc-4ee6-8458-805a2e227362"
|
||||||
|
},
|
||||||
|
"liquid_feeding_vials_non_titration": {
|
||||||
|
"liquid": "62ea6e95-3d5d-43db-bc1e-9a1802673861",
|
||||||
|
"observe": "3a167d99-6172-b67b-5f22-a7892197142e"
|
||||||
|
},
|
||||||
|
"liquid_feeding_solvents": {
|
||||||
|
"liquid": "1fcea355-2545-462b-b727-350b69a313bf",
|
||||||
|
"observe": "0553dfb3-9ac5-4ace-8e00-2f11029919a8"
|
||||||
|
},
|
||||||
|
"solid_feeding_vials": {
|
||||||
|
"feeding": "f7ae7448-4f20-4c1d-8096-df6fbadd787a",
|
||||||
|
"observe": "263c7ed5-7277-426b-bdff-d6fbf77bcc05"
|
||||||
|
},
|
||||||
|
"liquid_feeding_titration": {
|
||||||
|
"liquid": "a00ec41b-e666-4422-9c20-bfcd3cd15c54",
|
||||||
|
"observe": "ac738ff6-4c58-4155-87b1-d6f65a2c9ab5"
|
||||||
|
},
|
||||||
|
"drip_back": {
|
||||||
|
"liquid": "371be86a-ab77-4769-83e5-54580547c48a",
|
||||||
|
"observe": "ce024b9d-bd20-47b8-9f78-ca5ce7f44cf1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 工作流动作名称配置
|
||||||
|
ACTION_NAMES = {
|
||||||
|
"reactor_taken_in": {
|
||||||
|
"config": "通量-配置",
|
||||||
|
"stirring": "反应模块-开始搅拌"
|
||||||
|
},
|
||||||
|
"solid_feeding_vials": {
|
||||||
|
"feeding": "粉末加样模块-投料",
|
||||||
|
"observe": "反应模块-观察搅拌结果"
|
||||||
|
},
|
||||||
|
"liquid_feeding_vials_non_titration": {
|
||||||
|
"liquid": "稀释液瓶加液位-液体投料",
|
||||||
|
"observe": "反应模块-滴定结果观察"
|
||||||
|
},
|
||||||
|
"liquid_feeding_solvents": {
|
||||||
|
"liquid": "试剂AB放置位-试剂吸液分液",
|
||||||
|
"observe": "反应模块-观察搅拌结果"
|
||||||
|
},
|
||||||
|
"liquid_feeding_titration": {
|
||||||
|
"liquid": "稀释液瓶加液位-稀释液吸液分液",
|
||||||
|
"observe": "反应模块-滴定结果观察"
|
||||||
|
},
|
||||||
|
"liquid_feeding_beaker": {
|
||||||
|
"liquid": "烧杯溶液放置位-烧杯吸液分液",
|
||||||
|
"observe": "反应模块-观察搅拌结果"
|
||||||
|
},
|
||||||
|
"drip_back": {
|
||||||
|
"liquid": "试剂AB放置位-试剂吸液分液",
|
||||||
|
"observe": "反应模块-向下滴定结果观察"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 仓库配置
|
||||||
|
# ============================================================================
|
||||||
|
# 说明:
|
||||||
|
# - 出库和入库操作都需要UUID
|
||||||
|
WAREHOUSE_MAPPING = {
|
||||||
|
# ========== 反应站仓库 ==========
|
||||||
|
|
||||||
|
# 堆栈1左 - 反应站左侧堆栈 (4行×4列=16个库位, A01~D04)
|
||||||
|
"堆栈1左": {
|
||||||
|
"uuid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a14aa17-0d49-11d7-a6e1-f236b3e5e5a3",
|
||||||
|
"A02": "3a14aa17-0d49-4bc5-8836-517b75473f5f",
|
||||||
|
"A03": "3a14aa17-0d49-c2bc-6222-5cee8d2d94f8",
|
||||||
|
"A04": "3a14aa17-0d49-3ce2-8e9a-008c38d116fb",
|
||||||
|
"B01": "3a14aa17-0d49-f49c-6b66-b27f185a3b32",
|
||||||
|
"B02": "3a14aa17-0d49-cf46-df85-a979c9c9920c",
|
||||||
|
"B03": "3a14aa17-0d49-7698-4a23-f7ffb7d48ba3",
|
||||||
|
"B04": "3a14aa17-0d49-1231-99be-d5870e6478e9",
|
||||||
|
"C01": "3a14aa17-0d49-be34-6fae-4aed9d48b70b",
|
||||||
|
"C02": "3a14aa17-0d49-11d7-0897-34921dcf6b7c",
|
||||||
|
"C03": "3a14aa17-0d49-9840-0bd5-9c63c1bb2c29",
|
||||||
|
"C04": "3a14aa17-0d49-8335-3bff-01da69ea4911",
|
||||||
|
"D01": "3a14aa17-0d49-2bea-c8e5-2b32094935d5",
|
||||||
|
"D02": "3a14aa17-0d49-cff4-e9e8-5f5f0bc1ef32",
|
||||||
|
"D03": "3a14aa17-0d49-4948-cb0a-78f30d1ca9b8",
|
||||||
|
"D04": "3a14aa17-0d49-fd2f-9dfb-a29b11e84099",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
# 堆栈1右 - 反应站右侧堆栈 (4行×4列=16个库位, A05~D08)
|
||||||
|
"堆栈1右": {
|
||||||
|
"uuid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||||
|
"site_uuids": {
|
||||||
|
"A05": "3a14aa17-0d49-2c61-edc8-72a8ca7192dd",
|
||||||
|
"A06": "3a14aa17-0d49-60c8-2b00-40b17198f397",
|
||||||
|
"A07": "3a14aa17-0d49-ec5b-0b75-634dce8eed25",
|
||||||
|
"A08": "3a14aa17-0d49-3ec9-55b3-f3189c4ec53d",
|
||||||
|
"B05": "3a14aa17-0d49-6a4e-abcf-4c113eaaeaad",
|
||||||
|
"B06": "3a14aa17-0d49-e3f6-2dd6-28c2e8194fbe",
|
||||||
|
"B07": "3a14aa17-0d49-11a6-b861-ee895121bf52",
|
||||||
|
"B08": "3a14aa17-0d49-9c7d-1145-d554a6e482f0",
|
||||||
|
"C05": "3a14aa17-0d49-45c4-7a34-5105bc3e2368",
|
||||||
|
"C06": "3a14aa17-0d49-867e-39ab-31b3fe9014be",
|
||||||
|
"C07": "3a14aa17-0d49-ec56-c4b4-39fd9b2131e7",
|
||||||
|
"C08": "3a14aa17-0d49-1128-d7d9-ffb1231c98c0",
|
||||||
|
"D05": "3a14aa17-0d49-e843-f961-ea173326a14b",
|
||||||
|
"D06": "3a14aa17-0d49-4d26-a985-f188359c4f8b",
|
||||||
|
"D07": "3a14aa17-0d49-223a-b520-bc092bb42fe0",
|
||||||
|
"D08": "3a14aa17-0d49-4fa3-401a-6a444e1cca22",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
# 站内试剂存放堆栈
|
||||||
|
"站内试剂存放堆栈": {
|
||||||
|
"uuid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a14aa3b-9fab-adac-7b9c-e1ee446b51d5",
|
||||||
|
"A02": "3a14aa3b-9fab-ca72-febc-b7c304476c78"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
# 测量小瓶仓库(测密度)
|
||||||
|
"测量小瓶仓库": {
|
||||||
|
"uuid": "3a15012f-705b-c0de-3f9e-950c205f9921",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a15012f-705e-0524-3161-c523b5aebc97",
|
||||||
|
"A02": "3a15012f-705e-7cd1-32ab-ad4fd1ab75c8",
|
||||||
|
"A03": "3a15012f-705e-a5d6-edac-bdbfec236260",
|
||||||
|
"B01": "3a15012f-705e-e0ee-80e0-10a6b3fc500d",
|
||||||
|
"B02": "3a15012f-705e-e499-180d-de06d60d0b21",
|
||||||
|
"B03": "3a15012f-705e-eff6-63f1-09f742096b26"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
# 站内Tip盒堆栈 - 用于存放枪头盒 (耗材)
|
||||||
|
"站内Tip盒堆栈": {
|
||||||
|
"uuid": "3a14aa3a-2d3c-b5c1-9ddf-7c4a957d459a",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a14aa3a-2d3d-e700-411a-0ddf85e1f18a",
|
||||||
|
"A02": "3a14aa3a-2d3d-a7ce-099a-d5632fdafa24",
|
||||||
|
"A03": "3a14aa3a-2d3d-bdf6-a702-c60b38b08501",
|
||||||
|
"B01": "3a14aa3a-2d3d-d704-f076-2a8d5bc72cb8",
|
||||||
|
"B02": "3a14aa3a-2d3d-c350-2526-0778d173a5ac",
|
||||||
|
"B03": "3a14aa3a-2d3d-bc38-b356-f0de2e44e0c7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
# ========== 配液站仓库 ==========
|
||||||
|
"粉末堆栈": {
|
||||||
|
"uuid": "3a14198e-6928-121f-7ca6-88ad3ae7e6a0",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a14198e-6929-31f0-8a22-0f98f72260df",
|
||||||
|
"A02": "3a14198e-6929-4379-affa-9a2935c17f99",
|
||||||
|
"A03": "3a14198e-6929-56da-9a1c-7f5fbd4ae8af",
|
||||||
|
"A04": "3a14198e-6929-5e99-2b79-80720f7cfb54",
|
||||||
|
"B01": "3a14198e-6929-f525-9a1b-1857552b28ee",
|
||||||
|
"B02": "3a14198e-6929-bf98-0fd5-26e1d68bf62d",
|
||||||
|
"B03": "3a14198e-6929-2d86-a468-602175a2b5aa",
|
||||||
|
"B04": "3a14198e-6929-1a98-ae57-e97660c489ad",
|
||||||
|
"C01": "3a14198e-6929-46fe-841e-03dd753f1e4a",
|
||||||
|
"C02": "3a14198e-6929-72ac-32ce-9b50245682b8",
|
||||||
|
"C03": "3a14198e-6929-8a0b-b686-6f4a2955c4e2",
|
||||||
|
"C04": "3a14198e-6929-a0ec-5f15-c0f9f339f963",
|
||||||
|
"D01": "3a14198e-6929-1bc9-a9bd-3b7ca66e7f95",
|
||||||
|
"D02": "3a14198e-6929-3bd8-e6c7-4a9fd93be118",
|
||||||
|
"D03": "3a14198e-6929-dde1-fc78-34a84b71afdf",
|
||||||
|
"D04": "3a14198e-6929-7ac8-915a-fea51cb2e884"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"溶液堆栈": {
|
||||||
|
"uuid": "3a14198e-d723-2c13-7d12-50143e190a23",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a14198e-d724-e036-afdc-2ae39a7f3383",
|
||||||
|
"A02": "3a14198e-d724-d818-6d4f-5725191a24b5",
|
||||||
|
"A03": "3a14198e-d724-b5bb-adf3-4c5a0da6fb31",
|
||||||
|
"A04": "3a14198e-d724-d378-d266-2508a224a19f",
|
||||||
|
"B01": "3a14198e-d724-afa4-fc82-0ac8a9016791",
|
||||||
|
"B02": "3a14198e-d724-be8a-5e0b-012675e195c6",
|
||||||
|
"B03": "3a14198e-d724-ab4e-48cb-817c3c146707",
|
||||||
|
"B04": "3a14198e-d724-f56e-468b-0110a8feb36a",
|
||||||
|
"C01": "3a14198e-d724-ca48-bb9e-7e85751e55b6",
|
||||||
|
"C02": "3a14198e-d724-cc1e-5c2c-228a130f40a8",
|
||||||
|
"C03": "3a14198e-d724-7f18-1853-39d0c62e1d33",
|
||||||
|
"C04": "3a14198e-d724-0cf1-dea9-a1f40fe7e13c",
|
||||||
|
"D01": "3a14198e-d724-df6d-5e32-5483b3cab583",
|
||||||
|
"D02": "3a14198e-d724-1e28-c885-574c3df468d0",
|
||||||
|
"D03": "3a14198e-d724-28a2-a760-baa896f46b66",
|
||||||
|
"D04": "3a14198e-d724-0ddd-9654-f9352a421de9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"试剂堆栈": {
|
||||||
|
"uuid": "3a14198c-c2cc-0290-e086-44a428fba248",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a14198c-c2cf-8b40-af28-b467808f1c36", # x=1, y=1, code=0001-0001
|
||||||
|
"A02": "3a14198c-c2d0-dc7d-b8d0-e1d88cee3094", # x=1, y=2, code=0001-0002
|
||||||
|
"A03": "3a14198c-c2d0-354f-39ad-642e1a72fcb8", # x=1, y=3, code=0001-0003
|
||||||
|
"A04": "3a14198c-c2d0-725e-523d-34c037ac2440", # x=1, y=4, code=0001-0004
|
||||||
|
"B01": "3a14198c-c2d0-f3e7-871a-e470d144296f", # x=2, y=1, code=0001-0005
|
||||||
|
"B02": "3a14198c-c2d0-2070-efc8-44e245f10c6f", # x=2, y=2, code=0001-0006
|
||||||
|
"B03": "3a14198c-c2d0-1559-105d-0ea30682cab4", # x=2, y=3, code=0001-0007
|
||||||
|
"B04": "3a14198c-c2d0-efce-0939-69ca5a7dfd39" # x=2, y=4, code=0001-0008
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 物料类型配置
|
||||||
|
# ============================================================================
|
||||||
|
# 说明:
|
||||||
|
# - 格式: PyLabRobot资源类型名称 → Bioyond系统typeId的UUID
|
||||||
|
# - 这个映射基于 resource.model 属性 (不是显示名称!)
|
||||||
|
# - UUID为空表示该类型暂未在Bioyond系统中定义
|
||||||
|
MATERIAL_TYPE_MAPPINGS = {
|
||||||
|
# ================================================配液站资源============================================================
|
||||||
|
# ==================================================样品===============================================================
|
||||||
|
"BIOYOND_PolymerStation_1FlaskCarrier": ("烧杯", "3a14196b-24f2-ca49-9081-0cab8021bf1a"), # 配液站-样品-烧杯
|
||||||
|
"BIOYOND_PolymerStation_1BottleCarrier": ("试剂瓶", "3a14196b-8bcf-a460-4f74-23f21ca79e72"), # 配液站-样品-试剂瓶
|
||||||
|
"BIOYOND_PolymerStation_6StockCarrier": ("分装板", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"), # 配液站-样品-分装板
|
||||||
|
"BIOYOND_PolymerStation_Liquid_Vial": ("10%分装小瓶", "3a14196c-76be-2279-4e22-7310d69aed68"), # 配液站-样品-分装板-第一排小瓶
|
||||||
|
"BIOYOND_PolymerStation_Solid_Vial": ("90%分装小瓶", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"), # 配液站-样品-分装板-第二排小瓶
|
||||||
|
# ==================================================试剂===============================================================
|
||||||
|
"BIOYOND_PolymerStation_8StockCarrier": ("样品板", "3a14196e-b7a0-a5da-1931-35f3000281e9"), # 配液站-试剂-样品板(8孔)
|
||||||
|
"BIOYOND_PolymerStation_Solid_Stock": ("样品瓶", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"), # 配液站-试剂-样品板-样品瓶
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 动态生成的库位UUID映射(从WAREHOUSE_MAPPING中提取)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
LOCATION_MAPPING = {}
|
||||||
|
for warehouse_name, warehouse_config in WAREHOUSE_MAPPING.items():
|
||||||
|
if "site_uuids" in warehouse_config:
|
||||||
|
LOCATION_MAPPING.update(warehouse_config["site_uuids"])
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 物料默认参数配置
|
||||||
|
# ============================================================================
|
||||||
|
# 说明:
|
||||||
|
# - 为特定物料名称自动添加默认参数(如密度、分子量、单位等)
|
||||||
|
# - 格式: 物料名称 → {参数字典}
|
||||||
|
# - 在创建或更新物料时,会自动合并这些参数到 Parameters 字段
|
||||||
|
# - unit: 物料的计量单位(会用于 unit 字段)
|
||||||
|
# - density/densityUnit: 密度信息(会添加到 Parameters 中)
|
||||||
|
|
||||||
|
MATERIAL_DEFAULT_PARAMETERS = {
|
||||||
|
# 溶剂类
|
||||||
|
"NMP": {
|
||||||
|
"unit": "毫升",
|
||||||
|
"density": "1.03",
|
||||||
|
"densityUnit": "g/mL",
|
||||||
|
"description": "N-甲基吡咯烷酮 (N-Methyl-2-pyrrolidone)"
|
||||||
|
},
|
||||||
|
# 可以继续添加其他物料...
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 物料类型默认参数配置
|
||||||
|
# ============================================================================
|
||||||
|
# 说明:
|
||||||
|
# - 为特定物料类型(UUID)自动添加默认参数
|
||||||
|
# - 格式: Bioyond类型UUID → {参数字典}
|
||||||
|
# - 优先级低于按名称匹配的配置
|
||||||
|
MATERIAL_TYPE_PARAMETERS = {
|
||||||
|
# 示例:
|
||||||
|
# "3a14196b-24f2-ca49-9081-0cab8021bf1a": { # 烧杯
|
||||||
|
# "unit": "个"
|
||||||
|
# }
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import time
|
|||||||
from typing import Optional, Dict, Any, List
|
from typing import Optional, Dict, Any, List
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
import requests
|
import requests
|
||||||
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
|
import pint
|
||||||
|
|
||||||
|
|
||||||
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException
|
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException
|
||||||
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
|
||||||
@@ -25,13 +26,89 @@ class ComputeExperimentDesignReturn(TypedDict):
|
|||||||
class BioyondDispensingStation(BioyondWorkstation):
|
class BioyondDispensingStation(BioyondWorkstation):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config,
|
config: dict = None,
|
||||||
# 桌子
|
deck=None,
|
||||||
deck,
|
protocol_type=None,
|
||||||
*args,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(config, deck, *args, **kwargs)
|
"""初始化配液站
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: 配置字典,应包含material_type_mappings等配置
|
||||||
|
deck: Deck对象
|
||||||
|
protocol_type: 协议类型(由ROS系统传递,此处忽略)
|
||||||
|
**kwargs: 其他可能的参数
|
||||||
|
"""
|
||||||
|
if config is None:
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
# 将 kwargs 合并到 config 中 (处理扁平化配置如 api_key)
|
||||||
|
config.update(kwargs)
|
||||||
|
|
||||||
|
if deck is None and config:
|
||||||
|
deck = config.get('deck')
|
||||||
|
|
||||||
|
# 🔧 修复: 确保 Deck 上的 warehouses 具有正确的 UUID (必须在 super().__init__ 之前执行,因为父类会触发同步)
|
||||||
|
# 从配置中读取 warehouse_mapping,并应用到实际的 deck 资源上
|
||||||
|
if config and "warehouse_mapping" in config and deck:
|
||||||
|
warehouse_mapping = config["warehouse_mapping"]
|
||||||
|
print(f"正在根据配置更新 Deck warehouse UUIDs... (共有 {len(warehouse_mapping)} 个配置)")
|
||||||
|
|
||||||
|
user_deck = deck
|
||||||
|
# 初始化 warehouses 字典
|
||||||
|
if not hasattr(user_deck, "warehouses") or user_deck.warehouses is None:
|
||||||
|
user_deck.warehouses = {}
|
||||||
|
|
||||||
|
# 1. 尝试从 children 中查找匹配的资源
|
||||||
|
for child in user_deck.children:
|
||||||
|
# 简单判断: 如果名字在 mapping 中,就认为是 warehouse
|
||||||
|
if child.name in warehouse_mapping:
|
||||||
|
user_deck.warehouses[child.name] = child
|
||||||
|
print(f" - 从子资源中找到 warehouse: {child.name}")
|
||||||
|
|
||||||
|
# 2. 如果还是没找到,且 Deck 类有 setup 方法,尝试调用 setup (针对 Deck 对象正确但未初始化的情况)
|
||||||
|
if not user_deck.warehouses and hasattr(user_deck, "setup"):
|
||||||
|
print(" - 尝试调用 deck.setup() 初始化仓库...")
|
||||||
|
try:
|
||||||
|
user_deck.setup()
|
||||||
|
# setup 后重新检查
|
||||||
|
if hasattr(user_deck, "warehouses") and user_deck.warehouses:
|
||||||
|
print(f" - setup() 成功,找到 {len(user_deck.warehouses)} 个仓库")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" - 调用 setup() 失败: {e}")
|
||||||
|
|
||||||
|
# 3. 如果仍然为空,可能需要手动创建 (仅针对特定已知的 Deck 类型进行补救,这里暂时只打印警告)
|
||||||
|
if not user_deck.warehouses:
|
||||||
|
print(" - ⚠️ 仍然无法找到任何 warehouse 资源!")
|
||||||
|
|
||||||
|
for wh_name, wh_config in warehouse_mapping.items():
|
||||||
|
target_uuid = wh_config.get("uuid")
|
||||||
|
|
||||||
|
# 尝试在 deck.warehouses 中查找
|
||||||
|
wh_resource = None
|
||||||
|
if hasattr(user_deck, "warehouses") and wh_name in user_deck.warehouses:
|
||||||
|
wh_resource = user_deck.warehouses[wh_name]
|
||||||
|
|
||||||
|
# 如果没找到,尝试在所有子资源中查找
|
||||||
|
if not wh_resource:
|
||||||
|
wh_resource = user_deck.get_resource(wh_name)
|
||||||
|
|
||||||
|
if wh_resource:
|
||||||
|
if target_uuid:
|
||||||
|
current_uuid = getattr(wh_resource, "uuid", None)
|
||||||
|
print(f"✅ 更新仓库 '{wh_name}' UUID: {current_uuid} -> {target_uuid}")
|
||||||
|
|
||||||
|
# 动态添加 uuid 属性
|
||||||
|
wh_resource.uuid = target_uuid
|
||||||
|
# 同时也确保 category 正确,避免 graphio 识别错误
|
||||||
|
# wh_resource.category = "warehouse"
|
||||||
|
else:
|
||||||
|
print(f"⚠️ 仓库 '{wh_name}' 在配置中没有 UUID")
|
||||||
|
else:
|
||||||
|
print(f"❌ 在 Deck 中未找到配置的仓库: '{wh_name}'")
|
||||||
|
|
||||||
|
super().__init__(bioyond_config=config, deck=deck)
|
||||||
|
|
||||||
# self.config = config
|
# self.config = config
|
||||||
# self.api_key = config["api_key"]
|
# self.api_key = config["api_key"]
|
||||||
# self.host = config["api_host"]
|
# self.host = config["api_host"]
|
||||||
@@ -43,6 +120,41 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
# 用于跟踪任务完成状态的字典: {orderCode: {status, order_id, timestamp}}
|
# 用于跟踪任务完成状态的字典: {orderCode: {status, order_id, timestamp}}
|
||||||
self.order_completion_status = {}
|
self.order_completion_status = {}
|
||||||
|
|
||||||
|
# 初始化 pint 单位注册表
|
||||||
|
self.ureg = pint.UnitRegistry()
|
||||||
|
|
||||||
|
# 化合物信息
|
||||||
|
self.compound_info = {
|
||||||
|
"MolWt": {
|
||||||
|
"MDA": 108.14 * self.ureg.g / self.ureg.mol,
|
||||||
|
"TDA": 122.16 * self.ureg.g / self.ureg.mol,
|
||||||
|
"PAPP": 521.62 * self.ureg.g / self.ureg.mol,
|
||||||
|
"BTDA": 322.23 * self.ureg.g / self.ureg.mol,
|
||||||
|
"BPDA": 294.22 * self.ureg.g / self.ureg.mol,
|
||||||
|
"6FAP": 366.26 * self.ureg.g / self.ureg.mol,
|
||||||
|
"PMDA": 218.12 * self.ureg.g / self.ureg.mol,
|
||||||
|
"MPDA": 108.14 * self.ureg.g / self.ureg.mol,
|
||||||
|
"SIDA": 248.51 * self.ureg.g / self.ureg.mol,
|
||||||
|
"ODA": 200.236 * self.ureg.g / self.ureg.mol,
|
||||||
|
"4,4'-ODA": 200.236 * self.ureg.g / self.ureg.mol,
|
||||||
|
"134": 292.34 * self.ureg.g / self.ureg.mol,
|
||||||
|
},
|
||||||
|
"FuncGroup": {
|
||||||
|
"MDA": "Amine",
|
||||||
|
"TDA": "Amine",
|
||||||
|
"PAPP": "Amine",
|
||||||
|
"BTDA": "Anhydride",
|
||||||
|
"BPDA": "Anhydride",
|
||||||
|
"6FAP": "Amine",
|
||||||
|
"MPDA": "Amine",
|
||||||
|
"SIDA": "Amine",
|
||||||
|
"PMDA": "Anhydride",
|
||||||
|
"ODA": "Amine",
|
||||||
|
"4,4'-ODA": "Amine",
|
||||||
|
"134": "Amine",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
|
def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
|
||||||
"""项目接口通用POST调用
|
"""项目接口通用POST调用
|
||||||
|
|
||||||
@@ -54,7 +166,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
dict: 服务端响应,失败时返回 {code:0,message,...}
|
dict: 服务端响应,失败时返回 {code:0,message,...}
|
||||||
"""
|
"""
|
||||||
request_data = {
|
request_data = {
|
||||||
"apiKey": API_CONFIG["api_key"],
|
"apiKey": self.bioyond_config["api_key"],
|
||||||
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
||||||
"data": data
|
"data": data
|
||||||
}
|
}
|
||||||
@@ -85,7 +197,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
dict: 服务端响应,失败时返回 {code:0,message,...}
|
dict: 服务端响应,失败时返回 {code:0,message,...}
|
||||||
"""
|
"""
|
||||||
request_data = {
|
request_data = {
|
||||||
"apiKey": API_CONFIG["api_key"],
|
"apiKey": self.bioyond_config["api_key"],
|
||||||
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
||||||
"data": data
|
"data": data
|
||||||
}
|
}
|
||||||
@@ -118,20 +230,22 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
ratio = json.loads(ratio)
|
ratio = json.loads(ratio)
|
||||||
except Exception:
|
except Exception:
|
||||||
ratio = {}
|
ratio = {}
|
||||||
root = str(Path(__file__).resolve().parents[3])
|
|
||||||
if root not in sys.path:
|
|
||||||
sys.path.append(root)
|
|
||||||
try:
|
|
||||||
mod = importlib.import_module("tem.compute")
|
|
||||||
except Exception as e:
|
|
||||||
raise BioyondException(f"无法导入计算模块: {e}")
|
|
||||||
try:
|
try:
|
||||||
wp = float(wt_percent) if isinstance(wt_percent, str) else wt_percent
|
wp = float(wt_percent) if isinstance(wt_percent, str) else wt_percent
|
||||||
mt = float(m_tot) if isinstance(m_tot, str) else m_tot
|
mt = float(m_tot) if isinstance(m_tot, str) else m_tot
|
||||||
tp = float(titration_percent) if isinstance(titration_percent, str) else titration_percent
|
tp = float(titration_percent) if isinstance(titration_percent, str) else titration_percent
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BioyondException(f"参数解析失败: {e}")
|
raise BioyondException(f"参数解析失败: {e}")
|
||||||
res = mod.generate_experiment_design(ratio=ratio, wt_percent=wp, m_tot=mt, titration_percent=tp)
|
|
||||||
|
# 2. 调用内部计算方法
|
||||||
|
res = self._generate_experiment_design(
|
||||||
|
ratio=ratio,
|
||||||
|
wt_percent=wp,
|
||||||
|
m_tot=mt,
|
||||||
|
titration_percent=tp
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. 构造返回结果
|
||||||
out = {
|
out = {
|
||||||
"solutions": res.get("solutions", []),
|
"solutions": res.get("solutions", []),
|
||||||
"titration": res.get("titration", {}),
|
"titration": res.get("titration", {}),
|
||||||
@@ -140,11 +254,248 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
"return_info": json.dumps(res, ensure_ascii=False)
|
"return_info": json.dumps(res, ensure_ascii=False)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|
||||||
except BioyondException:
|
except BioyondException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BioyondException(str(e))
|
raise BioyondException(str(e))
|
||||||
|
|
||||||
|
def _generate_experiment_design(
|
||||||
|
self,
|
||||||
|
ratio: dict,
|
||||||
|
wt_percent: float = 0.25,
|
||||||
|
m_tot: float = 70,
|
||||||
|
titration_percent: float = 0.03,
|
||||||
|
) -> dict:
|
||||||
|
"""内部方法:生成实验设计
|
||||||
|
|
||||||
|
根据FuncGroup自动区分二胺和二酐,每种二胺单独配溶液,严格按照ratio顺序投料。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
ratio: 化合物配比字典,格式: {"compound_name": ratio_value}
|
||||||
|
wt_percent: 固体重量百分比
|
||||||
|
m_tot: 反应混合物总质量(g)
|
||||||
|
titration_percent: 滴定溶液百分比
|
||||||
|
|
||||||
|
返回:
|
||||||
|
包含实验设计详细参数的字典
|
||||||
|
"""
|
||||||
|
# 溶剂密度
|
||||||
|
ρ_solvent = 1.03 * self.ureg.g / self.ureg.ml
|
||||||
|
# 二酐溶解度
|
||||||
|
solubility = 0.02 * self.ureg.g / self.ureg.ml
|
||||||
|
# 投入固体时最小溶剂体积
|
||||||
|
V_min = 30 * self.ureg.ml
|
||||||
|
m_tot = m_tot * self.ureg.g
|
||||||
|
|
||||||
|
# 保持ratio中的顺序
|
||||||
|
compound_names = list(ratio.keys())
|
||||||
|
compound_ratios = list(ratio.values())
|
||||||
|
|
||||||
|
# 验证所有化合物是否在 compound_info 中定义
|
||||||
|
undefined_compounds = [name for name in compound_names if name not in self.compound_info["MolWt"]]
|
||||||
|
if undefined_compounds:
|
||||||
|
available = list(self.compound_info["MolWt"].keys())
|
||||||
|
raise ValueError(
|
||||||
|
f"以下化合物未在 compound_info 中定义: {undefined_compounds}。"
|
||||||
|
f"可用的化合物: {available}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取各化合物的分子量和官能团类型
|
||||||
|
molecular_weights = [self.compound_info["MolWt"][name] for name in compound_names]
|
||||||
|
func_groups = [self.compound_info["FuncGroup"][name] for name in compound_names]
|
||||||
|
|
||||||
|
# 记录化合物信息用于调试
|
||||||
|
self.hardware_interface._logger.info(f"化合物名称: {compound_names}")
|
||||||
|
self.hardware_interface._logger.info(f"官能团类型: {func_groups}")
|
||||||
|
|
||||||
|
# 按原始顺序分离二胺和二酐
|
||||||
|
ordered_compounds = list(zip(compound_names, compound_ratios, molecular_weights, func_groups))
|
||||||
|
diamine_compounds = [(name, ratio_val, mw, i) for i, (name, ratio_val, mw, fg) in enumerate(ordered_compounds) if fg == "Amine"]
|
||||||
|
anhydride_compounds = [(name, ratio_val, mw, i) for i, (name, ratio_val, mw, fg) in enumerate(ordered_compounds) if fg == "Anhydride"]
|
||||||
|
|
||||||
|
if not diamine_compounds or not anhydride_compounds:
|
||||||
|
raise ValueError(
|
||||||
|
f"需要同时包含二胺(Amine)和二酐(Anhydride)化合物。"
|
||||||
|
f"当前二胺: {[c[0] for c in diamine_compounds]}, "
|
||||||
|
f"当前二酐: {[c[0] for c in anhydride_compounds]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 计算加权平均分子量 (基于摩尔比)
|
||||||
|
total_molar_ratio = sum(compound_ratios)
|
||||||
|
weighted_molecular_weight = sum(ratio_val * mw for ratio_val, mw in zip(compound_ratios, molecular_weights))
|
||||||
|
|
||||||
|
# 取最后一个二酐用于滴定
|
||||||
|
titration_anhydride = anhydride_compounds[-1]
|
||||||
|
solid_anhydrides = anhydride_compounds[:-1] if len(anhydride_compounds) > 1 else []
|
||||||
|
|
||||||
|
# 二胺溶液配制参数 - 每种二胺单独配制
|
||||||
|
diamine_solutions = []
|
||||||
|
total_diamine_volume = 0 * self.ureg.ml
|
||||||
|
|
||||||
|
# 计算反应物的总摩尔量
|
||||||
|
n_reactant = m_tot * wt_percent / weighted_molecular_weight
|
||||||
|
|
||||||
|
for name, ratio_val, mw, order_index in diamine_compounds:
|
||||||
|
# 跳过 SIDA
|
||||||
|
if name == "SIDA":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 计算该二胺需要的摩尔数
|
||||||
|
n_diamine_needed = n_reactant * ratio_val
|
||||||
|
|
||||||
|
# 二胺溶液配制参数 (每种二胺固定配制参数)
|
||||||
|
m_diamine_solid = 5.0 * self.ureg.g # 每种二胺固体质量
|
||||||
|
V_solvent_for_this = 20 * self.ureg.ml # 每种二胺溶剂体积
|
||||||
|
m_solvent_for_this = ρ_solvent * V_solvent_for_this
|
||||||
|
|
||||||
|
# 计算该二胺溶液的浓度
|
||||||
|
c_diamine = (m_diamine_solid / mw) / V_solvent_for_this
|
||||||
|
|
||||||
|
# 计算需要移取的溶液体积
|
||||||
|
V_diamine_needed = n_diamine_needed / c_diamine
|
||||||
|
|
||||||
|
diamine_solutions.append({
|
||||||
|
"name": name,
|
||||||
|
"order": order_index,
|
||||||
|
"solid_mass": m_diamine_solid.magnitude,
|
||||||
|
"solvent_volume": V_solvent_for_this.magnitude,
|
||||||
|
"concentration": c_diamine.magnitude,
|
||||||
|
"volume_needed": V_diamine_needed.magnitude,
|
||||||
|
"molar_ratio": ratio_val
|
||||||
|
})
|
||||||
|
|
||||||
|
total_diamine_volume += V_diamine_needed
|
||||||
|
|
||||||
|
# 按原始顺序排序
|
||||||
|
diamine_solutions.sort(key=lambda x: x["order"])
|
||||||
|
|
||||||
|
# 计算滴定二酐的质量
|
||||||
|
titration_name, titration_ratio, titration_mw, _ = titration_anhydride
|
||||||
|
m_titration_anhydride = n_reactant * titration_ratio * titration_mw
|
||||||
|
m_titration_90 = m_titration_anhydride * (1 - titration_percent)
|
||||||
|
m_titration_10 = m_titration_anhydride * titration_percent
|
||||||
|
|
||||||
|
# 计算其他固体二酐的质量 (按顺序)
|
||||||
|
solid_anhydride_masses = []
|
||||||
|
for name, ratio_val, mw, order_index in solid_anhydrides:
|
||||||
|
mass = n_reactant * ratio_val * mw
|
||||||
|
solid_anhydride_masses.append({
|
||||||
|
"name": name,
|
||||||
|
"order": order_index,
|
||||||
|
"mass": mass.magnitude,
|
||||||
|
"molar_ratio": ratio_val
|
||||||
|
})
|
||||||
|
|
||||||
|
# 按原始顺序排序
|
||||||
|
solid_anhydride_masses.sort(key=lambda x: x["order"])
|
||||||
|
|
||||||
|
# 计算溶剂用量
|
||||||
|
total_diamine_solution_mass = sum(
|
||||||
|
sol["volume_needed"] * ρ_solvent for sol in diamine_solutions
|
||||||
|
) * self.ureg.ml
|
||||||
|
|
||||||
|
# 预估滴定溶剂量、计算补加溶剂量
|
||||||
|
m_solvent_titration = m_titration_10 / solubility * ρ_solvent
|
||||||
|
m_solvent_add = m_tot * (1 - wt_percent) - total_diamine_solution_mass - m_solvent_titration
|
||||||
|
|
||||||
|
# 检查最小溶剂体积要求
|
||||||
|
total_liquid_volume = (total_diamine_solution_mass + m_solvent_add) / ρ_solvent
|
||||||
|
m_tot_min = V_min / total_liquid_volume * m_tot
|
||||||
|
|
||||||
|
# 如果需要,按比例放大
|
||||||
|
scale_factor = 1.0
|
||||||
|
if m_tot_min > m_tot:
|
||||||
|
scale_factor = (m_tot_min / m_tot).magnitude
|
||||||
|
m_titration_90 *= scale_factor
|
||||||
|
m_titration_10 *= scale_factor
|
||||||
|
m_solvent_add *= scale_factor
|
||||||
|
m_solvent_titration *= scale_factor
|
||||||
|
|
||||||
|
# 更新二胺溶液用量
|
||||||
|
for sol in diamine_solutions:
|
||||||
|
sol["volume_needed"] *= scale_factor
|
||||||
|
|
||||||
|
# 更新固体二酐用量
|
||||||
|
for anhydride in solid_anhydride_masses:
|
||||||
|
anhydride["mass"] *= scale_factor
|
||||||
|
|
||||||
|
m_tot = m_tot_min
|
||||||
|
|
||||||
|
# 生成投料顺序
|
||||||
|
feeding_order = []
|
||||||
|
|
||||||
|
# 1. 固体二酐 (按顺序)
|
||||||
|
for anhydride in solid_anhydride_masses:
|
||||||
|
feeding_order.append({
|
||||||
|
"step": len(feeding_order) + 1,
|
||||||
|
"type": "solid_anhydride",
|
||||||
|
"name": anhydride["name"],
|
||||||
|
"amount": anhydride["mass"],
|
||||||
|
"order": anhydride["order"]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2. 二胺溶液 (按顺序)
|
||||||
|
for sol in diamine_solutions:
|
||||||
|
feeding_order.append({
|
||||||
|
"step": len(feeding_order) + 1,
|
||||||
|
"type": "diamine_solution",
|
||||||
|
"name": sol["name"],
|
||||||
|
"amount": sol["volume_needed"],
|
||||||
|
"order": sol["order"]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. 主要二酐粉末
|
||||||
|
feeding_order.append({
|
||||||
|
"step": len(feeding_order) + 1,
|
||||||
|
"type": "main_anhydride",
|
||||||
|
"name": titration_name,
|
||||||
|
"amount": m_titration_90.magnitude,
|
||||||
|
"order": titration_anhydride[3]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 4. 补加溶剂
|
||||||
|
if m_solvent_add > 0:
|
||||||
|
feeding_order.append({
|
||||||
|
"step": len(feeding_order) + 1,
|
||||||
|
"type": "additional_solvent",
|
||||||
|
"name": "溶剂",
|
||||||
|
"amount": m_solvent_add.magnitude,
|
||||||
|
"order": 999
|
||||||
|
})
|
||||||
|
|
||||||
|
# 5. 滴定二酐溶液
|
||||||
|
feeding_order.append({
|
||||||
|
"step": len(feeding_order) + 1,
|
||||||
|
"type": "titration_anhydride",
|
||||||
|
"name": f"{titration_name} 滴定液",
|
||||||
|
"amount": m_titration_10.magnitude,
|
||||||
|
"titration_solvent": m_solvent_titration.magnitude,
|
||||||
|
"order": titration_anhydride[3]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 返回实验设计结果
|
||||||
|
results = {
|
||||||
|
"total_mass": m_tot.magnitude,
|
||||||
|
"scale_factor": scale_factor,
|
||||||
|
"solutions": diamine_solutions,
|
||||||
|
"solids": solid_anhydride_masses,
|
||||||
|
"titration": {
|
||||||
|
"name": titration_name,
|
||||||
|
"main_portion": m_titration_90.magnitude,
|
||||||
|
"titration_portion": m_titration_10.magnitude,
|
||||||
|
"titration_solvent": m_solvent_titration.magnitude,
|
||||||
|
},
|
||||||
|
"solvents": {
|
||||||
|
"additional_solvent": m_solvent_add.magnitude,
|
||||||
|
"total_liquid_volume": total_liquid_volume.magnitude
|
||||||
|
},
|
||||||
|
"feeding_order": feeding_order,
|
||||||
|
"minimum_required_mass": m_tot_min.magnitude
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
# 90%10%小瓶投料任务创建方法
|
# 90%10%小瓶投料任务创建方法
|
||||||
def create_90_10_vial_feeding_task(self,
|
def create_90_10_vial_feeding_task(self,
|
||||||
order_name: str = None,
|
order_name: str = None,
|
||||||
@@ -961,6 +1312,108 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
'actualVolume': actual_volume
|
'actualVolume': actual_volume
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _simplify_report(self, report) -> Dict[str, Any]:
|
||||||
|
"""简化实验报告,只保留关键信息,去除冗余的工作流参数"""
|
||||||
|
if not isinstance(report, dict):
|
||||||
|
return report
|
||||||
|
|
||||||
|
data = report.get('data', {})
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return report
|
||||||
|
|
||||||
|
# 提取关键信息
|
||||||
|
simplified = {
|
||||||
|
'name': data.get('name'),
|
||||||
|
'code': data.get('code'),
|
||||||
|
'requester': data.get('requester'),
|
||||||
|
'workflowName': data.get('workflowName'),
|
||||||
|
'workflowStep': data.get('workflowStep'),
|
||||||
|
'requestTime': data.get('requestTime'),
|
||||||
|
'startPreparationTime': data.get('startPreparationTime'),
|
||||||
|
'completeTime': data.get('completeTime'),
|
||||||
|
'useTime': data.get('useTime'),
|
||||||
|
'status': data.get('status'),
|
||||||
|
'statusName': data.get('statusName'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 提取物料信息(简化版)
|
||||||
|
pre_intakes = data.get('preIntakes', [])
|
||||||
|
if pre_intakes and isinstance(pre_intakes, list):
|
||||||
|
first_intake = pre_intakes[0]
|
||||||
|
sample_materials = first_intake.get('sampleMaterials', [])
|
||||||
|
|
||||||
|
# 简化物料信息
|
||||||
|
simplified_materials = []
|
||||||
|
for material in sample_materials:
|
||||||
|
if isinstance(material, dict):
|
||||||
|
mat_info = {
|
||||||
|
'materialName': material.get('materialName'),
|
||||||
|
'materialTypeName': material.get('materialTypeName'),
|
||||||
|
'materialCode': material.get('materialCode'),
|
||||||
|
'materialLocation': material.get('materialLocation'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 解析parameters中的关键信息(如密度、加料历史等)
|
||||||
|
params_str = material.get('parameters', '{}')
|
||||||
|
try:
|
||||||
|
params = json.loads(params_str) if isinstance(params_str, str) else params_str
|
||||||
|
if isinstance(params, dict):
|
||||||
|
# 只保留关键参数
|
||||||
|
if 'density' in params:
|
||||||
|
mat_info['density'] = params['density']
|
||||||
|
if 'feedingHistory' in params:
|
||||||
|
mat_info['feedingHistory'] = params['feedingHistory']
|
||||||
|
if 'liquidVolume' in params:
|
||||||
|
mat_info['liquidVolume'] = params['liquidVolume']
|
||||||
|
if 'm_diamine_tot' in params:
|
||||||
|
mat_info['m_diamine_tot'] = params['m_diamine_tot']
|
||||||
|
if 'wt_diamine' in params:
|
||||||
|
mat_info['wt_diamine'] = params['wt_diamine']
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
simplified_materials.append(mat_info)
|
||||||
|
|
||||||
|
simplified['sampleMaterials'] = simplified_materials
|
||||||
|
|
||||||
|
# 提取extraProperties中的实际值
|
||||||
|
extra_props = first_intake.get('extraProperties', {})
|
||||||
|
if isinstance(extra_props, dict):
|
||||||
|
simplified_extra = {}
|
||||||
|
for key, value in extra_props.items():
|
||||||
|
try:
|
||||||
|
parsed_value = json.loads(value) if isinstance(value, str) else value
|
||||||
|
simplified_extra[key] = parsed_value
|
||||||
|
except:
|
||||||
|
simplified_extra[key] = value
|
||||||
|
simplified['extraProperties'] = simplified_extra
|
||||||
|
|
||||||
|
return {
|
||||||
|
'data': simplified,
|
||||||
|
'code': report.get('code'),
|
||||||
|
'message': report.get('message'),
|
||||||
|
'timestamp': report.get('timestamp')
|
||||||
|
}
|
||||||
|
|
||||||
|
def scheduler_start(self) -> dict:
|
||||||
|
"""启动调度器 - 启动Bioyond工作站的任务调度器,开始执行队列中的任务
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含return_info的字典,return_info为整型(1=成功)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BioyondException: 调度器启动失败时抛出异常
|
||||||
|
"""
|
||||||
|
result = self.hardware_interface.scheduler_start()
|
||||||
|
self.hardware_interface._logger.info(f"调度器启动结果: {result}")
|
||||||
|
|
||||||
|
if result != 1:
|
||||||
|
error_msg = "启动调度器失败: 有未处理错误,调度无法启动。请检查Bioyond系统状态。"
|
||||||
|
self.hardware_interface._logger.error(error_msg)
|
||||||
|
raise BioyondException(error_msg)
|
||||||
|
|
||||||
|
return {"return_info": result}
|
||||||
|
|
||||||
# 等待多个任务完成并获取实验报告
|
# 等待多个任务完成并获取实验报告
|
||||||
def wait_for_multiple_orders_and_get_reports(self,
|
def wait_for_multiple_orders_and_get_reports(self,
|
||||||
batch_create_result: str = None,
|
batch_create_result: str = None,
|
||||||
@@ -1002,7 +1455,12 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
|
|
||||||
# 验证batch_create_result参数
|
# 验证batch_create_result参数
|
||||||
if not batch_create_result or batch_create_result == "":
|
if not batch_create_result or batch_create_result == "":
|
||||||
raise BioyondException("batch_create_result参数为空,请确保从batch_create节点正确连接handle")
|
raise BioyondException(
|
||||||
|
"batch_create_result参数为空,请确保:\n"
|
||||||
|
"1. batch_create节点与wait节点之间正确连接了handle\n"
|
||||||
|
"2. batch_create节点成功执行并返回了结果\n"
|
||||||
|
"3. 检查上游batch_create任务是否成功创建了订单"
|
||||||
|
)
|
||||||
|
|
||||||
# 解析batch_create_result JSON对象
|
# 解析batch_create_result JSON对象
|
||||||
try:
|
try:
|
||||||
@@ -1031,7 +1489,17 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
|
|
||||||
# 验证提取的数据
|
# 验证提取的数据
|
||||||
if not order_codes:
|
if not order_codes:
|
||||||
raise BioyondException("batch_create_result中未找到order_codes字段或为空")
|
self.hardware_interface._logger.error(
|
||||||
|
f"batch_create任务未生成任何订单。batch_create_result内容: {batch_create_result}"
|
||||||
|
)
|
||||||
|
raise BioyondException(
|
||||||
|
"batch_create_result中未找到order_codes或为空。\n"
|
||||||
|
"可能的原因:\n"
|
||||||
|
"1. batch_create任务执行失败(检查任务是否报错)\n"
|
||||||
|
"2. 物料配置问题(如'物料样品板分配失败')\n"
|
||||||
|
"3. Bioyond系统状态异常\n"
|
||||||
|
f"请检查batch_create任务的执行结果"
|
||||||
|
)
|
||||||
if not order_ids:
|
if not order_ids:
|
||||||
raise BioyondException("batch_create_result中未找到order_ids字段或为空")
|
raise BioyondException("batch_create_result中未找到order_ids字段或为空")
|
||||||
|
|
||||||
@@ -1114,6 +1582,8 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
self.hardware_interface._logger.info(
|
self.hardware_interface._logger.info(
|
||||||
f"成功获取任务 {order_code} 的实验报告"
|
f"成功获取任务 {order_code} 的实验报告"
|
||||||
)
|
)
|
||||||
|
# 简化报告,去除冗余信息
|
||||||
|
report = self._simplify_report(report)
|
||||||
|
|
||||||
reports.append({
|
reports.append({
|
||||||
"order_code": order_code,
|
"order_code": order_code,
|
||||||
@@ -1288,7 +1758,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
f"开始执行批量物料转移: {len(transfer_groups)}组任务 -> {target_device_id}"
|
f"开始执行批量物料转移: {len(transfer_groups)}组任务 -> {target_device_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
from .config import WAREHOUSE_MAPPING
|
warehouse_mapping = self.bioyond_config.get("warehouse_mapping", {})
|
||||||
results = []
|
results = []
|
||||||
successful_count = 0
|
successful_count = 0
|
||||||
failed_count = 0
|
failed_count = 0
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ Bioyond Workstation Implementation
|
|||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Any, List, Optional, Union
|
from typing import Dict, Any, List, Optional, Union
|
||||||
import json
|
import json
|
||||||
@@ -23,12 +24,94 @@ from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
|||||||
from unilabos.ros.msgs.message_converter import convert_to_ros_msg, Float64, String
|
from unilabos.ros.msgs.message_converter import convert_to_ros_msg, Float64, String
|
||||||
from pylabrobot.resources.resource import Resource as ResourcePLR
|
from pylabrobot.resources.resource import Resource as ResourcePLR
|
||||||
|
|
||||||
from unilabos.devices.workstation.bioyond_studio.config import (
|
|
||||||
API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING, HTTP_SERVICE_CONFIG
|
|
||||||
)
|
|
||||||
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
|
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionMonitor:
|
||||||
|
"""Bioyond连接监控器"""
|
||||||
|
def __init__(self, workstation, check_interval=30):
|
||||||
|
self.workstation = workstation
|
||||||
|
self.check_interval = check_interval
|
||||||
|
self._running = False
|
||||||
|
self._thread = None
|
||||||
|
self._last_status = "unknown"
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self._running:
|
||||||
|
return
|
||||||
|
self._running = True
|
||||||
|
self._thread = threading.Thread(target=self._monitor_loop, daemon=True, name="BioyondConnectionMonitor")
|
||||||
|
self._thread.start()
|
||||||
|
logger.info("Bioyond连接监控器已启动")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._running = False
|
||||||
|
if self._thread:
|
||||||
|
self._thread.join(timeout=2)
|
||||||
|
logger.info("Bioyond连接监控器已停止")
|
||||||
|
|
||||||
|
def _monitor_loop(self):
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
|
# 使用 lightweight API 检查连接
|
||||||
|
# query_matial_type_list 是比较快的查询
|
||||||
|
start_time = time.time()
|
||||||
|
result = self.workstation.hardware_interface.material_type_list()
|
||||||
|
|
||||||
|
status = "online" if result else "offline"
|
||||||
|
msg = "Connection established" if status == "online" else "Failed to get material type list"
|
||||||
|
|
||||||
|
if status != self._last_status:
|
||||||
|
logger.info(f"Bioyond连接状态变更: {self._last_status} -> {status}")
|
||||||
|
self._publish_event(status, msg)
|
||||||
|
self._last_status = status
|
||||||
|
|
||||||
|
# 发布心跳 (可选,或者只在状态变更时发布)
|
||||||
|
# self._publish_event(status, msg)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Bioyond连接检查异常: {e}")
|
||||||
|
if self._last_status != "error":
|
||||||
|
self._publish_event("error", str(e))
|
||||||
|
self._last_status = "error"
|
||||||
|
|
||||||
|
time.sleep(self.check_interval)
|
||||||
|
|
||||||
|
def _publish_event(self, status, message):
|
||||||
|
try:
|
||||||
|
if hasattr(self.workstation, "_ros_node") and self.workstation._ros_node:
|
||||||
|
event_data = {
|
||||||
|
"status": status,
|
||||||
|
"message": message,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 动态发布消息,需要在 ROS2DeviceNode 中有对应支持
|
||||||
|
# 这里假设通用事件发布机制,使用 String 类型的 topic
|
||||||
|
# 话题: /<namespace>/events/device_status
|
||||||
|
ns = self.workstation._ros_node.namespace
|
||||||
|
topic = f"{ns}/events/device_status"
|
||||||
|
|
||||||
|
# 使用 ROS2DeviceNode 的发布功能
|
||||||
|
# 如果没有预定义的 publisher,需要动态创建
|
||||||
|
# 注意:workstation base node 可能没有自动创建 arbitrary publishers 的机制
|
||||||
|
# 这里我们先尝试用 String json 发布
|
||||||
|
|
||||||
|
# 在 ROS2DeviceNode 中通常需要先 create_publisher
|
||||||
|
# 为了简单起见,我们检查是否已有 publisher,没有则创建
|
||||||
|
if not hasattr(self.workstation, "_device_status_pub"):
|
||||||
|
self.workstation._device_status_pub = self.workstation._ros_node.create_publisher(
|
||||||
|
String, topic, 10
|
||||||
|
)
|
||||||
|
|
||||||
|
self.workstation._device_status_pub.publish(
|
||||||
|
convert_to_ros_msg(String, json.dumps(event_data, ensure_ascii=False))
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发布设备状态事件失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
class BioyondResourceSynchronizer(ResourceSynchronizer):
|
class BioyondResourceSynchronizer(ResourceSynchronizer):
|
||||||
"""Bioyond资源同步器
|
"""Bioyond资源同步器
|
||||||
|
|
||||||
@@ -174,9 +257,8 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
|
|||||||
else:
|
else:
|
||||||
logger.info(f"[同步→Bioyond] ➕ 物料不存在于 Bioyond,将创建新物料并入库")
|
logger.info(f"[同步→Bioyond] ➕ 物料不存在于 Bioyond,将创建新物料并入库")
|
||||||
|
|
||||||
# 第1步:获取仓库配置
|
# 第1步:从配置中获取仓库配置
|
||||||
from .config import WAREHOUSE_MAPPING
|
warehouse_mapping = self.bioyond_config.get("warehouse_mapping", {})
|
||||||
warehouse_mapping = WAREHOUSE_MAPPING
|
|
||||||
|
|
||||||
# 确定目标仓库名称
|
# 确定目标仓库名称
|
||||||
parent_name = None
|
parent_name = None
|
||||||
@@ -238,14 +320,20 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
|
|||||||
# 第2步:转换为 Bioyond 格式
|
# 第2步:转换为 Bioyond 格式
|
||||||
logger.info(f"[同步→Bioyond] 🔄 转换物料为 Bioyond 格式...")
|
logger.info(f"[同步→Bioyond] 🔄 转换物料为 Bioyond 格式...")
|
||||||
|
|
||||||
# 导入物料默认参数配置
|
# 从配置中获取物料默认参数
|
||||||
from .config import MATERIAL_DEFAULT_PARAMETERS
|
material_default_params = self.workstation.bioyond_config.get("material_default_parameters", {})
|
||||||
|
material_type_params = self.workstation.bioyond_config.get("material_type_parameters", {})
|
||||||
|
|
||||||
|
# 合并参数配置:物料名称参数 + typeId参数(转换为 type:<uuid> 格式)
|
||||||
|
merged_params = material_default_params.copy()
|
||||||
|
for type_id, params in material_type_params.items():
|
||||||
|
merged_params[f"type:{type_id}"] = params
|
||||||
|
|
||||||
bioyond_material = resource_plr_to_bioyond(
|
bioyond_material = resource_plr_to_bioyond(
|
||||||
[resource],
|
[resource],
|
||||||
type_mapping=self.workstation.bioyond_config["material_type_mappings"],
|
type_mapping=self.workstation.bioyond_config["material_type_mappings"],
|
||||||
warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"],
|
warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"],
|
||||||
material_params=MATERIAL_DEFAULT_PARAMETERS
|
material_params=merged_params
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
logger.info(f"[同步→Bioyond] 🔧 准备覆盖locations字段,目标仓库: {parent_name}, 库位: {update_site}, UUID: {target_location_uuid[:8]}...")
|
logger.info(f"[同步→Bioyond] 🔧 准备覆盖locations字段,目标仓库: {parent_name}, 库位: {update_site}, UUID: {target_location_uuid[:8]}...")
|
||||||
@@ -468,13 +556,20 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
|
|||||||
return material_bioyond_id
|
return material_bioyond_id
|
||||||
|
|
||||||
# 转换为 Bioyond 格式
|
# 转换为 Bioyond 格式
|
||||||
from .config import MATERIAL_DEFAULT_PARAMETERS
|
# 从配置中获取物料默认参数
|
||||||
|
material_default_params = self.workstation.bioyond_config.get("material_default_parameters", {})
|
||||||
|
material_type_params = self.workstation.bioyond_config.get("material_type_parameters", {})
|
||||||
|
|
||||||
|
# 合并参数配置:物料名称参数 + typeId参数(转换为 type:<uuid> 格式)
|
||||||
|
merged_params = material_default_params.copy()
|
||||||
|
for type_id, params in material_type_params.items():
|
||||||
|
merged_params[f"type:{type_id}"] = params
|
||||||
|
|
||||||
bioyond_material = resource_plr_to_bioyond(
|
bioyond_material = resource_plr_to_bioyond(
|
||||||
[resource],
|
[resource],
|
||||||
type_mapping=self.workstation.bioyond_config["material_type_mappings"],
|
type_mapping=self.workstation.bioyond_config["material_type_mappings"],
|
||||||
warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"],
|
warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"],
|
||||||
material_params=MATERIAL_DEFAULT_PARAMETERS
|
material_params=merged_params
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
# ⚠️ 关键:创建物料时不设置 locations,让 Bioyond 系统暂不分配库位
|
# ⚠️ 关键:创建物料时不设置 locations,让 Bioyond 系统暂不分配库位
|
||||||
@@ -528,8 +623,7 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
|
|||||||
logger.info(f"[物料入库] 目标库位: {update_site}")
|
logger.info(f"[物料入库] 目标库位: {update_site}")
|
||||||
|
|
||||||
# 获取仓库配置和目标库位 UUID
|
# 获取仓库配置和目标库位 UUID
|
||||||
from .config import WAREHOUSE_MAPPING
|
warehouse_mapping = self.workstation.bioyond_config.get("warehouse_mapping", {})
|
||||||
warehouse_mapping = WAREHOUSE_MAPPING
|
|
||||||
|
|
||||||
parent_name = None
|
parent_name = None
|
||||||
target_location_uuid = None
|
target_location_uuid = None
|
||||||
@@ -584,6 +678,44 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
集成Bioyond物料管理的工作站实现
|
集成Bioyond物料管理的工作站实现
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _publish_task_status(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
task_type: str,
|
||||||
|
status: str,
|
||||||
|
result: dict = None,
|
||||||
|
progress: float = 0.0,
|
||||||
|
task_code: str = None
|
||||||
|
):
|
||||||
|
"""发布任务状态事件"""
|
||||||
|
try:
|
||||||
|
if not getattr(self, "_ros_node", None):
|
||||||
|
return
|
||||||
|
|
||||||
|
event_data = {
|
||||||
|
"task_id": task_id,
|
||||||
|
"task_code": task_code,
|
||||||
|
"task_type": task_type,
|
||||||
|
"status": status,
|
||||||
|
"progress": progress,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
if result:
|
||||||
|
event_data["result"] = result
|
||||||
|
|
||||||
|
topic = f"{self._ros_node.namespace}/events/task_status"
|
||||||
|
|
||||||
|
if not hasattr(self, "_task_status_pub"):
|
||||||
|
self._task_status_pub = self._ros_node.create_publisher(
|
||||||
|
String, topic, 10
|
||||||
|
)
|
||||||
|
|
||||||
|
self._task_status_pub.publish(
|
||||||
|
convert_to_ros_msg(String, json.dumps(event_data, ensure_ascii=False))
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发布任务状态事件失败: {e}")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
bioyond_config: Optional[Dict[str, Any]] = None,
|
bioyond_config: Optional[Dict[str, Any]] = None,
|
||||||
@@ -605,10 +737,28 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
raise ValueError("Deck 配置不能为空,请在配置文件中添加正确的 deck 配置")
|
raise ValueError("Deck 配置不能为空,请在配置文件中添加正确的 deck 配置")
|
||||||
|
|
||||||
# 初始化 warehouses 属性
|
# 初始化 warehouses 属性
|
||||||
self.deck.warehouses = {}
|
if not hasattr(self.deck, "warehouses") or self.deck.warehouses is None:
|
||||||
for resource in self.deck.children:
|
self.deck.warehouses = {}
|
||||||
if isinstance(resource, WareHouse):
|
|
||||||
self.deck.warehouses[resource.name] = resource
|
# 仅当 warehouses 为空时尝试重新扫描(避免覆盖子类的修复)
|
||||||
|
if not self.deck.warehouses:
|
||||||
|
for resource in self.deck.children:
|
||||||
|
# 兼容性增强: 只要是仓库类别或者是 WareHouse 实例均可
|
||||||
|
is_warehouse = isinstance(resource, WareHouse) or getattr(resource, "category", "") == "warehouse"
|
||||||
|
|
||||||
|
# 如果配置中有定义,也可以认定为 warehouse
|
||||||
|
if not is_warehouse and "warehouse_mapping" in bioyond_config:
|
||||||
|
if resource.name in bioyond_config["warehouse_mapping"]:
|
||||||
|
is_warehouse = True
|
||||||
|
|
||||||
|
if is_warehouse:
|
||||||
|
self.deck.warehouses[resource.name] = resource
|
||||||
|
# 确保 category 被正确设置,方便后续使用
|
||||||
|
if getattr(resource, "category", "") != "warehouse":
|
||||||
|
try:
|
||||||
|
resource.category = "warehouse"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# 创建通信模块
|
# 创建通信模块
|
||||||
self._create_communication_module(bioyond_config)
|
self._create_communication_module(bioyond_config)
|
||||||
@@ -627,18 +777,22 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
self._set_workflow_mappings(bioyond_config["workflow_mappings"])
|
self._set_workflow_mappings(bioyond_config["workflow_mappings"])
|
||||||
|
|
||||||
# 准备 HTTP 报送接收服务配置(延迟到 post_init 启动)
|
# 准备 HTTP 报送接收服务配置(延迟到 post_init 启动)
|
||||||
# 从 bioyond_config 中获取,如果没有则使用 HTTP_SERVICE_CONFIG 的默认值
|
# 从 bioyond_config 中的 http_service_config 获取
|
||||||
|
http_service_cfg = bioyond_config.get("http_service_config", {})
|
||||||
self._http_service_config = {
|
self._http_service_config = {
|
||||||
"host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG["http_service_host"]),
|
"host": http_service_cfg.get("http_service_host", "127.0.0.1"),
|
||||||
"port": bioyond_config.get("http_service_port", HTTP_SERVICE_CONFIG["http_service_port"])
|
"port": http_service_cfg.get("http_service_port", 8080)
|
||||||
}
|
}
|
||||||
self.http_service = None # 将在 post_init 中启动
|
self.http_service = None # 将在 post_init 启动
|
||||||
|
self.connection_monitor = None # 将在 post_init 启动
|
||||||
|
|
||||||
logger.info(f"Bioyond工作站初始化完成")
|
logger.info(f"Bioyond工作站初始化完成")
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""析构函数:清理资源,停止 HTTP 服务"""
|
"""析构函数:清理资源,停止 HTTP 服务"""
|
||||||
try:
|
try:
|
||||||
|
if hasattr(self, 'connection_monitor') and self.connection_monitor:
|
||||||
|
self.connection_monitor.stop()
|
||||||
if hasattr(self, 'http_service') and self.http_service is not None:
|
if hasattr(self, 'http_service') and self.http_service is not None:
|
||||||
logger.info("正在停止 HTTP 报送服务...")
|
logger.info("正在停止 HTTP 报送服务...")
|
||||||
self.http_service.stop()
|
self.http_service.stop()
|
||||||
@@ -648,8 +802,19 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
def post_init(self, ros_node: ROS2WorkstationNode):
|
def post_init(self, ros_node: ROS2WorkstationNode):
|
||||||
self._ros_node = ros_node
|
self._ros_node = ros_node
|
||||||
|
|
||||||
|
# 启动连接监控
|
||||||
|
try:
|
||||||
|
self.connection_monitor = ConnectionMonitor(self)
|
||||||
|
self.connection_monitor.start()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"启动连接监控失败: {e}")
|
||||||
|
|
||||||
# 启动 HTTP 报送接收服务(现在 device_id 已可用)
|
# 启动 HTTP 报送接收服务(现在 device_id 已可用)
|
||||||
if hasattr(self, '_http_service_config'):
|
# ⚠️ 检查子类是否已经自己管理 HTTP 服务
|
||||||
|
if self.bioyond_config.get("_disable_auto_http_service"):
|
||||||
|
logger.info("🔧 检测到 _disable_auto_http_service 标志,跳过自动启动 HTTP 服务")
|
||||||
|
logger.info(" 子类(BioyondCellWorkstation)已自行管理 HTTP 服务")
|
||||||
|
elif hasattr(self, '_http_service_config'):
|
||||||
try:
|
try:
|
||||||
self.http_service = WorkstationHTTPService(
|
self.http_service = WorkstationHTTPService(
|
||||||
workstation_instance=self,
|
workstation_instance=self,
|
||||||
@@ -688,19 +853,14 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
|
|
||||||
def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None:
|
def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None:
|
||||||
"""创建Bioyond通信模块"""
|
"""创建Bioyond通信模块"""
|
||||||
# 创建默认配置
|
# 直接使用传入的配置,不再使用默认值
|
||||||
default_config = {
|
# 所有配置必须从 JSON 文件中提供
|
||||||
**API_CONFIG,
|
|
||||||
"workflow_mappings": WORKFLOW_MAPPINGS,
|
|
||||||
"material_type_mappings": MATERIAL_TYPE_MAPPINGS,
|
|
||||||
"warehouse_mapping": WAREHOUSE_MAPPING
|
|
||||||
}
|
|
||||||
|
|
||||||
# 如果传入了 config,合并配置(config 中的值会覆盖默认值)
|
|
||||||
if config:
|
if config:
|
||||||
self.bioyond_config = {**default_config, **config}
|
self.bioyond_config = config
|
||||||
else:
|
else:
|
||||||
self.bioyond_config = default_config
|
# 如果没有配置,使用空字典(会导致后续错误,但这是预期的)
|
||||||
|
self.bioyond_config = {}
|
||||||
|
print("警告: 未提供 bioyond_config,请确保在 JSON 配置文件中提供完整配置")
|
||||||
|
|
||||||
self.hardware_interface = BioyondV1RPC(self.bioyond_config)
|
self.hardware_interface = BioyondV1RPC(self.bioyond_config)
|
||||||
|
|
||||||
@@ -1014,7 +1174,15 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
|
|
||||||
workflow_id = self._get_workflow(actual_workflow_name)
|
workflow_id = self._get_workflow(actual_workflow_name)
|
||||||
if workflow_id:
|
if workflow_id:
|
||||||
self.workflow_sequence.append(workflow_id)
|
# 兼容 BioyondReactionStation 中 workflow_sequence 被重写为 property 的情况
|
||||||
|
if isinstance(self.workflow_sequence, list):
|
||||||
|
self.workflow_sequence.append(workflow_id)
|
||||||
|
elif hasattr(self, "_cached_workflow_sequence") and isinstance(self._cached_workflow_sequence, list):
|
||||||
|
self._cached_workflow_sequence.append(workflow_id)
|
||||||
|
else:
|
||||||
|
print(f"❌ 无法添加工作流: workflow_sequence 类型错误 {type(self.workflow_sequence)}")
|
||||||
|
return False
|
||||||
|
|
||||||
print(f"添加工作流到执行顺序: {actual_workflow_name} -> {workflow_id}")
|
print(f"添加工作流到执行顺序: {actual_workflow_name} -> {workflow_id}")
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -1215,6 +1383,22 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
# TODO: 根据实际业务需求处理步骤完成逻辑
|
# TODO: 根据实际业务需求处理步骤完成逻辑
|
||||||
# 例如:更新数据库、触发后续流程等
|
# 例如:更新数据库、触发后续流程等
|
||||||
|
|
||||||
|
# 发布任务状态事件 (running/progress update)
|
||||||
|
self._publish_task_status(
|
||||||
|
task_id=data.get('orderCode'), # 使用 OrderCode 作为关联 ID
|
||||||
|
task_code=data.get('orderCode'),
|
||||||
|
task_type="bioyond_step",
|
||||||
|
status="running",
|
||||||
|
progress=0.5, # 步骤完成视为任务进行中
|
||||||
|
result={"step_name": data.get('stepName'), "step_id": data.get('stepId')}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新物料信息
|
||||||
|
# 步骤完成后,物料状态可能发生变化(如位置、用量等),触发同步
|
||||||
|
logger.info(f"[步骤完成报送] 触发物料同步...")
|
||||||
|
self.resource_synchronizer.sync_from_external()
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"processed": True,
|
"processed": True,
|
||||||
"step_id": data.get('stepId'),
|
"step_id": data.get('stepId'),
|
||||||
@@ -1249,6 +1433,17 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
|
|
||||||
# TODO: 根据实际业务需求处理通量完成逻辑
|
# TODO: 根据实际业务需求处理通量完成逻辑
|
||||||
|
|
||||||
|
# 发布任务状态事件
|
||||||
|
self._publish_task_status(
|
||||||
|
task_id=data.get('orderCode'),
|
||||||
|
task_code=data.get('orderCode'),
|
||||||
|
task_type="bioyond_sample",
|
||||||
|
status="running",
|
||||||
|
progress=0.7,
|
||||||
|
result={"sample_id": data.get('sampleId'), "status": status_desc}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"processed": True,
|
"processed": True,
|
||||||
"sample_id": data.get('sampleId'),
|
"sample_id": data.get('sampleId'),
|
||||||
@@ -1288,6 +1483,32 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
# TODO: 根据实际业务需求处理任务完成逻辑
|
# TODO: 根据实际业务需求处理任务完成逻辑
|
||||||
# 例如:更新物料库存、生成报表等
|
# 例如:更新物料库存、生成报表等
|
||||||
|
|
||||||
|
# 映射状态到事件状态
|
||||||
|
event_status = "completed"
|
||||||
|
if str(data.get('status')) in ["-11", "-12"]:
|
||||||
|
event_status = "error"
|
||||||
|
elif str(data.get('status')) == "30":
|
||||||
|
event_status = "completed"
|
||||||
|
else:
|
||||||
|
event_status = "running" # 其他状态视为运行中(或根据实际定义)
|
||||||
|
|
||||||
|
# 发布任务状态事件
|
||||||
|
self._publish_task_status(
|
||||||
|
task_id=data.get('orderCode'),
|
||||||
|
task_code=data.get('orderCode'),
|
||||||
|
task_type="bioyond_order",
|
||||||
|
status=event_status,
|
||||||
|
progress=1.0 if event_status in ["completed", "error"] else 0.9,
|
||||||
|
result={"order_name": data.get('orderName'), "status": status_desc, "materials_count": len(used_materials)}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新物料信息
|
||||||
|
# 任务完成后,且状态为完成时,触发同步以更新最终物料状态
|
||||||
|
if event_status == "completed":
|
||||||
|
logger.info(f"[任务完成报送] 触发物料同步...")
|
||||||
|
self.resource_synchronizer.sync_from_external()
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"processed": True,
|
"processed": True,
|
||||||
"order_code": data.get('orderCode'),
|
"order_code": data.get('orderCode'),
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Modbus CSV 地址映射说明
|
||||||
|
|
||||||
|
本文档说明 `coin_cell_assembly_a.csv` 文件如何将命名节点映射到实际的 Modbus 地址,以及如何在代码中使用它们。
|
||||||
|
|
||||||
|
## 1. CSV 文件结构
|
||||||
|
|
||||||
|
地址表文件位于同级目录下:`coin_cell_assembly_a.csv`
|
||||||
|
|
||||||
|
每一行定义了一个 Modbus 节点,包含以下关键列:
|
||||||
|
|
||||||
|
| 列名 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| **Name** | **节点名称** (代码中引用的 Key) | `COIL_ALUMINUM_FOIL` |
|
||||||
|
| **DataType** | 数据类型 (BOOL, INT16, FLOAT32, STRING) | `BOOL` |
|
||||||
|
| **Comment** | 注释说明 | `使用铝箔垫` |
|
||||||
|
| **Attribute** | 属性 (通常留空或用于额外标记) | |
|
||||||
|
| **DeviceType** | Modbus 寄存器类型 (`coil`, `hold_register`) | `coil` |
|
||||||
|
| **Address** | **Modbus 地址** (十进制) | `8340` |
|
||||||
|
|
||||||
|
### 示例行 (铝箔垫片)
|
||||||
|
|
||||||
|
```csv
|
||||||
|
COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,8340,
|
||||||
|
```
|
||||||
|
|
||||||
|
- **名称**: `COIL_ALUMINUM_FOIL`
|
||||||
|
- **类型**: `coil` (线圈,读写单个位)
|
||||||
|
- **地址**: `8340`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 加载与注册流程
|
||||||
|
|
||||||
|
在 `coin_cell_assembly.py` 的初始化代码中:
|
||||||
|
|
||||||
|
1. **加载 CSV**: `BaseClient.load_csv()` 读取 CSV 并解析每行定义。
|
||||||
|
2. **注册节点**: `modbus_client.register_node_list()` 将解析后的节点注册到 Modbus 客户端实例中。
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 代码位置: coin_cell_assembly.py (L174-175)
|
||||||
|
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_a.csv'))
|
||||||
|
self.client = modbus_client.register_node_list(self.nodes)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 代码中的使用方式
|
||||||
|
|
||||||
|
注册后,通过 `self.client.use_node('节点名称')` 即可获取该节点对象并进行读写操作,无需关心具体地址。
|
||||||
|
|
||||||
|
### 控制铝箔垫片 (COIL_ALUMINUM_FOIL)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 代码位置: qiming_coin_cell_code 函数 (L1048)
|
||||||
|
self.client.use_node('COIL_ALUMINUM_FOIL').write(not lvbodian)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **写入 True**: 对应 Modbus 功能码 05 (Write Single Coil),向地址 `8340` 写入 `1` (ON)。
|
||||||
|
- **写入 False**: 向地址 `8340` 写入 `0` (OFF)。
|
||||||
|
|
||||||
|
> **注意**: 代码中使用了 `not lvbodian`,这意味着逻辑是反转的。如果 `lvbodian` 参数为 `True` (默认),写入的是 `False` (不使用铝箔垫)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 地址转换注意事项 (Modbus vs PLC)
|
||||||
|
|
||||||
|
CSV 中的 `Address` 列(如 `8340`)是 **Modbus 协议地址**。
|
||||||
|
|
||||||
|
如果使用 InoProShop (汇川 PLC 编程软件),看到的可能是 **PLC 内部地址** (如 `%QX...` 或 `%MW...`)。这两者之间通常需要转换。
|
||||||
|
|
||||||
|
### 常见的转换规则 (示例)
|
||||||
|
|
||||||
|
- **Coil (线圈) %QX**:
|
||||||
|
- `Modbus地址 = 字节地址 * 8 + 位偏移`
|
||||||
|
- *例子*: `%QX834.0` -> `834 * 8 + 0` = `6672`
|
||||||
|
- *注意*: 如果 CSV 中配置的是 `8340`,这可能是一个自定义映射,或者是基于不同规则(如直接对应 Word 地址的某种映射,或者可能就是地址写错了/使用了非标准映射)。
|
||||||
|
|
||||||
|
- **Register (寄存器) %MW**:
|
||||||
|
- 通常直接对应,或者有偏移量 (如 Modbus 40001 = PLC MW0)。
|
||||||
|
|
||||||
|
### 验证方法
|
||||||
|
由于 `test_unilab_interact.py` 中发现 `8450` (CSV风格) 不工作,而 `6760` (%QX845.0 计算值) 工作正常,**建议对 CSV 中的其他地址也进行核实**,特别是像 `8340` 这样以 0 结尾看起来像是 "字节地址+0" 的数值,可能实际上应该是 `%QX834.0` 对应的 `6672`。
|
||||||
|
|
||||||
|
如果发现设备控制无反应,请尝试按照标准的 Modbus 计算方式转换 PLC 地址。
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
# 2026-01-13 物料搜寻确认弹窗自动处理功能
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本次更新为设备初始化流程添加了**物料搜寻确认弹窗自动检测与处理功能**。在设备初始化过程中,PLC 会弹出物料搜寻确认对话框,现在系统可以根据用户参数自动点击"是"或"否"按钮,无需手动干预。
|
||||||
|
|
||||||
|
## 背景问题
|
||||||
|
|
||||||
|
### 原有流程
|
||||||
|
1. 调用 `func_pack_device_init_auto_start_combined()` 初始化设备
|
||||||
|
2. PLC 在初始化过程中弹出物料搜寻确认对话框
|
||||||
|
3. **需要人工手动点击**"是"或"否"按钮
|
||||||
|
4. PLC 继续完成初始化并启动
|
||||||
|
|
||||||
|
### 存在的问题
|
||||||
|
- 需要人工干预,无法实现全自动化
|
||||||
|
- 影响批量生产效率
|
||||||
|
- 容易遗忘点击导致流程卡住
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
### 新增 Modbus 地址配置
|
||||||
|
|
||||||
|
在 `coin_cell_assembly_b.csv` 第 69-71 行添加三个 coil:
|
||||||
|
|
||||||
|
| Name | DeviceType | Address | 说明 |
|
||||||
|
|------|-----------|---------|------|
|
||||||
|
| COIL_MATERIAL_SEARCH_DIALOG_APPEAR | coil | 6470 | 物料搜寻确认弹窗画面是否出现 |
|
||||||
|
| COIL_MATERIAL_SEARCH_CONFIRM_YES | coil | 6480 | 初始化物料搜寻确认按钮"是" |
|
||||||
|
| COIL_MATERIAL_SEARCH_CONFIRM_NO | coil | 6490 | 初始化物料搜寻确认按钮"否" |
|
||||||
|
|
||||||
|
**Modbus 地址转换:**
|
||||||
|
- CSV 6470 → Modbus 5176 (弹窗出现)
|
||||||
|
- CSV 6480 → Modbus 5184 (按钮"是")
|
||||||
|
- CSV 6490 → Modbus 5192 (按钮"否")
|
||||||
|
|
||||||
|
## 代码修改详情
|
||||||
|
|
||||||
|
### 1. coin_cell_assembly.py
|
||||||
|
|
||||||
|
#### 1.1 新增辅助方法 `_handle_material_search_dialog()`
|
||||||
|
|
||||||
|
**位置:** 第 799-901 行
|
||||||
|
|
||||||
|
**功能:**
|
||||||
|
- 监测物料搜寻确认弹窗是否出现(Coil 5176)
|
||||||
|
- 根据 `enable_search` 参数自动点击对应按钮
|
||||||
|
- 使用**脉冲模式**模拟真实按钮操作:`True` → 保持 0.5 秒 → `False`
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `enable_search: bool` - True=点击"是"(启用物料搜寻), False=点击"否"(不启用)
|
||||||
|
- `timeout: int = 30` - 等待弹窗出现的最大时间(秒)
|
||||||
|
|
||||||
|
**逻辑流程:**
|
||||||
|
```python
|
||||||
|
1. 监测 COIL_MATERIAL_SEARCH_DIALOG_APPEAR (每 0.5 秒检查一次)
|
||||||
|
2. 检测到弹窗出现 (Coil = True)
|
||||||
|
3. 选择按钮:
|
||||||
|
- enable_search=True → COIL_MATERIAL_SEARCH_CONFIRM_YES
|
||||||
|
- enable_search=False → COIL_MATERIAL_SEARCH_CONFIRM_NO
|
||||||
|
4. 执行脉冲操作:
|
||||||
|
- 写入 True (按下按钮)
|
||||||
|
- 等待 0.5 秒
|
||||||
|
- 写入 False (释放按钮)
|
||||||
|
- 验证状态
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 修改 `func_pack_device_init_auto_start_combined()`
|
||||||
|
|
||||||
|
**位置:** 第 904-1115 行
|
||||||
|
|
||||||
|
**主要改动:**
|
||||||
|
|
||||||
|
1. **添加新参数**
|
||||||
|
```python
|
||||||
|
def func_pack_device_init_auto_start_combined(
|
||||||
|
self,
|
||||||
|
material_search_enable: bool = False # 新增参数
|
||||||
|
) -> bool:
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **内联初始化逻辑并集成弹窗检测**
|
||||||
|
- 不再调用 `self.func_pack_device_init()`
|
||||||
|
- 将初始化逻辑直接实现在函数内
|
||||||
|
- **在等待初始化完成的循环中实时检测弹窗**
|
||||||
|
- 避免死锁:PLC 等待弹窗确认 ↔ 代码等待初始化完成
|
||||||
|
|
||||||
|
3. **关键代码片段**
|
||||||
|
```python
|
||||||
|
# 等待初始化完成,同时检测物料搜寻弹窗
|
||||||
|
while (self._sys_init_status()) == False:
|
||||||
|
# 检查超时
|
||||||
|
if time.time() - start_wait > max_wait_time:
|
||||||
|
raise RuntimeError(f"初始化超时")
|
||||||
|
|
||||||
|
# 如果还没处理弹窗,检测弹窗是否出现
|
||||||
|
if not dialog_handled:
|
||||||
|
dialog_state = self.client.use_node('COIL_MATERIAL_SEARCH_DIALOG_APPEAR').read(1)
|
||||||
|
if dialog_actual: # 弹窗出现
|
||||||
|
# 执行脉冲按钮点击
|
||||||
|
button_node.write(True) # 按下
|
||||||
|
time.sleep(0.5) # 保持
|
||||||
|
button_node.write(False) # 释放
|
||||||
|
dialog_handled = True
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **步骤调整**
|
||||||
|
- 步骤 0: 前置条件检查
|
||||||
|
- 步骤 1: 设备初始化(**包含弹窗检测**)
|
||||||
|
- 步骤 1.5: 已在步骤 1 中完成
|
||||||
|
- 步骤 2: 切换自动模式
|
||||||
|
- 步骤 3: 启动设备
|
||||||
|
|
||||||
|
### 2. coin_cell_workstation.yaml
|
||||||
|
|
||||||
|
**位置:** 第 292-312 行
|
||||||
|
|
||||||
|
**修改内容:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auto-func_pack_device_init_auto_start_combined:
|
||||||
|
goal_default:
|
||||||
|
material_search_enable: false # 新增默认值
|
||||||
|
|
||||||
|
schema:
|
||||||
|
description: 组合函数:设备初始化 + 物料搜寻确认 + 切换自动模式 + 启动。初始化过程中会自动检测物料搜寻确认弹窗,并根据参数自动点击"是"或"否"按钮
|
||||||
|
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
material_search_enable: # 新增参数配置
|
||||||
|
default: false
|
||||||
|
description: 是否启用物料搜寻功能。设备初始化后会弹出物料搜寻确认弹窗,此参数控制自动点击"是"(启用)或"否"(不启用)。默认为false(不启用物料搜寻)
|
||||||
|
type: boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 测试脚本(已创建,用户已删除)
|
||||||
|
|
||||||
|
#### 3.1 test_material_search_dialog.py
|
||||||
|
- 从 CSV 动态加载 Modbus 地址
|
||||||
|
- 支持 4 种测试模式:
|
||||||
|
- `query` - 查询所有状态
|
||||||
|
- `dialog <0|1>` - 设置弹窗出现/消失
|
||||||
|
- `yes` - 脉冲点击"是"按钮
|
||||||
|
- `no` - 脉冲点击"否"按钮
|
||||||
|
- 兼容 pymodbus 3.x API
|
||||||
|
|
||||||
|
#### 3.2 更新其他测试脚本
|
||||||
|
- `test_coin_cell_reset.py` - 更新为 pymodbus 3.x API
|
||||||
|
- `test_unilab_interact.py` - 更新为 pymodbus 3.x API
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 参数说明
|
||||||
|
|
||||||
|
| 参数 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `material_search_enable` | boolean | `false` | 是否启用物料搜寻功能 |
|
||||||
|
|
||||||
|
### 调用示例
|
||||||
|
|
||||||
|
#### 1. 不启用物料搜寻(默认)
|
||||||
|
```python
|
||||||
|
# 默认参数,点击"否"按钮
|
||||||
|
await device.func_pack_device_init_auto_start_combined()
|
||||||
|
```
|
||||||
|
|
||||||
|
或在 YAML workflow 中:
|
||||||
|
```yaml
|
||||||
|
# 使用默认值 false,不启用物料搜寻
|
||||||
|
- BatteryStation/auto-func_pack_device_init_auto_start_combined: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 启用物料搜寻
|
||||||
|
```python
|
||||||
|
# 显式设置为 True,点击"是"按钮
|
||||||
|
await device.func_pack_device_init_auto_start_combined(
|
||||||
|
material_search_enable=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
或在 YAML workflow 中:
|
||||||
|
```yaml
|
||||||
|
- BatteryStation/auto-func_pack_device_init_auto_start_combined:
|
||||||
|
goal:
|
||||||
|
material_search_enable: true # 启用物料搜寻
|
||||||
|
```
|
||||||
|
|
||||||
|
## 执行日志示例
|
||||||
|
|
||||||
|
```
|
||||||
|
26-01-13 [21:32:44] [INFO] 开始组合操作:设备初始化 → 物料搜寻确认 → 自动模式 → 启动
|
||||||
|
26-01-13 [21:32:44] [INFO] 【步骤 0/4】前置条件检查...
|
||||||
|
26-01-13 [21:32:44] [INFO] ✓ REG_UNILAB_INTERACT 检查通过
|
||||||
|
26-01-13 [21:32:44] [INFO] ✓ COIL_GB_L_IGNORE_CMD 检查通过
|
||||||
|
26-01-13 [21:32:44] [INFO] 【步骤 1/4】设备初始化...
|
||||||
|
26-01-13 [21:32:44] [INFO] 切换手动模式...
|
||||||
|
26-01-13 [21:32:46] [INFO] 发送初始化命令...
|
||||||
|
26-01-13 [21:32:47] [INFO] 等待初始化完成(同时监测物料搜寻弹窗)...
|
||||||
|
26-01-13 [21:33:05] [INFO] ✓ 在初始化过程中检测到物料搜寻确认弹窗!
|
||||||
|
26-01-13 [21:33:05] [INFO] 用户选择: 不启用物料搜寻(点击否)
|
||||||
|
26-01-13 [21:33:05] [INFO] → 按下按钮 '否'
|
||||||
|
26-01-13 [21:33:06] [INFO] → 释放按钮 '否'
|
||||||
|
26-01-13 [21:33:07] [INFO] ✓ 成功处理物料搜寻确认弹窗(选择: 否)
|
||||||
|
26-01-13 [21:33:08] [INFO] ✓ 初始化状态完成
|
||||||
|
26-01-13 [21:33:12] [INFO] ✓ 设备初始化完成
|
||||||
|
26-01-13 [21:33:12] [INFO] 【步骤 1.5/4】物料搜寻确认已在初始化过程中完成
|
||||||
|
26-01-13 [21:33:12] [INFO] 【步骤 2/4】切换自动模式...
|
||||||
|
26-01-13 [21:33:15] [INFO] ✓ 切换自动模式完成
|
||||||
|
26-01-13 [21:33:15] [INFO] 【步骤 3/4】启动设备...
|
||||||
|
26-01-13 [21:33:18] [INFO] ✓ 启动设备完成
|
||||||
|
26-01-13 [21:33:18] [INFO] 组合操作完成:设备已成功初始化、确认物料搜寻、切换自动模式并启动
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术要点
|
||||||
|
|
||||||
|
### 1. 脉冲模式按钮操作
|
||||||
|
模拟真实按钮按压过程:
|
||||||
|
1. 写入 `True` (按下)
|
||||||
|
2. 保持 0.5 秒
|
||||||
|
3. 写入 `False` (释放)
|
||||||
|
4. 验证状态
|
||||||
|
|
||||||
|
### 2. 避免死锁
|
||||||
|
**问题:** PLC 在初始化过程中等待弹窗确认,而代码等待初始化完成
|
||||||
|
**解决:** 在初始化等待循环中实时检测弹窗,一旦出现立即处理
|
||||||
|
|
||||||
|
### 3. 超时保护
|
||||||
|
- 弹窗检测超时:30 秒(在 `_handle_material_search_dialog` 中)
|
||||||
|
- 初始化超时:120 秒(在 `func_pack_device_init_auto_start_combined` 中)
|
||||||
|
|
||||||
|
### 4. PyModbus 3.x API 兼容
|
||||||
|
所有 Modbus 操作使用 keyword arguments:
|
||||||
|
```python
|
||||||
|
# 读取
|
||||||
|
client.read_coils(address=5176, count=1)
|
||||||
|
|
||||||
|
# 写入
|
||||||
|
client.write_coil(address=5184, value=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 向后兼容性
|
||||||
|
|
||||||
|
### 保留的原有函数
|
||||||
|
- `func_pack_device_init()` - 单独的初始化函数,不包含弹窗处理
|
||||||
|
- 仍可在 YAML 中通过 `auto-func_pack_device_init` 调用
|
||||||
|
- 用于不需要自动处理弹窗的场景
|
||||||
|
|
||||||
|
### 新增的功能
|
||||||
|
- 在 `func_pack_device_init_auto_start_combined()` 中集成弹窗处理
|
||||||
|
- 通过参数控制,默认行为与之前兼容(点击"否")
|
||||||
|
|
||||||
|
## 验证测试
|
||||||
|
|
||||||
|
### 测试场景
|
||||||
|
|
||||||
|
#### 场景 1:默认参数(不启用物料搜寻)
|
||||||
|
```bash
|
||||||
|
# 调用时不传参数
|
||||||
|
BatteryStation/auto-func_pack_device_init_auto_start_combined: {}
|
||||||
|
```
|
||||||
|
**预期结果:**
|
||||||
|
- ✅ 检测到弹窗
|
||||||
|
- ✅ 自动点击"否"按钮
|
||||||
|
- ✅ 初始化完成并启动成功
|
||||||
|
|
||||||
|
#### 场景 2:启用物料搜寻
|
||||||
|
```bash
|
||||||
|
# 设置 material_search_enable=true
|
||||||
|
BatteryStation/auto-func_pack_device_init_auto_start_combined:
|
||||||
|
goal:
|
||||||
|
material_search_enable: true
|
||||||
|
```
|
||||||
|
**预期结果:**
|
||||||
|
- ✅ 检测到弹窗
|
||||||
|
- ✅ 自动点击"是"按钮
|
||||||
|
- ✅ 初始化完成并启动成功
|
||||||
|
|
||||||
|
### 实际测试结果
|
||||||
|
|
||||||
|
**测试时间:** 2026-01-13 21:32:43
|
||||||
|
**测试参数:** `material_search_enable: false`
|
||||||
|
**测试结果:** ✅ 成功
|
||||||
|
|
||||||
|
**关键时间节点:**
|
||||||
|
- 21:33:05 - 检测到弹窗
|
||||||
|
- 21:33:05 - 按下"否"按钮
|
||||||
|
- 21:33:06 - 释放"否"按钮
|
||||||
|
- 21:33:07 - 弹窗处理完成
|
||||||
|
- 21:33:08 - 初始化状态完成
|
||||||
|
- 21:33:18 - 整个流程完成
|
||||||
|
|
||||||
|
**总耗时:** 约 35 秒(包含初始化全过程)
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **CSV 配置依赖**
|
||||||
|
- 确保 `coin_cell_assembly_b.csv` 包含 69-71 行的 coil 配置
|
||||||
|
- 地址转换逻辑:`modbus_addr = (csv_addr // 10) * 8 + (csv_addr % 10)`
|
||||||
|
|
||||||
|
2. **默认行为**
|
||||||
|
- 默认 `material_search_enable=false`,即不启用物料搜寻
|
||||||
|
- 如需启用,必须显式设置为 `true`
|
||||||
|
|
||||||
|
3. **日志级别**
|
||||||
|
- 弹窗检测过程中的 `waiting for init_cmd` 使用 DEBUG 级别
|
||||||
|
- 关键操作(检测到弹窗、按钮操作)使用 INFO 级别
|
||||||
|
|
||||||
|
4. **原有函数保留**
|
||||||
|
- `func_pack_device_init()` 仍然可用,但不包含弹窗处理
|
||||||
|
- 如果单独调用此函数,仍需手动处理弹窗
|
||||||
|
|
||||||
|
## 文件清单
|
||||||
|
|
||||||
|
### 修改的文件
|
||||||
|
1. `d:\UniLabdev\Uni-Lab-OS\unilabos\devices\workstation\coin_cell_assembly\coin_cell_assembly.py`
|
||||||
|
- 新增 `_handle_material_search_dialog()` 方法
|
||||||
|
- 修改 `func_pack_device_init_auto_start_combined()` 函数
|
||||||
|
|
||||||
|
2. `d:\UniLabdev\Uni-Lab-OS\unilabos\registry\devices\coin_cell_workstation.yaml`
|
||||||
|
- 更新 `auto-func_pack_device_init_auto_start_combined` 配置
|
||||||
|
- 添加 `material_search_enable` 参数说明
|
||||||
|
|
||||||
|
3. `d:\UniLabdev\Uni-Lab-OS\unilabos\devices\workstation\coin_cell_assembly\coin_cell_assembly_b.csv`
|
||||||
|
- 第 69-71 行添加三个 coil 配置
|
||||||
|
|
||||||
|
### 创建的测试文件(已删除)
|
||||||
|
1. `test_material_search_dialog.py` - 物料搜寻弹窗测试脚本
|
||||||
|
2. `test_coin_cell_reset.py` - 复位功能测试(更新为 pymodbus 3.x)
|
||||||
|
3. `test_unilab_interact.py` - Unilab 交互测试(更新为 pymodbus 3.x)
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
本次更新成功实现了设备初始化过程中物料搜寻确认弹窗的自动化处理,主要优势:
|
||||||
|
|
||||||
|
✅ **全自动化** - 无需人工干预
|
||||||
|
✅ **参数可配** - 灵活控制是否启用物料搜寻
|
||||||
|
✅ **实时检测** - 在初始化等待循环中检测,避免死锁
|
||||||
|
✅ **脉冲模式** - 模拟真实按钮操作
|
||||||
|
✅ **向后兼容** - 保留原有函数,不影响现有流程
|
||||||
|
✅ **完整日志** - 详细记录每一步操作
|
||||||
|
✅ **超时保护** - 防止无限等待
|
||||||
|
|
||||||
|
该功能已通过实际测试验证,可投入生产使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档版本:** 1.0
|
||||||
|
**创建日期:** 2026-01-13
|
||||||
|
**作者:** Antigravity AI Assistant
|
||||||
|
**最后更新:** 2026-01-13 21:36
|
||||||
@@ -0,0 +1,645 @@
|
|||||||
|
"""
|
||||||
|
纽扣电池组装工作站物料类定义
|
||||||
|
Button Battery Assembly Station Resource Classes
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
from typing import Any, Dict, List, Optional, TypedDict, Union, cast
|
||||||
|
|
||||||
|
from pylabrobot.resources.coordinate import Coordinate
|
||||||
|
from pylabrobot.resources.container import Container
|
||||||
|
from pylabrobot.resources.deck import Deck
|
||||||
|
from pylabrobot.resources.itemized_resource import ItemizedResource
|
||||||
|
from pylabrobot.resources.resource import Resource
|
||||||
|
from pylabrobot.resources.resource_stack import ResourceStack
|
||||||
|
from pylabrobot.resources.tip_rack import TipRack, TipSpot
|
||||||
|
from pylabrobot.resources.trash import Trash
|
||||||
|
from pylabrobot.resources.utils import create_ordered_items_2d
|
||||||
|
|
||||||
|
from unilabos.resources.battery.magazine import MagazineHolder_4_Cathode, MagazineHolder_6_Cathode, MagazineHolder_6_Anode, MagazineHolder_6_Battery
|
||||||
|
from unilabos.resources.battery.bottle_carriers import YIHUA_Electrolyte_12VialCarrier
|
||||||
|
from unilabos.resources.battery.electrode_sheet import ElectrodeSheet
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: 这个应该只能放一个极片
|
||||||
|
class MaterialHoleState(TypedDict):
|
||||||
|
diameter: int
|
||||||
|
depth: int
|
||||||
|
max_sheets: int
|
||||||
|
info: Optional[str] # 附加信息
|
||||||
|
|
||||||
|
class MaterialHole(Resource):
|
||||||
|
"""料板洞位类"""
|
||||||
|
children: List[ElectrodeSheet] = []
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
category: str = "material_hole",
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
category=category,
|
||||||
|
)
|
||||||
|
self._unilabos_state: MaterialHoleState = MaterialHoleState(
|
||||||
|
diameter=20,
|
||||||
|
depth=10,
|
||||||
|
max_sheets=1,
|
||||||
|
info=None
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_all_sheet_info(self):
|
||||||
|
info_list = []
|
||||||
|
for sheet in self.children:
|
||||||
|
info_list.append(sheet._unilabos_state["info"])
|
||||||
|
return info_list
|
||||||
|
|
||||||
|
#这个函数函数好像没用,一般不会集中赋值质量
|
||||||
|
def set_all_sheet_mass(self):
|
||||||
|
for sheet in self.children:
|
||||||
|
sheet._unilabos_state["mass"] = 0.5 # 示例:设置质量为0.5g
|
||||||
|
|
||||||
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
|
"""格式不变"""
|
||||||
|
super().load_state(state)
|
||||||
|
self._unilabos_state = state
|
||||||
|
|
||||||
|
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""格式不变"""
|
||||||
|
data = super().serialize_state()
|
||||||
|
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||||
|
return data
|
||||||
|
#移动极片前先取出对象
|
||||||
|
def get_sheet_with_name(self, name: str) -> Optional[ElectrodeSheet]:
|
||||||
|
for sheet in self.children:
|
||||||
|
if sheet.name == name:
|
||||||
|
return sheet
|
||||||
|
return None
|
||||||
|
|
||||||
|
def has_electrode_sheet(self) -> bool:
|
||||||
|
"""检查洞位是否有极片"""
|
||||||
|
return len(self.children) > 0
|
||||||
|
|
||||||
|
def assign_child_resource(
|
||||||
|
self,
|
||||||
|
resource: ElectrodeSheet,
|
||||||
|
location: Optional[Coordinate],
|
||||||
|
reassign: bool = True,
|
||||||
|
):
|
||||||
|
"""放置极片"""
|
||||||
|
# TODO: 这里要改,diameter找不到,加入._unilabos_state后应该没问题
|
||||||
|
#if resource._unilabos_state["diameter"] > self._unilabos_state["diameter"]:
|
||||||
|
# raise ValueError(f"极片直径 {resource._unilabos_state['diameter']} 超过洞位直径 {self._unilabos_state['diameter']}")
|
||||||
|
#if len(self.children) >= self._unilabos_state["max_sheets"]:
|
||||||
|
# raise ValueError(f"洞位已满,无法放置更多极片")
|
||||||
|
super().assign_child_resource(resource, location, reassign)
|
||||||
|
|
||||||
|
# 根据children的编号取物料对象。
|
||||||
|
def get_electrode_sheet_info(self, index: int) -> ElectrodeSheet:
|
||||||
|
return self.children[index]
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialPlateState(TypedDict):
|
||||||
|
hole_spacing_x: float
|
||||||
|
hole_spacing_y: float
|
||||||
|
hole_diameter: float
|
||||||
|
info: Optional[str] # 附加信息
|
||||||
|
|
||||||
|
class MaterialPlate(ItemizedResource[MaterialHole]):
|
||||||
|
"""料板类 - 4x4个洞位,每个洞位放1个极片"""
|
||||||
|
|
||||||
|
children: List[MaterialHole]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
ordered_items: Optional[Dict[str, MaterialHole]] = None,
|
||||||
|
ordering: Optional[OrderedDict[str, str]] = None,
|
||||||
|
category: str = "material_plate",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
fill: bool = False
|
||||||
|
):
|
||||||
|
"""初始化料板
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 料板名称
|
||||||
|
size_x: 长度 (mm)
|
||||||
|
size_y: 宽度 (mm)
|
||||||
|
size_z: 高度 (mm)
|
||||||
|
hole_diameter: 洞直径 (mm)
|
||||||
|
hole_depth: 洞深度 (mm)
|
||||||
|
hole_spacing_x: X方向洞位间距 (mm)
|
||||||
|
hole_spacing_y: Y方向洞位间距 (mm)
|
||||||
|
number: 编号
|
||||||
|
category: 类别
|
||||||
|
model: 型号
|
||||||
|
"""
|
||||||
|
self._unilabos_state: MaterialPlateState = MaterialPlateState(
|
||||||
|
hole_spacing_x=24.0,
|
||||||
|
hole_spacing_y=24.0,
|
||||||
|
hole_diameter=20.0,
|
||||||
|
info="",
|
||||||
|
)
|
||||||
|
# 创建4x4的洞位
|
||||||
|
# TODO: 这里要改,对应不同形状
|
||||||
|
holes = create_ordered_items_2d(
|
||||||
|
klass=MaterialHole,
|
||||||
|
num_items_x=4,
|
||||||
|
num_items_y=4,
|
||||||
|
dx=(size_x - 4 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中
|
||||||
|
dy=(size_y - 4 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中
|
||||||
|
dz=size_z,
|
||||||
|
item_dx=self._unilabos_state["hole_spacing_x"],
|
||||||
|
item_dy=self._unilabos_state["hole_spacing_y"],
|
||||||
|
size_x = 16,
|
||||||
|
size_y = 16,
|
||||||
|
size_z = 16,
|
||||||
|
)
|
||||||
|
if fill:
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
ordered_items=holes,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
ordered_items=ordered_items,
|
||||||
|
ordering=ordering,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_locations(self):
|
||||||
|
# TODO:调多次相加
|
||||||
|
holes = create_ordered_items_2d(
|
||||||
|
klass=MaterialHole,
|
||||||
|
num_items_x=4,
|
||||||
|
num_items_y=4,
|
||||||
|
dx=(self._size_x - 3 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中
|
||||||
|
dy=(self._size_y - 3 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中
|
||||||
|
dz=self._size_z,
|
||||||
|
item_dx=self._unilabos_state["hole_spacing_x"],
|
||||||
|
item_dy=self._unilabos_state["hole_spacing_y"],
|
||||||
|
size_x = 1,
|
||||||
|
size_y = 1,
|
||||||
|
size_z = 1,
|
||||||
|
)
|
||||||
|
for item, original_item in zip(holes.items(), self.children):
|
||||||
|
original_item.location = item[1].location
|
||||||
|
|
||||||
|
|
||||||
|
class PlateSlot(ResourceStack):
|
||||||
|
"""板槽位类 - 1个槽上能堆放8个板,移板只能操作最上方的板"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
max_plates: int = 8,
|
||||||
|
category: str = "plate_slot",
|
||||||
|
model: Optional[str] = None
|
||||||
|
):
|
||||||
|
"""初始化板槽位
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 槽位名称
|
||||||
|
max_plates: 最大板数量
|
||||||
|
category: 类别
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
direction="z", # Z方向堆叠
|
||||||
|
resources=[],
|
||||||
|
)
|
||||||
|
self.max_plates = max_plates
|
||||||
|
self.category = category
|
||||||
|
|
||||||
|
def can_add_plate(self) -> bool:
|
||||||
|
"""检查是否可以添加板"""
|
||||||
|
return len(self.children) < self.max_plates
|
||||||
|
|
||||||
|
def add_plate(self, plate: MaterialPlate) -> None:
|
||||||
|
"""添加料板"""
|
||||||
|
if not self.can_add_plate():
|
||||||
|
raise ValueError(f"槽位 {self.name} 已满,无法添加更多板")
|
||||||
|
self.assign_child_resource(plate)
|
||||||
|
|
||||||
|
def get_top_plate(self) -> MaterialPlate:
|
||||||
|
"""获取最上方的板"""
|
||||||
|
if len(self.children) == 0:
|
||||||
|
raise ValueError(f"槽位 {self.name} 为空")
|
||||||
|
return cast(MaterialPlate, self.get_top_item())
|
||||||
|
|
||||||
|
def take_top_plate(self) -> MaterialPlate:
|
||||||
|
"""取出最上方的板"""
|
||||||
|
top_plate = self.get_top_plate()
|
||||||
|
self.unassign_child_resource(top_plate)
|
||||||
|
return top_plate
|
||||||
|
|
||||||
|
def can_access_for_picking(self) -> bool:
|
||||||
|
"""检查是否可以进行取料操作(只有最上方的板能进行取料操作)"""
|
||||||
|
return len(self.children) > 0
|
||||||
|
|
||||||
|
def serialize(self) -> dict:
|
||||||
|
return {
|
||||||
|
**super().serialize(),
|
||||||
|
"max_plates": self.max_plates,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#是一种类型注解,不用self
|
||||||
|
class BatteryState(TypedDict):
|
||||||
|
"""电池状态字典"""
|
||||||
|
diameter: float
|
||||||
|
height: float
|
||||||
|
assembly_pressure: float
|
||||||
|
electrolyte_volume: float
|
||||||
|
electrolyte_name: str
|
||||||
|
|
||||||
|
class Battery(Resource):
|
||||||
|
"""电池类 - 可容纳极片"""
|
||||||
|
children: List[ElectrodeSheet] = []
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x=1,
|
||||||
|
size_y=1,
|
||||||
|
size_z=1,
|
||||||
|
category: str = "battery",
|
||||||
|
):
|
||||||
|
"""初始化电池
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 电池名称
|
||||||
|
diameter: 直径 (mm)
|
||||||
|
height: 高度 (mm)
|
||||||
|
max_volume: 最大容量 (μL)
|
||||||
|
barcode: 二维码编号
|
||||||
|
category: 类别
|
||||||
|
model: 型号
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=1,
|
||||||
|
size_y=1,
|
||||||
|
size_z=1,
|
||||||
|
category=category,
|
||||||
|
)
|
||||||
|
self._unilabos_state: BatteryState = BatteryState(
|
||||||
|
diameter = 1.0,
|
||||||
|
height = 1.0,
|
||||||
|
assembly_pressure = 1.0,
|
||||||
|
electrolyte_volume = 1.0,
|
||||||
|
electrolyte_name = "DP001"
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_electrolyte_with_bottle(self, bottle: Bottle) -> bool:
|
||||||
|
to_add_name = bottle._unilabos_state["electrolyte_name"]
|
||||||
|
if bottle.aspirate_electrolyte(10):
|
||||||
|
if self.add_electrolyte(to_add_name, 10):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
bottle._unilabos_state["electrolyte_volume"] += 10
|
||||||
|
|
||||||
|
def set_electrolyte(self, name: str, volume: float) -> None:
|
||||||
|
"""设置电解液信息"""
|
||||||
|
self._unilabos_state["electrolyte_name"] = name
|
||||||
|
self._unilabos_state["electrolyte_volume"] = volume
|
||||||
|
#这个应该没用,不会有加了后再加的事情
|
||||||
|
def add_electrolyte(self, name: str, volume: float) -> bool:
|
||||||
|
"""添加电解液信息"""
|
||||||
|
if name != self._unilabos_state["electrolyte_name"]:
|
||||||
|
return False
|
||||||
|
self._unilabos_state["electrolyte_volume"] += volume
|
||||||
|
|
||||||
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
|
"""格式不变"""
|
||||||
|
super().load_state(state)
|
||||||
|
self._unilabos_state = state
|
||||||
|
|
||||||
|
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""格式不变"""
|
||||||
|
data = super().serialize_state()
|
||||||
|
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||||
|
return data
|
||||||
|
|
||||||
|
# 电解液作为属性放进去
|
||||||
|
|
||||||
|
class BatteryPressSlotState(TypedDict):
|
||||||
|
"""电池状态字典"""
|
||||||
|
diameter: float =20.0
|
||||||
|
depth: float = 4.0
|
||||||
|
|
||||||
|
class BatteryPressSlot(Resource):
|
||||||
|
"""电池压制槽类 - 设备,可容纳一个电池"""
|
||||||
|
children: List[Battery] = []
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str = "BatteryPressSlot",
|
||||||
|
category: str = "battery_press_slot",
|
||||||
|
):
|
||||||
|
"""初始化电池压制槽
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 压制槽名称
|
||||||
|
diameter: 直径 (mm)
|
||||||
|
depth: 深度 (mm)
|
||||||
|
category: 类别
|
||||||
|
model: 型号
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=10,
|
||||||
|
size_y=12,
|
||||||
|
size_z=13,
|
||||||
|
category=category,
|
||||||
|
)
|
||||||
|
self._unilabos_state: BatteryPressSlotState = BatteryPressSlotState()
|
||||||
|
|
||||||
|
def has_battery(self) -> bool:
|
||||||
|
"""检查是否有电池"""
|
||||||
|
return len(self.children) > 0
|
||||||
|
|
||||||
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
|
"""格式不变"""
|
||||||
|
super().load_state(state)
|
||||||
|
self._unilabos_state = state
|
||||||
|
|
||||||
|
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""格式不变"""
|
||||||
|
data = super().serialize_state()
|
||||||
|
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def assign_child_resource(
|
||||||
|
self,
|
||||||
|
resource: Battery,
|
||||||
|
location: Optional[Coordinate],
|
||||||
|
reassign: bool = True,
|
||||||
|
):
|
||||||
|
"""放置极片"""
|
||||||
|
# TODO: 让高京看下槽位只有一个电池时是否这么写。
|
||||||
|
if self.has_battery():
|
||||||
|
raise ValueError(f"槽位已含有一个电池,无法再放置其他电池")
|
||||||
|
super().assign_child_resource(resource, location, reassign)
|
||||||
|
|
||||||
|
# 根据children的编号取物料对象。
|
||||||
|
def get_battery_info(self, index: int) -> Battery:
|
||||||
|
return self.children[0]
|
||||||
|
|
||||||
|
|
||||||
|
def TipBox64(
|
||||||
|
name: str,
|
||||||
|
size_x: float = 127.8,
|
||||||
|
size_y: float = 85.5,
|
||||||
|
size_z: float = 60.0,
|
||||||
|
category: str = "tip_rack",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""64孔枪头盒类"""
|
||||||
|
from pylabrobot.resources.tip import Tip
|
||||||
|
|
||||||
|
# 创建12x8=96个枪头位
|
||||||
|
def make_tip():
|
||||||
|
return Tip(
|
||||||
|
has_filter=False,
|
||||||
|
total_tip_length=20.0,
|
||||||
|
maximal_volume=1000, # 1mL
|
||||||
|
fitting_depth=8.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
tip_spots = create_ordered_items_2d(
|
||||||
|
klass=TipSpot,
|
||||||
|
num_items_x=12,
|
||||||
|
num_items_y=8,
|
||||||
|
dx=8.0,
|
||||||
|
dy=8.0,
|
||||||
|
dz=0.0,
|
||||||
|
item_dx=9.0,
|
||||||
|
item_dy=9.0,
|
||||||
|
size_x=10,
|
||||||
|
size_y=10,
|
||||||
|
size_z=0.0,
|
||||||
|
make_tip=make_tip,
|
||||||
|
)
|
||||||
|
idx_available = list(range(0, 32)) + list(range(64, 96))
|
||||||
|
tip_spots_available = {k: v for i, (k, v) in enumerate(tip_spots.items()) if i in idx_available}
|
||||||
|
tip_rack = TipRack(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
# ordered_items=tip_spots_available,
|
||||||
|
ordered_items=tip_spots,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
with_tips=False,
|
||||||
|
)
|
||||||
|
tip_rack.set_tip_state([True]*32 + [False]*32 + [True]*32) # 前32和后32个有枪头,中间32个无枪头
|
||||||
|
return tip_rack
|
||||||
|
|
||||||
|
|
||||||
|
class WasteTipBoxstate(TypedDict):
|
||||||
|
""""废枪头盒状态字典"""
|
||||||
|
max_tips: int = 100
|
||||||
|
tip_count: int = 0
|
||||||
|
|
||||||
|
#枪头不是一次性的(同一溶液则反复使用),根据寄存器判断
|
||||||
|
class WasteTipBox(Trash):
|
||||||
|
"""废枪头盒类 - 100个枪头容量"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float = 127.8,
|
||||||
|
size_y: float = 85.5,
|
||||||
|
size_z: float = 60.0,
|
||||||
|
material_z_thickness=0,
|
||||||
|
max_volume=float("inf"),
|
||||||
|
category="trash",
|
||||||
|
model=None,
|
||||||
|
compute_volume_from_height=None,
|
||||||
|
compute_height_from_volume=None,
|
||||||
|
):
|
||||||
|
"""初始化废枪头盒
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 废枪头盒名称
|
||||||
|
size_x: 长度 (mm)
|
||||||
|
size_y: 宽度 (mm)
|
||||||
|
size_z: 高度 (mm)
|
||||||
|
max_tips: 最大枪头容量
|
||||||
|
category: 类别
|
||||||
|
model: 型号
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate()
|
||||||
|
|
||||||
|
def add_tip(self) -> None:
|
||||||
|
"""添加废枪头"""
|
||||||
|
if self._unilabos_state["tip_count"] >= self._unilabos_state["max_tips"]:
|
||||||
|
raise ValueError(f"废枪头盒 {self.name} 已满")
|
||||||
|
self._unilabos_state["tip_count"] += 1
|
||||||
|
|
||||||
|
def get_tip_count(self) -> int:
|
||||||
|
"""获取枪头数量"""
|
||||||
|
return self._unilabos_state["tip_count"]
|
||||||
|
|
||||||
|
def empty(self) -> None:
|
||||||
|
"""清空废枪头盒"""
|
||||||
|
self._unilabos_state["tip_count"] = 0
|
||||||
|
|
||||||
|
|
||||||
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
|
"""格式不变"""
|
||||||
|
super().load_state(state)
|
||||||
|
self._unilabos_state = state
|
||||||
|
|
||||||
|
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""格式不变"""
|
||||||
|
data = super().serialize_state()
|
||||||
|
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class CoincellDeck(Deck):
|
||||||
|
"""纽扣电池组装工作站台面类"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str = "coin_cell_deck",
|
||||||
|
size_x: float = 1450.0, # 1m
|
||||||
|
size_y: float = 1450.0, # 1m
|
||||||
|
size_z: float = 100.0, # 0.9m
|
||||||
|
origin: Coordinate = Coordinate(-2200, 0, 0),
|
||||||
|
category: str = "coin_cell_deck",
|
||||||
|
setup: bool = False, # 是否自动执行 setup
|
||||||
|
):
|
||||||
|
"""初始化纽扣电池组装工作站台面
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 台面名称
|
||||||
|
size_x: 长度 (mm) - 1m
|
||||||
|
size_y: 宽度 (mm) - 1m
|
||||||
|
size_z: 高度 (mm) - 0.9m
|
||||||
|
origin: 原点坐标
|
||||||
|
category: 类别
|
||||||
|
setup: 是否自动执行 setup 配置标准布局
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=1450.0,
|
||||||
|
size_y=1450.0,
|
||||||
|
size_z=100.0,
|
||||||
|
origin=origin,
|
||||||
|
)
|
||||||
|
if setup:
|
||||||
|
self.setup()
|
||||||
|
|
||||||
|
def setup(self) -> None:
|
||||||
|
"""设置工作站的标准布局 - 包含子弹夹、料盘、瓶架等完整配置"""
|
||||||
|
# ====================================== 子弹夹 ============================================
|
||||||
|
|
||||||
|
# 正极片(4个洞位,2x2布局)
|
||||||
|
zhengji_zip = MagazineHolder_4_Cathode("正极&铝箔弹夹")
|
||||||
|
self.assign_child_resource(zhengji_zip, Coordinate(x=402.0, y=830.0, z=0))
|
||||||
|
|
||||||
|
# 正极壳、平垫片(6个洞位,2x2+2布局)
|
||||||
|
zhengjike_zip = MagazineHolder_6_Cathode("正极壳&平垫片弹夹")
|
||||||
|
self.assign_child_resource(zhengjike_zip, Coordinate(x=566.0, y=272.0, z=0))
|
||||||
|
|
||||||
|
# 负极壳、弹垫片(6个洞位,2x2+2布局)
|
||||||
|
fujike_zip = MagazineHolder_6_Anode("负极壳&弹垫片弹夹")
|
||||||
|
self.assign_child_resource(fujike_zip, Coordinate(x=474.0, y=276.0, z=0))
|
||||||
|
|
||||||
|
# 成品弹夹(6个洞位,3x2布局)
|
||||||
|
chengpindanjia_zip = MagazineHolder_6_Battery("成品弹夹")
|
||||||
|
self.assign_child_resource(chengpindanjia_zip, Coordinate(x=260.0, y=156.0, z=0))
|
||||||
|
|
||||||
|
# ====================================== 物料板 ============================================
|
||||||
|
# 创建物料板(料盘carrier)- 4x4布局
|
||||||
|
# 负极料盘
|
||||||
|
fujiliaopan = MaterialPlate(name="负极料盘", size_x=120, size_y=100, size_z=10.0, fill=True)
|
||||||
|
self.assign_child_resource(fujiliaopan, Coordinate(x=708.0, y=794.0, z=0))
|
||||||
|
# for i in range(16):
|
||||||
|
# fujipian = ElectrodeSheet(name=f"{fujiliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
|
||||||
|
# fujiliaopan.children[i].assign_child_resource(fujipian, location=None)
|
||||||
|
|
||||||
|
# 隔膜料盘
|
||||||
|
gemoliaopan = MaterialPlate(name="隔膜料盘", size_x=120, size_y=100, size_z=10.0, fill=True)
|
||||||
|
self.assign_child_resource(gemoliaopan, Coordinate(x=718.0, y=918.0, z=0))
|
||||||
|
# for i in range(16):
|
||||||
|
# gemopian = ElectrodeSheet(name=f"{gemoliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
|
||||||
|
# gemoliaopan.children[i].assign_child_resource(gemopian, location=None)
|
||||||
|
|
||||||
|
# ====================================== 瓶架、移液枪 ============================================
|
||||||
|
# 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒
|
||||||
|
# 奔耀上料5ml分液瓶小板 - 由奔曜跨站转运而来,不单独写,但是这里应该有一个堆栈用于摆放分液瓶小板
|
||||||
|
|
||||||
|
# bottle_rack_3x4 = BottleRack(
|
||||||
|
# name="bottle_rack_3x4",
|
||||||
|
# size_x=210.0,
|
||||||
|
# size_y=140.0,
|
||||||
|
# size_z=100.0,
|
||||||
|
# num_items_x=2,
|
||||||
|
# num_items_y=4,
|
||||||
|
# position_spacing=35.0,
|
||||||
|
# orientation="vertical",
|
||||||
|
# )
|
||||||
|
# self.assign_child_resource(bottle_rack_3x4, Coordinate(x=1542.0, y=717.0, z=0))
|
||||||
|
|
||||||
|
# 电解液缓存位 - 6x2布局
|
||||||
|
bottle_rack_6x2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2")
|
||||||
|
self.assign_child_resource(bottle_rack_6x2, Coordinate(x=1050.0, y=358.0, z=0))
|
||||||
|
# 电解液回收位6x2
|
||||||
|
bottle_rack_6x2_2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2_2")
|
||||||
|
self.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=914.0, y=358.0, z=0))
|
||||||
|
|
||||||
|
tip_box = TipBox64(name="tip_box_64")
|
||||||
|
self.assign_child_resource(tip_box, Coordinate(x=782.0, y=514.0, z=0))
|
||||||
|
|
||||||
|
waste_tip_box = WasteTipBox(name="waste_tip_box")
|
||||||
|
self.assign_child_resource(waste_tip_box, Coordinate(x=778.0, y=622.0, z=0))
|
||||||
|
|
||||||
|
|
||||||
|
def YH_Deck(name=""):
|
||||||
|
cd = CoincellDeck(name=name)
|
||||||
|
cd.setup()
|
||||||
|
return cd
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
deck = create_coin_cell_deck()
|
||||||
|
print(deck)
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,133 @@
|
|||||||
|
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
|
||||||
|
COIL_SYS_START_CMD,BOOL,,,,coil,8010,
|
||||||
|
COIL_SYS_STOP_CMD,BOOL,,,,coil,8020,
|
||||||
|
COIL_SYS_RESET_CMD,BOOL,,,,coil,8030,
|
||||||
|
COIL_SYS_HAND_CMD,BOOL,,,,coil,8040,
|
||||||
|
COIL_SYS_AUTO_CMD,BOOL,,,,coil,8050,
|
||||||
|
COIL_SYS_INIT_CMD,BOOL,,,,coil,8060,
|
||||||
|
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,8700,
|
||||||
|
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,8710,unilab_rec_msg_succ_cmd
|
||||||
|
COIL_SYS_START_STATUS,BOOL,,,,coil,8210,
|
||||||
|
COIL_SYS_STOP_STATUS,BOOL,,,,coil,8220,
|
||||||
|
COIL_SYS_RESET_STATUS,BOOL,,,,coil,8230,
|
||||||
|
COIL_SYS_HAND_STATUS,BOOL,,,,coil,8240,
|
||||||
|
COIL_SYS_AUTO_STATUS,BOOL,,,,coil,8250,
|
||||||
|
COIL_SYS_INIT_STATUS,BOOL,,,,coil,8260,
|
||||||
|
COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,8500,
|
||||||
|
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,8510,request_send_msg_status
|
||||||
|
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,11000,
|
||||||
|
REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,11002,unilab_send_msg_electrolyte_num
|
||||||
|
REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,11004,unilab_send_msg_electrolyte_vol
|
||||||
|
REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,11006,unilab_send_msg_assembly_type
|
||||||
|
REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,11008,unilab_send_msg_assembly_pressure
|
||||||
|
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,10000,data_assembly_coin_cell_num
|
||||||
|
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,10002,data_open_circuit_voltage
|
||||||
|
REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,10004,
|
||||||
|
REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,10006,
|
||||||
|
REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,10008,
|
||||||
|
REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,10010,data_pole_weight
|
||||||
|
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,10012,data_assembly_time
|
||||||
|
REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,10014,data_assembly_pressure
|
||||||
|
REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,10016,data_electrolyte_volume
|
||||||
|
REG_DATA_COIN_NUM,INT16,,,,hold_register,10018,data_coin_num
|
||||||
|
REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,10020,data_electrolyte_code()
|
||||||
|
REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,10030,data_coin_cell_code()
|
||||||
|
REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,12004,data_stack_vision_code()
|
||||||
|
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,10050,data_glove_box_pressure
|
||||||
|
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,10052,data_glove_box_water_content
|
||||||
|
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,10054,data_glove_box_o2_content
|
||||||
|
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8720,
|
||||||
|
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8520,
|
||||||
|
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,496,
|
||||||
|
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,10000,
|
||||||
|
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730,
|
||||||
|
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530,
|
||||||
|
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8
|
||||||
|
REG_UNILAB_INTERACT,BOOL,,,,coil,8450,
|
||||||
|
,,,,,coil,8320,
|
||||||
|
COIL_ALUMINUM_FOIL,BOOL,,,,coil,8340,
|
||||||
|
REG_MSG_NE_PLATE_MATRIX,INT16,,,,hold_register,440,
|
||||||
|
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,,,hold_register,450,
|
||||||
|
REG_MSG_TIP_BOX_MATRIX,INT16,,,,hold_register,480,
|
||||||
|
REG_MSG_NE_PLATE_NUM,INT16,,,,hold_register,443,
|
||||||
|
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,,,hold_register,453,
|
||||||
|
REG_MSG_PRESS_MODE,BOOL,,,,coil,8360,
|
||||||
|
,BOOL,,,,coil,8300,
|
||||||
|
,BOOL,,,,coil,8310,
|
||||||
|
COIL_GB_L_IGNORE_CMD,BOOL,,,,coil,8320,
|
||||||
|
COIL_GB_R_IGNORE_CMD,BOOL,,,,coil,8420,
|
||||||
|
,BOOL,,,,coil,8350,
|
||||||
|
COIL_ELECTROLYTE_DUAL_DROP_MODE,BOOL,,,,coil,8370,
|
||||||
|
,BOOL,,,,coil,8380,
|
||||||
|
,BOOL,,,,coil,8390,
|
||||||
|
,BOOL,,,,coil,8400,
|
||||||
|
,BOOL,,,,coil,8410,
|
||||||
|
REG_MSG_DUAL_DROP_FIRST_VOLUME,INT16,,,,hold_register,4001,
|
||||||
|
COIL_DUAL_DROP_SUCTION_TIMING,BOOL,,,,coil,8430,
|
||||||
|
COIL_DUAL_DROP_START_TIMING,BOOL,,,,coil,8470,
|
||||||
|
REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,,,coil,8460,
|
||||||
|
COIL_MATERIAL_SEARCH_DIALOG_APPEAR,BOOL,,,,coil,6470,
|
||||||
|
COIL_MATERIAL_SEARCH_CONFIRM_YES,BOOL,,,,coil,6480,
|
||||||
|
COIL_MATERIAL_SEARCH_CONFIRM_NO,BOOL,,,,coil,6490,
|
||||||
|
COIL_ALARM_100_SYSTEM_ERROR,BOOL,,,,coil,1000,异常100-系统异常
|
||||||
|
COIL_ALARM_101_EMERGENCY_STOP,BOOL,,,,coil,1010,异常101-急停
|
||||||
|
COIL_ALARM_111_GLOVEBOX_EMERGENCY_STOP,BOOL,,,,coil,1110,异常111-手套箱急停
|
||||||
|
COIL_ALARM_112_GLOVEBOX_GRATING_BLOCKED,BOOL,,,,coil,1120,异常112-手套箱内光栅遮挡
|
||||||
|
COIL_ALARM_160_PIPETTE_TIP_SHORTAGE,BOOL,,,,coil,1600,异常160-移液枪头缺料
|
||||||
|
COIL_ALARM_161_POSITIVE_SHELL_SHORTAGE,BOOL,,,,coil,1610,异常161-正极壳缺料
|
||||||
|
COIL_ALARM_162_ALUMINUM_FOIL_SHORTAGE,BOOL,,,,coil,1620,异常162-铝箔垫缺料
|
||||||
|
COIL_ALARM_163_POSITIVE_PLATE_SHORTAGE,BOOL,,,,coil,1630,异常163-正极片缺料
|
||||||
|
COIL_ALARM_164_SEPARATOR_SHORTAGE,BOOL,,,,coil,1640,异常164-隔膜缺料
|
||||||
|
COIL_ALARM_165_NEGATIVE_PLATE_SHORTAGE,BOOL,,,,coil,1650,异常165-负极片缺料
|
||||||
|
COIL_ALARM_166_FLAT_WASHER_SHORTAGE,BOOL,,,,coil,1660,异常166-平垫缺料
|
||||||
|
COIL_ALARM_167_SPRING_WASHER_SHORTAGE,BOOL,,,,coil,1670,异常167-弹垫缺料
|
||||||
|
COIL_ALARM_168_NEGATIVE_SHELL_SHORTAGE,BOOL,,,,coil,1680,异常168-负极壳缺料
|
||||||
|
COIL_ALARM_169_FINISHED_BATTERY_FULL,BOOL,,,,coil,1690,异常169-成品电池满料
|
||||||
|
COIL_ALARM_201_SERVO_AXIS_01_ERROR,BOOL,,,,coil,2010,异常201-伺服轴01异常
|
||||||
|
COIL_ALARM_202_SERVO_AXIS_02_ERROR,BOOL,,,,coil,2020,异常202-伺服轴02异常
|
||||||
|
COIL_ALARM_203_SERVO_AXIS_03_ERROR,BOOL,,,,coil,2030,异常203-伺服轴03异常
|
||||||
|
COIL_ALARM_204_SERVO_AXIS_04_ERROR,BOOL,,,,coil,2040,异常204-伺服轴04异常
|
||||||
|
COIL_ALARM_205_SERVO_AXIS_05_ERROR,BOOL,,,,coil,2050,异常205-伺服轴05异常
|
||||||
|
COIL_ALARM_206_SERVO_AXIS_06_ERROR,BOOL,,,,coil,2060,异常206-伺服轴06异常
|
||||||
|
COIL_ALARM_207_SERVO_AXIS_07_ERROR,BOOL,,,,coil,2070,异常207-伺服轴07异常
|
||||||
|
COIL_ALARM_208_SERVO_AXIS_08_ERROR,BOOL,,,,coil,2080,异常208-伺服轴08异常
|
||||||
|
COIL_ALARM_209_SERVO_AXIS_09_ERROR,BOOL,,,,coil,2090,异常209-伺服轴09异常
|
||||||
|
COIL_ALARM_210_SERVO_AXIS_10_ERROR,BOOL,,,,coil,2100,异常210-伺服轴10异常
|
||||||
|
COIL_ALARM_211_SERVO_AXIS_11_ERROR,BOOL,,,,coil,2110,异常211-伺服轴11异常
|
||||||
|
COIL_ALARM_212_SERVO_AXIS_12_ERROR,BOOL,,,,coil,2120,异常212-伺服轴12异常
|
||||||
|
COIL_ALARM_213_SERVO_AXIS_13_ERROR,BOOL,,,,coil,2130,异常213-伺服轴13异常
|
||||||
|
COIL_ALARM_214_SERVO_AXIS_14_ERROR,BOOL,,,,coil,2140,异常214-伺服轴14异常
|
||||||
|
COIL_ALARM_250_OTHER_COMPONENT_ERROR,BOOL,,,,coil,2500,异常250-其他元件异常
|
||||||
|
COIL_ALARM_251_PIPETTE_COMM_ERROR,BOOL,,,,coil,2510,异常251-移液枪通讯异常
|
||||||
|
COIL_ALARM_252_PIPETTE_ALARM,BOOL,,,,coil,2520,异常252-移液枪报警
|
||||||
|
COIL_ALARM_256_ELECTRIC_GRIPPER_ERROR,BOOL,,,,coil,2560,异常256-电爪异常
|
||||||
|
COIL_ALARM_262_RB_UNKNOWN_POSITION_ERROR,BOOL,,,,coil,2620,异常262-RB报警:未知点位错误
|
||||||
|
COIL_ALARM_263_RB_XYZ_PARAM_LIMIT_ERROR,BOOL,,,,coil,2630,异常263-RB报警:X、Y、Z参数超限制
|
||||||
|
COIL_ALARM_264_RB_VISION_PARAM_ERROR,BOOL,,,,coil,2640,异常264-RB报警:视觉参数误差过大
|
||||||
|
COIL_ALARM_265_RB_NOZZLE_1_PICK_FAIL,BOOL,,,,coil,2650,异常265-RB报警:1#吸嘴取料失败
|
||||||
|
COIL_ALARM_266_RB_NOZZLE_2_PICK_FAIL,BOOL,,,,coil,2660,异常266-RB报警:2#吸嘴取料失败
|
||||||
|
COIL_ALARM_267_RB_NOZZLE_3_PICK_FAIL,BOOL,,,,coil,2670,异常267-RB报警:3#吸嘴取料失败
|
||||||
|
COIL_ALARM_268_RB_NOZZLE_4_PICK_FAIL,BOOL,,,,coil,2680,异常268-RB报警:4#吸嘴取料失败
|
||||||
|
COIL_ALARM_269_RB_TRAY_PICK_FAIL,BOOL,,,,coil,2690,异常269-RB报警:取物料盘失败
|
||||||
|
COIL_ALARM_280_RB_COLLISION_ERROR,BOOL,,,,coil,2800,异常280-RB碰撞异常
|
||||||
|
COIL_ALARM_290_VISION_SYSTEM_COMM_ERROR,BOOL,,,,coil,2900,异常290-视觉系统通讯异常
|
||||||
|
COIL_ALARM_291_VISION_ALIGNMENT_NG,BOOL,,,,coil,2910,异常291-视觉对位NG异常
|
||||||
|
COIL_ALARM_292_BARCODE_SCANNER_COMM_ERROR,BOOL,,,,coil,2920,异常292-扫码枪通讯异常
|
||||||
|
COIL_ALARM_310_OCV_TRANSFER_NOZZLE_SUCTION_ERROR,BOOL,,,,coil,3100,异常310-开电移载吸嘴吸真空异常
|
||||||
|
COIL_ALARM_311_OCV_TRANSFER_NOZZLE_BREAK_ERROR,BOOL,,,,coil,3110,异常311-开电移载吸嘴破真空异常
|
||||||
|
COIL_ALARM_312_WEIGHT_TRANSFER_NOZZLE_SUCTION_ERROR,BOOL,,,,coil,3120,异常312-称重移载吸嘴吸真空异常
|
||||||
|
COIL_ALARM_313_WEIGHT_TRANSFER_NOZZLE_BREAK_ERROR,BOOL,,,,coil,3130,异常313-称重移载吸嘴破真空异常
|
||||||
|
COIL_ALARM_340_OCV_NOZZLE_TRANSFER_CYLINDER_ERROR,BOOL,,,,coil,3400,异常340-开路电压吸嘴移载气缸异常
|
||||||
|
COIL_ALARM_342_OCV_NOZZLE_LIFT_CYLINDER_ERROR,BOOL,,,,coil,3420,异常342-开路电压吸嘴升降气缸异常
|
||||||
|
COIL_ALARM_344_OCV_CRIMPING_CYLINDER_ERROR,BOOL,,,,coil,3440,异常344-开路电压旋压气缸异常
|
||||||
|
COIL_ALARM_350_WEIGHT_NOZZLE_TRANSFER_CYLINDER_ERROR,BOOL,,,,coil,3500,异常350-称重吸嘴移载气缸异常
|
||||||
|
COIL_ALARM_352_WEIGHT_NOZZLE_LIFT_CYLINDER_ERROR,BOOL,,,,coil,3520,异常352-称重吸嘴升降气缸异常
|
||||||
|
COIL_ALARM_354_CLEANING_CLOTH_TRANSFER_CYLINDER_ERROR,BOOL,,,,coil,3540,异常354-清洗无尘布移载气缸异常
|
||||||
|
COIL_ALARM_356_CLEANING_CLOTH_PRESS_CYLINDER_ERROR,BOOL,,,,coil,3560,异常356-清洗无尘布压紧气缸异常
|
||||||
|
COIL_ALARM_360_ELECTROLYTE_BOTTLE_POSITION_CYLINDER_ERROR,BOOL,,,,coil,3600,异常360-电解液瓶定位气缸异常
|
||||||
|
COIL_ALARM_362_PIPETTE_TIP_BOX_POSITION_CYLINDER_ERROR,BOOL,,,,coil,3620,异常362-移液枪头盒定位气缸异常
|
||||||
|
COIL_ALARM_364_REAGENT_BOTTLE_GRIPPER_LIFT_CYLINDER_ERROR,BOOL,,,,coil,3640,异常364-试剂瓶夹爪升降气缸异常
|
||||||
|
COIL_ALARM_366_REAGENT_BOTTLE_GRIPPER_CYLINDER_ERROR,BOOL,,,,coil,3660,异常366-试剂瓶夹爪气缸异常
|
||||||
|
COIL_ALARM_370_PRESS_MODULE_BLOW_CYLINDER_ERROR,BOOL,,,,coil,3700,异常370-压制模块吹气气缸异常
|
||||||
|
COIL_ALARM_151_ELECTROLYTE_BOTTLE_POSITION_ERROR,BOOL,,,,coil,1510,异常151-电解液瓶定位在籍异常
|
||||||
|
COIL_ALARM_152_ELECTROLYTE_BOTTLE_CAP_ERROR,BOOL,,,,coil,1520,异常152-电解液瓶盖在籍异常
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -459,12 +459,12 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
# 验证必需字段
|
# 验证必需字段
|
||||||
if 'brand' in request_data:
|
if 'brand' in request_data:
|
||||||
if request_data['brand'] == "bioyond": # 奔曜
|
if request_data['brand'] == "bioyond": # 奔曜
|
||||||
error_msg = request_data["text"]
|
material_data = request_data["text"]
|
||||||
logger.info(f"收到奔曜错误处理报送: {error_msg}")
|
logger.info(f"收到奔曜物料变更报送: {material_data}")
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
success=True,
|
success=True,
|
||||||
message=f"错误处理报送已收到: {error_msg}",
|
message=f"物料变更报送已收到: {material_data}",
|
||||||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_msg.get('action_id', 'unknown')}",
|
acknowledgment_id=f"MATERIAL_{int(time.time() * 1000)}_{material_data.get('id', 'unknown')}",
|
||||||
data=None
|
data=None
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ def scan_directory(
|
|||||||
executor: ThreadPoolExecutor = None,
|
executor: ThreadPoolExecutor = None,
|
||||||
exclude_files: Optional[set] = None,
|
exclude_files: Optional[set] = None,
|
||||||
cache: Optional[Dict[str, Any]] = None,
|
cache: Optional[Dict[str, Any]] = None,
|
||||||
include_files: Optional[List[Union[str, Path]]] = None,
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Recursively scan .py files under *root_dir* for @device and @resource
|
Recursively scan .py files under *root_dir* for @device and @resource
|
||||||
@@ -165,7 +164,6 @@ def scan_directory(
|
|||||||
exclude_files: 要排除的文件名集合 (如 {"lab_resources.py"})
|
exclude_files: 要排除的文件名集合 (如 {"lab_resources.py"})
|
||||||
cache: Mutable cache dict (``load_scan_cache()`` result). Hits are read
|
cache: Mutable cache dict (``load_scan_cache()`` result). Hits are read
|
||||||
from here; misses are written back so the caller can persist later.
|
from here; misses are written back so the caller can persist later.
|
||||||
include_files: 指定扫描的文件列表,提供时跳过目录递归收集,直接扫描这些文件。
|
|
||||||
"""
|
"""
|
||||||
if executor is None:
|
if executor is None:
|
||||||
raise ValueError("executor is required and must not be None")
|
raise ValueError("executor is required and must not be None")
|
||||||
@@ -177,10 +175,7 @@ def scan_directory(
|
|||||||
python_path = Path(python_path).resolve()
|
python_path = Path(python_path).resolve()
|
||||||
|
|
||||||
# --- Collect files (depth/count limited) ---
|
# --- Collect files (depth/count limited) ---
|
||||||
if include_files is not None:
|
py_files = _collect_py_files(root_dir, max_depth=max_depth, max_files=max_files, exclude_files=exclude_files)
|
||||||
py_files = [Path(f).resolve() for f in include_files if Path(f).resolve().exists()]
|
|
||||||
else:
|
|
||||||
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 {}
|
cache_files: Dict[str, Any] = cache.get("files", {}) if cache else {}
|
||||||
|
|
||||||
@@ -679,17 +674,14 @@ def _resolve_name(name: str, import_map: Dict[str, str]) -> str:
|
|||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
_DECORATOR_ENUM_CLASSES = frozenset({"Side", "DataSource", "NodeType"})
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_attribute(node: ast.Attribute, import_map: Dict[str, str]) -> str:
|
def _resolve_attribute(node: ast.Attribute, import_map: Dict[str, str]) -> str:
|
||||||
"""
|
"""
|
||||||
Resolve an attribute access like Side.NORTH or DataSource.HANDLE.
|
Resolve an attribute access like Side.NORTH or DataSource.HANDLE.
|
||||||
|
|
||||||
对于来自 ``unilabos.registry.decorators`` 的枚举类 (Side / DataSource / NodeType),
|
Returns a string like "NORTH" for enum values, or
|
||||||
直接返回枚举成员名 (如 ``"NORTH"`` / ``"HANDLE"`` / ``"MANUAL_CONFIRM"``),
|
"module.path:Class.attr" for imported references.
|
||||||
省去消费端二次 rsplit 解析。其它 import 仍返回完整模块路径。
|
|
||||||
"""
|
"""
|
||||||
|
# Get the full dotted path
|
||||||
parts = []
|
parts = []
|
||||||
current = node
|
current = node
|
||||||
while isinstance(current, ast.Attribute):
|
while isinstance(current, ast.Attribute):
|
||||||
@@ -699,20 +691,21 @@ def _resolve_attribute(node: ast.Attribute, import_map: Dict[str, str]) -> str:
|
|||||||
parts.append(current.id)
|
parts.append(current.id)
|
||||||
|
|
||||||
parts.reverse()
|
parts.reverse()
|
||||||
# parts = ["Side", "NORTH"] or ["DataSource", "HANDLE"] or ["NodeType", "MANUAL_CONFIRM"]
|
# parts = ["Side", "NORTH"] or ["DataSource", "HANDLE"]
|
||||||
|
|
||||||
if len(parts) >= 2:
|
if len(parts) >= 2:
|
||||||
base = parts[0]
|
base = parts[0]
|
||||||
attr = ".".join(parts[1:])
|
attr = ".".join(parts[1:])
|
||||||
|
|
||||||
if base in _DECORATOR_ENUM_CLASSES:
|
# If the base is an imported name, resolve it
|
||||||
source = import_map.get(base, "")
|
|
||||||
if not source or _REGISTRY_DECORATOR_MODULE in source:
|
|
||||||
return parts[-1]
|
|
||||||
|
|
||||||
if base in import_map:
|
if base in import_map:
|
||||||
return f"{import_map[base]}.{attr}"
|
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)
|
return ".".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Usage:
|
|||||||
device, action, resource,
|
device, action, resource,
|
||||||
InputHandle, OutputHandle,
|
InputHandle, OutputHandle,
|
||||||
ActionInputHandle, ActionOutputHandle,
|
ActionInputHandle, ActionOutputHandle,
|
||||||
HardwareInterface, Side, DataSource, NodeType,
|
HardwareInterface, Side, DataSource,
|
||||||
)
|
)
|
||||||
|
|
||||||
@device(
|
@device(
|
||||||
@@ -73,13 +73,6 @@ class DataSource(str, Enum):
|
|||||||
EXECUTOR = "executor" # 从执行器输出数据 (用于 OutputHandle)
|
EXECUTOR = "executor" # 从执行器输出数据 (用于 OutputHandle)
|
||||||
|
|
||||||
|
|
||||||
class NodeType(str, Enum):
|
|
||||||
"""动作的节点类型(用于区分 ILab 节点和人工确认节点等)"""
|
|
||||||
|
|
||||||
ILAB = "ILab"
|
|
||||||
MANUAL_CONFIRM = "manual_confirm"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Device / Resource Handle (设备/资源级别端口, 序列化时包含 io_type)
|
# Device / Resource Handle (设备/资源级别端口, 序列化时包含 io_type)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -342,7 +335,6 @@ def action(
|
|||||||
description: str = "",
|
description: str = "",
|
||||||
auto_prefix: bool = False,
|
auto_prefix: bool = False,
|
||||||
parent: bool = False,
|
parent: bool = False,
|
||||||
node_type: Optional["NodeType"] = None,
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
动作方法装饰器
|
动作方法装饰器
|
||||||
@@ -373,8 +365,6 @@ def action(
|
|||||||
description: 动作描述
|
description: 动作描述
|
||||||
auto_prefix: 若为 True,动作名使用 auto-{method_name} 形式(与无 @action 时一致)
|
auto_prefix: 若为 True,动作名使用 auto-{method_name} 形式(与无 @action 时一致)
|
||||||
parent: 若为 True,当方法参数为空 (*args, **kwargs) 时,通过 MRO 从父类获取真实方法参数
|
parent: 若为 True,当方法参数为空 (*args, **kwargs) 时,通过 MRO 从父类获取真实方法参数
|
||||||
node_type: 动作的节点类型 (NodeType.ILAB / NodeType.MANUAL_CONFIRM)。
|
|
||||||
不填写时不写入注册表。
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(func: F) -> F:
|
def decorator(func: F) -> F:
|
||||||
@@ -399,8 +389,6 @@ def action(
|
|||||||
"auto_prefix": auto_prefix,
|
"auto_prefix": auto_prefix,
|
||||||
"parent": parent,
|
"parent": parent,
|
||||||
}
|
}
|
||||||
if node_type is not None:
|
|
||||||
meta["node_type"] = node_type.value if isinstance(node_type, NodeType) else str(node_type)
|
|
||||||
wrapper._action_registry_meta = meta # type: ignore[attr-defined]
|
wrapper._action_registry_meta = meta # type: ignore[attr-defined]
|
||||||
|
|
||||||
# 设置 _is_always_free 保持与旧 @always_free 装饰器兼容
|
# 设置 _is_always_free 保持与旧 @always_free 装饰器兼容
|
||||||
@@ -527,38 +515,6 @@ def clear_registry():
|
|||||||
_registered_resources.clear()
|
_registered_resources.clear()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 枚举值归一化
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_enum_value(raw: Any, enum_cls) -> Optional[str]:
|
|
||||||
"""将 AST 提取的枚举成员名 / YAML 值字符串 / 旧格式长路径统一归一化为枚举值。
|
|
||||||
|
|
||||||
适用于 Side、DataSource、NodeType 等继承自 ``str, Enum`` 的装饰器枚举。
|
|
||||||
|
|
||||||
处理以下格式:
|
|
||||||
- "MANUAL_CONFIRM" → NodeType["MANUAL_CONFIRM"].value = "manual_confirm"
|
|
||||||
- "manual_confirm" → NodeType("manual_confirm").value = "manual_confirm"
|
|
||||||
- "HANDLE" → DataSource["HANDLE"].value = "handle"
|
|
||||||
- "NORTH" → Side["NORTH"].value = "NORTH"
|
|
||||||
- 旧缓存长路径 "unilabos...NodeType.MANUAL_CONFIRM" → 先 rsplit 再查找
|
|
||||||
"""
|
|
||||||
if not raw:
|
|
||||||
return None
|
|
||||||
raw_str = str(raw)
|
|
||||||
if "." in raw_str:
|
|
||||||
raw_str = raw_str.rsplit(".", 1)[-1]
|
|
||||||
try:
|
|
||||||
return enum_cls[raw_str].value
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
return enum_cls(raw_str).value
|
|
||||||
except ValueError:
|
|
||||||
return raw_str
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# topic_config / not_action / always_free 装饰器
|
# topic_config / not_action / always_free 装饰器
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,589 +0,0 @@
|
|||||||
workstation.bioyond_dispensing_station:
|
|
||||||
category:
|
|
||||||
- workstation
|
|
||||||
- bioyond
|
|
||||||
class:
|
|
||||||
action_value_mappings:
|
|
||||||
auto-batch_create_90_10_vial_feeding_tasks:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
delay_time: null
|
|
||||||
hold_m_name: null
|
|
||||||
liquid_material_name: NMP
|
|
||||||
speed: null
|
|
||||||
temperature: null
|
|
||||||
titration: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
delay_time:
|
|
||||||
type: string
|
|
||||||
hold_m_name:
|
|
||||||
type: string
|
|
||||||
liquid_material_name:
|
|
||||||
default: NMP
|
|
||||||
type: string
|
|
||||||
speed:
|
|
||||||
type: string
|
|
||||||
temperature:
|
|
||||||
type: string
|
|
||||||
titration:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- titration
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: batch_create_90_10_vial_feeding_tasks参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-batch_create_diamine_solution_tasks:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
delay_time: null
|
|
||||||
liquid_material_name: NMP
|
|
||||||
solutions: null
|
|
||||||
speed: null
|
|
||||||
temperature: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
delay_time:
|
|
||||||
type: string
|
|
||||||
liquid_material_name:
|
|
||||||
default: NMP
|
|
||||||
type: string
|
|
||||||
solutions:
|
|
||||||
type: string
|
|
||||||
speed:
|
|
||||||
type: string
|
|
||||||
temperature:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- solutions
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: batch_create_diamine_solution_tasks参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-brief_step_parameters:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
data: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- data
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: brief_step_parameters参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-compute_experiment_design:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
m_tot: '70'
|
|
||||||
ratio: null
|
|
||||||
titration_percent: '0.03'
|
|
||||||
wt_percent: '0.25'
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
m_tot:
|
|
||||||
default: '70'
|
|
||||||
type: string
|
|
||||||
ratio:
|
|
||||||
type: object
|
|
||||||
titration_percent:
|
|
||||||
default: '0.03'
|
|
||||||
type: string
|
|
||||||
wt_percent:
|
|
||||||
default: '0.25'
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- ratio
|
|
||||||
type: object
|
|
||||||
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
|
|
||||||
- titration
|
|
||||||
- solvents
|
|
||||||
- feeding_order
|
|
||||||
- return_info
|
|
||||||
title: ComputeExperimentDesignReturn
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: compute_experiment_design参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-process_order_finish_report:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
report_request: null
|
|
||||||
used_materials: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
report_request:
|
|
||||||
type: string
|
|
||||||
used_materials:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- report_request
|
|
||||||
- used_materials
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: process_order_finish_report参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-project_order_report:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
order_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
order_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- order_id
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: project_order_report参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-query_resource_by_name:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
material_name: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
material_name:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- material_name
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: query_resource_by_name参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-transfer_materials_to_reaction_station:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
target_device_id: null
|
|
||||||
transfer_groups: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
target_device_id:
|
|
||||||
type: string
|
|
||||||
transfer_groups:
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- target_device_id
|
|
||||||
- transfer_groups
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: transfer_materials_to_reaction_station参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-wait_for_multiple_orders_and_get_reports:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
batch_create_result: null
|
|
||||||
check_interval: 10
|
|
||||||
timeout: 7200
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
batch_create_result:
|
|
||||||
type: string
|
|
||||||
check_interval:
|
|
||||||
default: 10
|
|
||||||
type: integer
|
|
||||||
timeout:
|
|
||||||
default: 7200
|
|
||||||
type: integer
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: wait_for_multiple_orders_and_get_reports参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-workflow_sample_locations:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
workflow_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
workflow_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- workflow_id
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: workflow_sample_locations参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
create_90_10_vial_feeding_task:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
delay_time: delay_time
|
|
||||||
hold_m_name: hold_m_name
|
|
||||||
order_name: order_name
|
|
||||||
percent_10_1_assign_material_name: percent_10_1_assign_material_name
|
|
||||||
percent_10_1_liquid_material_name: percent_10_1_liquid_material_name
|
|
||||||
percent_10_1_target_weigh: percent_10_1_target_weigh
|
|
||||||
percent_10_1_volume: percent_10_1_volume
|
|
||||||
percent_10_2_assign_material_name: percent_10_2_assign_material_name
|
|
||||||
percent_10_2_liquid_material_name: percent_10_2_liquid_material_name
|
|
||||||
percent_10_2_target_weigh: percent_10_2_target_weigh
|
|
||||||
percent_10_2_volume: percent_10_2_volume
|
|
||||||
percent_10_3_assign_material_name: percent_10_3_assign_material_name
|
|
||||||
percent_10_3_liquid_material_name: percent_10_3_liquid_material_name
|
|
||||||
percent_10_3_target_weigh: percent_10_3_target_weigh
|
|
||||||
percent_10_3_volume: percent_10_3_volume
|
|
||||||
percent_90_1_assign_material_name: percent_90_1_assign_material_name
|
|
||||||
percent_90_1_target_weigh: percent_90_1_target_weigh
|
|
||||||
percent_90_2_assign_material_name: percent_90_2_assign_material_name
|
|
||||||
percent_90_2_target_weigh: percent_90_2_target_weigh
|
|
||||||
percent_90_3_assign_material_name: percent_90_3_assign_material_name
|
|
||||||
percent_90_3_target_weigh: percent_90_3_target_weigh
|
|
||||||
speed: speed
|
|
||||||
temperature: temperature
|
|
||||||
goal_default:
|
|
||||||
delay_time: ''
|
|
||||||
hold_m_name: ''
|
|
||||||
order_name: ''
|
|
||||||
percent_10_1_assign_material_name: ''
|
|
||||||
percent_10_1_liquid_material_name: ''
|
|
||||||
percent_10_1_target_weigh: ''
|
|
||||||
percent_10_1_volume: ''
|
|
||||||
percent_10_2_assign_material_name: ''
|
|
||||||
percent_10_2_liquid_material_name: ''
|
|
||||||
percent_10_2_target_weigh: ''
|
|
||||||
percent_10_2_volume: ''
|
|
||||||
percent_10_3_assign_material_name: ''
|
|
||||||
percent_10_3_liquid_material_name: ''
|
|
||||||
percent_10_3_target_weigh: ''
|
|
||||||
percent_10_3_volume: ''
|
|
||||||
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: ''
|
|
||||||
speed: ''
|
|
||||||
temperature: ''
|
|
||||||
handles: {}
|
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
title: DispenStationVialFeed_Feedback
|
|
||||||
type: object
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
delay_time:
|
|
||||||
type: string
|
|
||||||
hold_m_name:
|
|
||||||
type: string
|
|
||||||
order_name:
|
|
||||||
type: string
|
|
||||||
percent_10_1_assign_material_name:
|
|
||||||
type: string
|
|
||||||
percent_10_1_liquid_material_name:
|
|
||||||
type: string
|
|
||||||
percent_10_1_target_weigh:
|
|
||||||
type: string
|
|
||||||
percent_10_1_volume:
|
|
||||||
type: string
|
|
||||||
percent_10_2_assign_material_name:
|
|
||||||
type: string
|
|
||||||
percent_10_2_liquid_material_name:
|
|
||||||
type: string
|
|
||||||
percent_10_2_target_weigh:
|
|
||||||
type: string
|
|
||||||
percent_10_2_volume:
|
|
||||||
type: string
|
|
||||||
percent_10_3_assign_material_name:
|
|
||||||
type: string
|
|
||||||
percent_10_3_liquid_material_name:
|
|
||||||
type: string
|
|
||||||
percent_10_3_target_weigh:
|
|
||||||
type: string
|
|
||||||
percent_10_3_volume:
|
|
||||||
type: string
|
|
||||||
percent_90_1_assign_material_name:
|
|
||||||
type: string
|
|
||||||
percent_90_1_target_weigh:
|
|
||||||
type: string
|
|
||||||
percent_90_2_assign_material_name:
|
|
||||||
type: string
|
|
||||||
percent_90_2_target_weigh:
|
|
||||||
type: string
|
|
||||||
percent_90_3_assign_material_name:
|
|
||||||
type: string
|
|
||||||
percent_90_3_target_weigh:
|
|
||||||
type: string
|
|
||||||
speed:
|
|
||||||
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:
|
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: DispenStationVialFeed_Result
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: DispenStationVialFeed
|
|
||||||
type: object
|
|
||||||
type: DispenStationVialFeed
|
|
||||||
create_diamine_solution_task:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
delay_time: delay_time
|
|
||||||
hold_m_name: hold_m_name
|
|
||||||
liquid_material_name: liquid_material_name
|
|
||||||
material_name: material_name
|
|
||||||
order_name: order_name
|
|
||||||
speed: speed
|
|
||||||
target_weigh: target_weigh
|
|
||||||
temperature: temperature
|
|
||||||
volume: volume
|
|
||||||
goal_default:
|
|
||||||
delay_time: ''
|
|
||||||
hold_m_name: ''
|
|
||||||
liquid_material_name: ''
|
|
||||||
material_name: ''
|
|
||||||
order_name: ''
|
|
||||||
speed: ''
|
|
||||||
target_weigh: ''
|
|
||||||
temperature: ''
|
|
||||||
volume: ''
|
|
||||||
handles: {}
|
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
title: DispenStationSolnPrep_Feedback
|
|
||||||
type: object
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
delay_time:
|
|
||||||
type: string
|
|
||||||
hold_m_name:
|
|
||||||
type: string
|
|
||||||
liquid_material_name:
|
|
||||||
type: string
|
|
||||||
material_name:
|
|
||||||
type: string
|
|
||||||
order_name:
|
|
||||||
type: string
|
|
||||||
speed:
|
|
||||||
type: string
|
|
||||||
target_weigh:
|
|
||||||
type: string
|
|
||||||
temperature:
|
|
||||||
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:
|
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: DispenStationSolnPrep_Result
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: DispenStationSolnPrep
|
|
||||||
type: object
|
|
||||||
type: DispenStationSolnPrep
|
|
||||||
module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation
|
|
||||||
status_types: {}
|
|
||||||
type: python
|
|
||||||
config_info: []
|
|
||||||
description: ''
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema:
|
|
||||||
config:
|
|
||||||
properties:
|
|
||||||
config:
|
|
||||||
type: string
|
|
||||||
deck:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- config
|
|
||||||
- deck
|
|
||||||
type: object
|
|
||||||
data:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
version: 1.0.0
|
|
||||||
@@ -5303,13 +5303,13 @@ liquid_handler.biomek:
|
|||||||
handler_key: tip_rack
|
handler_key: tip_rack
|
||||||
label: tip_rack
|
label: tip_rack
|
||||||
output:
|
output:
|
||||||
- data_key: sources
|
- data_key: liquid
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: sources_out
|
handler_key: sources_out
|
||||||
label: sources
|
label: sources
|
||||||
- data_key: targets
|
- data_key: liquid
|
||||||
data_source: handle
|
data_source: executor
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets_out
|
handler_key: targets_out
|
||||||
label: targets
|
label: targets
|
||||||
@@ -7581,43 +7581,6 @@ liquid_handler.prcxi:
|
|||||||
title: iter_tips参数
|
title: iter_tips参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
auto-magnetic_action:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
height: null
|
|
||||||
is_wait: null
|
|
||||||
module_no: null
|
|
||||||
time: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
height:
|
|
||||||
type: integer
|
|
||||||
is_wait:
|
|
||||||
type: boolean
|
|
||||||
module_no:
|
|
||||||
type: integer
|
|
||||||
time:
|
|
||||||
type: integer
|
|
||||||
required:
|
|
||||||
- time
|
|
||||||
- module_no
|
|
||||||
- height
|
|
||||||
- is_wait
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: magnetic_action参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommandAsync
|
|
||||||
auto-move_to:
|
auto-move_to:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -7746,47 +7709,6 @@ liquid_handler.prcxi:
|
|||||||
title: shaker_action参数
|
title: shaker_action参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommandAsync
|
type: UniLabJsonCommandAsync
|
||||||
auto-shaking_incubation_action:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
amplitude: null
|
|
||||||
is_wait: null
|
|
||||||
module_no: null
|
|
||||||
temperature: null
|
|
||||||
time: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
amplitude:
|
|
||||||
type: integer
|
|
||||||
is_wait:
|
|
||||||
type: boolean
|
|
||||||
module_no:
|
|
||||||
type: integer
|
|
||||||
temperature:
|
|
||||||
type: integer
|
|
||||||
time:
|
|
||||||
type: integer
|
|
||||||
required:
|
|
||||||
- time
|
|
||||||
- module_no
|
|
||||||
- amplitude
|
|
||||||
- is_wait
|
|
||||||
- temperature
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: shaking_incubation_action参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommandAsync
|
|
||||||
auto-touch_tip:
|
auto-touch_tip:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -9914,7 +9836,7 @@ liquid_handler.prcxi:
|
|||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: targets_identifier
|
handler_key: targets_identifier
|
||||||
label: 转移目标
|
label: 转移目标
|
||||||
- data_key: tip_racks
|
- data_key: tip_rack
|
||||||
data_source: handle
|
data_source: handle
|
||||||
data_type: resource
|
data_type: resource
|
||||||
handler_key: tip_rack_identifier
|
handler_key: tip_rack_identifier
|
||||||
|
|||||||
@@ -2804,203 +2804,6 @@ virtual_rotavap:
|
|||||||
- vacuum_pressure
|
- vacuum_pressure
|
||||||
type: object
|
type: object
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
virtual_sample_demo:
|
|
||||||
category:
|
|
||||||
- virtual_device
|
|
||||||
class:
|
|
||||||
action_value_mappings:
|
|
||||||
analyze_readings:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
readings: readings
|
|
||||||
samples: samples
|
|
||||||
goal_default:
|
|
||||||
readings: null
|
|
||||||
samples: null
|
|
||||||
handles:
|
|
||||||
input:
|
|
||||||
- data_key: readings
|
|
||||||
data_source: handle
|
|
||||||
data_type: sample_list
|
|
||||||
handler_key: readings_in
|
|
||||||
label: 测量读数
|
|
||||||
- data_key: samples
|
|
||||||
data_source: handle
|
|
||||||
data_type: sample_index
|
|
||||||
handler_key: samples_in
|
|
||||||
label: 样品索引
|
|
||||||
output:
|
|
||||||
- data_key: scores
|
|
||||||
data_source: executor
|
|
||||||
data_type: sample_list
|
|
||||||
handler_key: scores_out
|
|
||||||
label: 分析得分
|
|
||||||
- data_key: passed
|
|
||||||
data_source: executor
|
|
||||||
data_type: sample_list
|
|
||||||
handler_key: passed_out
|
|
||||||
label: 是否通过
|
|
||||||
- data_key: samples
|
|
||||||
data_source: executor
|
|
||||||
data_type: sample_index
|
|
||||||
handler_key: samples_result_out
|
|
||||||
label: 样品索引
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 对 split_and_measure 输出做二次分析,入参和出参都带 samples 列
|
|
||||||
properties:
|
|
||||||
feedback:
|
|
||||||
title: AnalyzeReadings_Feedback
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
readings:
|
|
||||||
description: 测量读数(来自 split_and_measure)
|
|
||||||
items:
|
|
||||||
type: number
|
|
||||||
type: array
|
|
||||||
samples:
|
|
||||||
description: 每行归属的输入样品 index (0-based)
|
|
||||||
items:
|
|
||||||
type: integer
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- readings
|
|
||||||
- samples
|
|
||||||
title: AnalyzeReadings_Goal
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
title: AnalyzeReadings_Result
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: analyze_readings参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommandAsync
|
|
||||||
measure_samples:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
concentrations: concentrations
|
|
||||||
goal_default:
|
|
||||||
concentrations: null
|
|
||||||
handles:
|
|
||||||
output:
|
|
||||||
- data_key: concentrations
|
|
||||||
data_source: executor
|
|
||||||
data_type: sample_list
|
|
||||||
handler_key: concentrations_out
|
|
||||||
label: 浓度列表
|
|
||||||
- data_key: absorbance
|
|
||||||
data_source: executor
|
|
||||||
data_type: sample_list
|
|
||||||
handler_key: absorbance_out
|
|
||||||
label: 吸光度列表
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 模拟光度测量,入参出参等长
|
|
||||||
properties:
|
|
||||||
feedback:
|
|
||||||
title: MeasureSamples_Feedback
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
concentrations:
|
|
||||||
description: 样品浓度列表
|
|
||||||
items:
|
|
||||||
type: number
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- concentrations
|
|
||||||
title: MeasureSamples_Goal
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
title: MeasureSamples_Result
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: measure_samples参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommandAsync
|
|
||||||
split_and_measure:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
split_count: split_count
|
|
||||||
volumes: volumes
|
|
||||||
goal_default:
|
|
||||||
split_count: 3
|
|
||||||
volumes: null
|
|
||||||
handles:
|
|
||||||
output:
|
|
||||||
- data_key: readings
|
|
||||||
data_source: executor
|
|
||||||
data_type: sample_list
|
|
||||||
handler_key: readings_out
|
|
||||||
label: 测量读数
|
|
||||||
- data_key: samples
|
|
||||||
data_source: executor
|
|
||||||
data_type: sample_index
|
|
||||||
handler_key: samples_out
|
|
||||||
label: 样品索引
|
|
||||||
- data_key: volumes
|
|
||||||
data_source: executor
|
|
||||||
data_type: sample_list
|
|
||||||
handler_key: volumes_out
|
|
||||||
label: 均分体积
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 均分样品后逐份测量,输出带 samples 列标注归属
|
|
||||||
properties:
|
|
||||||
feedback:
|
|
||||||
title: SplitAndMeasure_Feedback
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
split_count:
|
|
||||||
default: 3
|
|
||||||
description: 每个样品均分的份数
|
|
||||||
type: integer
|
|
||||||
volumes:
|
|
||||||
description: 样品体积列表
|
|
||||||
items:
|
|
||||||
type: number
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- volumes
|
|
||||||
title: SplitAndMeasure_Goal
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
title: SplitAndMeasure_Result
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: split_and_measure参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommandAsync
|
|
||||||
module: unilabos.devices.virtual.virtual_sample_demo:VirtualSampleDemo
|
|
||||||
status_types:
|
|
||||||
status: str
|
|
||||||
type: python
|
|
||||||
config_info: []
|
|
||||||
description: Virtual sample tracking demo device
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema:
|
|
||||||
config:
|
|
||||||
properties:
|
|
||||||
config:
|
|
||||||
type: object
|
|
||||||
device_id:
|
|
||||||
type: string
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
data:
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- status
|
|
||||||
type: object
|
|
||||||
version: 1.0.0
|
|
||||||
virtual_separator:
|
virtual_separator:
|
||||||
category:
|
category:
|
||||||
- virtual_device
|
- virtual_device
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ from unilabos.registry.decorators import (
|
|||||||
is_not_action,
|
is_not_action,
|
||||||
is_always_free,
|
is_always_free,
|
||||||
get_topic_config,
|
get_topic_config,
|
||||||
NodeType,
|
|
||||||
normalize_enum_value,
|
|
||||||
)
|
)
|
||||||
from unilabos.registry.utils import (
|
from unilabos.registry.utils import (
|
||||||
ROSMsgNotFound,
|
ROSMsgNotFound,
|
||||||
@@ -114,7 +112,7 @@ class Registry:
|
|||||||
# 统一入口
|
# 统一入口
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def setup(self, devices_dirs=None, upload_registry=False, complete_registry=False, external_only=False):
|
def setup(self, devices_dirs=None, upload_registry=False, complete_registry=False):
|
||||||
"""统一构建注册表入口。"""
|
"""统一构建注册表入口。"""
|
||||||
if self._setup_called:
|
if self._setup_called:
|
||||||
logger.critical("[UniLab Registry] setup方法已被调用过,不允许多次调用")
|
logger.critical("[UniLab Registry] setup方法已被调用过,不允许多次调用")
|
||||||
@@ -125,27 +123,24 @@ class Registry:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 1. AST 静态扫描 (快速, 无需 import)
|
# 1. AST 静态扫描 (快速, 无需 import)
|
||||||
self._run_ast_scan(devices_dirs, upload_registry=upload_registry, external_only=external_only)
|
self._run_ast_scan(devices_dirs, upload_registry=upload_registry)
|
||||||
|
|
||||||
# 2. Host node 内置设备
|
# 2. Host node 内置设备
|
||||||
self._setup_host_node()
|
self._setup_host_node()
|
||||||
|
|
||||||
# 3. YAML 注册表加载 (兼容旧格式) — external_only 模式下跳过
|
# 3. YAML 注册表加载 (兼容旧格式)
|
||||||
if external_only:
|
self.registry_paths = [Path(path).absolute() for path in self.registry_paths]
|
||||||
logger.info("[UniLab Registry] external_only 模式: 跳过 YAML 注册表加载")
|
for i, path in enumerate(self.registry_paths):
|
||||||
else:
|
sys_path = path.parent
|
||||||
self.registry_paths = [Path(path).absolute() for path in self.registry_paths]
|
logger.trace(f"[UniLab Registry] Path {i+1}/{len(self.registry_paths)}: {sys_path}")
|
||||||
for i, path in enumerate(self.registry_paths):
|
sys.path.append(str(sys_path))
|
||||||
sys_path = path.parent
|
self.load_device_types(path, complete_registry=complete_registry)
|
||||||
logger.trace(f"[UniLab Registry] Path {i+1}/{len(self.registry_paths)}: {sys_path}")
|
if BasicConfig.enable_resource_load:
|
||||||
sys.path.append(str(sys_path))
|
self.load_resource_types(path, upload_registry, complete_registry=complete_registry)
|
||||||
self.load_device_types(path, complete_registry=complete_registry)
|
else:
|
||||||
if BasicConfig.enable_resource_load:
|
logger.warning(
|
||||||
self.load_resource_types(path, upload_registry, complete_registry=complete_registry)
|
"[UniLab Registry] 资源加载已禁用 (enable_resource_load=False),跳过资源注册表加载"
|
||||||
else:
|
)
|
||||||
logger.warning(
|
|
||||||
"[UniLab Registry] 资源加载已禁用 (enable_resource_load=False),跳过资源注册表加载"
|
|
||||||
)
|
|
||||||
self._startup_executor.shutdown(wait=True)
|
self._startup_executor.shutdown(wait=True)
|
||||||
self._startup_executor = None
|
self._startup_executor = None
|
||||||
self._setup_called = True
|
self._setup_called = True
|
||||||
@@ -161,10 +156,9 @@ class Registry:
|
|||||||
ast_entry = self.device_type_registry.get("host_node", {})
|
ast_entry = self.device_type_registry.get("host_node", {})
|
||||||
ast_actions = ast_entry.get("class", {}).get("action_value_mappings", {})
|
ast_actions = ast_entry.get("class", {}).get("action_value_mappings", {})
|
||||||
|
|
||||||
# 取出 AST 生成的 action entries, 补充特定覆写
|
# 取出 AST 生成的 auto-method entries, 补充特定覆写
|
||||||
test_latency_action = ast_actions.get("auto-test_latency", {})
|
test_latency_action = ast_actions.get("auto-test_latency", {})
|
||||||
test_resource_action = ast_actions.get("auto-test_resource", {})
|
test_resource_action = ast_actions.get("auto-test_resource", {})
|
||||||
manual_confirm_action = ast_actions.get("manual_confirm", {})
|
|
||||||
test_resource_action["handles"] = {
|
test_resource_action["handles"] = {
|
||||||
"input": [
|
"input": [
|
||||||
{
|
{
|
||||||
@@ -237,11 +231,9 @@ class Registry:
|
|||||||
"parent": "unilabos_nodes",
|
"parent": "unilabos_nodes",
|
||||||
"class_name": "unilabos_class",
|
"class_name": "unilabos_class",
|
||||||
},
|
},
|
||||||
"always_free": True,
|
|
||||||
},
|
},
|
||||||
"test_latency": test_latency_action,
|
"test_latency": test_latency_action,
|
||||||
"auto-test_resource": test_resource_action,
|
"auto-test_resource": test_resource_action,
|
||||||
"manual_confirm": manual_confirm_action,
|
|
||||||
},
|
},
|
||||||
"init_params": {},
|
"init_params": {},
|
||||||
},
|
},
|
||||||
@@ -261,7 +253,7 @@ class Registry:
|
|||||||
# AST 静态扫描
|
# AST 静态扫描
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _run_ast_scan(self, devices_dirs=None, upload_registry=False, external_only=False):
|
def _run_ast_scan(self, devices_dirs=None, upload_registry=False):
|
||||||
"""
|
"""
|
||||||
执行 AST 静态扫描,从 Python 代码中提取 @device / @resource 装饰器元数据。
|
执行 AST 静态扫描,从 Python 代码中提取 @device / @resource 装饰器元数据。
|
||||||
无需 import 任何驱动模块,速度极快。
|
无需 import 任何驱动模块,速度极快。
|
||||||
@@ -306,30 +298,16 @@ class Registry:
|
|||||||
extra_dirs.append(d_path)
|
extra_dirs.append(d_path)
|
||||||
|
|
||||||
# 主扫描
|
# 主扫描
|
||||||
if external_only:
|
exclude_files = {"lab_resources.py"} if not BasicConfig.extra_resource else None
|
||||||
core_files = [
|
scan_result = scan_directory(
|
||||||
pkg_root / "ros" / "nodes" / "presets" / "host_node.py",
|
scan_root, python_path=python_path, executor=self._startup_executor,
|
||||||
pkg_root / "resources" / "container.py",
|
exclude_files=exclude_files, cache=ast_cache,
|
||||||
]
|
)
|
||||||
scan_result = scan_directory(
|
if exclude_files:
|
||||||
scan_root, python_path=python_path, executor=self._startup_executor,
|
|
||||||
cache=ast_cache, include_files=core_files,
|
|
||||||
)
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[UniLab Registry] external_only 模式: 仅扫描核心文件 "
|
f"[UniLab Registry] 排除扫描文件: {exclude_files} "
|
||||||
f"({', '.join(f.name for f in core_files)})"
|
f"(可通过 --extra_resource 启用加载)"
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
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})
|
total_stats = scan_result.pop("_cache_stats", {"hits": 0, "misses": 0, "total": 0})
|
||||||
@@ -852,9 +830,6 @@ class Registry:
|
|||||||
}
|
}
|
||||||
if (action_args or {}).get("always_free") or method_info.get("always_free"):
|
if (action_args or {}).get("always_free") or method_info.get("always_free"):
|
||||||
entry["always_free"] = True
|
entry["always_free"] = True
|
||||||
nt = normalize_enum_value((action_args or {}).get("node_type"), NodeType)
|
|
||||||
if nt:
|
|
||||||
entry["node_type"] = nt
|
|
||||||
return action_name, entry
|
return action_name, entry
|
||||||
|
|
||||||
# 1) auto- actions
|
# 1) auto- actions
|
||||||
@@ -979,9 +954,6 @@ class Registry:
|
|||||||
}
|
}
|
||||||
if action_args.get("always_free") or method_info.get("always_free"):
|
if action_args.get("always_free") or method_info.get("always_free"):
|
||||||
action_entry["always_free"] = True
|
action_entry["always_free"] = True
|
||||||
nt = normalize_enum_value(action_args.get("node_type"), NodeType)
|
|
||||||
if nt:
|
|
||||||
action_entry["node_type"] = nt
|
|
||||||
action_value_mappings[action_name] = action_entry
|
action_value_mappings[action_name] = action_entry
|
||||||
|
|
||||||
action_value_mappings = dict(sorted(action_value_mappings.items()))
|
action_value_mappings = dict(sorted(action_value_mappings.items()))
|
||||||
@@ -1164,7 +1136,7 @@ class Registry:
|
|||||||
return Path(BasicConfig.working_dir) / "registry_cache.pkl"
|
return Path(BasicConfig.working_dir) / "registry_cache.pkl"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
_CACHE_VERSION = 4
|
_CACHE_VERSION = 3
|
||||||
|
|
||||||
def _load_config_cache(self) -> dict:
|
def _load_config_cache(self) -> dict:
|
||||||
import pickle
|
import pickle
|
||||||
@@ -1562,9 +1534,9 @@ class Registry:
|
|||||||
del resource_info["config_info"]
|
del resource_info["config_info"]
|
||||||
if "file_path" in resource_info:
|
if "file_path" in resource_info:
|
||||||
del resource_info["file_path"]
|
del resource_info["file_path"]
|
||||||
|
complete_data[resource_id] = copy.deepcopy(dict(sorted(resource_info.items())))
|
||||||
resource_info["registry_type"] = "resource"
|
resource_info["registry_type"] = "resource"
|
||||||
resource_info["file_path"] = str(file.absolute()).replace("\\", "/")
|
resource_info["file_path"] = str(file.absolute()).replace("\\", "/")
|
||||||
complete_data[resource_id] = copy.deepcopy(dict(sorted(resource_info.items())))
|
|
||||||
|
|
||||||
for rid in skip_ids:
|
for rid in skip_ids:
|
||||||
data.pop(rid, None)
|
data.pop(rid, None)
|
||||||
@@ -1889,9 +1861,6 @@ class Registry:
|
|||||||
}
|
}
|
||||||
if v.get("always_free"):
|
if v.get("always_free"):
|
||||||
entry["always_free"] = True
|
entry["always_free"] = True
|
||||||
old_node_type = old_cfg.get("node_type")
|
|
||||||
if old_node_type in [NodeType.ILAB.value, NodeType.MANUAL_CONFIRM.value]:
|
|
||||||
entry["node_type"] = old_node_type
|
|
||||||
device_config["class"]["action_value_mappings"][action_key] = entry
|
device_config["class"]["action_value_mappings"][action_key] = entry
|
||||||
|
|
||||||
device_config["init_param_schema"] = {}
|
device_config["init_param_schema"] = {}
|
||||||
@@ -2206,7 +2175,7 @@ class Registry:
|
|||||||
lab_registry = Registry()
|
lab_registry = Registry()
|
||||||
|
|
||||||
|
|
||||||
def build_registry(registry_paths=None, devices_dirs=None, upload_registry=False, check_mode=False, complete_registry=False, external_only=False):
|
def build_registry(registry_paths=None, devices_dirs=None, upload_registry=False, check_mode=False, complete_registry=False):
|
||||||
"""
|
"""
|
||||||
构建或获取Registry单例实例
|
构建或获取Registry单例实例
|
||||||
"""
|
"""
|
||||||
@@ -2220,7 +2189,7 @@ def build_registry(registry_paths=None, devices_dirs=None, upload_registry=False
|
|||||||
if path not in current_paths:
|
if path not in current_paths:
|
||||||
lab_registry.registry_paths.append(path)
|
lab_registry.registry_paths.append(path)
|
||||||
|
|
||||||
lab_registry.setup(devices_dirs=devices_dirs, upload_registry=upload_registry, complete_registry=complete_registry, external_only=external_only)
|
lab_registry.setup(devices_dirs=devices_dirs, upload_registry=upload_registry, complete_registry=complete_registry)
|
||||||
|
|
||||||
# 将 AST 扫描的字符串类型替换为实际 ROS2 消息类(仅查找 ROS2 类型,不 import 设备模块)
|
# 将 AST 扫描的字符串类型替换为实际 ROS2 消息类(仅查找 ROS2 类型,不 import 设备模块)
|
||||||
lab_registry.resolve_all_types()
|
lab_registry.resolve_all_types()
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# UniLabOS 资源注册架构详解
|
||||||
|
|
||||||
|
> **目标受众**: 主要开发 `unilabos/registry/devices` 抽象层的开发者
|
||||||
|
> **最后更新**: 2026-01-11
|
||||||
|
> **维护者**: Uni-Lab-OS 开发团队
|
||||||
|
|
||||||
|
本文档详细说明 UniLabOS 资源注册系统的架构、资源的完整生命周期,以及如何实现动态物料位置追踪。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 目录
|
||||||
|
|
||||||
|
- [核心概念](#核心概念)
|
||||||
|
- [三层架构详解](#三层架构详解)
|
||||||
|
- [资源注册机制](#资源注册机制)
|
||||||
|
- [物料生命周期管理](#物料生命周期管理)
|
||||||
|
- [动态物料位置追踪](#动态物料位置追踪)
|
||||||
|
- [实战案例](#实战案例)
|
||||||
|
- [常见问题排查](#常见问题排查)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
### 1. Resources vs Registry
|
||||||
|
|
||||||
|
UniLabOS 采用**声明式注册**模式,将资源的**定义**(Python)与**注册信息**(YAML)分离:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ unilabos/resources (Python 实现) │
|
||||||
|
│ - 定义资源的物理属性、行为和创建逻辑 │
|
||||||
|
│ - 例如: 瓶子的尺寸、容量、材质 │
|
||||||
|
├──────────────────────────────────────────────────────────┤
|
||||||
|
│ unilabos/registry/resources (YAML 注册表) │
|
||||||
|
│ - 声明哪些资源可以被前端使用 │
|
||||||
|
│ - 定义资源的分类、图标、初始化参数 │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**为什么要分离?**
|
||||||
|
|
||||||
|
1. **解耦**: Python 代码可以定义无限多的资源类,但只有在 YAML 中注册的才能被前端识别
|
||||||
|
2. **灵活性**: 无需修改 Python 代码,只需修改 YAML 就能添加/移除可用资源
|
||||||
|
3. **可扩展性**: 第三方开发者可以通过 YAML 注册自己的资源,无需修改核心代码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三层架构详解
|
||||||
|
|
||||||
|
UniLabOS 资源系统采用**三层架构**,实现从前端UI到底层硬件的完整映射:
|
||||||
|
|
||||||
|
### 架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ 第1层: YAML 注册表 (registry/resources) │
|
||||||
|
│ - 告诉系统"哪些资源可用" │
|
||||||
|
│ - 前端通过此层获取可用资源列表 │
|
||||||
|
│ - 文件: YB_bottle.yaml, YB_bottle_carriers.yaml │
|
||||||
|
├─────────────────────────────────────────────────────┤
|
||||||
|
│ 第2层: Python 实现 (resources/bioyond) │
|
||||||
|
│ - 定义资源的具体属性和行为 │
|
||||||
|
│ - 创建资源实例的工厂函数 │
|
||||||
|
│ - 文件: YB_bottles.py, YB_bottle_carriers.py │
|
||||||
|
├─────────────────────────────────────────────────────┤
|
||||||
|
│ 第3层: Hardware/API 集成 (devices/workstation) │
|
||||||
|
│ - 连接 Bioyond 系统 API │
|
||||||
|
│ - 同步物料位置和状态 │
|
||||||
|
│ - 文件: station.py, bioyond_rpc.py, config.py │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第1层: YAML 注册表
|
||||||
|
|
||||||
|
#### YB_bottle.yaml - 单个瓶子注册
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
YB_5ml_fenyeping:
|
||||||
|
category:
|
||||||
|
- yb3 # 系统分类
|
||||||
|
- YB_bottle # 资源类型
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottles:YB_5ml_fenyeping # Python 函数路径
|
||||||
|
type: pylabrobot # 框架类型
|
||||||
|
description: YB_5ml_fenyeping # 前端显示名称
|
||||||
|
handles: []
|
||||||
|
icon: '' # 图标路径
|
||||||
|
init_param_schema: {} # 初始化参数 schema
|
||||||
|
registry_type: resource
|
||||||
|
version: 1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
**作用:**
|
||||||
|
- 前端通过读取此文件知道有一个叫 "YB_5ml_fenyeping" 的资源
|
||||||
|
- 用户拖拽时,系统会调用 `YB_bottles:YB_5ml_fenyeping()` 创建实例
|
||||||
|
|
||||||
|
#### YB_bottle_carriers.yaml - 载架(容器)注册
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
YB_5ml_fenyepingban:
|
||||||
|
category:
|
||||||
|
- yb3
|
||||||
|
- YB_bottle_carriers
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_5ml_fenyepingban
|
||||||
|
type: pylabrobot
|
||||||
|
description: YB_5ml_fenyepingban # 5ml分液瓶板
|
||||||
|
```
|
||||||
|
|
||||||
|
**作用:**
|
||||||
|
- 载架是容器,里面可以放多个瓶子
|
||||||
|
- 例如: `YB_5ml_fenyepingban` 是一个 4x2 布局的板,可以放 8 个 5ml 瓶子
|
||||||
|
|
||||||
|
### 第2层: Python 实现
|
||||||
|
|
||||||
|
#### YB_bottles.py - 瓶子工厂函数
|
||||||
|
|
||||||
|
```python
|
||||||
|
def YB_5ml_fenyeping(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 20.0, # 直径 (mm)
|
||||||
|
height: float = 50.0, # 高度 (mm)
|
||||||
|
max_volume: float = 5000.0, # 最大容量 (μL)
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
\"\"\"创建5ml分液瓶\"\"\"
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="YB_5ml_fenyeping", # ⭐ 与 YAML 中的名称对应
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点:**
|
||||||
|
- 函数名 `YB_5ml_fenyeping` 必须与 YAML 中的 `module` 路径末尾一致
|
||||||
|
- 返回一个 `Bottle` 对象(PyLabRobot 资源类型)
|
||||||
|
- `model` 字段用于在 Bioyond 系统中识别资源类型
|
||||||
|
|
||||||
|
**详细文档请参考完整版 README**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文件索引
|
||||||
|
|
||||||
|
### 核心文件
|
||||||
|
|
||||||
|
| 文件 | 功能 | 路径 |
|
||||||
|
|------|------|------|
|
||||||
|
| `YB_bottle.yaml` | 瓶子注册表 | `unilabos/registry/resources/bioyond/` |
|
||||||
|
| `YB_bottle_carriers.yaml` | 载架注册表 | `unilabos/registry/resources/bioyond/` |
|
||||||
|
| `deck.yaml` | Deck注册表 | `unilabos/registry/resources/bioyond/` |
|
||||||
|
| `YB_bottles.py` | 瓶子实现 | `unilabos/resources/bioyond/` |
|
||||||
|
| `YB_bottle_carriers.py` | 载架实现 | `unilabos/resources/bioyond/` |
|
||||||
|
| `YB_warehouses.py` | 仓库实现 | `unilabos/resources/bioyond/` |
|
||||||
|
| `decks.py` | Deck布局 | `unilabos/resources/bioyond/` |
|
||||||
|
| `station.py` | 物料同步 | `unilabos/devices/workstation/bioyond_studio/` |
|
||||||
|
| `config.py` | UUID映射 | `unilabos/devices/workstation/bioyond_studio/` |
|
||||||
|
|
||||||
|
### 仓库相关文档
|
||||||
|
|
||||||
|
- [README_WAREHOUSE.md](../../resources/bioyond/README_WAREHOUSE.md) - 仓库系统开发指南
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**维护者:** Uni-Lab-OS 开发团队
|
||||||
|
**最后更新:** 2026-01-11
|
||||||
@@ -20,6 +20,17 @@ BIOYOND_PolymerStation_Liquid_Vial:
|
|||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
BIOYOND_PolymerStation_Measurement_Vial:
|
||||||
|
category:
|
||||||
|
- bottles
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Measurement_Vial
|
||||||
|
type: pylabrobot
|
||||||
|
description: 聚合站-测量小瓶(测密度)
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
BIOYOND_PolymerStation_Reactor:
|
BIOYOND_PolymerStation_Reactor:
|
||||||
category:
|
category:
|
||||||
- bottles
|
- bottles
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,6 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
|||||||
from msgcenterpy.instances.typed_dict_instance import TypedDictMessageInstance
|
from msgcenterpy.instances.typed_dict_instance import TypedDictMessageInstance
|
||||||
|
|
||||||
from unilabos.utils.cls_creator import import_class
|
from unilabos.utils.cls_creator import import_class
|
||||||
from unilabos.registry.decorators import Side, DataSource, normalize_enum_value
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -488,7 +487,10 @@ def normalize_ast_handles(handles_raw: Any) -> List[Dict[str, Any]]:
|
|||||||
}
|
}
|
||||||
side = h.get("side")
|
side = h.get("side")
|
||||||
if side:
|
if side:
|
||||||
entry["side"] = normalize_enum_value(side, Side) or 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")
|
label = h.get("label")
|
||||||
if label:
|
if label:
|
||||||
entry["label"] = label
|
entry["label"] = label
|
||||||
@@ -497,7 +499,10 @@ def normalize_ast_handles(handles_raw: Any) -> List[Dict[str, Any]]:
|
|||||||
entry["data_key"] = data_key
|
entry["data_key"] = data_key
|
||||||
data_source = h.get("data_source")
|
data_source = h.get("data_source")
|
||||||
if data_source:
|
if data_source:
|
||||||
entry["data_source"] = normalize_enum_value(data_source, DataSource) or 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")
|
description = h.get("description")
|
||||||
if description:
|
if description:
|
||||||
entry["description"] = description
|
entry["description"] = description
|
||||||
@@ -532,12 +537,17 @@ def normalize_ast_action_handles(handles_raw: Any) -> Dict[str, Any]:
|
|||||||
"data_type": h.get("data_type", ""),
|
"data_type": h.get("data_type", ""),
|
||||||
"label": h.get("label", ""),
|
"label": h.get("label", ""),
|
||||||
}
|
}
|
||||||
_FIELD_ENUM_MAP = {"side": Side, "data_source": DataSource}
|
|
||||||
for opt_key in ("side", "data_key", "data_source", "description", "io_type"):
|
for opt_key in ("side", "data_key", "data_source", "description", "io_type"):
|
||||||
val = h.get(opt_key)
|
val = h.get(opt_key)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
if opt_key in _FIELD_ENUM_MAP:
|
# Only resolve enum-style refs (e.g. DataSource.HANDLE -> handle) for data_source/side
|
||||||
val = normalize_enum_value(val, _FIELD_ENUM_MAP[opt_key]) or val
|
# 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
|
entry[opt_key] = val
|
||||||
|
|
||||||
# io_type: only add when explicitly set; do not default output to "sink" (YAML convention omits it)
|
# io_type: only add when explicitly set; do not default output to "sink" (YAML convention omits it)
|
||||||
|
|||||||
56
unilabos/resources/battery/bottle_carriers.py
Normal file
56
unilabos/resources/battery/bottle_carriers.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
|
||||||
|
|
||||||
|
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
|
||||||
|
from unilabos.resources.bioyond.YB_bottles import (
|
||||||
|
YB_pei_ye_xiao_Bottle,
|
||||||
|
)
|
||||||
|
# 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial
|
||||||
|
|
||||||
|
|
||||||
|
def YIHUA_Electrolyte_12VialCarrier(name: str) -> BottleCarrier:
|
||||||
|
"""12瓶载架 - 2x6布局"""
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 120.0
|
||||||
|
carrier_size_y = 250.0
|
||||||
|
carrier_size_z = 50.0
|
||||||
|
|
||||||
|
# 瓶位尺寸
|
||||||
|
bottle_diameter = 35.0
|
||||||
|
bottle_spacing_x = 35.0 # X方向间距
|
||||||
|
bottle_spacing_y = 35.0 # Y方向间距
|
||||||
|
|
||||||
|
# 计算起始位置 (居中排列)
|
||||||
|
start_x = (carrier_size_x - (2 - 1) * bottle_spacing_x - bottle_diameter) / 2
|
||||||
|
start_y = (carrier_size_y - (6 - 1) * bottle_spacing_y - bottle_diameter) / 2
|
||||||
|
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=2,
|
||||||
|
num_items_y=6,
|
||||||
|
dx=start_x,
|
||||||
|
dy=start_y,
|
||||||
|
dz=5.0,
|
||||||
|
item_dx=bottle_spacing_x,
|
||||||
|
item_dy=bottle_spacing_y,
|
||||||
|
|
||||||
|
size_x=bottle_diameter,
|
||||||
|
size_y=bottle_diameter,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
)
|
||||||
|
for k, v in sites.items():
|
||||||
|
v.name = f"{name}_{v.name}"
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=sites,
|
||||||
|
model="Electrolyte_12VialCarrier",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 2
|
||||||
|
carrier.num_items_y = 6
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
for i in range(12):
|
||||||
|
carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_vial_{i+1}")
|
||||||
|
return carrier
|
||||||
195
unilabos/resources/battery/electrode_sheet.py
Normal file
195
unilabos/resources/battery/electrode_sheet.py
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
from typing import Any, Dict, Optional, TypedDict
|
||||||
|
|
||||||
|
from pylabrobot.resources import Resource as ResourcePLR
|
||||||
|
from pylabrobot.resources import Container
|
||||||
|
|
||||||
|
|
||||||
|
electrode_colors = {
|
||||||
|
"PositiveCan": "#ff0000",
|
||||||
|
"PositiveElectrode": "#cc3333",
|
||||||
|
"NegativeCan": "#000000",
|
||||||
|
"NegativeElectrode": "#666666",
|
||||||
|
"SpringWasher": "#8b7355",
|
||||||
|
"FlatWasher": "a9a9a9",
|
||||||
|
"AluminumFoil": "#ffcccc",
|
||||||
|
"Battery": "#00ff00",
|
||||||
|
}
|
||||||
|
|
||||||
|
class ElectrodeSheetState(TypedDict):
|
||||||
|
diameter: float # 直径 (mm)
|
||||||
|
thickness: float # 厚度 (mm)
|
||||||
|
mass: float # 质量 (g)
|
||||||
|
material_type: str # 材料类型(铜、铝、不锈钢、弹簧钢等)
|
||||||
|
color: str # 材料类型对应的颜色
|
||||||
|
info: Optional[str] # 附加信息
|
||||||
|
|
||||||
|
|
||||||
|
class ElectrodeSheet(ResourcePLR):
|
||||||
|
"""极片类 - 包含正负极片、隔膜、弹片、垫片、铝箔等所有片状材料"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str = "极片",
|
||||||
|
size_x: float = 10,
|
||||||
|
size_y: float = 10,
|
||||||
|
size_z: float = 10,
|
||||||
|
category: str = "electrode_sheet",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""初始化极片
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 极片名称
|
||||||
|
size_x: 长度 (mm)
|
||||||
|
size_y: 宽度 (mm)
|
||||||
|
size_z: 高度 (mm)
|
||||||
|
category: 类别
|
||||||
|
model: 型号
|
||||||
|
**kwargs: 其他参数传递给父类
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
self._unilabos_state: ElectrodeSheetState = ElectrodeSheetState(
|
||||||
|
diameter=14,
|
||||||
|
thickness=0.1,
|
||||||
|
mass=0.5,
|
||||||
|
material_type="copper",
|
||||||
|
color="#8b4513",
|
||||||
|
info=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: 这个还要不要?给self._unilabos_state赋值的?
|
||||||
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
|
"""格式不变"""
|
||||||
|
super().load_state(state)
|
||||||
|
self._unilabos_state = state
|
||||||
|
#序列化
|
||||||
|
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""格式不变"""
|
||||||
|
data = super().serialize_state()
|
||||||
|
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def PositiveCan(name: str) -> ElectrodeSheet:
|
||||||
|
"""创建正极壳"""
|
||||||
|
sheet = ElectrodeSheet(name=name, size_x=12, size_y=12, size_z=3.0, model="PositiveCan")
|
||||||
|
sheet.load_state({"diameter": 20.0, "thickness": 0.5, "mass": 0.5, "material_type": "aluminum", "color": electrode_colors["PositiveCan"], "info": None})
|
||||||
|
return sheet
|
||||||
|
|
||||||
|
|
||||||
|
def PositiveElectrode(name: str) -> ElectrodeSheet:
|
||||||
|
"""创建正极片"""
|
||||||
|
sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.1, model="PositiveElectrode")
|
||||||
|
sheet.load_state({"material_type": "positive_electrode", "color": electrode_colors["PositiveElectrode"]})
|
||||||
|
return sheet
|
||||||
|
|
||||||
|
|
||||||
|
def NegativeCan(name: str) -> ElectrodeSheet:
|
||||||
|
"""创建负极壳"""
|
||||||
|
sheet = ElectrodeSheet(name=name, size_x=12, size_y=12, size_z=2.0, model="NegativeCan")
|
||||||
|
sheet.load_state({"material_type": "steel", "color": electrode_colors["NegativeCan"]})
|
||||||
|
return sheet
|
||||||
|
|
||||||
|
|
||||||
|
def NegativeElectrode(name: str) -> ElectrodeSheet:
|
||||||
|
"""创建负极片"""
|
||||||
|
sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.1, model="NegativeElectrode")
|
||||||
|
sheet.load_state({"material_type": "negative_electrode", "color": electrode_colors["NegativeElectrode"]})
|
||||||
|
return sheet
|
||||||
|
|
||||||
|
|
||||||
|
def SpringWasher(name: str) -> ElectrodeSheet:
|
||||||
|
"""创建弹片"""
|
||||||
|
sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.5, model="SpringWasher")
|
||||||
|
sheet.load_state({"material_type": "spring_steel", "color": electrode_colors["SpringWasher"]})
|
||||||
|
return sheet
|
||||||
|
|
||||||
|
|
||||||
|
def FlatWasher(name: str) -> ElectrodeSheet:
|
||||||
|
"""创建垫片"""
|
||||||
|
sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.2, model="FlatWasher")
|
||||||
|
sheet.load_state({"material_type": "steel", "color": electrode_colors["FlatWasher"]})
|
||||||
|
return sheet
|
||||||
|
|
||||||
|
|
||||||
|
def AluminumFoil(name: str) -> ElectrodeSheet:
|
||||||
|
"""创建铝箔"""
|
||||||
|
sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.05, model="AluminumFoil")
|
||||||
|
sheet.load_state({"material_type": "aluminum", "color": electrode_colors["AluminumFoil"]})
|
||||||
|
return sheet
|
||||||
|
|
||||||
|
|
||||||
|
class BatteryState(TypedDict):
|
||||||
|
color: str # 材料类型对应的颜色
|
||||||
|
electrolyte_name: str
|
||||||
|
data_electrolyte_code: str
|
||||||
|
open_circuit_voltage: float
|
||||||
|
assembly_pressure: float
|
||||||
|
electrolyte_volume: float
|
||||||
|
|
||||||
|
info: Optional[str] # 附加信息
|
||||||
|
|
||||||
|
|
||||||
|
class Battery(Container):
|
||||||
|
"""电池类 - 包含组装好的电池"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str = "电池",
|
||||||
|
size_x: float = 12,
|
||||||
|
size_y: float = 12,
|
||||||
|
size_z: float = 6,
|
||||||
|
category: str = "battery",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""初始化电池
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 电池名称
|
||||||
|
size_x: 长度 (mm)
|
||||||
|
size_y: 宽度 (mm)
|
||||||
|
size_z: 高度 (mm)
|
||||||
|
category: 类别
|
||||||
|
model: 型号
|
||||||
|
**kwargs: 其他参数传递给父类
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
self._unilabos_state: BatteryState = BatteryState(
|
||||||
|
color=electrode_colors["Battery"],
|
||||||
|
electrolyte_name="无",
|
||||||
|
data_electrolyte_code="",
|
||||||
|
open_circuit_voltage=0.0,
|
||||||
|
assembly_pressure=0.0,
|
||||||
|
electrolyte_volume=0.0,
|
||||||
|
info=None
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
|
"""格式不变"""
|
||||||
|
super().load_state(state)
|
||||||
|
self._unilabos_state = state
|
||||||
|
|
||||||
|
#序列化
|
||||||
|
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""格式不变"""
|
||||||
|
data = super().serialize_state()
|
||||||
|
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||||
|
return data
|
||||||
344
unilabos/resources/battery/magazine.py
Normal file
344
unilabos/resources/battery/magazine.py
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
from typing import Dict, List, Optional, OrderedDict, Union, Callable
|
||||||
|
import math
|
||||||
|
|
||||||
|
from pylabrobot.resources.coordinate import Coordinate
|
||||||
|
from pylabrobot.resources import Resource, ResourceStack, ItemizedResource
|
||||||
|
from pylabrobot.resources.carrier import create_homogeneous_resources
|
||||||
|
|
||||||
|
from unilabos.resources.battery.electrode_sheet import (
|
||||||
|
PositiveCan, PositiveElectrode,
|
||||||
|
NegativeCan, NegativeElectrode,
|
||||||
|
SpringWasher, FlatWasher,
|
||||||
|
AluminumFoil,
|
||||||
|
Battery
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Magazine(ResourceStack):
|
||||||
|
"""子弹夹洞位类"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
direction: str = 'z',
|
||||||
|
resources: Optional[List[Resource]] = None,
|
||||||
|
max_sheets: int = 100,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""初始化子弹夹洞位
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 洞位名称
|
||||||
|
direction: 堆叠方向
|
||||||
|
resources: 资源列表
|
||||||
|
max_sheets: 最大极片数量
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
direction=direction,
|
||||||
|
resources=resources,
|
||||||
|
)
|
||||||
|
self.max_sheets = max_sheets
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size_x(self) -> float:
|
||||||
|
return self.get_size_x()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size_y(self) -> float:
|
||||||
|
return self.get_size_y()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size_z(self) -> float:
|
||||||
|
return self.get_size_z()
|
||||||
|
|
||||||
|
def serialize(self) -> dict:
|
||||||
|
return {
|
||||||
|
**super().serialize(),
|
||||||
|
"size_x": self.size_x or 10.0,
|
||||||
|
"size_y": self.size_y or 10.0,
|
||||||
|
"size_z": self.size_z or 10.0,
|
||||||
|
"max_sheets": self.max_sheets,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MagazineHolder(ItemizedResource):
|
||||||
|
"""子弹夹类 - 有多个洞位,每个洞位放多个极片"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
ordered_items: Optional[Dict[str, Magazine]] = None,
|
||||||
|
ordering: Optional[OrderedDict[str, str]] = None,
|
||||||
|
hole_diameter: float = 14.0,
|
||||||
|
hole_depth: float = 10.0,
|
||||||
|
max_sheets_per_hole: int = 100,
|
||||||
|
cross_section_type: str = "circle",
|
||||||
|
category: str = "magazine_holder",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""初始化子弹夹
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 子弹夹名称
|
||||||
|
size_x: 长度 (mm)
|
||||||
|
size_y: 宽度 (mm)
|
||||||
|
size_z: 高度 (mm)
|
||||||
|
hole_diameter: 洞直径 (mm)
|
||||||
|
hole_depth: 洞深度 (mm)
|
||||||
|
max_sheets_per_hole: 每个洞位最大极片数量
|
||||||
|
category: 类别
|
||||||
|
model: 型号
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
ordered_items=ordered_items,
|
||||||
|
ordering=ordering,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存洞位的直径和深度
|
||||||
|
self.hole_diameter = hole_diameter
|
||||||
|
self.hole_depth = hole_depth
|
||||||
|
self.max_sheets_per_hole = max_sheets_per_hole
|
||||||
|
self.cross_section_type = cross_section_type
|
||||||
|
|
||||||
|
def serialize(self) -> dict:
|
||||||
|
return {
|
||||||
|
**super().serialize(),
|
||||||
|
"hole_diameter": self.hole_diameter,
|
||||||
|
"hole_depth": self.hole_depth,
|
||||||
|
"max_sheets_per_hole": self.max_sheets_per_hole,
|
||||||
|
"cross_section_type": self.cross_section_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def magazine_factory(
|
||||||
|
name: str,
|
||||||
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
locations: List[Coordinate],
|
||||||
|
klasses: Optional[List[Callable[[str], str]]] = None,
|
||||||
|
hole_diameter: float = 14.0,
|
||||||
|
hole_depth: float = 10.0,
|
||||||
|
max_sheets_per_hole: int = 100,
|
||||||
|
category: str = "magazine_holder",
|
||||||
|
model: Optional[str] = None,
|
||||||
|
) -> 'MagazineHolder':
|
||||||
|
"""工厂函数:创建子弹夹
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 子弹夹名称
|
||||||
|
size_x: 长度 (mm)
|
||||||
|
size_y: 宽度 (mm)
|
||||||
|
size_z: 高度 (mm)
|
||||||
|
locations: 洞位坐标列表
|
||||||
|
klasses: 每个洞位中极片的类列表
|
||||||
|
hole_diameter: 洞直径 (mm)
|
||||||
|
hole_depth: 洞深度 (mm)
|
||||||
|
max_sheets_per_hole: 每个洞位最大极片数量
|
||||||
|
category: 类别
|
||||||
|
model: 型号
|
||||||
|
"""
|
||||||
|
for loc in locations:
|
||||||
|
loc.x -= hole_diameter / 2
|
||||||
|
loc.y -= hole_diameter / 2
|
||||||
|
|
||||||
|
# 创建洞位
|
||||||
|
_sites = create_homogeneous_resources(
|
||||||
|
klass=Magazine,
|
||||||
|
locations=locations,
|
||||||
|
resource_size_x=hole_diameter,
|
||||||
|
resource_size_y=hole_diameter,
|
||||||
|
name_prefix=name,
|
||||||
|
max_sheets=max_sheets_per_hole,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 生成编号键
|
||||||
|
keys = [f"A{i+1}" for i in range(len(locations))]
|
||||||
|
sites = dict(zip(keys, _sites.values()))
|
||||||
|
|
||||||
|
holder = MagazineHolder(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
ordered_items=sites,
|
||||||
|
hole_diameter=hole_diameter,
|
||||||
|
hole_depth=hole_depth,
|
||||||
|
max_sheets_per_hole=max_sheets_per_hole,
|
||||||
|
category=category,
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
|
||||||
|
if klasses is not None:
|
||||||
|
for i, klass in enumerate(klasses):
|
||||||
|
hole_key = keys[i]
|
||||||
|
hole = holder.children[i]
|
||||||
|
for j in reversed(range(max_sheets_per_hole)):
|
||||||
|
item_name = f"{hole_key}_sheet{j+1}"
|
||||||
|
item = klass(name=item_name)
|
||||||
|
hole.assign_child_resource(item)
|
||||||
|
return holder
|
||||||
|
|
||||||
|
|
||||||
|
def MagazineHolder_6_Cathode(
|
||||||
|
name: str,
|
||||||
|
size_x: float = 80.0,
|
||||||
|
size_y: float = 80.0,
|
||||||
|
size_z: float = 40.0,
|
||||||
|
hole_diameter: float = 14.0,
|
||||||
|
hole_depth: float = 10.0,
|
||||||
|
hole_spacing: float = 20.0,
|
||||||
|
max_sheets_per_hole: int = 100,
|
||||||
|
) -> MagazineHolder:
|
||||||
|
"""创建6孔子弹夹 - 六边形排布"""
|
||||||
|
center_x = size_x / 2
|
||||||
|
center_y = size_y / 2
|
||||||
|
|
||||||
|
locations = []
|
||||||
|
|
||||||
|
# 周围6个孔,按六边形排布
|
||||||
|
for i in range(6):
|
||||||
|
angle = i * 60 * math.pi / 180 # 每60度一个孔
|
||||||
|
x = center_x + hole_spacing * math.cos(angle)
|
||||||
|
y = center_y + hole_spacing * math.sin(angle)
|
||||||
|
locations.append(Coordinate(x, y, size_z - hole_depth))
|
||||||
|
|
||||||
|
return magazine_factory(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
locations=locations,
|
||||||
|
klasses=[FlatWasher, PositiveCan, PositiveCan, FlatWasher, PositiveCan, PositiveCan],
|
||||||
|
hole_diameter=hole_diameter,
|
||||||
|
hole_depth=hole_depth,
|
||||||
|
max_sheets_per_hole=max_sheets_per_hole,
|
||||||
|
category="magazine_holder",
|
||||||
|
model="MagazineHolder_6_Cathode",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def MagazineHolder_6_Anode(
|
||||||
|
name: str,
|
||||||
|
size_x: float = 80.0,
|
||||||
|
size_y: float = 80.0,
|
||||||
|
size_z: float = 40.0,
|
||||||
|
hole_diameter: float = 14.0,
|
||||||
|
hole_depth: float = 10.0,
|
||||||
|
hole_spacing: float = 20.0,
|
||||||
|
max_sheets_per_hole: int = 100,
|
||||||
|
) -> MagazineHolder:
|
||||||
|
"""创建6孔子弹夹 - 六边形排布"""
|
||||||
|
center_x = size_x / 2
|
||||||
|
center_y = size_y / 2
|
||||||
|
|
||||||
|
locations = []
|
||||||
|
|
||||||
|
# 周围6个孔,按六边形排布
|
||||||
|
for i in range(6):
|
||||||
|
angle = i * 60 * math.pi / 180 # 每60度一个孔
|
||||||
|
x = center_x + hole_spacing * math.cos(angle)
|
||||||
|
y = center_y + hole_spacing * math.sin(angle)
|
||||||
|
locations.append(Coordinate(x, y, size_z - hole_depth))
|
||||||
|
|
||||||
|
return magazine_factory(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
locations=locations,
|
||||||
|
klasses=[SpringWasher, NegativeCan, NegativeCan, SpringWasher, NegativeCan, NegativeCan],
|
||||||
|
hole_diameter=hole_diameter,
|
||||||
|
hole_depth=hole_depth,
|
||||||
|
max_sheets_per_hole=max_sheets_per_hole,
|
||||||
|
category="magazine_holder",
|
||||||
|
model="MagazineHolder_6_Anode",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def MagazineHolder_6_Battery(
|
||||||
|
name: str,
|
||||||
|
size_x: float = 80.0,
|
||||||
|
size_y: float = 80.0,
|
||||||
|
size_z: float = 40.0,
|
||||||
|
hole_diameter: float = 14.0,
|
||||||
|
hole_depth: float = 10.0,
|
||||||
|
hole_spacing: float = 20.0,
|
||||||
|
max_sheets_per_hole: int = 100,
|
||||||
|
) -> MagazineHolder:
|
||||||
|
"""创建6孔子弹夹 - 六边形排布"""
|
||||||
|
center_x = size_x / 2
|
||||||
|
center_y = size_y / 2
|
||||||
|
|
||||||
|
locations = []
|
||||||
|
|
||||||
|
# 周围6个孔,按六边形排布
|
||||||
|
for i in range(6):
|
||||||
|
angle = i * 60 * math.pi / 180 # 每60度一个孔
|
||||||
|
x = center_x + hole_spacing * math.cos(angle)
|
||||||
|
y = center_y + hole_spacing * math.sin(angle)
|
||||||
|
locations.append(Coordinate(x, y, size_z - hole_depth))
|
||||||
|
|
||||||
|
return magazine_factory(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
locations=locations,
|
||||||
|
klasses=None, # 初始化时,不放入装好的电池
|
||||||
|
hole_diameter=hole_diameter,
|
||||||
|
hole_depth=hole_depth,
|
||||||
|
max_sheets_per_hole=max_sheets_per_hole,
|
||||||
|
category="magazine_holder",
|
||||||
|
model="MagazineHolder_6_Battery",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def MagazineHolder_4_Cathode(
|
||||||
|
name: str,
|
||||||
|
) -> MagazineHolder:
|
||||||
|
"""创建4孔子弹夹 - 正方形四角排布"""
|
||||||
|
size_x: float = 80.0
|
||||||
|
size_y: float = 80.0
|
||||||
|
size_z: float = 10.0
|
||||||
|
hole_diameter: float = 14.0
|
||||||
|
hole_depth: float = 10.0
|
||||||
|
hole_spacing: float = 25.0
|
||||||
|
max_sheets_per_hole: int = 100
|
||||||
|
|
||||||
|
# 计算4个洞位的坐标(正方形四角排布)
|
||||||
|
center_x = size_x / 2
|
||||||
|
center_y = size_y / 2
|
||||||
|
offset = hole_spacing / 2
|
||||||
|
|
||||||
|
locations = [
|
||||||
|
Coordinate(center_x - offset, center_y - offset, size_z - hole_depth), # 左下
|
||||||
|
Coordinate(center_x + offset, center_y - offset, size_z - hole_depth), # 右下
|
||||||
|
Coordinate(center_x - offset, center_y + offset, size_z - hole_depth), # 左上
|
||||||
|
Coordinate(center_x + offset, center_y + offset, size_z - hole_depth), # 右上
|
||||||
|
]
|
||||||
|
|
||||||
|
return magazine_factory(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
locations=locations,
|
||||||
|
klasses=[AluminumFoil, PositiveElectrode, PositiveElectrode, PositiveElectrode],
|
||||||
|
hole_diameter=hole_diameter,
|
||||||
|
hole_depth=hole_depth,
|
||||||
|
max_sheets_per_hole=max_sheets_per_hole,
|
||||||
|
category="magazine_holder",
|
||||||
|
model="MagazineHolder_4_Cathode",
|
||||||
|
)
|
||||||
548
unilabos/resources/bioyond/README_WAREHOUSE.md
Normal file
548
unilabos/resources/bioyond/README_WAREHOUSE.md
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
# Bioyond 仓库系统开发指南
|
||||||
|
|
||||||
|
本文档详细说明 Bioyond 仓库(Warehouse)系统的架构、配置和使用方法,帮助开发者快速理解和维护仓库相关代码。
|
||||||
|
|
||||||
|
## 📚 目录
|
||||||
|
|
||||||
|
- [系统架构](#系统架构)
|
||||||
|
- [核心概念](#核心概念)
|
||||||
|
- [三层映射关系](#三层映射关系)
|
||||||
|
- [warehouse_factory 详解](#warehouse_factory-详解)
|
||||||
|
- [创建新仓库](#创建新仓库)
|
||||||
|
- [常见问题](#常见问题)
|
||||||
|
- [调试技巧](#调试技巧)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 系统架构
|
||||||
|
|
||||||
|
Bioyond 仓库系统采用**三层架构**,实现从前端显示到后端 API 的完整映射:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ 前端显示层 (YB_warehouses.py) │
|
||||||
|
│ - warehouse_factory 自动生成库位网格 │
|
||||||
|
│ - 生成库位名称:A01, B02, C03... │
|
||||||
|
│ - 存储在 WareHouse.sites 字典中 │
|
||||||
|
└────────────────┬────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Deck 布局层 (decks.py) │
|
||||||
|
│ - 定义仓库在 Deck 上的物理位置 │
|
||||||
|
│ - 组织多个仓库形成完整布局 │
|
||||||
|
└────────────────┬────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ UUID 映射层 (config.py) │
|
||||||
|
│ - 将库位名称映射到 Bioyond 系统 UUID │
|
||||||
|
│ - 用于 API 调用时的物料入库操作 │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
### 仓库(Warehouse)
|
||||||
|
|
||||||
|
仓库是一个**三维网格**,用于存放物料。由以下参数定义:
|
||||||
|
|
||||||
|
- **num_items_x**: 列数(X 轴)
|
||||||
|
- **num_items_y**: 行数(Y 轴)
|
||||||
|
- **num_items_z**: 层数(Z 轴)
|
||||||
|
|
||||||
|
例如:`5行×3列×1层` = 5×3×1 = 15个库位
|
||||||
|
|
||||||
|
### 库位(Site)
|
||||||
|
|
||||||
|
库位是仓库中的单个存储位置,由**字母行+数字列**命名:
|
||||||
|
|
||||||
|
- **字母行**:A, B, C, D, E, F...(对应 Y 轴)
|
||||||
|
- **数字列**:01, 02, 03, 04...(对应 X 轴或 Z 轴)
|
||||||
|
|
||||||
|
示例:`A01`, `B02`, `C03`
|
||||||
|
|
||||||
|
### 布局模式(Layout)
|
||||||
|
|
||||||
|
控制库位的排序和 Y 坐标计算:
|
||||||
|
|
||||||
|
| 模式 | 说明 | 生成顺序 | Y 坐标计算 | 显示效果 |
|
||||||
|
|------|------|----------|-----------|---------|
|
||||||
|
| `col-major` | 列优先(默认) | A01, B01, C01, A02... | `dy + (num_y - row - 1) * item_dy` | A 可能在下 |
|
||||||
|
| `row-major` | 行优先 | A01, A02, A03, B01... | `dy + row * item_dy` | **A 在上** ✓ |
|
||||||
|
|
||||||
|
**重要:** 使用 `row-major` 可以避免上下颠倒问题!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三层映射关系
|
||||||
|
|
||||||
|
### 示例:手动传递窗右(A01-E03)
|
||||||
|
|
||||||
|
#### 1️⃣ 前端显示层 - [`YB_warehouses.py`](YB_warehouses.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def bioyond_warehouse_5x3x1(name: str, row_offset: int = 0) -> WareHouse:
|
||||||
|
"""创建 5行×3列×1层 仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3, # 3列
|
||||||
|
num_items_y=5, # 5行
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
row_offset=row_offset,
|
||||||
|
layout="row-major",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**自动生成的库位:** A01, A02, A03, B01, B02, B03, ..., E01, E02, E03
|
||||||
|
|
||||||
|
#### 2️⃣ Deck 布局层 - [`decks.py`](decks.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.warehouses = {
|
||||||
|
"手动传递窗右": bioyond_warehouse_5x3x1("手动传递窗右", row_offset=0),
|
||||||
|
}
|
||||||
|
self.warehouse_locations = {
|
||||||
|
"手动传递窗右": Coordinate(4160.0, 877.0, 0.0),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**作用:**
|
||||||
|
- 创建仓库实例
|
||||||
|
- 设置在 Deck 上的物理坐标
|
||||||
|
|
||||||
|
#### 3️⃣ UUID 映射层 - [`config.py`](../../devices/workstation/bioyond_studio/config.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
WAREHOUSE_MAPPING = {
|
||||||
|
"手动传递窗右": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea",
|
||||||
|
"A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe",
|
||||||
|
# ... 其他库位
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**作用:**
|
||||||
|
- 用户拖拽物料到"手动传递窗右"的"A01"位置时
|
||||||
|
- 系统查找 `WAREHOUSE_MAPPING["手动传递窗右"]["site_uuids"]["A01"]`
|
||||||
|
- 获取 UUID `"3a19deae-2c7a-36f5-5e41-02c5b66feaea"`
|
||||||
|
- 调用 Bioyond API 将物料入库到该 UUID 位置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实际配置案例
|
||||||
|
|
||||||
|
### 案例:手动传递窗左/右的完整配置
|
||||||
|
|
||||||
|
本案例展示如何为"手动传递窗右"和"手动传递窗左"建立完整的三层映射。
|
||||||
|
|
||||||
|
#### 背景需求
|
||||||
|
- **手动传递窗右**: 需要 A01-E03(5行×3列=15个库位)
|
||||||
|
- **手动传递窗左**: 需要 F01-J03(5行×3列=15个库位)
|
||||||
|
- 这两个仓库共享同一个物理堆栈的 UUID("手动堆栈")
|
||||||
|
|
||||||
|
#### 实施步骤
|
||||||
|
|
||||||
|
**1️⃣ 修复前端布局** - [`YB_warehouses.py`](YB_warehouses.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 创建新的 5×3×1 仓库函数(之前是错误的 1×3×3)
|
||||||
|
def bioyond_warehouse_5x3x1(name: str, row_offset: int = 0) -> WareHouse:
|
||||||
|
"""创建5行×3列×1层仓库,支持行偏移生成不同字母行"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3, # 3列
|
||||||
|
num_items_y=5, # 5行 ← 修正
|
||||||
|
num_items_z=1, # 1层 ← 修正
|
||||||
|
row_offset=row_offset, # ← 支持 F-J 行
|
||||||
|
layout="row-major", # ← 避免上下颠倒
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**2️⃣ 更新 Deck 配置** - [`decks.py`](decks.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from unilabos.resources.bioyond.YB_warehouses import (
|
||||||
|
bioyond_warehouse_5x3x1, # 新增导入
|
||||||
|
)
|
||||||
|
|
||||||
|
class BIOYOND_YB_Deck(Deck):
|
||||||
|
def setup(self) -> None:
|
||||||
|
self.warehouses = {
|
||||||
|
# 修改前: bioyond_warehouse_1x3x3 (错误尺寸)
|
||||||
|
# 修改后: bioyond_warehouse_5x3x1 (正确尺寸)
|
||||||
|
"手动传递窗右": bioyond_warehouse_5x3x1("手动传递窗右", row_offset=0), # A01-E03
|
||||||
|
"手动传递窗左": bioyond_warehouse_5x3x1("手动传递窗左", row_offset=5), # F01-J03
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3️⃣ 添加 UUID 映射** - [`config.py`](../../devices/workstation/bioyond_studio/config.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
WAREHOUSE_MAPPING = {
|
||||||
|
# 保持原有的"手动堆栈"配置不变(A01-J03共30个库位)
|
||||||
|
"手动堆栈": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea",
|
||||||
|
# ... A02-E03 共15个
|
||||||
|
"F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a",
|
||||||
|
# ... F02-J03 共15个
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
# [新增] 手动传递窗右 - 复用"手动堆栈"的 A01-E03 UUID
|
||||||
|
"手动传递窗右": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea", # ← 与手动堆栈A01相同
|
||||||
|
"A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe",
|
||||||
|
"A03": "3a19deae-2c7a-5876-c454-6b7e224ca927",
|
||||||
|
"B01": "3a19deae-2c7a-2426-6d71-e9de3cb158b1",
|
||||||
|
"B02": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3",
|
||||||
|
"B03": "3a19deae-2c7a-b9eb-f4e3-e308e0cf839a",
|
||||||
|
"C01": "3a19deae-2c7a-32bc-768e-556647e292f3",
|
||||||
|
"C02": "3a19deae-2c7a-e97a-8484-f5a4599447c4",
|
||||||
|
"C03": "3a19deae-2c7a-3056-6504-10dc73fbc276",
|
||||||
|
"D01": "3a19deae-2c7a-ffad-875e-8c4cda61d440",
|
||||||
|
"D02": "3a19deae-2c7a-61be-601c-b6fb5610499a",
|
||||||
|
"D03": "3a19deae-2c7a-c0f7-05a7-e3fe2491e560",
|
||||||
|
"E01": "3a19deae-2c7a-a6f4-edd1-b436a7576363",
|
||||||
|
"E02": "3a19deae-2c7a-4367-96dd-1ca2186f4910",
|
||||||
|
"E03": "3a19deae-2c7a-b163-2219-23df15200311",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
# [新增] 手动传递窗左 - 复用"手动堆栈"的 F01-J03 UUID
|
||||||
|
"手动传递窗左": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a", # ← 与手动堆栈F01相同
|
||||||
|
"F02": "3a19deae-2c7a-a194-ea63-8b342b8d8679",
|
||||||
|
"F03": "3a19deae-2c7a-f7c4-12bd-425799425698",
|
||||||
|
"G01": "3a19deae-2c7a-0b56-72f1-8ab86e53b955",
|
||||||
|
"G02": "3a19deae-2c7a-204e-95ed-1f1950f28343",
|
||||||
|
"G03": "3a19deae-2c7a-392b-62f1-4907c66343f8",
|
||||||
|
"H01": "3a19deae-2c7a-5602-e876-d27aca4e3201",
|
||||||
|
"H02": "3a19deae-2c7a-f15c-70e0-25b58a8c9702",
|
||||||
|
"H03": "3a19deae-2c7a-780b-8965-2e1345f7e834",
|
||||||
|
"I01": "3a19deae-2c7a-8849-e172-07de14ede928",
|
||||||
|
"I02": "3a19deae-2c7a-4772-a37f-ff99270bafc0",
|
||||||
|
"I03": "3a19deae-2c7a-cce7-6e4a-25ea4a2068c4",
|
||||||
|
"J01": "3a19deae-2c7a-1848-de92-b5d5ed054cc6",
|
||||||
|
"J02": "3a19deae-2c7a-1d45-b4f8-6f866530e205",
|
||||||
|
"J03": "3a19deae-2c7a-f237-89d9-8fe19025dee9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 关键要点
|
||||||
|
|
||||||
|
1. **UUID 可以复用**: 三个仓库(手动堆栈、手动传递窗右、手动传递窗左)可以共享相同的物理库位 UUID
|
||||||
|
2. **库位名称必须匹配**: 前端生成的库位名称(如 F01)必须与 config.py 中的键名完全一致
|
||||||
|
3. **row_offset 的妙用**:
|
||||||
|
- `row_offset=0` → 生成 A-E 行
|
||||||
|
- `row_offset=5` → 生成 F-J 行(跳过前5个字母)
|
||||||
|
|
||||||
|
#### 验证结果
|
||||||
|
|
||||||
|
配置完成后,拖拽测试:
|
||||||
|
|
||||||
|
| 拖拽位置 | 前端库位 | 查找路径 | UUID | 结果 |
|
||||||
|
|---------|---------|---------|------|------|
|
||||||
|
| 手动传递窗右/A01 | A01 | `WAREHOUSE_MAPPING["手动传递窗右"]["site_uuids"]["A01"]` | `3a19...eaea` | ✅ 正确入库 |
|
||||||
|
| 手动传递窗左/F01 | F01 | `WAREHOUSE_MAPPING["手动传递窗左"]["site_uuids"]["F01"]` | `3a19...c4a` | ✅ 正确入库 |
|
||||||
|
| 手动堆栈/A01 | A01 | `WAREHOUSE_MAPPING["手动堆栈"]["site_uuids"]["A01"]` | `3a19...eaea` | ✅ 仍然正常 |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## warehouse_factory 详解
|
||||||
|
|
||||||
|
### 函数签名
|
||||||
|
|
||||||
|
```python
|
||||||
|
def warehouse_factory(
|
||||||
|
name: str,
|
||||||
|
num_items_x: int = 1, # 列数
|
||||||
|
num_items_y: int = 4, # 行数
|
||||||
|
num_items_z: int = 4, # 层数
|
||||||
|
dx: float = 137.0, # X 起始偏移
|
||||||
|
dy: float = 96.0, # Y 起始偏移
|
||||||
|
dz: float = 120.0, # Z 起始偏移
|
||||||
|
item_dx: float = 10.0, # X 间距
|
||||||
|
item_dy: float = 10.0, # Y 间距
|
||||||
|
item_dz: float = 10.0, # Z 间距
|
||||||
|
col_offset: int = 0, # 列偏移(影响数字)
|
||||||
|
row_offset: int = 0, # 行偏移(影响字母)
|
||||||
|
layout: str = "col-major", # 布局模式
|
||||||
|
) -> WareHouse:
|
||||||
|
```
|
||||||
|
|
||||||
|
### 参数说明
|
||||||
|
|
||||||
|
#### 尺寸参数
|
||||||
|
- **num_items_x, y, z**: 定义仓库的网格尺寸
|
||||||
|
- **注意**: 当 `num_items_z > 1` 时,Z 轴会被映射为数字列
|
||||||
|
|
||||||
|
#### 位置参数
|
||||||
|
- **dx, dy, dz**: 第一个库位的起始坐标
|
||||||
|
- **item_dx, dy, dz**: 库位之间的间距
|
||||||
|
|
||||||
|
#### 偏移参数
|
||||||
|
- **col_offset**: 列起始偏移,用于生成 A05-D08 等命名
|
||||||
|
```python
|
||||||
|
col_offset=4 # 生成 A05, A06, A07, A08
|
||||||
|
```
|
||||||
|
|
||||||
|
- **row_offset**: 行起始偏移,用于生成 F01-J03 等命名
|
||||||
|
```python
|
||||||
|
row_offset=5 # 生成 F01, F02, F03(跳过 A-E)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 布局参数
|
||||||
|
- **layout**:
|
||||||
|
- `"col-major"`: 列优先(默认),可能导致上下颠倒
|
||||||
|
- `"row-major"`: 行优先,**推荐使用**,A 显示在上
|
||||||
|
|
||||||
|
### 库位生成逻辑
|
||||||
|
|
||||||
|
```python
|
||||||
|
# row-major 模式(推荐)
|
||||||
|
keys = [f"{LETTERS[j + row_offset]}{i + 1 + col_offset:02d}"
|
||||||
|
for j in range(num_y)
|
||||||
|
for i in range(num_x)]
|
||||||
|
|
||||||
|
# 示例:num_y=2, num_x=3, row_offset=0, col_offset=0
|
||||||
|
# 生成:A01, A02, A03, B01, B02, B03
|
||||||
|
```
|
||||||
|
|
||||||
|
### Y 坐标计算
|
||||||
|
|
||||||
|
```python
|
||||||
|
if layout == "row-major":
|
||||||
|
# A 在上(Y 较小)
|
||||||
|
y = dy + row * item_dy
|
||||||
|
else:
|
||||||
|
# A 在下(Y 较大)- 不推荐
|
||||||
|
y = dy + (num_items_y - row - 1) * item_dy
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 创建新仓库
|
||||||
|
|
||||||
|
### 步骤 1: 在 YB_warehouses.py 中创建函数
|
||||||
|
|
||||||
|
```python
|
||||||
|
def bioyond_warehouse_3x4x1(name: str) -> WareHouse:
|
||||||
|
"""创建 3行×4列×1层 仓库
|
||||||
|
|
||||||
|
布局:
|
||||||
|
A01 | A02 | A03 | A04
|
||||||
|
B01 | B02 | B03 | B04
|
||||||
|
C01 | C02 | C03 | C04
|
||||||
|
"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=4, # 4列
|
||||||
|
num_items_y=3, # 3行
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=120.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
layout="row-major", # ⭐ 推荐使用
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 2: 在 decks.py 中使用
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1. 导入函数
|
||||||
|
from unilabos.resources.bioyond.YB_warehouses import (
|
||||||
|
bioyond_warehouse_3x4x1, # 新增
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 在 setup() 中添加
|
||||||
|
self.warehouses = {
|
||||||
|
"我的新仓库": bioyond_warehouse_3x4x1("我的新仓库"),
|
||||||
|
}
|
||||||
|
self.warehouse_locations = {
|
||||||
|
"我的新仓库": Coordinate(100.0, 200.0, 0.0),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 3: 在 config.py 中配置 UUID(可选)
|
||||||
|
|
||||||
|
```python
|
||||||
|
WAREHOUSE_MAPPING = {
|
||||||
|
"我的新仓库": {
|
||||||
|
"uuid": "",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "从 Bioyond 系统获取的 UUID",
|
||||||
|
"A02": "从 Bioyond 系统获取的 UUID",
|
||||||
|
# ... 其他 11 个库位
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意:** 如果不需要拖拽入库功能,可跳过此步骤。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q1: 为什么库位显示上下颠倒(C 在上,A 在下)?
|
||||||
|
|
||||||
|
**原因:** 使用了默认的 `col-major` 布局。
|
||||||
|
|
||||||
|
**解决:** 在 `warehouse_factory` 中添加 `layout="row-major"`
|
||||||
|
|
||||||
|
```python
|
||||||
|
return warehouse_factory(
|
||||||
|
...
|
||||||
|
layout="row-major", # ← 添加这行
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q2: 我需要 1×3×3 还是 3×3×1?
|
||||||
|
|
||||||
|
**判断方法:**
|
||||||
|
- **1×3×3**: 1列×3行×3**层**(垂直堆叠,有高度)
|
||||||
|
- **3×3×1**: 3行×3列×1**层**(平面网格)
|
||||||
|
|
||||||
|
**推荐:** 大多数情况使用 `X×Y×1`(平面网格)更直观。
|
||||||
|
|
||||||
|
### Q3: 如何生成 F01-J03 而非 A01-E03?
|
||||||
|
|
||||||
|
**方法:** 使用 `row_offset` 参数
|
||||||
|
|
||||||
|
```python
|
||||||
|
bioyond_warehouse_5x3x1("仓库名", row_offset=5)
|
||||||
|
# row_offset=5 跳过 A-E,从 F 开始
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q4: 拖拽物料后找不到 UUID 怎么办?
|
||||||
|
|
||||||
|
**检查清单:**
|
||||||
|
1. `config.py` 中是否有该仓库的配置?
|
||||||
|
2. 仓库名称是否完全匹配?
|
||||||
|
3. 库位名称(如 A01)是否在 `site_uuids` 中?
|
||||||
|
|
||||||
|
**示例错误:**
|
||||||
|
```python
|
||||||
|
# decks.py
|
||||||
|
"手动传递窗右": bioyond_warehouse_5x3x1(...)
|
||||||
|
|
||||||
|
# config.py - ❌ 名称不匹配
|
||||||
|
"手动传递窗": { ... } # 缺少"右"字
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q5: 库位重叠怎么办?
|
||||||
|
|
||||||
|
**原因:** 间距(`item_dx/dy/dz`)太小。
|
||||||
|
|
||||||
|
**解决:** 增大间距参数
|
||||||
|
|
||||||
|
```python
|
||||||
|
item_dx=150.0, # 增大 X 间距
|
||||||
|
item_dy=130.0, # 增大 Y 间距
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 调试技巧
|
||||||
|
|
||||||
|
### 1. 查看生成的库位
|
||||||
|
|
||||||
|
```python
|
||||||
|
warehouse = bioyond_warehouse_5x3x1("测试仓库")
|
||||||
|
print(list(warehouse.sites.keys()))
|
||||||
|
# 输出:['A01', 'A02', 'A03', 'B01', 'B02', ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 检查库位坐标
|
||||||
|
|
||||||
|
```python
|
||||||
|
for name, site in warehouse.sites.items():
|
||||||
|
print(f"{name}: {site.location}")
|
||||||
|
# 输出:
|
||||||
|
# A01: Coordinate(x=10.0, y=10.0, z=120.0)
|
||||||
|
# A02: Coordinate(x=147.0, y=10.0, z=120.0)
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 验证 UUID 映射
|
||||||
|
|
||||||
|
```python
|
||||||
|
from unilabos.devices.workstation.bioyond_studio.config import WAREHOUSE_MAPPING
|
||||||
|
|
||||||
|
warehouse_name = "手动传递窗右"
|
||||||
|
location_code = "A01"
|
||||||
|
|
||||||
|
if warehouse_name in WAREHOUSE_MAPPING:
|
||||||
|
uuid = WAREHOUSE_MAPPING[warehouse_name]["site_uuids"].get(location_code)
|
||||||
|
print(f"{warehouse_name}/{location_code} → {uuid}")
|
||||||
|
else:
|
||||||
|
print(f"❌ 未找到仓库: {warehouse_name}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件关系图
|
||||||
|
|
||||||
|
```
|
||||||
|
unilabos/
|
||||||
|
├── resources/
|
||||||
|
│ ├── warehouse.py # warehouse_factory 核心实现
|
||||||
|
│ └── bioyond/
|
||||||
|
│ ├── YB_warehouses.py # ⭐ 仓库函数定义
|
||||||
|
│ ├── decks.py # ⭐ Deck 布局配置
|
||||||
|
│ └── README_WAREHOUSE.md # 📖 本文档
|
||||||
|
└── devices/
|
||||||
|
└── workstation/
|
||||||
|
└── bioyond_studio/
|
||||||
|
├── config.py # ⭐ UUID 映射配置
|
||||||
|
└── bioyond_cell/
|
||||||
|
└── bioyond_cell_workstation.py # 业务逻辑
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本历史
|
||||||
|
|
||||||
|
- **v1.1** (2026-01-08): 补充实际配置案例
|
||||||
|
- 添加"手动传递窗右"和"手动传递窗左"的完整配置示例
|
||||||
|
- 展示 UUID 复用的实际应用
|
||||||
|
- 说明三个仓库共享物理堆栈的配置方法
|
||||||
|
|
||||||
|
- **v1.0** (2026-01-07): 初始版本
|
||||||
|
- 新增 `row_offset` 参数支持
|
||||||
|
- 创建 `bioyond_warehouse_5x3x1` 和 `bioyond_warehouse_2x2x1`
|
||||||
|
- 修复多个仓库的上下颠倒问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关资源
|
||||||
|
|
||||||
|
- [warehouse.py](../warehouse.py) - 核心工厂函数实现
|
||||||
|
- [YB_warehouses.py](YB_warehouses.py) - 所有仓库定义
|
||||||
|
- [decks.py](decks.py) - Deck 布局配置
|
||||||
|
- [config.py](../../devices/workstation/bioyond_studio/config.py) - UUID 映射
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**维护者:** Uni-Lab-OS 开发团队
|
||||||
|
**最后更新:** 2026-01-07
|
||||||
653
unilabos/resources/bioyond/YB_bottle_carriers.py
Normal file
653
unilabos/resources/bioyond/YB_bottle_carriers.py
Normal file
@@ -0,0 +1,653 @@
|
|||||||
|
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
|
||||||
|
|
||||||
|
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
|
||||||
|
from unilabos.resources.bioyond.YB_bottles import (
|
||||||
|
YB_jia_yang_tou_da,
|
||||||
|
YB_ye_Bottle,
|
||||||
|
YB_ye_100ml_Bottle,
|
||||||
|
YB_gao_nian_ye_Bottle,
|
||||||
|
YB_5ml_fenyeping,
|
||||||
|
YB_20ml_fenyeping,
|
||||||
|
YB_pei_ye_xiao_Bottle,
|
||||||
|
YB_pei_ye_da_Bottle,
|
||||||
|
YB_qiang_tou,
|
||||||
|
)
|
||||||
|
# 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial
|
||||||
|
|
||||||
|
|
||||||
|
def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier:
|
||||||
|
"""6瓶载架 - 2x3布局"""
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 50.0
|
||||||
|
|
||||||
|
# 瓶位尺寸
|
||||||
|
bottle_diameter = 30.0
|
||||||
|
bottle_spacing_x = 42.0 # X方向间距
|
||||||
|
bottle_spacing_y = 35.0 # Y方向间距
|
||||||
|
|
||||||
|
# 计算起始位置 (居中排列)
|
||||||
|
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
|
||||||
|
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
|
||||||
|
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=2,
|
||||||
|
dx=start_x,
|
||||||
|
dy=start_y,
|
||||||
|
dz=5.0,
|
||||||
|
item_dx=bottle_spacing_x,
|
||||||
|
item_dy=bottle_spacing_y,
|
||||||
|
|
||||||
|
size_x=bottle_diameter,
|
||||||
|
size_y=bottle_diameter,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
)
|
||||||
|
for k, v in sites.items():
|
||||||
|
v.name = f"{name}_{v.name}"
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=sites,
|
||||||
|
model="Electrolyte_6VialCarrier",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 3
|
||||||
|
carrier.num_items_y = 2
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
# for i in range(6):
|
||||||
|
# carrier[i] = YB_Solid_Vial(f"{name}_vial_{i+1}")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
|
||||||
|
def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier:
|
||||||
|
"""1瓶载架 - 单个中央位置"""
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 100.0
|
||||||
|
|
||||||
|
# 烧杯尺寸
|
||||||
|
beaker_diameter = 80.0
|
||||||
|
|
||||||
|
# 计算中央位置
|
||||||
|
center_x = (carrier_size_x - beaker_diameter) / 2
|
||||||
|
center_y = (carrier_size_y - beaker_diameter) / 2
|
||||||
|
center_z = 5.0
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=create_homogeneous_resources(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
locations=[Coordinate(center_x, center_y, center_z)],
|
||||||
|
resource_size_x=beaker_diameter,
|
||||||
|
resource_size_y=beaker_diameter,
|
||||||
|
name_prefix=name,
|
||||||
|
),
|
||||||
|
model="Electrolyte_1BottleCarrier",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 1
|
||||||
|
carrier.num_items_y = 1
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
# carrier[0] = YB_Solution_Beaker(f"{name}_beaker_1")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
|
||||||
|
def YB_6StockCarrier(name: str) -> BottleCarrier:
|
||||||
|
"""6瓶载架 - 2x3布局"""
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 50.0
|
||||||
|
|
||||||
|
# 瓶位尺寸
|
||||||
|
bottle_diameter = 20.0
|
||||||
|
bottle_spacing_x = 42.0 # X方向间距
|
||||||
|
bottle_spacing_y = 35.0 # Y方向间距
|
||||||
|
|
||||||
|
# 计算起始位置 (居中排列)
|
||||||
|
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
|
||||||
|
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
|
||||||
|
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=2,
|
||||||
|
dx=start_x,
|
||||||
|
dy=start_y,
|
||||||
|
dz=5.0,
|
||||||
|
item_dx=bottle_spacing_x,
|
||||||
|
item_dy=bottle_spacing_y,
|
||||||
|
|
||||||
|
size_x=bottle_diameter,
|
||||||
|
size_y=bottle_diameter,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
)
|
||||||
|
for k, v in sites.items():
|
||||||
|
v.name = f"{name}_{v.name}"
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=sites,
|
||||||
|
model="6StockCarrier",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 3
|
||||||
|
carrier.num_items_y = 2
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
|
||||||
|
# for i in range(6):
|
||||||
|
# carrier[i] = YB_Solid_Stock(f"{name}_vial_{ordering[i]}")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
|
||||||
|
def YB_6VialCarrier(name: str) -> BottleCarrier:
|
||||||
|
"""6瓶载架 - 2x3布局"""
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 50.0
|
||||||
|
|
||||||
|
# 瓶位尺寸
|
||||||
|
bottle_diameter = 30.0
|
||||||
|
bottle_spacing_x = 42.0 # X方向间距
|
||||||
|
bottle_spacing_y = 35.0 # Y方向间距
|
||||||
|
|
||||||
|
# 计算起始位置 (居中排列)
|
||||||
|
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
|
||||||
|
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
|
||||||
|
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=2,
|
||||||
|
dx=start_x,
|
||||||
|
dy=start_y,
|
||||||
|
dz=5.0,
|
||||||
|
item_dx=bottle_spacing_x,
|
||||||
|
item_dy=bottle_spacing_y,
|
||||||
|
|
||||||
|
size_x=bottle_diameter,
|
||||||
|
size_y=bottle_diameter,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
)
|
||||||
|
for k, v in sites.items():
|
||||||
|
v.name = f"{name}_{v.name}"
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=sites,
|
||||||
|
model="6VialCarrier",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 3
|
||||||
|
carrier.num_items_y = 2
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
|
||||||
|
# for i in range(3):
|
||||||
|
# carrier[i] = YB_Solid_Vial(f"{name}_solidvial_{ordering[i]}")
|
||||||
|
# for i in range(3, 6):
|
||||||
|
# carrier[i] = YB_Liquid_Vial(f"{name}_liquidvial_{ordering[i]}")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
# 1瓶载架 - 单个中央位置
|
||||||
|
def YB_ye(name: str) -> BottleCarrier:
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 20.0
|
||||||
|
|
||||||
|
# 烧杯尺寸
|
||||||
|
beaker_diameter = 60.0
|
||||||
|
|
||||||
|
# 计算中央位置
|
||||||
|
center_x = (carrier_size_x - beaker_diameter) / 2
|
||||||
|
center_y = (carrier_size_y - beaker_diameter) / 2
|
||||||
|
center_z = 5.0
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=create_homogeneous_resources(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
locations=[Coordinate(center_x, center_y, center_z)],
|
||||||
|
resource_size_x=beaker_diameter,
|
||||||
|
resource_size_y=beaker_diameter,
|
||||||
|
name_prefix=name,
|
||||||
|
),
|
||||||
|
model="YB_ye",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 1
|
||||||
|
carrier.num_items_y = 1
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
carrier[0] = YB_ye_Bottle(f"{name}_flask_1")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
|
||||||
|
# 高粘液瓶载架 - 单个中央位置
|
||||||
|
def YB_gaonianye(name: str) -> BottleCarrier:
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 20.0
|
||||||
|
|
||||||
|
# 烧杯尺寸
|
||||||
|
beaker_diameter = 60.0
|
||||||
|
|
||||||
|
# 计算中央位置
|
||||||
|
center_x = (carrier_size_x - beaker_diameter) / 2
|
||||||
|
center_y = (carrier_size_y - beaker_diameter) / 2
|
||||||
|
center_z = 5.0
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=create_homogeneous_resources(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
locations=[Coordinate(center_x, center_y, center_z)],
|
||||||
|
resource_size_x=beaker_diameter,
|
||||||
|
resource_size_y=beaker_diameter,
|
||||||
|
name_prefix=name,
|
||||||
|
),
|
||||||
|
model="YB_gaonianye",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 1
|
||||||
|
carrier.num_items_y = 1
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
carrier[0] = YB_gao_nian_ye_Bottle(f"{name}_flask_1")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
|
||||||
|
# 100ml液体瓶载架 - 单个中央位置
|
||||||
|
def YB_100ml_yeti(name: str) -> BottleCarrier:
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 20.0
|
||||||
|
|
||||||
|
# 烧杯尺寸
|
||||||
|
beaker_diameter = 60.0
|
||||||
|
|
||||||
|
# 计算中央位置
|
||||||
|
center_x = (carrier_size_x - beaker_diameter) / 2
|
||||||
|
center_y = (carrier_size_y - beaker_diameter) / 2
|
||||||
|
center_z = 5.0
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=create_homogeneous_resources(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
locations=[Coordinate(center_x, center_y, center_z)],
|
||||||
|
resource_size_x=beaker_diameter,
|
||||||
|
resource_size_y=beaker_diameter,
|
||||||
|
name_prefix=name,
|
||||||
|
),
|
||||||
|
model="YB_100ml_yeti",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 1
|
||||||
|
carrier.num_items_y = 1
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
carrier[0] = YB_ye_100ml_Bottle(f"{name}_flask_1")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
# 5ml分液瓶板 - 4x2布局,8个位置
|
||||||
|
def YB_5ml_fenyepingban(name: str) -> BottleCarrier:
|
||||||
|
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 50.0
|
||||||
|
|
||||||
|
# 瓶位尺寸
|
||||||
|
bottle_diameter = 15.0
|
||||||
|
bottle_spacing_x = 42.0 # X方向间距
|
||||||
|
bottle_spacing_y = 35.0 # Y方向间距
|
||||||
|
|
||||||
|
# 计算起始位置 (居中排列)
|
||||||
|
start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2
|
||||||
|
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
|
||||||
|
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=4,
|
||||||
|
num_items_y=2,
|
||||||
|
dx=start_x,
|
||||||
|
dy=start_y,
|
||||||
|
dz=5.0,
|
||||||
|
item_dx=bottle_spacing_x,
|
||||||
|
item_dy=bottle_spacing_y,
|
||||||
|
size_x=bottle_diameter,
|
||||||
|
size_y=bottle_diameter,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
)
|
||||||
|
for k, v in sites.items():
|
||||||
|
v.name = f"{name}_{v.name}"
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=sites,
|
||||||
|
model="YB_5ml_fenyepingban",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 4
|
||||||
|
carrier.num_items_y = 2
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
|
||||||
|
for i in range(8):
|
||||||
|
carrier[i] = YB_5ml_fenyeping(f"{name}_vial_{ordering[i]}")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
# 20ml分液瓶板 - 4x2布局,8个位置
|
||||||
|
def YB_20ml_fenyepingban(name: str) -> BottleCarrier:
|
||||||
|
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 70.0
|
||||||
|
|
||||||
|
# 瓶位尺寸
|
||||||
|
bottle_diameter = 20.0
|
||||||
|
bottle_spacing_x = 42.0 # X方向间距
|
||||||
|
bottle_spacing_y = 35.0 # Y方向间距
|
||||||
|
|
||||||
|
# 计算起始位置 (居中排列)
|
||||||
|
start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2
|
||||||
|
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
|
||||||
|
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=4,
|
||||||
|
num_items_y=2,
|
||||||
|
dx=start_x,
|
||||||
|
dy=start_y,
|
||||||
|
dz=5.0,
|
||||||
|
item_dx=bottle_spacing_x,
|
||||||
|
item_dy=bottle_spacing_y,
|
||||||
|
size_x=bottle_diameter,
|
||||||
|
size_y=bottle_diameter,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
)
|
||||||
|
for k, v in sites.items():
|
||||||
|
v.name = f"{name}_{v.name}"
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=sites,
|
||||||
|
model="YB_20ml_fenyepingban",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 4
|
||||||
|
carrier.num_items_y = 2
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
|
||||||
|
for i in range(8):
|
||||||
|
carrier[i] = YB_20ml_fenyeping(f"{name}_vial_{ordering[i]}")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
# 配液瓶(小)板 - 4x2布局,8个位置
|
||||||
|
def YB_peiyepingxiaoban(name: str) -> BottleCarrier:
|
||||||
|
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 65.0
|
||||||
|
|
||||||
|
# 瓶位尺寸
|
||||||
|
bottle_diameter = 35.0
|
||||||
|
bottle_spacing_x = 42.0 # X方向间距
|
||||||
|
bottle_spacing_y = 35.0 # Y方向间距
|
||||||
|
|
||||||
|
# 计算起始位置 (居中排列)
|
||||||
|
start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2
|
||||||
|
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
|
||||||
|
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=4,
|
||||||
|
num_items_y=2,
|
||||||
|
dx=start_x,
|
||||||
|
dy=start_y,
|
||||||
|
dz=5.0,
|
||||||
|
item_dx=bottle_spacing_x,
|
||||||
|
item_dy=bottle_spacing_y,
|
||||||
|
size_x=bottle_diameter,
|
||||||
|
size_y=bottle_diameter,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
)
|
||||||
|
for k, v in sites.items():
|
||||||
|
v.name = f"{name}_{v.name}"
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=sites,
|
||||||
|
model="YB_peiyepingxiaoban",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 4
|
||||||
|
carrier.num_items_y = 2
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
|
||||||
|
for i in range(8):
|
||||||
|
carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_bottle_{ordering[i]}")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
|
||||||
|
# 配液瓶(大)板 - 2x2布局,4个位置
|
||||||
|
def YB_peiyepingdaban(name: str) -> BottleCarrier:
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 95.0
|
||||||
|
|
||||||
|
# 瓶位尺寸
|
||||||
|
bottle_diameter = 55.0
|
||||||
|
bottle_spacing_x = 60.0 # X方向间距
|
||||||
|
bottle_spacing_y = 60.0 # Y方向间距
|
||||||
|
|
||||||
|
# 计算起始位置 (居中排列)
|
||||||
|
start_x = (carrier_size_x - (2 - 1) * bottle_spacing_x - bottle_diameter) / 2
|
||||||
|
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
|
||||||
|
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=2,
|
||||||
|
num_items_y=2,
|
||||||
|
dx=start_x,
|
||||||
|
dy=start_y,
|
||||||
|
dz=5.0,
|
||||||
|
item_dx=bottle_spacing_x,
|
||||||
|
item_dy=bottle_spacing_y,
|
||||||
|
size_x=bottle_diameter,
|
||||||
|
size_y=bottle_diameter,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
)
|
||||||
|
for k, v in sites.items():
|
||||||
|
v.name = f"{name}_{v.name}"
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=sites,
|
||||||
|
model="YB_peiyepingdaban",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 2
|
||||||
|
carrier.num_items_y = 2
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
ordering = ["A1", "A2", "B1", "B2"]
|
||||||
|
for i in range(4):
|
||||||
|
carrier[i] = YB_pei_ye_da_Bottle(f"{name}_bottle_{ordering[i]}")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
# 加样头(大)板 - 1x1布局,1个位置
|
||||||
|
def YB_jia_yang_tou_da_Carrier(name: str) -> BottleCarrier:
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 95.0
|
||||||
|
|
||||||
|
# 瓶位尺寸
|
||||||
|
bottle_diameter = 35.0
|
||||||
|
bottle_spacing_x = 42.0 # X方向间距
|
||||||
|
bottle_spacing_y = 35.0 # Y方向间距
|
||||||
|
|
||||||
|
# 计算起始位置 (居中排列)
|
||||||
|
start_x = (carrier_size_x - (1 - 1) * bottle_spacing_x - bottle_diameter) / 2
|
||||||
|
start_y = (carrier_size_y - (1 - 1) * bottle_spacing_y - bottle_diameter) / 2
|
||||||
|
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=1,
|
||||||
|
num_items_y=1,
|
||||||
|
dx=start_x,
|
||||||
|
dy=start_y,
|
||||||
|
dz=5.0,
|
||||||
|
item_dx=bottle_spacing_x,
|
||||||
|
item_dy=bottle_spacing_y,
|
||||||
|
size_x=bottle_diameter,
|
||||||
|
size_y=bottle_diameter,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
)
|
||||||
|
for k, v in sites.items():
|
||||||
|
v.name = f"{name}_{v.name}"
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=sites,
|
||||||
|
model="YB_jia_yang_tou_da_Carrier",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 1
|
||||||
|
carrier.num_items_y = 1
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
carrier[0] = YB_jia_yang_tou_da(f"{name}_head_1")
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
|
||||||
|
def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier:
|
||||||
|
"""适配器块 - 单个中央位置"""
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 30.0
|
||||||
|
|
||||||
|
# 适配器尺寸
|
||||||
|
adapter_diameter = 80.0
|
||||||
|
|
||||||
|
# 计算中央位置
|
||||||
|
center_x = (carrier_size_x - adapter_diameter) / 2
|
||||||
|
center_y = (carrier_size_y - adapter_diameter) / 2
|
||||||
|
center_z = 0.0
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=create_homogeneous_resources(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
locations=[Coordinate(center_x, center_y, center_z)],
|
||||||
|
resource_size_x=adapter_diameter,
|
||||||
|
resource_size_y=adapter_diameter,
|
||||||
|
name_prefix=name,
|
||||||
|
),
|
||||||
|
model="YB_shi_pei_qi_kuai",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 1
|
||||||
|
carrier.num_items_y = 1
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
# 适配器块本身不包含瓶子,只是一个支撑结构
|
||||||
|
return carrier
|
||||||
|
|
||||||
|
|
||||||
|
def YB_qiang_tou_he(name: str) -> BottleCarrier:
|
||||||
|
"""枪头盒 - 8x12布局,96个位置"""
|
||||||
|
|
||||||
|
# 载架尺寸 (mm)
|
||||||
|
carrier_size_x = 127.8
|
||||||
|
carrier_size_y = 85.5
|
||||||
|
carrier_size_z = 55.0
|
||||||
|
|
||||||
|
# 枪头尺寸
|
||||||
|
tip_diameter = 10.0
|
||||||
|
tip_spacing_x = 9.0 # X方向间距
|
||||||
|
tip_spacing_y = 9.0 # Y方向间距
|
||||||
|
|
||||||
|
# 计算起始位置 (居中排列)
|
||||||
|
start_x = (carrier_size_x - (12 - 1) * tip_spacing_x - tip_diameter) / 2
|
||||||
|
start_y = (carrier_size_y - (8 - 1) * tip_spacing_y - tip_diameter) / 2
|
||||||
|
|
||||||
|
sites = create_ordered_items_2d(
|
||||||
|
klass=ResourceHolder,
|
||||||
|
num_items_x=12,
|
||||||
|
num_items_y=8,
|
||||||
|
dx=start_x,
|
||||||
|
dy=start_y,
|
||||||
|
dz=5.0,
|
||||||
|
item_dx=tip_spacing_x,
|
||||||
|
item_dy=tip_spacing_y,
|
||||||
|
size_x=tip_diameter,
|
||||||
|
size_y=tip_diameter,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
)
|
||||||
|
for k, v in sites.items():
|
||||||
|
v.name = f"{name}_{v.name}"
|
||||||
|
|
||||||
|
carrier = BottleCarrier(
|
||||||
|
name=name,
|
||||||
|
size_x=carrier_size_x,
|
||||||
|
size_y=carrier_size_y,
|
||||||
|
size_z=carrier_size_z,
|
||||||
|
sites=sites,
|
||||||
|
model="YB_qiang_tou_he",
|
||||||
|
)
|
||||||
|
carrier.num_items_x = 12
|
||||||
|
carrier.num_items_y = 8
|
||||||
|
carrier.num_items_z = 1
|
||||||
|
# 创建96个枪头
|
||||||
|
for i in range(96):
|
||||||
|
row = chr(65 + i // 12) # A-H
|
||||||
|
col = (i % 12) + 1 # 1-12
|
||||||
|
carrier[i] = YB_qiang_tou(f"{name}_tip_{row}{col}")
|
||||||
|
return carrier
|
||||||
|
|
||||||
163
unilabos/resources/bioyond/YB_bottles.py
Normal file
163
unilabos/resources/bioyond/YB_bottles.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
|
||||||
|
# 工厂函数
|
||||||
|
"""加样头(大)"""
|
||||||
|
def YB_jia_yang_tou_da(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 20.0,
|
||||||
|
height: float = 100.0,
|
||||||
|
max_volume: float = 30000.0, # 30mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建粉末瓶"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,# 未知
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="YB_jia_yang_tou_da",
|
||||||
|
)
|
||||||
|
|
||||||
|
"""液1x1"""
|
||||||
|
def YB_ye_Bottle(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 40.0,
|
||||||
|
height: float = 70.0,
|
||||||
|
max_volume: float = 50000.0, # 50mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建液体瓶"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="YB_ye_Bottle",
|
||||||
|
)
|
||||||
|
|
||||||
|
"""100ml液体"""
|
||||||
|
def YB_ye_100ml_Bottle(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 50.0,
|
||||||
|
height: float = 90.0,
|
||||||
|
max_volume: float = 100000.0, # 100mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建100ml液体瓶"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="YB_100ml_yeti",
|
||||||
|
)
|
||||||
|
|
||||||
|
"""高粘液"""
|
||||||
|
def YB_gao_nian_ye_Bottle(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 40.0,
|
||||||
|
height: float = 70.0,
|
||||||
|
max_volume: float = 50000.0, # 50mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建高粘液瓶"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="High_Viscosity_Liquid",
|
||||||
|
)
|
||||||
|
|
||||||
|
"""5ml分液瓶"""
|
||||||
|
def YB_5ml_fenyeping(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 20.0,
|
||||||
|
height: float = 50.0,
|
||||||
|
max_volume: float = 5000.0, # 5mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建5ml分液瓶"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="YB_5ml_fenyeping",
|
||||||
|
)
|
||||||
|
|
||||||
|
"""20ml分液瓶"""
|
||||||
|
def YB_20ml_fenyeping(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 30.0,
|
||||||
|
height: float = 65.0,
|
||||||
|
max_volume: float = 20000.0, # 20mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建20ml分液瓶"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="YB_20ml_fenyeping",
|
||||||
|
)
|
||||||
|
|
||||||
|
"""配液瓶(小)"""
|
||||||
|
def YB_pei_ye_xiao_Bottle(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 35.0,
|
||||||
|
height: float = 60.0,
|
||||||
|
max_volume: float = 30000.0, # 30mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建配液瓶(小)"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="YB_pei_ye_xiao_Bottle",
|
||||||
|
)
|
||||||
|
|
||||||
|
"""配液瓶(大)"""
|
||||||
|
def YB_pei_ye_da_Bottle(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 55.0,
|
||||||
|
height: float = 100.0,
|
||||||
|
max_volume: float = 150000.0, # 150mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建配液瓶(大)"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="YB_pei_ye_da_Bottle",
|
||||||
|
)
|
||||||
|
|
||||||
|
"""枪头"""
|
||||||
|
def YB_qiang_tou(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 10.0,
|
||||||
|
height: float = 50.0,
|
||||||
|
max_volume: float = 1000.0, # 1mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建枪头"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="YB_qiang_tou",
|
||||||
|
)
|
||||||
384
unilabos/resources/bioyond/YB_warehouses.py
Normal file
384
unilabos/resources/bioyond/YB_warehouses.py
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
from unilabos.resources.warehouse import WareHouse, warehouse_factory
|
||||||
|
|
||||||
|
# ================ 反应站相关堆栈 ================
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x4x1仓库 (左侧堆栈: A01~D04)
|
||||||
|
|
||||||
|
使用行优先排序,前端展示为:
|
||||||
|
A01 | A02 | A03 | A04
|
||||||
|
B01 | B02 | B03 | B04
|
||||||
|
C01 | C02 | C03 | C04
|
||||||
|
D01 | D02 | D03 | D04
|
||||||
|
"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=4, # 4列
|
||||||
|
num_items_y=4, # 4行
|
||||||
|
num_items_z=1,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=147.0,
|
||||||
|
item_dy=106.0,
|
||||||
|
item_dz=130.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=0, # 从01开始: A01, A02, A03, A04
|
||||||
|
layout="row-major", # ⭐ 改为行优先排序
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x4x4_right(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x4x1仓库 (右侧堆栈: A05~D08)"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=4,
|
||||||
|
num_items_y=4,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=147.0,
|
||||||
|
item_dy=106.0,
|
||||||
|
item_dz=130.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=4, # 从05开始: A05, A06, A07, A08
|
||||||
|
layout="row-major", # ⭐ 改为行优先排序
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_density_vial(name: str) -> WareHouse:
|
||||||
|
"""创建测量小瓶仓库(测密度) A01~B03"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3, # 3列(01-03)
|
||||||
|
num_items_y=2, # 2行(A-B)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=40.0,
|
||||||
|
item_dy=40.0,
|
||||||
|
item_dz=50.0,
|
||||||
|
# 用更小的 resource_size 来表现 "小点的孔位"
|
||||||
|
resource_size_x=30.0,
|
||||||
|
resource_size_y=30.0,
|
||||||
|
resource_size_z=12.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=0,
|
||||||
|
layout="row-major",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_reagent_storage(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond站内试剂存放堆栈(A01~A02, 1行×2列)"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=2, # 2列(01-02)
|
||||||
|
num_items_y=1, # 1行(A)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_tipbox_storage(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond站内Tip盒堆栈(A01~B03, 2行×3列)"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3, # 3列(01-03)
|
||||||
|
num_items_y=2, # 2行(A-B)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=0,
|
||||||
|
layout="row-major",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_liquid_preparation(name: str) -> WareHouse:
|
||||||
|
"""已弃用,创建BioYond移液站内10%分装液体准备仓库(A01~B04)"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=4, # 4列(01-04)
|
||||||
|
num_items_y=2, # 2行(A-B)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=0,
|
||||||
|
layout="row-major",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ================ 配液站相关堆栈 ================
|
||||||
|
|
||||||
|
def bioyond_warehouse_reagent_stack(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 试剂堆栈 2x4x1 (2行×4列: A01-A04, B01-B04)
|
||||||
|
|
||||||
|
使用行优先排序,前端展示为:
|
||||||
|
A01 | A02 | A03 | A04
|
||||||
|
B01 | B02 | B03 | B04
|
||||||
|
"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=4, # 4列 (01-04)
|
||||||
|
num_items_y=2, # 2行 (A-B)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=147.0,
|
||||||
|
item_dy=106.0,
|
||||||
|
item_dz=130.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=0, # 从01开始
|
||||||
|
layout="row-major", # ⭐ 使用行优先排序: A01,A02,A03,A04, B01,B02,B03,B04
|
||||||
|
)
|
||||||
|
|
||||||
|
# 定义bioyond的堆栈
|
||||||
|
|
||||||
|
# =================== Other ===================
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x2x1仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=1,
|
||||||
|
num_items_y=4,
|
||||||
|
num_items_z=2,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
removed_positions=None
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x2x2(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 1x2x2仓库(1列×2行×2层)- 旧版本,已弃用
|
||||||
|
|
||||||
|
布局(2层):
|
||||||
|
层1: A01
|
||||||
|
B01
|
||||||
|
层2: A02
|
||||||
|
B02
|
||||||
|
"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=1,
|
||||||
|
num_items_y=2,
|
||||||
|
num_items_z=2,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
layout="row-major", # 使用行优先避免上下颠倒
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_2x2x1(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 2x2x1仓库(2行×2列×1层)
|
||||||
|
|
||||||
|
布局:
|
||||||
|
A01 | A02
|
||||||
|
B01 | B02
|
||||||
|
"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=2, # 2列
|
||||||
|
num_items_y=2, # 2行
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
layout="row-major", # 使用行优先避免上下颠倒
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def bioyond_warehouse_10x1x1(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 10x1x1仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=10,
|
||||||
|
num_items_y=1,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x3x3(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 1x3x3仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=1,
|
||||||
|
num_items_y=3,
|
||||||
|
num_items_z=3,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=120.0, # 增大Y方向间距以避免重叠
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_5x3x1(name: str, row_offset: int = 0) -> WareHouse:
|
||||||
|
"""创建BioYond 5x3x1仓库(5行×3列×1层)
|
||||||
|
|
||||||
|
标准布局(row_offset=0):
|
||||||
|
A01 | A02 | A03
|
||||||
|
B01 | B02 | B03
|
||||||
|
C01 | C02 | C03
|
||||||
|
D01 | D02 | D03
|
||||||
|
E01 | E02 | E03
|
||||||
|
|
||||||
|
带偏移布局(row_offset=5):
|
||||||
|
F01 | F02 | F03
|
||||||
|
G01 | G02 | G03
|
||||||
|
H01 | H02 | H03
|
||||||
|
I01 | I02 | I03
|
||||||
|
J01 | J02 | J03
|
||||||
|
"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3, # 3列
|
||||||
|
num_items_y=5, # 5行
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=120.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=0,
|
||||||
|
row_offset=row_offset, # 支持行偏移
|
||||||
|
layout="row-major", # 使用行优先避免颠倒
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def bioyond_warehouse_3x3x1(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 3x3x1仓库(3行×3列×1层)
|
||||||
|
|
||||||
|
布局:
|
||||||
|
A01 | A02 | A03
|
||||||
|
B01 | B02 | B03
|
||||||
|
C01 | C02 | C03
|
||||||
|
"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=3,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
layout="row-major", # ⭐ 使用行优先避免上下颠倒
|
||||||
|
)
|
||||||
|
def bioyond_warehouse_2x1x3(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 2x1x3仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=2,
|
||||||
|
num_items_y=1,
|
||||||
|
num_items_z=3,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def bioyond_warehouse_5x1x1(name: str) -> WareHouse:
|
||||||
|
"""已弃用:创建BioYond 5x1x1仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=5,
|
||||||
|
num_items_y=1,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_3x3x1_2(name: str) -> WareHouse:
|
||||||
|
"""已弃用:创建BioYond 3x3x1仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=3,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=12.0,
|
||||||
|
dy=12.0,
|
||||||
|
dz=12.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond开关盖加液模块台面"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=2,
|
||||||
|
num_items_y=5,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
removed_positions=None
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x8x4(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 8x4x1反应站堆栈(A01~D08)"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=8, # 8列(01-08)
|
||||||
|
num_items_y=4, # 4行(A-D)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=147.0,
|
||||||
|
item_dy=106.0,
|
||||||
|
item_dz=130.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
@@ -193,3 +193,20 @@ def BIOYOND_PolymerStation_Flask(
|
|||||||
barcode=barcode,
|
barcode=barcode,
|
||||||
model="BIOYOND_PolymerStation_Flask",
|
model="BIOYOND_PolymerStation_Flask",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def BIOYOND_PolymerStation_Measurement_Vial(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 25.0,
|
||||||
|
height: float = 60.0,
|
||||||
|
max_volume: float = 20000.0, # 20mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建测量小瓶"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="BIOYOND_PolymerStation_Measurement_Vial",
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
from os import name
|
from os import name
|
||||||
from pylabrobot.resources import Deck, Coordinate, Rotation
|
from pylabrobot.resources import Deck, Coordinate, Rotation
|
||||||
|
|
||||||
from unilabos.resources.bioyond.warehouses import (
|
from unilabos.resources.bioyond.YB_warehouses import (
|
||||||
bioyond_warehouse_1x4x4,
|
bioyond_warehouse_1x4x4,
|
||||||
bioyond_warehouse_1x4x4_right, # 新增:右侧仓库 (A05~D08)
|
bioyond_warehouse_1x4x4_right, # 新增:右侧仓库 (A05~D08)
|
||||||
bioyond_warehouse_1x4x2,
|
bioyond_warehouse_1x4x2,
|
||||||
bioyond_warehouse_reagent_stack, # 新增:试剂堆栈 (A1-B4)
|
bioyond_warehouse_reagent_stack, # 新增:试剂堆栈 (A1-B4)
|
||||||
bioyond_warehouse_liquid_and_lid_handling,
|
bioyond_warehouse_liquid_and_lid_handling,
|
||||||
bioyond_warehouse_1x2x2,
|
bioyond_warehouse_1x2x2,
|
||||||
|
bioyond_warehouse_2x2x1, # 新增:321和43窗口 (2行×2列)
|
||||||
bioyond_warehouse_1x3x3,
|
bioyond_warehouse_1x3x3,
|
||||||
|
bioyond_warehouse_5x3x1, # 新增:手动传递窗仓库 (5行×3列)
|
||||||
bioyond_warehouse_10x1x1,
|
bioyond_warehouse_10x1x1,
|
||||||
bioyond_warehouse_3x3x1,
|
bioyond_warehouse_3x3x1,
|
||||||
bioyond_warehouse_3x3x1_2,
|
bioyond_warehouse_3x3x1_2,
|
||||||
@@ -16,9 +18,12 @@ from unilabos.resources.bioyond.warehouses import (
|
|||||||
bioyond_warehouse_1x8x4,
|
bioyond_warehouse_1x8x4,
|
||||||
bioyond_warehouse_reagent_storage,
|
bioyond_warehouse_reagent_storage,
|
||||||
# bioyond_warehouse_liquid_preparation,
|
# bioyond_warehouse_liquid_preparation,
|
||||||
bioyond_warehouse_tipbox_storage, # 新增:Tip盒堆栈
|
|
||||||
bioyond_warehouse_density_vial,
|
bioyond_warehouse_density_vial,
|
||||||
)
|
)
|
||||||
|
from unilabos.resources.bioyond.warehouses import (
|
||||||
|
bioyond_warehouse_tipbox_storage_left, # 新增:Tip盒堆栈(左)
|
||||||
|
bioyond_warehouse_tipbox_storage_right, # 新增:Tip盒堆栈(右)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BIOYOND_PolymerReactionStation_Deck(Deck):
|
class BIOYOND_PolymerReactionStation_Deck(Deck):
|
||||||
@@ -45,24 +50,22 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
|
|||||||
"堆栈1右": bioyond_warehouse_1x4x4_right("堆栈1右"), # 右侧堆栈: A05~D08
|
"堆栈1右": bioyond_warehouse_1x4x4_right("堆栈1右"), # 右侧堆栈: A05~D08
|
||||||
"站内试剂存放堆栈": bioyond_warehouse_reagent_storage("站内试剂存放堆栈"), # A01~A02
|
"站内试剂存放堆栈": bioyond_warehouse_reagent_storage("站内试剂存放堆栈"), # A01~A02
|
||||||
# "移液站内10%分装液体准备仓库": bioyond_warehouse_liquid_preparation("移液站内10%分装液体准备仓库"), # A01~B04
|
# "移液站内10%分装液体准备仓库": bioyond_warehouse_liquid_preparation("移液站内10%分装液体准备仓库"), # A01~B04
|
||||||
"站内Tip盒堆栈": bioyond_warehouse_tipbox_storage("站内Tip盒堆栈"), # A01~B03, 存放枪头盒.
|
"站内Tip盒堆栈(左)": bioyond_warehouse_tipbox_storage_left("站内Tip盒堆栈(左)"), # A02~B03
|
||||||
|
"站内Tip盒堆栈(右)": bioyond_warehouse_tipbox_storage_right("站内Tip盒堆栈(右)"), # A01~B01
|
||||||
"测量小瓶仓库(测密度)": bioyond_warehouse_density_vial("测量小瓶仓库(测密度)"), # A01~B03
|
"测量小瓶仓库(测密度)": bioyond_warehouse_density_vial("测量小瓶仓库(测密度)"), # A01~B03
|
||||||
}
|
}
|
||||||
self.warehouse_locations = {
|
self.warehouse_locations = {
|
||||||
"堆栈1左": Coordinate(0.0, 430.0, 0.0), # 左侧位置
|
"堆栈1左": Coordinate(-200.0, 400.0, 0.0), # 左侧位置
|
||||||
"堆栈1右": Coordinate(2500.0, 430.0, 0.0), # 右侧位置
|
"堆栈1右": Coordinate(2350.0, 400.0, 0.0), # 右侧位置
|
||||||
"站内试剂存放堆栈": Coordinate(640.0, 480.0, 0.0),
|
"站内试剂存放堆栈": Coordinate(640.0, 400.0, 0.0),
|
||||||
# "移液站内10%分装液体准备仓库": Coordinate(1200.0, 600.0, 0.0),
|
"站内Tip盒堆栈(左)": Coordinate(300.0, 100.0, 0.0),
|
||||||
"站内Tip盒堆栈": Coordinate(300.0, 150.0, 0.0),
|
"站内Tip盒堆栈(右)": Coordinate(2250.0, 100.0, 0.0), # 向右偏移 2 * item_dx (137.0)
|
||||||
"测量小瓶仓库(测密度)": Coordinate(922.0, 552.0, 0.0),
|
"测量小瓶仓库(测密度)": Coordinate(1000.0, 530.0, 0.0),
|
||||||
}
|
}
|
||||||
self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90)
|
|
||||||
self.warehouses["测量小瓶仓库(测密度)"].rotation = Rotation(z=270)
|
|
||||||
|
|
||||||
for warehouse_name, warehouse in self.warehouses.items():
|
for warehouse_name, warehouse in self.warehouses.items():
|
||||||
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||||
|
|
||||||
|
|
||||||
class BIOYOND_PolymerPreparationStation_Deck(Deck):
|
class BIOYOND_PolymerPreparationStation_Deck(Deck):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -90,9 +93,9 @@ class BIOYOND_PolymerPreparationStation_Deck(Deck):
|
|||||||
"溶液堆栈": bioyond_warehouse_1x4x4("溶液堆栈"), # 4行×4列 (A01-D04)
|
"溶液堆栈": bioyond_warehouse_1x4x4("溶液堆栈"), # 4行×4列 (A01-D04)
|
||||||
}
|
}
|
||||||
self.warehouse_locations = {
|
self.warehouse_locations = {
|
||||||
"粉末堆栈": Coordinate(0.0, 450.0, 0.0),
|
"粉末堆栈": Coordinate(-200.0, 400.0, 0.0),
|
||||||
"试剂堆栈": Coordinate(1850.0, 200.0, 0.0),
|
"试剂堆栈": Coordinate(1750.0, 160.0, 0.0),
|
||||||
"溶液堆栈": Coordinate(2500.0, 450.0, 0.0),
|
"溶液堆栈": Coordinate(2350.0, 400.0, 0.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
for warehouse_name, warehouse in self.warehouses.items():
|
for warehouse_name, warehouse in self.warehouses.items():
|
||||||
@@ -115,10 +118,10 @@ class BIOYOND_YB_Deck(Deck):
|
|||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
# 添加仓库
|
# 添加仓库
|
||||||
self.warehouses = {
|
self.warehouses = {
|
||||||
"321窗口": bioyond_warehouse_1x2x2("321窗口"),
|
"321窗口": bioyond_warehouse_2x2x1("321窗口"), # 2行×2列
|
||||||
"43窗口": bioyond_warehouse_1x2x2("43窗口"),
|
"43窗口": bioyond_warehouse_2x2x1("43窗口"), # 2行×2列
|
||||||
"手动传递窗左": bioyond_warehouse_1x3x3("手动传递窗左"),
|
"手动传递窗右": bioyond_warehouse_5x3x1("手动传递窗右", row_offset=0), # A01-E03
|
||||||
"手动传递窗右": bioyond_warehouse_1x3x3("手动传递窗右"),
|
"手动传递窗左": bioyond_warehouse_5x3x1("手动传递窗左", row_offset=5), # F01-J03
|
||||||
"加样头堆栈左": bioyond_warehouse_10x1x1("加样头堆栈左"),
|
"加样头堆栈左": bioyond_warehouse_10x1x1("加样头堆栈左"),
|
||||||
"加样头堆栈右": bioyond_warehouse_10x1x1("加样头堆栈右"),
|
"加样头堆栈右": bioyond_warehouse_10x1x1("加样头堆栈右"),
|
||||||
|
|
||||||
@@ -126,6 +129,7 @@ class BIOYOND_YB_Deck(Deck):
|
|||||||
"母液加样右": bioyond_warehouse_3x3x1_2("母液加样右"),
|
"母液加样右": bioyond_warehouse_3x3x1_2("母液加样右"),
|
||||||
"大瓶母液堆栈左": bioyond_warehouse_5x1x1("大瓶母液堆栈左"),
|
"大瓶母液堆栈左": bioyond_warehouse_5x1x1("大瓶母液堆栈左"),
|
||||||
"大瓶母液堆栈右": bioyond_warehouse_5x1x1("大瓶母液堆栈右"),
|
"大瓶母液堆栈右": bioyond_warehouse_5x1x1("大瓶母液堆栈右"),
|
||||||
|
"2号手套箱内部堆栈": bioyond_warehouse_3x3x1("2号手套箱内部堆栈"), # 新增:3行×3列 (A01-C03)
|
||||||
}
|
}
|
||||||
# warehouse 的位置
|
# warehouse 的位置
|
||||||
self.warehouse_locations = {
|
self.warehouse_locations = {
|
||||||
@@ -140,10 +144,12 @@ class BIOYOND_YB_Deck(Deck):
|
|||||||
"母液加样右": Coordinate(2152.0, 333.0, 0.0),
|
"母液加样右": Coordinate(2152.0, 333.0, 0.0),
|
||||||
"大瓶母液堆栈左": Coordinate(1164.0, 676.0, 0.0),
|
"大瓶母液堆栈左": Coordinate(1164.0, 676.0, 0.0),
|
||||||
"大瓶母液堆栈右": Coordinate(2717.0, 676.0, 0.0),
|
"大瓶母液堆栈右": Coordinate(2717.0, 676.0, 0.0),
|
||||||
|
"2号手套箱内部堆栈": Coordinate(-800, -500.0, 0.0), # 新增:位置需根据实际硬件调整
|
||||||
}
|
}
|
||||||
|
|
||||||
for warehouse_name, warehouse in self.warehouses.items():
|
for warehouse_name, warehouse in self.warehouses.items():
|
||||||
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||||
|
|
||||||
def YB_Deck(name: str) -> Deck:
|
def YB_Deck(name: str) -> Deck:
|
||||||
by=BIOYOND_YB_Deck(name=name)
|
by=BIOYOND_YB_Deck(name=name)
|
||||||
by.setup()
|
by.setup()
|
||||||
|
|||||||
@@ -46,48 +46,62 @@ def bioyond_warehouse_1x4x4_right(name: str) -> WareHouse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def bioyond_warehouse_density_vial(name: str) -> WareHouse:
|
def bioyond_warehouse_density_vial(name: str) -> WareHouse:
|
||||||
"""创建测量小瓶仓库(测密度) A01~B03"""
|
"""创建测量小瓶仓库(测密度) - 竖向排列2列3行
|
||||||
|
布局(从下到上,从左到右):
|
||||||
|
| A03 | B03 | ← 顶部
|
||||||
|
| A02 | B02 | ← 中部
|
||||||
|
| A01 | B01 | ← 底部
|
||||||
|
"""
|
||||||
return warehouse_factory(
|
return warehouse_factory(
|
||||||
name=name,
|
name=name,
|
||||||
num_items_x=3, # 3列(01-03)
|
num_items_x=2, # 2列(A, B)
|
||||||
num_items_y=2, # 2行(A-B)
|
num_items_y=3, # 3行(01-03,从下到上)
|
||||||
num_items_z=1, # 1层
|
num_items_z=1, # 1层
|
||||||
dx=10.0,
|
dx=10.0,
|
||||||
dy=10.0,
|
dy=10.0,
|
||||||
dz=10.0,
|
dz=10.0,
|
||||||
item_dx=40.0,
|
item_dx=40.0, # 列间距(A到B的横向距离)
|
||||||
item_dy=40.0,
|
item_dy=40.0, # 行间距(01到02到03的竖向距离)
|
||||||
item_dz=50.0,
|
item_dz=50.0,
|
||||||
# 用更小的 resource_size 来表现 "小点的孔位"
|
# ⭐ 竖向warehouse:槽位尺寸也是竖向的(小瓶已经是正方形,无需调整)
|
||||||
resource_size_x=30.0,
|
resource_size_x=30.0,
|
||||||
resource_size_y=30.0,
|
resource_size_y=30.0,
|
||||||
resource_size_z=12.0,
|
resource_size_z=12.0,
|
||||||
category="warehouse",
|
category="warehouse",
|
||||||
col_offset=0,
|
col_offset=0,
|
||||||
layout="row-major",
|
layout="vertical-col-major", # ⭐ 竖向warehouse专用布局
|
||||||
)
|
)
|
||||||
|
|
||||||
def bioyond_warehouse_reagent_storage(name: str) -> WareHouse:
|
def bioyond_warehouse_reagent_storage(name: str) -> WareHouse:
|
||||||
"""创建BioYond站内试剂存放堆栈(A01~A02, 1行×2列)"""
|
"""创建BioYond站内试剂存放堆栈 - 竖向排列1列2行
|
||||||
|
布局(竖向,从下到上):
|
||||||
|
| A02 | ← 顶部
|
||||||
|
| A01 | ← 底部
|
||||||
|
"""
|
||||||
return warehouse_factory(
|
return warehouse_factory(
|
||||||
name=name,
|
name=name,
|
||||||
num_items_x=2, # 2列(01-02)
|
num_items_x=1, # 1列
|
||||||
num_items_y=1, # 1行(A)
|
num_items_y=2, # 2行(01-02,从下到上)
|
||||||
num_items_z=1, # 1层
|
num_items_z=1, # 1层
|
||||||
dx=10.0,
|
dx=10.0,
|
||||||
dy=10.0,
|
dy=10.0,
|
||||||
dz=10.0,
|
dz=10.0,
|
||||||
item_dx=137.0,
|
item_dx=96.0, # 列间距(这里只有1列,不重要)
|
||||||
item_dy=96.0,
|
item_dy=137.0, # 行间距(A01到A02的竖向距离)
|
||||||
item_dz=120.0,
|
item_dz=120.0,
|
||||||
|
# ⭐ 竖向warehouse:交换槽位尺寸,使槽位框也是竖向的
|
||||||
|
resource_size_x=86.0, # 原来的 resource_size_y
|
||||||
|
resource_size_y=127.0, # 原来的 resource_size_x
|
||||||
|
resource_size_z=25.0,
|
||||||
category="warehouse",
|
category="warehouse",
|
||||||
|
layout="vertical-col-major", # ⭐ 竖向warehouse专用布局
|
||||||
)
|
)
|
||||||
|
|
||||||
def bioyond_warehouse_tipbox_storage(name: str) -> WareHouse:
|
def bioyond_warehouse_tipbox_storage_left(name: str) -> WareHouse:
|
||||||
"""创建BioYond站内Tip盒堆栈(A01~B03),用于存放枪头盒"""
|
"""创建BioYond站内Tip盒堆栈左侧部分(A02~B03),2列2行"""
|
||||||
return warehouse_factory(
|
return warehouse_factory(
|
||||||
name=name,
|
name=name,
|
||||||
num_items_x=3, # 3列(01-03)
|
num_items_x=2, # 2列
|
||||||
num_items_y=2, # 2行(A-B)
|
num_items_y=2, # 2行(A-B)
|
||||||
num_items_z=1, # 1层
|
num_items_z=1, # 1层
|
||||||
dx=10.0,
|
dx=10.0,
|
||||||
@@ -97,7 +111,25 @@ def bioyond_warehouse_tipbox_storage(name: str) -> WareHouse:
|
|||||||
item_dy=96.0,
|
item_dy=96.0,
|
||||||
item_dz=120.0,
|
item_dz=120.0,
|
||||||
category="warehouse",
|
category="warehouse",
|
||||||
col_offset=0,
|
col_offset=1, # 从02开始: A02, A03
|
||||||
|
layout="row-major",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_tipbox_storage_right(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond站内Tip盒堆栈右侧部分(A01~B01),1列2行"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=1, # 1列
|
||||||
|
num_items_y=2, # 2行(A-B)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=0, # 从01开始: A01
|
||||||
layout="row-major",
|
layout="row-major",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -819,6 +819,22 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
if not locations:
|
if not locations:
|
||||||
logger.debug(f"[物料位置] {unique_name} 没有location信息,跳过warehouse放置")
|
logger.debug(f"[物料位置] {unique_name} 没有location信息,跳过warehouse放置")
|
||||||
|
|
||||||
|
# ⭐ 预先检查:如果物料的任何location在竖向warehouse中,提前交换尺寸
|
||||||
|
# 这样可以避免多个location时尺寸不一致的问题
|
||||||
|
needs_size_swap = False
|
||||||
|
for loc in locations:
|
||||||
|
wh_name_check = loc.get("whName")
|
||||||
|
if wh_name_check in ["站内试剂存放堆栈", "测量小瓶仓库(测密度)"]:
|
||||||
|
needs_size_swap = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if needs_size_swap and hasattr(plr_material, 'size_x') and hasattr(plr_material, 'size_y'):
|
||||||
|
original_x = plr_material.size_x
|
||||||
|
original_y = plr_material.size_y
|
||||||
|
plr_material.size_x = original_y
|
||||||
|
plr_material.size_y = original_x
|
||||||
|
logger.debug(f" 物料 {unique_name} 将放入竖向warehouse,预先交换尺寸: {original_x}×{original_y} → {plr_material.size_x}×{plr_material.size_y}")
|
||||||
|
|
||||||
for loc in locations:
|
for loc in locations:
|
||||||
wh_name = loc.get("whName")
|
wh_name = loc.get("whName")
|
||||||
logger.debug(f"[物料位置] {unique_name} 尝试放置到 warehouse: {wh_name} (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')}, z={loc.get('z')})")
|
logger.debug(f"[物料位置] {unique_name} 尝试放置到 warehouse: {wh_name} (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')}, z={loc.get('z')})")
|
||||||
@@ -835,12 +851,20 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
logger.warning(f"物料 {material['name']} 的列号 x={x_val} 超出范围,无法映射到堆栈1左或堆栈1右")
|
logger.warning(f"物料 {material['name']} 的列号 x={x_val} 超出范围,无法映射到堆栈1左或堆栈1右")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 特殊处理: Bioyond的"站内Tip盒堆栈"也需要进行拆分映射
|
||||||
|
if wh_name == "站内Tip盒堆栈":
|
||||||
|
y_val = loc.get("y", 1)
|
||||||
|
if y_val == 1:
|
||||||
|
wh_name = "站内Tip盒堆栈(右)"
|
||||||
|
elif y_val in [2, 3]:
|
||||||
|
wh_name = "站内Tip盒堆栈(左)"
|
||||||
|
y = y - 1 # 调整列号,因为左侧仓库对应的 Bioyond y=2 实际上是它的第1列
|
||||||
|
|
||||||
if hasattr(deck, "warehouses") and wh_name in deck.warehouses:
|
if hasattr(deck, "warehouses") and wh_name in deck.warehouses:
|
||||||
warehouse = deck.warehouses[wh_name]
|
warehouse = deck.warehouses[wh_name]
|
||||||
logger.debug(f"[Warehouse匹配] 找到warehouse: {wh_name} (容量: {warehouse.capacity}, 行×列: {warehouse.num_items_x}×{warehouse.num_items_y})")
|
logger.debug(f"[Warehouse匹配] 找到warehouse: {wh_name} (容量: {warehouse.capacity}, 行×列: {warehouse.num_items_x}×{warehouse.num_items_y})")
|
||||||
|
|
||||||
# Bioyond坐标映射 (重要!): x→行(1=A,2=B...), y→列(1=01,2=02...), z→层(通常=1)
|
# Bioyond坐标映射 (重要!): x→行(1=A,2=B...), y→列(1=01,2=02...), z→层(通常=1)
|
||||||
# PyLabRobot warehouse是列优先存储: A01,B01,C01,D01, A02,B02,C02,D02, ...
|
|
||||||
x = loc.get("x", 1) # 行号 (1-based: 1=A, 2=B, 3=C, 4=D)
|
x = loc.get("x", 1) # 行号 (1-based: 1=A, 2=B, 3=C, 4=D)
|
||||||
y = loc.get("y", 1) # 列号 (1-based: 1=01, 2=02, 3=03...)
|
y = loc.get("y", 1) # 列号 (1-based: 1=01, 2=02, 3=03...)
|
||||||
z = loc.get("z", 1) # 层号 (1-based, 通常为1)
|
z = loc.get("z", 1) # 层号 (1-based, 通常为1)
|
||||||
@@ -849,12 +873,23 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
if wh_name == "堆栈1右":
|
if wh_name == "堆栈1右":
|
||||||
y = y - 4 # 将5-8映射到1-4
|
y = y - 4 # 将5-8映射到1-4
|
||||||
|
|
||||||
# 特殊处理:对于1行×N列的横向warehouse(如站内试剂存放堆栈)
|
# 特殊处理竖向warehouse(站内试剂存放堆栈、测量小瓶仓库)
|
||||||
# Bioyond的y坐标表示线性位置序号,而不是列号
|
# 这些warehouse使用 vertical-col-major 布局
|
||||||
if warehouse.num_items_y == 1:
|
if wh_name in ["站内试剂存放堆栈", "测量小瓶仓库(测密度)"]:
|
||||||
# 1行warehouse: 直接用y作为线性索引
|
# vertical-col-major 布局的坐标映射:
|
||||||
idx = y - 1
|
# - Bioyond的x(1=A,2=B)对应warehouse的列(col, x方向)
|
||||||
logger.debug(f"1行warehouse {wh_name}: y={y} → idx={idx}")
|
# - Bioyond的y(1=01,2=02,3=03)对应warehouse的行(row, y方向),从下到上
|
||||||
|
# vertical-col-major 中: row=0 对应底部,row=n-1 对应顶部
|
||||||
|
# Bioyond y=1(01) 对应底部 → row=0, y=2(02) 对应中间 → row=1
|
||||||
|
# 索引计算: idx = row * num_cols + col
|
||||||
|
col_idx = x - 1 # Bioyond的x(A,B) → col索引(0,1)
|
||||||
|
row_idx = y - 1 # Bioyond的y(01,02,03) → row索引(0,1,2)
|
||||||
|
layer_idx = z - 1
|
||||||
|
|
||||||
|
idx = layer_idx * (warehouse.num_items_x * warehouse.num_items_y) + row_idx * warehouse.num_items_y + col_idx
|
||||||
|
logger.debug(f"🔍 竖向warehouse {wh_name}: Bioyond(x={x},y={y},z={z}) → warehouse(col={col_idx},row={row_idx},layer={layer_idx}) → idx={idx}, capacity={warehouse.capacity}")
|
||||||
|
|
||||||
|
# 普通横向warehouse的处理
|
||||||
else:
|
else:
|
||||||
# 多行warehouse: 根据 layout 使用不同的索引计算
|
# 多行warehouse: 根据 layout 使用不同的索引计算
|
||||||
row_idx = x - 1 # x表示行: 转为0-based
|
row_idx = x - 1 # x表示行: 转为0-based
|
||||||
@@ -878,6 +913,7 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
|
|
||||||
if 0 <= idx < warehouse.capacity:
|
if 0 <= idx < warehouse.capacity:
|
||||||
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
|
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
|
||||||
|
# 物料尺寸已在放入warehouse前根据需要进行了交换
|
||||||
warehouse[idx] = plr_material
|
warehouse[idx] = plr_material
|
||||||
logger.debug(f"✅ 物料 {unique_name} 放置到 {wh_name}[{idx}] (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')})")
|
logger.debug(f"✅ 物料 {unique_name} 放置到 {wh_name}[{idx}] (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')})")
|
||||||
else:
|
else:
|
||||||
@@ -1051,11 +1087,24 @@ def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict
|
|||||||
logger.debug(f" 📭 [单瓶物料] {resource.name} 无液体,使用资源名: {material_name}")
|
logger.debug(f" 📭 [单瓶物料] {resource.name} 无液体,使用资源名: {material_name}")
|
||||||
|
|
||||||
# 🎯 处理物料默认参数和单位
|
# 🎯 处理物料默认参数和单位
|
||||||
# 检查是否有该物料名称的默认参数配置
|
# 优先级: typeId参数 > 物料名称参数 > 默认值
|
||||||
default_unit = "个" # 默认单位
|
default_unit = "个" # 默认单位
|
||||||
material_parameters = {}
|
material_parameters = {}
|
||||||
|
|
||||||
if material_name in material_params:
|
# 1️⃣ 首先检查是否有 typeId 对应的参数配置(从 material_params 中获取,key 格式为 "type:<typeId>")
|
||||||
|
type_params_key = f"type:{type_id}"
|
||||||
|
if type_params_key in material_params:
|
||||||
|
params_config = material_params[type_params_key].copy()
|
||||||
|
|
||||||
|
# 提取 unit 字段(如果有)
|
||||||
|
if "unit" in params_config:
|
||||||
|
default_unit = params_config.pop("unit") # 从参数中移除,放到外层
|
||||||
|
|
||||||
|
# 剩余的字段放入 Parameters
|
||||||
|
material_parameters = params_config
|
||||||
|
logger.debug(f" 🔧 [物料参数-按typeId] 为 typeId={type_id[:8]}... 应用配置: unit={default_unit}, parameters={material_parameters}")
|
||||||
|
# 2️⃣ 其次检查是否有该物料名称的默认参数配置
|
||||||
|
elif material_name in material_params:
|
||||||
params_config = material_params[material_name].copy()
|
params_config = material_params[material_name].copy()
|
||||||
|
|
||||||
# 提取 unit 字段(如果有)
|
# 提取 unit 字段(如果有)
|
||||||
@@ -1064,7 +1113,7 @@ def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict
|
|||||||
|
|
||||||
# 剩余的字段放入 Parameters
|
# 剩余的字段放入 Parameters
|
||||||
material_parameters = params_config
|
material_parameters = params_config
|
||||||
logger.debug(f" 🔧 [物料参数] 为 {material_name} 应用配置: unit={default_unit}, parameters={material_parameters}")
|
logger.debug(f" 🔧 [物料参数-按名称] 为 {material_name} 应用配置: unit={default_unit}, parameters={material_parameters}")
|
||||||
|
|
||||||
# 转换为 JSON 字符串
|
# 转换为 JSON 字符串
|
||||||
parameters_json = json.dumps(material_parameters) if material_parameters else "{}"
|
parameters_json = json.dumps(material_parameters) if material_parameters else "{}"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Bottle(Well):
|
|||||||
size_x: float = 0.0,
|
size_x: float = 0.0,
|
||||||
size_y: float = 0.0,
|
size_y: float = 0.0,
|
||||||
size_z: float = 0.0,
|
size_z: float = 0.0,
|
||||||
barcode: Optional[str] = "",
|
barcode: Optional[str] = None,
|
||||||
category: str = "container",
|
category: str = "container",
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -50,13 +50,45 @@ class Bottle(Well):
|
|||||||
self.barcode = barcode
|
self.barcode = barcode
|
||||||
|
|
||||||
def serialize(self) -> dict:
|
def serialize(self) -> dict:
|
||||||
|
# Pylabrobot expects barcode to be an object with serialize(), but here it is a str.
|
||||||
|
# We temporarily unset it to avoid AttributeError in super().serialize().
|
||||||
|
_barcode = self.barcode
|
||||||
|
self.barcode = None
|
||||||
|
try:
|
||||||
|
data = super().serialize()
|
||||||
|
finally:
|
||||||
|
self.barcode = _barcode
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**super().serialize(),
|
**data,
|
||||||
"diameter": self.diameter,
|
"diameter": self.diameter,
|
||||||
"height": self.height,
|
"height": self.height,
|
||||||
"barcode": self.barcode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def deserialize(cls, data: dict, allow_marshal: bool = False):
|
||||||
|
# Extract barcode before calling parent deserialize to avoid type error
|
||||||
|
barcode_data = data.pop("barcode", None)
|
||||||
|
|
||||||
|
# Call parent deserialize
|
||||||
|
instance = super(Bottle, cls).deserialize(data, allow_marshal=allow_marshal)
|
||||||
|
|
||||||
|
# Set barcode as string (not as Barcode object)
|
||||||
|
if barcode_data:
|
||||||
|
if isinstance(barcode_data, str):
|
||||||
|
instance.barcode = barcode_data
|
||||||
|
elif isinstance(barcode_data, dict):
|
||||||
|
# If it's a dict (Barcode serialized format), extract the data field
|
||||||
|
instance.barcode = barcode_data.get("data", "")
|
||||||
|
else:
|
||||||
|
instance.barcode = ""
|
||||||
|
|
||||||
|
# Set additional attributes
|
||||||
|
instance.diameter = data.get("diameter", instance._size_x)
|
||||||
|
instance.height = data.get("height", instance._size_z)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
T = TypeVar("T", bound=ResourceHolder)
|
T = TypeVar("T", bound=ResourceHolder)
|
||||||
|
|
||||||
S = TypeVar("S", bound=ResourceHolder)
|
S = TypeVar("S", bound=ResourceHolder)
|
||||||
@@ -79,7 +111,6 @@ class ItemizedCarrier(ResourcePLR):
|
|||||||
category: Optional[str] = "carrier",
|
category: Optional[str] = "carrier",
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
invisible_slots: Optional[str] = None,
|
invisible_slots: Optional[str] = None,
|
||||||
content_type: Optional[List[str]] = ["bottle", "container", "tube", "bottle_carrier", "tip_rack"],
|
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
@@ -93,7 +124,6 @@ class ItemizedCarrier(ResourcePLR):
|
|||||||
self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z
|
self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z
|
||||||
self.invisible_slots = [] if invisible_slots is None else invisible_slots
|
self.invisible_slots = [] if invisible_slots is None else invisible_slots
|
||||||
self.layout = "z-y" if self.num_items_z > 1 and self.num_items_x == 1 else "x-z" if self.num_items_z > 1 and self.num_items_y == 1 else "x-y"
|
self.layout = "z-y" if self.num_items_z > 1 and self.num_items_x == 1 else "x-z" if self.num_items_z > 1 and self.num_items_y == 1 else "x-y"
|
||||||
self.content_type = content_type
|
|
||||||
|
|
||||||
if isinstance(sites, dict):
|
if isinstance(sites, dict):
|
||||||
sites = sites or {}
|
sites = sites or {}
|
||||||
@@ -421,7 +451,7 @@ class ItemizedCarrier(ResourcePLR):
|
|||||||
self[identifier] if isinstance(self[identifier], str) else None,
|
self[identifier] if isinstance(self[identifier], str) else None,
|
||||||
"position": {"x": location.x, "y": location.y, "z": location.z},
|
"position": {"x": location.x, "y": location.y, "z": location.z},
|
||||||
"size": self.child_size[identifier],
|
"size": self.child_size[identifier],
|
||||||
"content_type": self.content_type
|
"content_type": ["bottle", "container", "tube", "bottle_carrier", "tip_rack"]
|
||||||
} for identifier, location in self.child_locations.items()]
|
} for identifier, location in self.child_locations.items()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -123,24 +123,6 @@ class ResourceDictType(TypedDict):
|
|||||||
machine_name: str
|
machine_name: str
|
||||||
|
|
||||||
|
|
||||||
class ResourceDictType(TypedDict):
|
|
||||||
id: str
|
|
||||||
uuid: str
|
|
||||||
name: str
|
|
||||||
description: str
|
|
||||||
resource_schema: Dict[str, Any]
|
|
||||||
model: Dict[str, Any]
|
|
||||||
icon: str
|
|
||||||
parent_uuid: Optional[str]
|
|
||||||
parent: Optional["ResourceDictType"]
|
|
||||||
type: Union[Literal["device"], str]
|
|
||||||
klass: str
|
|
||||||
pose: ResourceDictPositionType
|
|
||||||
config: Dict[str, Any]
|
|
||||||
data: Dict[str, Any]
|
|
||||||
extra: Dict[str, Any]
|
|
||||||
|
|
||||||
|
|
||||||
# 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化
|
# 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化
|
||||||
class ResourceDict(BaseModel):
|
class ResourceDict(BaseModel):
|
||||||
id: str = Field(description="Resource ID")
|
id: str = Field(description="Resource ID")
|
||||||
@@ -571,17 +553,10 @@ class ResourceTreeSet(object):
|
|||||||
trees.append(tree_instance)
|
trees.append(tree_instance)
|
||||||
return cls(trees)
|
return cls(trees)
|
||||||
|
|
||||||
def to_plr_resources(
|
def to_plr_resources(self, skip_devices=True) -> List["PLRResource"]:
|
||||||
self, skip_devices: bool = True, requested_uuids: Optional[List[str]] = None
|
|
||||||
) -> List["PLRResource"]:
|
|
||||||
"""
|
"""
|
||||||
将 ResourceTreeSet 转换为 PLR 资源列表
|
将 ResourceTreeSet 转换为 PLR 资源列表
|
||||||
|
|
||||||
Args:
|
|
||||||
skip_devices: 是否跳过 device 类型节点
|
|
||||||
requested_uuids: 若指定,则按此 UUID 顺序返回对应资源(用于批量查询时一一对应),
|
|
||||||
否则返回各树的根节点列表
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[PLRResource]: PLR 资源实例列表
|
List[PLRResource]: PLR 资源实例列表
|
||||||
"""
|
"""
|
||||||
@@ -637,71 +612,6 @@ class ResourceTreeSet(object):
|
|||||||
d["model"] = res.config.get("model", None)
|
d["model"] = res.config.get("model", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
# deserialize 会单独处理的元数据 key,不传给构造函数
|
|
||||||
_META_KEYS = {"type", "parent_name", "location", "children", "rotation", "barcode"}
|
|
||||||
# deserialize 自定义逻辑使用的 key(如 TipSpot 用 prototype_tip 构建 make_tip),需保留
|
|
||||||
_DESERIALIZE_PRESERVED_KEYS = {"prototype_tip"}
|
|
||||||
|
|
||||||
def remove_incompatible_params(plr_d: dict) -> None:
|
|
||||||
"""递归移除 PLR 类不接受的参数,避免 deserialize 报错。
|
|
||||||
- 移除构造函数不接受的参数(如 compute_height_from_volume、ordering、category)
|
|
||||||
- 对 TubeRack:将 ordering 转为 ordered_items
|
|
||||||
- 保留 deserialize 自定义逻辑需要的 key(如 prototype_tip)
|
|
||||||
"""
|
|
||||||
if "type" in plr_d:
|
|
||||||
sub_cls = find_subclass(plr_d["type"], PLRResource)
|
|
||||||
if sub_cls is not None:
|
|
||||||
spec = inspect.signature(sub_cls)
|
|
||||||
valid_params = set(spec.parameters.keys())
|
|
||||||
# TubeRack 特殊处理:先转换 ordering,再参与后续过滤
|
|
||||||
if "ordering" not in valid_params and "ordering" in plr_d:
|
|
||||||
ordering = plr_d.pop("ordering", None)
|
|
||||||
if sub_cls.__name__ == "TubeRack":
|
|
||||||
plr_d["ordered_items"] = (
|
|
||||||
_ordering_to_ordered_items(plr_d, ordering)
|
|
||||||
if ordering
|
|
||||||
else {}
|
|
||||||
)
|
|
||||||
# 移除构造函数不接受的参数(保留 META 和 deserialize 自定义逻辑需要的 key)
|
|
||||||
for key in list(plr_d.keys()):
|
|
||||||
if (
|
|
||||||
key not in _META_KEYS
|
|
||||||
and key not in _DESERIALIZE_PRESERVED_KEYS
|
|
||||||
and key not in valid_params
|
|
||||||
):
|
|
||||||
plr_d.pop(key, None)
|
|
||||||
for child in plr_d.get("children", []):
|
|
||||||
remove_incompatible_params(child)
|
|
||||||
|
|
||||||
def _ordering_to_ordered_items(plr_d: dict, ordering: dict) -> dict:
|
|
||||||
"""将 ordering 转为 ordered_items,从 children 构建 Tube 对象"""
|
|
||||||
from pylabrobot.resources import Tube, Coordinate
|
|
||||||
from pylabrobot.serializer import deserialize as plr_deserialize
|
|
||||||
|
|
||||||
children = plr_d.get("children", [])
|
|
||||||
ordered_items = {}
|
|
||||||
for idx, (ident, child_name) in enumerate(ordering.items()):
|
|
||||||
child_data = children[idx] if idx < len(children) else None
|
|
||||||
if child_data is None:
|
|
||||||
continue
|
|
||||||
loc_data = child_data.get("location")
|
|
||||||
loc = (
|
|
||||||
plr_deserialize(loc_data)
|
|
||||||
if loc_data
|
|
||||||
else Coordinate(0, 0, 0)
|
|
||||||
)
|
|
||||||
tube = Tube(
|
|
||||||
name=child_data.get("name", child_name or ident),
|
|
||||||
size_x=child_data.get("size_x", 10),
|
|
||||||
size_y=child_data.get("size_y", 10),
|
|
||||||
size_z=child_data.get("size_z", 50),
|
|
||||||
max_volume=child_data.get("max_volume", 1000),
|
|
||||||
)
|
|
||||||
tube.location = loc
|
|
||||||
ordered_items[ident] = tube
|
|
||||||
plr_d["children"] = [] # 已并入 ordered_items,避免重复反序列化
|
|
||||||
return ordered_items
|
|
||||||
|
|
||||||
plr_resources = []
|
plr_resources = []
|
||||||
tracker = DeviceNodeResourceTracker()
|
tracker = DeviceNodeResourceTracker()
|
||||||
|
|
||||||
@@ -721,7 +631,9 @@ class ResourceTreeSet(object):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}"
|
f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}"
|
||||||
)
|
)
|
||||||
remove_incompatible_params(plr_dict)
|
spec = inspect.signature(sub_cls)
|
||||||
|
if "category" not in spec.parameters:
|
||||||
|
plr_dict.pop("category", None)
|
||||||
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
|
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
|
||||||
from pylabrobot.resources import Coordinate
|
from pylabrobot.resources import Coordinate
|
||||||
from pylabrobot.serializer import deserialize
|
from pylabrobot.serializer import deserialize
|
||||||
@@ -741,18 +653,6 @@ class ResourceTreeSet(object):
|
|||||||
logger.error(f"堆栈: {traceback.format_exc()}")
|
logger.error(f"堆栈: {traceback.format_exc()}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if requested_uuids:
|
|
||||||
# 按请求的 UUID 顺序返回对应资源(从整棵树中按 uuid 提取)
|
|
||||||
result = []
|
|
||||||
for uid in requested_uuids:
|
|
||||||
if uid in tracker.uuid_to_resources:
|
|
||||||
result.append(tracker.uuid_to_resources[uid])
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"请求的 UUID {uid} 在资源树中未找到。"
|
|
||||||
f"可用 UUID 数量: {len(tracker.uuid_to_resources)}"
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
return plr_resources
|
return plr_resources
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -841,6 +741,16 @@ class ResourceTreeSet(object):
|
|||||||
"""
|
"""
|
||||||
return [tree.root_node for tree in self.trees]
|
return [tree.root_node for tree in self.trees]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def root_nodes_uuid(self) -> List[ResourceDictInstance]:
|
||||||
|
"""
|
||||||
|
获取所有树的根节点
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
所有根节点的资源实例列表
|
||||||
|
"""
|
||||||
|
return [tree.root_node.res_content.uuid for tree in self.trees]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all_nodes(self) -> List[ResourceDictInstance]:
|
def all_nodes(self) -> List[ResourceDictInstance]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ def warehouse_factory(
|
|||||||
category: str = "warehouse",
|
category: str = "warehouse",
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
col_offset: int = 0, # 列起始偏移量,用于生成A05-D08等命名
|
col_offset: int = 0, # 列起始偏移量,用于生成A05-D08等命名
|
||||||
|
row_offset: int = 0, # 行起始偏移量,用于生成F01-J03等命名
|
||||||
layout: str = "col-major", # 新增:排序方式,"col-major"=列优先,"row-major"=行优先
|
layout: str = "col-major", # 新增:排序方式,"col-major"=列优先,"row-major"=行优先
|
||||||
):
|
):
|
||||||
# 创建位置坐标
|
# 创建位置坐标
|
||||||
@@ -42,6 +43,10 @@ def warehouse_factory(
|
|||||||
if layout == "row-major":
|
if layout == "row-major":
|
||||||
# 行优先:row=0(A行) 应该显示在上方,需要较小的 y 值
|
# 行优先:row=0(A行) 应该显示在上方,需要较小的 y 值
|
||||||
y = dy + row * item_dy
|
y = dy + row * item_dy
|
||||||
|
elif layout == "vertical-col-major":
|
||||||
|
# 竖向warehouse: row=0 对应顶部(y小),row=n-1 对应底部(y大)
|
||||||
|
# 但标签 01 应该在底部,所以使用反向映射
|
||||||
|
y = dy + (num_items_y - row - 1) * item_dy
|
||||||
else:
|
else:
|
||||||
# 列优先:保持原逻辑(row=0 对应较大的 y)
|
# 列优先:保持原逻辑(row=0 对应较大的 y)
|
||||||
y = dy + (num_items_y - row - 1) * item_dy
|
y = dy + (num_items_y - row - 1) * item_dy
|
||||||
@@ -65,10 +70,10 @@ def warehouse_factory(
|
|||||||
if layout == "row-major":
|
if layout == "row-major":
|
||||||
# 行优先顺序: A01,A02,A03,A04, B01,B02,B03,B04
|
# 行优先顺序: A01,A02,A03,A04, B01,B02,B03,B04
|
||||||
# locations[0] 对应 row=0, y最大(前端顶部)→ 应该是 A01
|
# locations[0] 对应 row=0, y最大(前端顶部)→ 应该是 A01
|
||||||
keys = [f"{LETTERS[j]}{i + 1 + col_offset:02d}" for j in range(len_y) for i in range(len_x)]
|
keys = [f"{LETTERS[j + row_offset]}{i + 1 + col_offset:02d}" for j in range(len_y) for i in range(len_x)]
|
||||||
else:
|
else:
|
||||||
# 列优先顺序: A01,B01,C01,D01, A02,B02,C02,D02
|
# 列优先顺序: A01,B01,C01,D01, A02,B02,C02,D02
|
||||||
keys = [f"{LETTERS[j]}{i + 1 + col_offset:02d}" for i in range(len_x) for j in range(len_y)]
|
keys = [f"{LETTERS[j + row_offset]}{i + 1 + col_offset:02d}" for i in range(len_x) for j in range(len_y)]
|
||||||
|
|
||||||
sites = {i: site for i, site in zip(keys, _sites.values())}
|
sites = {i: site for i, site in zip(keys, _sites.values())}
|
||||||
|
|
||||||
|
|||||||
@@ -1486,13 +1486,9 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
if uuid_indices:
|
if uuid_indices:
|
||||||
uuids = [item[1] for item in uuid_indices]
|
uuids = [item[1] for item in uuid_indices]
|
||||||
resource_tree = await self.get_resource(uuids)
|
resource_tree = await self.get_resource(uuids)
|
||||||
plr_resources = resource_tree.to_plr_resources(requested_uuids=uuids)
|
plr_resources = resource_tree.to_plr_resources()
|
||||||
for i, (idx, _, resource_data) in enumerate(uuid_indices):
|
for i, (idx, _, resource_data) in enumerate(uuid_indices):
|
||||||
try:
|
plr_resource = plr_resources[i]
|
||||||
plr_resource = plr_resources[i]
|
|
||||||
except Exception as e:
|
|
||||||
self.lab_logger().error(f"资源查询结果: 共 {len(queried_resources)} 个资源,但查询结果只有 {len(plr_resources)} 个资源,索引为 {i} 的资源不存在")
|
|
||||||
raise e
|
|
||||||
if "sample_id" in resource_data:
|
if "sample_id" in resource_data:
|
||||||
plr_resource.unilabos_extra[EXTRA_SAMPLE_UUID] = resource_data["sample_id"]
|
plr_resource.unilabos_extra[EXTRA_SAMPLE_UUID] = resource_data["sample_id"]
|
||||||
queried_resources[idx] = plr_resource
|
queried_resources[idx] = plr_resource
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from unilabos_msgs.srv import (
|
|||||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||||
from unique_identifier_msgs.msg import UUID
|
from unique_identifier_msgs.msg import UUID
|
||||||
|
|
||||||
from unilabos.registry.decorators import device, action, NodeType
|
from unilabos.registry.decorators import device
|
||||||
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
|
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
|
||||||
from unilabos.registry.registry import lab_registry
|
from unilabos.registry.registry import lab_registry
|
||||||
from unilabos.resources.container import RegularContainer
|
from unilabos.resources.container import RegularContainer
|
||||||
@@ -313,9 +313,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
callback_group=self.callback_group,
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
} # 用来存储多个ActionClient实例
|
} # 用来存储多个ActionClient实例
|
||||||
self._action_value_mappings: Dict[str, Dict] = {
|
self._action_value_mappings: Dict[str, Dict] = {} # device_id -> action_value_mappings(本地+远程设备统一存储)
|
||||||
device_id: self._action_value_mappings
|
|
||||||
} # device_id -> action_value_mappings(本地+远程设备统一存储)
|
|
||||||
self._slave_registry_configs: Dict[str, Dict] = {} # registry_name -> registry_config(含action_value_mappings)
|
self._slave_registry_configs: Dict[str, Dict] = {} # registry_name -> registry_config(含action_value_mappings)
|
||||||
self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态
|
self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态
|
||||||
self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备
|
self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备
|
||||||
@@ -1623,18 +1621,6 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@action(always_free=True, node_type=NodeType.MANUAL_CONFIRM, placeholder_keys={
|
|
||||||
"assignee_user_ids": "unilabos_manual_confirm"
|
|
||||||
}, goal_default={
|
|
||||||
"timeout_seconds": 3600,
|
|
||||||
"assignee_user_ids": []
|
|
||||||
})
|
|
||||||
def manual_confirm(self, timeout_seconds: int, assignee_user_ids: list[str], **kwargs) -> dict:
|
|
||||||
"""
|
|
||||||
timeout_seconds: 超时时间(秒),默认3600秒
|
|
||||||
"""
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def test_resource(
|
def test_resource(
|
||||||
self,
|
self,
|
||||||
sample_uuids: SampleUUIDsType,
|
sample_uuids: SampleUUIDsType,
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from asyncio import events
|
|
||||||
import threading
|
|
||||||
|
|
||||||
import rclpy
|
|
||||||
from rclpy.impl.implementation_singleton import rclpy_implementation as _rclpy
|
|
||||||
from rclpy.executors import await_or_execute, Executor
|
|
||||||
from rclpy.action import ActionClient, ActionServer
|
|
||||||
from rclpy.action.server import ServerGoalHandle, GoalResponse, GoalInfo, GoalStatus
|
|
||||||
from std_msgs.msg import String
|
|
||||||
from action_tutorials_interfaces.action import Fibonacci
|
|
||||||
|
|
||||||
|
|
||||||
loop = None
|
|
||||||
|
|
||||||
def get_event_loop():
|
|
||||||
global loop
|
|
||||||
return loop
|
|
||||||
|
|
||||||
|
|
||||||
async def default_handle_accepted_callback_async(goal_handle):
|
|
||||||
"""Execute the goal."""
|
|
||||||
await goal_handle.execute()
|
|
||||||
|
|
||||||
|
|
||||||
class ServerGoalHandleX(ServerGoalHandle):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
async def execute(self, execute_callback=None):
|
|
||||||
# It's possible that there has been a request to cancel the goal prior to executing.
|
|
||||||
# In this case we want to avoid the illegal state transition to EXECUTING
|
|
||||||
# but still call the users execute callback to let them handle canceling the goal.
|
|
||||||
if not self.is_cancel_requested:
|
|
||||||
self._update_state(_rclpy.GoalEvent.EXECUTE)
|
|
||||||
await self._action_server.notify_execute_async(self, execute_callback)
|
|
||||||
|
|
||||||
|
|
||||||
class ActionServerX(ActionServer):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.register_handle_accepted_callback(default_handle_accepted_callback_async)
|
|
||||||
|
|
||||||
async def _execute_goal_request(self, request_header_and_message):
|
|
||||||
request_header, goal_request = request_header_and_message
|
|
||||||
goal_uuid = goal_request.goal_id
|
|
||||||
goal_info = GoalInfo()
|
|
||||||
goal_info.goal_id = goal_uuid
|
|
||||||
|
|
||||||
self._node.get_logger().debug('New goal request with ID: {0}'.format(goal_uuid.uuid))
|
|
||||||
|
|
||||||
# Check if goal ID is already being tracked by this action server
|
|
||||||
with self._lock:
|
|
||||||
goal_id_exists = self._handle.goal_exists(goal_info)
|
|
||||||
|
|
||||||
accepted = False
|
|
||||||
if not goal_id_exists:
|
|
||||||
# Call user goal callback
|
|
||||||
response = await await_or_execute(self._goal_callback, goal_request.goal)
|
|
||||||
if not isinstance(response, GoalResponse):
|
|
||||||
self._node.get_logger().warning(
|
|
||||||
'Goal request callback did not return a GoalResponse type. Rejecting goal.')
|
|
||||||
else:
|
|
||||||
accepted = GoalResponse.ACCEPT == response
|
|
||||||
|
|
||||||
if accepted:
|
|
||||||
# Stamp time of acceptance
|
|
||||||
goal_info.stamp = self._node.get_clock().now().to_msg()
|
|
||||||
|
|
||||||
# Create a goal handle
|
|
||||||
try:
|
|
||||||
with self._lock:
|
|
||||||
goal_handle = ServerGoalHandleX(self, goal_info, goal_request.goal)
|
|
||||||
except RuntimeError as e:
|
|
||||||
self._node.get_logger().error(
|
|
||||||
'Failed to accept new goal with ID {0}: {1}'.format(goal_uuid.uuid, e))
|
|
||||||
accepted = False
|
|
||||||
else:
|
|
||||||
self._goal_handles[bytes(goal_uuid.uuid)] = goal_handle
|
|
||||||
|
|
||||||
# Send response
|
|
||||||
response_msg = self._action_type.Impl.SendGoalService.Response()
|
|
||||||
response_msg.accepted = accepted
|
|
||||||
response_msg.stamp = goal_info.stamp
|
|
||||||
self._handle.send_goal_response(request_header, response_msg)
|
|
||||||
|
|
||||||
if not accepted:
|
|
||||||
self._node.get_logger().debug('New goal rejected: {0}'.format(goal_uuid.uuid))
|
|
||||||
return
|
|
||||||
|
|
||||||
self._node.get_logger().debug('New goal accepted: {0}'.format(goal_uuid.uuid))
|
|
||||||
|
|
||||||
# Provide the user a reference to the goal handle
|
|
||||||
# await await_or_execute(self._handle_accepted_callback, goal_handle)
|
|
||||||
asyncio.create_task(self._handle_accepted_callback(goal_handle))
|
|
||||||
|
|
||||||
async def notify_execute_async(self, goal_handle, execute_callback):
|
|
||||||
# Use provided callback, defaulting to a previously registered callback
|
|
||||||
if execute_callback is None:
|
|
||||||
if self._execute_callback is None:
|
|
||||||
return
|
|
||||||
execute_callback = self._execute_callback
|
|
||||||
|
|
||||||
# Schedule user callback for execution
|
|
||||||
self._node.get_logger().info(f"{events.get_running_loop()}")
|
|
||||||
asyncio.create_task(self._execute_goal(execute_callback, goal_handle))
|
|
||||||
# loop = asyncio.new_event_loop()
|
|
||||||
# asyncio.set_event_loop(loop)
|
|
||||||
# task = loop.create_task(self._execute_goal(execute_callback, goal_handle))
|
|
||||||
# await task
|
|
||||||
|
|
||||||
|
|
||||||
class ActionClientX(ActionClient):
|
|
||||||
feedback_queue = asyncio.Queue()
|
|
||||||
|
|
||||||
async def feedback_cb(self, msg):
|
|
||||||
await self.feedback_queue.put(msg)
|
|
||||||
|
|
||||||
async def send_goal_async(self, goal_msg):
|
|
||||||
goal_future = super().send_goal_async(
|
|
||||||
goal_msg,
|
|
||||||
feedback_callback=self.feedback_cb
|
|
||||||
)
|
|
||||||
client_goal_handle = await asyncio.ensure_future(goal_future)
|
|
||||||
if not client_goal_handle.accepted:
|
|
||||||
raise Exception("Goal rejected.")
|
|
||||||
result_future = client_goal_handle.get_result_async()
|
|
||||||
while True:
|
|
||||||
feedback_future = asyncio.ensure_future(self.feedback_queue.get())
|
|
||||||
tasks = [result_future, feedback_future]
|
|
||||||
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
|
||||||
if result_future.done():
|
|
||||||
result = result_future.result().result
|
|
||||||
yield (None, result)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
feedback = feedback_future.result().feedback
|
|
||||||
yield (feedback, None)
|
|
||||||
|
|
||||||
|
|
||||||
async def main(node):
|
|
||||||
print('Node started.')
|
|
||||||
action_client = ActionClientX(node, Fibonacci, 'fibonacci')
|
|
||||||
goal_msg = Fibonacci.Goal()
|
|
||||||
goal_msg.order = 10
|
|
||||||
async for (feedback, result) in action_client.send_goal_async(goal_msg):
|
|
||||||
if feedback:
|
|
||||||
print(f'Feedback: {feedback}')
|
|
||||||
else:
|
|
||||||
print(f'Result: {result}')
|
|
||||||
print('Finished.')
|
|
||||||
|
|
||||||
|
|
||||||
async def ros_loop_node(node):
|
|
||||||
while rclpy.ok():
|
|
||||||
rclpy.spin_once(node, timeout_sec=0)
|
|
||||||
await asyncio.sleep(1e-4)
|
|
||||||
|
|
||||||
|
|
||||||
async def ros_loop(executor: Executor):
|
|
||||||
while rclpy.ok():
|
|
||||||
executor.spin_once(timeout_sec=0)
|
|
||||||
await asyncio.sleep(1e-4)
|
|
||||||
|
|
||||||
|
|
||||||
def run_event_loop():
|
|
||||||
global loop
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
loop.run_forever()
|
|
||||||
|
|
||||||
|
|
||||||
def run_event_loop_in_thread():
|
|
||||||
thread = threading.Thread(target=run_event_loop, args=())
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
rclpy.init()
|
|
||||||
node = rclpy.create_node('async_subscriber')
|
|
||||||
future = asyncio.wait([ros_loop(node), main()])
|
|
||||||
asyncio.get_event_loop().run_until_complete(future)
|
|
||||||
@@ -9,49 +9,125 @@
|
|||||||
"parent": null,
|
"parent": null,
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "bioyond_dispensing_station",
|
"class": "bioyond_dispensing_station",
|
||||||
"config": {
|
"position": {
|
||||||
"config": {
|
"x": 0,
|
||||||
"api_key": "DE9BDDA0",
|
"y": 0,
|
||||||
"api_host": "http://192.168.1.200:44388",
|
"z": 0
|
||||||
"material_type_mappings": {
|
|
||||||
"BIOYOND_PolymerStation_1FlaskCarrier": [
|
|
||||||
"烧杯",
|
|
||||||
"3a14196b-24f2-ca49-9081-0cab8021bf1a"
|
|
||||||
],
|
|
||||||
"BIOYOND_PolymerStation_1BottleCarrier": [
|
|
||||||
"试剂瓶",
|
|
||||||
"3a14196b-8bcf-a460-4f74-23f21ca79e72"
|
|
||||||
],
|
|
||||||
"BIOYOND_PolymerStation_6StockCarrier": [
|
|
||||||
"分装板",
|
|
||||||
"3a14196e-5dfe-6e21-0c79-fe2036d052c4"
|
|
||||||
],
|
|
||||||
"BIOYOND_PolymerStation_Liquid_Vial": [
|
|
||||||
"10%分装小瓶",
|
|
||||||
"3a14196c-76be-2279-4e22-7310d69aed68"
|
|
||||||
],
|
|
||||||
"BIOYOND_PolymerStation_Solid_Vial": [
|
|
||||||
"90%分装小瓶",
|
|
||||||
"3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"
|
|
||||||
],
|
|
||||||
"BIOYOND_PolymerStation_8StockCarrier": [
|
|
||||||
"样品板",
|
|
||||||
"3a14196e-b7a0-a5da-1931-35f3000281e9"
|
|
||||||
],
|
|
||||||
"BIOYOND_PolymerStation_Solid_Stock": [
|
|
||||||
"样品瓶",
|
|
||||||
"3a14196a-cf7d-8aea-48d8-b9662c7dba94"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"deck": {
|
|
||||||
"data": {
|
|
||||||
"_resource_child_name": "Bioyond_Dispensing_Deck",
|
|
||||||
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"protocol_type": []
|
|
||||||
},
|
},
|
||||||
|
"config": {
|
||||||
|
"api_key": "YOUR_API_KEY",
|
||||||
|
"api_host": "http://your-api-host:port",
|
||||||
|
"material_type_mappings": {
|
||||||
|
"BIOYOND_PolymerStation_1FlaskCarrier": [
|
||||||
|
"烧杯",
|
||||||
|
"uuid-placeholder-flask"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_1BottleCarrier": [
|
||||||
|
"试剂瓶",
|
||||||
|
"uuid-placeholder-bottle"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_6StockCarrier": [
|
||||||
|
"分装板",
|
||||||
|
"uuid-placeholder-stock-6"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_Liquid_Vial": [
|
||||||
|
"10%分装小瓶",
|
||||||
|
"uuid-placeholder-liquid-vial"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_Solid_Vial": [
|
||||||
|
"90%分装小瓶",
|
||||||
|
"uuid-placeholder-solid-vial"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_8StockCarrier": [
|
||||||
|
"样品板",
|
||||||
|
"uuid-placeholder-stock-8"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_Solid_Stock": [
|
||||||
|
"样品瓶",
|
||||||
|
"uuid-placeholder-solid-stock"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"warehouse_mapping": {
|
||||||
|
"粉末堆栈": {
|
||||||
|
"uuid": "uuid-placeholder-powder-stack",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "uuid-placeholder-powder-A01",
|
||||||
|
"A02": "uuid-placeholder-powder-A02",
|
||||||
|
"A03": "uuid-placeholder-powder-A03",
|
||||||
|
"A04": "uuid-placeholder-powder-A04",
|
||||||
|
"B01": "uuid-placeholder-powder-B01",
|
||||||
|
"B02": "uuid-placeholder-powder-B02",
|
||||||
|
"B03": "uuid-placeholder-powder-B03",
|
||||||
|
"B04": "uuid-placeholder-powder-B04",
|
||||||
|
"C01": "uuid-placeholder-powder-C01",
|
||||||
|
"C02": "uuid-placeholder-powder-C02",
|
||||||
|
"C03": "uuid-placeholder-powder-C03",
|
||||||
|
"C04": "uuid-placeholder-powder-C04",
|
||||||
|
"D01": "uuid-placeholder-powder-D01",
|
||||||
|
"D02": "uuid-placeholder-powder-D02",
|
||||||
|
"D03": "uuid-placeholder-powder-D03",
|
||||||
|
"D04": "uuid-placeholder-powder-D04"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"溶液堆栈": {
|
||||||
|
"uuid": "uuid-placeholder-liquid-stack",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "uuid-placeholder-liquid-A01",
|
||||||
|
"A02": "uuid-placeholder-liquid-A02",
|
||||||
|
"A03": "uuid-placeholder-liquid-A03",
|
||||||
|
"A04": "uuid-placeholder-liquid-A04",
|
||||||
|
"B01": "uuid-placeholder-liquid-B01",
|
||||||
|
"B02": "uuid-placeholder-liquid-B02",
|
||||||
|
"B03": "uuid-placeholder-liquid-B03",
|
||||||
|
"B04": "uuid-placeholder-liquid-B04",
|
||||||
|
"C01": "uuid-placeholder-liquid-C01",
|
||||||
|
"C02": "uuid-placeholder-liquid-C02",
|
||||||
|
"C03": "uuid-placeholder-liquid-C03",
|
||||||
|
"C04": "uuid-placeholder-liquid-C04",
|
||||||
|
"D01": "uuid-placeholder-liquid-D01",
|
||||||
|
"D02": "uuid-placeholder-liquid-D02",
|
||||||
|
"D03": "uuid-placeholder-liquid-D03",
|
||||||
|
"D04": "uuid-placeholder-liquid-D04"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"试剂堆栈": {
|
||||||
|
"uuid": "uuid-placeholder-reagent-stack",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "uuid-placeholder-reagent-A01",
|
||||||
|
"A02": "uuid-placeholder-reagent-A02",
|
||||||
|
"A03": "uuid-placeholder-reagent-A03",
|
||||||
|
"A04": "uuid-placeholder-reagent-A04",
|
||||||
|
"B01": "uuid-placeholder-reagent-B01",
|
||||||
|
"B02": "uuid-placeholder-reagent-B02",
|
||||||
|
"B03": "uuid-placeholder-reagent-B03",
|
||||||
|
"B04": "uuid-placeholder-reagent-B04"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http_service_config": {
|
||||||
|
"http_service_host": "127.0.0.1",
|
||||||
|
"http_service_port": 8080
|
||||||
|
},
|
||||||
|
"material_default_parameters": {
|
||||||
|
"NMP": {
|
||||||
|
"unit": "毫升",
|
||||||
|
"density": "1.03",
|
||||||
|
"densityUnit": "g/mL",
|
||||||
|
"description": "N-甲基吡咯烷酮 (N-Methyl-2-pyrrolidone)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"material_type_parameters": {}
|
||||||
|
},
|
||||||
|
"deck": {
|
||||||
|
"data": {
|
||||||
|
"_resource_child_name": "Bioyond_Dispensing_Deck",
|
||||||
|
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size_x": 2700.0,
|
||||||
|
"size_y": 1080.0,
|
||||||
|
"size_z": 1500.0,
|
||||||
|
"protocol_type": [],
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,837 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "PRCXI",
|
|
||||||
"name": "PRCXI",
|
|
||||||
"type": "device",
|
|
||||||
"class": "liquid_handler.prcxi",
|
|
||||||
"parent": "",
|
|
||||||
"pose": {
|
|
||||||
"size": {
|
|
||||||
"width": 550,
|
|
||||||
"height": 400,
|
|
||||||
"depth": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"axis": "Left",
|
|
||||||
"deck": {
|
|
||||||
"_resource_type": "unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Deck",
|
|
||||||
"_resource_child_name": "PRCXI_Deck"
|
|
||||||
},
|
|
||||||
"host": "10.20.30.184",
|
|
||||||
"port": 9999,
|
|
||||||
"debug": false,
|
|
||||||
"setup": true,
|
|
||||||
"is_9320": true,
|
|
||||||
"timeout": 10,
|
|
||||||
"matrix_id": "5de524d0-3f95-406c-86dd-f83626ebc7cb",
|
|
||||||
"simulator": false,
|
|
||||||
"step_mode": false,
|
|
||||||
"channel_num": 2
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"reset_ok": true
|
|
||||||
},
|
|
||||||
"schema": {},
|
|
||||||
"description": "",
|
|
||||||
"model": null,
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 700,
|
|
||||||
"z": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "PRCXI_Deck",
|
|
||||||
"name": "PRCXI_Deck",
|
|
||||||
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI",
|
|
||||||
"type": "deck",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300Deck",
|
|
||||||
"size_x": 550,
|
|
||||||
"size_y": 400,
|
|
||||||
"size_z": 17,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "deck",
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T1",
|
|
||||||
"name": "T1",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 5,
|
|
||||||
"y": 301,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T1",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T2",
|
|
||||||
"name": "T2",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 142.5,
|
|
||||||
"y": 301,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T2",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T3",
|
|
||||||
"name": "T3",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 280,
|
|
||||||
"y": 301,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T3",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T4",
|
|
||||||
"name": "T4",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 417.5,
|
|
||||||
"y": 301,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 94,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T4",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T5",
|
|
||||||
"name": "T5",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 5,
|
|
||||||
"y": 205,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T5",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T6",
|
|
||||||
"name": "T6",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 142.5,
|
|
||||||
"y": 205,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T6",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T7",
|
|
||||||
"name": "T7",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 280,
|
|
||||||
"y": 205,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T7",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T8",
|
|
||||||
"name": "T8",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 417.5,
|
|
||||||
"y": 205,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T8",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T9",
|
|
||||||
"name": "T9",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 5,
|
|
||||||
"y": 109,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T9",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T10",
|
|
||||||
"name": "T10",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 142.5,
|
|
||||||
"y": 109,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T10",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T11",
|
|
||||||
"name": "T11",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 280,
|
|
||||||
"y": 109,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T11",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T12",
|
|
||||||
"name": "T12",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 417.5,
|
|
||||||
"y": 109,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T12",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T13",
|
|
||||||
"name": "T13",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 5,
|
|
||||||
"y": 13,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T13",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T14",
|
|
||||||
"name": "T14",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 142.5,
|
|
||||||
"y": 13,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T14",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T15",
|
|
||||||
"name": "T15",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 280,
|
|
||||||
"y": 13,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T15",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "T16",
|
|
||||||
"name": "T16",
|
|
||||||
"children": [],
|
|
||||||
"parent": "PRCXI_Deck",
|
|
||||||
"type": "plate",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 417.5,
|
|
||||||
"y": 13,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300PlateAdapterSite",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 28,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
|
|
||||||
"sites": [
|
|
||||||
{
|
|
||||||
"label": "T16",
|
|
||||||
"visible": true,
|
|
||||||
"position": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
|
||||||
"content_type": [
|
|
||||||
"plate",
|
|
||||||
"tip_rack",
|
|
||||||
"plates",
|
|
||||||
"tip_racks",
|
|
||||||
"tube_rack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "trash",
|
|
||||||
"name": "trash",
|
|
||||||
|
|
||||||
"children": [],
|
|
||||||
"parent": "T16",
|
|
||||||
"type": "trash",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "PRCXI9300Trash",
|
|
||||||
"size_x": 127.5,
|
|
||||||
"size_y": 86,
|
|
||||||
"size_z": 10,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "trash",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
"max_volume": "Infinity",
|
|
||||||
"material_z_thickness": 0,
|
|
||||||
"compute_volume_from_height": null,
|
|
||||||
"compute_height_from_volume": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"liquids": [],
|
|
||||||
"pending_liquids": [],
|
|
||||||
"liquid_history": [],
|
|
||||||
"Material": {
|
|
||||||
"uuid": "730067cf07ae43849ddf4034299030e9"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"edges": []
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -14,60 +14,200 @@
|
|||||||
],
|
],
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "reaction_station.bioyond",
|
"class": "reaction_station.bioyond",
|
||||||
"position": {"x": 0, "y": 3800, "z": 0},
|
"position": {
|
||||||
"config": {
|
"x": 0,
|
||||||
"config": {
|
"y": 1100,
|
||||||
"api_key": "DE9BDDA0",
|
"z": 0
|
||||||
"api_host": "http://192.168.1.200:44402",
|
|
||||||
"workflow_mappings": {
|
|
||||||
"reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1",
|
|
||||||
"reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6",
|
|
||||||
"Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6",
|
|
||||||
"Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47",
|
|
||||||
"Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046",
|
|
||||||
"Liquid_feeding(titration)": "3a16082a-96ac-0449-446a-4ed39f3365b6",
|
|
||||||
"liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
|
|
||||||
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
|
||||||
},
|
|
||||||
"material_type_mappings": {
|
|
||||||
"BIOYOND_PolymerStation_Reactor": [
|
|
||||||
"反应器",
|
|
||||||
"3a14233b-902d-0d7b-4533-3f60f1c41c1b"
|
|
||||||
],
|
|
||||||
"BIOYOND_PolymerStation_1BottleCarrier": [
|
|
||||||
"试剂瓶",
|
|
||||||
"3a14233b-56e3-6c53-a8ab-fcaac163a9ba"
|
|
||||||
],
|
|
||||||
"BIOYOND_PolymerStation_1FlaskCarrier": [
|
|
||||||
"烧杯",
|
|
||||||
"3a14233b-f0a9-ba84-eaa9-0d4718b361b6"
|
|
||||||
],
|
|
||||||
"BIOYOND_PolymerStation_6StockCarrier": [
|
|
||||||
"样品板",
|
|
||||||
"3a142339-80de-8f25-6093-1b1b1b6c322e"
|
|
||||||
],
|
|
||||||
"BIOYOND_PolymerStation_Solid_Vial": [
|
|
||||||
"90%分装小瓶",
|
|
||||||
"3a14233a-26e1-28f8-af6a-60ca06ba0165"
|
|
||||||
],
|
|
||||||
"BIOYOND_PolymerStation_Liquid_Vial": [
|
|
||||||
"10%分装小瓶",
|
|
||||||
"3a14233a-84a3-088d-6676-7cb4acd57c64"
|
|
||||||
],
|
|
||||||
"BIOYOND_PolymerStation_TipBox": [
|
|
||||||
"枪头盒",
|
|
||||||
"3a143890-9d51-60ac-6d6f-6edb43c12041"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"deck": {
|
|
||||||
"data": {
|
|
||||||
"_resource_child_name": "Bioyond_Deck",
|
|
||||||
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"protocol_type": []
|
|
||||||
},
|
},
|
||||||
|
"config": {
|
||||||
|
"api_key": "YOUR_API_KEY",
|
||||||
|
"api_host": "http://your-api-host:port",
|
||||||
|
"workflow_mappings": {
|
||||||
|
"reactor_taken_out": "workflow-uuid-reactor-out",
|
||||||
|
"reactor_taken_in": "workflow-uuid-reactor-in",
|
||||||
|
"Solid_feeding_vials": "workflow-uuid-solid-vials",
|
||||||
|
"Liquid_feeding_vials(non-titration)": "workflow-uuid-liquid-vials",
|
||||||
|
"Liquid_feeding_solvents": "workflow-uuid-solvents",
|
||||||
|
"Liquid_feeding(titration)": "workflow-uuid-titration",
|
||||||
|
"liquid_feeding_beaker": "workflow-uuid-beaker",
|
||||||
|
"Drip_back": "workflow-uuid-drip-back"
|
||||||
|
},
|
||||||
|
"material_type_mappings": {
|
||||||
|
"BIOYOND_PolymerStation_Reactor": [
|
||||||
|
"反应器",
|
||||||
|
"uuid-placeholder-reactor"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_1BottleCarrier": [
|
||||||
|
"试剂瓶",
|
||||||
|
"uuid-placeholder-bottle"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_1FlaskCarrier": [
|
||||||
|
"烧杯",
|
||||||
|
"uuid-placeholder-beaker"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_6StockCarrier": [
|
||||||
|
"样品板",
|
||||||
|
"uuid-placeholder-sample-plate"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_Solid_Vial": [
|
||||||
|
"90%分装小瓶",
|
||||||
|
"uuid-placeholder-solid-vial"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_Liquid_Vial": [
|
||||||
|
"10%分装小瓶",
|
||||||
|
"uuid-placeholder-liquid-vial"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_TipBox": [
|
||||||
|
"枪头盒",
|
||||||
|
"uuid-placeholder-tipbox"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_Measurement_Vial": [
|
||||||
|
"测量小瓶",
|
||||||
|
"uuid-placeholder-measure-vial"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"warehouse_mapping": {
|
||||||
|
"堆栈1左": {
|
||||||
|
"uuid": "uuid-placeholder-stack1-left",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "uuid-placeholder-site-A01",
|
||||||
|
"A02": "uuid-placeholder-site-A02",
|
||||||
|
"A03": "uuid-placeholder-site-A03",
|
||||||
|
"A04": "uuid-placeholder-site-A04",
|
||||||
|
"B01": "uuid-placeholder-site-B01",
|
||||||
|
"B02": "uuid-placeholder-site-B02",
|
||||||
|
"B03": "uuid-placeholder-site-B03",
|
||||||
|
"B04": "uuid-placeholder-site-B04",
|
||||||
|
"C01": "uuid-placeholder-site-C01",
|
||||||
|
"C02": "uuid-placeholder-site-C02",
|
||||||
|
"C03": "uuid-placeholder-site-C03",
|
||||||
|
"C04": "uuid-placeholder-site-C04",
|
||||||
|
"D01": "uuid-placeholder-site-D01",
|
||||||
|
"D02": "uuid-placeholder-site-D02",
|
||||||
|
"D03": "uuid-placeholder-site-D03",
|
||||||
|
"D04": "uuid-placeholder-site-D04"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"堆栈1右": {
|
||||||
|
"uuid": "uuid-placeholder-stack1-right",
|
||||||
|
"site_uuids": {
|
||||||
|
"A05": "uuid-placeholder-site-A05",
|
||||||
|
"A06": "uuid-placeholder-site-A06",
|
||||||
|
"A07": "uuid-placeholder-site-A07",
|
||||||
|
"A08": "uuid-placeholder-site-A08",
|
||||||
|
"B05": "uuid-placeholder-site-B05",
|
||||||
|
"B06": "uuid-placeholder-site-B06",
|
||||||
|
"B07": "uuid-placeholder-site-B07",
|
||||||
|
"B08": "uuid-placeholder-site-B08",
|
||||||
|
"C05": "uuid-placeholder-site-C05",
|
||||||
|
"C06": "uuid-placeholder-site-C06",
|
||||||
|
"C07": "uuid-placeholder-site-C07",
|
||||||
|
"C08": "uuid-placeholder-site-C08",
|
||||||
|
"D05": "uuid-placeholder-site-D05",
|
||||||
|
"D06": "uuid-placeholder-site-D06",
|
||||||
|
"D07": "uuid-placeholder-site-D07",
|
||||||
|
"D08": "uuid-placeholder-site-D08"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"站内试剂存放堆栈": {
|
||||||
|
"uuid": "uuid-placeholder-reagent-stack",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "uuid-placeholder-reagent-A01",
|
||||||
|
"A02": "uuid-placeholder-reagent-A02"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"测量小瓶仓库(测密度)": {
|
||||||
|
"uuid": "uuid-placeholder-density-stack",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "uuid-placeholder-density-A01",
|
||||||
|
"A02": "uuid-placeholder-density-A02",
|
||||||
|
"A03": "uuid-placeholder-density-A03",
|
||||||
|
"B01": "uuid-placeholder-density-B01",
|
||||||
|
"B02": "uuid-placeholder-density-B02",
|
||||||
|
"B03": "uuid-placeholder-density-B03"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"站内Tip盒堆栈(左)": {
|
||||||
|
"uuid": "uuid-placeholder-tipstack-left",
|
||||||
|
"site_uuids": {
|
||||||
|
"A02": "uuid-placeholder-tip-A02",
|
||||||
|
"A03": "uuid-placeholder-tip-A03",
|
||||||
|
"B02": "uuid-placeholder-tip-B02",
|
||||||
|
"B03": "uuid-placeholder-tip-B03"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"站内Tip盒堆栈(右)": {
|
||||||
|
"uuid": "uuid-placeholder-tipstack-right",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "uuid-placeholder-tip-A01",
|
||||||
|
"B01": "uuid-placeholder-tip-B01"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workflow_to_section_map": {
|
||||||
|
"reactor_taken_in": "反应器放入",
|
||||||
|
"reactor_taken_out": "反应器取出",
|
||||||
|
"Solid_feeding_vials": "固体投料-小瓶",
|
||||||
|
"Liquid_feeding_vials(non-titration)": "液体投料-小瓶(非滴定)",
|
||||||
|
"Liquid_feeding_solvents": "液体投料-溶剂",
|
||||||
|
"Liquid_feeding(titration)": "液体投料-滴定",
|
||||||
|
"liquid_feeding_beaker": "液体投料-烧杯",
|
||||||
|
"Drip_back": "液体回滴"
|
||||||
|
},
|
||||||
|
"action_names": {
|
||||||
|
"reactor_taken_in": {
|
||||||
|
"config": "通量-配置",
|
||||||
|
"stirring": "反应模块-开始搅拌"
|
||||||
|
},
|
||||||
|
"solid_feeding_vials": {
|
||||||
|
"feeding": "粉末加样模块-投料",
|
||||||
|
"observe": "反应模块-观察搅拌结果"
|
||||||
|
},
|
||||||
|
"liquid_feeding_vials_non_titration": {
|
||||||
|
"liquid": "稀释液瓶加液位-液体投料",
|
||||||
|
"observe": "反应模块-滴定结果观察"
|
||||||
|
},
|
||||||
|
"liquid_feeding_solvents": {
|
||||||
|
"liquid": "试剂AB放置位-试剂吸液分液",
|
||||||
|
"observe": "反应模块-观察搅拌结果"
|
||||||
|
},
|
||||||
|
"liquid_feeding_titration": {
|
||||||
|
"liquid": "稀释液瓶加液位-稀释液吸液分液",
|
||||||
|
"observe": "反应模块-滴定结果观察"
|
||||||
|
},
|
||||||
|
"liquid_feeding_beaker": {
|
||||||
|
"liquid": "烧杯溶液放置位-烧杯吸液分液",
|
||||||
|
"observe": "反应模块-观察搅拌结果"
|
||||||
|
},
|
||||||
|
"drip_back": {
|
||||||
|
"liquid": "试剂AB放置位-试剂吸液分液",
|
||||||
|
"observe": "反应模块-向下滴定结果观察"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http_service_config": {
|
||||||
|
"http_service_host": "127.0.0.1",
|
||||||
|
"http_service_port": 8080
|
||||||
|
},
|
||||||
|
"material_default_parameters": {
|
||||||
|
"NMP": {
|
||||||
|
"unit": "毫升",
|
||||||
|
"density": "1.03",
|
||||||
|
"densityUnit": "g/mL",
|
||||||
|
"description": "N-甲基吡咯烷酮 (N-Methyl-2-pyrrolidone)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"material_type_parameters": {}
|
||||||
|
},
|
||||||
|
"deck": {
|
||||||
|
"data": {
|
||||||
|
"_resource_child_name": "Bioyond_Deck",
|
||||||
|
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size_x": 2700.0,
|
||||||
|
"size_y": 1080.0,
|
||||||
|
"size_z": 2500.0,
|
||||||
|
"protocol_type": [],
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -77,7 +217,11 @@
|
|||||||
"parent": "reaction_station_bioyond",
|
"parent": "reaction_station_bioyond",
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "reaction_station.reactor",
|
"class": "reaction_station.reactor",
|
||||||
"position": {"x": 1150, "y": 380, "z": 0},
|
"position": {
|
||||||
|
"x": 1150,
|
||||||
|
"y": 300,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
"config": {},
|
"config": {},
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
@@ -88,7 +232,11 @@
|
|||||||
"parent": "reaction_station_bioyond",
|
"parent": "reaction_station_bioyond",
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "reaction_station.reactor",
|
"class": "reaction_station.reactor",
|
||||||
"position": {"x": 1365, "y": 380, "z": 0},
|
"position": {
|
||||||
|
"x": 1365,
|
||||||
|
"y": 300,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
"config": {},
|
"config": {},
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
@@ -99,7 +247,11 @@
|
|||||||
"parent": "reaction_station_bioyond",
|
"parent": "reaction_station_bioyond",
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "reaction_station.reactor",
|
"class": "reaction_station.reactor",
|
||||||
"position": {"x": 1580, "y": 380, "z": 0},
|
"position": {
|
||||||
|
"x": 1580,
|
||||||
|
"y": 300,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
"config": {},
|
"config": {},
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
@@ -110,7 +262,11 @@
|
|||||||
"parent": "reaction_station_bioyond",
|
"parent": "reaction_station_bioyond",
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "reaction_station.reactor",
|
"class": "reaction_station.reactor",
|
||||||
"position": {"x": 1790, "y": 380, "z": 0},
|
"position": {
|
||||||
|
"x": 1790,
|
||||||
|
"y": 300,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
"config": {},
|
"config": {},
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
@@ -121,7 +277,11 @@
|
|||||||
"parent": "reaction_station_bioyond",
|
"parent": "reaction_station_bioyond",
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "reaction_station.reactor",
|
"class": "reaction_station.reactor",
|
||||||
"position": {"x": 2010, "y": 380, "z": 0},
|
"position": {
|
||||||
|
"x": 2010,
|
||||||
|
"y": 300,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
"config": {},
|
"config": {},
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
@@ -134,7 +294,7 @@
|
|||||||
"class": "BIOYOND_PolymerReactionStation_Deck",
|
"class": "BIOYOND_PolymerReactionStation_Deck",
|
||||||
"position": {
|
"position": {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 1100,
|
||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
|||||||
28
unilabos/test/experiments/virtual_bench.json
Normal file
28
unilabos/test/experiments/virtual_bench.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "workbench_1",
|
||||||
|
"name": "虚拟工作台",
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "virtual_workbench",
|
||||||
|
"position": {
|
||||||
|
"x": 400,
|
||||||
|
"y": 300,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"arm_operation_time": 3.0,
|
||||||
|
"heating_time": 10.0,
|
||||||
|
"num_heating_stations": 3
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"status": "Ready",
|
||||||
|
"arm_state": "idle",
|
||||||
|
"message": "工作台就绪"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": []
|
||||||
|
}
|
||||||
126
unilabos/test/experiments/yibin_electrolyte_config_example.json
Normal file
126
unilabos/test/experiments/yibin_electrolyte_config_example.json
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "bioyond_cell_workstation",
|
||||||
|
"name": "配液分液工站 (示例)",
|
||||||
|
"parent": null,
|
||||||
|
"children": [
|
||||||
|
"YB_Bioyond_Deck"
|
||||||
|
],
|
||||||
|
"type": "device",
|
||||||
|
"class": "bioyond_cell",
|
||||||
|
"config": {
|
||||||
|
"deck": {
|
||||||
|
"data": {
|
||||||
|
"_resource_child_name": "YB_Bioyond_Deck",
|
||||||
|
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"protocol_type": [],
|
||||||
|
"bioyond_config": {
|
||||||
|
"api_host": "http://YOUR_API_HOST:PORT",
|
||||||
|
"api_key": "YOUR_API_KEY",
|
||||||
|
"timeout": 30,
|
||||||
|
"report_token": "YOUR_REPORT_TOKEN",
|
||||||
|
"HTTP_host": "YOUR_LOCAL_IP",
|
||||||
|
"HTTP_port": 8080,
|
||||||
|
"debug_mode": false,
|
||||||
|
"material_type_mappings": {
|
||||||
|
"100ml液体": [
|
||||||
|
"YB_100ml_yeti",
|
||||||
|
"00000000-0000-0000-0000-000000000000"
|
||||||
|
],
|
||||||
|
"液": [
|
||||||
|
"YB_ye",
|
||||||
|
"00000000-0000-0000-0000-000000000000"
|
||||||
|
],
|
||||||
|
"高粘液": [
|
||||||
|
"YB_gaonianye",
|
||||||
|
"00000000-0000-0000-0000-000000000000"
|
||||||
|
],
|
||||||
|
"加样头(大)": [
|
||||||
|
"YB_jia_yang_tou_da_Carrier",
|
||||||
|
"00000000-0000-0000-0000-000000000000"
|
||||||
|
],
|
||||||
|
"5ml分液瓶板": [
|
||||||
|
"YB_5ml_fenyepingban",
|
||||||
|
"00000000-0000-0000-0000-000000000000"
|
||||||
|
],
|
||||||
|
"5ml分液瓶": [
|
||||||
|
"YB_5ml_fenyeping",
|
||||||
|
"00000000-0000-0000-0000-000000000000"
|
||||||
|
],
|
||||||
|
"20ml分液瓶板": [
|
||||||
|
"YB_20ml_fenyepingban",
|
||||||
|
"00000000-0000-0000-0000-000000000000"
|
||||||
|
],
|
||||||
|
"20ml分液瓶": [
|
||||||
|
"YB_20ml_fenyeping",
|
||||||
|
"00000000-0000-0000-0000-000000000000"
|
||||||
|
],
|
||||||
|
"配液瓶(小)板": [
|
||||||
|
"YB_peiyepingxiaoban",
|
||||||
|
"00000000-0000-0000-0000-000000000000"
|
||||||
|
],
|
||||||
|
"配液瓶(小)": [
|
||||||
|
"YB_pei_ye_xiao_Bottle",
|
||||||
|
"00000000-0000-0000-0000-000000000000"
|
||||||
|
],
|
||||||
|
"枪头盒": [
|
||||||
|
"YB_qiang_tou_he",
|
||||||
|
"00000000-0000-0000-0000-000000000000"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"warehouse_mapping": {
|
||||||
|
"示例堆栈": {
|
||||||
|
"uuid": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"site_uuids": {
|
||||||
|
"A01": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"B01": "00000000-0000-0000-0000-000000000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"solid_liquid_mappings": {
|
||||||
|
"示例物料": {
|
||||||
|
"typeId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"code": "",
|
||||||
|
"barCode": "",
|
||||||
|
"name": "Example_Material",
|
||||||
|
"unit": "g",
|
||||||
|
"parameters": "",
|
||||||
|
"quantity": "2",
|
||||||
|
"warningQuantity": "1",
|
||||||
|
"details": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "YB_Bioyond_Deck",
|
||||||
|
"name": "YB_Bioyond_Deck",
|
||||||
|
"children": [],
|
||||||
|
"parent": "bioyond_cell_workstation",
|
||||||
|
"type": "deck",
|
||||||
|
"class": "BIOYOND_YB_Deck",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"type": "BIOYOND_YB_Deck",
|
||||||
|
"setup": true,
|
||||||
|
"rotation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0,
|
||||||
|
"type": "Rotation"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": []
|
||||||
|
}
|
||||||
@@ -6,180 +6,20 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import importlib
|
import importlib
|
||||||
import locale
|
import locale
|
||||||
import shutil
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from unilabos.utils.banner_print import print_status
|
from unilabos.utils.banner_print import print_status
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 底层安装工具
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _is_chinese_locale() -> bool:
|
|
||||||
try:
|
|
||||||
lang = locale.getdefaultlocale()[0]
|
|
||||||
return bool(lang and ("zh" in lang.lower() or "chinese" in lang.lower()))
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
_USE_UV: Optional[bool] = None
|
|
||||||
|
|
||||||
|
|
||||||
def _has_uv() -> bool:
|
|
||||||
global _USE_UV
|
|
||||||
if _USE_UV is None:
|
|
||||||
_USE_UV = shutil.which("uv") is not None
|
|
||||||
return _USE_UV
|
|
||||||
|
|
||||||
|
|
||||||
def _install_packages(
|
|
||||||
packages: List[str],
|
|
||||||
upgrade: bool = False,
|
|
||||||
label: str = "",
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
安装/升级一组包。优先 uv pip install,回退 sys pip。
|
|
||||||
逐个安装,任意一个失败不影响后续包。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if all succeeded, False otherwise.
|
|
||||||
"""
|
|
||||||
if not packages:
|
|
||||||
return True
|
|
||||||
|
|
||||||
is_chinese = _is_chinese_locale()
|
|
||||||
use_uv = _has_uv()
|
|
||||||
failed: List[str] = []
|
|
||||||
|
|
||||||
for pkg in packages:
|
|
||||||
action_word = "升级" if upgrade else "安装"
|
|
||||||
if label:
|
|
||||||
print_status(f"[{label}] 正在{action_word} {pkg}...", "info")
|
|
||||||
else:
|
|
||||||
print_status(f"正在{action_word} {pkg}...", "info")
|
|
||||||
|
|
||||||
if use_uv:
|
|
||||||
cmd = ["uv", "pip", "install"]
|
|
||||||
if upgrade:
|
|
||||||
cmd.append("--upgrade")
|
|
||||||
cmd.append(pkg)
|
|
||||||
if is_chinese:
|
|
||||||
cmd.extend(["--index-url", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"])
|
|
||||||
else:
|
|
||||||
cmd = [sys.executable, "-m", "pip", "install"]
|
|
||||||
if upgrade:
|
|
||||||
cmd.append("--upgrade")
|
|
||||||
cmd.append(pkg)
|
|
||||||
if is_chinese:
|
|
||||||
cmd.extend(["-i", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"])
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
||||||
if result.returncode == 0:
|
|
||||||
installer = "uv" if use_uv else "pip"
|
|
||||||
print_status(f"✓ {pkg} {action_word}成功 (via {installer})", "success")
|
|
||||||
else:
|
|
||||||
stderr_short = result.stderr.strip().split("\n")[-1] if result.stderr else "unknown error"
|
|
||||||
print_status(f"× {pkg} {action_word}失败: {stderr_short}", "error")
|
|
||||||
failed.append(pkg)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
print_status(f"× {pkg} {action_word}超时 (300s)", "error")
|
|
||||||
failed.append(pkg)
|
|
||||||
except Exception as e:
|
|
||||||
print_status(f"× {pkg} {action_word}异常: {e}", "error")
|
|
||||||
failed.append(pkg)
|
|
||||||
|
|
||||||
if failed:
|
|
||||||
print_status(f"有 {len(failed)} 个包操作失败: {', '.join(failed)}", "error")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# requirements.txt 安装(可多次调用)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def install_requirements_txt(req_path: str | Path, label: str = "") -> bool:
|
|
||||||
"""
|
|
||||||
读取一个 requirements.txt 文件,检查缺失的包并安装。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
req_path: requirements.txt 文件路径
|
|
||||||
label: 日志前缀标签(如 "device_package_sim")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if all ok, False if any install failed.
|
|
||||||
"""
|
|
||||||
req_path = Path(req_path)
|
|
||||||
if not req_path.exists():
|
|
||||||
return True
|
|
||||||
|
|
||||||
tag = label or req_path.parent.name
|
|
||||||
print_status(f"[{tag}] 检查依赖: {req_path}", "info")
|
|
||||||
|
|
||||||
reqs: List[str] = []
|
|
||||||
with open(req_path, "r", encoding="utf-8") as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if line and not line.startswith("#") and not line.startswith("-"):
|
|
||||||
reqs.append(line)
|
|
||||||
|
|
||||||
if not reqs:
|
|
||||||
return True
|
|
||||||
|
|
||||||
missing: List[str] = []
|
|
||||||
for req in reqs:
|
|
||||||
pkg_import = req.split(">=")[0].split("==")[0].split("<")[0].split("[")[0].split(">")[0].strip()
|
|
||||||
pkg_import = pkg_import.replace("-", "_")
|
|
||||||
try:
|
|
||||||
importlib.import_module(pkg_import)
|
|
||||||
except ImportError:
|
|
||||||
missing.append(req)
|
|
||||||
|
|
||||||
if not missing:
|
|
||||||
print_status(f"[{tag}] ✓ 依赖检查通过 ({len(reqs)} 个包)", "success")
|
|
||||||
return True
|
|
||||||
|
|
||||||
print_status(f"[{tag}] 缺失 {len(missing)} 个依赖: {', '.join(missing)}", "warning")
|
|
||||||
return _install_packages(missing, label=tag)
|
|
||||||
|
|
||||||
|
|
||||||
def check_device_package_requirements(devices_dirs: list[str]) -> bool:
|
|
||||||
"""
|
|
||||||
检查 --devices 指定的所有外部设备包目录中的 requirements.txt。
|
|
||||||
对每个目录查找 requirements.txt(先在目录内找,再在父目录找)。
|
|
||||||
"""
|
|
||||||
if not devices_dirs:
|
|
||||||
return True
|
|
||||||
|
|
||||||
all_ok = True
|
|
||||||
for d in devices_dirs:
|
|
||||||
d_path = Path(d).resolve()
|
|
||||||
req_file = d_path / "requirements.txt"
|
|
||||||
if not req_file.exists():
|
|
||||||
req_file = d_path.parent / "requirements.txt"
|
|
||||||
if not req_file.exists():
|
|
||||||
continue
|
|
||||||
if not install_requirements_txt(req_file, label=d_path.name):
|
|
||||||
all_ok = False
|
|
||||||
|
|
||||||
return all_ok
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# UniLabOS 核心环境检查
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class EnvironmentChecker:
|
class EnvironmentChecker:
|
||||||
"""环境检查器"""
|
"""环境检查器"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
# 定义必需的包及其安装名称的映射
|
||||||
self.required_packages = {
|
self.required_packages = {
|
||||||
|
# 包导入名 : pip安装名
|
||||||
|
# "pymodbus.framer.FramerType": "pymodbus==3.9.2",
|
||||||
"websockets": "websockets",
|
"websockets": "websockets",
|
||||||
"msgcenterpy": "msgcenterpy",
|
"msgcenterpy": "msgcenterpy",
|
||||||
"orjson": "orjson",
|
"orjson": "orjson",
|
||||||
@@ -188,17 +28,33 @@ class EnvironmentChecker:
|
|||||||
"crcmod": "crcmod-plus",
|
"crcmod": "crcmod-plus",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 特殊安装包(需要特殊处理的包)
|
||||||
self.special_packages = {"pylabrobot": "git+https://github.com/Xuwznln/pylabrobot.git"}
|
self.special_packages = {"pylabrobot": "git+https://github.com/Xuwznln/pylabrobot.git"}
|
||||||
|
|
||||||
|
# 包版本要求(包名: 最低版本)
|
||||||
self.version_requirements = {
|
self.version_requirements = {
|
||||||
"msgcenterpy": "0.1.8",
|
"msgcenterpy": "0.1.8", # msgcenterpy 最低版本要求
|
||||||
}
|
}
|
||||||
|
|
||||||
self.missing_packages: List[tuple] = []
|
self.missing_packages = []
|
||||||
self.failed_installs: List[tuple] = []
|
self.failed_installs = []
|
||||||
self.packages_need_upgrade: List[tuple] = []
|
self.packages_need_upgrade = []
|
||||||
|
|
||||||
|
# 检测系统语言
|
||||||
|
self.is_chinese = self._is_chinese_locale()
|
||||||
|
|
||||||
|
def _is_chinese_locale(self) -> bool:
|
||||||
|
"""检测系统是否为中文环境"""
|
||||||
|
try:
|
||||||
|
lang = locale.getdefaultlocale()[0]
|
||||||
|
if lang and ("zh" in lang.lower() or "chinese" in lang.lower()):
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
def check_package_installed(self, package_name: str) -> bool:
|
def check_package_installed(self, package_name: str) -> bool:
|
||||||
|
"""检查包是否已安装"""
|
||||||
try:
|
try:
|
||||||
importlib.import_module(package_name)
|
importlib.import_module(package_name)
|
||||||
return True
|
return True
|
||||||
@@ -206,6 +62,7 @@ class EnvironmentChecker:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def get_package_version(self, package_name: str) -> str | None:
|
def get_package_version(self, package_name: str) -> str | None:
|
||||||
|
"""获取已安装包的版本"""
|
||||||
try:
|
try:
|
||||||
module = importlib.import_module(package_name)
|
module = importlib.import_module(package_name)
|
||||||
return getattr(module, "__version__", None)
|
return getattr(module, "__version__", None)
|
||||||
@@ -213,32 +70,88 @@ class EnvironmentChecker:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def compare_version(self, current: str, required: str) -> bool:
|
def compare_version(self, current: str, required: str) -> bool:
|
||||||
|
"""
|
||||||
|
比较版本号
|
||||||
|
Returns:
|
||||||
|
True: current >= required
|
||||||
|
False: current < required
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
current_parts = [int(x) for x in current.split(".")]
|
current_parts = [int(x) for x in current.split(".")]
|
||||||
required_parts = [int(x) for x in required.split(".")]
|
required_parts = [int(x) for x in required.split(".")]
|
||||||
|
|
||||||
|
# 补齐长度
|
||||||
max_len = max(len(current_parts), len(required_parts))
|
max_len = max(len(current_parts), len(required_parts))
|
||||||
current_parts.extend([0] * (max_len - len(current_parts)))
|
current_parts.extend([0] * (max_len - len(current_parts)))
|
||||||
required_parts.extend([0] * (max_len - len(required_parts)))
|
required_parts.extend([0] * (max_len - len(required_parts)))
|
||||||
|
|
||||||
return current_parts >= required_parts
|
return current_parts >= required_parts
|
||||||
except Exception:
|
except Exception:
|
||||||
return True
|
return True # 如果无法比较,假设版本满足要求
|
||||||
|
|
||||||
|
def install_package(self, package_name: str, pip_name: str, upgrade: bool = False) -> bool:
|
||||||
|
"""安装包"""
|
||||||
|
try:
|
||||||
|
action = "升级" if upgrade else "安装"
|
||||||
|
print_status(f"正在{action} {package_name} ({pip_name})...", "info")
|
||||||
|
|
||||||
|
# 构建安装命令
|
||||||
|
cmd = [sys.executable, "-m", "pip", "install"]
|
||||||
|
|
||||||
|
# 如果是升级操作,添加 --upgrade 参数
|
||||||
|
if upgrade:
|
||||||
|
cmd.append("--upgrade")
|
||||||
|
|
||||||
|
cmd.append(pip_name)
|
||||||
|
|
||||||
|
# 如果是中文环境,使用清华镜像源
|
||||||
|
if self.is_chinese:
|
||||||
|
cmd.extend(["-i", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"])
|
||||||
|
|
||||||
|
# 执行安装
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) # 5分钟超时
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print_status(f"✓ {package_name} {action}成功", "success")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print_status(f"× {package_name} {action}失败: {result.stderr}", "error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
print_status(f"× {package_name} {action}超时", "error")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print_status(f"× {package_name} {action}异常: {str(e)}", "error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def upgrade_package(self, package_name: str, pip_name: str) -> bool:
|
||||||
|
"""升级包"""
|
||||||
|
return self.install_package(package_name, pip_name, upgrade=True)
|
||||||
|
|
||||||
def check_all_packages(self) -> bool:
|
def check_all_packages(self) -> bool:
|
||||||
|
"""检查所有必需的包"""
|
||||||
print_status("开始检查环境依赖...", "info")
|
print_status("开始检查环境依赖...", "info")
|
||||||
|
|
||||||
|
# 检查常规包
|
||||||
for import_name, pip_name in self.required_packages.items():
|
for import_name, pip_name in self.required_packages.items():
|
||||||
if not self.check_package_installed(import_name):
|
if not self.check_package_installed(import_name):
|
||||||
self.missing_packages.append((import_name, pip_name))
|
self.missing_packages.append((import_name, pip_name))
|
||||||
elif import_name in self.version_requirements:
|
else:
|
||||||
current_version = self.get_package_version(import_name)
|
# 检查版本要求
|
||||||
required_version = self.version_requirements[import_name]
|
if import_name in self.version_requirements:
|
||||||
if current_version and not self.compare_version(current_version, required_version):
|
current_version = self.get_package_version(import_name)
|
||||||
print_status(
|
required_version = self.version_requirements[import_name]
|
||||||
f"{import_name} 版本过低 (当前: {current_version}, 需要: >={required_version})",
|
|
||||||
"warning",
|
|
||||||
)
|
|
||||||
self.packages_need_upgrade.append((import_name, pip_name))
|
|
||||||
|
|
||||||
|
if current_version:
|
||||||
|
if not self.compare_version(current_version, required_version):
|
||||||
|
print_status(
|
||||||
|
f"{import_name} 版本过低 (当前: {current_version}, 需要: >={required_version})",
|
||||||
|
"warning",
|
||||||
|
)
|
||||||
|
self.packages_need_upgrade.append((import_name, pip_name))
|
||||||
|
|
||||||
|
# 检查特殊包
|
||||||
for package_name, install_url in self.special_packages.items():
|
for package_name, install_url in self.special_packages.items():
|
||||||
if not self.check_package_installed(package_name):
|
if not self.check_package_installed(package_name):
|
||||||
self.missing_packages.append((package_name, install_url))
|
self.missing_packages.append((package_name, install_url))
|
||||||
@@ -257,6 +170,7 @@ class EnvironmentChecker:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def install_missing_packages(self, auto_install: bool = True) -> bool:
|
def install_missing_packages(self, auto_install: bool = True) -> bool:
|
||||||
|
"""安装缺失的包"""
|
||||||
if not self.missing_packages and not self.packages_need_upgrade:
|
if not self.missing_packages and not self.packages_need_upgrade:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -264,36 +178,62 @@ class EnvironmentChecker:
|
|||||||
if self.missing_packages:
|
if self.missing_packages:
|
||||||
print_status("缺失以下包:", "warning")
|
print_status("缺失以下包:", "warning")
|
||||||
for import_name, pip_name in self.missing_packages:
|
for import_name, pip_name in self.missing_packages:
|
||||||
print_status(f" - {import_name} ({pip_name})", "warning")
|
print_status(f" - {import_name} (pip install {pip_name})", "warning")
|
||||||
if self.packages_need_upgrade:
|
if self.packages_need_upgrade:
|
||||||
print_status("需要升级以下包:", "warning")
|
print_status("需要升级以下包:", "warning")
|
||||||
for import_name, pip_name in self.packages_need_upgrade:
|
for import_name, pip_name in self.packages_need_upgrade:
|
||||||
print_status(f" - {import_name} ({pip_name})", "warning")
|
print_status(f" - {import_name} (pip install --upgrade {pip_name})", "warning")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# 安装缺失的包
|
||||||
if self.missing_packages:
|
if self.missing_packages:
|
||||||
pkgs = [pip_name for _, pip_name in self.missing_packages]
|
print_status(f"开始自动安装 {len(self.missing_packages)} 个缺失的包...", "info")
|
||||||
if not _install_packages(pkgs, label="unilabos"):
|
|
||||||
self.failed_installs.extend(self.missing_packages)
|
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
for import_name, pip_name in self.missing_packages:
|
||||||
|
if self.install_package(import_name, pip_name):
|
||||||
|
success_count += 1
|
||||||
|
else:
|
||||||
|
self.failed_installs.append((import_name, pip_name))
|
||||||
|
|
||||||
|
print_status(f"✓ 成功安装 {success_count}/{len(self.missing_packages)} 个包", "success")
|
||||||
|
|
||||||
|
# 升级需要更新的包
|
||||||
if self.packages_need_upgrade:
|
if self.packages_need_upgrade:
|
||||||
pkgs = [pip_name for _, pip_name in self.packages_need_upgrade]
|
print_status(f"开始自动升级 {len(self.packages_need_upgrade)} 个包...", "info")
|
||||||
if not _install_packages(pkgs, upgrade=True, label="unilabos"):
|
|
||||||
self.failed_installs.extend(self.packages_need_upgrade)
|
|
||||||
|
|
||||||
return not self.failed_installs
|
upgrade_success_count = 0
|
||||||
|
for import_name, pip_name in self.packages_need_upgrade:
|
||||||
|
if self.upgrade_package(import_name, pip_name):
|
||||||
|
upgrade_success_count += 1
|
||||||
|
else:
|
||||||
|
self.failed_installs.append((import_name, pip_name))
|
||||||
|
|
||||||
|
print_status(f"✓ 成功升级 {upgrade_success_count}/{len(self.packages_need_upgrade)} 个包", "success")
|
||||||
|
|
||||||
|
if self.failed_installs:
|
||||||
|
print_status(f"有 {len(self.failed_installs)} 个包操作失败:", "error")
|
||||||
|
for import_name, pip_name in self.failed_installs:
|
||||||
|
print_status(f" - {import_name} ({pip_name})", "error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def verify_installation(self) -> bool:
|
def verify_installation(self) -> bool:
|
||||||
|
"""验证安装结果"""
|
||||||
if not self.missing_packages and not self.packages_need_upgrade:
|
if not self.missing_packages and not self.packages_need_upgrade:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
print_status("验证安装结果...", "info")
|
print_status("验证安装结果...", "info")
|
||||||
|
|
||||||
failed_verification = []
|
failed_verification = []
|
||||||
|
|
||||||
|
# 验证新安装的包
|
||||||
for import_name, pip_name in self.missing_packages:
|
for import_name, pip_name in self.missing_packages:
|
||||||
if not self.check_package_installed(import_name):
|
if not self.check_package_installed(import_name):
|
||||||
failed_verification.append((import_name, pip_name))
|
failed_verification.append((import_name, pip_name))
|
||||||
|
|
||||||
|
# 验证升级的包
|
||||||
for import_name, pip_name in self.packages_need_upgrade:
|
for import_name, pip_name in self.packages_need_upgrade:
|
||||||
if not self.check_package_installed(import_name):
|
if not self.check_package_installed(import_name):
|
||||||
failed_verification.append((import_name, pip_name))
|
failed_verification.append((import_name, pip_name))
|
||||||
@@ -330,14 +270,17 @@ def check_environment(auto_install: bool = True, show_details: bool = True) -> b
|
|||||||
"""
|
"""
|
||||||
checker = EnvironmentChecker()
|
checker = EnvironmentChecker()
|
||||||
|
|
||||||
|
# 检查包
|
||||||
if checker.check_all_packages():
|
if checker.check_all_packages():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# 安装缺失的包
|
||||||
if not checker.install_missing_packages(auto_install):
|
if not checker.install_missing_packages(auto_install):
|
||||||
if show_details:
|
if show_details:
|
||||||
print_status("请手动安装缺失的包后重新启动程序", "error")
|
print_status("请手动安装缺失的包后重新启动程序", "error")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# 验证安装
|
||||||
if not checker.verify_installation():
|
if not checker.verify_installation():
|
||||||
if show_details:
|
if show_details:
|
||||||
print_status("安装验证失败,请检查网络连接或手动安装", "error")
|
print_status("安装验证失败,请检查网络连接或手动安装", "error")
|
||||||
@@ -347,12 +290,14 @@ def check_environment(auto_install: bool = True, show_details: bool = True) -> b
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
# 命令行参数解析
|
||||||
parser = argparse.ArgumentParser(description="UniLabOS 环境依赖检查工具")
|
parser = argparse.ArgumentParser(description="UniLabOS 环境依赖检查工具")
|
||||||
parser.add_argument("--no-auto-install", action="store_true", help="仅检查环境,不自动安装缺失的包")
|
parser.add_argument("--no-auto-install", action="store_true", help="仅检查环境,不自动安装缺失的包")
|
||||||
parser.add_argument("--silent", action="store_true", help="静默模式,不显示详细信息")
|
parser.add_argument("--silent", action="store_true", help="静默模式,不显示详细信息")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# 执行环境检查
|
||||||
auto_install = not args.no_auto_install
|
auto_install = not args.no_auto_install
|
||||||
show_details = not args.silent
|
show_details = not args.silent
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import psutil
|
import psutil
|
||||||
import pywinauto
|
import pywinauto
|
||||||
from pywinauto_recorder import UIApplication
|
try:
|
||||||
from pywinauto_recorder.player import UIPath, click, focus_on_application, exists, find, get_wrapper_path
|
from pywinauto_recorder import UIApplication
|
||||||
|
from pywinauto_recorder.player import UIPath, click, focus_on_application, exists, find, get_wrapper_path
|
||||||
|
except ImportError:
|
||||||
|
print("未安装pywinauto_recorder,部分功能无法使用,安装时注意enum")
|
||||||
|
pass
|
||||||
from pywinauto.controls.uiawrapper import UIAWrapper
|
from pywinauto.controls.uiawrapper import UIAWrapper
|
||||||
from pywinauto.application import WindowSpecification
|
from pywinauto.application import WindowSpecification
|
||||||
from pywinauto import findbestmatch
|
from pywinauto import findbestmatch
|
||||||
|
|||||||
@@ -80,12 +80,11 @@ def get_result_info_str(error: str, suc: bool, return_value=None) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
JSON字符串格式的结果信息
|
JSON字符串格式的结果信息
|
||||||
"""
|
"""
|
||||||
# 请在返回的字典中使用 unilabos_samples进行返回
|
samples = None
|
||||||
# samples = None
|
if isinstance(return_value, dict):
|
||||||
# if isinstance(return_value, dict):
|
if "samples" in return_value:
|
||||||
# if "samples" in return_value and type(return_value["samples"]) in [list, tuple] and type(return_value["samples"][0]) == dict:
|
samples = return_value.pop("samples")
|
||||||
# samples = return_value.pop("samples")
|
result_info = {"error": error, "suc": suc, "return_value": return_value, "samples": samples}
|
||||||
result_info = {"error": error, "suc": suc, "return_value": return_value}
|
|
||||||
|
|
||||||
return json.dumps(result_info, ensure_ascii=False, cls=ResultInfoEncoder)
|
return json.dumps(result_info, ensure_ascii=False, cls=ResultInfoEncoder)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user