mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-04-23 20:49:59 +00:00
Compare commits
32 Commits
prcix9320
...
workstatio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99ee27bfc2 | ||
|
|
e8f54d50f9 | ||
|
|
201b1064d7 | ||
|
|
2ebe35e70e | ||
|
|
717f236332 | ||
|
|
79c0815b70 | ||
|
|
f431d61d85 | ||
|
|
3af86a07f2 | ||
|
|
d1713fcca1 | ||
|
|
52b460466d | ||
|
|
7efccbc688 | ||
|
|
dc1de44b19 | ||
|
|
4581ee1eeb | ||
|
|
620cb8435f | ||
|
|
83565038cb | ||
|
|
01d281189a | ||
|
|
db22156d77 | ||
|
|
20342c6484 | ||
|
|
008c355754 | ||
|
|
0895252bc1 | ||
|
|
3e43359460 | ||
|
|
73add2dc06 | ||
|
|
dd21d93151 | ||
|
|
e11c3533c7 | ||
|
|
ed952e8a44 | ||
|
|
467f0b1115 | ||
|
|
91928a87ac | ||
|
|
d7850b050b | ||
|
|
dff70bd72b | ||
|
|
03e3719b18 | ||
|
|
41a018febc | ||
|
|
7505e024f3 |
@@ -27,14 +27,15 @@ python -c "import base64,sys; print('Authorization: Lab ' + base64.b64encode(f'{
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
|-------------|------|
|
||||
| `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` |
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------ | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
AUTH="Authorization: Lab <gen_auth.py 输出的 token>"
|
||||
@@ -65,7 +66,7 @@ curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
返回:
|
||||
|
||||
```json
|
||||
{"code": 0, "data": {"uuid": "xxx", "name": "实验室名称"}}
|
||||
{ "code": 0, "data": { "uuid": "xxx", "name": "实验室名称" } }
|
||||
```
|
||||
|
||||
记住 `data.uuid` 为 `lab_uuid`。
|
||||
@@ -90,6 +91,7 @@ curl -s -X POST "$BASE/api/v1/lab/reagent" \
|
||||
```
|
||||
|
||||
返回成功时包含试剂 UUID:
|
||||
|
||||
```json
|
||||
{"code": 0, "data": {"uuid": "xxx", ...}}
|
||||
```
|
||||
@@ -98,28 +100,28 @@ curl -s -X POST "$BASE/api/v1/lab/reagent" \
|
||||
|
||||
## 试剂字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 | 示例 |
|
||||
|------|------|------|------|------|
|
||||
| `lab_uuid` | string | 是 | 实验室 UUID(从 API #1 获取) | `"8511c672-..."` |
|
||||
| `cas` | string | 是 | CAS 注册号 | `"7732-18-3"` |
|
||||
| `name` | string | 是 | 试剂中文/英文名称 | `"水"` |
|
||||
| `molecular_formula` | string | 是 | 分子式 | `"H2O"` |
|
||||
| `smiles` | string | 是 | SMILES 表示 | `"O"` |
|
||||
| `stock_in_quantity` | number | 是 | 入库数量 | `10` |
|
||||
| `unit` | string | 是 | 单位(字符串,见下表) | `"mL"` |
|
||||
| `supplier` | string | 否 | 供应商名称 | `"国药集团"` |
|
||||
| `production_date` | string | 否 | 生产日期(ISO 8601) | `"2025-11-18T00:00:00Z"` |
|
||||
| `expiry_date` | string | 否 | 过期日期(ISO 8601) | `"2026-11-18T00:00:00Z"` |
|
||||
| 字段 | 类型 | 必填 | 说明 | 示例 |
|
||||
| ------------------- | ------ | ---- | ----------------------------- | ------------------------ |
|
||||
| `lab_uuid` | string | 是 | 实验室 UUID(从 API #1 获取) | `"8511c672-..."` |
|
||||
| `cas` | string | 是 | CAS 注册号 | `"7732-18-3"` |
|
||||
| `name` | string | 是 | 试剂中文/英文名称 | `"水"` |
|
||||
| `molecular_formula` | string | 是 | 分子式 | `"H2O"` |
|
||||
| `smiles` | string | 是 | SMILES 表示 | `"O"` |
|
||||
| `stock_in_quantity` | number | 是 | 入库数量 | `10` |
|
||||
| `unit` | string | 是 | 单位(字符串,见下表) | `"mL"` |
|
||||
| `supplier` | string | 否 | 供应商名称 | `"国药集团"` |
|
||||
| `production_date` | string | 否 | 生产日期(ISO 8601) | `"2025-11-18T00:00:00Z"` |
|
||||
| `expiry_date` | string | 否 | 过期日期(ISO 8601) | `"2026-11-18T00:00:00Z"` |
|
||||
|
||||
### unit 单位值
|
||||
|
||||
| 值 | 单位 |
|
||||
|------|------|
|
||||
| 值 | 单位 |
|
||||
| ------ | ---- |
|
||||
| `"mL"` | 毫升 |
|
||||
| `"L"` | 升 |
|
||||
| `"g"` | 克 |
|
||||
| `"L"` | 升 |
|
||||
| `"g"` | 克 |
|
||||
| `"kg"` | 千克 |
|
||||
| `"瓶"` | 瓶 |
|
||||
| `"瓶"` | 瓶 |
|
||||
|
||||
> 根据试剂状态选择:液体用 `"mL"` / `"L"`,固体用 `"g"` / `"kg"`。
|
||||
|
||||
@@ -133,8 +135,22 @@ curl -s -X POST "$BASE/api/v1/lab/reagent" \
|
||||
|
||||
```json
|
||||
[
|
||||
{"cas": "7732-18-3", "name": "水", "molecular_formula": "H2O", "smiles": "O", "stock_in_quantity": 10, "unit": "mL"},
|
||||
{"cas": "64-17-5", "name": "乙醇", "molecular_formula": "C2H6O", "smiles": "CCO", "stock_in_quantity": 5, "unit": "L"}
|
||||
{
|
||||
"cas": "7732-18-3",
|
||||
"name": "水",
|
||||
"molecular_formula": "H2O",
|
||||
"smiles": "O",
|
||||
"stock_in_quantity": 10,
|
||||
"unit": "mL"
|
||||
},
|
||||
{
|
||||
"cas": "64-17-5",
|
||||
"name": "乙醇",
|
||||
"molecular_formula": "C2H6O",
|
||||
"smiles": "CCO",
|
||||
"stock_in_quantity": 5,
|
||||
"unit": "L"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
@@ -160,9 +176,20 @@ cas,name,molecular_formula,smiles,stock_in_quantity,unit,supplier,production_dat
|
||||
7732-18-3,水,H2O,O,10,mL,农夫山泉,2025-11-18T00:00:00Z,2026-11-18T00:00:00Z
|
||||
```
|
||||
|
||||
### 日期格式规则(重要)
|
||||
|
||||
所有日期字段(`production_date`、`expiry_date`)**必须**使用 ISO 8601 完整格式:`YYYY-MM-DDTHH:MM:SSZ`。
|
||||
|
||||
- 用户输入 `2025-03-01` → 转换为 `"2025-03-01T00:00:00Z"`
|
||||
- 用户输入 `2025/9/1` → 转换为 `"2025-09-01T00:00:00Z"`
|
||||
- 用户未提供日期 → 使用当天日期 + `T00:00:00Z`,有效期默认 +1 年
|
||||
|
||||
**禁止**发送不带时间部分的日期字符串(如 `"2025-03-01"`),API 会拒绝。
|
||||
|
||||
### 执行与汇报
|
||||
|
||||
每次 API 调用后:
|
||||
|
||||
1. 检查返回 `code`(0 = 成功)
|
||||
2. 记录成功/失败数量
|
||||
3. 全部完成后汇总:「共录入 N 条试剂,成功 X 条,失败 Y 条」
|
||||
@@ -172,28 +199,29 @@ cas,name,molecular_formula,smiles,stock_in_quantity,unit,supplier,production_dat
|
||||
|
||||
## 常见试剂速查表
|
||||
|
||||
| 名称 | CAS | 分子式 | SMILES |
|
||||
|------|-----|--------|--------|
|
||||
| 水 | 7732-18-3 | H2O | O |
|
||||
| 乙醇 | 64-17-5 | C2H6O | CCO |
|
||||
| 甲醇 | 67-56-1 | CH4O | CO |
|
||||
| 丙酮 | 67-64-1 | C3H6O | CC(C)=O |
|
||||
| 二甲基亚砜(DMSO) | 67-68-5 | C2H6OS | CS(C)=O |
|
||||
| 乙酸乙酯 | 141-78-6 | C4H8O2 | CCOC(C)=O |
|
||||
| 二氯甲烷 | 75-09-2 | CH2Cl2 | ClCCl |
|
||||
| 四氢呋喃(THF) | 109-99-9 | C4H8O | C1CCOC1 |
|
||||
| N,N-二甲基甲酰胺(DMF) | 68-12-2 | C3H7NO | CN(C)C=O |
|
||||
| 氯仿 | 67-66-3 | CHCl3 | ClC(Cl)Cl |
|
||||
| 乙腈 | 75-05-8 | C2H3N | CC#N |
|
||||
| 甲苯 | 108-88-3 | C7H8 | Cc1ccccc1 |
|
||||
| 正己烷 | 110-54-3 | C6H14 | CCCCCC |
|
||||
| 异丙醇 | 67-63-0 | C3H8O | CC(C)O |
|
||||
| 盐酸 | 7647-01-0 | HCl | Cl |
|
||||
| 硫酸 | 7664-93-9 | H2SO4 | OS(O)(=O)=O |
|
||||
| 氢氧化钠 | 1310-73-2 | NaOH | [Na]O |
|
||||
| 碳酸钠 | 497-19-8 | Na2CO3 | [Na]OC([O-])=O.[Na+] |
|
||||
| 氯化钠 | 7647-14-5 | NaCl | [Na]Cl |
|
||||
| 乙二胺四乙酸(EDTA) | 60-00-4 | C10H16N2O8 | OC(=O)CN(CCN(CC(O)=O)CC(O)=O)CC(O)=O |
|
||||
| 名称 | CAS | 分子式 | SMILES |
|
||||
| --------------------- | --------- | ---------- | ------------------------------------ |
|
||||
| 水 | 7732-18-3 | H2O | O |
|
||||
| 乙醇 | 64-17-5 | C2H6O | CCO |
|
||||
| 乙酸 | 64-19-7 | C2H4O2 | CC(O)=O |
|
||||
| 甲醇 | 67-56-1 | CH4O | CO |
|
||||
| 丙酮 | 67-64-1 | C3H6O | CC(C)=O |
|
||||
| 二甲基亚砜(DMSO) | 67-68-5 | C2H6OS | CS(C)=O |
|
||||
| 乙酸乙酯 | 141-78-6 | C4H8O2 | CCOC(C)=O |
|
||||
| 二氯甲烷 | 75-09-2 | CH2Cl2 | ClCCl |
|
||||
| 四氢呋喃(THF) | 109-99-9 | C4H8O | C1CCOC1 |
|
||||
| N,N-二甲基甲酰胺(DMF) | 68-12-2 | C3H7NO | CN(C)C=O |
|
||||
| 氯仿 | 67-66-3 | CHCl3 | ClC(Cl)Cl |
|
||||
| 乙腈 | 75-05-8 | C2H3N | CC#N |
|
||||
| 甲苯 | 108-88-3 | C7H8 | Cc1ccccc1 |
|
||||
| 正己烷 | 110-54-3 | C6H14 | CCCCCC |
|
||||
| 异丙醇 | 67-63-0 | C3H8O | CC(C)O |
|
||||
| 盐酸 | 7647-01-0 | HCl | Cl |
|
||||
| 硫酸 | 7664-93-9 | H2SO4 | OS(O)(=O)=O |
|
||||
| 氢氧化钠 | 1310-73-2 | NaOH | [Na]O |
|
||||
| 碳酸钠 | 497-19-8 | Na2CO3 | [Na]OC([O-])=O.[Na+] |
|
||||
| 氯化钠 | 7647-14-5 | NaCl | [Na]Cl |
|
||||
| 乙二胺四乙酸(EDTA) | 60-00-4 | C10H16N2O8 | OC(=O)CN(CCN(CC(O)=O)CC(O)=O)CC(O)=O |
|
||||
|
||||
> 此表仅供快速参考。对于不在表中的试剂,agent 应根据化学知识推断或提示用户补充。
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
---
|
||||
name: batch-submit-experiment
|
||||
description: Batch submit experiments (notebooks) to Uni-Lab platform — list workflows, generate node_params from registry schemas, submit multiple rounds, check notebook status. Use when the user wants to submit experiments, create notebooks, batch run workflows, check experiment status, or mentions 提交实验/批量实验/notebook/实验轮次/实验状态.
|
||||
description: Batch submit experiments (notebooks) to the Uni-Lab cloud platform (leap-lab) — list workflows, generate node_params from registry schemas, submit multiple rounds, check notebook status. Use when the user wants to submit experiments, create notebooks, batch run workflows, check experiment status, or mentions 提交实验/批量实验/notebook/实验轮次/实验状态.
|
||||
---
|
||||
|
||||
# 批量提交实验指南
|
||||
# Uni-Lab 批量提交实验指南
|
||||
|
||||
通过云端 API 批量提交实验(notebook),支持多轮实验参数配置。根据 workflow 模板详情和本地设备注册表自动生成 `node_params` 模板。
|
||||
通过 Uni-Lab 云端 API 批量提交实验(notebook),支持多轮实验参数配置。根据 workflow 模板详情和本地设备注册表自动生成 `node_params` 模板。
|
||||
|
||||
> **重要**:本指南中的 `Authorization: Lab <token>` 是 **Uni-Lab 平台专用的认证方式**,`Lab` 是 Uni-Lab 的 auth scheme 关键字,**不是** HTTP Basic 认证。请勿将其替换为 `Basic`。
|
||||
|
||||
## 前置条件(缺一不可)
|
||||
|
||||
@@ -18,25 +20,28 @@ description: Batch submit experiments (notebooks) to Uni-Lab platform — list w
|
||||
生成 AUTH token(任选一种方式):
|
||||
|
||||
```bash
|
||||
# 方式一:Python 一行生成
|
||||
# 方式一:Python 一行生成(注意:scheme 是 "Lab" 不是 "Basic")
|
||||
python -c "import base64,sys; print('Authorization: Lab ' + base64.b64encode(f'{sys.argv[1]}:{sys.argv[2]}'.encode()).decode())" <ak> <sk>
|
||||
|
||||
# 方式二:手动计算
|
||||
# base64(ak:sk) → Authorization: Lab <token>
|
||||
# ⚠️ 这里的 "Lab" 是 Uni-Lab 平台的 auth scheme,绝对不能用 "Basic" 替代
|
||||
```
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
|-------------|------|
|
||||
| `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` |
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------ | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
# ⚠️ Auth scheme 必须是 "Lab"(Uni-Lab 专用),不是 "Basic"
|
||||
AUTH="Authorization: Lab <上面命令输出的 token>"
|
||||
```
|
||||
|
||||
@@ -44,18 +49,19 @@ AUTH="Authorization: Lab <上面命令输出的 token>"
|
||||
|
||||
**批量提交实验时需要本地注册表来解析 workflow 节点的参数 schema。**
|
||||
|
||||
按优先级搜索:
|
||||
**必须先用 Glob 工具搜索文件**,不要直接猜测路径:
|
||||
|
||||
```
|
||||
<workspace 根目录>/unilabos_data/req_device_registry_upload.json
|
||||
<workspace 根目录>/req_device_registry_upload.json
|
||||
Glob: **/req_device_registry_upload.json
|
||||
```
|
||||
|
||||
也可直接 Glob 搜索:`**/req_device_registry_upload.json`
|
||||
常见位置(仅供参考,以 Glob 实际结果为准):
|
||||
- `<workspace>/unilabos_data/req_device_registry_upload.json`
|
||||
- `<workspace>/req_device_registry_upload.json`
|
||||
|
||||
找到后**检查文件修改时间**并告知用户。超过 1 天提醒用户是否需要重新启动 `unilab`。
|
||||
|
||||
**如果文件不存在** → 告知用户先运行 `unilab` 启动命令,等注册表生成后再执行。可跳过此步,但将无法自动生成参数模板,需要用户手动填写 `param`。
|
||||
**如果 Glob 搜索无结果** → 告知用户先运行 `unilab` 启动命令,等注册表生成后再执行。可跳过此步,但将无法自动生成参数模板,需要用户手动填写 `param`。
|
||||
|
||||
### 4. workflow_uuid(目标工作流)
|
||||
|
||||
@@ -93,7 +99,7 @@ curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
返回:
|
||||
|
||||
```json
|
||||
{"code": 0, "data": {"uuid": "xxx", "name": "实验室名称"}}
|
||||
{ "code": 0, "data": { "uuid": "xxx", "name": "实验室名称" } }
|
||||
```
|
||||
|
||||
记住 `data.uuid` 为 `lab_uuid`。
|
||||
@@ -104,9 +110,33 @@ curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
curl -s -X GET "$BASE/api/v1/lab/project/list?lab_uuid=$lab_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回项目列表,展示给用户选择。列出每个项目的 `uuid` 和 `name`。
|
||||
返回:
|
||||
|
||||
用户**必须**选择一个项目,记住 `project_uuid`,后续创建 notebook 时需要提供。
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"uuid": "1b3f249a-...",
|
||||
"name": "bt",
|
||||
"description": null,
|
||||
"status": "active",
|
||||
"created_at": "2026-04-09T14:31:28+08:00"
|
||||
},
|
||||
{
|
||||
"uuid": "b6366243-...",
|
||||
"name": "default",
|
||||
"description": "默认项目",
|
||||
"status": "active",
|
||||
"created_at": "2026-03-26T11:13:36+08:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
展示 `data.items[]` 中每个项目的 `name` 和 `uuid`,让用户选择。用户**必须**选择一个项目,记住 `project_uuid`(即选中项目的 `uuid`),后续创建 notebook 时需要提供。
|
||||
|
||||
### 3. 列出可用 workflow
|
||||
|
||||
@@ -123,6 +153,7 @@ curl -s -X GET "$BASE/api/v1/lab/workflow/template/detail/$workflow_uuid" -H "$A
|
||||
```
|
||||
|
||||
返回 workflow 的完整结构,包含所有 action 节点信息。需要从响应中提取:
|
||||
|
||||
- 每个 action 节点的 `node_uuid`
|
||||
- 每个节点对应的设备 ID(`resource_template_name`)
|
||||
- 每个节点的动作名(`node_template_name`)
|
||||
@@ -142,30 +173,30 @@ curl -s -X POST "$BASE/api/v1/lab/notebook" \
|
||||
|
||||
```json
|
||||
{
|
||||
"lab_uuid": "<lab_uuid>",
|
||||
"project_uuid": "<project_uuid>",
|
||||
"workflow_uuid": "<workflow_uuid>",
|
||||
"name": "<实验名称>",
|
||||
"node_params": [
|
||||
"lab_uuid": "<lab_uuid>",
|
||||
"project_uuid": "<project_uuid>",
|
||||
"workflow_uuid": "<workflow_uuid>",
|
||||
"name": "<实验名称>",
|
||||
"node_params": [
|
||||
{
|
||||
"sample_uuids": ["<样品UUID1>", "<样品UUID2>"],
|
||||
"datas": [
|
||||
{
|
||||
"sample_uuids": ["<样品UUID1>", "<样品UUID2>"],
|
||||
"datas": [
|
||||
{
|
||||
"node_uuid": "<workflow中的节点UUID>",
|
||||
"param": {},
|
||||
"sample_params": [
|
||||
{
|
||||
"container_uuid": "<容器UUID>",
|
||||
"sample_value": {
|
||||
"liquid_names": "<液体名称>",
|
||||
"volumes": 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"node_uuid": "<workflow中的节点UUID>",
|
||||
"param": {},
|
||||
"sample_params": [
|
||||
{
|
||||
"container_uuid": "<容器UUID>",
|
||||
"sample_value": {
|
||||
"liquid_names": "<液体名称>",
|
||||
"volumes": 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -194,25 +225,25 @@ curl -s -X GET "$BASE/api/v1/lab/notebook/status?uuid=$notebook_uuid" -H "$AUTH"
|
||||
|
||||
### 每轮的字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| 字段 | 类型 | 说明 |
|
||||
| -------------- | ------------- | ----------------------------------------- |
|
||||
| `sample_uuids` | array\<uuid\> | 该轮实验的样品 UUID 数组,无样品时传 `[]` |
|
||||
| `datas` | array | 该轮中每个 workflow 节点的参数配置 |
|
||||
| `datas` | array | 该轮中每个 workflow 节点的参数配置 |
|
||||
|
||||
### datas 中每个节点
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `node_uuid` | string | workflow 模板中的节点 UUID(从 API #4 获取) |
|
||||
| `param` | object | 动作参数(根据本地注册表 schema 填写) |
|
||||
| `sample_params` | array | 样品相关参数(液体名、体积等) |
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------------- | ------ | -------------------------------------------- |
|
||||
| `node_uuid` | string | workflow 模板中的节点 UUID(从 API #4 获取) |
|
||||
| `param` | object | 动作参数(根据本地注册表 schema 填写) |
|
||||
| `sample_params` | array | 样品相关参数(液体名、体积等) |
|
||||
|
||||
### sample_params 中每条
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `container_uuid` | string | 容器 UUID |
|
||||
| `sample_value` | object | 样品值,如 `{"liquid_names": "水", "volumes": 1000}` |
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---------------- | ------ | ---------------------------------------------------- |
|
||||
| `container_uuid` | string | 容器 UUID |
|
||||
| `sample_value` | object | 样品值,如 `{"liquid_names": "水", "volumes": 1000}` |
|
||||
|
||||
---
|
||||
|
||||
@@ -233,6 +264,7 @@ python scripts/gen_notebook_params.py \
|
||||
> 脚本位于本文档同级目录下的 `scripts/gen_notebook_params.py`。
|
||||
|
||||
脚本会:
|
||||
|
||||
1. 调用 workflow detail API 获取所有 action 节点
|
||||
2. 读取本地注册表,为每个节点查找对应的 action schema
|
||||
3. 生成 `notebook_template.json`,包含:
|
||||
@@ -270,8 +302,11 @@ python scripts/gen_notebook_params.py \
|
||||
"properties": {
|
||||
"goal": {
|
||||
"properties": {
|
||||
"asp_vols": {"type": "array", "items": {"type": "number"}},
|
||||
"sources": {"type": "array"}
|
||||
"asp_vols": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" }
|
||||
},
|
||||
"sources": { "type": "array" }
|
||||
},
|
||||
"required": ["asp_vols", "sources"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
选项:
|
||||
--auth <token> Lab token(base64(ak:sk) 的结果,不含 "Lab " 前缀)
|
||||
--base <url> API 基础 URL(如 https://uni-lab.test.bohrium.com)
|
||||
--base <url> API 基础 URL(如 https://leap-lab.test.bohrium.com)
|
||||
--workflow-uuid <uuid> 目标 workflow 的 UUID
|
||||
--registry <path> 本地注册表文件路径(默认自动搜索)
|
||||
--rounds <n> 实验轮次数(默认 1)
|
||||
@@ -17,7 +17,7 @@
|
||||
示例:
|
||||
python gen_notebook_params.py \\
|
||||
--auth YTFmZDlkNGUtxxxx \\
|
||||
--base https://uni-lab.test.bohrium.com \\
|
||||
--base https://leap-lab.test.bohrium.com \\
|
||||
--workflow-uuid abc-123-def \\
|
||||
--rounds 2
|
||||
"""
|
||||
|
||||
@@ -40,13 +40,13 @@ python ./scripts/gen_auth.py --config <config.py>
|
||||
|
||||
决定 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 |
|
||||
| `--addr` 值 | BASE URL |
|
||||
| -------------- | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
| 其他自定义 URL | 直接使用该 URL |
|
||||
|
||||
#### 必备项 ③:req_device_registry_upload.json(设备注册表)
|
||||
|
||||
@@ -54,11 +54,11 @@ python ./scripts/gen_auth.py --config <config.py>
|
||||
|
||||
**推断 working_dir**(即 `unilabos_data` 所在目录):
|
||||
|
||||
| 条件 | working_dir 取值 |
|
||||
|------|------------------|
|
||||
| 条件 | working_dir 取值 |
|
||||
| -------------------- | -------------------------------------------------------- |
|
||||
| 传了 `--working_dir` | `<working_dir>/unilabos_data/`(若子目录已存在则直接用) |
|
||||
| 仅传了 `--config` | `<config 文件所在目录>/unilabos_data/` |
|
||||
| 都没传 | `<当前工作目录>/unilabos_data/` |
|
||||
| 仅传了 `--config` | `<config 文件所在目录>/unilabos_data/` |
|
||||
| 都没传 | `<当前工作目录>/unilabos_data/` |
|
||||
|
||||
**按优先级搜索文件**:
|
||||
|
||||
@@ -84,24 +84,6 @@ python ./scripts/gen_auth.py --config <config.py>
|
||||
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 — 列出可用设备
|
||||
@@ -129,6 +111,7 @@ python ./scripts/extract_device_actions.py [--registry <path>] <device_id> ./ski
|
||||
脚本会显示设备的 Python 源码路径和类名,方便阅读源码了解参数含义。
|
||||
|
||||
每个 action 生成一个 JSON 文件,包含:
|
||||
|
||||
- `type` — 作为 API 调用的 `action_type`
|
||||
- `schema` — 完整 JSON Schema(含 `properties.goal.properties` 参数定义)
|
||||
- `goal` — goal 字段映射(含占位符 `$placeholder`)
|
||||
@@ -136,13 +119,14 @@ python ./scripts/extract_device_actions.py [--registry <path>] <device_id> ./ski
|
||||
|
||||
### Step 3 — 写 action-index.md
|
||||
|
||||
按模板为每个 action 写条目:
|
||||
按模板为每个 action 写条目(**必须包含 `action_type`**):
|
||||
|
||||
```markdown
|
||||
### `<action_name>`
|
||||
|
||||
<用途描述(一句话)>
|
||||
|
||||
- **action_type**: `<从 actions/<name>.json 的 type 字段获取>`
|
||||
- **Schema**: [`actions/<filename>.json`](actions/<filename>.json)
|
||||
- **核心参数**: `param1`, `param2`(从 schema.required 获取)
|
||||
- **可选参数**: `param3`, `param4`
|
||||
@@ -150,6 +134,8 @@ python ./scripts/extract_device_actions.py [--registry <path>] <device_id> ./ski
|
||||
```
|
||||
|
||||
描述规则:
|
||||
|
||||
- **每个 action 必须标注 `action_type`**(从 JSON 的 `type` 字段读取),这是 API #9 调用时的必填参数,传错会导致任务永远卡住
|
||||
- 从 `schema.properties` 读参数列表(schema 已提升为 goal 内容)
|
||||
- 从 `schema.required` 区分核心/可选参数
|
||||
- 按功能分类(移液、枪头、外设等)
|
||||
@@ -165,6 +151,7 @@ python ./scripts/extract_device_actions.py [--registry <path>] <device_id> ./ski
|
||||
### Step 4 — 写 SKILL.md
|
||||
|
||||
直接复用 `unilab-device-api` 的 API 模板,修改:
|
||||
|
||||
- 设备名称
|
||||
- Action 数量
|
||||
- 目录列表
|
||||
@@ -172,42 +159,77 @@ python ./scripts/extract_device_actions.py [--registry <path>] <device_id> ./ski
|
||||
- **AUTH 头** — 使用 Step 0 中 `gen_auth.py` 生成的 `Authorization: Lab <token>`(不要硬编码 `Api` 类型的 key)
|
||||
- **Python 源码路径** — 在 SKILL.md 开头注明设备对应的源码文件,方便参考参数含义
|
||||
- **Slot 字段表** — 列出本设备哪些 action 的哪些字段需要填入 Slot(物料/设备/节点/类名)
|
||||
- **action_type 速查表** — 在 API #9 说明后面紧跟一个表格,列出每个 action 对应的 `action_type` 值(从 JSON `type` 字段提取),方便 agent 快速查找而无需打开 JSON 文件
|
||||
|
||||
API 模板结构:
|
||||
|
||||
```markdown
|
||||
## 设备信息
|
||||
|
||||
- device_id, Python 源码路径, 设备类名
|
||||
|
||||
## 前置条件(缺一不可)
|
||||
|
||||
- ak/sk → AUTH, --addr → BASE URL
|
||||
|
||||
## 请求约定
|
||||
|
||||
- Windows 平台必须用 curl.exe(非 PowerShell 的 curl 别名)
|
||||
|
||||
## Session State
|
||||
|
||||
- lab_uuid(通过 GET /edge/lab/info 直接获取,不要问用户), device_name
|
||||
|
||||
## API Endpoints
|
||||
# - #1 GET /edge/lab/info → 直接拿到 lab_uuid
|
||||
# - #2 创建工作流 POST /lab/workflow/owner → 拼 URL 告知用户
|
||||
# - #3 创建节点 POST /edge/workflow/node
|
||||
# body: {workflow_uuid, resource_template_name: "<device_id>", node_template_name: "<action_name>"}
|
||||
# - #4 删除节点 DELETE /lab/workflow/nodes
|
||||
# - #5 更新节点参数 PATCH /lab/workflow/node
|
||||
# - #6 查询节点 handles POST /lab/workflow/node-handles
|
||||
# body: {node_uuids: ["uuid1","uuid2"]} → 返回各节点的 handle_uuid
|
||||
# - #7 批量创建边 POST /lab/workflow/edges
|
||||
# body: {edges: [{source_node_uuid, target_node_uuid, source_handle_uuid, target_handle_uuid}]}
|
||||
# - #8 启动工作流 POST /lab/workflow/{uuid}/run
|
||||
# - #9 运行设备单动作 POST /lab/mcp/run/action
|
||||
|
||||
# - #1 GET /edge/lab/info → 直接拿到 lab_uuid
|
||||
|
||||
# - #2 创建工作流 POST /lab/workflow/owner → 拼 URL 告知用户
|
||||
|
||||
# - #3 创建节点 POST /edge/workflow/node
|
||||
|
||||
# body: {workflow_uuid, resource_template_name: "<device_id>", node_template_name: "<action_name>"}
|
||||
|
||||
# - #4 删除节点 DELETE /lab/workflow/nodes
|
||||
|
||||
# - #5 更新节点参数 PATCH /lab/workflow/node
|
||||
|
||||
# - #6 查询节点 handles POST /lab/workflow/node-handles
|
||||
|
||||
# body: {node_uuids: ["uuid1","uuid2"]} → 返回各节点的 handle_uuid
|
||||
|
||||
# - #7 批量创建边 POST /lab/workflow/edges
|
||||
|
||||
# body: {edges: [{source_node_uuid, target_node_uuid, source_handle_uuid, target_handle_uuid}]}
|
||||
|
||||
# - #8 启动工作流 POST /lab/workflow/{uuid}/run
|
||||
|
||||
# - #9 运行设备单动作 POST /lab/mcp/run/action(⚠️ action_type 必须从 action-index.md 或 actions/<name>.json 的 type 字段获取,传错会导致任务永远卡住)
|
||||
|
||||
# - #10 查询任务状态 GET /lab/mcp/task/{task_uuid}
|
||||
|
||||
# - #11 运行工作流单节点 POST /lab/mcp/run/workflow/action
|
||||
|
||||
# - #12 获取资源树 GET /lab/material/download/{lab_uuid}
|
||||
|
||||
# - #13 获取工作流模板详情 GET /lab/workflow/template/detail/{workflow_uuid}
|
||||
# 返回 workflow 完整结构:data.nodes[] 含每个节点的 uuid、name、param、device_name、handles
|
||||
|
||||
# 返回 workflow 完整结构:data.nodes[] 含每个节点的 uuid、name、param、device_name、handles
|
||||
|
||||
# - #14 按名称查询物料模板 GET /lab/material/template/by-name?lab_uuid=&name=
|
||||
|
||||
# 返回 res_template_uuid,用于 #15 创建物料时的必填字段
|
||||
|
||||
# - #15 创建物料节点 POST /edge/material/node
|
||||
|
||||
# body: {res_template_uuid(从#14获取), name(自定义), display_name, parent_uuid?(从#12获取), ...}
|
||||
|
||||
# - #16 更新物料节点 PUT /edge/material/node
|
||||
|
||||
# body: {uuid(从#12获取), display_name?, description?, init_param_data?, data?, ...}
|
||||
|
||||
## Placeholder Slot 填写规则
|
||||
|
||||
- unilabos_resources → ResourceSlot → {"id":"/path/name","name":"name","uuid":"xxx"}
|
||||
- unilabos_devices → DeviceSlot → "/parent/device" 路径字符串
|
||||
- unilabos_nodes → NodeSlot → "/parent/node" 路径字符串
|
||||
@@ -217,13 +239,15 @@ API 模板结构:
|
||||
- 列出本设备所有 Slot 字段、类型及含义
|
||||
|
||||
## 渐进加载策略
|
||||
|
||||
## 完整工作流 Checklist
|
||||
```
|
||||
|
||||
### Step 5 — 验证
|
||||
|
||||
检查文件完整性:
|
||||
- [ ] `SKILL.md` 包含 API endpoint(#1 获取 lab_uuid、#2-#7 工作流/节点/边、#8-#11 运行/查询、#12 资源树、#13 工作流模板详情)
|
||||
|
||||
- [ ] `SKILL.md` 包含 API endpoint(#1 获取 lab_uuid、#2-#7 工作流/节点/边、#8-#11 运行/查询、#12 资源树、#13 工作流模板详情、#14-#16 物料管理)
|
||||
- [ ] `SKILL.md` 包含 Placeholder Slot 填写规则(ResourceSlot / DeviceSlot / NodeSlot / ClassSlot / FormulationSlot + create_resource 特例)和本设备的 Slot 字段表
|
||||
- [ ] `action-index.md` 列出所有 action 并有描述
|
||||
- [ ] `actions/` 目录中每个 action 有对应 JSON 文件
|
||||
@@ -272,92 +296,48 @@ API 模板结构:
|
||||
|
||||
`placeholder_keys` / `_unilabos_placeholder_info` 中有 5 种值,对应不同的填写方式:
|
||||
|
||||
| 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 |
|
||||
| `unilabos_formulation` | FormulationSlot | `[{well_name, liquids: [{name, volume}]}]` | 资源树中物料节点的 **name**,配合液体配方 |
|
||||
| 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 |
|
||||
| `unilabos_formulation` | FormulationSlot | `[{well_name, liquids: [{name, volume}]}]` | 资源树中物料节点的 **name**,配合液体配方 |
|
||||
|
||||
### ResourceSlot(`unilabos_resources`)
|
||||
|
||||
最常见的类型。从资源树中选取**物料**节点(孔板、枪头盒、试剂槽等):
|
||||
|
||||
```json
|
||||
{"id": "/workstation/container1", "name": "container1", "uuid": "ff149a9a-2cb8-419d-8db5-d3ba056fb3c2"}
|
||||
```
|
||||
- 单个:`{"id": "/workstation/container1", "name": "container1", "uuid": "ff149a9a-..."}`
|
||||
- 数组:`[{"id": "/path/a", "name": "a", "uuid": "xxx"}, ...]`
|
||||
- `id` 从 parent 计算的路径格式,根据 action 语义选择正确的物料
|
||||
|
||||
- 单个(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`,目标物料可能尚不存在,直接填期望路径,不需要 uuid。
|
||||
|
||||
> **特例**:`create_resource` 的 `res_id` 字段,目标物料可能**尚不存在**,此时直接填写期望的路径(如 `"/workstation/container1"`),不需要 uuid。
|
||||
### DeviceSlot / NodeSlot / ClassSlot
|
||||
|
||||
### 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"
|
||||
```
|
||||
- **DeviceSlot**(`unilabos_devices`):路径字符串如 `"/host_node"`,仅 type=device 的节点
|
||||
- **NodeSlot**(`unilabos_nodes`):路径字符串如 `"/PRCXI/PRCXI_Deck"`,设备 + 物料均可选
|
||||
- **ClassSlot**(`unilabos_class`):类名字符串如 `"container"`,从 `req_resource_registry_upload.json` 查找
|
||||
|
||||
### FormulationSlot(`unilabos_formulation`)
|
||||
|
||||
描述**液体配方**:向哪些物料容器中加入哪些液体及体积。填写为**对象数组**:
|
||||
描述**液体配方**:向哪些容器中加入哪些液体及体积。
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"sample_uuid": "",
|
||||
"well_name": "YB_PrepBottle_15mL_Carrier_bottle_A1",
|
||||
"liquids": [
|
||||
{ "name": "LiPF6", "volume": 0.6 },
|
||||
{ "name": "DMC", "volume": 1.2 }
|
||||
]
|
||||
"well_name": "bottle_A1",
|
||||
"liquids": [{ "name": "LiPF6", "volume": 0.6 }]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `sample_uuid` | string | 样品 UUID,无样品时传空字符串 `""` |
|
||||
| `well_name` | string | 目标物料容器的 **name**(从资源树中取物料节点的 `name` 字段,如瓶子、孔位名称) |
|
||||
| `liquids` | array | 要加入的液体列表 |
|
||||
| `liquids[].name` | string | 液体名称(如试剂名、溶剂名) |
|
||||
| `liquids[].volume` | number | 液体体积(单位由设备决定,通常为 mL) |
|
||||
|
||||
#### 填写规则
|
||||
|
||||
- `well_name` 必须是资源树中已存在的物料节点 `name`(不是 `id` 路径),通过 API #12 获取资源树后筛选
|
||||
- 每个数组元素代表一个目标容器的配方
|
||||
- 一个容器可以加入多种液体(`liquids` 数组多条记录)
|
||||
- 与 ResourceSlot 的区别:ResourceSlot 填 `{id, name, uuid}` 指向物料本身;FormulationSlot 用 `well_name` 引用物料,并附带液体配方信息
|
||||
- `well_name` — 目标物料的 **name**(从资源树取,不是 `id` 路径)
|
||||
- `liquids[]` — 液体列表,每条含 `name`(试剂名)和 `volume`(体积,单位由上下文决定;pylabrobot 内部统一 uL)
|
||||
- `sample_uuid` — 样品 UUID,无样品传 `""`
|
||||
- 与 ResourceSlot 的区别:ResourceSlot 指向物料本身,FormulationSlot 引用物料名并附带配方信息
|
||||
|
||||
### 通过 API #12 获取资源树
|
||||
|
||||
@@ -365,7 +345,147 @@ API 模板结构:
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
注意 `lab_uuid` 在路径中(不是查询参数)。资源树返回所有节点,每个节点包含 `id`(路径格式)、`name`、`uuid`、`type`、`parent` 等字段。填写 Slot 时需根据 placeholder 类型筛选正确的节点。
|
||||
注意 `lab_uuid` 在路径中(不是查询参数)。返回结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"nodes": [
|
||||
{"name": "host_node", "uuid": "c3ec1e68-...", "type": "device", "parent": ""},
|
||||
{"name": "PRCXI", "uuid": "e249c9a6-...", "type": "device", "parent": ""},
|
||||
{"name": "PRCXI_Deck", "uuid": "fb6a8b71-...", "type": "deck", "parent": "PRCXI"}
|
||||
],
|
||||
"edges": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `data.nodes[]` — 所有节点(设备 + 物料),每个节点含 `name`、`uuid`、`type`、`parent`
|
||||
- `type` 区分设备(`device`)和物料(`deck`、`container`、`resource` 等)
|
||||
- `parent` 为父节点名称(空字符串表示顶级)
|
||||
- 填写 Slot 时根据 placeholder 类型筛选:ResourceSlot 取非 device 节点,DeviceSlot 取 device 节点
|
||||
- 创建/更新物料时:`parent_uuid` 取父节点的 `uuid`,更新目标的 `uuid` 取节点自身的 `uuid`
|
||||
|
||||
## 物料管理 API
|
||||
|
||||
设备 Skill 除了设备动作外,还需支持物料节点的创建和参数设定,用于在资源树中动态管理物料。
|
||||
|
||||
典型流程:先通过 **#14 按名称查询模板** 获取 `res_template_uuid` → 再通过 **#15 创建物料** → 之后可通过 **#16 更新物料** 修改属性。更新时需要的 `uuid` 和 `parent_uuid` 均从 **#12 资源树下载** 获取。
|
||||
|
||||
### API #14 — 按名称查询物料模板
|
||||
|
||||
创建物料前,需要先获取物料模板的 UUID。通过模板名称查询:
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/template/by-name?lab_uuid=$lab_uuid&name=<template_name>" -H "$AUTH"
|
||||
```
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
| ---------- | ------ | -------------------------------- |
|
||||
| `lab_uuid` | **是** | 实验室 UUID(从 API #1 获取) |
|
||||
| `name` | **是** | 物料模板名称(如 `"container"`) |
|
||||
|
||||
返回 `code: 0` 时,**`data.uuid`** 即为 `res_template_uuid`,用于 API #15 创建物料。返回还包含 `name`、`resource_type`、`handles`、`config_infos` 等模板元信息。
|
||||
|
||||
模板不存在时返回 `code: 10002`,`data` 为空对象。模板名称来自资源注册表中已注册的资源类型。
|
||||
|
||||
### API #15 — 创建物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '<request_body>'
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"res_template_uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"name": "my_custom_bottle",
|
||||
"display_name": "自定义瓶子",
|
||||
"parent_uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"type": "",
|
||||
"init_param_data": {},
|
||||
"schema": {},
|
||||
"data": {
|
||||
"liquids": [["water", 1000, "uL"]],
|
||||
"max_volume": 50000
|
||||
},
|
||||
"plate_well_datas": {},
|
||||
"plate_reagent_datas": {},
|
||||
"pose": {},
|
||||
"model": {}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 类型 | 数据来源 | 说明 |
|
||||
| --------------------- | ------ | ------------- | ----------------------------------- | -------------------------------------- |
|
||||
| `res_template_uuid` | **是** | string (UUID) | **API #14** 按名称查询获取 | 物料模板 UUID |
|
||||
| `name` | 否 | string | **用户自定义** | 节点名称(标识符),可自由命名 |
|
||||
| `display_name` | 否 | string | 用户自定义 | 显示名称(UI 展示用) |
|
||||
| `parent_uuid` | 否 | string (UUID) | **API #12** 资源树中父节点的 `uuid` | 父节点,为空则创建顶级节点 |
|
||||
| `type` | 否 | string | 从模板继承 | 节点类型 |
|
||||
| `init_param_data` | 否 | object | 用户指定 | 初始化参数,覆盖模板默认值 |
|
||||
| `data` | 否 | object | 用户指定 | 节点数据,container 见下方 data 格式 |
|
||||
| `plate_well_datas` | 否 | object | 用户指定 | 孔板子节点数据(创建带孔位的板时使用) |
|
||||
| `plate_reagent_datas` | 否 | object | 用户指定 | 试剂关联数据 |
|
||||
| `schema` | 否 | object | 从模板继承 | 自定义 schema,不传则从模板继承 |
|
||||
| `pose` | 否 | object | 用户指定 | 位姿信息 |
|
||||
| `model` | 否 | object | 用户指定 | 3D 模型信息 |
|
||||
|
||||
#### container 的 `data` 格式
|
||||
|
||||
> **体积单位统一为 uL(微升)**。pylabrobot 体系中所有体积值(`max_volume`、`liquids` 中的 volume)均为 uL。外部如果是 mL 需乘 1000 转换。
|
||||
|
||||
```json
|
||||
{
|
||||
"liquids": [["water", 1000, "uL"], ["ethanol", 500, "uL"]],
|
||||
"max_volume": 50000
|
||||
}
|
||||
```
|
||||
|
||||
- `liquids` — 液体列表,每条为 `[液体名称, 体积(uL), 单位字符串]`
|
||||
- `max_volume` — 容器最大容量(uL),如 50 mL = 50000 uL
|
||||
|
||||
### API #16 — 更新物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X PUT "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '<request_body>'
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"parent_uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"display_name": "新显示名称",
|
||||
"description": "新描述",
|
||||
"init_param_data": {},
|
||||
"data": {},
|
||||
"pose": {},
|
||||
"schema": {},
|
||||
"extra": {}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 类型 | 数据来源 | 说明 |
|
||||
| ----------------- | ------ | ------------- | ------------------------------------- | ---------------- |
|
||||
| `uuid` | **是** | string (UUID) | **API #12** 资源树中目标节点的 `uuid` | 要更新的物料节点 |
|
||||
| `parent_uuid` | 否 | string (UUID) | API #12 资源树 | 移动到新父节点 |
|
||||
| `display_name` | 否 | string | 用户指定 | 更新显示名称 |
|
||||
| `description` | 否 | string | 用户指定 | 更新描述 |
|
||||
| `init_param_data` | 否 | object | 用户指定 | 更新初始化参数 |
|
||||
| `data` | 否 | object | 用户指定 | 更新节点数据 |
|
||||
| `pose` | 否 | object | 用户指定 | 更新位姿 |
|
||||
| `schema` | 否 | object | 用户指定 | 更新 schema |
|
||||
| `extra` | 否 | object | 用户指定 | 更新扩展数据 |
|
||||
|
||||
> 只传需要更新的字段,未传的字段保持不变。
|
||||
|
||||
## 最终目录结构
|
||||
|
||||
|
||||
251
.cursor/skills/host-node/SKILL.md
Normal file
251
.cursor/skills/host-node/SKILL.md
Normal file
@@ -0,0 +1,251 @@
|
||||
---
|
||||
name: host-node
|
||||
description: Operate Uni-Lab host node via REST API — create resources, test latency, test resource tree, manual confirm. Use when the user mentions host_node, creating resources, resource management, testing latency, or any host node operation.
|
||||
---
|
||||
|
||||
# Host Node API Skill
|
||||
|
||||
## 设备信息
|
||||
|
||||
- **device_id**: `host_node`
|
||||
- **Python 源码**: `unilabos/ros/nodes/presets/host_node.py`
|
||||
- **设备类**: `HostNode`
|
||||
- **动作数**: 4(`create_resource`, `test_latency`, `auto-test_resource`, `manual_confirm`)
|
||||
|
||||
## 前置条件(缺一不可)
|
||||
|
||||
使用本 skill 前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。
|
||||
|
||||
### 1. ak / sk → AUTH
|
||||
|
||||
从启动参数 `--ak` `--sk` 或 config.py 中获取,生成 token:`base64(ak:sk)` → `Authorization: Lab <token>`
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------ | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
AUTH="Authorization: Lab <token>"
|
||||
```
|
||||
|
||||
**两项全部就绪后才可发起 API 请求。**
|
||||
|
||||
## Session State
|
||||
|
||||
在整个对话过程中,agent 需要记住以下状态,避免重复询问用户:
|
||||
|
||||
- `lab_uuid` — 实验室 UUID(首次通过 API #1 自动获取,**不需要问用户**)
|
||||
- `device_name` — `host_node`
|
||||
|
||||
## 请求约定
|
||||
|
||||
所有请求使用 `curl -s`,POST/PATCH/DELETE 需加 `Content-Type: application/json`。
|
||||
|
||||
> **Windows 平台**必须使用 `curl.exe`(而非 PowerShell 的 `curl` 别名)。
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. 获取实验室信息(自动获取 lab_uuid)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `lab_uuid`,`data.name` 为 `lab_name`。
|
||||
|
||||
### 2. 创建工作流
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/owner" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"name":"<名称>","lab_uuid":"<lab_uuid>","description":"<描述>"}'
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `workflow_uuid`。创建成功后告知用户链接:`$BASE/laboratory/$lab_uuid/workflow/$workflow_uuid`
|
||||
|
||||
### 3. 创建节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/workflow/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"workflow_uuid":"<workflow_uuid>","resource_template_name":"host_node","node_template_name":"<action_name>"}'
|
||||
```
|
||||
|
||||
- `resource_template_name` 固定为 `host_node`
|
||||
- `node_template_name` — action 名称(如 `create_resource`, `test_latency`)
|
||||
|
||||
### 4. 删除节点
|
||||
|
||||
```bash
|
||||
curl -s -X DELETE "$BASE/api/v1/lab/workflow/nodes" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuids":["<uuid1>"],"workflow_uuid":"<workflow_uuid>"}'
|
||||
```
|
||||
|
||||
### 5. 更新节点参数
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "$BASE/api/v1/lab/workflow/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"workflow_uuid":"<wf_uuid>","uuid":"<node_uuid>","param":{...}}'
|
||||
```
|
||||
|
||||
`param` 直接使用创建节点返回的 `data.param` 结构,修改需要填入的字段值。参考 [action-index.md](action-index.md) 确定哪些字段是 Slot。
|
||||
|
||||
### 6. 查询节点 handles
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/node-handles" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuids":["<node_uuid_1>","<node_uuid_2>"]}'
|
||||
```
|
||||
|
||||
### 7. 批量创建边
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/edges" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"edges":[{"source_node_uuid":"<uuid>","target_node_uuid":"<uuid>","source_handle_uuid":"<uuid>","target_handle_uuid":"<uuid>"}]}'
|
||||
```
|
||||
|
||||
### 8. 启动工作流
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/<workflow_uuid>/run" -H "$AUTH"
|
||||
```
|
||||
|
||||
### 9. 运行设备单动作
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/mcp/run/action" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"lab_uuid":"<lab_uuid>","device_id":"host_node","action":"<action_name>","action_type":"<type>","param":{...}}'
|
||||
```
|
||||
|
||||
`param` 直接放 goal 里的属性,**不要**再包一层 `{"goal": {...}}`。
|
||||
|
||||
> **WARNING: `action_type` 必须正确,传错会导致任务永远卡住无法完成。** 从下表或 `actions/<name>.json` 的 `type` 字段获取。
|
||||
|
||||
#### action_type 速查表
|
||||
|
||||
| action | action_type |
|
||||
|--------|-------------|
|
||||
| `test_latency` | `UniLabJsonCommand` |
|
||||
| `create_resource` | `ResourceCreateFromOuterEasy` |
|
||||
| `auto-test_resource` | `UniLabJsonCommand` |
|
||||
| `manual_confirm` | `UniLabJsonCommand` |
|
||||
|
||||
### 10. 查询任务状态
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/mcp/task/<task_uuid>" -H "$AUTH"
|
||||
```
|
||||
|
||||
### 11. 运行工作流单节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/mcp/run/workflow/action" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuid":"<node_uuid>"}'
|
||||
```
|
||||
|
||||
### 12. 获取资源树(物料信息)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
注意 `lab_uuid` 在路径中。返回 `data.nodes[]` 含所有节点(设备 + 物料),每个节点含 `name`、`uuid`、`type`、`parent`。
|
||||
|
||||
### 13. 获取工作流模板详情
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/workflow/template/detail/$workflow_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
> 必须使用 `/lab/workflow/template/detail/{uuid}`,其他路径会返回 404。
|
||||
|
||||
### 14. 按名称查询物料模板
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/template/by-name?lab_uuid=$lab_uuid&name=<template_name>" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `res_template_uuid`,用于 API #15。
|
||||
|
||||
### 15. 创建物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"res_template_uuid":"<uuid>","name":"<名称>","display_name":"<显示名>","parent_uuid":"<父节点uuid>","data":{...}}'
|
||||
```
|
||||
|
||||
### 16. 更新物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X PUT "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"uuid":"<节点uuid>","display_name":"<新名称>","data":{...}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Placeholder Slot 填写规则
|
||||
|
||||
| `placeholder_keys` 值 | 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"` | 注册表中已注册的资源类 |
|
||||
|
||||
### host_node 设备的 Slot 字段表
|
||||
|
||||
| Action | 字段 | Slot 类型 | 说明 |
|
||||
| ----------------- | ----------- | ------------ | ------------------------------ |
|
||||
| `create_resource` | `res_id` | ResourceSlot | 新资源路径(可填不存在的路径) |
|
||||
| `create_resource` | `device_id` | DeviceSlot | 归属设备 |
|
||||
| `create_resource` | `parent` | NodeSlot | 父节点路径 |
|
||||
| `create_resource` | `class_name`| ClassSlot | 资源类名如 `"container"` |
|
||||
| `auto-test_resource` | `resource` | ResourceSlot | 单个测试物料 |
|
||||
| `auto-test_resource` | `resources` | ResourceSlot | 测试物料数组 |
|
||||
| `auto-test_resource` | `device` | DeviceSlot | 测试设备 |
|
||||
| `auto-test_resource` | `devices` | DeviceSlot | 测试设备 |
|
||||
|
||||
---
|
||||
|
||||
## 渐进加载策略
|
||||
|
||||
1. **SKILL.md**(本文件)— API 端点 + session state 管理
|
||||
2. **[action-index.md](action-index.md)** — 按分类浏览 4 个动作的描述和核心参数
|
||||
3. **[actions/\<name\>.json](actions/)** — 仅在需要构建具体请求时,加载对应 action 的完整 JSON Schema
|
||||
|
||||
---
|
||||
|
||||
## 完整工作流 Checklist
|
||||
|
||||
```
|
||||
Task Progress:
|
||||
- [ ] Step 1: GET /edge/lab/info 获取 lab_uuid
|
||||
- [ ] Step 2: 获取资源树 (GET #12) → 记住可用物料
|
||||
- [ ] Step 3: 读 action-index.md 确定要用的 action 名
|
||||
- [ ] Step 4: 创建工作流 (POST #2) → 记住 workflow_uuid,告知用户链接
|
||||
- [ ] Step 5: 创建节点 (POST #3, resource_template_name=host_node) → 记住 node_uuid + data.param
|
||||
- [ ] Step 6: 根据 _unilabos_placeholder_info 和资源树,填写 data.param 中的 Slot 字段
|
||||
- [ ] Step 7: 更新节点参数 (PATCH #5)
|
||||
- [ ] Step 8: 查询节点 handles (POST #6) → 获取各节点的 handle_uuid
|
||||
- [ ] Step 9: 批量创建边 (POST #7) → 用 handle_uuid 连接节点
|
||||
- [ ] Step 10: 启动工作流 (POST #8) 或运行单节点 (POST #11)
|
||||
- [ ] Step 11: 查询任务状态 (GET #10) 确认完成
|
||||
```
|
||||
58
.cursor/skills/host-node/action-index.md
Normal file
58
.cursor/skills/host-node/action-index.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Action Index — host_node
|
||||
|
||||
4 个动作,按功能分类。每个动作的完整 JSON Schema 在 `actions/<name>.json`。
|
||||
|
||||
---
|
||||
|
||||
## 资源管理
|
||||
|
||||
### `create_resource`
|
||||
|
||||
在资源树中创建新资源(容器、物料等),支持指定位置、类型和初始液体
|
||||
|
||||
- **action_type**: `ResourceCreateFromOuterEasy`
|
||||
- **Schema**: [`actions/create_resource.json`](actions/create_resource.json)
|
||||
- **可选参数**: `res_id`, `device_id`, `class_name`, `parent`, `bind_locations`, `liquid_input_slot`, `liquid_type`, `liquid_volume`, `slot_on_deck`
|
||||
- **占位符字段**:
|
||||
- `res_id` — **ResourceSlot**(特例:目标物料可能尚不存在,直接填期望路径)
|
||||
- `device_id` — **DeviceSlot**,填路径字符串如 `"/host_node"`
|
||||
- `parent` — **NodeSlot**,填路径字符串如 `"/workstation/deck"`
|
||||
- `class_name` — **ClassSlot**,填类名如 `"container"`
|
||||
|
||||
### `auto-test_resource`
|
||||
|
||||
测试资源系统,返回当前资源树和设备列表
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/test_resource.json`](actions/test_resource.json)
|
||||
- **可选参数**: `resource`, `resources`, `device`, `devices`
|
||||
- **占位符字段**:
|
||||
- `resource` — **ResourceSlot**,单个物料节点 `{id, name, uuid}`
|
||||
- `resources` — **ResourceSlot**,物料节点数组 `[{id, name, uuid}, ...]`
|
||||
- `device` — **DeviceSlot**,设备路径字符串
|
||||
- `devices` — **DeviceSlot**,设备路径字符串
|
||||
|
||||
---
|
||||
|
||||
## 系统工具
|
||||
|
||||
### `test_latency`
|
||||
|
||||
测试设备通信延迟,返回 RTT、时间差、任务延迟等指标
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/test_latency.json`](actions/test_latency.json)
|
||||
- **参数**: 无(零参数调用)
|
||||
|
||||
---
|
||||
|
||||
## 人工确认
|
||||
|
||||
### `manual_confirm`
|
||||
|
||||
创建人工确认节点,等待用户手动确认后继续
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/manual_confirm.json`](actions/manual_confirm.json)
|
||||
- **核心参数**: `timeout_seconds`(超时时间,秒), `assignee_user_ids`(指派用户 ID 列表)
|
||||
- **占位符字段**: `assignee_user_ids` — `unilabos_manual_confirm` 类型
|
||||
93
.cursor/skills/host-node/actions/create_resource.json
Normal file
93
.cursor/skills/host-node/actions/create_resource.json
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"type": "ResourceCreateFromOuterEasy",
|
||||
"goal": {
|
||||
"res_id": "res_id",
|
||||
"class_name": "class_name",
|
||||
"parent": "parent",
|
||||
"device_id": "device_id",
|
||||
"bind_locations": "bind_locations",
|
||||
"liquid_input_slot": "liquid_input_slot[]",
|
||||
"liquid_type": "liquid_type[]",
|
||||
"liquid_volume": "liquid_volume[]",
|
||||
"slot_on_deck": "slot_on_deck"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"res_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"class_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"bind_locations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "bind_locations",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"liquid_input_slot": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"liquid_type": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"liquid_volume": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"slot_on_deck": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"_unilabos_placeholder_info": {
|
||||
"res_id": "unilabos_resources",
|
||||
"device_id": "unilabos_devices",
|
||||
"parent": "unilabos_nodes",
|
||||
"class_name": "unilabos_class"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"res_id": "unilabos_resources",
|
||||
"device_id": "unilabos_devices",
|
||||
"parent": "unilabos_nodes",
|
||||
"class_name": "unilabos_class"
|
||||
}
|
||||
}
|
||||
32
.cursor/skills/host-node/actions/manual_confirm.json
Normal file
32
.cursor/skills/host-node/actions/manual_confirm.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"timeout_seconds": "timeout_seconds",
|
||||
"assignee_user_ids": "assignee_user_ids"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timeout_seconds": {
|
||||
"type": "integer"
|
||||
},
|
||||
"assignee_user_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"timeout_seconds",
|
||||
"assignee_user_ids"
|
||||
],
|
||||
"_unilabos_placeholder_info": {
|
||||
"assignee_user_ids": "unilabos_manual_confirm"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"assignee_user_ids": "unilabos_manual_confirm"
|
||||
}
|
||||
}
|
||||
11
.cursor/skills/host-node/actions/test_latency.json
Normal file
11
.cursor/skills/host-node/actions/test_latency.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
255
.cursor/skills/host-node/actions/test_resource.json
Normal file
255
.cursor/skills/host-node/actions/test_resource.json
Normal file
@@ -0,0 +1,255 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"resource": "resource",
|
||||
"resources": "resources",
|
||||
"device": "device",
|
||||
"devices": "devices"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "resource"
|
||||
},
|
||||
"resources": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "resources"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"device": {
|
||||
"type": "string",
|
||||
"description": "device reference"
|
||||
},
|
||||
"devices": {
|
||||
"type": "string",
|
||||
"description": "device reference"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"_unilabos_placeholder_info": {
|
||||
"resource": "unilabos_resources",
|
||||
"resources": "unilabos_resources",
|
||||
"device": "unilabos_devices",
|
||||
"devices": "unilabos_devices"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"resource": "unilabos_resources",
|
||||
"resources": "unilabos_resources",
|
||||
"device": "unilabos_devices",
|
||||
"devices": "unilabos_devices"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
---
|
||||
name: submit-agent-result
|
||||
description: Submit historical experiment results (agent_result) to Uni-Lab notebook — read data files, assemble JSON payload, PUT to cloud API. Use when the user wants to submit experiment results, upload agent results, report experiment data, or mentions agent_result/实验结果/历史记录/notebook结果.
|
||||
description: Submit historical experiment results (agent_result) to Uni-Lab cloud platform (leap-lab) notebook — read data files, assemble JSON payload, PUT to cloud API. Use when the user wants to submit experiment results, upload agent results, report experiment data, or mentions agent_result/实验结果/历史记录/notebook结果.
|
||||
---
|
||||
|
||||
# 提交历史实验记录指南
|
||||
# Uni-Lab 提交历史实验记录指南
|
||||
|
||||
通过云端 API 向已创建的 notebook 提交实验结果数据(agent_result)。支持从 JSON / CSV 文件读取数据,整合后提交。
|
||||
通过 Uni-Lab 云端 API 向已创建的 notebook 提交实验结果数据(agent_result)。支持从 JSON / CSV 文件读取数据,整合后提交。
|
||||
|
||||
> **重要**:本指南中的 `Authorization: Lab <token>` 是 **Uni-Lab 平台专用的认证方式**,`Lab` 是 Uni-Lab 的 auth scheme 关键字,**不是** HTTP Basic 认证。请勿将其替换为 `Basic`。
|
||||
|
||||
## 前置条件(缺一不可)
|
||||
|
||||
@@ -18,23 +20,26 @@ description: Submit historical experiment results (agent_result) to Uni-Lab note
|
||||
生成 AUTH token:
|
||||
|
||||
```bash
|
||||
# ⚠️ 注意:scheme 是 "Lab"(Uni-Lab 专用),不是 "Basic"
|
||||
python -c "import base64,sys; print(base64.b64encode(f'{sys.argv[1]}:{sys.argv[2]}'.encode()).decode())" <ak> <sk>
|
||||
```
|
||||
|
||||
输出即为 token 值,拼接为 `Authorization: Lab <token>`。
|
||||
输出即为 token 值,拼接为 `Authorization: Lab <token>`(`Lab` 是 Uni-Lab 平台 auth scheme,不可替换为 `Basic`)。
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
|-------------|------|
|
||||
| `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` |
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------ | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
# ⚠️ Auth scheme 必须是 "Lab"(Uni-Lab 专用),不是 "Basic"
|
||||
AUTH="Authorization: Lab <上面命令输出的 token>"
|
||||
```
|
||||
|
||||
@@ -45,6 +50,7 @@ AUTH="Authorization: Lab <上面命令输出的 token>"
|
||||
notebook_uuid 来自之前通过「批量提交实验」创建的实验批次,即 `POST /api/v1/lab/notebook` 返回的 `data.uuid`。
|
||||
|
||||
如果用户不记得,可提示:
|
||||
|
||||
- 查看之前的对话记录中创建 notebook 时返回的 UUID
|
||||
- 或通过平台页面查找对应的 notebook
|
||||
|
||||
@@ -54,11 +60,11 @@ notebook_uuid 来自之前通过「批量提交实验」创建的实验批次,
|
||||
|
||||
用户需要提供实验结果数据,支持以下方式:
|
||||
|
||||
| 方式 | 说明 |
|
||||
|------|------|
|
||||
| JSON 文件 | 直接作为 `agent_result` 的内容合并 |
|
||||
| CSV 文件 | 转为 `{"文件名": [行数据...]}` 格式 |
|
||||
| 手动指定 | 用户直接告知 key-value 数据,由 agent 构建 JSON |
|
||||
| 方式 | 说明 |
|
||||
| --------- | ----------------------------------------------- |
|
||||
| JSON 文件 | 直接作为 `agent_result` 的内容合并 |
|
||||
| CSV 文件 | 转为 `{"文件名": [行数据...]}` 格式 |
|
||||
| 手动指定 | 用户直接告知 key-value 数据,由 agent 构建 JSON |
|
||||
|
||||
**四项全部就绪后才可开始。**
|
||||
|
||||
@@ -90,7 +96,7 @@ curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
返回:
|
||||
|
||||
```json
|
||||
{"code": 0, "data": {"uuid": "xxx", "name": "实验室名称"}}
|
||||
{ "code": 0, "data": { "uuid": "xxx", "name": "实验室名称" } }
|
||||
```
|
||||
|
||||
记住 `data.uuid` 为 `lab_uuid`。
|
||||
@@ -121,42 +127,45 @@ curl -s -X PUT "$BASE/api/v1/lab/notebook/agent-result" \
|
||||
|
||||
#### 必要字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------------- | ------------- | ------------------------------------------- |
|
||||
| `notebook_uuid` | string (UUID) | 目标 notebook 的 UUID,从批量提交实验时获取 |
|
||||
| `agent_result` | object | 实验结果数据,任意 JSON 对象 |
|
||||
| `agent_result` | object | 实验结果数据,任意 JSON 对象 |
|
||||
|
||||
#### agent_result 内容格式
|
||||
|
||||
`agent_result` 接受**任意 JSON 对象**,常见格式:
|
||||
|
||||
**简单键值对**:
|
||||
|
||||
```json
|
||||
{
|
||||
"avg_rtt_ms": 12.5,
|
||||
"status": "success",
|
||||
"test_count": 5
|
||||
"avg_rtt_ms": 12.5,
|
||||
"status": "success",
|
||||
"test_count": 5
|
||||
}
|
||||
```
|
||||
|
||||
**包含嵌套结构**:
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": {"total": 100, "passed": 98, "failed": 2},
|
||||
"measurements": [
|
||||
{"sample_id": "S001", "value": 3.14, "unit": "mg/mL"},
|
||||
{"sample_id": "S002", "value": 2.71, "unit": "mg/mL"}
|
||||
]
|
||||
"summary": { "total": 100, "passed": 98, "failed": 2 },
|
||||
"measurements": [
|
||||
{ "sample_id": "S001", "value": 3.14, "unit": "mg/mL" },
|
||||
{ "sample_id": "S002", "value": 2.71, "unit": "mg/mL" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**从 CSV 文件导入**(脚本自动转换):
|
||||
|
||||
```json
|
||||
{
|
||||
"experiment_data": [
|
||||
{"温度": 25, "压力": 101.3, "产率": 0.85},
|
||||
{"温度": 30, "压力": 101.3, "产率": 0.91}
|
||||
]
|
||||
"experiment_data": [
|
||||
{ "温度": 25, "压力": 101.3, "产率": 0.85 },
|
||||
{ "温度": 30, "压力": 101.3, "产率": 0.91 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -178,22 +187,22 @@ python scripts/prepare_agent_result.py \
|
||||
[--output <output.json>]
|
||||
```
|
||||
|
||||
| 参数 | 必选 | 说明 |
|
||||
|------|------|------|
|
||||
| `--notebook-uuid` | 是 | 目标 notebook UUID |
|
||||
| `--files` | 是 | 输入文件路径(支持多个,JSON / CSV) |
|
||||
| `--auth` | 提交时必选 | Lab token(base64(ak:sk)) |
|
||||
| `--base` | 提交时必选 | API base URL |
|
||||
| `--submit` | 否 | 加上此标志则直接提交到云端 |
|
||||
| `--output` | 否 | 输出 JSON 路径(默认 `agent_result_body.json`) |
|
||||
| 参数 | 必选 | 说明 |
|
||||
| ----------------- | ---------- | ----------------------------------------------- |
|
||||
| `--notebook-uuid` | 是 | 目标 notebook UUID |
|
||||
| `--files` | 是 | 输入文件路径(支持多个,JSON / CSV) |
|
||||
| `--auth` | 提交时必选 | Lab token(base64(ak:sk)) |
|
||||
| `--base` | 提交时必选 | API base URL |
|
||||
| `--submit` | 否 | 加上此标志则直接提交到云端 |
|
||||
| `--output` | 否 | 输出 JSON 路径(默认 `agent_result_body.json`) |
|
||||
|
||||
### 文件合并规则
|
||||
|
||||
| 文件类型 | 合并方式 |
|
||||
|----------|----------|
|
||||
| `.json`(dict) | 字段直接合并到 `agent_result` 顶层 |
|
||||
| `.json`(list/other) | 以文件名为 key 放入 `agent_result` |
|
||||
| `.csv` | 以文件名(不含扩展名)为 key,值为行对象数组 |
|
||||
| 文件类型 | 合并方式 |
|
||||
| --------------------- | -------------------------------------------- |
|
||||
| `.json`(dict) | 字段直接合并到 `agent_result` 顶层 |
|
||||
| `.json`(list/other) | 以文件名为 key 放入 `agent_result` |
|
||||
| `.csv` | 以文件名(不含扩展名)为 key,值为行对象数组 |
|
||||
|
||||
多个文件的字段会合并。JSON dict 中的重复 key 后者覆盖前者。
|
||||
|
||||
@@ -210,7 +219,7 @@ python scripts/prepare_agent_result.py \
|
||||
--notebook-uuid 73c67dca-c8cc-4936-85a0-329106aa7cca \
|
||||
--files results.json \
|
||||
--auth YTFmZDlkNGUt... \
|
||||
--base https://uni-lab.test.bohrium.com \
|
||||
--base https://leap-lab.test.bohrium.com \
|
||||
--submit
|
||||
```
|
||||
|
||||
@@ -272,4 +281,4 @@ Task Progress:
|
||||
|
||||
### Q: 认证方式是 Lab 还是 Api?
|
||||
|
||||
本指南统一使用 `Authorization: Lab <base64(ak:sk)>` 方式。如果用户有独立的 API Key,也可用 `Authorization: Api <key>` 替代。
|
||||
本指南统一使用 `Authorization: Lab <base64(ak:sk)>` 方式(`Lab` 是 Uni-Lab 平台的 auth scheme,**绝不能用 `Basic` 替代**)。如果用户有独立的 API Key,也可用 `Authorization: Api <key>` 替代。
|
||||
|
||||
272
.cursor/skills/virtual-workbench/SKILL.md
Normal file
272
.cursor/skills/virtual-workbench/SKILL.md
Normal file
@@ -0,0 +1,272 @@
|
||||
---
|
||||
name: virtual-workbench
|
||||
description: Operate Virtual Workbench via REST API — prepare materials, move to heating stations, start heating, move to output, transfer resources. Use when the user mentions virtual workbench, virtual_workbench, 虚拟工作台, heating stations, material processing, or workbench operations.
|
||||
---
|
||||
|
||||
# Virtual Workbench API Skill
|
||||
|
||||
## 设备信息
|
||||
|
||||
- **device_id**: `virtual_workbench`
|
||||
- **Python 源码**: `unilabos/devices/virtual/workbench.py`
|
||||
- **设备类**: `VirtualWorkbench`
|
||||
- **动作数**: 6(`auto-prepare_materials`, `auto-move_to_heating_station`, `auto-start_heating`, `auto-move_to_output`, `transfer`, `manual_confirm`)
|
||||
- **设备描述**: 模拟工作台,包含 1 个机械臂(每次操作 2s,独占锁)和 3 个加热台(每次加热 60s,可并行)
|
||||
|
||||
### 典型工作流程
|
||||
|
||||
1. `prepare_materials` — 生成 A1-A5 物料(5 个 output handle)
|
||||
2. `move_to_heating_station` — 物料并发竞争机械臂,移动到空闲加热台
|
||||
3. `start_heating` — 启动加热(3 个加热台可并行)
|
||||
4. `move_to_output` — 加热完成后移到输出位置 Cn
|
||||
|
||||
## 前置条件(缺一不可)
|
||||
|
||||
使用本 skill 前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。
|
||||
|
||||
### 1. ak / sk → AUTH
|
||||
|
||||
从启动参数 `--ak` `--sk` 或 config.py 中获取,生成 token:`base64(ak:sk)` → `Authorization: Lab <token>`
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------ | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
AUTH="Authorization: Lab <token>"
|
||||
```
|
||||
|
||||
**两项全部就绪后才可发起 API 请求。**
|
||||
|
||||
## Session State
|
||||
|
||||
- `lab_uuid` — 实验室 UUID(首次通过 API #1 自动获取,**不需要问用户**)
|
||||
- `device_name` — `virtual_workbench`
|
||||
|
||||
## 请求约定
|
||||
|
||||
所有请求使用 `curl -s`,POST/PATCH/DELETE 需加 `Content-Type: application/json`。
|
||||
|
||||
> **Windows 平台**必须使用 `curl.exe`(而非 PowerShell 的 `curl` 别名)。
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. 获取实验室信息(自动获取 lab_uuid)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `lab_uuid`,`data.name` 为 `lab_name`。
|
||||
|
||||
### 2. 创建工作流
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/owner" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"name":"<名称>","lab_uuid":"<lab_uuid>","description":"<描述>"}'
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `workflow_uuid`。创建成功后告知用户链接:`$BASE/laboratory/$lab_uuid/workflow/$workflow_uuid`
|
||||
|
||||
### 3. 创建节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/workflow/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"workflow_uuid":"<workflow_uuid>","resource_template_name":"virtual_workbench","node_template_name":"<action_name>"}'
|
||||
```
|
||||
|
||||
- `resource_template_name` 固定为 `virtual_workbench`
|
||||
- `node_template_name` — action 名称(如 `auto-prepare_materials`, `auto-move_to_heating_station`)
|
||||
|
||||
### 4. 删除节点
|
||||
|
||||
```bash
|
||||
curl -s -X DELETE "$BASE/api/v1/lab/workflow/nodes" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuids":["<uuid1>"],"workflow_uuid":"<workflow_uuid>"}'
|
||||
```
|
||||
|
||||
### 5. 更新节点参数
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "$BASE/api/v1/lab/workflow/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"workflow_uuid":"<wf_uuid>","uuid":"<node_uuid>","param":{...}}'
|
||||
```
|
||||
|
||||
参考 [action-index.md](action-index.md) 确定哪些字段是 Slot。
|
||||
|
||||
### 6. 查询节点 handles
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/node-handles" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuids":["<node_uuid_1>","<node_uuid_2>"]}'
|
||||
```
|
||||
|
||||
### 7. 批量创建边
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/edges" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"edges":[{"source_node_uuid":"<uuid>","target_node_uuid":"<uuid>","source_handle_uuid":"<uuid>","target_handle_uuid":"<uuid>"}]}'
|
||||
```
|
||||
|
||||
### 8. 启动工作流
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/<workflow_uuid>/run" -H "$AUTH"
|
||||
```
|
||||
|
||||
### 9. 运行设备单动作
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/mcp/run/action" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"lab_uuid":"<lab_uuid>","device_id":"virtual_workbench","action":"<action_name>","action_type":"<type>","param":{...}}'
|
||||
```
|
||||
|
||||
`param` 直接放 goal 里的属性,**不要**再包一层 `{"goal": {...}}`。
|
||||
|
||||
> **WARNING: `action_type` 必须正确,传错会导致任务永远卡住无法完成。** 从下表或 `actions/<name>.json` 的 `type` 字段获取。
|
||||
|
||||
#### action_type 速查表
|
||||
|
||||
| action | action_type |
|
||||
|--------|-------------|
|
||||
| `auto-prepare_materials` | `UniLabJsonCommand` |
|
||||
| `auto-move_to_heating_station` | `UniLabJsonCommand` |
|
||||
| `auto-start_heating` | `UniLabJsonCommand` |
|
||||
| `auto-move_to_output` | `UniLabJsonCommand` |
|
||||
| `transfer` | `UniLabJsonCommandAsync` |
|
||||
| `manual_confirm` | `UniLabJsonCommand` |
|
||||
|
||||
### 10. 查询任务状态
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/mcp/task/<task_uuid>" -H "$AUTH"
|
||||
```
|
||||
|
||||
### 11. 运行工作流单节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/mcp/run/workflow/action" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuid":"<node_uuid>"}'
|
||||
```
|
||||
|
||||
### 12. 获取资源树(物料信息)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
注意 `lab_uuid` 在路径中。返回 `data.nodes[]` 含所有节点(设备 + 物料),每个节点含 `name`、`uuid`、`type`、`parent`。
|
||||
|
||||
### 13. 获取工作流模板详情
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/workflow/template/detail/$workflow_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
> 必须使用 `/lab/workflow/template/detail/{uuid}`,其他路径会返回 404。
|
||||
|
||||
### 14. 按名称查询物料模板
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/template/by-name?lab_uuid=$lab_uuid&name=<template_name>" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `res_template_uuid`,用于 API #15。
|
||||
|
||||
### 15. 创建物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"res_template_uuid":"<uuid>","name":"<名称>","display_name":"<显示名>","parent_uuid":"<父节点uuid>","data":{...}}'
|
||||
```
|
||||
|
||||
### 16. 更新物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X PUT "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"uuid":"<节点uuid>","display_name":"<新名称>","data":{...}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Placeholder Slot 填写规则
|
||||
|
||||
| `placeholder_keys` 值 | 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"` | 注册表中已注册的资源类 |
|
||||
|
||||
### virtual_workbench 设备的 Slot 字段表
|
||||
|
||||
| Action | 字段 | Slot 类型 | 说明 |
|
||||
| ----------------- | ---------------- | ------------ | -------------------- |
|
||||
| `transfer` | `resource` | ResourceSlot | 待转移物料数组 |
|
||||
| `transfer` | `target_device` | DeviceSlot | 目标设备路径 |
|
||||
| `transfer` | `mount_resource` | ResourceSlot | 目标孔位数组 |
|
||||
| `manual_confirm` | `resource` | ResourceSlot | 确认用物料数组 |
|
||||
| `manual_confirm` | `target_device` | DeviceSlot | 确认用目标设备 |
|
||||
| `manual_confirm` | `mount_resource` | ResourceSlot | 确认用目标孔位数组 |
|
||||
|
||||
> `prepare_materials`、`move_to_heating_station`、`start_heating`、`move_to_output` 这 4 个动作**无 Slot 字段**,参数为纯数值/整数。
|
||||
|
||||
---
|
||||
|
||||
## 渐进加载策略
|
||||
|
||||
1. **SKILL.md**(本文件)— API 端点 + session state 管理 + 设备工作流概览
|
||||
2. **[action-index.md](action-index.md)** — 按分类浏览 6 个动作的描述和核心参数
|
||||
3. **[actions/\<name\>.json](actions/)** — 仅在需要构建具体请求时,加载对应 action 的完整 JSON Schema
|
||||
|
||||
---
|
||||
|
||||
## 完整工作流 Checklist
|
||||
|
||||
```
|
||||
Task Progress:
|
||||
- [ ] Step 1: GET /edge/lab/info 获取 lab_uuid
|
||||
- [ ] Step 2: 获取资源树 (GET #12) → 记住可用物料
|
||||
- [ ] Step 3: 读 action-index.md 确定要用的 action 名
|
||||
- [ ] Step 4: 创建工作流 (POST #2) → 记住 workflow_uuid,告知用户链接
|
||||
- [ ] Step 5: 创建节点 (POST #3, resource_template_name=virtual_workbench) → 记住 node_uuid + data.param
|
||||
- [ ] Step 6: 根据 _unilabos_placeholder_info 和资源树,填写 data.param 中的 Slot 字段
|
||||
- [ ] Step 7: 更新节点参数 (PATCH #5)
|
||||
- [ ] Step 8: 查询节点 handles (POST #6) → 获取各节点的 handle_uuid
|
||||
- [ ] Step 9: 批量创建边 (POST #7) → 用 handle_uuid 连接节点
|
||||
- [ ] Step 10: 启动工作流 (POST #8) 或运行单节点 (POST #11)
|
||||
- [ ] Step 11: 查询任务状态 (GET #10) 确认完成
|
||||
```
|
||||
|
||||
### 典型 5 物料并发加热工作流示例
|
||||
|
||||
```
|
||||
prepare_materials (count=5)
|
||||
├─ channel_1 → move_to_heating_station (material_number=1) → start_heating → move_to_output
|
||||
├─ channel_2 → move_to_heating_station (material_number=2) → start_heating → move_to_output
|
||||
├─ channel_3 → move_to_heating_station (material_number=3) → start_heating → move_to_output
|
||||
├─ channel_4 → move_to_heating_station (material_number=4) → start_heating → move_to_output
|
||||
└─ channel_5 → move_to_heating_station (material_number=5) → start_heating → move_to_output
|
||||
```
|
||||
|
||||
创建节点时,`prepare_materials` 的 5 个 output handle(`channel_1` ~ `channel_5`)分别连接到 5 个 `move_to_heating_station` 节点的 `material_input` handle。每个 `move_to_heating_station` 的 `heating_station_output` 和 `material_number_output` 连接到对应 `start_heating` 的 `station_id_input` 和 `material_number_input`。
|
||||
76
.cursor/skills/virtual-workbench/action-index.md
Normal file
76
.cursor/skills/virtual-workbench/action-index.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Action Index — virtual_workbench
|
||||
|
||||
6 个动作,按功能分类。每个动作的完整 JSON Schema 在 `actions/<name>.json`。
|
||||
|
||||
---
|
||||
|
||||
## 物料准备
|
||||
|
||||
### `auto-prepare_materials`
|
||||
|
||||
批量准备物料(虚拟起始节点),生成 A1-A5 物料编号,输出 5 个 handle 供后续节点使用
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/prepare_materials.json`](actions/prepare_materials.json)
|
||||
- **可选参数**: `count`(物料数量,默认 5)
|
||||
|
||||
---
|
||||
|
||||
## 机械臂 & 加热台操作
|
||||
|
||||
### `auto-move_to_heating_station`
|
||||
|
||||
将物料从 An 位置移动到空闲加热台(竞争机械臂,自动查找空闲加热台)
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/move_to_heating_station.json`](actions/move_to_heating_station.json)
|
||||
- **核心参数**: `material_number`(物料编号,integer)
|
||||
|
||||
### `auto-start_heating`
|
||||
|
||||
启动指定加热台的加热程序(可并行,3 个加热台同时工作)
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/start_heating.json`](actions/start_heating.json)
|
||||
- **核心参数**: `station_id`(加热台 ID),`material_number`(物料编号)
|
||||
|
||||
### `auto-move_to_output`
|
||||
|
||||
将加热完成的物料从加热台移动到输出位置 Cn
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/move_to_output.json`](actions/move_to_output.json)
|
||||
- **核心参数**: `station_id`(加热台 ID),`material_number`(物料编号)
|
||||
|
||||
---
|
||||
|
||||
## 物料转移
|
||||
|
||||
### `transfer`
|
||||
|
||||
异步转移物料到目标设备(通过 ROS 资源转移)
|
||||
|
||||
- **action_type**: `UniLabJsonCommandAsync`
|
||||
- **Schema**: [`actions/transfer.json`](actions/transfer.json)
|
||||
- **核心参数**: `resource`, `target_device`, `mount_resource`
|
||||
- **占位符字段**:
|
||||
- `resource` — **ResourceSlot**,待转移的物料数组 `[{id, name, uuid}, ...]`
|
||||
- `target_device` — **DeviceSlot**,目标设备路径字符串
|
||||
- `mount_resource` — **ResourceSlot**,目标孔位数组 `[{id, name, uuid}, ...]`
|
||||
|
||||
---
|
||||
|
||||
## 人工确认
|
||||
|
||||
### `manual_confirm`
|
||||
|
||||
创建人工确认节点,等待用户手动确认后继续(含物料转移上下文)
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/manual_confirm.json`](actions/manual_confirm.json)
|
||||
- **核心参数**: `resource`, `target_device`, `mount_resource`, `timeout_seconds`, `assignee_user_ids`
|
||||
- **占位符字段**:
|
||||
- `resource` — **ResourceSlot**,物料数组
|
||||
- `target_device` — **DeviceSlot**,目标设备路径
|
||||
- `mount_resource` — **ResourceSlot**,目标孔位数组
|
||||
- `assignee_user_ids` — `unilabos_manual_confirm` 类型
|
||||
270
.cursor/skills/virtual-workbench/actions/manual_confirm.json
Normal file
270
.cursor/skills/virtual-workbench/actions/manual_confirm.json
Normal file
@@ -0,0 +1,270 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"resource": "resource",
|
||||
"target_device": "target_device",
|
||||
"mount_resource": "mount_resource",
|
||||
"timeout_seconds": "timeout_seconds",
|
||||
"assignee_user_ids": "assignee_user_ids"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "resource"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"target_device": {
|
||||
"type": "string",
|
||||
"description": "device reference"
|
||||
},
|
||||
"mount_resource": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "mount_resource"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeout_seconds": {
|
||||
"type": "integer"
|
||||
},
|
||||
"assignee_user_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"resource",
|
||||
"target_device",
|
||||
"mount_resource",
|
||||
"timeout_seconds",
|
||||
"assignee_user_ids"
|
||||
],
|
||||
"_unilabos_placeholder_info": {
|
||||
"resource": "unilabos_resources",
|
||||
"target_device": "unilabos_devices",
|
||||
"mount_resource": "unilabos_resources",
|
||||
"assignee_user_ids": "unilabos_manual_confirm"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"resource": "unilabos_resources",
|
||||
"target_device": "unilabos_devices",
|
||||
"mount_resource": "unilabos_resources",
|
||||
"assignee_user_ids": "unilabos_manual_confirm"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"material_number": "material_number"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"material_number": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"material_number"
|
||||
]
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
24
.cursor/skills/virtual-workbench/actions/move_to_output.json
Normal file
24
.cursor/skills/virtual-workbench/actions/move_to_output.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"station_id": "station_id",
|
||||
"material_number": "material_number"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"station_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"material_number": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"station_id",
|
||||
"material_number"
|
||||
]
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"count": "count"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"goal_default": {
|
||||
"count": 5
|
||||
},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
24
.cursor/skills/virtual-workbench/actions/start_heating.json
Normal file
24
.cursor/skills/virtual-workbench/actions/start_heating.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"station_id": "station_id",
|
||||
"material_number": "material_number"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"station_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"material_number": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"station_id",
|
||||
"material_number"
|
||||
]
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
255
.cursor/skills/virtual-workbench/actions/transfer.json
Normal file
255
.cursor/skills/virtual-workbench/actions/transfer.json
Normal file
@@ -0,0 +1,255 @@
|
||||
{
|
||||
"type": "UniLabJsonCommandAsync",
|
||||
"goal": {
|
||||
"resource": "resource",
|
||||
"target_device": "target_device",
|
||||
"mount_resource": "mount_resource"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "resource"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"target_device": {
|
||||
"type": "string",
|
||||
"description": "device reference"
|
||||
},
|
||||
"mount_resource": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "mount_resource"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"resource",
|
||||
"target_device",
|
||||
"mount_resource"
|
||||
],
|
||||
"_unilabos_placeholder_info": {
|
||||
"resource": "unilabos_resources",
|
||||
"target_device": "unilabos_devices",
|
||||
"mount_resource": "unilabos_resources"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"resource": "unilabos_resources",
|
||||
"target_device": "unilabos_devices",
|
||||
"mount_resource": "unilabos_resources"
|
||||
}
|
||||
}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -251,4 +251,7 @@ ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2
|
||||
*.bz2
|
||||
test_config.py
|
||||
|
||||
|
||||
# Local config files with secrets
|
||||
yibin_coin_cell_only_config.json
|
||||
yibin_electrolyte_config.json
|
||||
yibin_electrolyte_only_config.json
|
||||
|
||||
72
260415csv_export_walkthrough.md
Normal file
72
260415csv_export_walkthrough.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# CSV 导出功能变更概要
|
||||
|
||||
## 修改的文件
|
||||
|
||||
### 1. [bioyond_cell_workstation.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py)
|
||||
|
||||
#### 新增导入
|
||||
- `import csv` 和 `import os`(L14-15)
|
||||
|
||||
#### 新增方法
|
||||
|
||||
| 方法 | 功能 |
|
||||
|------|------|
|
||||
| `_extract_prep_bottle_from_report` | 从 order_finish 报文提取**配液瓶**信息(每订单最多1个) |
|
||||
| `_extract_vial_bottles_from_report` | 从 order_finish 报文提取**分液瓶**信息(每订单可多个,返回数组) |
|
||||
| `_export_order_csv` | 汇总所有信息写入 CSV 文件 |
|
||||
|
||||
#### 配液瓶筛选逻辑 (`_extract_prep_bottle_from_report`)
|
||||
- `typemode="1"`, `realQuantity=1`, `usedQuantity=1`
|
||||
- `locationId` 以 `3a19deae-2c7a-` 开头(手动传递窗)
|
||||
- LIMS API 二次确认:`typeName` 含"配液瓶(小)"或"配液瓶(大)"
|
||||
|
||||
#### 分液瓶筛选逻辑 (`_extract_vial_bottles_from_report`)
|
||||
- `typemode="1"`, `realQuantity=1`, `usedQuantity=1`
|
||||
- `locationId` 以 `3a19debc-84b5-` 或 `3a19debe-5200` 开头(自动堆栈-左/右)
|
||||
- LIMS API 二次确认:`typeName` 为"5ml分液瓶"或"20ml分液瓶"
|
||||
- **返回数组**,支持 1×5ml + n×20ml 的组合
|
||||
|
||||
#### 修改的方法
|
||||
|
||||
| 方法 | 变更 |
|
||||
|------|------|
|
||||
| `_submit_and_wait_orders` | 新增配液瓶+分液瓶提取步骤,将 `prep_bottles` 和 `vial_bottles` 存入 `final_result` |
|
||||
| `create_orders` | 添加 `csv_export_path` 参数,末尾调用 `_export_order_csv` |
|
||||
| `create_orders_formulation` | 添加 `csv_export_path` 参数,末尾调用 `_export_order_csv` |
|
||||
|
||||
#### CSV 输出格式
|
||||
```
|
||||
orderCode, orderName, 配液瓶类型, 配液瓶二维码, 分液瓶类型, 分液瓶二维码, 目标配液质量比, 真实配液质量比, 时间
|
||||
```
|
||||
- 单个分液瓶时直接写值;多个分液瓶时类型和二维码用 JSON 数组表示
|
||||
- CSV 编码使用 `utf-8-sig`(兼容 Excel 打开)
|
||||
- `csv_export_path` 默认为空字符串,不传则不导出(向后兼容)
|
||||
|
||||
---
|
||||
|
||||
### 2. [bioyond_cell.yaml](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/registry/devices/bioyond_cell.yaml)
|
||||
|
||||
为两个 action 注册了 `csv_export_path` 参数:
|
||||
|
||||
- `auto-create_orders`: `goal_default` + `schema.properties.goal.properties` 中添加 `csv_export_path`
|
||||
- `auto-create_orders_formulation`: 同上
|
||||
|
||||
---
|
||||
|
||||
### 3. [coin_cell_assembly.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py) 的 CSV 改动与全流程追溯
|
||||
|
||||
在 `bioyond_cell_workstation.py` 的 `_submit_and_wait_orders` 最后阶段,提取 `prep_bottles`(配液瓶)和 `vial_bottles`(分液瓶)的条码并随 `mass_ratios` 数组一起下发给各下游工站(例如扣电组装站),实现跨站的全流程配方追溯。
|
||||
|
||||
并在扣电站生成的 `date_xxx.csv` 中,**替换并新增**了以下列:
|
||||
- 移除了原有的 `formulation_order_code` 与合并的 `formulation_ratio` 列。
|
||||
- 新增 `orderName` 导出
|
||||
- 新增 `prep_bottle_barcode`(奔曜传递的配液瓶二维码)
|
||||
- 新增 `vial_bottle_barcodes`(奔曜传递的分液瓶二维码,多瓶时存 JSON 数组)
|
||||
- 新增 `target_mass_ratio` 理论目标质量比
|
||||
- 新增 `real_mass_ratio` 实际称量真实质量比
|
||||
|
||||
*注意:这与操作人员在手套箱内扫码传入扣电站的 `electrolyte_code` 是单独记录的,方便做数据核对。*
|
||||
|
||||
## 向后兼容性
|
||||
- `csv_export_path` 默认值为 `""`(空字符串),现有调用不受影响
|
||||
- 新增的 `prep_bottles` 和 `vial_bottles` 字段为 `final_result` 和 `mass_ratios` 内部的新增附属字段,不破坏现有数据结构。
|
||||
168
CHANGES_2026_03_24.md
Normal file
168
CHANGES_2026_03_24.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# 变更说明 2026-03-24
|
||||
|
||||
## 问题背景
|
||||
|
||||
`BioyondElectrolyteDeck`(原 `BIOYOND_YB_Deck`)迁移后,前端物料未能正常上传/同步。
|
||||
|
||||
---
|
||||
|
||||
## 修复内容
|
||||
|
||||
### 1. `unilabos/resources/bioyond/decks.py`
|
||||
|
||||
- 补回 `setup: bool = False` 参数及 `if setup: self.setup()` 逻辑,与旧版 `BIOYOND_YB_Deck` 保持一致
|
||||
- 工厂函数 `bioyond_electrolyte_deck` 保留显式调用 `deck.setup()`,避免重复初始化
|
||||
|
||||
```python
|
||||
# 修复前(缺少 setup 参数,无法通过 setup=True 触发初始化)
|
||||
def __init__(self, name, size_x, size_y, size_z, category):
|
||||
super().__init__(...)
|
||||
|
||||
# 修复后
|
||||
def __init__(self, name, size_x, size_y, size_z, category, setup: bool = False):
|
||||
super().__init__(...)
|
||||
if setup:
|
||||
self.setup()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `unilabos/resources/graphio.py`
|
||||
|
||||
- 修复 `resource_bioyond_to_plr` 中两处 `bottle.tracker.liquids` 直接赋值导致的崩溃
|
||||
- `ResourceHolder`(如枪头盒的 TipSpot 槽位)没有 `tracker` 属性,直接访问会抛出 `AttributeError`,阻断整个 Bioyond 同步流程
|
||||
|
||||
```python
|
||||
# 修复前
|
||||
bottle.tracker.liquids = [...]
|
||||
|
||||
# 修复后
|
||||
if hasattr(bottle, 'tracker') and bottle.tracker is not None:
|
||||
bottle.tracker.liquids = [...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. `unilabos/app/main.py`
|
||||
|
||||
- 保留 `file_path is not None` 条件不变(已还原),并补充注释说明原因
|
||||
- 该逻辑只在**本地文件模式**下有意义:本地 graph 文件只含设备结构,远端有已保存物料,merge 才能将两者合并
|
||||
- 远端模式(`file_path=None`)下,`resource_tree_set` 和 `request_startup_json` 来自同一份数据,merge 为空操作,条件是否加 `file_path is not None` 对结果没有影响
|
||||
|
||||
---
|
||||
|
||||
### 4. `unilabos/devices/workstation/bioyond_studio/station.py` ⭐ 核心修复
|
||||
|
||||
- 当 deck 通过反序列化创建时,不会自动调用 `setup()`,导致 `deck.children` 为空,`warehouses` 始终是 `{}`
|
||||
- 增加兜底逻辑:仓库扫描后仍为空,则主动调用 `deck.setup()` 初始化仓库
|
||||
- 这是导致所有物料放置失败(`warehouse '...' 在deck中不存在。可用warehouses: []`)的根本原因
|
||||
|
||||
```python
|
||||
# 新增兜底
|
||||
if not self.deck.warehouses and hasattr(self.deck, "setup") and callable(self.deck.setup):
|
||||
logger.info("Deck 无仓库子节点,调用 setup() 初始化仓库")
|
||||
self.deck.setup()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 补充修复 2026-03-25:依华扣电组装工站子物料未上传
|
||||
|
||||
### 问题
|
||||
|
||||
`CoinCellAssemblyWorkstation.post_init` 直接上传空 deck,未调用 `deck.setup()`,导致:
|
||||
- 前端子物料(成品弹夹、料盘、瓶架等)不显示
|
||||
- 运行时 `self.deck.get_resource("成品弹夹")` 抛出 `ResourceNotFoundError`
|
||||
|
||||
### 修复文件
|
||||
|
||||
**`unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py`**
|
||||
- `YihuaCoinCellDeck.__init__` 补回 `setup: bool = False` 参数及 `if setup: self.setup()` 逻辑
|
||||
|
||||
**`unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py`**
|
||||
- `post_init` 中增加与 Bioyond 工站相同的兜底逻辑:deck 无子节点时调用 `deck.setup()` 初始化
|
||||
|
||||
```python
|
||||
# post_init 中新增
|
||||
if self.deck and not self.deck.children and hasattr(self.deck, "setup") and callable(self.deck.setup):
|
||||
logger.info("YihuaCoinCellDeck 无子节点,调用 setup() 初始化")
|
||||
self.deck.setup()
|
||||
```
|
||||
|
||||
### 联动 Bug:`MaterialPlate.create_with_holes` 构造顺序错误
|
||||
|
||||
**现象**:`deck.setup()` 被调用后,启动时抛出:
|
||||
```
|
||||
设备后初始化失败: Must specify either `ordered_items` or `ordering`.
|
||||
```
|
||||
|
||||
**根因**:`create_with_holes` 原来的逻辑是先构造空的 `MaterialPlate` 实例,再 assign 洞位:
|
||||
```python
|
||||
# 旧(错误):cls(...) 时 ordered_items=None → ItemizedResource.__init__ 立即报错
|
||||
plate = cls(name=name, ...) # ← 这里就崩了
|
||||
holes = create_ordered_items_2d(...) # ← 根本没走到这里
|
||||
for hole_name, hole in holes.items():
|
||||
plate.assign_child_resource(...)
|
||||
```
|
||||
pylabrobot 的 `ItemizedResource.__init__` 强制要求 `ordered_items` 和 `ordering` 必须有一个不为 `None`,空构造直接失败。
|
||||
|
||||
**修复**:先建洞位,再作为 `ordered_items` 传给构造函数:
|
||||
```python
|
||||
# 新(正确):先建洞位,再一次性传入构造函数
|
||||
holes = create_ordered_items_2d(klass=MaterialHole, num_items_x=4, ...)
|
||||
return cls(name=name, ..., ordered_items=holes)
|
||||
```
|
||||
|
||||
> 此 bug 此前未被触发,是因为 `deck.setup()` 从未被调用到——正是上面 `post_init` 兜底修复引出的联动问题。
|
||||
|
||||
---
|
||||
|
||||
## 补充修复 2026-03-25:3→2→1 转运资源同步失败
|
||||
|
||||
### 问题
|
||||
|
||||
配液工站(Bioyond)完成分液后,调用 `transfer_3_to_2_to_1_auto` 将分液瓶板转运到扣电工站(BatteryStation)。物理 LIMS 转运成功,但数字孪生资源树同步始终失败:
|
||||
```
|
||||
[资源同步] ❌ 失败: 目标设备 'BatteryStation' 中未找到资源 'bottle_rack_6x2'
|
||||
```
|
||||
|
||||
### 根因
|
||||
|
||||
`_get_resource_from_device` 方法负责跨设备查找资源对象,有两个问题:
|
||||
|
||||
1. **原始路径完全失效**:尝试 `from unilabos.app.ros2_app import get_device_plr_resource_by_name`,但该模块不存在,`ImportError` 被 `except Exception: pass` 静默吞掉
|
||||
2. **降级路径搜错地方**:遍历 `self._plr_resources`(Bioyond 自己的资源),不可能找到 BatteryStation 的 `bottle_rack_6x2`
|
||||
|
||||
### 修复文件
|
||||
|
||||
**`unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`**
|
||||
|
||||
改用全局设备注册表 `registered_devices` 跨设备访问目标 deck:
|
||||
|
||||
```python
|
||||
# 修复前(失效)
|
||||
from unilabos.app.ros2_app import get_device_plr_resource_by_name # 模块不存在
|
||||
return get_device_plr_resource_by_name(device_id, resource_name)
|
||||
|
||||
# 修复后
|
||||
from unilabos.ros.nodes.base_device_node import registered_devices
|
||||
device_info = registered_devices.get(device_id)
|
||||
if device_info is not None:
|
||||
driver = device_info.get("driver_instance") # TypedDict 是 dict,必须用 .get()
|
||||
if driver is not None:
|
||||
deck = getattr(driver, "deck", None)
|
||||
if deck is not None:
|
||||
res = deck.get_resource(resource_name)
|
||||
```
|
||||
|
||||
关键细节:`DeviceInfoType` 是 `TypedDict`(即普通 `dict`),必须用 `device_info.get("driver_instance")` 而非 `getattr(device_info, "driver_instance", None)`——后者对字典永远返回 `None`。
|
||||
|
||||
---
|
||||
|
||||
## 根本原因分析
|
||||
|
||||
旧版以**本地文件模式**启动(有 `graph` 文件),deck 在启动前已通过 `merge_remote_resources` 获得仓库子节点,反序列化时能正确恢复 warehouses。
|
||||
|
||||
新版以**远端模式**启动(`file_path=None`),deck 反序列化时没有仓库子节点,`station.py` 扫描为空,所有物料的 warehouse 匹配失败,Bioyond 同步的 16 个资源全部无法放置到对应仓库位,前端不显示。
|
||||
@@ -12,7 +12,7 @@ Uni-Lab 使用 Python 格式的配置文件(`.py`),默认为 `unilabos_dat
|
||||
|
||||
**获取方式:**
|
||||
|
||||
进入 [Uni-Lab 实验室](https://uni-lab.bohrium.com),点击左下角的头像,在实验室详情中获取所在实验室的 ak 和 sk:
|
||||
进入 [Uni-Lab 实验室](https://leap-lab.bohrium.com),点击左下角的头像,在实验室详情中获取所在实验室的 ak 和 sk:
|
||||
|
||||

|
||||
|
||||
@@ -69,7 +69,7 @@ class WSConfig:
|
||||
|
||||
# HTTP配置
|
||||
class HTTPConfig:
|
||||
remote_addr = "https://uni-lab.bohrium.com/api/v1" # 远程服务器地址
|
||||
remote_addr = "https://leap-lab.bohrium.com/api/v1" # 远程服务器地址
|
||||
|
||||
# ROS配置
|
||||
class ROSConfig:
|
||||
@@ -209,8 +209,8 @@ unilab --ak "key" --sk "secret" --addr "test" --upload_registry --2d_vis -g grap
|
||||
|
||||
`--addr` 参数支持以下预设值,会自动转换为对应的完整 URL:
|
||||
|
||||
- `test` → `https://uni-lab.test.bohrium.com/api/v1`
|
||||
- `uat` → `https://uni-lab.uat.bohrium.com/api/v1`
|
||||
- `test` → `https://leap-lab.test.bohrium.com/api/v1`
|
||||
- `uat` → `https://leap-lab.uat.bohrium.com/api/v1`
|
||||
- `local` → `http://127.0.0.1:48197/api/v1`
|
||||
- 其他值 → 直接使用作为完整 URL
|
||||
|
||||
@@ -248,7 +248,7 @@ unilab --ak "key" --sk "secret" --addr "test" --upload_registry --2d_vis -g grap
|
||||
|
||||
`ak` 和 `sk` 是必需的认证参数:
|
||||
|
||||
1. **获取方式**:在 [Uni-Lab 官网](https://uni-lab.bohrium.com) 注册实验室后获得
|
||||
1. **获取方式**:在 [Uni-Lab 官网](https://leap-lab.bohrium.com) 注册实验室后获得
|
||||
2. **配置方式**:
|
||||
- **命令行参数**:`--ak "your_key" --sk "your_secret"`(最高优先级,推荐)
|
||||
- **环境变量**:`UNILABOS_BASICCONFIG_AK` 和 `UNILABOS_BASICCONFIG_SK`
|
||||
@@ -275,15 +275,15 @@ WebSocket 是 Uni-Lab 的主要通信方式:
|
||||
|
||||
HTTP 客户端配置用于与云端服务通信:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------- | ---- | -------------------------------------- | ------------ |
|
||||
| `remote_addr` | str | `"https://uni-lab.bohrium.com/api/v1"` | 远程服务地址 |
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------- | ---- | --------------------------------------- | ------------ |
|
||||
| `remote_addr` | str | `"https://leap-lab.bohrium.com/api/v1"` | 远程服务地址 |
|
||||
|
||||
**预设环境地址**:
|
||||
|
||||
- 生产环境:`https://uni-lab.bohrium.com/api/v1`(默认)
|
||||
- 测试环境:`https://uni-lab.test.bohrium.com/api/v1`
|
||||
- UAT 环境:`https://uni-lab.uat.bohrium.com/api/v1`
|
||||
- 生产环境:`https://leap-lab.bohrium.com/api/v1`(默认)
|
||||
- 测试环境:`https://leap-lab.test.bohrium.com/api/v1`
|
||||
- UAT 环境:`https://leap-lab.uat.bohrium.com/api/v1`
|
||||
- 本地环境:`http://127.0.0.1:48197/api/v1`
|
||||
|
||||
### 4. ROSConfig - ROS 配置
|
||||
@@ -401,7 +401,7 @@ export UNILABOS_WSCONFIG_RECONNECT_INTERVAL="10"
|
||||
export UNILABOS_WSCONFIG_MAX_RECONNECT_ATTEMPTS="500"
|
||||
|
||||
# 设置HTTP配置
|
||||
export UNILABOS_HTTPCONFIG_REMOTE_ADDR="https://uni-lab.test.bohrium.com/api/v1"
|
||||
export UNILABOS_HTTPCONFIG_REMOTE_ADDR="https://leap-lab.test.bohrium.com/api/v1"
|
||||
```
|
||||
|
||||
## 配置文件使用方法
|
||||
@@ -484,13 +484,13 @@ export UNILABOS_WSCONFIG_MAX_RECONNECT_ATTEMPTS=100
|
||||
|
||||
```python
|
||||
class HTTPConfig:
|
||||
remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
|
||||
remote_addr = "https://leap-lab.test.bohrium.com/api/v1"
|
||||
```
|
||||
|
||||
**环境变量方式:**
|
||||
|
||||
```bash
|
||||
export UNILABOS_HTTPCONFIG_REMOTE_ADDR=https://uni-lab.test.bohrium.com/api/v1
|
||||
export UNILABOS_HTTPCONFIG_REMOTE_ADDR=https://leap-lab.test.bohrium.com/api/v1
|
||||
```
|
||||
|
||||
**命令行方式(推荐):**
|
||||
|
||||
@@ -23,7 +23,7 @@ Uni-Lab-OS 支持多种部署模式:
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Cloud Platform/Self-hosted Platform │
|
||||
│ uni-lab.bohrium.com │
|
||||
│ leap-lab.bohrium.com │
|
||||
│ (Resource Management, Task Scheduling, │
|
||||
│ Monitoring) │
|
||||
└────────────────────┬─────────────────────────┘
|
||||
@@ -444,7 +444,7 @@ ros2 daemon stop && ros2 daemon start
|
||||
|
||||
```bash
|
||||
# 测试云端连接
|
||||
curl https://uni-lab.bohrium.com/api/v1/health
|
||||
curl https://leap-lab.bohrium.com/api/v1/health
|
||||
|
||||
# 测试WebSocket
|
||||
# 启动Uni-Lab后查看日志
|
||||
|
||||
@@ -33,11 +33,11 @@
|
||||
|
||||
**选择合适的安装包:**
|
||||
|
||||
| 安装包 | 适用场景 | 包含组件 |
|
||||
|--------|----------|----------|
|
||||
| `unilabos` | **推荐大多数用户**,生产部署 | 完整安装包,开箱即用 |
|
||||
| `unilabos-env` | 开发者(可编辑安装) | 仅环境依赖,通过 pip 安装 unilabos |
|
||||
| `unilabos-full` | 仿真/可视化 | unilabos + 完整 ROS2 桌面版 + Gazebo + MoveIt |
|
||||
| 安装包 | 适用场景 | 包含组件 |
|
||||
| --------------- | ---------------------------- | --------------------------------------------- |
|
||||
| `unilabos` | **推荐大多数用户**,生产部署 | 完整安装包,开箱即用 |
|
||||
| `unilabos-env` | 开发者(可编辑安装) | 仅环境依赖,通过 pip 安装 unilabos |
|
||||
| `unilabos-full` | 仿真/可视化 | unilabos + 完整 ROS2 桌面版 + Gazebo + MoveIt |
|
||||
|
||||
**关键步骤:**
|
||||
|
||||
@@ -66,6 +66,7 @@ mamba install uni-lab::unilabos-full -c robostack-staging -c conda-forge
|
||||
```
|
||||
|
||||
**选择建议:**
|
||||
|
||||
- **日常使用/生产部署**:使用 `unilabos`(推荐),完整功能,开箱即用
|
||||
- **开发者**:使用 `unilabos-env` + `pip install -e .` + `uv pip install -r unilabos/utils/requirements.txt`,代码修改立即生效
|
||||
- **仿真/可视化**:使用 `unilabos-full`,含 Gazebo、rviz2、MoveIt
|
||||
@@ -88,7 +89,7 @@ python -c "from unilabos_msgs.msg import Resource; print('ROS msgs OK')"
|
||||
|
||||
#### 2.1 注册实验室账号
|
||||
|
||||
1. 访问 [https://uni-lab.bohrium.com](https://uni-lab.bohrium.com)
|
||||
1. 访问 [https://leap-lab.bohrium.com](https://leap-lab.bohrium.com)
|
||||
2. 注册账号并登录
|
||||
3. 创建新实验室
|
||||
|
||||
@@ -297,7 +298,7 @@ unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json
|
||||
|
||||
#### 5.2 访问 Web 界面
|
||||
|
||||
启动系统后,访问[https://uni-lab.bohrium.com](https://uni-lab.bohrium.com)
|
||||
启动系统后,访问[https://leap-lab.bohrium.com](https://leap-lab.bohrium.com)
|
||||
|
||||
#### 5.3 添加设备和物料
|
||||
|
||||
@@ -306,12 +307,10 @@ unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json
|
||||
**示例场景:** 创建一个简单的液体转移实验
|
||||
|
||||
1. **添加工作站(必需):**
|
||||
|
||||
- 在"仪器设备"中找到 `work_station`
|
||||
- 添加 `workstation` x1
|
||||
|
||||
2. **添加虚拟转移泵:**
|
||||
|
||||
- 在"仪器设备"中找到 `virtual_device`
|
||||
- 添加 `virtual_transfer_pump` x1
|
||||
|
||||
@@ -818,6 +817,7 @@ uv pip install -r unilabos/utils/requirements.txt
|
||||
```
|
||||
|
||||
**为什么使用这种方式?**
|
||||
|
||||
- `unilabos-env` 提供 ROS2 核心组件和 uv(通过 conda 安装,避免编译)
|
||||
- `unilabos/utils/requirements.txt` 包含所有运行时需要的 pip 依赖
|
||||
- `dev_install.py` 自动检测中文环境,中文系统自动使用清华镜像
|
||||
@@ -1796,32 +1796,27 @@ unilab --ak your_ak --sk your_sk -g graph.json \
|
||||
**详细步骤:**
|
||||
|
||||
1. **需求分析**:
|
||||
|
||||
- 明确实验流程
|
||||
- 列出所需设备和物料
|
||||
- 设计工作流程图
|
||||
|
||||
2. **环境搭建**:
|
||||
|
||||
- 安装 Uni-Lab-OS
|
||||
- 创建实验室账号
|
||||
- 准备开发工具(IDE、Git)
|
||||
|
||||
3. **原型验证**:
|
||||
|
||||
- 使用虚拟设备测试流程
|
||||
- 验证工作流逻辑
|
||||
- 调整参数
|
||||
|
||||
4. **迭代开发**:
|
||||
|
||||
- 实现自定义设备驱动(同时撰写单点函数测试)
|
||||
- 编写注册表
|
||||
- 单元测试
|
||||
- 集成测试
|
||||
|
||||
5. **测试部署**:
|
||||
|
||||
- 连接真实硬件
|
||||
- 空跑测试
|
||||
- 小规模试验
|
||||
@@ -1871,7 +1866,7 @@ unilab --ak your_ak --sk your_sk -g graph.json \
|
||||
#### 14.5 社区支持
|
||||
|
||||
- **GitHub Issues**:[https://github.com/deepmodeling/Uni-Lab-OS/issues](https://github.com/deepmodeling/Uni-Lab-OS/issues)
|
||||
- **官方网站**:[https://uni-lab.bohrium.com](https://uni-lab.bohrium.com)
|
||||
- **官方网站**:[https://leap-lab.bohrium.com](https://leap-lab.bohrium.com)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -626,7 +626,7 @@ unilab
|
||||
|
||||
**云端图文件管理**:
|
||||
|
||||
1. 登录 https://uni-lab.bohrium.com
|
||||
1. 登录 https://leap-lab.bohrium.com
|
||||
2. 进入"设备配置"
|
||||
3. 创建或编辑配置
|
||||
4. 保存到云端
|
||||
|
||||
@@ -54,7 +54,6 @@ Uni-Lab 的启动过程分为以下几个阶段:
|
||||
您可以直接跟随 unilabos 的提示进行,无需查阅本节
|
||||
|
||||
- **工作目录设置**:
|
||||
|
||||
- 如果当前目录以 `unilabos_data` 结尾,则使用当前目录
|
||||
- 否则使用 `当前目录/unilabos_data` 作为工作目录
|
||||
- 可通过 `--working_dir` 指定自定义工作目录
|
||||
@@ -68,8 +67,8 @@ Uni-Lab 的启动过程分为以下几个阶段:
|
||||
|
||||
支持多种后端环境:
|
||||
|
||||
- `--addr test`:测试环境 (`https://uni-lab.test.bohrium.com/api/v1`)
|
||||
- `--addr uat`:UAT 环境 (`https://uni-lab.uat.bohrium.com/api/v1`)
|
||||
- `--addr test`:测试环境 (`https://leap-lab.test.bohrium.com/api/v1`)
|
||||
- `--addr uat`:UAT 环境 (`https://leap-lab.uat.bohrium.com/api/v1`)
|
||||
- `--addr local`:本地环境 (`http://127.0.0.1:48197/api/v1`)
|
||||
- 自定义地址:直接指定完整 URL
|
||||
|
||||
@@ -176,7 +175,7 @@ unilab --config path/to/your/config.py
|
||||
|
||||
如果是首次使用,系统会:
|
||||
|
||||
1. 提示前往 https://uni-lab.bohrium.com 注册实验室
|
||||
1. 提示前往 https://leap-lab.bohrium.com 注册实验室
|
||||
2. 引导创建配置文件
|
||||
3. 设置工作目录
|
||||
|
||||
@@ -216,7 +215,7 @@ unilab --ak your_ak --sk your_sk --port 8080 --disable_browser
|
||||
|
||||
如果提示 "后续运行必须拥有一个实验室",请确保:
|
||||
|
||||
- 已在 https://uni-lab.bohrium.com 注册实验室
|
||||
- 已在 https://leap-lab.bohrium.com 注册实验室
|
||||
- 正确设置了 `--ak` 和 `--sk` 参数
|
||||
- 配置文件中包含正确的认证信息
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ def parse_args():
|
||||
parser.add_argument(
|
||||
"--addr",
|
||||
type=str,
|
||||
default="https://uni-lab.bohrium.com/api/v1",
|
||||
default="https://leap-lab.bohrium.com/api/v1",
|
||||
help="Laboratory backend address",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -438,10 +438,10 @@ def main():
|
||||
if args.addr != parser.get_default("addr"):
|
||||
if args.addr == "test":
|
||||
print_status("使用测试环境地址", "info")
|
||||
HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
|
||||
HTTPConfig.remote_addr = "https://leap-lab.test.bohrium.com/api/v1"
|
||||
elif args.addr == "uat":
|
||||
print_status("使用uat环境地址", "info")
|
||||
HTTPConfig.remote_addr = "https://uni-lab.uat.bohrium.com/api/v1"
|
||||
HTTPConfig.remote_addr = "https://leap-lab.uat.bohrium.com/api/v1"
|
||||
elif args.addr == "local":
|
||||
print_status("使用本地环境地址", "info")
|
||||
HTTPConfig.remote_addr = "http://127.0.0.1:48197/api/v1"
|
||||
@@ -553,7 +553,7 @@ def main():
|
||||
os._exit(0)
|
||||
|
||||
if not BasicConfig.ak or not BasicConfig.sk:
|
||||
print_status("后续运行必须拥有一个实验室,请前往 https://uni-lab.bohrium.com 注册实验室!", "warning")
|
||||
print_status("后续运行必须拥有一个实验室,请前往 https://leap-lab.bohrium.com 注册实验室!", "warning")
|
||||
os._exit(1)
|
||||
graph: nx.Graph
|
||||
resource_tree_set: ResourceTreeSet
|
||||
@@ -621,6 +621,8 @@ def main():
|
||||
continue
|
||||
|
||||
# 如果从远端获取了物料信息,则与本地物料进行同步
|
||||
# 仅在本地文件模式下有意义:本地文件只含设备结构,远端有已保存的物料,需要 merge
|
||||
# 远端模式下 resource_tree_set 与 request_startup_json 来自同一份数据,merge 为空操作
|
||||
if file_path is not None and request_startup_json and "nodes" in request_startup_json:
|
||||
print_status("开始同步远端物料到本地...", "info")
|
||||
remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"])
|
||||
|
||||
@@ -36,6 +36,9 @@ class HTTPClient:
|
||||
auth_secret = BasicConfig.auth_secret()
|
||||
self.auth = auth_secret
|
||||
info(f"正在使用ak sk作为授权信息:[{auth_secret}]")
|
||||
# 复用 TCP/TLS 连接,避免每次请求重新握手
|
||||
self._session = requests.Session()
|
||||
self._session.headers.update({"Authorization": f"Lab {self.auth}"})
|
||||
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}")
|
||||
|
||||
def resource_edge_add(self, resources: List[Dict[str, Any]]) -> requests.Response:
|
||||
@@ -48,7 +51,7 @@ class HTTPClient:
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/edge/material/edge",
|
||||
json={
|
||||
"edges": resources,
|
||||
@@ -75,26 +78,28 @@ class HTTPClient:
|
||||
Returns:
|
||||
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
|
||||
"""
|
||||
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_add.json"), "w", encoding="utf-8") as f:
|
||||
payload = {"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}
|
||||
f.write(json.dumps(payload, indent=4))
|
||||
# 从序列化数据中提取所有节点的UUID(保存旧UUID)
|
||||
old_uuids = {n.res_content.uuid: n for n in resources.all_nodes}
|
||||
# dump() 只调用一次,复用给文件保存和 HTTP 请求
|
||||
nodes_info = [x for xs in resources.dump() for x in xs]
|
||||
old_uuids = {n.res_content.uuid: n for n in resources.all_nodes}
|
||||
payload = {"nodes": nodes_info, "mount_uuid": mount_uuid}
|
||||
body_bytes = _fast_dumps(payload)
|
||||
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_add.json"), "wb") as f:
|
||||
f.write(_fast_dumps_pretty(payload))
|
||||
http_headers = {"Content-Type": "application/json"}
|
||||
if not self.initialized or first_add:
|
||||
self.initialized = True
|
||||
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/edge/material",
|
||||
json={"nodes": nodes_info, "mount_uuid": mount_uuid},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
data=body_bytes,
|
||||
headers=http_headers,
|
||||
timeout=60,
|
||||
)
|
||||
else:
|
||||
response = requests.put(
|
||||
response = self._session.put(
|
||||
f"{self.remote_addr}/edge/material",
|
||||
json={"nodes": nodes_info, "mount_uuid": mount_uuid},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
data=body_bytes,
|
||||
headers=http_headers,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
@@ -133,7 +138,7 @@ class HTTPClient:
|
||||
"""
|
||||
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_get.json"), "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps({"uuids": uuid_list, "with_children": with_children}, indent=4))
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/edge/material/query",
|
||||
json={"uuids": uuid_list, "with_children": with_children},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
@@ -147,6 +152,7 @@ class HTTPClient:
|
||||
logger.error(f"查询物料失败: {response.text}")
|
||||
else:
|
||||
data = res["data"]["nodes"]
|
||||
logger.trace(f"resource_tree_get查询到物料: {data}")
|
||||
return data
|
||||
else:
|
||||
logger.error(f"查询物料失败: {response.text}")
|
||||
@@ -164,14 +170,14 @@ class HTTPClient:
|
||||
if not self.initialized:
|
||||
self.initialized = True
|
||||
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
else:
|
||||
response = requests.put(
|
||||
response = self._session.put(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
@@ -198,7 +204,7 @@ class HTTPClient:
|
||||
"""
|
||||
with open(os.path.join(BasicConfig.working_dir, "req_resource_get.json"), "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps({"id": id, "with_children": with_children}, indent=4))
|
||||
response = requests.get(
|
||||
response = self._session.get(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
params={"id": id, "with_children": with_children},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
@@ -239,14 +245,14 @@ class HTTPClient:
|
||||
if not self.initialized:
|
||||
self.initialized = True
|
||||
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
else:
|
||||
response = requests.put(
|
||||
response = self._session.put(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
@@ -276,7 +282,7 @@ class HTTPClient:
|
||||
with open(file_path, "rb") as file:
|
||||
files = {"files": file}
|
||||
logger.info(f"上传文件: {file_path} 到 {scene}")
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/api/account/file_upload/{scene}",
|
||||
files=files,
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
@@ -316,7 +322,7 @@ class HTTPClient:
|
||||
"Content-Type": "application/json",
|
||||
"Content-Encoding": "gzip",
|
||||
}
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/lab/resource",
|
||||
data=compressed_body,
|
||||
headers=headers,
|
||||
@@ -350,7 +356,7 @@ class HTTPClient:
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.get(
|
||||
response = self._session.get(
|
||||
f"{self.remote_addr}/edge/material/download",
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=(3, 30),
|
||||
@@ -411,7 +417,7 @@ class HTTPClient:
|
||||
with open(os.path.join(BasicConfig.working_dir, "req_workflow_upload.json"), "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(payload, indent=4, ensure_ascii=False))
|
||||
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/lab/workflow/owner/import",
|
||||
json=payload,
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
|
||||
@@ -1269,7 +1269,13 @@ class QueueProcessor:
|
||||
if not queued_jobs:
|
||||
return
|
||||
|
||||
logger.debug(f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs")
|
||||
queue_summary = {}
|
||||
for j in queued_jobs:
|
||||
key = f"{j.device_id}/{j.action_name}"
|
||||
queue_summary[key] = queue_summary.get(key, 0) + 1
|
||||
logger.debug(
|
||||
f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs: {queue_summary}"
|
||||
)
|
||||
|
||||
for job_info in queued_jobs:
|
||||
# 快照可能已过期:在遍历过程中 end_job() 可能已将此 job 移至 READY,
|
||||
|
||||
@@ -46,7 +46,7 @@ class WSConfig:
|
||||
|
||||
# HTTP配置
|
||||
class HTTPConfig:
|
||||
remote_addr = "https://uni-lab.bohrium.com/api/v1"
|
||||
remote_addr = "https://leap-lab.bohrium.com/api/v1"
|
||||
|
||||
|
||||
# ROS配置
|
||||
|
||||
@@ -219,10 +219,10 @@ device = NewareBatteryTestSystem(
|
||||
|
||||
#### 步骤 2:提交测试任务
|
||||
|
||||
使用 `submit_from_csv` 提交测试任务:
|
||||
使用 `submit_from_csv_export_ndax` 提交测试任务:
|
||||
|
||||
```python
|
||||
result = device.submit_from_csv(
|
||||
result = device.submit_from_csv_export_ndax(
|
||||
csv_path="test_data.csv",
|
||||
output_dir="D:/neware_output"
|
||||
)
|
||||
@@ -489,7 +489,7 @@ A: 重新获取新的 Token 并更新环境变量 `UNI_LAB_AUTH_TOKEN`。
|
||||
**Q: 可以自定义上传路径吗?**
|
||||
A: 当前版本路径由统一 API 自动分配,`oss_prefix` 参数暂不使用(保留接口兼容性)。
|
||||
|
||||
**Q: 为什么不在 `submit_from_csv` 中自动上传?**
|
||||
**Q: 为什么不在 `submit_from_csv_export_ndax` 中自动上传?**
|
||||
A: 因为备份文件在测试进行中逐步生成,方法返回时可能文件尚未完全生成,因此提供独立的上传方法更灵活。
|
||||
|
||||
**Q: 上传后如何访问文件?**
|
||||
|
||||
@@ -230,10 +230,10 @@ device = NewareBatteryTestSystem(
|
||||
|
||||
#### Step 2: Submit Test Tasks
|
||||
|
||||
Use `submit_from_csv` to submit test tasks:
|
||||
Use `submit_from_csv_export_ndax` to submit test tasks:
|
||||
|
||||
```python
|
||||
result = device.submit_from_csv(
|
||||
result = device.submit_from_csv_export_ndax(
|
||||
csv_path="test_data.csv",
|
||||
output_dir="D:/neware_output"
|
||||
)
|
||||
@@ -500,7 +500,7 @@ A: Obtain a new API Key and update the `UNI_LAB_AUTH_TOKEN` environment variable
|
||||
**Q: Can I customize upload paths?**
|
||||
A: Current version has paths automatically assigned by unified API. `oss_prefix` parameter is currently unused (retained for interface compatibility).
|
||||
|
||||
**Q: Why not auto-upload in `submit_from_csv`?**
|
||||
**Q: Why not auto-upload in `submit_from_csv_export_ndax`?**
|
||||
A: Because backup files are generated progressively during testing, they may not be fully generated when the method returns. A separate upload method provides more flexibility.
|
||||
|
||||
**Q: How to access files after upload?**
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"config": {
|
||||
"ip": "127.0.0.1",
|
||||
"port": 502,
|
||||
"machine_id": 1,
|
||||
"machine_ids": [1, 2, 3, 4, 5, 6, 86],
|
||||
"devtype": "27",
|
||||
"timeout": 20,
|
||||
"size_x": 500.0,
|
||||
@@ -26,10 +26,10 @@
|
||||
"data": {
|
||||
"功能说明": "新威电池测试系统,提供720通道监控和CSV批量提交功能",
|
||||
"监控功能": "支持720个通道的实时状态监控、2盘电池物料管理、状态导出等",
|
||||
"提交功能": "通过submit_from_csv action从CSV文件批量提交测试任务。CSV必须包含: Battery_Code, Pole_Weight, 集流体质量, 活性物质含量, 克容量mah/g, 电池体系, 设备号, 排号, 通道号"
|
||||
"提交功能": "通过submit_from_csv action从CSV文件批量提交测试任务(NDA备份),或通过submit_from_csv_export_excel action提交并备份为Excel格式。CSV必须包含: Battery_Code, Pole_Weight, 集流体质量, 活性物质含量, 克容量mah/g, 电池体系, 设备号, 排号, 通道号"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
}
|
||||
|
||||
1644
unilabos/devices/neware_battery_test_system/generate_xml_content.py
Normal file
1644
unilabos/devices/neware_battery_test_system/generate_xml_content.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
56
unilabos/devices/neware_battery_test_system/neware_driver.py
Normal file
56
unilabos/devices/neware_battery_test_system/neware_driver.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import socket
|
||||
END_MARKS = [b"\r\n#\r\n", b"</bts>"] # 读到任一标志即可判定完整响应
|
||||
|
||||
def build_start_command(devid, subdevid, chlid, CoinID,
|
||||
ip_in_xml="127.0.0.1",
|
||||
devtype:int=27,
|
||||
recipe_path:str=f"D:\\HHM_test\\A001.xml",
|
||||
backup_dir:str=f"D:\\HHM_test\\backup",
|
||||
filetype:int=1) -> str:
|
||||
"""
|
||||
filetype: 备份文件类型。0=NDA(新威原生),1=Excel。默认 1。
|
||||
"""
|
||||
lines = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<bts version="1.0">',
|
||||
' <cmd>start</cmd>',
|
||||
' <list count="1">',
|
||||
f' <start ip="{ip_in_xml}" devtype="{devtype}" devid="{devid}" subdevid="{subdevid}" chlid="{chlid}" barcode="{CoinID}">{recipe_path}</start>',
|
||||
f' <backup backupdir="{backup_dir}" remotedir="" filenametype="1" customfilename="" createdirbydate="0" filetype="{int(filetype)}" backupontime="1" backupontimeinterval="1" backupfree="0" />',
|
||||
' </list>',
|
||||
'</bts>',
|
||||
]
|
||||
# TCP 模式:请求必须以 #\r\n 结束(协议要求)
|
||||
return "\r\n".join(lines) + "\r\n#\r\n"
|
||||
|
||||
def recv_until_marks(sock: socket.socket, timeout=60):
|
||||
sock.settimeout(timeout) # 上限给足,协议允许到 30s:contentReference[oaicite:2]{index=2}
|
||||
buf = bytearray()
|
||||
while True:
|
||||
chunk = sock.recv(8192)
|
||||
if not chunk:
|
||||
break
|
||||
buf += chunk
|
||||
# 读到结束标志就停,避免等对端断开
|
||||
for m in END_MARKS:
|
||||
if m in buf:
|
||||
return bytes(buf)
|
||||
# 保险:读到完整 XML 结束标签也停
|
||||
if b"</bts>" in buf:
|
||||
return bytes(buf)
|
||||
return bytes(buf)
|
||||
|
||||
def start_test(ip="127.0.0.1", port=502, devid=3, subdevid=2, chlid=1, CoinID="A001", recipe_path=f"D:\\HHM_test\\A001.xml", backup_dir=f"D:\\HHM_test\\backup", filetype:int=1):
|
||||
"""
|
||||
filetype: 备份文件类型,0=NDA,1=Excel。默认 1。
|
||||
"""
|
||||
xml_cmd = build_start_command(devid=devid, subdevid=subdevid, chlid=chlid, CoinID=CoinID, recipe_path=recipe_path, backup_dir=backup_dir, filetype=filetype)
|
||||
#print(xml_cmd)
|
||||
with socket.create_connection((ip, port), timeout=60) as s:
|
||||
s.sendall(xml_cmd.encode("utf-8"))
|
||||
data = recv_until_marks(s, timeout=60)
|
||||
return data.decode("utf-8", errors="replace")
|
||||
|
||||
if __name__ == "__main__":
|
||||
resp = start_test(ip="127.0.0.1", port=502, devid=4, subdevid=10, chlid=1, CoinID="A001", recipe_path=f"D:\\HHM_test\\A001.xml", backup_dir=f"D:\\HHM_test\\backup")
|
||||
print(resp)
|
||||
@@ -22,10 +22,11 @@ from threading import Lock, RLock
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from unilabos.registry.decorators import (
|
||||
device, action, ActionInputHandle, ActionOutputHandle, DataSource, topic_config, not_action
|
||||
device, action, ActionInputHandle, ActionOutputHandle, DataSource, topic_config, not_action, NodeType
|
||||
)
|
||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample
|
||||
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
|
||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode
|
||||
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, ResourceTreeSet
|
||||
|
||||
|
||||
# ============ TypedDict 返回类型定义 ============
|
||||
@@ -290,6 +291,126 @@ class VirtualWorkbench:
|
||||
self._update_data_status(f"机械臂已释放 (完成: {task})")
|
||||
self.logger.info(f"机械臂已释放 (完成: {task})")
|
||||
|
||||
@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": []
|
||||
}, feedback_interval=300,
|
||||
handles=[
|
||||
ActionInputHandle(key="target_device", data_type="device_id",
|
||||
label="目标设备", data_key="target_device", data_source=DataSource.HANDLE),
|
||||
ActionInputHandle(key="resource", data_type="resource",
|
||||
label="待转移资源", data_key="resource", data_source=DataSource.HANDLE),
|
||||
ActionInputHandle(key="mount_resource", data_type="resource",
|
||||
label="目标孔位", data_key="mount_resource", data_source=DataSource.HANDLE),
|
||||
|
||||
ActionInputHandle(key="collector_mass", data_type="collector_mass",
|
||||
label="极流体质量", data_key="collector_mass", data_source=DataSource.HANDLE),
|
||||
ActionInputHandle(key="active_material", data_type="active_material",
|
||||
label="活性物质含量", data_key="active_material", data_source=DataSource.HANDLE),
|
||||
ActionInputHandle(key="capacity", data_type="capacity",
|
||||
label="克容量", data_key="capacity", data_source=DataSource.HANDLE),
|
||||
ActionInputHandle(key="battery_system", data_type="battery_system",
|
||||
label="电池体系", data_key="battery_system", data_source=DataSource.HANDLE),
|
||||
# transfer使用
|
||||
ActionOutputHandle(key="target_device", data_type="device_id",
|
||||
label="目标设备", data_key="target_device", data_source=DataSource.EXECUTOR),
|
||||
ActionOutputHandle(key="resource", data_type="resource",
|
||||
label="待转移资源", data_key="resource.@flatten", data_source=DataSource.EXECUTOR),
|
||||
ActionOutputHandle(key="mount_resource", data_type="resource",
|
||||
label="目标孔位", data_key="mount_resource.@flatten", data_source=DataSource.EXECUTOR),
|
||||
# test使用
|
||||
ActionOutputHandle(key="collector_mass", data_type="collector_mass",
|
||||
label="极流体质量", data_key="collector_mass", data_source=DataSource.EXECUTOR),
|
||||
ActionOutputHandle(key="active_material", data_type="active_material",
|
||||
label="活性物质含量", data_key="active_material", data_source=DataSource.EXECUTOR),
|
||||
ActionOutputHandle(key="capacity", data_type="capacity",
|
||||
label="克容量", data_key="capacity", data_source=DataSource.EXECUTOR),
|
||||
ActionOutputHandle(key="battery_system", data_type="battery_system",
|
||||
label="电池体系", data_key="battery_system", data_source=DataSource.EXECUTOR),
|
||||
]
|
||||
)
|
||||
def manual_confirm(
|
||||
self,
|
||||
resource: List[ResourceSlot],
|
||||
target_device: DeviceSlot,
|
||||
mount_resource: List[ResourceSlot],
|
||||
collector_mass: List[float],
|
||||
active_material: List[float],
|
||||
capacity: List[float],
|
||||
battery_system: List[str],
|
||||
timeout_seconds: int,
|
||||
assignee_user_ids: list[str],
|
||||
**kwargs
|
||||
) -> dict:
|
||||
"""
|
||||
timeout_seconds: 超时时间(秒),默认3600秒
|
||||
collector_mass: 极流体质量
|
||||
active_material: 活性物质含量
|
||||
capacity: 克容量(mAh/g)
|
||||
battery_system: 电池体系
|
||||
修改的结果无效,是只读的
|
||||
"""
|
||||
resource = ResourceTreeSet.from_plr_resources(resource).dump()
|
||||
mount_resource = ResourceTreeSet.from_plr_resources(mount_resource).dump()
|
||||
kwargs.update(locals())
|
||||
kwargs.pop("kwargs")
|
||||
kwargs.pop("self")
|
||||
return kwargs
|
||||
|
||||
@action(
|
||||
description="转移物料",
|
||||
handles=[
|
||||
ActionInputHandle(key="target_device", data_type="device_id",
|
||||
label="目标设备", data_key="target_device", data_source=DataSource.HANDLE),
|
||||
ActionInputHandle(key="resource", data_type="resource",
|
||||
label="待转移资源", data_key="resource", data_source=DataSource.HANDLE),
|
||||
ActionInputHandle(key="mount_resource", data_type="resource",
|
||||
label="目标孔位", data_key="mount_resource", data_source=DataSource.HANDLE),
|
||||
]
|
||||
)
|
||||
async def transfer(self, resource: List[ResourceSlot], target_device: DeviceSlot, mount_resource: List[ResourceSlot]):
|
||||
future = ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True,
|
||||
**{
|
||||
"plr_resources": resource,
|
||||
"target_device_id": target_device,
|
||||
"target_resources": mount_resource,
|
||||
"sites": [None] * len(mount_resource),
|
||||
})
|
||||
result = await future
|
||||
return result
|
||||
|
||||
|
||||
@action(
|
||||
description="扣电测试启动",
|
||||
handles=[
|
||||
ActionInputHandle(key="resource", data_type="resource",
|
||||
label="待转移资源", data_key="resource", data_source=DataSource.HANDLE),
|
||||
ActionInputHandle(key="mount_resource", data_type="resource",
|
||||
label="目标孔位", data_key="mount_resource", data_source=DataSource.HANDLE),
|
||||
|
||||
ActionInputHandle(key="collector_mass", data_type="collector_mass",
|
||||
label="极流体质量", data_key="collector_mass", data_source=DataSource.HANDLE),
|
||||
ActionInputHandle(key="active_material", data_type="active_material",
|
||||
label="活性物质含量", data_key="active_material", data_source=DataSource.HANDLE),
|
||||
ActionInputHandle(key="capacity", data_type="capacity",
|
||||
label="克容量", data_key="capacity", data_source=DataSource.HANDLE),
|
||||
ActionInputHandle(key="battery_system", data_type="battery_system",
|
||||
label="电池体系", data_key="battery_system", data_source=DataSource.HANDLE),
|
||||
]
|
||||
)
|
||||
async def test(
|
||||
self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], collector_mass: List[float], active_material: List[float], capacity: List[float], battery_system: list[str]
|
||||
):
|
||||
print(resource)
|
||||
print(mount_resource)
|
||||
print(collector_mass)
|
||||
print(active_material)
|
||||
print(capacity)
|
||||
print(battery_system)
|
||||
|
||||
@action(
|
||||
auto_prefix=True,
|
||||
description="批量准备物料 - 虚拟起始节点, 生成A1-A5物料, 输出5个handle供后续节点使用",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -258,7 +258,7 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
|
||||
logger.info(f"[同步→Bioyond] ➕ 物料不存在于 Bioyond,将创建新物料并入库")
|
||||
|
||||
# 第1步:从配置中获取仓库配置
|
||||
warehouse_mapping = self.bioyond_config.get("warehouse_mapping", {})
|
||||
warehouse_mapping = self.workstation.bioyond_config.get("warehouse_mapping", {})
|
||||
|
||||
# 确定目标仓库名称
|
||||
parent_name = None
|
||||
@@ -760,10 +760,9 @@ class BioyondWorkstation(WorkstationBase):
|
||||
except:
|
||||
pass
|
||||
|
||||
# 创建通信模块
|
||||
# 创建通信模块;同步器将在 post_init 中初始化并执行首次同步
|
||||
self._create_communication_module(bioyond_config)
|
||||
self.resource_synchronizer = BioyondResourceSynchronizer(self)
|
||||
self.resource_synchronizer.sync_from_external()
|
||||
self.resource_synchronizer = None
|
||||
|
||||
# TODO: self._ros_node里面拿属性
|
||||
|
||||
@@ -802,6 +801,15 @@ class BioyondWorkstation(WorkstationBase):
|
||||
def post_init(self, ros_node: ROS2WorkstationNode):
|
||||
self._ros_node = ros_node
|
||||
|
||||
# Deck 为空时(反序列化未恢复子节点),主动调用 setup() 初始化仓库
|
||||
if self.deck and not self.deck.children and hasattr(self.deck, "setup") and callable(self.deck.setup):
|
||||
logger.info("Deck 无仓库子节点,调用 setup() 初始化仓库")
|
||||
self.deck.setup()
|
||||
|
||||
# 初始化同步器并执行首次同步(需在仓库初始化之后)
|
||||
self.resource_synchronizer = BioyondResourceSynchronizer(self)
|
||||
self.resource_synchronizer.sync_from_external()
|
||||
|
||||
# 启动连接监控
|
||||
try:
|
||||
self.connection_monitor = ConnectionMonitor(self)
|
||||
|
||||
219
unilabos/devices/workstation/changelog_2026-03-12.md
Normal file
219
unilabos/devices/workstation/changelog_2026-03-12.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# 代码变更说明 — 2026-03-12
|
||||
|
||||
> 本次变更基于 `implementation_plan_v2.md` 执行,目标:**物理几何结构初始化与物料内容物填充彻底解耦**,消除 PLR 反序列化时的 `Resource already assigned to deck` 错误,并修复若干运行时新增问题。
|
||||
|
||||
---
|
||||
|
||||
## 一、物料系统标准化重构(主线任务)
|
||||
|
||||
### 1. `unilabos/resources/battery/magazine.py`
|
||||
|
||||
**改动**:`MagazineHolder_6_Cathode`、`MagazineHolder_6_Anode`、`MagazineHolder_4_Cathode` 三个工厂函数的 `klasses` 参数改为 `None`。
|
||||
|
||||
**原因**:原来三个工厂函数在初始化时就向洞位填满极片对象(`ElectrodeSheet`),导致 PLR 反序列化时"几何结构已创建子节点 + DB 再次 assign"双重冲突。
|
||||
|
||||
**原则**:物料余量改由寄存器直读(阶段 F),资源树不再追踪每个极片实体。`MagazineHolder_6_Battery` 原本就是 `klasses=None`,三者现在保持一致。
|
||||
|
||||
---
|
||||
|
||||
### 2. `unilabos/resources/battery/magazine.py`(追加,响应重复 UUID 问题)
|
||||
|
||||
**改动**:为 `Magazine`(洞位类)新增 `serialize` 和 `deserialize` 重写:
|
||||
- `serialize`:序列化时强制将 `children` 置空,不再把极片写回数据库。
|
||||
- `deserialize`:反序列化时强制忽略 `children` 字段,阻止数据库中旧极片记录被恢复。
|
||||
|
||||
**原因**:数据库中遗留有旧的 `ElectrodeSheet` 记录(`A1_sheet100` 等),启动时被 PLR 反序列化进来,导致同一 UUID 出现在多个 Magazine 洞位中,触发 `发现重复的uuid` 错误。此修复从源头截断旧数据,经过一次完整的"启动 → 资源树写回"后,数据库旧极片记录也会被干净覆盖。
|
||||
|
||||
---
|
||||
|
||||
### 3. `unilabos/resources/battery/bottle_carriers.py`
|
||||
|
||||
**改动**:删除 `YIHUA_Electrolyte_12VialCarrier` 末尾的 12 瓶填充循环及对应 `import`。
|
||||
|
||||
**原因**:`bottle_rack_6x2` 和 `bottle_rack_6x2_2` 应初始化为空载架,瓶子由 Bioyond 侧实际转运后再填入。原来初始化时直接塞满 `YB_pei_ye_xiao_Bottle`,反序列化时产生重复 assign。
|
||||
|
||||
---
|
||||
|
||||
### 4. `unilabos/resources/bioyond/decks.py`
|
||||
|
||||
**改动**:
|
||||
- 将 `BIOYOND_YB_Deck` 重命名为 `BioyondElectrolyteDeck`,保留 `BIOYOND_YB_Deck` 作为向后兼容别名。
|
||||
- 工厂函数 `YB_Deck()` 重命名为 `bioyond_electrolyte_deck()`,保留 `YB_Deck` 作为别名。
|
||||
- `BIOYOND_PolymerReactionStation_Deck`、`BIOYOND_PolymerPreparationStation_Deck`、`BioyondElectrolyteDeck` 三个 Deck 类:
|
||||
- 移除 `__init__` 中的 `setup: bool = False` 参数及 `if setup: self.setup()` 调用。
|
||||
- 删除临时 `deserialize` 补丁(该补丁是为了强制 `setup=False`,根本原因消除后不再需要)。
|
||||
|
||||
**原因**:`setup` 参数导致 PLR 反序列化时先通过 `__init__` 创建所有子资源,再从 JSON `children` 字段再次 assign,产生 `already assigned to deck` 错误。正确模式:`__init__` 只初始化自身几何,`setup()` 由工厂函数调用,反序列化由 PLR 从 DB 数据重建子资源。
|
||||
|
||||
---
|
||||
|
||||
### 5. `unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py`
|
||||
|
||||
**改动**:
|
||||
- `CoincellDeck` 重命名为 `YihuaCoinCellDeck`,保留 `CoincellDeck` 作为向后兼容别名。
|
||||
- 工厂函数 `YH_Deck()` 重命名为 `yihua_coin_cell_deck()`,保留 `YH_Deck` 作为别名。
|
||||
- 移除 `YihuaCoinCellDeck.__init__` 中的 `setup: bool = False` 参数及调用,删除 `deserialize` 补丁(原因同 decks.py)。
|
||||
- `MaterialPlate.__init__` 移除 `fill` 参数和 `fill=True` 分支,新增类方法 `MaterialPlate.create_with_holes()` 作为"带洞位"的工厂方法,`setup()` 改为调用该工厂方法。
|
||||
- `YihuaCoinCellDeck.setup()` 末尾新增 `electrolyte_buffer`(`ResourceStack`)接驳槽,用于接收来自 Bioyond 侧的分液瓶板,命名与 `bioyond_cell_workstation.py` 中 `sites=["electrolyte_buffer"]` 一致。
|
||||
|
||||
---
|
||||
|
||||
### 6. `unilabos/resources/resource_tracker.py`
|
||||
|
||||
**改动 1**:`to_plr_resources` 中,`load_all_state` 调用前预填 `Container` 类资源缺失的键:
|
||||
|
||||
```python
|
||||
state.setdefault("liquid_history", [])
|
||||
state.setdefault("pending_liquids", {})
|
||||
```
|
||||
|
||||
**原因**:新版 PLR 要求 `Container` 状态中必须包含这两个键,旧数据库记录缺失时 `load_all_state` 会抛出 `KeyError`。
|
||||
|
||||
**改动 2**:`_validate_tree` 中,遇到重复 UUID 时改为自动重新分配新 UUID 并打 `WARNING`,不再直接抛异常崩溃。
|
||||
|
||||
**原因**:旧数据库中存在多个同名同 UUID 的极片对象(历史脏数据),严格校验会导致节点无法启动。改为 WARNING + 自动修复,确保启动成功,下次资源树写回后脏数据自然清除。
|
||||
|
||||
---
|
||||
|
||||
### 7. `unilabos/resources/itemized_carrier.py`
|
||||
|
||||
**改动**:将原来的 `idx is None` 兜底补丁(静默调用 `super().assign_child_resource`,不更新槽位追踪)替换为两段式逻辑:
|
||||
|
||||
1. **XY 近似匹配**(容差 2mm):精确三维坐标匹配失败时,仅对比 XY 二维坐标,找到最近槽位后用槽位的正确坐标(含 Z)完成 assign,并打 `WARNING`。
|
||||
2. **XY 也失败才抛异常**:给出详细的槽位列表和传入坐标,便于问题排查。
|
||||
|
||||
**原因**:数据库中存储的资源坐标 Z=0,而 `warehouse_factory` 定义的槽位 Z=dz(如 10mm)。精确匹配永远失败,原补丁静默兜底掩盖了这一问题。近似匹配修复了 Z 偏移,同时保留了真正异常时的报错能力。
|
||||
|
||||
---
|
||||
|
||||
### 8. `unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`
|
||||
|
||||
**改动 1**:更新导入:`BIOYOND_YB_Deck` → `BioyondElectrolyteDeck, bioyond_electrolyte_deck`。
|
||||
|
||||
**改动 2**:`__main__` 入口处改为调用 `bioyond_electrolyte_deck(name="YB_Deck")`。
|
||||
|
||||
**改动 3**:新增 `_get_resource_from_device(device_id, resource_name)` 方法,用于从目标设备的资源树中动态查找 PLR 资源对象(带降级回退逻辑)。
|
||||
|
||||
**改动 4**:跨站转运逻辑中,将原来"创建 `size=1,1,1` 的虚拟 `ResourcePLR` + 硬编码 UUID"的方式,改为通过 `_get_resource_from_device` 从目标设备获取真实的 `electrolyte_buffer` 资源对象。
|
||||
|
||||
**原因**:原代码使用硬编码 UUID 的虚拟资源作为转运目标,该对象在 YihuaCoinCellDeck 的资源树中不存在,转移后资源树状态混乱。
|
||||
|
||||
---
|
||||
|
||||
### 9. `unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py`
|
||||
|
||||
**改动 1**:更新导入:`CoincellDeck` → `YihuaCoinCellDeck, yihua_coin_cell_deck`,`__main__` 入口改为调用 `yihua_coin_cell_deck()`。
|
||||
|
||||
**改动 2**:新增 10 个 `@property`,实现对依华扣电工站 Modbus 寄存器的直读:
|
||||
|
||||
| 属性名 | 寄存器地址 | 说明 |
|
||||
|---|---|---|
|
||||
| `data_10mm_positive_plate_remaining` | 520 | 10mm正极片余量 |
|
||||
| `data_12mm_positive_plate_remaining` | 522 | 12mm正极片余量 |
|
||||
| `data_16mm_positive_plate_remaining` | 524 | 16mm正极片余量 |
|
||||
| `data_aluminum_foil_remaining` | 526 | 铝箔余量 |
|
||||
| `data_positive_shell_remaining` | 528 | 正极壳余量 |
|
||||
| `data_flat_washer_remaining` | 530 | 平垫余量 |
|
||||
| `data_negative_shell_remaining` | 532 | 负极壳余量 |
|
||||
| `data_spring_washer_remaining` | 534 | 弹垫余量 |
|
||||
| `data_finished_battery_remaining_capacity` | 536 | 成品电池余量 |
|
||||
| `data_finished_battery_ng_remaining_capacity` | 538 | 成品电池NG槽余量 |
|
||||
|
||||
**原因**:`coin_cell_workstation.yaml` 的 `status_types` 中定义了这 10 个属性,但代码中从未实现,导致每次前端轮询时均报 `AttributeError`。
|
||||
|
||||
---
|
||||
|
||||
## 二、配置与注册表更新
|
||||
|
||||
### 10. `yibin_electrolyte_config.json`
|
||||
- `BIOYOND_YB_Deck` → `BioyondElectrolyteDeck`(class、type、_resource_type 三处)
|
||||
- `CoincellDeck` → `YihuaCoinCellDeck`(class、type、_resource_type 三处)
|
||||
- 移除 `"setup": true` 字段
|
||||
|
||||
### 11. `yibin_coin_cell_only_config.json`
|
||||
- `CoincellDeck` → `YihuaCoinCellDeck`
|
||||
- 移除 `"setup": true`
|
||||
|
||||
### 12. `yibin_electrolyte_only_config.json`
|
||||
- `BIOYOND_YB_Deck` → `BioyondElectrolyteDeck`
|
||||
- 移除 `"setup": true`
|
||||
|
||||
### 13. `unilabos/registry/resources/bioyond/deck.yaml`
|
||||
- `BIOYOND_YB_Deck` → `BioyondElectrolyteDeck`,工厂函数路径更新为 `bioyond_electrolyte_deck`
|
||||
- `CoincellDeck` → `YihuaCoinCellDeck`,工厂函数路径更新为 `yihua_coin_cell_deck`
|
||||
|
||||
---
|
||||
|
||||
## 三、独立 Bug 修复
|
||||
|
||||
### 14. `unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_b.csv`
|
||||
|
||||
**改动**:10 条余量寄存器记录的 `DataType` 列从 `REAL` 改为 `FLOAT32`。
|
||||
|
||||
**原因**:`REAL` 是 IEC 61131-3 PLC 工程师惯用名称,但 pymodbus 的 `DATATYPE` 枚举只有 `FLOAT32`,`DataType['REAL']` 查表时抛 `KeyError: 'REAL'`,导致 `CoinCellAssemblyWorkstation` 节点启动失败。
|
||||
|
||||
---
|
||||
|
||||
## 四、运行期新增 Bug 修复(第二轮,2026-03-12 18:12 日志)
|
||||
|
||||
### 15. `unilabos/devices/workstation/bioyond_studio/station.py`
|
||||
|
||||
**改动**:第 261 行 `self.bioyond_config` → `self.workstation.bioyond_config`。
|
||||
|
||||
**原因**:`BioyondResourceSynchronizer.sync_to_external` 内部误用了 `self.bioyond_config`,而该类从未设置此属性(应通过 `self.workstation.bioyond_config` 访问)。触发场景:用户在前端将任意物料拖入仓库时,同步到 Bioyond 必定抛出 `AttributeError: 'BioyondResourceSynchronizer' object has no attribute 'bioyond_config'`。
|
||||
|
||||
---
|
||||
|
||||
### 16. `unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`
|
||||
|
||||
**改动**:`_get_type_id_by_name` 方法新增"直接英文 key 命中"分支:
|
||||
|
||||
- **原逻辑**:仅按 `value[0]`(中文名,如 `"5ml分液瓶板"`)遍历比较。
|
||||
- **新逻辑**:先以 `type_name` 直接查找 `material_type_mappings` 字典 key(英文 model 名,如 `"YB_Vial_5mL_Carrier"`),命中则立即返回 UUID;否则再按中文名兜底遍历。
|
||||
|
||||
**原因**:`resource_tree_transfer` 将 `plr_resource.model`(英文 key)作为 `board_type` / `bottle_type` 传给 `create_sample`,后者再调用 `_get_type_id_by_name`。旧版函数只按中文名查,导致英文 key 永远匹配不到 → `ValueError: 未找到板类型 'YB_Vial_5mL_Carrier' 的配置`。新函数兼容两种查找方式,同时保持向后兼容。
|
||||
|
||||
---
|
||||
|
||||
## 五、运行期新增 Bug 修复(第三轮,2026-03-12 20:30 日志)
|
||||
|
||||
### 17. `unilabos/resources/resource_tracker.py`(追加)
|
||||
|
||||
**改动**:在 `to_plr_resources` 中,`sub_cls.deserialize` 调用前新增 `_deduplicate_plr_dict(plr_dict)` 预处理函数。
|
||||
|
||||
**函数逻辑**:递归遍历整个 `plr_dict` 树,在**全树范围**对 `children` 列表按 `name` 去重——保留首次出现的同名节点,跳过重复项并打 `WARNING`。
|
||||
|
||||
**根本原因**:
|
||||
1. 用户通过前端将 `YB_Vial_5mL_Carrier` 拖入仓库 E01,carrier 及其子 vial(`YB_Vial_5mL_Carrier_vial_A1` 等)被写入数据库。
|
||||
2. 随后 `sync_from_external`(Bioyond 定期同步)以**新 UUID** 重新创建同名 carrier 并赋给同一槽位,PLR 内存树中的旧 carrier 被替换,但**数据库旧记录未被清除**。
|
||||
3. 下次重启时,数据库同一 `WareHouse` 下存在两条同名 `BottleCarrier`(不同 UUID),`node_to_plr_dict` 将二者都放入 `children` 列表,PLR 反序列化第二个 carrier 时子 vial 命名冲突,抛出 `ValueError: Resource with name 'YB_Vial_5mL_Carrier_vial_A1' already exists in the tree.`,整个 deck 无法加载,系统启动失败。
|
||||
|
||||
**连锁错误(随根因修复自动消除)**:
|
||||
- `TypeError: Deck.__init__() got an unexpected keyword argument 'data'` — deck 加载失败后 `driver_creator.py` 触发降级路径,参数类型错误
|
||||
- `AttributeError: 'ResourceDictInstance' object has no attribute 'copy'` — 另一条降级路径失败
|
||||
- `ValueError: Deck 配置不能为空` — 所有 deck 创建路径失败,`deck=None` 传入工作站
|
||||
|
||||
---
|
||||
|
||||
> **验证状态**:2026-03-12 20:56 日志确认系统正常运行,无新增 ERROR 级错误。
|
||||
|
||||
---
|
||||
|
||||
## 六、变更文件汇总(最终)
|
||||
|
||||
| 文件 | 变更类型 | 轮次 |
|
||||
|---|---|---|
|
||||
| `resources/battery/magazine.py` | 重构 + Bug 修复(极片子节点解耦 + 旧数据清理) | 第一轮 |
|
||||
| `resources/battery/bottle_carriers.py` | 重构(移除初始化时自动填瓶) | 第一轮 |
|
||||
| `resources/bioyond/decks.py` | 重构 + 重命名(BioyondElectrolyteDeck) | 第一轮 |
|
||||
| `devices/workstation/coin_cell_assembly/YB_YH_materials.py` | 重构 + 重命名(YihuaCoinCellDeck)+ 新增 electrolyte_buffer 槽位 | 第一轮 |
|
||||
| `resources/resource_tracker.py` | Bug 修复 × 3(Container 状态键预填 + 重复 UUID 自动修复 + 树级名称去重) | 第一/三轮 |
|
||||
| `resources/itemized_carrier.py` | Bug 修复(XY 近似坐标匹配,修复 Z 偏移) | 第一轮 |
|
||||
| `devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py` | 重构 + Bug 修复(跨站转运 + 类型映射双模式查找) | 第一/二轮 |
|
||||
| `devices/workstation/bioyond_studio/station.py` | Bug 修复(sync_to_external 属性访问路径) | 第二轮 |
|
||||
| `devices/workstation/coin_cell_assembly/coin_cell_assembly.py` | 新增 10 个 Modbus 余量属性 + 更新导入 | 第一轮 |
|
||||
| `yibin_electrolyte_config.json` | 配置更新(类名 + 移除 setup) | 第一轮 |
|
||||
| `yibin_coin_cell_only_config.json` | 配置更新(类名 + 移除 setup) | 第一轮 |
|
||||
| `yibin_electrolyte_only_config.json` | 配置更新(类名 + 移除 setup) | 第一轮 |
|
||||
| `registry/resources/bioyond/deck.yaml` | 注册表更新(类名 + 工厂函数路径) | 第一轮 |
|
||||
| `devices/workstation/coin_cell_assembly/coin_cell_assembly_b.csv` | Bug 修复(REAL → FLOAT32) | 第一轮 |
|
||||
@@ -130,20 +130,14 @@ class MaterialPlate(ItemizedResource[MaterialHole]):
|
||||
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: 型号
|
||||
"""
|
||||
@@ -153,42 +147,50 @@ class MaterialPlate(ItemizedResource[MaterialHole]):
|
||||
hole_diameter=20.0,
|
||||
info="",
|
||||
)
|
||||
# 创建4x4的洞位
|
||||
# TODO: 这里要改,对应不同形状
|
||||
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,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_with_holes(
|
||||
cls,
|
||||
name: str,
|
||||
size_x: float,
|
||||
size_y: float,
|
||||
size_z: float,
|
||||
category: str = "material_plate",
|
||||
model: Optional[str] = None,
|
||||
) -> "MaterialPlate":
|
||||
"""工厂方法:创建带 4x4 洞位的料板(仅用于初始 setup,不在反序列化路径调用)"""
|
||||
# 默认洞位间距(与 _unilabos_state 默认值保持一致)
|
||||
hole_spacing_x = 24.0
|
||||
hole_spacing_y = 24.0
|
||||
# 先建洞位,再作为 ordered_items 传入构造函数
|
||||
# (ItemizedResource.__init__ 要求 ordered_items 或 ordering 二选一必须有值)
|
||||
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, # 居中
|
||||
dx=(size_x - 4 * hole_spacing_x) / 2,
|
||||
dy=(size_y - 4 * 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,
|
||||
item_dx=hole_spacing_x,
|
||||
item_dy=hole_spacing_y,
|
||||
size_x=16,
|
||||
size_y=16,
|
||||
size_z=16,
|
||||
)
|
||||
return cls(
|
||||
name=name, size_x=size_x, size_y=size_y, size_z=size_z,
|
||||
ordered_items=holes, category=category, model=model,
|
||||
)
|
||||
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:调多次相加
|
||||
@@ -534,30 +536,19 @@ class WasteTipBox(Trash):
|
||||
return data
|
||||
|
||||
|
||||
class CoincellDeck(Deck):
|
||||
"""纽扣电池组装工作站台面类"""
|
||||
class YihuaCoinCellDeck(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
|
||||
size_x: float = 1450.0,
|
||||
size_y: float = 1450.0,
|
||||
size_z: float = 100.0,
|
||||
origin: Coordinate = Coordinate(-2200, 0, 0),
|
||||
category: str = "coin_cell_deck",
|
||||
setup: bool = False, # 是否自动执行 setup
|
||||
setup: bool = False,
|
||||
):
|
||||
"""初始化纽扣电池组装工作站台面
|
||||
|
||||
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,
|
||||
@@ -591,14 +582,11 @@ class CoincellDeck(Deck):
|
||||
# ====================================== 物料板 ============================================
|
||||
# 创建物料板(料盘carrier)- 4x4布局
|
||||
# 负极料盘
|
||||
fujiliaopan = MaterialPlate(name="负极料盘", size_x=120, size_y=100, size_z=10.0, fill=True)
|
||||
fujiliaopan = MaterialPlate.create_with_holes(name="负极料盘", size_x=120, size_y=100, size_z=10.0)
|
||||
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)
|
||||
gemoliaopan = MaterialPlate.create_with_holes(name="隔膜料盘", size_x=120, size_y=100, size_z=10.0)
|
||||
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)
|
||||
@@ -633,11 +621,27 @@ class CoincellDeck(Deck):
|
||||
waste_tip_box = WasteTipBox(name="waste_tip_box")
|
||||
self.assign_child_resource(waste_tip_box, Coordinate(x=778.0, y=622.0, z=0))
|
||||
|
||||
# 分液瓶板接驳区 - 接收来自 BioyondElectrolyte 侧的完整 Vial Carrier 板
|
||||
# 命名 electrolyte_buffer 与 bioyond_cell_workstation.py 中 sites=["electrolyte_buffer"] 对应
|
||||
electrolyte_buffer = ResourceStack(
|
||||
name="electrolyte_buffer",
|
||||
direction="z",
|
||||
resources=[],
|
||||
)
|
||||
self.assign_child_resource(electrolyte_buffer, Coordinate(x=1050.0, y=700.0, z=0))
|
||||
|
||||
def YH_Deck(name=""):
|
||||
cd = CoincellDeck(name=name)
|
||||
cd.setup()
|
||||
return cd
|
||||
|
||||
def yihua_coin_cell_deck(name: str = "coin_cell_deck") -> YihuaCoinCellDeck:
|
||||
deck = YihuaCoinCellDeck(name=name)
|
||||
deck.setup()
|
||||
return deck
|
||||
|
||||
|
||||
# 向后兼容别名,日后废弃
|
||||
CoincellDeck = YihuaCoinCellDeck
|
||||
|
||||
def YH_Deck(name: str = "") -> YihuaCoinCellDeck:
|
||||
return yihua_coin_cell_deck(name=name or "coin_cell_deck")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -17,7 +17,7 @@ from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNo
|
||||
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import *
|
||||
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
|
||||
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
||||
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import CoincellDeck
|
||||
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import YihuaCoinCellDeck, yihua_coin_cell_deck
|
||||
from unilabos.resources.graphio import convert_resources_to_type
|
||||
from unilabos.utils.log import logger
|
||||
import struct
|
||||
@@ -161,7 +161,9 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
logger.info("没有传入依华deck,检查启动json文件")
|
||||
super().__init__(deck=deck, *args, **kwargs,)
|
||||
self.debug_mode = debug_mode
|
||||
|
||||
self._modbus_address = address
|
||||
self._modbus_port = port
|
||||
|
||||
""" 连接初始化 """
|
||||
modbus_client = TCPClient(addr=address, port=port)
|
||||
logger.debug(f"创建 Modbus 客户端: {modbus_client}")
|
||||
@@ -178,9 +180,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
raise ValueError('modbus tcp connection failed')
|
||||
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_b.csv'))
|
||||
self.client = modbus_client.register_node_list(self.nodes)
|
||||
self._modbus_client_raw = modbus_client
|
||||
else:
|
||||
print("测试模式,跳过连接")
|
||||
self.nodes, self.client = None, None
|
||||
self._modbus_client_raw = None
|
||||
|
||||
""" 工站的配置 """
|
||||
|
||||
@@ -191,9 +195,40 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
self.csv_export_file = None
|
||||
self.coin_num_N = 0 #已组装电池数量
|
||||
|
||||
def _ensure_modbus_connected(self) -> None:
|
||||
"""检查 Modbus TCP 连接是否存活,若已断开则自动重连(防止长时间空闲后连接超时)"""
|
||||
if self.debug_mode or self._modbus_client_raw is None:
|
||||
return
|
||||
raw_client = self._modbus_client_raw.client
|
||||
if raw_client.is_socket_open():
|
||||
return
|
||||
logger.warning("[Modbus] 检测到连接已断开,尝试重连...")
|
||||
try:
|
||||
raw_client.close()
|
||||
except Exception:
|
||||
pass
|
||||
count = 10
|
||||
while count > 0:
|
||||
count -= 1
|
||||
try:
|
||||
raw_client.connect()
|
||||
except Exception:
|
||||
pass
|
||||
if raw_client.is_socket_open():
|
||||
break
|
||||
time.sleep(2)
|
||||
if not raw_client.is_socket_open():
|
||||
raise RuntimeError(f"Modbus TCP 重连失败({self._modbus_address}:{self._modbus_port}),请检查设备连接")
|
||||
logger.info("[Modbus] 重连成功")
|
||||
|
||||
def post_init(self, ros_node: ROS2WorkstationNode):
|
||||
self._ros_node = ros_node
|
||||
#self.deck = create_a_coin_cell_deck()
|
||||
|
||||
# Deck 为空时(反序列化未恢复子节点),主动调用 setup() 初始化子物料
|
||||
if self.deck and not self.deck.children and hasattr(self.deck, "setup") and callable(self.deck.setup):
|
||||
logger.info("YihuaCoinCellDeck 无子节点,调用 setup() 初始化")
|
||||
self.deck.setup()
|
||||
|
||||
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
||||
"resources": [self.deck]
|
||||
})
|
||||
@@ -623,12 +658,28 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
return vol
|
||||
|
||||
@property
|
||||
def data_coin_num(self) -> int:
|
||||
"""当前电池数量 (INT16)"""
|
||||
def data_coin_type(self) -> int:
|
||||
"""电池类型 - 7种或8种组装物料 (INT16)"""
|
||||
if self.debug_mode:
|
||||
return 7
|
||||
coin_type, read_err = self.client.use_node('REG_DATA_COIN_TYPE').read(1)
|
||||
return coin_type
|
||||
|
||||
@property
|
||||
def data_current_assembling_count(self) -> int:
|
||||
"""当前进行组装的电池数量 - Current assembling battery count (INT16)"""
|
||||
if self.debug_mode:
|
||||
return 0
|
||||
num, read_err = self.client.use_node('REG_DATA_COIN_NUM').read(1)
|
||||
return num
|
||||
count, read_err = self.client.use_node('REG_DATA_CURRENT_ASSEMBLING_COUNT').read(1)
|
||||
return count
|
||||
|
||||
@property
|
||||
def data_current_completed_count(self) -> int:
|
||||
"""当前完成组装的电池数量 - Current completed battery count (INT16)"""
|
||||
if self.debug_mode:
|
||||
return 0
|
||||
count, read_err = self.client.use_node('REG_DATA_CURRENT_COMPLETED_COUNT').read(1)
|
||||
return count
|
||||
|
||||
@property
|
||||
def data_coin_cell_code(self) -> str:
|
||||
@@ -726,6 +777,116 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
return 0.0
|
||||
return _decode_float32_correct(result.registers)
|
||||
|
||||
@property
|
||||
def data_10mm_positive_plate_remaining(self) -> float:
|
||||
"""10mm正极片剩余物料数量 (FLOAT32)"""
|
||||
if self.debug_mode:
|
||||
return 0.0
|
||||
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_10MM_POSITIVE_PLATE_REMAINING_COUNT').address, count=2)
|
||||
if result.isError():
|
||||
logger.error("读取10mm正极片余量失败")
|
||||
return 0.0
|
||||
return _decode_float32_correct(result.registers)
|
||||
|
||||
@property
|
||||
def data_12mm_positive_plate_remaining(self) -> float:
|
||||
"""12mm正极片剩余物料数量 (FLOAT32)"""
|
||||
if self.debug_mode:
|
||||
return 0.0
|
||||
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_12MM_POSITIVE_PLATE_REMAINING_COUNT').address, count=2)
|
||||
if result.isError():
|
||||
logger.error("读取12mm正极片余量失败")
|
||||
return 0.0
|
||||
return _decode_float32_correct(result.registers)
|
||||
|
||||
@property
|
||||
def data_16mm_positive_plate_remaining(self) -> float:
|
||||
"""16mm正极片剩余物料数量 (FLOAT32)"""
|
||||
if self.debug_mode:
|
||||
return 0.0
|
||||
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_16MM_POSITIVE_PLATE_REMAINING_COUNT').address, count=2)
|
||||
if result.isError():
|
||||
logger.error("读取16mm正极片余量失败")
|
||||
return 0.0
|
||||
return _decode_float32_correct(result.registers)
|
||||
|
||||
@property
|
||||
def data_aluminum_foil_remaining(self) -> float:
|
||||
"""铝箔剩余物料数量 (FLOAT32)"""
|
||||
if self.debug_mode:
|
||||
return 0.0
|
||||
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_ALUMINUM_FOIL_REMAINING_COUNT').address, count=2)
|
||||
if result.isError():
|
||||
logger.error("读取铝箔余量失败")
|
||||
return 0.0
|
||||
return _decode_float32_correct(result.registers)
|
||||
|
||||
@property
|
||||
def data_positive_shell_remaining(self) -> float:
|
||||
"""正极壳剩余物料数量 (FLOAT32)"""
|
||||
if self.debug_mode:
|
||||
return 0.0
|
||||
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_POSITIVE_SHELL_REMAINING_COUNT').address, count=2)
|
||||
if result.isError():
|
||||
logger.error("读取正极壳余量失败")
|
||||
return 0.0
|
||||
return _decode_float32_correct(result.registers)
|
||||
|
||||
@property
|
||||
def data_flat_washer_remaining(self) -> float:
|
||||
"""平垫剩余物料数量 (FLOAT32)"""
|
||||
if self.debug_mode:
|
||||
return 0.0
|
||||
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_FLAT_WASHER_REMAINING_COUNT').address, count=2)
|
||||
if result.isError():
|
||||
logger.error("读取平垫余量失败")
|
||||
return 0.0
|
||||
return _decode_float32_correct(result.registers)
|
||||
|
||||
@property
|
||||
def data_negative_shell_remaining(self) -> float:
|
||||
"""负极壳剩余物料数量 (FLOAT32)"""
|
||||
if self.debug_mode:
|
||||
return 0.0
|
||||
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_NEGATIVE_SHELL_REMAINING_COUNT').address, count=2)
|
||||
if result.isError():
|
||||
logger.error("读取负极壳余量失败")
|
||||
return 0.0
|
||||
return _decode_float32_correct(result.registers)
|
||||
|
||||
@property
|
||||
def data_spring_washer_remaining(self) -> float:
|
||||
"""弹垫剩余物料数量 (FLOAT32)"""
|
||||
if self.debug_mode:
|
||||
return 0.0
|
||||
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_SPRING_WASHER_REMAINING_COUNT').address, count=2)
|
||||
if result.isError():
|
||||
logger.error("读取弹垫余量失败")
|
||||
return 0.0
|
||||
return _decode_float32_correct(result.registers)
|
||||
|
||||
@property
|
||||
def data_finished_battery_remaining_capacity(self) -> float:
|
||||
"""成品电池剩余可容纳数量 (FLOAT32)"""
|
||||
if self.debug_mode:
|
||||
return 0.0
|
||||
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_FINISHED_BATTERY_REMAINING_CAPACITY').address, count=2)
|
||||
if result.isError():
|
||||
logger.error("读取成品电池余量失败")
|
||||
return 0.0
|
||||
return _decode_float32_correct(result.registers)
|
||||
|
||||
@property
|
||||
def data_finished_battery_ng_remaining_capacity(self) -> float:
|
||||
"""成品电池NG槽剩余可容纳数量 (FLOAT32)"""
|
||||
if self.debug_mode:
|
||||
return 0.0
|
||||
result = self.client.client.read_holding_registers(address=self.client.use_node('REG_DATA_FINISHED_BATTERY_NG_REMAINING_CAPACITY').address, count=2)
|
||||
if result.isError():
|
||||
logger.error("读取成品电池NG槽余量失败")
|
||||
return 0.0
|
||||
return _decode_float32_correct(result.registers)
|
||||
|
||||
# @property
|
||||
# def data_stack_vision_code(self) -> int:
|
||||
# """物料堆叠复检图片编码 (INT16)"""
|
||||
@@ -925,6 +1086,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
|
||||
# 步骤0: 前置条件检查
|
||||
logger.info("\n【步骤 0/4】前置条件检查...")
|
||||
self._ensure_modbus_connected()
|
||||
try:
|
||||
# 检查 REG_UNILAB_INTERACT (应该为False,表示使用Unilab交互)
|
||||
unilab_interact_node = self.client.use_node('REG_UNILAB_INTERACT')
|
||||
@@ -985,6 +1147,42 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
logger.info(" ✓ COIL_GB_L_IGNORE_CMD 检查通过 (值为False,使用左手套箱)")
|
||||
|
||||
# 检查握手寄存器残留(正常初始状态均应为False)
|
||||
# 若上次运行意外断网,这些Unilab侧COIL可能被遗留为True,导致PLC逻辑卡死
|
||||
handshake_checks = [
|
||||
("COIL_UNILAB_SEND_MSG_SUCC_CMD", "Unilab→PLC 配方发送完毕", "上次配方握手未正常复位,PLC可能处于等待配方的卡死状态"),
|
||||
("COIL_UNILAB_REC_MSG_SUCC_CMD", "Unilab→PLC 数据接收完毕", "上次数据接收握手未正常复位"),
|
||||
("UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM", "Unilab→PLC 瓶数发送完毕", "上次瓶数握手未正常复位"),
|
||||
("UNILAB_SEND_FINISHED_CMD", "Unilab→PLC 一组完成确认", "上次完成握手未正常复位"),
|
||||
("COIL_REQUEST_REC_MSG_STATUS", "PLC→Unilab 请求接收配方", "PLC正处于等待配方状态,设备流程已卡死,需重启PLC或手动复位握手"),
|
||||
("COIL_REQUEST_SEND_MSG_STATUS", "PLC→Unilab 请求发送测试数据", "PLC正处于等待发送数据状态,设备流程已卡死"),
|
||||
]
|
||||
for coil_name, coil_desc, stuck_reason in handshake_checks:
|
||||
try:
|
||||
hs_node = self.client.use_node(coil_name)
|
||||
hs_value, hs_err = hs_node.read(1)
|
||||
if hs_err:
|
||||
logger.warning(f" ⚠ 无法读取 {coil_name},跳过此项检查")
|
||||
continue
|
||||
hs_actual = hs_value[0] if isinstance(hs_value, (list, tuple)) else hs_value
|
||||
logger.info(f" {coil_name} 当前值: {hs_actual}")
|
||||
if hs_actual:
|
||||
error_msg = (
|
||||
"❌ 前置握手寄存器检查失败!\n"
|
||||
f" {coil_name} = True (期望值: False)\n"
|
||||
f" 含义: {coil_desc}\n"
|
||||
f" 原因: {stuck_reason}\n"
|
||||
" 建议: 检查上次运行是否意外中断,手动将该寄存器置为False后重试"
|
||||
)
|
||||
logger.error(error_msg)
|
||||
raise RuntimeError(error_msg)
|
||||
logger.info(f" ✓ {coil_name} 检查通过 (值为False)")
|
||||
except RuntimeError:
|
||||
raise
|
||||
except Exception as hs_e:
|
||||
logger.warning(f" ⚠ 检查 {coil_name} 时发生异常: {hs_e},跳过此项")
|
||||
|
||||
logger.info("✓ 所有前置条件检查通过!")
|
||||
|
||||
except ValueError as e:
|
||||
@@ -1158,7 +1356,8 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
lvbodian: bool = True,
|
||||
battery_pressure_mode: bool = True,
|
||||
battery_clean_ignore: bool = False,
|
||||
file_path: str = "/Users/sml/work"
|
||||
file_path: str = "/Users/sml/work",
|
||||
formulations: List[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
发送瓶数+简化组装函数(适用于第二批次及后续批次)
|
||||
@@ -1185,17 +1384,44 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
battery_pressure_mode: 是否启用压力模式
|
||||
battery_clean_ignore: 是否忽略电池清洁
|
||||
file_path: 实验记录保存路径
|
||||
formulations: 配方信息列表(从 create_orders.mass_ratios 获取)
|
||||
包含 orderCode, target_mass_ratio, real_mass_ratio 等
|
||||
用于CSV数据追溯,可选参数
|
||||
|
||||
Returns:
|
||||
dict: 包含组装结果的字典
|
||||
|
||||
注意:
|
||||
注意:
|
||||
- 第一次启动需先调用 func_pack_device_init_auto_start_combined()
|
||||
- 后续批次直接调用此函数即可
|
||||
"""
|
||||
logger.info("=" * 60)
|
||||
logger.info("开始发送瓶数+简化组装流程...")
|
||||
logger.info(f"电解液瓶数: {elec_num}, 每瓶电池数: {elec_use_num}")
|
||||
|
||||
# 存储配方信息到设备状态(供 CSV 写入使用)
|
||||
if formulations:
|
||||
logger.info(f"接收到配方信息: {len(formulations)} 条")
|
||||
# 将配方信息按 orderCode 索引,方便后续查找
|
||||
self._formulations_map = {
|
||||
f["orderCode"]: f for f in formulations
|
||||
} if formulations else {}
|
||||
# ✅ 新增:存储配方列表(按接收顺序),用于索引访问
|
||||
self._formulations_list = formulations
|
||||
else:
|
||||
logger.warning("未接收到配方信息,CSV将不包含配方字段")
|
||||
self._formulations_map = {}
|
||||
self._formulations_list = []
|
||||
|
||||
# ✅ 新增:存储每瓶电池数,用于计算当前使用的瓶号
|
||||
# ⚠️ 确保转换为整数(前端可能传递字符串)
|
||||
self._elec_use_num = int(elec_use_num) if elec_use_num else 0
|
||||
logger.info(f"已存储参数: 每瓶电池数={self._elec_use_num}, 配方数={len(self._formulations_list)}")
|
||||
|
||||
# ✅ 新增:软件层电池计数器(防止硬件计数器不准确)
|
||||
self._software_battery_counter = 0 # 从0开始,每写入一次CSV递增
|
||||
logger.info("软件层电池计数器已初始化")
|
||||
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 步骤1: 发送电解液瓶数(触发物料搬运)
|
||||
@@ -1331,7 +1557,8 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
data_assembly_time = self.data_assembly_time
|
||||
data_assembly_pressure = self.data_assembly_pressure
|
||||
data_electrolyte_volume = self.data_electrolyte_volume
|
||||
data_coin_num = self.data_coin_num
|
||||
data_coin_type = self.data_coin_type # 电池类型(7或8种物料)
|
||||
data_battery_number = self.data_current_assembling_count # ✅ 真正的电池编号
|
||||
|
||||
# 处理电解液二维码 - 确保是字符串类型
|
||||
try:
|
||||
@@ -1361,28 +1588,32 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
logger.debug(f"data_assembly_time: {data_assembly_time}")
|
||||
logger.debug(f"data_assembly_pressure: {data_assembly_pressure}")
|
||||
logger.debug(f"data_electrolyte_volume: {data_electrolyte_volume}")
|
||||
logger.debug(f"data_coin_num: {data_coin_num}")
|
||||
logger.debug(f"data_coin_type: {data_coin_type}") # 电池类型
|
||||
logger.debug(f"data_battery_number: {data_battery_number}") # ✅ 电池编号
|
||||
logger.debug(f"data_electrolyte_code: {data_electrolyte_code}")
|
||||
logger.debug(f"data_coin_cell_code: {data_coin_cell_code}")
|
||||
#接收完信息后,读取完毕标志位置True
|
||||
liaopan3 = self.deck.get_resource("成品弹夹")
|
||||
finished_battery_magazine = self.deck.get_resource("成品弹夹")
|
||||
|
||||
# 计算电池应该放在哪个洞,以及洞内的堆叠位置
|
||||
# 成品弹夹有6个洞,每个洞可堆叠20颗电池
|
||||
# 前5个洞(索引0-4)放正常电池,第6个洞(索引5)放NG电池
|
||||
BATTERIES_PER_HOLE = 20
|
||||
MAX_NORMAL_BATTERIES = 100 # 5个洞 × 20颗/洞
|
||||
|
||||
hole_index = self.coin_num_N // BATTERIES_PER_HOLE # 第几个洞(0-4为正常电池)
|
||||
in_hole_position = self.coin_num_N % BATTERIES_PER_HOLE # 洞内的堆叠序号
|
||||
|
||||
if hole_index >= 5:
|
||||
logger.error(f"电池数量超出正常容量范围: {self.coin_num_N + 1} > {MAX_NORMAL_BATTERIES}")
|
||||
raise ValueError(f"成品弹夹正常洞位已满(最多{MAX_NORMAL_BATTERIES}颗),当前尝试放置第{self.coin_num_N + 1}颗")
|
||||
|
||||
target_hole = finished_battery_magazine.children[hole_index] # 获取目标洞
|
||||
|
||||
# 生成唯一的电池名称(使用时间戳确保唯一性)
|
||||
timestamp_suffix = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||
battery_name = f"battery_{self.coin_num_N}_{timestamp_suffix}"
|
||||
|
||||
# 检查目标位置是否已有资源,如果有则先卸载
|
||||
target_slot = liaopan3.children[self.coin_num_N]
|
||||
if target_slot.children:
|
||||
logger.warning(f"位置 {self.coin_num_N} 已有资源,将先卸载旧资源")
|
||||
try:
|
||||
# 卸载所有现有子资源
|
||||
for child in list(target_slot.children):
|
||||
target_slot.unassign_child_resource(child)
|
||||
logger.info(f"已卸载旧资源: {child.name}")
|
||||
except Exception as e:
|
||||
logger.error(f"卸载旧资源时出错: {e}")
|
||||
|
||||
# 创建新的电池资源
|
||||
battery = ElectrodeSheet(name=battery_name, size_x=14, size_y=14, size_z=2)
|
||||
battery._unilabos_state = {
|
||||
@@ -1393,13 +1624,12 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
"electrolyte_volume": data_electrolyte_volume
|
||||
}
|
||||
|
||||
# 分配新资源到目标位置
|
||||
# 将电池堆叠到目标洞中
|
||||
try:
|
||||
target_slot.assign_child_resource(battery, location=None)
|
||||
logger.info(f"成功分配电池 {battery_name} 到位置 {self.coin_num_N}")
|
||||
target_hole.assign_child_resource(battery, location=None)
|
||||
logger.info(f"成功放置电池 {battery_name} 到弹夹洞{hole_index}的第{in_hole_position + 1}层 (总计第{self.coin_num_N + 1}颗)")
|
||||
except Exception as e:
|
||||
logger.error(f"分配电池资源失败: {e}")
|
||||
# 如果分配失败,尝试使用更简单的方法
|
||||
logger.error(f"放置电池资源失败: {e}")
|
||||
raise
|
||||
|
||||
#print(jipian2.parent)
|
||||
@@ -1420,6 +1650,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
time_date = datetime.now().strftime("%Y%m%d")
|
||||
#秒级时间戳用于标记每一行电池数据
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
self._last_assembly_timestamp = timestamp
|
||||
#生成输出文件的变量
|
||||
self.csv_export_file = os.path.join(file_path, f"date_{time_date}.csv")
|
||||
#将数据存入csv文件
|
||||
@@ -1430,17 +1661,79 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
writer.writerow([
|
||||
'Time', 'open_circuit_voltage', 'pole_weight',
|
||||
'assembly_time', 'assembly_pressure', 'electrolyte_volume',
|
||||
'coin_num', 'electrolyte_code', 'coin_cell_code'
|
||||
'data_coin_type', 'electrolyte_code', 'coin_cell_code',
|
||||
'orderName', 'prep_bottle_barcode', 'vial_bottle_barcodes',
|
||||
'target_mass_ratio', 'real_mass_ratio'
|
||||
])
|
||||
#立刻写入磁盘
|
||||
csvfile.flush()
|
||||
#开始追加电池信息
|
||||
with open(self.csv_export_file, 'a', newline='', encoding='utf-8') as csvfile:
|
||||
writer = csv.writer(csvfile)
|
||||
|
||||
# ========== 提取配方信息 ==========
|
||||
formulation_order_name = ""
|
||||
prep_bottle_barcode = ""
|
||||
vial_bottle_barcodes = ""
|
||||
target_ratio_str = ""
|
||||
real_ratio_str = ""
|
||||
|
||||
# 从 self._formulations_list 获取配方信息
|
||||
if hasattr(self, '_formulations_list') and self._formulations_list:
|
||||
# ✅ 新方案:根据电池编号和每瓶电池数计算当前瓶号
|
||||
# 例如:elec_use_num=2时,电池1-2用瓶0,电池3-4用瓶1
|
||||
if hasattr(self, '_elec_use_num') and self._elec_use_num:
|
||||
# ⚠️ 确保转换为整数(防御性编程)
|
||||
elec_use_num_int = int(self._elec_use_num) if self._elec_use_num else 1
|
||||
if elec_use_num_int > 0:
|
||||
current_bottle_index = (data_battery_number - 1) // elec_use_num_int
|
||||
else:
|
||||
current_bottle_index = 0
|
||||
|
||||
logger.debug(
|
||||
f"[CSV写入] 电池 {data_battery_number}: 计算瓶号索引={current_bottle_index} "
|
||||
f"(每瓶{self._elec_use_num}颗电池)"
|
||||
)
|
||||
else:
|
||||
# 降级方案:尝试从二维码解析(仅当参数未设置时)
|
||||
current_bottle_index = int(data_electrolyte_code.split('-')[-1]) if '-' in str(data_electrolyte_code) else 0
|
||||
logger.debug(
|
||||
f"[CSV写入] 电池 {data_battery_number}: 从二维码解析瓶号索引={current_bottle_index}"
|
||||
)
|
||||
|
||||
# 从配方列表中获取对应配方
|
||||
if 0 <= current_bottle_index < len(self._formulations_list):
|
||||
formulation = self._formulations_list[current_bottle_index]
|
||||
formulation_order_name = formulation.get("orderName", "")
|
||||
prep_bottle_barcode = formulation.get("prep_bottle_barcode", "")
|
||||
vial_bottle_barcodes = formulation.get("vial_bottle_barcodes", "")
|
||||
|
||||
real_ratio = formulation.get("real_mass_ratio", {})
|
||||
target_ratio = formulation.get("target_mass_ratio", {})
|
||||
|
||||
# 将配方比例转为JSON字符串
|
||||
import json
|
||||
target_ratio_str = json.dumps(target_ratio, ensure_ascii=False) if target_ratio else ""
|
||||
real_ratio_str = json.dumps(real_ratio, ensure_ascii=False) if real_ratio else ""
|
||||
|
||||
logger.info(
|
||||
f"[CSV写入] 电池 {data_battery_number}: 使用配方[{current_bottle_index}] "
|
||||
f"orderName={formulation_order_name}, 配液瓶={prep_bottle_barcode}, 分液瓶={vial_bottle_barcodes}"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[CSV写入] 电池 {data_battery_number}: 瓶号索引 {current_bottle_index} "
|
||||
f"超出配方列表范围 (共{len(self._formulations_list)}个配方)"
|
||||
)
|
||||
else:
|
||||
logger.debug(f"[CSV写入] 电池 {data_battery_number}: 未找到配方信息数据")
|
||||
|
||||
writer.writerow([
|
||||
timestamp, data_open_circuit_voltage, data_pole_weight,
|
||||
data_assembly_time, data_assembly_pressure, data_electrolyte_volume,
|
||||
data_coin_num, data_electrolyte_code, data_coin_cell_code
|
||||
data_coin_type, data_electrolyte_code, data_coin_cell_code,
|
||||
formulation_order_name, prep_bottle_barcode, vial_bottle_barcodes,
|
||||
target_ratio_str, real_ratio_str
|
||||
])
|
||||
#立刻写入磁盘
|
||||
csvfile.flush()
|
||||
@@ -1585,17 +1878,18 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
pole_weight = 0.0
|
||||
|
||||
battery_info = {
|
||||
"battery_index": coin_num_N + 1,
|
||||
"battery_barcode": battery_qr_code,
|
||||
"electrolyte_barcode": electrolyte_qr_code,
|
||||
"Time": getattr(self, "_last_assembly_timestamp", datetime.now().strftime("%Y%m%d_%H%M%S")),
|
||||
"open_circuit_voltage": open_circuit_voltage,
|
||||
"pole_weight": pole_weight,
|
||||
"assembly_time": self.data_assembly_time,
|
||||
"assembly_pressure": self.data_assembly_pressure,
|
||||
"electrolyte_volume": self.data_electrolyte_volume
|
||||
"electrolyte_volume": self.data_electrolyte_volume,
|
||||
"data_coin_type": getattr(self, "data_coin_type", 0),
|
||||
"electrolyte_code": electrolyte_qr_code,
|
||||
"coin_cell_code": battery_qr_code,
|
||||
}
|
||||
battery_data_list.append(battery_info)
|
||||
print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['battery_barcode']}, 电解液码={battery_info['electrolyte_barcode']}")
|
||||
print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['coin_cell_code']}, 电解液码={battery_info['electrolyte_code']}")
|
||||
|
||||
time.sleep(1)
|
||||
# TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑
|
||||
@@ -1624,6 +1918,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
"success": True,
|
||||
"total_batteries": len(battery_data_list),
|
||||
"batteries": battery_data_list,
|
||||
"assembly_data": battery_data_list,
|
||||
"summary": {
|
||||
"electrolyte_bottles_used": elec_num,
|
||||
"batteries_per_bottle": elec_use_num,
|
||||
@@ -1667,8 +1962,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
file_path: str = "/Users/sml/work"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
简化版电池组装函数,整合了原 qiming_coin_cell_code 的参数设置和双滴模式
|
||||
|
||||
|
||||
此函数是 func_allpack_cmd 的增强版本,自动处理以下配置:
|
||||
- 负极片和隔膜的盘数及矩阵点位
|
||||
- 枪头盒矩阵点位
|
||||
@@ -1839,17 +2133,18 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
pole_weight = 0.0
|
||||
|
||||
battery_info = {
|
||||
"battery_index": coin_num_N + 1,
|
||||
"battery_barcode": battery_qr_code,
|
||||
"electrolyte_barcode": electrolyte_qr_code,
|
||||
"Time": getattr(self, "_last_assembly_timestamp", datetime.now().strftime("%Y%m%d_%H%M%S")),
|
||||
"open_circuit_voltage": open_circuit_voltage,
|
||||
"pole_weight": pole_weight,
|
||||
"assembly_time": self.data_assembly_time,
|
||||
"assembly_pressure": self.data_assembly_pressure,
|
||||
"electrolyte_volume": self.data_electrolyte_volume
|
||||
"electrolyte_volume": self.data_electrolyte_volume,
|
||||
"data_coin_type": getattr(self, "data_coin_type", 0),
|
||||
"electrolyte_code": electrolyte_qr_code,
|
||||
"coin_cell_code": battery_qr_code,
|
||||
}
|
||||
battery_data_list.append(battery_info)
|
||||
print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['battery_barcode']}, 电解液码={battery_info['electrolyte_barcode']}")
|
||||
print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['coin_cell_code']}, 电解液码={battery_info['electrolyte_code']}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
@@ -1876,6 +2171,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
"success": True,
|
||||
"total_batteries": len(battery_data_list),
|
||||
"batteries": battery_data_list,
|
||||
"assembly_data": battery_data_list,
|
||||
"summary": {
|
||||
"electrolyte_bottles_used": elec_num,
|
||||
"batteries_per_bottle": elec_use_num,
|
||||
@@ -1922,7 +2218,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
|
||||
def fun_wuliao_test(self) -> bool:
|
||||
#找到data_init中构建的2个物料盘
|
||||
liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8")
|
||||
test_battery_plate = self.deck.get_resource("\u7535\u6c60\u6599\u76d8")
|
||||
for i in range(16):
|
||||
battery = ElectrodeSheet(name=f"battery_{i}", size_x=16, size_y=16, size_z=2)
|
||||
battery._unilabos_state = {
|
||||
@@ -1932,7 +2228,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
"electrolyte_volume": 20.0,
|
||||
"electrolyte_name": f"DP{i}"
|
||||
}
|
||||
liaopan3.children[i].assign_child_resource(battery, location=None)
|
||||
test_battery_plate.children[i].assign_child_resource(battery, location=None)
|
||||
|
||||
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
||||
"resources": [self.deck]
|
||||
@@ -1975,7 +2271,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
data_assembly_time = self.data_assembly_time
|
||||
data_assembly_pressure = self.data_assembly_pressure
|
||||
data_electrolyte_volume = self.data_electrolyte_volume
|
||||
data_coin_num = self.data_coin_num
|
||||
data_coin_type = self.data_coin_type # 电池类型(7或8种物料)
|
||||
data_electrolyte_code = self.data_electrolyte_code
|
||||
data_coin_cell_code = self.data_coin_cell_code
|
||||
# 电解液瓶位置
|
||||
@@ -2089,7 +2385,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
writer.writerow([
|
||||
timestamp, data_open_circuit_voltage, data_pole_weight,
|
||||
data_assembly_time, data_assembly_pressure, data_electrolyte_volume,
|
||||
data_coin_num, data_electrolyte_code, data_coin_cell_code
|
||||
data_coin_type, data_electrolyte_code, data_coin_cell_code # ✅ 已修正
|
||||
])
|
||||
#立刻写入磁盘
|
||||
csvfile.flush()
|
||||
@@ -2140,7 +2436,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 简单测试
|
||||
workstation = CoinCellAssemblyWorkstation(deck=CoincellDeck(setup=True, name="coin_cell_deck"))
|
||||
workstation = CoinCellAssemblyWorkstation(deck=yihua_coin_cell_deck(name="coin_cell_deck"))
|
||||
# workstation.qiming_coin_cell_code(fujipian_panshu=1, fujipian_juzhendianwei=2, gemopanshu=3, gemo_juzhendianwei=4, lvbodian=False, battery_pressure_mode=False, battery_pressure=4200, battery_clean_ignore=False)
|
||||
# print(f"工作站创建成功: {workstation.deck.name}")
|
||||
# print(f"料盘数量: {len(workstation.deck.children)}")
|
||||
|
||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
|
||||
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,
|
||||
@@ -29,7 +29,9 @@ 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_COIN_TYPE,INT16,,,,hold_register,10018,data_coin_type
|
||||
REG_DATA_CURRENT_ASSEMBLING_COUNT,INT16,,,,hold_register,10072,data_current_assembling_count
|
||||
REG_DATA_CURRENT_COMPLETED_COUNT,INT16,,,,hold_register,10074,data_current_completed_count
|
||||
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()
|
||||
@@ -69,65 +71,75 @@ 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-电解液瓶盖在籍异常
|
||||
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-?????????
|
||||
REG_DATA_10MM_POSITIVE_PLATE_REMAINING_COUNT,FLOAT32,,,,hold_register,520,10mm??????????R?
|
||||
REG_DATA_12MM_POSITIVE_PLATE_REMAINING_COUNT,FLOAT32,,,,hold_register,522,12mm??????????R?
|
||||
REG_DATA_16MM_POSITIVE_PLATE_REMAINING_COUNT,FLOAT32,,,,hold_register,524,16mm??????????R?
|
||||
REG_DATA_ALUMINUM_FOIL_REMAINING_COUNT,FLOAT32,,,,hold_register,526,?????????R?
|
||||
REG_DATA_POSITIVE_SHELL_REMAINING_COUNT,FLOAT32,,,,hold_register,528,??????????R?
|
||||
REG_DATA_FLAT_WASHER_REMAINING_COUNT,FLOAT32,,,,hold_register,530,?????????R?
|
||||
REG_DATA_NEGATIVE_SHELL_REMAINING_COUNT,FLOAT32,,,,hold_register,532,??????????R?
|
||||
REG_DATA_SPRING_WASHER_REMAINING_COUNT,FLOAT32,,,,hold_register,534,?????????R?
|
||||
REG_DATA_FINISHED_BATTERY_REMAINING_CAPACITY,FLOAT32,,,,hold_register,536,????????????R?
|
||||
REG_DATA_FINISHED_BATTERY_NG_REMAINING_CAPACITY,FLOAT32,,,,hold_register,538,????NG?????????R?
|
||||
|
||||
|
88
unilabos/devices/workstation/implementation_plan.md
Normal file
88
unilabos/devices/workstation/implementation_plan.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 物料系统标准化重构方案
|
||||
|
||||
根据开发者的反馈,本方案旨在遵循“标准化而非绕过”的原则,对资源类(Deck、Carrier、Magazine)进行重构。核心目标是将物理结构的初始化与物料/极片的初始填充逻辑解耦,彻底解决反序列化过程中的初始化冲突。
|
||||
|
||||
## 拟议变更
|
||||
|
||||
### [参考] PRCXI9300 标准化模式
|
||||
#### [参考文件] [prcxi.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/liquid_handling/prcxi/prcxi.py)
|
||||
* **PRCXI9300Deck**: 演示了如何在 `serialize` 中导出 `sites` 元数据,以及如何在 `assign_child_resource` 中实现稳健的槽位匹配(支持按名称、坐标或索引匹配)。
|
||||
* **PRCXI9300Container**: 演示了标准的 `load_state` 和 `serialize_state` 模式,确保业务状态(如 `Material` UUID)能正确往返序列化。
|
||||
|
||||
### [组件] 台面 (Decks)
|
||||
#### [修改] [decks.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/bioyond/decks.py)
|
||||
* 将 `BIOYOND_YB_Deck` 重命名为 **`BioyondElectrolyteDeck`**,对应工厂函数 `YB_Deck()` 重命名为 **`bioyond_electrolyte_deck()`**。
|
||||
* `BIOYOND_PolymerReactionStation_Deck` 和 `BIOYOND_PolymerPreparationStation_Deck` **保持不变**。
|
||||
* 以上三个 Deck 的 `__init__` 中均移除 `setup` 参数和 `setup()` 调用,删除临时的 `deserialize` 重写。
|
||||
|
||||
#### [修改 + 重命名] [YB_YH_materials.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py) → `yihua_coin_cell_materials.py`
|
||||
* 将 `CoincellDeck` 重命名为 **`YihuaCoinCellDeck`**,对应工厂函数 `YH_Deck()` 重命名为 **`yihua_coin_cell_deck()`**。
|
||||
* 从 `YihuaCoinCellDeck.__init__` 中移除 `setup` 参数和 `setup()` 调用,删除临时的 `deserialize` 重写。
|
||||
|
||||
### [组件] 容器类与弹夹 (Itemized Carriers & Magazines)
|
||||
#### [修改] [magazine.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/battery/magazine.py)
|
||||
* 重构 `magazine_factory`:将创建 `MagazineHolder` 几何结构(空槽位)的过程与填充 `ElectrodeSheet` 物料的过程分离。
|
||||
* 确保 `MagazineHolder` 和 `Magazine` 的 `__init__` 过程中不主动创建任何内容物。
|
||||
|
||||
#### [修改] [warehouse.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/warehouse.py)
|
||||
* 确保 `WareHouse` 类和 `warehouse_factory` 遵循相同模式:先初始化几何结构,内容物另行处理。
|
||||
|
||||
#### [修改] [itemized_carrier.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/itemized_carrier.py)
|
||||
* 移除之前添加的 `idx is None` 兜底补丁。
|
||||
* 修复命名规范,确保 `assign_child_resource` 在反序列化时能准确匹配资源。
|
||||
|
||||
### [组件] 状态兼容性 (State Compatibility)
|
||||
#### [修改] [resource_tracker.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/resource_tracker.py)
|
||||
* 在 `to_plr_resources` 方法中调用 `load_all_state` 之前,预处理 `all_states` 字典。
|
||||
* 对于 `Container` 类型的资源,如果其状态中缺少 `liquid_history` 或 `pending_liquids` 等 PLR 新版本要求的键,则填充默认值(如空列表/字典),防止反序列化中断。
|
||||
|
||||
### [组件] 料盘 (Material Plates)
|
||||
#### [修改] [YB_YH_materials.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py)
|
||||
* 重构 `MaterialPlate`:不在 `__init__` 中直接调用 `create_ordered_items_2d`。
|
||||
* 重构 `YIHUA_Electrolyte_12VialCarrier`:将其修改为标准的基类定义或在工厂方法中彻底剥离内部 12 个 `YB_pei_ye_xiao_Bottle` 的强制初始化,以防反序列化冲突。
|
||||
|
||||
### [组件] 跨站转运与分液瓶板 (Vial Plate Transfer)
|
||||
#### [修改] [bioyond_cell_workstation.py] & [YB_YH_materials.py]
|
||||
* **分析**:目前的 `bioyond_cell_workstation.py` 在执行转移时,是用 `sites=["electrolyte_buffer"]` 试图把整块 `YB_Vial_5mL_Carrier` 板转移给目标。但由于实际工艺中,配液站将分液瓶板传往扣电工站后,是由扣电工站的机械臂**逐瓶抓取**并放入内部的 `bottle_rack_6x2`(电解液缓存位),用完后再放入 `bottle_rack_6x2_2`(废液位),因此配液站的这一次“跨工位资源树转移”在逻辑上存在偏差:目标槽位不应该是装单瓶的载体 `bottle_rack`。
|
||||
* **修复方案**:
|
||||
1. **目标端 (Yihua 侧)**:
|
||||
* 在 `YB_YH_materials.py` 中为从配液站传过来的“分液瓶板”本身设置一个接驳专用的 `PlateSlot`(或者单纯直接移到 Deck 指定坐标)。这个位置负责真正在资源树层级上合法接收配液站传过来的完整 Board。
|
||||
* 重构 `YIHUA_Electrolyte_12VialCarrier`:为了防止初始化反序列化冲突,取消内部在 `__init__` 中自动填充满 12 个 `YB_pei_ye_xiao_Bottle` 实例的逻辑。`bottle_rack_6x2` 和 `bottle_rack_6x2_2` 初始化时均应为空。
|
||||
2. **转运端 (Bioyond 侧)**:
|
||||
* 修改 `bioyond_cell_workstation.py` 的资源树数字转运代码,将其转移目标对应到 Yihua 侧新设立的“分液瓶板接驳区域”资源,或者干脆只更新资源树坐标位置(使其脱离 Bioyond Deck 加入 Yihua Deck),而不再强行挂载到一个无法容纳 Carrier 的 `bottle_rack_6x2` 内部。
|
||||
|
||||
### [组件] 依华扣电组装工站物料余量监控 (Material Monitoring)
|
||||
#### [修改] 寄存器直读与前端集成
|
||||
* **物理对象保留但虚化追踪**:原有的实体台面对象(如 `MaterialPlate`、`MagazineHolder` 各类型及其对应的洞位坐标)**仍然保留并使用**。保留它们是为了给机器臂提供基础的物理空间取放标定,以及作为前端页面的可视和可交互区块。
|
||||
* **内部物料免追踪**:既然余量完全由寄存器接管,**我们将不再在这些弹夹或洞位内部显式生成、塞入和追踪每一个具体的极片或外壳对象 (如 `ElectrodeSheet` 等)**。这恰好与我们的重构主旨(不主动在 `__init__` 建子物料以避开反序列化冲突)完美结合,进一步极大地减轻了后台资源树对象的复杂度。
|
||||
* **监控方式变更**:放弃现有的物料余量方式,直接读取依华扣电组装工站开放的寄存器地址以获取准确余量。
|
||||
* **前端界面集成**:在前端界面点击负极壳、弹垫片等弹夹的 data view 时,直接读取并显示寄存器中的各自余量。
|
||||
* **新增寄存器映射** (参考 `coin_cell_assembly_b.csv`):
|
||||
* `10mm正极片剩余物料数量(R)`:`read hold_register 520` (REAL)
|
||||
* `12mm正极片剩余物料数量(R)`:`read hold_register 522` (REAL)
|
||||
* `16mm正极片剩余物料数量(R)`:`read hold_register 524` (REAL)
|
||||
* `铝箔剩余物料数量(R)`:`read hold_register 526` (REAL)
|
||||
* `正极壳剩余物料数量(R)`:`read hold_register 528` (REAL)
|
||||
* `平垫剩余物料数量(R)`:`read hold_register 530` (REAL)
|
||||
* `负极壳剩余物料数量(R)`:`read hold_register 532` (REAL)
|
||||
* `弹垫剩余物料数量(R)`:`read hold_register 534` (REAL)
|
||||
* `成品电池剩余可容纳数量(R)`:`read hold_register 536` (REAL)
|
||||
* `成品电池NG槽剩余可容纳数量(R)`:`read hold_register 538` (REAL)
|
||||
|
||||
### [配置] JSON 配置文件 (Configuration Files)
|
||||
#### [修改] 资源类型名称更新
|
||||
* 更新以下配置文件,将其中的 `BIOYOND_YB_Deck` 替换为新的类名 **`BioyondElectrolyteDeck`**,以及将 `coin_cell_deck` 替换为 **`YihuaCoinCellDeck`**:
|
||||
* `yibin_electrolyte_config.json`
|
||||
* `yibin_coin_cell_only_config.json`
|
||||
* `yibin_electrolyte_only_config.json`
|
||||
|
||||
## 验证计划
|
||||
|
||||
### 自动化测试
|
||||
* 对重构后的类运行 `pylabrobot` 序列化/反序列化测试,确保状态能够完美恢复。
|
||||
* 检查各工作站节点启动时是否仍存在 `ValueError: Resource '...' already assigned to deck` 报错。
|
||||
* 检查 `resource_tracker` 中是否仍存在重复 UUID 报错。
|
||||
|
||||
### 手动验证
|
||||
* 重启各工作站节点,验证资源树是否能根据数据库数据正确还还原。
|
||||
* 验证“自动”与“手动”传输窗资源在台面上的分配是否正确。
|
||||
388
unilabos/devices/workstation/implementation_plan_v2.md
Normal file
388
unilabos/devices/workstation/implementation_plan_v2.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# 物料系统标准化重构方案 v2(增强版)
|
||||
|
||||
> **基于原始方案 (`implementation_plan.md`) 的补充与细化**。
|
||||
> 本文档在原方案基础上:①增加当前代码现状核查结果;②明确各任务的执行顺序与文件级改动;③新增注意事项与回归测试命令。
|
||||
|
||||
---
|
||||
|
||||
## 0. 核心原则(保持不变)
|
||||
|
||||
"**物理几何结构初始化(Deck / Carrier / Magazine 的 `__init__`)与物料内容物填充(`setup()` / `klasses` 参数)必须彻底解耦**",以消除 PLR 反序列化时的 `Resource already assigned to deck` 错误。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前代码现状核查(2026-03-12)
|
||||
|
||||
| 文件 | 计划要求 | 当前状态 | 是否完成 |
|
||||
|---|---|---|---|
|
||||
| `resources/bioyond/decks.py` | 重命名类;移除 `setup` 参数和 `deserialize` 补丁 | 仍是 `BIOYOND_YB_Deck`;`setup` 参数和 `deserialize` 均存在 | ❌ |
|
||||
| `coin_cell_assembly/YB_YH_materials.py` | 重命名类;文件迁移;移除补丁 | 仍是 `CoincellDeck`;`setup` 参数和 `deserialize` 均存在 | ❌ |
|
||||
| `resources/battery/magazine.py` | `magazine_factory` 不主动填充物料 | `MagazineHolder_6_Cathode` / `_6_Anode` / `_4_Cathode` 仍传 `klasses`,初始化时填满极片 | ❌ |
|
||||
| `resources/battery/bottle_carriers.py` | `YIHUA_Electrolyte_12VialCarrier` 初始化时不填充瓶子 | 第 54-55 行仍循环填充 12 个 `YB_pei_ye_xiao_Bottle` | ❌ |
|
||||
| `resources/itemized_carrier.py` | 移除 `idx is None` 兜底补丁 | 第 182-190 行仍保留该兜底逻辑 | ❌(待前置任务完成后移除) |
|
||||
| `resources/resource_tracker.py` | `load_all_state` 前预填 `Container` 缺失键 | 第 616 行直接调用,无预处理 | ❌ |
|
||||
| `bioyond_cell_workstation.py` | 修正跨站转运目标为合法接驳槽 | 第 1563 行仍 `sites=["electrolyte_buffer"]`,目标 UUID 为硬编码虚拟资源 | ❌ |
|
||||
| `yibin_*.json` 配置文件 | 更新类名 | 仍使用 `BIOYOND_YB_Deck` / `CoincellDeck` | ❌ |
|
||||
| `registry/resources/bioyond/deck.yaml` | 更新类名(原计划未提及) | 仍使用 `BIOYOND_YB_Deck` / `CoincellDeck` | ❌(**新增**) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 执行顺序(含依赖关系)
|
||||
|
||||
```
|
||||
阶段 A(底层资源类)
|
||||
A1. magazine.py — 移除 klasses 填充
|
||||
A2. bottle_carriers.py — 移除瓶子填充
|
||||
|
||||
阶段 B(Deck 层)
|
||||
B1. decks.py — 移除 setup 参数和 deserialize 补丁;重命名
|
||||
B2. YB_YH_materials.py → 重命名;移除 CoincellDeck 的 setup 参数和 deserialize 补丁
|
||||
|
||||
阶段 C(状态兼容)
|
||||
C1. resource_tracker.py — 预填 Container 缺失键
|
||||
C2. itemized_carrier.py — 移除 idx is None 兜底补丁(B 阶段完成后)
|
||||
|
||||
阶段 D(跨站转运修复)
|
||||
D1. YB_YH_materials.py 新增 vial_plate_dock(接驳专用槽)
|
||||
D2. bioyond_cell_workstation.py 修正 transfer 目标
|
||||
|
||||
阶段 E(配置与注册表)
|
||||
E1. yibin_*.json 更新类名
|
||||
E2. registry/resources/bioyond/deck.yaml 更新类名
|
||||
E3. coin_cell_assembly.py 更新导入路径(若文件重命名)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 分阶段详细说明
|
||||
|
||||
---
|
||||
|
||||
### 阶段 A — 底层资源类
|
||||
|
||||
#### A1. `unilabos/resources/battery/magazine.py`
|
||||
|
||||
**问题**:`MagazineHolder_6_Cathode`、`MagazineHolder_6_Anode`、`MagazineHolder_4_Cathode` 在调用 `magazine_factory` 时传入 `klasses`,导致每次 `__init__` 就填满极片,反序列化时重复添加。
|
||||
|
||||
**修改**:
|
||||
|
||||
- 将三个函数中的 `klasses=[...]` 改为 `klasses=None`(与 `MagazineHolder_6_Battery` 保持一致)。
|
||||
- **理由**:物料余量已由寄存器管理(见阶段 F),不需要在资源树中追踪每一个极片。
|
||||
|
||||
```python
|
||||
# 修改前(MagazineHolder_6_Cathode 举例)
|
||||
klasses=[FlatWasher, PositiveCan, PositiveCan, FlatWasher, PositiveCan, PositiveCan],
|
||||
|
||||
# 修改后
|
||||
klasses=None,
|
||||
```
|
||||
|
||||
> **注意**:`magazine_factory` 中 `klasses` 参数及循环体代码保留(仍可按需在非序列化场景使用),只是各具体工厂函数不再传入。
|
||||
|
||||
---
|
||||
|
||||
#### A2. `unilabos/resources/battery/bottle_carriers.py`
|
||||
|
||||
**问题**:`YIHUA_Electrolyte_12VialCarrier` 第 54-55 行在工厂函数末尾循环填充 12 个瓶子。
|
||||
|
||||
**修改**:删除以下两行:
|
||||
|
||||
```python
|
||||
# 删除
|
||||
for i in range(12):
|
||||
carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_vial_{i+1}")
|
||||
```
|
||||
|
||||
**理由**:`bottle_rack_6x2` 和 `bottle_rack_6x2_2` 均应初始化为空,瓶子由 Bioyond 侧实际转运后再填入。
|
||||
|
||||
---
|
||||
|
||||
### 阶段 B — Deck 层重构
|
||||
|
||||
#### B1. `unilabos/resources/bioyond/decks.py`
|
||||
|
||||
**改动列表**:
|
||||
|
||||
1. **重命名** `BIOYOND_YB_Deck` → `BioyondElectrolyteDeck`
|
||||
2. **重命名** `YB_Deck()` 工厂函数 → `bioyond_electrolyte_deck()`
|
||||
3. **移除** `__init__` 中的 `setup: bool = False` 参数及 `if setup: self.setup()` 调用
|
||||
4. **删除** `deserialize` 方法重写(该临时补丁在 `setup` 参数移除后自然失效,继续保留反而掩盖问题)
|
||||
5. `BIOYOND_PolymerReactionStation_Deck` 和 `BIOYOND_PolymerPreparationStation_Deck` 同步执行第 3、4 步
|
||||
|
||||
**重构后初始化模式**:
|
||||
|
||||
```python
|
||||
class BioyondElectrolyteDeck(Deck):
|
||||
def __init__(self, name: str = "YB_Deck", ...):
|
||||
super().__init__(name=name, ...)
|
||||
# ❌ 不调用 self.setup()
|
||||
# PLR 反序列化时只会调用 __init__,然后从 children JSON 重建子资源
|
||||
|
||||
def setup(self) -> None:
|
||||
# 完整的子资源初始化逻辑保留在这里,只由工厂函数调用
|
||||
...
|
||||
|
||||
def bioyond_electrolyte_deck(name: str) -> BioyondElectrolyteDeck:
|
||||
deck = BioyondElectrolyteDeck(name=name)
|
||||
deck.setup() # ✅ 工厂函数负责填充
|
||||
return deck
|
||||
```
|
||||
|
||||
**同步修改**:
|
||||
- `bioyond_cell_workstation.py` 第 20 行:
|
||||
```python
|
||||
# 修改前
|
||||
from unilabos.resources.bioyond.decks import BIOYOND_YB_Deck
|
||||
# 修改后
|
||||
from unilabos.resources.bioyond.decks import BioyondElectrolyteDeck
|
||||
```
|
||||
- 同文件第 2440 行:`BIOYOND_YB_Deck(setup=True)` → `bioyond_electrolyte_deck(name="YB_Deck")`
|
||||
|
||||
---
|
||||
|
||||
#### B2. `unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py`
|
||||
|
||||
**改动列表**:
|
||||
|
||||
1. **重命名** `CoincellDeck` → `YihuaCoinCellDeck`
|
||||
2. **重命名** `YH_Deck()` → `yihua_coin_cell_deck()`(可保留 `YH_Deck` 作为兼容别名,日后废弃)
|
||||
3. **移除** `CoincellDeck.__init__` 中 `setup: bool = False` 参数及调用
|
||||
4. **删除** `CoincellDeck.deserialize` 重写方法
|
||||
5. `MaterialPlate.__init__` 中移除 `fill` 参数,始终不主动调用 `create_ordered_items_2d`(当前 `fill=False` 路径已正确,只需删除 `fill=True` 分支)
|
||||
|
||||
```python
|
||||
# 修改前(MaterialPlate.__init__ 片段)
|
||||
if fill:
|
||||
super().__init__(..., ordered_items=holes, ...)
|
||||
else:
|
||||
super().__init__(..., ordered_items=ordered_items, ...)
|
||||
|
||||
# 修改后(始终走 "不填充" 路径)
|
||||
super().__init__(..., ordered_items=ordered_items, ...)
|
||||
# holes 的创建代码整体移入独立工厂方法
|
||||
```
|
||||
|
||||
**同步修改**:
|
||||
- `coin_cell_assembly.py` 第 20 行导入:
|
||||
```python
|
||||
# 修改前
|
||||
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import CoincellDeck
|
||||
# 修改后
|
||||
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import YihuaCoinCellDeck
|
||||
```
|
||||
- 同文件第 2245 行:`CoincellDeck(setup=True, name="coin_cell_deck")` → `yihua_coin_cell_deck(name="coin_cell_deck")`
|
||||
- 文件重命名(可选):`YB_YH_materials.py` → `yihua_coin_cell_materials.py`(若重命名,所有 import 路径需全局替换)
|
||||
|
||||
---
|
||||
|
||||
### 阶段 C — 状态兼容
|
||||
|
||||
#### C1. `unilabos/resources/resource_tracker.py`
|
||||
|
||||
**问题**:第 616 行直接调用 `plr_resource.load_all_state(all_states)`,若 `Container` 类资源的 `data` 字段缺少 `liquid_history` 或 `pending_liquids`,PLR 新版本会抛出 `KeyError`。
|
||||
|
||||
**修改**:在第 616 行前插入预处理:
|
||||
|
||||
```python
|
||||
# 在 load_all_state 调用前预填缺失键
|
||||
from pylabrobot.resources.container import Container as PLRContainer
|
||||
for res_name, state in all_states.items():
|
||||
if state and isinstance(state, dict):
|
||||
# Container 类型要求这两个键存在
|
||||
state.setdefault("liquid_history", [])
|
||||
state.setdefault("pending_liquids", {})
|
||||
|
||||
plr_resource.load_all_state(all_states)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### C2. `unilabos/resources/itemized_carrier.py`
|
||||
|
||||
**前提**:B1、B2 阶段完成,Deck 类名与资源命名规范已对齐后再执行此步。
|
||||
|
||||
**修改**:删除第 182-190 行的兜底补丁:
|
||||
|
||||
```python
|
||||
# 删除以下整个 if 块
|
||||
if idx is None:
|
||||
fallback_location = location if location is not None else Coordinate.zero()
|
||||
super().assign_child_resource(resource, location=fallback_location, reassign=reassign)
|
||||
return
|
||||
```
|
||||
|
||||
**替代**:改为抛出带诊断信息的异常,便于后续问题排查:
|
||||
|
||||
```python
|
||||
if idx is None:
|
||||
raise ValueError(
|
||||
f"[ItemizedCarrier] 无法为资源 '{resource.name}' 找到匹配的槽位。"
|
||||
f"已知槽位:{list(self.child_locations.keys())},"
|
||||
f"传入坐标:{location}"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 阶段 D — 跨站转运修复
|
||||
|
||||
#### D1. `YB_YH_materials.py` — 新增分液瓶板接驳槽
|
||||
|
||||
在 `YihuaCoinCellDeck.setup()` 中,新增一个专用于接收 Bioyond 侧传来的完整分液瓶板的 `ResourceStack`(或 `PlateSlot`):
|
||||
|
||||
```python
|
||||
# 在 setup() 末尾追加
|
||||
from pylabrobot.resources.resource_stack import ResourceStack
|
||||
|
||||
vial_plate_dock = ResourceStack(
|
||||
name="electrolyte_buffer", # 保持与 bioyond_cell_workstation.py 的 sites 键一致
|
||||
direction="z",
|
||||
resources=[],
|
||||
)
|
||||
self.assign_child_resource(vial_plate_dock, Coordinate(x=1050.0, y=700.0, z=0))
|
||||
```
|
||||
|
||||
> **说明**:槽位命名 `electrolyte_buffer` 与 `bioyond_cell_workstation.py` 现有的 `sites=["electrolyte_buffer"]` 对应,减少改动量。如改名,D2 需同步。
|
||||
|
||||
---
|
||||
|
||||
#### D2. `bioyond_cell_workstation.py` — 修正 transfer 目标
|
||||
|
||||
**问题**:第 1545-1552 行创建了一个 `size=1,1,1` 的虚拟 `ResourcePLR` 并硬编码 UUID,这个对象在 YihuaCoinCellDeck 的资源树中不存在,导致转移后资源树状态混乱。
|
||||
|
||||
**修改**:
|
||||
|
||||
```python
|
||||
# 修改前:创建虚拟目标资源
|
||||
target_resource_obj = ResourcePLR(name=target_location, size_x=1.0, ...)
|
||||
target_resource_obj.unilabos_uuid = "550e8400-e29b-41d4-a716-446655440001" # 硬编码
|
||||
|
||||
# 修改后:通过 ROS2/设备注册表查询真实资源
|
||||
# (需要从 target_device 的资源树中取出 electrolyte_buffer 的真实对象)
|
||||
target_resource_obj = self._get_resource_from_device(
|
||||
device_id=target_device,
|
||||
resource_name=target_location
|
||||
)
|
||||
if target_resource_obj is None:
|
||||
raise RuntimeError(
|
||||
f"目标设备 {target_device} 中未找到资源 '{target_location}',"
|
||||
f"请确认 YihuaCoinCellDeck.setup() 中已添加 electrolyte_buffer 槽位"
|
||||
)
|
||||
```
|
||||
|
||||
> **说明**:`_get_resource_from_device` 需根据现有 ROS2 资源同步机制实现,或复用已有的 `get_plr_resource_by_name` 类似方法。
|
||||
|
||||
---
|
||||
|
||||
### 阶段 E — 配置与注册表
|
||||
|
||||
#### E1. `yibin_electrolyte_config.json` / `yibin_coin_cell_only_config.json` / `yibin_electrolyte_only_config.json`
|
||||
|
||||
全局替换以下字符串:
|
||||
|
||||
| 旧值 | 新值 |
|
||||
|---|---|
|
||||
| `BIOYOND_YB_Deck` | `BioyondElectrolyteDeck` |
|
||||
| `unilabos.resources.bioyond.decks:BIOYOND_YB_Deck` | `unilabos.resources.bioyond.decks:BioyondElectrolyteDeck` |
|
||||
| `CoincellDeck` | `YihuaCoinCellDeck` |
|
||||
| `unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:CoincellDeck` | 若文件已重命名:`unilabos.devices.workstation.coin_cell_assembly.yihua_coin_cell_materials:YihuaCoinCellDeck` |
|
||||
|
||||
---
|
||||
|
||||
#### E2. `unilabos/registry/resources/bioyond/deck.yaml`(**原计划未覆盖,新增**)
|
||||
|
||||
当前第 25 行和第 37 行仍使用旧类名,需同步更新:
|
||||
|
||||
```yaml
|
||||
# 修改前
|
||||
BIOYOND_YB_Deck:
|
||||
...
|
||||
CoincellDeck:
|
||||
...
|
||||
|
||||
# 修改后
|
||||
BioyondElectrolyteDeck:
|
||||
...
|
||||
YihuaCoinCellDeck:
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 阶段 F — 物料余量监控集成(原计划第5节细化)
|
||||
|
||||
**目标**:弃用资源树内极片对象计数,改为直读依华扣电工站寄存器。
|
||||
|
||||
#### F1. `coin_cell_assembly/coin_cell_assembly.py` — 新增寄存器读取方法
|
||||
|
||||
参考 `coin_cell_assembly_b.csv` 中的地址,封装读取工具方法:
|
||||
|
||||
```python
|
||||
MATERIAL_REGISTER_MAP = {
|
||||
"10mm正极片": (520, "REAL"),
|
||||
"12mm正极片": (522, "REAL"),
|
||||
"16mm正极片": (524, "REAL"),
|
||||
"铝箔": (526, "REAL"),
|
||||
"正极壳": (528, "REAL"),
|
||||
"平垫": (530, "REAL"),
|
||||
"负极壳": (532, "REAL"),
|
||||
"弹垫": (534, "REAL"),
|
||||
"成品容量": (536, "REAL"),
|
||||
"成品NG容量": (538, "REAL"),
|
||||
}
|
||||
|
||||
def get_material_remaining(self, material_name: str) -> float:
|
||||
"""通过寄存器直读指定物料的剩余数量"""
|
||||
if material_name not in MATERIAL_REGISTER_MAP:
|
||||
raise KeyError(f"未知物料名称: {material_name}")
|
||||
address, dtype = MATERIAL_REGISTER_MAP[material_name]
|
||||
return self.read_hold_register(address, dtype) # 复用现有 Modbus 读取方法
|
||||
```
|
||||
|
||||
#### F2. 前端 data view 集成
|
||||
|
||||
- 前端点击 `MagazineHolder` 类资源的 data view 时,调用后端 `get_material_remaining` 接口(而非读取 `children` 长度)。
|
||||
- 具体接口路径和前端调用代码需与前端开发同步,本文档不作具体实现约定。
|
||||
|
||||
---
|
||||
|
||||
## 4. 验证计划(细化)
|
||||
|
||||
### 4.1 单元测试(自动化)
|
||||
|
||||
```bash
|
||||
# 序列化/反序列化往返测试
|
||||
python -m pytest unilabos/test/ -k "serial" -v
|
||||
|
||||
# 特别检查以下错误消失:
|
||||
# - ValueError: Resource '...' already assigned to deck
|
||||
# - KeyError: 'liquid_history'
|
||||
# - 重复 UUID 报错
|
||||
```
|
||||
|
||||
### 4.2 集成测试(手动)
|
||||
|
||||
按以下顺序逐步验证,确保每步正常后再进行下一步:
|
||||
|
||||
1. **单独启动 `BatteryStation` 节点**,检查 `CoincellDeck`(现 `YihuaCoinCellDeck`)能否从数据库状态正确还原,无 `already assigned` 报错。
|
||||
2. **单独启动 `BioyondElectrolyte` 节点**,检查 `BioyondElectrolyteDeck` 反序列化正常。
|
||||
3. **同时启动两个节点**,模拟执行一次分液→扣电的完整跨站转运,确认:
|
||||
- `electrolyte_buffer` 槽位正确接收分液瓶板。
|
||||
- `bottle_rack_6x2` 初始为空,不出现虚拟瓶子。
|
||||
4. **重启两个节点**(模拟断电恢复),确认资源树从数据库还原后,`electrolyte_buffer` 中仍持有正确的分液瓶板对象。
|
||||
5. **寄存器余量读取**:手动触发 `get_material_remaining("负极壳")`,确认返回值与设备显示一致。
|
||||
|
||||
---
|
||||
|
||||
## 5. 与原计划的差异对照
|
||||
|
||||
| 维度 | 原计划 | 本文档新增/修订 |
|
||||
|---|---|---|
|
||||
| 执行顺序 | 未排序 | 明确 A→B→C→D→E→F 的依赖顺序 |
|
||||
| `itemized_carrier.py` | 移除兜底补丁 | 补充:替换为带诊断信息的异常,便于排查 |
|
||||
| `bottle_carriers.py` | 提及 `YIHUA_Electrolyte_12VialCarrier` 需修改 | 明确:删除第 54-55 行的瓶子填充循环 |
|
||||
| `MaterialPlate` | 提及移除 `fill` 参数 | 说明保留 `fill=False` 路径;整体删除 `fill=True` 分支 |
|
||||
| `deck.yaml` | 未提及 | **新增**:该注册文件也需要同步更新类名 |
|
||||
| `resource_tracker.py` | 简略描述 | 提供具体的 `setdefault` 预处理代码示例 |
|
||||
| 跨站转运 | 描述了问题和方向 | 细化:新增 `electrolyte_buffer` 槽位的具体名称和坐标;修正 `transfer` 目标查找方式 |
|
||||
| 验证计划 | 简述目标 | 提供具体测试命令和逐步手动验证流程 |
|
||||
@@ -825,6 +825,7 @@ def _extract_class_body(
|
||||
action_args.setdefault("placeholder_keys", {})
|
||||
action_args.setdefault("always_free", False)
|
||||
action_args.setdefault("is_protocol", False)
|
||||
action_args.setdefault("feedback_interval", 1.0)
|
||||
action_args.setdefault("description", "")
|
||||
action_args.setdefault("auto_prefix", False)
|
||||
action_args.setdefault("parent", False)
|
||||
|
||||
@@ -343,6 +343,7 @@ def action(
|
||||
auto_prefix: bool = False,
|
||||
parent: bool = False,
|
||||
node_type: Optional["NodeType"] = None,
|
||||
feedback_interval: Optional[float] = None,
|
||||
):
|
||||
"""
|
||||
动作方法装饰器
|
||||
@@ -378,9 +379,16 @@ def action(
|
||||
"""
|
||||
|
||||
def decorator(func: F) -> F:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
import asyncio as _asyncio
|
||||
|
||||
if _asyncio.iscoroutinefunction(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
# action_type 为哨兵值 => 用户没传, 视为 None (UniLabJsonCommand)
|
||||
resolved_type = None if action_type is _ACTION_TYPE_UNSET else action_type
|
||||
@@ -399,6 +407,8 @@ def action(
|
||||
"auto_prefix": auto_prefix,
|
||||
"parent": parent,
|
||||
}
|
||||
if feedback_interval is not None:
|
||||
meta["feedback_interval"] = feedback_interval
|
||||
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]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -64,59 +64,12 @@ coincellassemblyworkstation_device:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result:
|
||||
type: boolean
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: fun_wuliao_test参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_allpack_cmd:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
assembly_pressure: 4200
|
||||
assembly_type: 7
|
||||
elec_num: null
|
||||
elec_use_num: null
|
||||
elec_vol: 50
|
||||
file_path: /Users/sml/work
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
assembly_pressure:
|
||||
default: 4200
|
||||
type: integer
|
||||
assembly_type:
|
||||
default: 7
|
||||
type: integer
|
||||
elec_num:
|
||||
type: string
|
||||
elec_use_num:
|
||||
type: string
|
||||
elec_vol:
|
||||
default: 50
|
||||
type: integer
|
||||
file_path:
|
||||
default: /Users/sml/work
|
||||
type: string
|
||||
required:
|
||||
- elec_num
|
||||
- elec_use_num
|
||||
type: object
|
||||
result:
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: func_allpack_cmd参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-func_allpack_cmd_simp:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
@@ -149,7 +102,7 @@ coincellassemblyworkstation_device:
|
||||
goal:
|
||||
properties:
|
||||
assembly_pressure:
|
||||
default: 4200
|
||||
default: 3200
|
||||
description: 电池压制力(N)
|
||||
type: integer
|
||||
assembly_type:
|
||||
@@ -165,7 +118,7 @@ coincellassemblyworkstation_device:
|
||||
description: 是否启用压力模式
|
||||
type: boolean
|
||||
dual_drop_first_volume:
|
||||
default: 25
|
||||
default: 0
|
||||
description: 二次滴液第一次排液体积(μL)
|
||||
type: integer
|
||||
dual_drop_mode:
|
||||
@@ -184,6 +137,7 @@ coincellassemblyworkstation_device:
|
||||
description: 电解液瓶数
|
||||
type: string
|
||||
elec_use_num:
|
||||
default: 5
|
||||
description: 每瓶电解液组装电池数
|
||||
type: string
|
||||
elec_vol:
|
||||
@@ -191,7 +145,7 @@ coincellassemblyworkstation_device:
|
||||
description: 电解液吸液量(μL)
|
||||
type: integer
|
||||
file_path:
|
||||
default: /Users/sml/work
|
||||
default: D:\UniLabdev\Uni-Lab-OS\unilabos\devices\workstation\coin_cell_assembly
|
||||
description: 实验记录保存路径
|
||||
type: string
|
||||
fujipian_juzhendianwei:
|
||||
@@ -222,8 +176,7 @@ coincellassemblyworkstation_device:
|
||||
- elec_num
|
||||
- elec_use_num
|
||||
type: object
|
||||
result:
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_allpack_cmd_simp参数
|
||||
@@ -312,8 +265,7 @@ coincellassemblyworkstation_device:
|
||||
type: boolean
|
||||
required: []
|
||||
type: object
|
||||
result:
|
||||
type: boolean
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_pack_device_init_auto_start_combined参数
|
||||
@@ -355,8 +307,7 @@ coincellassemblyworkstation_device:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result:
|
||||
type: boolean
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_pack_device_stop参数
|
||||
@@ -381,8 +332,7 @@ coincellassemblyworkstation_device:
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
result:
|
||||
type: boolean
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_pack_get_msg_cmd参数
|
||||
@@ -396,12 +346,10 @@ coincellassemblyworkstation_device:
|
||||
handles:
|
||||
input:
|
||||
- data_key: bottle_num
|
||||
data_source: workflow
|
||||
data_source: handle
|
||||
data_type: integer
|
||||
handler_key: bottle_count
|
||||
io_type: source
|
||||
label: 配液瓶数
|
||||
required: true
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
@@ -436,8 +384,7 @@ coincellassemblyworkstation_device:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result:
|
||||
type: boolean
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_pack_send_finished_cmd参数
|
||||
@@ -474,8 +421,7 @@ coincellassemblyworkstation_device:
|
||||
- assembly_type
|
||||
- assembly_pressure
|
||||
type: object
|
||||
result:
|
||||
type: boolean
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_pack_send_msg_cmd参数
|
||||
@@ -531,12 +477,21 @@ coincellassemblyworkstation_device:
|
||||
handles:
|
||||
input:
|
||||
- data_key: elec_num
|
||||
data_source: workflow
|
||||
data_source: handle
|
||||
data_type: integer
|
||||
handler_key: bottle_count
|
||||
io_type: source
|
||||
label: 配液瓶数
|
||||
required: true
|
||||
- data_key: formulations
|
||||
data_source: handle
|
||||
data_type: array
|
||||
handler_key: formulations_input
|
||||
label: 配方信息列表
|
||||
output:
|
||||
- data_key: assembly_data
|
||||
data_source: executor
|
||||
data_type: array
|
||||
handler_key: assembly_data_output
|
||||
label: 扣电组装数据列表
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
@@ -619,8 +574,7 @@ coincellassemblyworkstation_device:
|
||||
- elec_num
|
||||
- elec_use_num
|
||||
type: object
|
||||
result:
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: func_sendbottle_allpack_multi参数
|
||||
@@ -672,6 +626,31 @@ coincellassemblyworkstation_device:
|
||||
title: modify_deck_name参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-post_init:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
ros_node: null
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
ros_node:
|
||||
type: object
|
||||
required:
|
||||
- ros_node
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: post_init参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
auto-qiming_coin_cell_code:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
@@ -719,8 +698,7 @@ coincellassemblyworkstation_device:
|
||||
required:
|
||||
- fujipian_panshu
|
||||
type: object
|
||||
result:
|
||||
type: boolean
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: qiming_coin_cell_code参数
|
||||
@@ -728,6 +706,10 @@ coincellassemblyworkstation_device:
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
|
||||
status_types:
|
||||
data_10mm_positive_plate_remaining: float
|
||||
data_12mm_positive_plate_remaining: float
|
||||
data_16mm_positive_plate_remaining: float
|
||||
data_aluminum_foil_remaining: float
|
||||
data_assembly_coin_cell_num: int
|
||||
data_assembly_pressure: int
|
||||
data_assembly_time: float
|
||||
@@ -735,14 +717,22 @@ coincellassemblyworkstation_device:
|
||||
data_axis_y_pos: float
|
||||
data_axis_z_pos: float
|
||||
data_coin_cell_code: str
|
||||
data_coin_num: int
|
||||
data_coin_type: int
|
||||
data_current_assembling_count: int
|
||||
data_current_completed_count: int
|
||||
data_electrolyte_code: str
|
||||
data_electrolyte_volume: int
|
||||
data_finished_battery_ng_remaining_capacity: float
|
||||
data_finished_battery_remaining_capacity: float
|
||||
data_flat_washer_remaining: float
|
||||
data_glove_box_o2_content: float
|
||||
data_glove_box_pressure: float
|
||||
data_glove_box_water_content: float
|
||||
data_negative_shell_remaining: float
|
||||
data_open_circuit_voltage: float
|
||||
data_pole_weight: float
|
||||
data_positive_shell_remaining: float
|
||||
data_spring_washer_remaining: float
|
||||
request_rec_msg_status: bool
|
||||
request_send_msg_status: bool
|
||||
sys_mode: str
|
||||
@@ -772,6 +762,14 @@ coincellassemblyworkstation_device:
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
data_10mm_positive_plate_remaining:
|
||||
type: number
|
||||
data_12mm_positive_plate_remaining:
|
||||
type: number
|
||||
data_16mm_positive_plate_remaining:
|
||||
type: number
|
||||
data_aluminum_foil_remaining:
|
||||
type: number
|
||||
data_assembly_coin_cell_num:
|
||||
type: integer
|
||||
data_assembly_pressure:
|
||||
@@ -786,22 +784,38 @@ coincellassemblyworkstation_device:
|
||||
type: number
|
||||
data_coin_cell_code:
|
||||
type: string
|
||||
data_coin_num:
|
||||
data_coin_type:
|
||||
type: integer
|
||||
data_current_assembling_count:
|
||||
type: integer
|
||||
data_current_completed_count:
|
||||
type: integer
|
||||
data_electrolyte_code:
|
||||
type: string
|
||||
data_electrolyte_volume:
|
||||
type: integer
|
||||
data_finished_battery_ng_remaining_capacity:
|
||||
type: number
|
||||
data_finished_battery_remaining_capacity:
|
||||
type: number
|
||||
data_flat_washer_remaining:
|
||||
type: number
|
||||
data_glove_box_o2_content:
|
||||
type: number
|
||||
data_glove_box_pressure:
|
||||
type: number
|
||||
data_glove_box_water_content:
|
||||
type: number
|
||||
data_negative_shell_remaining:
|
||||
type: number
|
||||
data_open_circuit_voltage:
|
||||
type: number
|
||||
data_pole_weight:
|
||||
type: number
|
||||
data_positive_shell_remaining:
|
||||
type: number
|
||||
data_spring_washer_remaining:
|
||||
type: number
|
||||
request_rec_msg_status:
|
||||
type: boolean
|
||||
request_send_msg_status:
|
||||
@@ -811,24 +825,36 @@ coincellassemblyworkstation_device:
|
||||
sys_status:
|
||||
type: string
|
||||
required:
|
||||
- sys_status
|
||||
- sys_mode
|
||||
- request_rec_msg_status
|
||||
- request_send_msg_status
|
||||
- data_assembly_coin_cell_num
|
||||
- data_assembly_pressure
|
||||
- data_assembly_time
|
||||
- data_open_circuit_voltage
|
||||
- data_axis_x_pos
|
||||
- data_axis_y_pos
|
||||
- data_axis_z_pos
|
||||
- data_coin_cell_code
|
||||
- data_coin_num
|
||||
- data_electrolyte_code
|
||||
- data_electrolyte_volume
|
||||
- data_glove_box_o2_content
|
||||
- data_glove_box_pressure
|
||||
- data_glove_box_water_content
|
||||
- data_open_circuit_voltage
|
||||
- data_pole_weight
|
||||
- request_rec_msg_status
|
||||
- request_send_msg_status
|
||||
- sys_mode
|
||||
- sys_status
|
||||
- data_assembly_pressure
|
||||
- data_electrolyte_volume
|
||||
- data_coin_type
|
||||
- data_current_assembling_count
|
||||
- data_current_completed_count
|
||||
- data_coin_cell_code
|
||||
- data_electrolyte_code
|
||||
- data_glove_box_pressure
|
||||
- data_glove_box_o2_content
|
||||
- data_glove_box_water_content
|
||||
- data_10mm_positive_plate_remaining
|
||||
- data_12mm_positive_plate_remaining
|
||||
- data_16mm_positive_plate_remaining
|
||||
- data_aluminum_foil_remaining
|
||||
- data_positive_shell_remaining
|
||||
- data_flat_washer_remaining
|
||||
- data_negative_shell_remaining
|
||||
- data_spring_washer_remaining
|
||||
- data_finished_battery_remaining_capacity
|
||||
- data_finished_battery_ng_remaining_capacity
|
||||
type: object
|
||||
registry_type: device
|
||||
version: 1.0.0
|
||||
|
||||
@@ -31,6 +31,6 @@ hotel.thermo_orbitor_rs2_hotel:
|
||||
type: object
|
||||
model:
|
||||
mesh: thermo_orbitor_rs2_hotel
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/thermo_orbitor_rs2_hotel/macro_device.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/thermo_orbitor_rs2_hotel/macro_device.xacro
|
||||
type: device
|
||||
version: 1.0.0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -329,7 +329,7 @@ robotic_arm.SCARA_with_slider.moveit.virtual:
|
||||
type: object
|
||||
model:
|
||||
mesh: arm_slider
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/arm_slider/macro_device.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/arm_slider/macro_device.xacro
|
||||
type: device
|
||||
version: 1.0.0
|
||||
robotic_arm.UR:
|
||||
|
||||
@@ -238,6 +238,7 @@ class Registry:
|
||||
"class_name": "unilabos_class",
|
||||
},
|
||||
"always_free": True,
|
||||
"feedback_interval": 300.0,
|
||||
},
|
||||
"test_latency": test_latency_action,
|
||||
"auto-test_resource": test_resource_action,
|
||||
@@ -829,8 +830,9 @@ class Registry:
|
||||
raw_handles = (action_args or {}).get("handles")
|
||||
handles = normalize_ast_action_handles(raw_handles) if isinstance(raw_handles, list) else (raw_handles or {})
|
||||
|
||||
# placeholder_keys: 优先用装饰器显式配置,否则从参数类型检测
|
||||
pk = (action_args or {}).get("placeholder_keys") or detect_placeholder_keys(params)
|
||||
# placeholder_keys: 先从参数类型自动检测,再用装饰器显式配置覆盖/补充
|
||||
pk = detect_placeholder_keys(params)
|
||||
pk.update((action_args or {}).get("placeholder_keys") or {})
|
||||
|
||||
# 从方法返回值类型生成 result schema
|
||||
result_schema = None
|
||||
@@ -852,6 +854,8 @@ class Registry:
|
||||
}
|
||||
if (action_args or {}).get("always_free") or method_info.get("always_free"):
|
||||
entry["always_free"] = True
|
||||
_fb_iv = (action_args or {}).get("feedback_interval", method_info.get("feedback_interval", 1.0))
|
||||
entry["feedback_interval"] = _fb_iv
|
||||
nt = normalize_enum_value((action_args or {}).get("node_type"), NodeType)
|
||||
if nt:
|
||||
entry["node_type"] = nt
|
||||
@@ -975,10 +979,12 @@ class Registry:
|
||||
"schema": schema,
|
||||
"goal_default": goal_default,
|
||||
"handles": handles,
|
||||
"placeholder_keys": action_args.get("placeholder_keys") or detect_placeholder_keys(method_params),
|
||||
"placeholder_keys": {**detect_placeholder_keys(method_params), **(action_args.get("placeholder_keys") or {})},
|
||||
}
|
||||
if action_args.get("always_free") or method_info.get("always_free"):
|
||||
action_entry["always_free"] = True
|
||||
_fb_iv = action_args.get("feedback_interval", method_info.get("feedback_interval", 1.0))
|
||||
action_entry["feedback_interval"] = _fb_iv
|
||||
nt = normalize_enum_value(action_args.get("node_type"), NodeType)
|
||||
if nt:
|
||||
action_entry["node_type"] = nt
|
||||
|
||||
12
unilabos/registry/resources/battery/bottle_carriers.yaml
Normal file
12
unilabos/registry/resources/battery/bottle_carriers.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
YIHUA_Electrolyte_12VialCarrier:
|
||||
category:
|
||||
- battery_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.battery.bottle_carriers:YIHUA_Electrolyte_12VialCarrier
|
||||
type: pylabrobot
|
||||
description: YIHUA 12-vial electrolyte carrier for coin cell assembly workstation
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
@@ -1,84 +1,140 @@
|
||||
YB_20ml_fenyeping:
|
||||
YB_Vial_20mL:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_20ml_fenyeping
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_Vial_20mL
|
||||
type: pylabrobot
|
||||
description: YB_20ml_fenyeping
|
||||
description: YB_Vial_20mL
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
YB_5ml_fenyeping:
|
||||
YB_Vial_5mL:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_5ml_fenyeping
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_Vial_5mL
|
||||
type: pylabrobot
|
||||
description: YB_5ml_fenyeping
|
||||
description: YB_Vial_5mL
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
YB_jia_yang_tou_da:
|
||||
YB_DosingHead_L:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_jia_yang_tou_da
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_DosingHead_L
|
||||
type: pylabrobot
|
||||
description: YB_jia_yang_tou_da
|
||||
description: YB_DosingHead_L
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
YB_pei_ye_da_Bottle:
|
||||
YB_PrepBottle_60mL:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_da_Bottle
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_PrepBottle_60mL
|
||||
type: pylabrobot
|
||||
description: YB_pei_ye_da_Bottle
|
||||
description: YB_PrepBottle_60mL
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
YB_pei_ye_xiao_Bottle:
|
||||
YB_PrepBottle_15mL:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_xiao_Bottle
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_PrepBottle_15mL
|
||||
type: pylabrobot
|
||||
description: YB_pei_ye_xiao_Bottle
|
||||
description: YB_PrepBottle_15mL
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
YB_qiang_tou:
|
||||
YB_Tip_5000uL:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_qiang_tou
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_Tip_5000uL
|
||||
type: pylabrobot
|
||||
description: YB_qiang_tou
|
||||
description: YB_Tip_5000uL
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
YB_ye_Bottle:
|
||||
YB_Tip_1000uL:
|
||||
category:
|
||||
- YB_bottle
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_Tip_1000uL
|
||||
type: pylabrobot
|
||||
description: YB_Tip_1000uL
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_Tip_50uL:
|
||||
category:
|
||||
- YB_bottle
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_Tip_50uL
|
||||
type: pylabrobot
|
||||
description: YB_Tip_50uL
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_NormalLiq_250mL_Bottle:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
- YB_bottle
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_ye_Bottle
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_NormalLiq_250mL_Bottle
|
||||
type: pylabrobot
|
||||
description: YB_ye_Bottle
|
||||
description: YB_NormalLiq_250mL_Bottle
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_NormalLiq_100mL_Bottle:
|
||||
category:
|
||||
- YB_bottle_carriers
|
||||
- YB_bottle
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_NormalLiq_100mL_Bottle
|
||||
type: pylabrobot
|
||||
description: YB_NormalLiq_100mL_Bottle
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_HighVis_250mL_Bottle:
|
||||
category:
|
||||
- YB_bottle_carriers
|
||||
- YB_bottle
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_HighVis_250mL_Bottle
|
||||
type: pylabrobot
|
||||
description: YB_HighVis_250mL_Bottle
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_HighVis_100mL_Bottle:
|
||||
category:
|
||||
- YB_bottle_carriers
|
||||
- YB_bottle
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_HighVis_100mL_Bottle
|
||||
type: pylabrobot
|
||||
description: YB_HighVis_100mL_Bottle
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
|
||||
@@ -1,42 +1,29 @@
|
||||
YB_100ml_yeti:
|
||||
YB_Vial_20mL_Carrier:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_100ml_yeti
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_Vial_20mL_Carrier
|
||||
type: pylabrobot
|
||||
description: YB_100ml_yeti
|
||||
description: YB_Vial_20mL_Carrier
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_20ml_fenyepingban:
|
||||
YB_Vial_5mL_Carrier:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_20ml_fenyepingban
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_Vial_5mL_Carrier
|
||||
type: pylabrobot
|
||||
description: YB_20ml_fenyepingban
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
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
|
||||
description: YB_Vial_5mL_Carrier
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_6StockCarrier:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6StockCarrier
|
||||
@@ -45,10 +32,10 @@ YB_6StockCarrier:
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_6VialCarrier:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6VialCarrier
|
||||
@@ -57,112 +44,137 @@ YB_6VialCarrier:
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_gao_nian_ye_Bottle:
|
||||
YB_DosingHead_L_Carrier:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_gao_nian_ye_Bottle
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_DosingHead_L_Carrier
|
||||
type: pylabrobot
|
||||
description: YB_gao_nian_ye_Bottle
|
||||
description: YB_DosingHead_L_Carrier
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_gaonianye:
|
||||
YB_PrepBottle_60mL_Carrier:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_gaonianye
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_PrepBottle_60mL_Carrier
|
||||
type: pylabrobot
|
||||
description: YB_gaonianye
|
||||
description: YB_PrepBottle_60mL_Carrier
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_jia_yang_tou_da_Carrier:
|
||||
YB_PrepBottle_15mL_Carrier:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_jia_yang_tou_da_Carrier
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_PrepBottle_15mL_Carrier
|
||||
type: pylabrobot
|
||||
description: YB_jia_yang_tou_da_Carrier
|
||||
description: YB_PrepBottle_15mL_Carrier
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_peiyepingdaban:
|
||||
YB_TipRack_Mixed:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingdaban
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_TipRack_Mixed
|
||||
type: pylabrobot
|
||||
description: YB_peiyepingdaban
|
||||
description: YB_TipRack_Mixed
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_peiyepingxiaoban:
|
||||
YB_TipRack_5000uL:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingxiaoban
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_TipRack_5000uL
|
||||
type: pylabrobot
|
||||
description: YB_peiyepingxiaoban
|
||||
description: YB_TipRack_5000uL
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_qiang_tou_he:
|
||||
YB_TipRack_50uL:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_qiang_tou_he
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_TipRack_50uL
|
||||
type: pylabrobot
|
||||
description: YB_qiang_tou_he
|
||||
description: YB_TipRack_50uL
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_shi_pei_qi_kuai:
|
||||
YB_Adapter_60mL:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_shi_pei_qi_kuai
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_Adapter_60mL
|
||||
type: pylabrobot
|
||||
description: YB_shi_pei_qi_kuai
|
||||
description: YB_Adapter_60mL
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_ye:
|
||||
YB_NormalLiq_250mL_Carrier:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_ye
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_NormalLiq_250mL_Carrier
|
||||
type: pylabrobot
|
||||
description: YB_ye_Bottle_Carrier
|
||||
description: YB_NormalLiq_250mL_Carrier
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_ye_100ml_Bottle:
|
||||
YB_NormalLiq_100mL_Carrier:
|
||||
category:
|
||||
- yb3
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottles:YB_ye_100ml_Bottle
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_NormalLiq_100mL_Carrier
|
||||
type: pylabrobot
|
||||
description: YB_ye_100ml_Bottle
|
||||
description: YB_NormalLiq_100mL_Carrier
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_HighVis_250mL_Carrier:
|
||||
category:
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_HighVis_250mL_Carrier
|
||||
type: pylabrobot
|
||||
description: YB_HighVis_250mL_Carrier
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
YB_HighVis_100mL_Carrier:
|
||||
category:
|
||||
- YB_bottle_carriers
|
||||
class:
|
||||
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_HighVis_100mL_Carrier
|
||||
type: pylabrobot
|
||||
description: YB_HighVis_100mL_Carrier
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema: {}
|
||||
registry_type: resource
|
||||
version: 1.0.0
|
||||
@@ -20,22 +20,22 @@ BIOYOND_PolymerReactionStation_Deck:
|
||||
icon: 反应站.webp
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
BIOYOND_YB_Deck:
|
||||
BioyondElectrolyteDeck:
|
||||
category:
|
||||
- deck
|
||||
class:
|
||||
module: unilabos.resources.bioyond.decks:YB_Deck
|
||||
module: unilabos.resources.bioyond.decks:bioyond_electrolyte_deck
|
||||
type: pylabrobot
|
||||
description: BIOYOND ElectrolyteFormulationStation Deck
|
||||
handles: []
|
||||
icon: 配液站.webp
|
||||
init_param_schema: {}
|
||||
version: 1.0.0
|
||||
CoincellDeck:
|
||||
YihuaCoinCellDeck:
|
||||
category:
|
||||
- deck
|
||||
class:
|
||||
module: unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:YH_Deck
|
||||
module: unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:yihua_coin_cell_deck
|
||||
type: pylabrobot
|
||||
description: YIHUA CoinCellAssembly Deck
|
||||
handles: []
|
||||
|
||||
@@ -17,7 +17,7 @@ hplc_plate:
|
||||
- 0
|
||||
- 0
|
||||
- 3.1416
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/hplc_plate/modal.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/hplc_plate/modal.xacro
|
||||
type: resource
|
||||
version: 1.0.0
|
||||
plate_96:
|
||||
@@ -39,7 +39,7 @@ plate_96:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96/modal.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96/modal.xacro
|
||||
type: resource
|
||||
version: 1.0.0
|
||||
plate_96_high:
|
||||
@@ -61,7 +61,7 @@ plate_96_high:
|
||||
- 1.5708
|
||||
- 0
|
||||
- 1.5708
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96_high/modal.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/plate_96_high/modal.xacro
|
||||
type: resource
|
||||
version: 1.0.0
|
||||
tiprack_96_high:
|
||||
@@ -76,7 +76,7 @@ tiprack_96_high:
|
||||
init_param_schema: {}
|
||||
model:
|
||||
children_mesh: generic_labware_tube_10_75/meshes/0_base.stl
|
||||
children_mesh_path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/generic_labware_tube_10_75/modal.xacro
|
||||
children_mesh_path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/generic_labware_tube_10_75/modal.xacro
|
||||
children_mesh_tf:
|
||||
- 0.0018
|
||||
- 0.0018
|
||||
@@ -92,7 +92,7 @@ tiprack_96_high:
|
||||
- 1.5708
|
||||
- 0
|
||||
- 1.5708
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_96_high/modal.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_96_high/modal.xacro
|
||||
type: resource
|
||||
version: 1.0.0
|
||||
tiprack_box:
|
||||
@@ -107,7 +107,7 @@ tiprack_box:
|
||||
init_param_schema: {}
|
||||
model:
|
||||
children_mesh: tip/meshes/tip.stl
|
||||
children_mesh_path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tip/modal.xacro
|
||||
children_mesh_path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tip/modal.xacro
|
||||
children_mesh_tf:
|
||||
- 0.0045
|
||||
- 0.0045
|
||||
@@ -123,6 +123,6 @@ tiprack_box:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_box/modal.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tiprack_box/modal.xacro
|
||||
type: resource
|
||||
version: 1.0.0
|
||||
|
||||
@@ -11,7 +11,7 @@ bottle_container:
|
||||
init_param_schema: {}
|
||||
model:
|
||||
children_mesh: bottle/meshes/bottle.stl
|
||||
children_mesh_path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/bottle/modal.xacro
|
||||
children_mesh_path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/bottle/modal.xacro
|
||||
children_mesh_tf:
|
||||
- 0.04
|
||||
- 0.04
|
||||
@@ -27,7 +27,7 @@ bottle_container:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/bottle_container/modal.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/bottle_container/modal.xacro
|
||||
type: resource
|
||||
version: 1.0.0
|
||||
tube_container:
|
||||
@@ -43,7 +43,7 @@ tube_container:
|
||||
init_param_schema: {}
|
||||
model:
|
||||
children_mesh: tube/meshes/tube.stl
|
||||
children_mesh_path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tube/modal.xacro
|
||||
children_mesh_path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tube/modal.xacro
|
||||
children_mesh_tf:
|
||||
- 0.017
|
||||
- 0.017
|
||||
@@ -59,6 +59,6 @@ tube_container:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tube_container/modal.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tube_container/modal.xacro
|
||||
type: resource
|
||||
version: 1.0.0
|
||||
|
||||
@@ -10,6 +10,6 @@ TransformXYZDeck:
|
||||
init_param_schema: {}
|
||||
model:
|
||||
mesh: liquid_transform_xyz
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/liquid_transform_xyz/macro_device.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/liquid_transform_xyz/macro_device.xacro
|
||||
type: device
|
||||
version: 1.0.0
|
||||
|
||||
@@ -10,7 +10,7 @@ OTDeck:
|
||||
init_param_schema: {}
|
||||
model:
|
||||
mesh: opentrons_liquid_handler
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/opentrons_liquid_handler/macro_device.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/opentrons_liquid_handler/macro_device.xacro
|
||||
type: device
|
||||
version: 1.0.0
|
||||
hplc_station:
|
||||
@@ -25,6 +25,6 @@ hplc_station:
|
||||
init_param_schema: {}
|
||||
model:
|
||||
mesh: hplc_station
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/hplc_station/macro_device.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/devices/hplc_station/macro_device.xacro
|
||||
type: device
|
||||
version: 1.0.0
|
||||
|
||||
@@ -109,7 +109,7 @@ nest_96_wellplate_100ul_pcr_full_skirt:
|
||||
init_param_schema: {}
|
||||
model:
|
||||
children_mesh: generic_labware_tube_10_75/meshes/0_base.stl
|
||||
children_mesh_path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/generic_labware_tube_10_75/modal.xacro
|
||||
children_mesh_path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/generic_labware_tube_10_75/modal.xacro
|
||||
children_mesh_tf:
|
||||
- 0.0018
|
||||
- 0.0018
|
||||
@@ -125,7 +125,7 @@ nest_96_wellplate_100ul_pcr_full_skirt:
|
||||
- -1.5708
|
||||
- 0
|
||||
- 1.5708
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
|
||||
type: resource
|
||||
version: 1.0.0
|
||||
nest_96_wellplate_200ul_flat:
|
||||
@@ -158,7 +158,7 @@ nest_96_wellplate_2ml_deep:
|
||||
- -1.5708
|
||||
- 0
|
||||
- 1.5708
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
|
||||
type: resource
|
||||
version: 1.0.0
|
||||
thermoscientificnunc_96_wellplate_1300ul:
|
||||
|
||||
@@ -69,7 +69,7 @@ opentrons_96_filtertiprack_1000ul:
|
||||
- -1.5708
|
||||
- 0
|
||||
- 1.5708
|
||||
path: https://uni-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
|
||||
path: https://leap-lab.oss-cn-zhangjiakou.aliyuncs.com/uni-lab/resources/tecan_nested_tip_rack/modal.xacro
|
||||
type: resource
|
||||
version: 1.0.0
|
||||
opentrons_96_filtertiprack_10ul:
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -51,6 +48,5 @@ def YIHUA_Electrolyte_12VialCarrier(name: str) -> BottleCarrier:
|
||||
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}")
|
||||
# 载架初始化为空,瓶子由实际转运操作填入,避免反序列化时重复 assign
|
||||
return carrier
|
||||
|
||||
@@ -135,6 +135,7 @@ class BatteryState(TypedDict):
|
||||
open_circuit_voltage: float
|
||||
assembly_pressure: float
|
||||
electrolyte_volume: float
|
||||
pole_weight: float # 极片称重 (mg)
|
||||
|
||||
info: Optional[str] # 附加信息
|
||||
|
||||
@@ -179,6 +180,7 @@ class Battery(Container):
|
||||
open_circuit_voltage=0.0,
|
||||
assembly_pressure=0.0,
|
||||
electrolyte_volume=0.0,
|
||||
pole_weight=0.0,
|
||||
info=None
|
||||
)
|
||||
|
||||
|
||||
@@ -53,13 +53,28 @@ class Magazine(ResourceStack):
|
||||
return self.get_size_z()
|
||||
|
||||
def serialize(self) -> dict:
|
||||
return {
|
||||
**super().serialize(),
|
||||
data = super().serialize()
|
||||
# 物料余量由寄存器接管,不再持久化极片子节点,
|
||||
# 防止旧数据写回数据库后下次启动时再次引发重复 UUID。
|
||||
data["children"] = []
|
||||
data.update({
|
||||
"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,
|
||||
}
|
||||
})
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, data: dict, allow_marshal: bool = False):
|
||||
"""反序列化时丢弃极片子节点(ElectrodeSheet 等)。
|
||||
|
||||
物料余量已由寄存器接管,不再在资源树中追踪每个极片实体。
|
||||
清空 children 可防止数据库中的旧极片记录被重新加载,避免重复 UUID 报错。
|
||||
"""
|
||||
data = dict(data)
|
||||
data["children"] = []
|
||||
return super().deserialize(data, allow_marshal=allow_marshal)
|
||||
|
||||
|
||||
class MagazineHolder(ItemizedResource):
|
||||
@@ -220,7 +235,7 @@ def MagazineHolder_6_Cathode(
|
||||
size_y=size_y,
|
||||
size_z=size_z,
|
||||
locations=locations,
|
||||
klasses=[FlatWasher, PositiveCan, PositiveCan, FlatWasher, PositiveCan, PositiveCan],
|
||||
klasses=None,
|
||||
hole_diameter=hole_diameter,
|
||||
hole_depth=hole_depth,
|
||||
max_sheets_per_hole=max_sheets_per_hole,
|
||||
@@ -258,7 +273,7 @@ def MagazineHolder_6_Anode(
|
||||
size_y=size_y,
|
||||
size_z=size_z,
|
||||
locations=locations,
|
||||
klasses=[SpringWasher, NegativeCan, NegativeCan, SpringWasher, NegativeCan, NegativeCan],
|
||||
klasses=None,
|
||||
hole_diameter=hole_diameter,
|
||||
hole_depth=hole_depth,
|
||||
max_sheets_per_hole=max_sheets_per_hole,
|
||||
@@ -335,7 +350,7 @@ def MagazineHolder_4_Cathode(
|
||||
size_y=size_y,
|
||||
size_z=size_z,
|
||||
locations=locations,
|
||||
klasses=[AluminumFoil, PositiveElectrode, PositiveElectrode, PositiveElectrode],
|
||||
klasses=None,
|
||||
hole_diameter=hole_diameter,
|
||||
hole_depth=hole_depth,
|
||||
max_sheets_per_hole=max_sheets_per_hole,
|
||||
|
||||
@@ -2,15 +2,18 @@ from pylabrobot.resources import create_homogeneous_resources, Coordinate, Resou
|
||||
|
||||
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,
|
||||
YB_DosingHead_L,
|
||||
YB_NormalLiq_250mL_Bottle,
|
||||
YB_NormalLiq_100mL_Bottle,
|
||||
YB_HighVis_250mL_Bottle,
|
||||
YB_HighVis_100mL_Bottle,
|
||||
YB_Vial_5mL,
|
||||
YB_Vial_20mL,
|
||||
YB_PrepBottle_15mL,
|
||||
YB_PrepBottle_60mL,
|
||||
YB_Tip_5000uL,
|
||||
YB_Tip_1000uL,
|
||||
YB_Tip_50uL,
|
||||
)
|
||||
# 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial
|
||||
|
||||
@@ -206,7 +209,7 @@ def YB_6VialCarrier(name: str) -> BottleCarrier:
|
||||
return carrier
|
||||
|
||||
# 1瓶载架 - 单个中央位置
|
||||
def YB_ye(name: str) -> BottleCarrier:
|
||||
def YB_NormalLiq_250mL_Carrier(name: str) -> BottleCarrier:
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
carrier_size_x = 127.8
|
||||
@@ -233,17 +236,17 @@ def YB_ye(name: str) -> BottleCarrier:
|
||||
resource_size_y=beaker_diameter,
|
||||
name_prefix=name,
|
||||
),
|
||||
model="YB_ye",
|
||||
model="YB_NormalLiq_250mL_Carrier",
|
||||
)
|
||||
carrier.num_items_x = 1
|
||||
carrier.num_items_y = 1
|
||||
carrier.num_items_z = 1
|
||||
carrier[0] = YB_ye_Bottle(f"{name}_flask_1")
|
||||
carrier[0] = YB_NormalLiq_250mL_Bottle(f"{name}_flask_1")
|
||||
return carrier
|
||||
|
||||
|
||||
# 高粘液瓶载架 - 单个中央位置
|
||||
def YB_gaonianye(name: str) -> BottleCarrier:
|
||||
def YB_HighVis_250mL_Carrier(name: str) -> BottleCarrier:
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
carrier_size_x = 127.8
|
||||
@@ -270,17 +273,17 @@ def YB_gaonianye(name: str) -> BottleCarrier:
|
||||
resource_size_y=beaker_diameter,
|
||||
name_prefix=name,
|
||||
),
|
||||
model="YB_gaonianye",
|
||||
model="YB_HighVis_250mL_Carrier",
|
||||
)
|
||||
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")
|
||||
carrier[0] = YB_HighVis_250mL_Bottle(f"{name}_flask_1")
|
||||
return carrier
|
||||
|
||||
|
||||
# 100ml液体瓶载架 - 单个中央位置
|
||||
def YB_100ml_yeti(name: str) -> BottleCarrier:
|
||||
# 100mL普通液瓶载架 - 单个中央位置
|
||||
def YB_NormalLiq_100mL_Carrier(name: str) -> BottleCarrier:
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
carrier_size_x = 127.8
|
||||
@@ -307,16 +310,52 @@ def YB_100ml_yeti(name: str) -> BottleCarrier:
|
||||
resource_size_y=beaker_diameter,
|
||||
name_prefix=name,
|
||||
),
|
||||
model="YB_100ml_yeti",
|
||||
model="YB_NormalLiq_100mL_Carrier",
|
||||
)
|
||||
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")
|
||||
carrier[0] = YB_NormalLiq_100mL_Bottle(f"{name}_flask_1")
|
||||
return carrier
|
||||
|
||||
# 5ml分液瓶板 - 4x2布局,8个位置
|
||||
def YB_5ml_fenyepingban(name: str) -> BottleCarrier:
|
||||
# 100mL高粘液瓶载架 - 单个中央位置
|
||||
def YB_HighVis_100mL_Carrier(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_HighVis_100mL_Carrier",
|
||||
)
|
||||
carrier.num_items_x = 1
|
||||
carrier.num_items_y = 1
|
||||
carrier.num_items_z = 1
|
||||
carrier[0] = YB_HighVis_100mL_Bottle(f"{name}_flask_1")
|
||||
return carrier
|
||||
|
||||
# 5mL分液瓶板 - 4x2布局,8个位置
|
||||
def YB_Vial_5mL_Carrier(name: str) -> BottleCarrier:
|
||||
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
@@ -355,18 +394,18 @@ def YB_5ml_fenyepingban(name: str) -> BottleCarrier:
|
||||
size_y=carrier_size_y,
|
||||
size_z=carrier_size_z,
|
||||
sites=sites,
|
||||
model="YB_5ml_fenyepingban",
|
||||
model="YB_Vial_5mL_Carrier",
|
||||
)
|
||||
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]}")
|
||||
carrier[i] = YB_Vial_5mL(f"{name}_vial_{ordering[i]}")
|
||||
return carrier
|
||||
|
||||
# 20ml分液瓶板 - 4x2布局,8个位置
|
||||
def YB_20ml_fenyepingban(name: str) -> BottleCarrier:
|
||||
# 20mL分液瓶板 - 4x2布局,8个位置
|
||||
def YB_Vial_20mL_Carrier(name: str) -> BottleCarrier:
|
||||
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
@@ -405,18 +444,18 @@ def YB_20ml_fenyepingban(name: str) -> BottleCarrier:
|
||||
size_y=carrier_size_y,
|
||||
size_z=carrier_size_z,
|
||||
sites=sites,
|
||||
model="YB_20ml_fenyepingban",
|
||||
model="YB_Vial_20mL_Carrier",
|
||||
)
|
||||
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]}")
|
||||
carrier[i] = YB_Vial_20mL(f"{name}_vial_{ordering[i]}")
|
||||
return carrier
|
||||
|
||||
# 配液瓶(小)板 - 4x2布局,8个位置
|
||||
def YB_peiyepingxiaoban(name: str) -> BottleCarrier:
|
||||
def YB_PrepBottle_15mL_Carrier(name: str) -> BottleCarrier:
|
||||
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
@@ -455,19 +494,19 @@ def YB_peiyepingxiaoban(name: str) -> BottleCarrier:
|
||||
size_y=carrier_size_y,
|
||||
size_z=carrier_size_z,
|
||||
sites=sites,
|
||||
model="YB_peiyepingxiaoban",
|
||||
model="YB_PrepBottle_15mL_Carrier",
|
||||
)
|
||||
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]}")
|
||||
carrier[i] = YB_PrepBottle_15mL(f"{name}_bottle_{ordering[i]}")
|
||||
return carrier
|
||||
|
||||
|
||||
# 配液瓶(大)板 - 2x2布局,4个位置
|
||||
def YB_peiyepingdaban(name: str) -> BottleCarrier:
|
||||
def YB_PrepBottle_60mL_Carrier(name: str) -> BottleCarrier:
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
carrier_size_x = 127.8
|
||||
@@ -505,18 +544,18 @@ def YB_peiyepingdaban(name: str) -> BottleCarrier:
|
||||
size_y=carrier_size_y,
|
||||
size_z=carrier_size_z,
|
||||
sites=sites,
|
||||
model="YB_peiyepingdaban",
|
||||
model="YB_PrepBottle_60mL_Carrier",
|
||||
)
|
||||
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]}")
|
||||
carrier[i] = YB_PrepBottle_60mL(f"{name}_bottle_{ordering[i]}")
|
||||
return carrier
|
||||
|
||||
# 加样头(大)板 - 1x1布局,1个位置
|
||||
def YB_jia_yang_tou_da_Carrier(name: str) -> BottleCarrier:
|
||||
def YB_DosingHead_L_Carrier(name: str) -> BottleCarrier:
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
carrier_size_x = 127.8
|
||||
@@ -554,16 +593,16 @@ def YB_jia_yang_tou_da_Carrier(name: str) -> BottleCarrier:
|
||||
size_y=carrier_size_y,
|
||||
size_z=carrier_size_z,
|
||||
sites=sites,
|
||||
model="YB_jia_yang_tou_da_Carrier",
|
||||
model="YB_DosingHead_L_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")
|
||||
carrier[0] = YB_DosingHead_L(f"{name}_head_1")
|
||||
return carrier
|
||||
|
||||
|
||||
def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier:
|
||||
def YB_Adapter_60mL(name: str) -> BottleCarrier:
|
||||
"""适配器块 - 单个中央位置"""
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
@@ -591,7 +630,7 @@ def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier:
|
||||
resource_size_y=adapter_diameter,
|
||||
name_prefix=name,
|
||||
),
|
||||
model="YB_shi_pei_qi_kuai",
|
||||
model="YB_Adapter_60mL",
|
||||
)
|
||||
carrier.num_items_x = 1
|
||||
carrier.num_items_y = 1
|
||||
@@ -600,7 +639,7 @@ def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier:
|
||||
return carrier
|
||||
|
||||
|
||||
def YB_qiang_tou_he(name: str) -> BottleCarrier:
|
||||
def YB_TipRack_50uL(name: str) -> BottleCarrier:
|
||||
"""枪头盒 - 8x12布局,96个位置"""
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
@@ -609,9 +648,9 @@ def YB_qiang_tou_he(name: str) -> BottleCarrier:
|
||||
carrier_size_z = 55.0
|
||||
|
||||
# 枪头尺寸
|
||||
tip_diameter = 10.0
|
||||
tip_spacing_x = 9.0 # X方向间距
|
||||
tip_spacing_y = 9.0 # Y方向间距
|
||||
tip_diameter = 7.0
|
||||
tip_spacing_x = 7.5 # X方向间距
|
||||
tip_spacing_y = 7.5 # Y方向间距
|
||||
|
||||
# 计算起始位置 (居中排列)
|
||||
start_x = (carrier_size_x - (12 - 1) * tip_spacing_x - tip_diameter) / 2
|
||||
@@ -639,7 +678,7 @@ def YB_qiang_tou_he(name: str) -> BottleCarrier:
|
||||
size_y=carrier_size_y,
|
||||
size_z=carrier_size_z,
|
||||
sites=sites,
|
||||
model="YB_qiang_tou_he",
|
||||
model="YB_TipRack_50uL",
|
||||
)
|
||||
carrier.num_items_x = 12
|
||||
carrier.num_items_y = 8
|
||||
@@ -648,6 +687,182 @@ def YB_qiang_tou_he(name: str) -> BottleCarrier:
|
||||
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}")
|
||||
carrier[i] = YB_Tip_50uL(f"{name}_tip_{row}{col}")
|
||||
return carrier
|
||||
|
||||
|
||||
def YB_TipRack_5000uL(name: str) -> BottleCarrier:
|
||||
"""枪头盒 - 4x6布局,24个位置"""
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
carrier_size_x = 127.8
|
||||
carrier_size_y = 85.5
|
||||
carrier_size_z = 95.0
|
||||
|
||||
# 枪头尺寸
|
||||
tip_diameter = 16.0
|
||||
tip_spacing_x = 16.5 # X方向间距
|
||||
tip_spacing_y = 16.5 # Y方向间距
|
||||
|
||||
# 计算起始位置 (居中排列)
|
||||
start_x = (carrier_size_x - (6 - 1) * tip_spacing_x - tip_diameter) / 2
|
||||
start_y = (carrier_size_y - (4 - 1) * tip_spacing_y - tip_diameter) / 2
|
||||
|
||||
sites = create_ordered_items_2d(
|
||||
klass=ResourceHolder,
|
||||
num_items_x=6,
|
||||
num_items_y=4,
|
||||
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_TipRack_5000uL",
|
||||
)
|
||||
carrier.num_items_x = 6
|
||||
carrier.num_items_y = 4
|
||||
carrier.num_items_z = 1
|
||||
# 创建24个枪头
|
||||
for i in range(24):
|
||||
row = chr(65 + i // 6) # A-D
|
||||
col = (i % 6) + 1 # 1-6
|
||||
carrier[i] = YB_Tip_5000uL(f"{name}_tip_{row}{col}")
|
||||
return carrier
|
||||
|
||||
|
||||
|
||||
def YB_TipRack_Mixed(name: str) -> BottleCarrier:
|
||||
"""混合枪头盒 - 复杂布局
|
||||
上层: 2x8空位(原50uL枪头位置,现空余)
|
||||
中层: 4x4布局,放5000uL枪头
|
||||
下层: 2x8布局,放1000uL枪头
|
||||
"""
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
carrier_size_x = 127.8
|
||||
carrier_size_y = 85.5
|
||||
carrier_size_z = 95.0
|
||||
|
||||
# 各类枪头的尺寸参数
|
||||
tip_5000_diameter = 16.0
|
||||
tip_5000_spacing_x = 16.5
|
||||
tip_5000_spacing_y = 16.5
|
||||
|
||||
tip_1000_diameter = 7.0
|
||||
tip_1000_spacing_x = 7.5
|
||||
tip_1000_spacing_y = 7.5
|
||||
|
||||
# 空位尺寸(上层2x8,原50uL位置)
|
||||
empty_diameter = 7.0
|
||||
empty_spacing_x = 7.5
|
||||
empty_spacing_y = 7.5
|
||||
|
||||
# 计算各层的起始位置
|
||||
# 上层空位 (2x8)
|
||||
empty_top_start_x = (carrier_size_x - (8 - 1) * empty_spacing_x - empty_diameter) / 2
|
||||
empty_top_start_y = 5.0
|
||||
|
||||
# 中层5000uL (4x4)
|
||||
tip_5000_start_x = (carrier_size_x - (4 - 1) * tip_5000_spacing_x - tip_5000_diameter) / 2
|
||||
tip_5000_start_y = empty_top_start_y + 2 * empty_spacing_y + 5.0
|
||||
|
||||
# 下层1000uL (2x8)
|
||||
tip_1000_start_x = (carrier_size_x - (8 - 1) * tip_1000_spacing_x - tip_1000_diameter) / 2
|
||||
tip_1000_start_y = tip_5000_start_y + 4 * tip_5000_spacing_y + 5.0
|
||||
|
||||
sites = {}
|
||||
|
||||
# 创建上层空位 (2x8) - 不创建实际的枪头对象
|
||||
empty_top_sites = create_ordered_items_2d(
|
||||
klass=ResourceHolder,
|
||||
num_items_x=8,
|
||||
num_items_y=2,
|
||||
dx=empty_top_start_x,
|
||||
dy=empty_top_start_y,
|
||||
dz=5.0,
|
||||
item_dx=empty_spacing_x,
|
||||
item_dy=empty_spacing_y,
|
||||
size_x=empty_diameter,
|
||||
size_y=empty_diameter,
|
||||
size_z=carrier_size_z,
|
||||
)
|
||||
# 添加空位,索引 0-15
|
||||
for k, v in empty_top_sites.items():
|
||||
v.name = f"{name}_empty_top_{v.name}"
|
||||
sites[k] = v
|
||||
|
||||
# 创建中层5000uL枪头位 (4x4),索引 16-31
|
||||
tip_5000_sites = create_ordered_items_2d(
|
||||
klass=ResourceHolder,
|
||||
num_items_x=4,
|
||||
num_items_y=4,
|
||||
dx=tip_5000_start_x,
|
||||
dy=tip_5000_start_y,
|
||||
dz=15.0,
|
||||
item_dx=tip_5000_spacing_x,
|
||||
item_dy=tip_5000_spacing_y,
|
||||
size_x=tip_5000_diameter,
|
||||
size_y=tip_5000_diameter,
|
||||
size_z=carrier_size_z,
|
||||
)
|
||||
for i, (k, v) in enumerate(tip_5000_sites.items()):
|
||||
v.name = f"{name}_5000_{v.name}"
|
||||
sites[16 + i] = v
|
||||
|
||||
# 创建下层1000uL枪头位 (2x8),索引 32-47
|
||||
tip_1000_sites = create_ordered_items_2d(
|
||||
klass=ResourceHolder,
|
||||
num_items_x=8,
|
||||
num_items_y=2,
|
||||
dx=tip_1000_start_x,
|
||||
dy=tip_1000_start_y,
|
||||
dz=25.0,
|
||||
item_dx=tip_1000_spacing_x,
|
||||
item_dy=tip_1000_spacing_y,
|
||||
size_x=tip_1000_diameter,
|
||||
size_y=tip_1000_diameter,
|
||||
size_z=carrier_size_z,
|
||||
)
|
||||
for i, (k, v) in enumerate(tip_1000_sites.items()):
|
||||
v.name = f"{name}_1000_{v.name}"
|
||||
sites[32 + i] = v
|
||||
|
||||
carrier = BottleCarrier(
|
||||
name=name,
|
||||
size_x=carrier_size_x,
|
||||
size_y=carrier_size_y,
|
||||
size_z=carrier_size_z,
|
||||
sites=sites,
|
||||
model="YB_TipRack_Mixed",
|
||||
)
|
||||
carrier.num_items_x = 8 # 最大宽度
|
||||
carrier.num_items_y = 8 # 总行数 (2+4+2)
|
||||
carrier.num_items_z = 1
|
||||
|
||||
# 为5000uL枪头创建实例 (16个),对应索引 16-31
|
||||
for i in range(16):
|
||||
row = chr(65 + i // 4) # A-D
|
||||
col = (i % 4) + 1 # 1-4
|
||||
carrier[16 + i] = YB_Tip_5000uL(f"{name}_tip5000_{row}{col}")
|
||||
|
||||
# 为1000uL枪头创建实例 (16个),对应索引 32-47
|
||||
for i in range(16):
|
||||
row = chr(65 + i // 8) # A-B
|
||||
col = (i % 8) + 1 # 1-8
|
||||
carrier[32 + i] = YB_Tip_1000uL(f"{name}_tip1000_{row}{col}")
|
||||
|
||||
return carrier
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
|
||||
# 工厂函数
|
||||
"""加样头(大)"""
|
||||
def YB_jia_yang_tou_da(
|
||||
def YB_DosingHead_L(
|
||||
name: str,
|
||||
diameter: float = 20.0,
|
||||
height: float = 100.0,
|
||||
@@ -15,11 +15,11 @@ def YB_jia_yang_tou_da(
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="YB_jia_yang_tou_da",
|
||||
model="YB_DosingHead_L",
|
||||
)
|
||||
|
||||
"""液1x1"""
|
||||
def YB_ye_Bottle(
|
||||
"""250mL普通液"""
|
||||
def YB_NormalLiq_250mL_Bottle(
|
||||
name: str,
|
||||
diameter: float = 40.0,
|
||||
height: float = 70.0,
|
||||
@@ -33,87 +33,105 @@ def YB_ye_Bottle(
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="YB_ye_Bottle",
|
||||
model="YB_NormalLiq_250mL_Bottle",
|
||||
)
|
||||
|
||||
"""100ml液体"""
|
||||
def YB_ye_100ml_Bottle(
|
||||
"""100mL普通液"""
|
||||
def YB_NormalLiq_100mL_Bottle(
|
||||
name: str,
|
||||
diameter: float = 50.0,
|
||||
height: float = 90.0,
|
||||
max_volume: float = 100000.0, # 100mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建100ml液体瓶"""
|
||||
"""创建100mL普通液瓶"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="YB_100ml_yeti",
|
||||
model="YB_NormalLiq_100mL_Bottle",
|
||||
)
|
||||
|
||||
"""高粘液"""
|
||||
def YB_gao_nian_ye_Bottle(
|
||||
"""100mL高粘液"""
|
||||
def YB_HighVis_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_HighVis_100mL_Bottle",
|
||||
)
|
||||
|
||||
"""250mL高粘液"""
|
||||
def YB_HighVis_250mL_Bottle(
|
||||
name: str,
|
||||
diameter: float = 40.0,
|
||||
height: float = 70.0,
|
||||
max_volume: float = 50000.0, # 50mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建高粘液瓶"""
|
||||
"""创建250mL高粘液瓶"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="High_Viscosity_Liquid",
|
||||
model="YB_HighVis_250mL_Bottle",
|
||||
)
|
||||
|
||||
"""5ml分液瓶"""
|
||||
def YB_5ml_fenyeping(
|
||||
"""5mL分液瓶"""
|
||||
def YB_Vial_5mL(
|
||||
name: str,
|
||||
diameter: float = 20.0,
|
||||
height: float = 50.0,
|
||||
max_volume: float = 5000.0, # 5mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建5ml分液瓶"""
|
||||
"""创建5mL分液瓶"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="YB_5ml_fenyeping",
|
||||
model="YB_Vial_5mL",
|
||||
)
|
||||
|
||||
"""20ml分液瓶"""
|
||||
def YB_20ml_fenyeping(
|
||||
"""20mL分液瓶"""
|
||||
def YB_Vial_20mL(
|
||||
name: str,
|
||||
diameter: float = 30.0,
|
||||
height: float = 65.0,
|
||||
max_volume: float = 20000.0, # 20mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建20ml分液瓶"""
|
||||
"""创建20mL分液瓶"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="YB_20ml_fenyeping",
|
||||
model="YB_Vial_20mL",
|
||||
)
|
||||
|
||||
"""配液瓶(小)"""
|
||||
def YB_pei_ye_xiao_Bottle(
|
||||
def YB_PrepBottle_15mL(
|
||||
name: str,
|
||||
diameter: float = 35.0,
|
||||
height: float = 60.0,
|
||||
max_volume: float = 30000.0, # 30mL
|
||||
max_volume: float = 15000.0, # 15mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建配液瓶(小)"""
|
||||
@@ -123,15 +141,15 @@ def YB_pei_ye_xiao_Bottle(
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="YB_pei_ye_xiao_Bottle",
|
||||
model="YB_PrepBottle_15mL",
|
||||
)
|
||||
|
||||
"""配液瓶(大)"""
|
||||
def YB_pei_ye_da_Bottle(
|
||||
def YB_PrepBottle_60mL(
|
||||
name: str,
|
||||
diameter: float = 55.0,
|
||||
height: float = 100.0,
|
||||
max_volume: float = 150000.0, # 150mL
|
||||
max_volume: float = 60000.0, # 60mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建配液瓶(大)"""
|
||||
@@ -141,11 +159,29 @@ def YB_pei_ye_da_Bottle(
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="YB_pei_ye_da_Bottle",
|
||||
model="YB_PrepBottle_60mL",
|
||||
)
|
||||
|
||||
"""枪头"""
|
||||
def YB_qiang_tou(
|
||||
"""5000uL枪头"""
|
||||
def YB_Tip_5000uL(
|
||||
name: str,
|
||||
diameter: float = 10.0,
|
||||
height: float = 50.0,
|
||||
max_volume: float = 5000.0, # 5mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建枪头"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="YB_Tip_5000uL",
|
||||
)
|
||||
|
||||
"""1000uL枪头"""
|
||||
def YB_Tip_1000uL(
|
||||
name: str,
|
||||
diameter: float = 10.0,
|
||||
height: float = 50.0,
|
||||
@@ -159,5 +195,23 @@ def YB_qiang_tou(
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="YB_qiang_tou",
|
||||
model="YB_Tip_1000uL",
|
||||
)
|
||||
|
||||
"""50uL枪头"""
|
||||
def YB_Tip_50uL(
|
||||
name: str,
|
||||
diameter: float = 10.0,
|
||||
height: float = 50.0,
|
||||
max_volume: float = 50.0, # 50uL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建枪头"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="YB_Tip_50uL",
|
||||
)
|
||||
@@ -1,4 +1,3 @@
|
||||
from os import name
|
||||
from pylabrobot.resources import Deck, Coordinate, Rotation
|
||||
|
||||
from unilabos.resources.bioyond.YB_warehouses import (
|
||||
@@ -34,11 +33,8 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
|
||||
size_y: float = 1080.0,
|
||||
size_z: float = 1500.0,
|
||||
category: str = "deck",
|
||||
setup: bool = False
|
||||
) -> None:
|
||||
super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0)
|
||||
if setup:
|
||||
self.setup()
|
||||
|
||||
def setup(self) -> None:
|
||||
# 添加仓库
|
||||
@@ -66,6 +62,7 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
|
||||
for warehouse_name, warehouse in self.warehouses.items():
|
||||
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||
|
||||
|
||||
class BIOYOND_PolymerPreparationStation_Deck(Deck):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -74,11 +71,8 @@ class BIOYOND_PolymerPreparationStation_Deck(Deck):
|
||||
size_y: float = 1080.0,
|
||||
size_z: float = 1500.0,
|
||||
category: str = "deck",
|
||||
setup: bool = False
|
||||
) -> None:
|
||||
super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0)
|
||||
if setup:
|
||||
self.setup()
|
||||
|
||||
def setup(self) -> None:
|
||||
# 添加仓库 - 配液站的3个堆栈,使用Bioyond系统中的实际名称
|
||||
@@ -101,7 +95,8 @@ class BIOYOND_PolymerPreparationStation_Deck(Deck):
|
||||
for warehouse_name, warehouse in self.warehouses.items():
|
||||
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||
|
||||
class BIOYOND_YB_Deck(Deck):
|
||||
|
||||
class BioyondElectrolyteDeck(Deck):
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "YB_Deck",
|
||||
@@ -109,7 +104,7 @@ class BIOYOND_YB_Deck(Deck):
|
||||
size_y: float = 1400.0,
|
||||
size_z: float = 2670.0,
|
||||
category: str = "deck",
|
||||
setup: bool = False
|
||||
setup: bool = False,
|
||||
) -> None:
|
||||
super().__init__(name=name, size_x=4150.0, size_y=1400.0, size_z=2670.0)
|
||||
if setup:
|
||||
@@ -118,8 +113,8 @@ class BIOYOND_YB_Deck(Deck):
|
||||
def setup(self) -> None:
|
||||
# 添加仓库
|
||||
self.warehouses = {
|
||||
"321窗口": bioyond_warehouse_2x2x1("321窗口"), # 2行×2列
|
||||
"43窗口": bioyond_warehouse_2x2x1("43窗口"), # 2行×2列
|
||||
"自动堆栈-左": bioyond_warehouse_2x2x1("自动堆栈-左"), # 2行×2列
|
||||
"自动堆栈-右": bioyond_warehouse_2x2x1("自动堆栈-右"), # 2行×2列
|
||||
"手动传递窗右": bioyond_warehouse_5x3x1("手动传递窗右", row_offset=0), # A01-E03
|
||||
"手动传递窗左": bioyond_warehouse_5x3x1("手动传递窗左", row_offset=5), # F01-J03
|
||||
"加样头堆栈左": bioyond_warehouse_10x1x1("加样头堆栈左"),
|
||||
@@ -133,29 +128,34 @@ class BIOYOND_YB_Deck(Deck):
|
||||
}
|
||||
# warehouse 的位置
|
||||
self.warehouse_locations = {
|
||||
"321窗口": Coordinate(-150.0, 158.0, 0.0),
|
||||
"43窗口": Coordinate(4160.0, 158.0, 0.0),
|
||||
"手动传递窗左": Coordinate(-150.0, 877.0, 0.0),
|
||||
"手动传递窗右": Coordinate(4160.0, 877.0, 0.0),
|
||||
"加样头堆栈左": Coordinate(385.0, 1300.0, 0.0),
|
||||
"加样头堆栈右": Coordinate(2187.0, 1300.0, 0.0),
|
||||
"自动堆栈-左": Coordinate(-150.0, 1142.0, 0.0),
|
||||
"自动堆栈-右": Coordinate(4160.0, 1142.0, 0.0),
|
||||
"手动传递窗左": Coordinate(-150.0, 423.0, 0.0),
|
||||
"手动传递窗右": Coordinate(4160.0, 423.0, 0.0),
|
||||
"加样头堆栈左": Coordinate(385.0, 0, 0.0),
|
||||
"加样头堆栈右": Coordinate(2187.0, 0, 0.0),
|
||||
|
||||
"15ml配液堆栈左": Coordinate(749.0, 355.0, 0.0),
|
||||
"母液加样右": Coordinate(2152.0, 333.0, 0.0),
|
||||
"大瓶母液堆栈左": Coordinate(1164.0, 676.0, 0.0),
|
||||
"大瓶母液堆栈右": Coordinate(2717.0, 676.0, 0.0),
|
||||
"2号手套箱内部堆栈": Coordinate(-800, -500.0, 0.0), # 新增:位置需根据实际硬件调整
|
||||
"15ml配液堆栈左": Coordinate(749.0, 945.0, 0.0),
|
||||
"母液加样右": Coordinate(2152.0, 967.0, 0.0),
|
||||
"大瓶母液堆栈左": Coordinate(1164.0, 624.0, 0.0),
|
||||
"大瓶母液堆栈右": Coordinate(2717.0, 624.0, 0.0),
|
||||
"2号手套箱内部堆栈": Coordinate(-800, 800.0, 0.0), # 新增:位置需根据实际硬件调整
|
||||
}
|
||||
|
||||
for warehouse_name, warehouse in self.warehouses.items():
|
||||
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||
|
||||
def YB_Deck(name: str) -> Deck:
|
||||
by=BIOYOND_YB_Deck(name=name)
|
||||
by.setup()
|
||||
return by
|
||||
|
||||
|
||||
# 向后兼容别名,日后废弃
|
||||
BIOYOND_YB_Deck = BioyondElectrolyteDeck
|
||||
|
||||
|
||||
def bioyond_electrolyte_deck(name: str) -> BioyondElectrolyteDeck:
|
||||
deck = BioyondElectrolyteDeck(name=name)
|
||||
deck.setup()
|
||||
return deck
|
||||
|
||||
|
||||
# 向后兼容别名,日后废弃
|
||||
def YB_Deck(name: str) -> BioyondElectrolyteDeck:
|
||||
return bioyond_electrolyte_deck(name)
|
||||
|
||||
@@ -797,9 +797,10 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
||||
bottle = plr_material[number] = initialize_resource(
|
||||
{"name": f'{detail["name"]}_{number}', "class": reverse_type_mapping[typeName][0]}, resource_type=ResourcePLR
|
||||
)
|
||||
bottle.tracker.liquids = [
|
||||
(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)
|
||||
]
|
||||
if hasattr(bottle, 'tracker') and bottle.tracker is not None:
|
||||
bottle.tracker.liquids = [
|
||||
(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)
|
||||
]
|
||||
bottle.code = detail.get("code", "")
|
||||
logger.debug(f" └─ [子物料] {detail['name']} → {plr_material.name}[{number}] (类型:{typeName})")
|
||||
else:
|
||||
@@ -808,9 +809,10 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
||||
# 只对有 capacity 属性的容器(液体容器)处理液体追踪
|
||||
if hasattr(plr_material, 'capacity'):
|
||||
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
|
||||
bottle.tracker.liquids = [
|
||||
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
|
||||
]
|
||||
if hasattr(bottle, 'tracker') and bottle.tracker is not None:
|
||||
bottle.tracker.liquids = [
|
||||
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
|
||||
]
|
||||
|
||||
plr_materials.append(plr_material)
|
||||
|
||||
@@ -1033,7 +1035,7 @@ def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict
|
||||
logger.debug(f"🔍 [PLR→Bioyond] detail转换: {bottle.name} → PLR(x={site['x']},y={site['y']},id={site.get('identifier','?')}) → Bioyond(x={bioyond_x},y={bioyond_y})")
|
||||
|
||||
# 🔥 提取物料名称:从 tracker.liquids 中获取第一个液体的名称(去除PLR系统添加的后缀)
|
||||
# tracker.liquids 格式: [(物料名称, 数量), ...]
|
||||
# tracker.liquids 格式: [(物料名称, 数量, 单位), ...]
|
||||
material_name = bottle_type_info[0] # 默认使用类型名称(如"样品瓶")
|
||||
if hasattr(bottle, "tracker") and bottle.tracker.liquids:
|
||||
# 如果有液体,使用液体的名称
|
||||
@@ -1051,7 +1053,7 @@ def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict
|
||||
"typeId": bottle_type_info[1],
|
||||
"code": bottle.code if hasattr(bottle, "code") else "",
|
||||
"name": material_name, # 使用物料名称(如"9090"),而不是类型名称("样品瓶")
|
||||
"quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
|
||||
"quantity": sum(qty for _, qty, *_ in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
|
||||
"x": bioyond_x,
|
||||
"y": bioyond_y,
|
||||
"z": 1,
|
||||
@@ -1124,7 +1126,7 @@ def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict
|
||||
"barCode": "",
|
||||
"name": material_name, # 使用物料名称而不是资源名称
|
||||
"unit": default_unit, # 使用配置的单位或默认单位
|
||||
"quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
|
||||
"quantity": sum(qty for _, qty, *_ in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
|
||||
"Parameters": parameters_json # API 实际要求的字段(必需)
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,35 @@ class ItemizedCarrier(ResourcePLR):
|
||||
idx = i
|
||||
break
|
||||
|
||||
if idx is None and location is not None:
|
||||
# 精确坐标匹配失败(常见原因:DB 存储的 z=0,而槽位定义 z=dz>0)。
|
||||
# 降级为仅按 XY 坐标进行近似匹配,找到后使用槽位自身的正确坐标写回,
|
||||
# 避免因 Z 偏移导致反序列化中断。
|
||||
_XY_TOLERANCE = 2.0 # mm,覆盖浮点误差和 z 偏移
|
||||
min_dist = float("inf")
|
||||
nearest_idx = None
|
||||
for _i, _loc in enumerate(self.child_locations.values()):
|
||||
_d = (((_loc.x - location.x) ** 2) + ((_loc.y - location.y) ** 2)) ** 0.5
|
||||
if _d < min_dist:
|
||||
min_dist = _d
|
||||
nearest_idx = _i
|
||||
if nearest_idx is not None and min_dist <= _XY_TOLERANCE:
|
||||
from unilabos.utils.log import logger as _logger
|
||||
_slot_label = list(self.child_locations.keys())[nearest_idx]
|
||||
_logger.warning(
|
||||
f"[ItemizedCarrier '{self.name}'] 资源 '{resource.name}' 坐标 {location} 与槽位 "
|
||||
f"'{_slot_label}' {list(self.child_locations.values())[nearest_idx]} 的 XY 吻合"
|
||||
f"(XY 偏差={min_dist:.2f}mm),按 XY 近似匹配成功,z 偏移已被修正。"
|
||||
)
|
||||
idx = nearest_idx
|
||||
|
||||
if idx is None:
|
||||
raise ValueError(
|
||||
f"[ItemizedCarrier '{self.name}'] 无法为资源 '{resource.name}' 找到匹配的槽位。\n"
|
||||
f" 已知槽位: {list(self.child_locations.keys())}\n"
|
||||
f" 传入坐标: {location}\n"
|
||||
f" 提示: XY 近似匹配也失败,请检查资源坐标或 Carrier 槽位定义是否正确。"
|
||||
)
|
||||
if not reassign and self.sites[idx] is not None:
|
||||
raise ValueError(f"a site with index {idx} already exists")
|
||||
location = list(self.child_locations.values())[idx]
|
||||
|
||||
@@ -612,6 +612,31 @@ class ResourceTreeSet(object):
|
||||
d["model"] = res.config.get("model", None)
|
||||
return d
|
||||
|
||||
def _deduplicate_plr_dict(d: dict, _seen: set = None) -> dict:
|
||||
"""递归去除 children 中同名重复节点(全树范围、保留首次出现)。
|
||||
|
||||
根本原因:同一槽位被 sync_from_external(Bioyond 同步)重复写入,
|
||||
导致数据库中同一 WareHouse 下存在多条同名 BottleCarrier 记录(不同 UUID)。
|
||||
PLR 的 _check_naming_conflicts 在全树范围检查名称唯一性,
|
||||
重复名称会在 deserialize 时抛出 ValueError,导致节点启动失败。
|
||||
此函数在 sub_cls.deserialize 前预先清理,保证名称唯一。
|
||||
"""
|
||||
if _seen is None:
|
||||
_seen = set()
|
||||
children = d.get("children", [])
|
||||
deduped = []
|
||||
for child in children:
|
||||
child = _deduplicate_plr_dict(child, _seen)
|
||||
cname = child.get("name")
|
||||
if cname not in _seen:
|
||||
_seen.add(cname)
|
||||
deduped.append(child)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[资源树去重] 发现重复资源名称 '{cname}',跳过重复项(历史脏数据)"
|
||||
)
|
||||
return {**d, "children": deduped}
|
||||
|
||||
plr_resources = []
|
||||
tracker = DeviceNodeResourceTracker()
|
||||
|
||||
@@ -622,6 +647,8 @@ class ResourceTreeSet(object):
|
||||
collect_node_data(tree.root_node, name_to_uuid, all_states, name_to_extra)
|
||||
has_model = tree.root_node.res_content.type != "deck"
|
||||
plr_dict = node_to_plr_dict(tree.root_node, has_model)
|
||||
plr_dict = _deduplicate_plr_dict(plr_dict)
|
||||
|
||||
try:
|
||||
sub_cls = find_subclass(plr_dict["type"], PLRResource)
|
||||
if skip_devices and plr_dict["type"] == "device":
|
||||
@@ -640,6 +667,14 @@ class ResourceTreeSet(object):
|
||||
|
||||
location = cast(Coordinate, deserialize(plr_dict["location"]))
|
||||
plr_resource.location = location
|
||||
|
||||
# 预填 Container 类型资源在新版 PLR 中要求必须存在的键,
|
||||
# 防止旧数据库状态缺失这些键时 load_all_state 抛出 KeyError。
|
||||
for state in all_states.values():
|
||||
if isinstance(state, dict):
|
||||
state.setdefault("liquid_history", [])
|
||||
state.setdefault("pending_liquids", {})
|
||||
|
||||
plr_resource.load_all_state(all_states)
|
||||
# 使用 DeviceNodeResourceTracker 设置 UUID 和 Extra
|
||||
tracker.loop_set_uuid(plr_resource, name_to_uuid)
|
||||
|
||||
@@ -41,8 +41,9 @@ def warehouse_factory(
|
||||
|
||||
# 根据 layout 决定 y 坐标计算
|
||||
if layout == "row-major":
|
||||
# 行优先:row=0(A行) 应该显示在上方,需要较小的 y 值
|
||||
y = dy + row * item_dy
|
||||
# 行优先:row=0(A行) 应该显示在上方
|
||||
# 前端现在 y 越大越靠上,所以 row=0 对应最大的 y
|
||||
y = dy + (num_items_y - row - 1) * item_dy
|
||||
elif layout == "vertical-col-major":
|
||||
# 竖向warehouse: row=0 对应顶部(y小),row=n-1 对应底部(y大)
|
||||
# 但标签 01 应该在底部,所以使用反向映射
|
||||
|
||||
@@ -4,6 +4,8 @@ import json
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from unilabos.utils.tools import fast_dumps_str as _fast_dumps_str, fast_loads as _fast_loads
|
||||
from typing import (
|
||||
get_type_hints,
|
||||
TypeVar,
|
||||
@@ -78,6 +80,67 @@ if TYPE_CHECKING:
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class RclpyAsyncMutex:
|
||||
"""rclpy executor 兼容的异步互斥锁
|
||||
|
||||
通过 executor.create_task 唤醒等待者,避免 timer 的 InvalidHandle 问题。
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = ""):
|
||||
self._lock = threading.Lock()
|
||||
self._acquired = False
|
||||
self._queue: List[Future] = []
|
||||
self._name = name
|
||||
self._holder: Optional[str] = None
|
||||
|
||||
async def acquire(self, node: "BaseROS2DeviceNode", tag: str = ""):
|
||||
"""获取锁。如果已被占用,则异步等待直到锁释放。"""
|
||||
# t0 = time.time()
|
||||
with self._lock:
|
||||
# qlen = len(self._queue)
|
||||
if not self._acquired:
|
||||
self._acquired = True
|
||||
self._holder = tag
|
||||
# node.lab_logger().debug(
|
||||
# f"[Mutex:{self._name}] 获取锁 tag={tag} (无等待, queue=0)"
|
||||
# )
|
||||
return
|
||||
waiter = Future()
|
||||
self._queue.append(waiter)
|
||||
# node.lab_logger().info(
|
||||
# f"[Mutex:{self._name}] 等待锁 tag={tag} "
|
||||
# f"(holder={self._holder}, queue={qlen + 1})"
|
||||
# )
|
||||
await waiter
|
||||
# wait_ms = (time.time() - t0) * 1000
|
||||
self._holder = tag
|
||||
# node.lab_logger().info(
|
||||
# f"[Mutex:{self._name}] 获取锁 tag={tag} (等了 {wait_ms:.0f}ms)"
|
||||
# )
|
||||
|
||||
def release(self, node: "BaseROS2DeviceNode"):
|
||||
"""释放锁,通过 executor task 唤醒下一个等待者。"""
|
||||
with self._lock:
|
||||
# old_holder = self._holder
|
||||
if self._queue:
|
||||
next_waiter = self._queue.pop(0)
|
||||
# node.lab_logger().debug(
|
||||
# f"[Mutex:{self._name}] 释放锁 holder={old_holder} → 唤醒下一个 (剩余 queue={len(self._queue)})"
|
||||
# )
|
||||
|
||||
async def _wake():
|
||||
if not next_waiter.done():
|
||||
next_waiter.set_result(None)
|
||||
|
||||
rclpy.get_global_executor().create_task(_wake())
|
||||
else:
|
||||
self._acquired = False
|
||||
self._holder = None
|
||||
# node.lab_logger().debug(
|
||||
# f"[Mutex:{self._name}] 释放锁 holder={old_holder} → 空闲"
|
||||
# )
|
||||
|
||||
|
||||
# 在线设备注册表
|
||||
registered_devices: Dict[str, "DeviceInfoType"] = {}
|
||||
|
||||
@@ -355,6 +418,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
max_workers=max(len(action_value_mappings), 1), thread_name_prefix=f"ROSDevice{self.device_id}"
|
||||
)
|
||||
|
||||
self._append_resource_lock = RclpyAsyncMutex(name=f"AR:{device_id}")
|
||||
|
||||
# 创建资源管理客户端
|
||||
self._resource_clients: Dict[str, Client] = {
|
||||
"resource_add": self.create_client(ResourceAdd, "/resources/add", callback_group=self.callback_group),
|
||||
@@ -378,15 +443,40 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
return res
|
||||
|
||||
async def append_resource(req: SerialCommand_Request, res: SerialCommand_Response):
|
||||
_cmd = _fast_loads(req.command)
|
||||
_res_name = _cmd.get("resource", [{}])
|
||||
_res_name = (_res_name[0].get("id", "?") if isinstance(_res_name, list) and _res_name
|
||||
else _res_name.get("id", "?") if isinstance(_res_name, dict) else "?")
|
||||
_ar_tag = f"{_res_name}"
|
||||
# _t_enter = time.time()
|
||||
# self.lab_logger().info(f"[AR:{_ar_tag}] 进入 append_resource")
|
||||
await self._append_resource_lock.acquire(self, tag=_ar_tag)
|
||||
# _t_locked = time.time()
|
||||
try:
|
||||
return await _append_resource_inner(req, res, _ar_tag)
|
||||
# _t_done = time.time()
|
||||
# self.lab_logger().info(
|
||||
# f"[AR:{_ar_tag}] 完成 "
|
||||
# f"等锁={(_t_locked - _t_enter) * 1000:.0f}ms "
|
||||
# f"执行={(_t_done - _t_locked) * 1000:.0f}ms "
|
||||
# f"总计={(_t_done - _t_enter) * 1000:.0f}ms"
|
||||
# )
|
||||
except Exception as _ex:
|
||||
self.lab_logger().error(f"[AR:{_ar_tag}] 异常: {_ex}")
|
||||
raise
|
||||
finally:
|
||||
self._append_resource_lock.release(self)
|
||||
|
||||
async def _append_resource_inner(req: SerialCommand_Request, res: SerialCommand_Response, _ar_tag: str = ""):
|
||||
from pylabrobot.resources.deck import Deck
|
||||
from pylabrobot.resources import Coordinate
|
||||
from pylabrobot.resources import Plate
|
||||
|
||||
# 物料传输到对应的node节点
|
||||
# _t0 = time.time()
|
||||
client = self._resource_clients["c2s_update_resource_tree"]
|
||||
request = SerialCommand.Request()
|
||||
request2 = SerialCommand.Request()
|
||||
command_json = json.loads(req.command)
|
||||
command_json = _fast_loads(req.command)
|
||||
namespace = command_json["namespace"]
|
||||
bind_parent_id = command_json["bind_parent_id"]
|
||||
edge_device_id = command_json["edge_device_id"]
|
||||
@@ -439,7 +529,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
f"更新物料{container_instance.name}出现不支持的数据类型{type(found_resource)} {found_resource}"
|
||||
)
|
||||
# noinspection PyUnresolvedReferences
|
||||
request.command = json.dumps(
|
||||
# _t1 = time.time()
|
||||
# self.lab_logger().debug(
|
||||
# f"[AR:{_ar_tag}] 准备完成 PLR转换+序列化 {((_t1 - _t0) * 1000):.0f}ms, 发送首次上传..."
|
||||
# )
|
||||
request.command = _fast_dumps_str(
|
||||
{
|
||||
"action": "add",
|
||||
"data": {
|
||||
@@ -450,7 +544,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
}
|
||||
)
|
||||
tree_response: SerialCommand.Response = await client.call_async(request)
|
||||
uuid_maps = json.loads(tree_response.response)
|
||||
# _t2 = time.time()
|
||||
# self.lab_logger().debug(
|
||||
# f"[AR:{_ar_tag}] 首次上传完成 {((_t2 - _t1) * 1000):.0f}ms"
|
||||
# )
|
||||
uuid_maps = _fast_loads(tree_response.response)
|
||||
plr_instances = rts.to_plr_resources()
|
||||
for plr_instance in plr_instances:
|
||||
self.resource_tracker.loop_update_uuid(plr_instance, uuid_maps)
|
||||
@@ -508,7 +606,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
for input_well, liquid_type, liquid_volume, liquid_input_slot in zip(
|
||||
input_wells, ADD_LIQUID_TYPE, LIQUID_VOLUME, LIQUID_INPUT_SLOT
|
||||
):
|
||||
input_well.set_liquids([(liquid_type, liquid_volume, "uL")])
|
||||
input_well.set_liquids([(liquid_type, liquid_volume, "ul")])
|
||||
final_response["liquid_input_resource_tree"] = ResourceTreeSet.from_plr_resources(
|
||||
input_wells
|
||||
).dump()
|
||||
@@ -527,12 +625,13 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
Coordinate(location["x"], location["y"], location["z"]),
|
||||
**other_calling_param,
|
||||
)
|
||||
# 调整了液体以及Deck之后要重新Assign
|
||||
# noinspection PyUnresolvedReferences
|
||||
# _t3 = time.time()
|
||||
rts_with_parent = ResourceTreeSet.from_plr_resources([parent_resource])
|
||||
# _n_parent = len(rts_with_parent.all_nodes)
|
||||
if rts_with_parent.root_nodes[0].res_content.uuid_parent is None:
|
||||
rts_with_parent.root_nodes[0].res_content.parent_uuid = self.uuid
|
||||
request.command = json.dumps(
|
||||
request.command = _fast_dumps_str(
|
||||
{
|
||||
"action": "add",
|
||||
"data": {
|
||||
@@ -542,11 +641,18 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
},
|
||||
}
|
||||
)
|
||||
# _t4 = time.time()
|
||||
# self.lab_logger().debug(
|
||||
# f"[AR:{_ar_tag}] 二次上传序列化 {_n_parent}节点 {((_t4 - _t3) * 1000):.0f}ms, 发送中..."
|
||||
# )
|
||||
tree_response: SerialCommand.Response = await client.call_async(request)
|
||||
uuid_maps = json.loads(tree_response.response)
|
||||
# _t5 = time.time()
|
||||
uuid_maps = _fast_loads(tree_response.response)
|
||||
self.resource_tracker.loop_update_uuid(input_resources, uuid_maps)
|
||||
self._lab_logger.info(f"Resource tree added. UUID mapping: {len(uuid_maps)} nodes")
|
||||
# 这里created_resources不包含parent_resource
|
||||
# self._lab_logger.info(
|
||||
# f"[AR:{_ar_tag}] 二次上传完成 HTTP={(_t5 - _t4) * 1000:.0f}ms "
|
||||
# f"UUID映射={len(uuid_maps)}节点 总执行={(_t5 - _t0) * 1000:.0f}ms"
|
||||
# )
|
||||
# 发送给ResourceMeshManager
|
||||
action_client = ActionClient(
|
||||
self,
|
||||
@@ -683,7 +789,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
)
|
||||
# 发送请求并等待响应
|
||||
response: SerialCommand_Response = await self._resource_clients["resource_get"].call_async(r)
|
||||
if not response.response:
|
||||
raise ValueError(f"查询资源 {resource_id} 失败:服务端返回空响应")
|
||||
raw_data = json.loads(response.response)
|
||||
if not raw_data:
|
||||
raise ValueError(f"查询资源 {resource_id} 失败:返回数据为空")
|
||||
|
||||
# 转换为 PLR 资源
|
||||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_data)
|
||||
@@ -1132,7 +1242,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
if uid is None:
|
||||
raise ValueError(f"目标物料{target_resource}没有unilabos_uuid属性,无法转运")
|
||||
target_uids.append(uid)
|
||||
srv_address = f"/srv{target_device_id}/s2c_resource_tree"
|
||||
_ns = target_device_id if target_device_id.startswith("/devices/") else f"/devices/{target_device_id.lstrip('/')}"
|
||||
srv_address = f"/srv{_ns}/s2c_resource_tree"
|
||||
sclient = self.create_client(SerialCommand, srv_address)
|
||||
# 等待服务可用(设置超时)
|
||||
if not sclient.wait_for_service(timeout_sec=5.0):
|
||||
@@ -1182,7 +1293,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
return False
|
||||
time.sleep(0.05)
|
||||
self.lab_logger().info(f"资源本地增加到{target_device_id}结果: {response.response}")
|
||||
return None
|
||||
return "转运完成"
|
||||
|
||||
def register_device(self):
|
||||
"""向注册表中注册设备信息"""
|
||||
@@ -1567,37 +1678,75 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
feedback_msg_types = action_type.Feedback.get_fields_and_field_types()
|
||||
result_msg_types = action_type.Result.get_fields_and_field_types()
|
||||
|
||||
while future is not None and not future.done():
|
||||
if goal_handle.is_cancel_requested:
|
||||
self.lab_logger().info(f"取消动作: {action_name}")
|
||||
future.cancel() # 尝试取消线程池中的任务
|
||||
goal_handle.canceled()
|
||||
return action_type.Result()
|
||||
# 低频 feedback timer(10s),不阻塞完成检测
|
||||
_feedback_timer = None
|
||||
|
||||
self._time_spent = time.time() - time_start
|
||||
self._time_remaining = time_overall - self._time_spent
|
||||
def _publish_feedback():
|
||||
if future is not None and not future.done():
|
||||
self._time_spent = time.time() - time_start
|
||||
self._time_remaining = time_overall - self._time_spent
|
||||
feedback_values = {}
|
||||
for msg_name, attr_name in action_value_mapping["feedback"].items():
|
||||
if hasattr(self.driver_instance, f"get_{attr_name}"):
|
||||
method = getattr(self.driver_instance, f"get_{attr_name}")
|
||||
if not asyncio.iscoroutinefunction(method):
|
||||
feedback_values[msg_name] = method()
|
||||
elif hasattr(self.driver_instance, attr_name):
|
||||
feedback_values[msg_name] = getattr(self.driver_instance, attr_name)
|
||||
if self._print_publish:
|
||||
self.lab_logger().info(f"反馈: {feedback_values}")
|
||||
feedback_msg = convert_to_ros_msg_with_mapping(
|
||||
ros_msg_type=action_type.Feedback(),
|
||||
obj=feedback_values,
|
||||
value_mapping=action_value_mapping["feedback"],
|
||||
)
|
||||
goal_handle.publish_feedback(feedback_msg)
|
||||
|
||||
# 发布反馈
|
||||
feedback_values = {}
|
||||
for msg_name, attr_name in action_value_mapping["feedback"].items():
|
||||
if hasattr(self.driver_instance, f"get_{attr_name}"):
|
||||
method = getattr(self.driver_instance, f"get_{attr_name}")
|
||||
if not asyncio.iscoroutinefunction(method):
|
||||
feedback_values[msg_name] = method()
|
||||
elif hasattr(self.driver_instance, attr_name):
|
||||
feedback_values[msg_name] = getattr(self.driver_instance, attr_name)
|
||||
|
||||
if self._print_publish:
|
||||
self.lab_logger().info(f"反馈: {feedback_values}")
|
||||
|
||||
feedback_msg = convert_to_ros_msg_with_mapping(
|
||||
ros_msg_type=action_type.Feedback(),
|
||||
obj=feedback_values,
|
||||
value_mapping=action_value_mapping["feedback"],
|
||||
if action_value_mapping.get("feedback"):
|
||||
_fb_interval = action_value_mapping.get("feedback_interval", 0.5)
|
||||
_feedback_timer = self.create_timer(
|
||||
_fb_interval, _publish_feedback, callback_group=self.callback_group
|
||||
)
|
||||
|
||||
goal_handle.publish_feedback(feedback_msg)
|
||||
time.sleep(0.5)
|
||||
# 等待 action 完成
|
||||
if future is not None:
|
||||
if isinstance(future, Task):
|
||||
# rclpy Task:直接 await,完成瞬间唤醒
|
||||
try:
|
||||
_raw_result = await future
|
||||
except Exception as e:
|
||||
_raw_result = e
|
||||
else:
|
||||
# concurrent.futures.Future(同步 action):用 rclpy 兼容的轮询
|
||||
_poll_future = Future()
|
||||
|
||||
def _on_sync_done(fut):
|
||||
if not _poll_future.done():
|
||||
_poll_future.set_result(None)
|
||||
|
||||
future.add_done_callback(_on_sync_done)
|
||||
await _poll_future
|
||||
try:
|
||||
_raw_result = future.result()
|
||||
except Exception as e:
|
||||
_raw_result = e
|
||||
|
||||
# 确保 execution_error/success 被正确设置(不依赖 done callback 时序)
|
||||
if isinstance(_raw_result, BaseException):
|
||||
if not execution_error:
|
||||
execution_error = traceback.format_exception(
|
||||
type(_raw_result), _raw_result, _raw_result.__traceback__
|
||||
)
|
||||
execution_error = "".join(execution_error)
|
||||
execution_success = False
|
||||
action_return_value = _raw_result
|
||||
elif not execution_error:
|
||||
execution_success = True
|
||||
action_return_value = _raw_result
|
||||
|
||||
# 清理 feedback timer
|
||||
if _feedback_timer is not None:
|
||||
_feedback_timer.cancel()
|
||||
|
||||
if future is not None and future.cancelled():
|
||||
self.lab_logger().info(f"动作 {action_name} 已取消")
|
||||
@@ -1606,8 +1755,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
# self.lab_logger().info(f"动作执行完成: {action_name}")
|
||||
del future
|
||||
|
||||
# 执行失败时跳过物料状态更新
|
||||
if execution_error:
|
||||
execution_success = False
|
||||
|
||||
# 向Host更新物料当前状态
|
||||
if action_name not in ["create_resource_detailed", "create_resource"]:
|
||||
if not execution_error and action_name not in ["create_resource_detailed", "create_resource"]:
|
||||
for k, v in goal.get_fields_and_field_types().items():
|
||||
if v not in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||
continue
|
||||
@@ -1663,7 +1816,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
|
||||
for attr_name in result_msg_types.keys():
|
||||
if attr_name in ["success", "reached_goal"]:
|
||||
setattr(result_msg, attr_name, True)
|
||||
setattr(result_msg, attr_name, execution_success)
|
||||
elif attr_name == "return_info":
|
||||
setattr(
|
||||
result_msg,
|
||||
@@ -1769,7 +1922,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
raise ValueError("至少需要提供一个 UUID")
|
||||
|
||||
uuids_list = list(uuids)
|
||||
future = self._resource_clients["c2s_update_resource_tree"].call_async(
|
||||
future: Future = self._resource_clients["c2s_update_resource_tree"].call_async(
|
||||
SerialCommand.Request(
|
||||
command=json.dumps(
|
||||
{
|
||||
@@ -1795,6 +1948,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
raise Exception(f"资源查询返回空结果: {uuids_list}")
|
||||
|
||||
raw_data = json.loads(response.response)
|
||||
if not raw_data:
|
||||
raise Exception(f"资源原始查询返回空结果: {raw_data}")
|
||||
|
||||
# 转换为 PLR 资源
|
||||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_data)
|
||||
@@ -1816,10 +1971,15 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
|
||||
mapped_plr_resources = []
|
||||
for uuid in uuids_list:
|
||||
found = None
|
||||
for plr_resource in figured_resources:
|
||||
r = self.resource_tracker.loop_find_with_uuid(plr_resource, uuid)
|
||||
mapped_plr_resources.append(r)
|
||||
break
|
||||
if r is not None:
|
||||
found = r
|
||||
break
|
||||
if found is None:
|
||||
raise Exception(f"未能在已解析的资源树中找到 uuid={uuid} 对应的资源")
|
||||
mapped_plr_resources.append(found)
|
||||
|
||||
return mapped_plr_resources
|
||||
|
||||
@@ -1912,16 +2072,27 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
f"执行动作时JSON缺少function_name或function_args: {ex}\n原JSON: {string}\n{traceback.format_exc()}"
|
||||
)
|
||||
|
||||
async def _convert_resource_async(self, resource_data: Dict[str, Any]):
|
||||
"""异步转换资源数据为实例"""
|
||||
# 使用封装的get_resource_with_dir方法获取PLR资源
|
||||
plr_resource = await self.get_resource_with_dir(resource_ids=resource_data["id"], with_children=True)
|
||||
async def _convert_resource_async(self, resource_data: "ResourceDictType"):
|
||||
"""异步转换 ResourceDictType 为 PLR 实例,优先用 uuid 查询"""
|
||||
unilabos_uuid = resource_data.get("uuid")
|
||||
|
||||
if unilabos_uuid:
|
||||
resource_tree = await self.get_resource([unilabos_uuid], with_children=True)
|
||||
plr_resources = resource_tree.to_plr_resources()
|
||||
if plr_resources:
|
||||
plr_resource = plr_resources[0]
|
||||
else:
|
||||
raise ValueError(f"通过 uuid={unilabos_uuid} 查询资源为空")
|
||||
else:
|
||||
res_id = resource_data.get("id") or resource_data.get("name", "")
|
||||
if not res_id:
|
||||
raise ValueError(f"资源数据缺少 uuid 和 id: {list(resource_data.keys())}")
|
||||
plr_resource = await self.get_resource_with_dir(resource_id=res_id, with_children=True)
|
||||
|
||||
# 通过资源跟踪器获取本地实例
|
||||
res = self.resource_tracker.figure_resource(plr_resource, try_mode=True)
|
||||
if len(res) == 0:
|
||||
# todo: 后续通过decoration来区分,减少warning
|
||||
self.lab_logger().warning(f"资源转换未能索引到实例: {resource_data},返回新建实例")
|
||||
self.lab_logger().warning(f"资源转换未能索引到实例: {resource_data.get('id', '?')},返回新建实例")
|
||||
return plr_resource
|
||||
elif len(res) == 1:
|
||||
return res[0]
|
||||
@@ -2169,4 +2340,4 @@ class DeviceInfoType(TypedDict):
|
||||
status_publishers: Dict[str, PropertyPublisher]
|
||||
actions: Dict[str, ActionServer]
|
||||
hardware_interface: Dict[str, Any]
|
||||
base_node_instance: BaseROS2DeviceNode
|
||||
base_node_instance: BaseROS2DeviceNode
|
||||
@@ -4,6 +4,8 @@ import threading
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
from unilabos.utils.tools import fast_dumps_str as _fast_dumps_str, fast_loads as _fast_loads
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Optional, Dict, Any, List, ClassVar, Set, Union
|
||||
|
||||
@@ -618,22 +620,17 @@ class HostNode(BaseROS2DeviceNode):
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
response: List[str] = await self.create_resource_detailed(
|
||||
resources, device_ids, bind_parent_id, bind_location, other_calling_param
|
||||
)
|
||||
|
||||
try:
|
||||
assert len(response) == 1, "Create Resource应当只返回一个结果"
|
||||
for i in response:
|
||||
res = json.loads(i)
|
||||
if "suc" in res:
|
||||
raise ValueError(res.get("error"))
|
||||
return res
|
||||
except Exception as ex:
|
||||
pass
|
||||
_n = "\n"
|
||||
raise ValueError(f"创建资源时失败!\n{_n.join(response)}")
|
||||
assert len(response) == 1, "Create Resource应当只返回一个结果"
|
||||
for i in response:
|
||||
res = json.loads(i)
|
||||
if "suc" in res and not res["suc"]:
|
||||
raise ValueError(res.get("error", "未知错误"))
|
||||
return res
|
||||
raise ValueError(f"创建资源时失败!响应为空")
|
||||
|
||||
def initialize_device(self, device_id: str, device_config: ResourceDictInstance) -> None:
|
||||
"""
|
||||
@@ -1168,7 +1165,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
else:
|
||||
physical_setup_graph.nodes[resource_dict["id"]]["data"].update(resource_dict.get("data", {}))
|
||||
|
||||
response.response = json.dumps(uuid_mapping) if success else "FAILED"
|
||||
response.response = _fast_dumps_str(uuid_mapping) if success else "FAILED"
|
||||
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
|
||||
|
||||
async def _resource_tree_action_get_callback(self, data: dict, response: SerialCommand_Response): # OK
|
||||
@@ -1178,6 +1175,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
|
||||
resource_response = http_client.resource_tree_get(uuid_list, with_children)
|
||||
response.response = json.dumps(resource_response)
|
||||
self.lab_logger().trace(f"[Host Node-Resource] Resource tree get request callback {response.response}")
|
||||
|
||||
async def _resource_tree_action_remove_callback(self, data: dict, response: SerialCommand_Response):
|
||||
"""
|
||||
@@ -1230,9 +1228,26 @@ class HostNode(BaseROS2DeviceNode):
|
||||
"""
|
||||
try:
|
||||
# 解析请求数据
|
||||
data = json.loads(request.command)
|
||||
data = _fast_loads(request.command)
|
||||
action = data["action"]
|
||||
self.lab_logger().info(f"[Host Node-Resource] Resource tree {action} request received")
|
||||
inner = data.get("data", {})
|
||||
if action == "add":
|
||||
mount_uuid = inner.get("mount_uuid", "?")[:8] if isinstance(inner, dict) else "?"
|
||||
tree_data = inner.get("data", []) if isinstance(inner, dict) else inner
|
||||
node_count = len(tree_data) if isinstance(tree_data, list) else "?"
|
||||
source = f"mount={mount_uuid}.. nodes≈{node_count}"
|
||||
elif action in ("get", "remove"):
|
||||
uid_list = inner.get("data", inner) if isinstance(inner, dict) else inner
|
||||
source = f"uuids={len(uid_list) if isinstance(uid_list, list) else '?'}"
|
||||
elif action == "update":
|
||||
tree_data = inner.get("data", []) if isinstance(inner, dict) else inner
|
||||
node_count = len(tree_data) if isinstance(tree_data, list) else "?"
|
||||
source = f"nodes≈{node_count}"
|
||||
else:
|
||||
source = ""
|
||||
self.lab_logger().info(
|
||||
f"[Host Node-Resource] Resource tree {action} request received ({source})"
|
||||
)
|
||||
data = data["data"]
|
||||
if action == "add":
|
||||
await self._resource_tree_action_add_callback(data, response)
|
||||
|
||||
@@ -22,6 +22,447 @@
|
||||
"arm_state": "idle",
|
||||
"message": "工作台就绪"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "PRCXI",
|
||||
"name": "PRCXI",
|
||||
"type": "device",
|
||||
"class": "liquid_handler.prcxi",
|
||||
"parent": "",
|
||||
"pose": {
|
||||
"size": {
|
||||
"width": 562,
|
||||
"height": 394,
|
||||
"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": true,
|
||||
"setup": true,
|
||||
"is_9320": true,
|
||||
"timeout": 10,
|
||||
"matrix_id": "5de524d0-3f95-406c-86dd-f83626ebc7cb",
|
||||
"simulator": true,
|
||||
"channel_num": 2
|
||||
},
|
||||
"data": {
|
||||
"reset_ok": true
|
||||
},
|
||||
"schema": {},
|
||||
"description": "",
|
||||
"model": null,
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 240,
|
||||
"z": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "PRCXI_Deck",
|
||||
"name": "PRCXI_Deck",
|
||||
"children": [],
|
||||
"parent": "PRCXI",
|
||||
"type": "deck",
|
||||
"class": "",
|
||||
"position": {
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "PRCXI9300Deck",
|
||||
"size_x": 542,
|
||||
"size_y": 374,
|
||||
"size_z": 0,
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"type": "Rotation"
|
||||
},
|
||||
"category": "deck",
|
||||
"barcode": null,
|
||||
"preferred_pickup_location": null,
|
||||
"sites": [
|
||||
{
|
||||
"label": "T1",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"container",
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T2",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 138,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T3",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 276,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T4",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 414,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T5",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 96,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T6",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 138,
|
||||
"y": 96,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T7",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 276,
|
||||
"y": 96,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T8",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 414,
|
||||
"y": 96,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T9",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 192,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T10",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 138,
|
||||
"y": 192,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T11",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 276,
|
||||
"y": 192,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T12",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 414,
|
||||
"y": 192,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T13",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 288,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T14",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 138,
|
||||
"y": 288,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T15",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 276,
|
||||
"y": 288,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "T16",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": {
|
||||
"x": 414,
|
||||
"y": 288,
|
||||
"z": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 86,
|
||||
"depth": 0
|
||||
},
|
||||
"content_type": [
|
||||
"plate",
|
||||
"tip_rack",
|
||||
"plates",
|
||||
"tip_racks",
|
||||
"tube_rack",
|
||||
"adaptor"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"deck": {
|
||||
"data": {
|
||||
"_resource_child_name": "YB_Bioyond_Deck",
|
||||
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck"
|
||||
"_resource_type": "unilabos.resources.bioyond.decks:BioyondElectrolyteDeck"
|
||||
}
|
||||
},
|
||||
"protocol_type": [],
|
||||
@@ -103,15 +103,14 @@
|
||||
"children": [],
|
||||
"parent": "bioyond_cell_workstation",
|
||||
"type": "deck",
|
||||
"class": "BIOYOND_YB_Deck",
|
||||
"class": "BioyondElectrolyteDeck",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"type": "BIOYOND_YB_Deck",
|
||||
"setup": true,
|
||||
"type": "BioyondElectrolyteDeck",
|
||||
"rotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
|
||||
385
unilabos/utils/log-origin.py
Normal file
385
unilabos/utils/log-origin.py
Normal file
@@ -0,0 +1,385 @@
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
from datetime import datetime
|
||||
import ctypes
|
||||
import atexit
|
||||
import inspect
|
||||
from typing import Tuple, cast
|
||||
|
||||
# 添加TRACE级别到logging模块
|
||||
TRACE_LEVEL = 5
|
||||
logging.addLevelName(TRACE_LEVEL, "TRACE")
|
||||
|
||||
|
||||
class CustomRecord:
|
||||
custom_stack_info: Tuple[str, int, str, str]
|
||||
|
||||
|
||||
# Windows颜色支持
|
||||
if platform.system() == "Windows":
|
||||
# 尝试启用Windows终端的ANSI支持
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
# 获取STD_OUTPUT_HANDLE
|
||||
STD_OUTPUT_HANDLE = -11
|
||||
# 启用ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
||||
# 获取当前控制台模式
|
||||
handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
|
||||
mode = ctypes.c_ulong()
|
||||
kernel32.GetConsoleMode(handle, ctypes.byref(mode))
|
||||
# 启用ANSI处理
|
||||
kernel32.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
||||
|
||||
# 程序退出时恢复控制台设置
|
||||
@atexit.register
|
||||
def reset_console():
|
||||
kernel32.SetConsoleMode(handle, mode.value)
|
||||
|
||||
|
||||
# 定义不同日志级别的颜色
|
||||
class ColoredFormatter(logging.Formatter):
|
||||
"""自定义日志格式化器,支持颜色输出"""
|
||||
|
||||
# ANSI 颜色代码
|
||||
COLORS = {
|
||||
"RESET": "\033[0m", # 重置
|
||||
"BOLD": "\033[1m", # 加粗
|
||||
"GRAY": "\033[37m", # 灰色
|
||||
"WHITE": "\033[97m", # 白色
|
||||
"BLACK": "\033[30m", # 黑色
|
||||
"TRACE_LEVEL": "\033[1;90m", # 加粗深灰色
|
||||
"DEBUG_LEVEL": "\033[1;36m", # 加粗青色
|
||||
"INFO_LEVEL": "\033[1;32m", # 加粗绿色
|
||||
"WARNING_LEVEL": "\033[1;33m", # 加粗黄色
|
||||
"ERROR_LEVEL": "\033[1;31m", # 加粗红色
|
||||
"CRITICAL_LEVEL": "\033[1;35m", # 加粗紫色
|
||||
"TRACE_TEXT": "\033[90m", # 深灰色
|
||||
"DEBUG_TEXT": "\033[37m", # 灰色
|
||||
"INFO_TEXT": "\033[97m", # 白色
|
||||
"WARNING_TEXT": "\033[33m", # 黄色
|
||||
"ERROR_TEXT": "\033[31m", # 红色
|
||||
"CRITICAL_TEXT": "\033[35m", # 紫色
|
||||
"DATE": "\033[37m", # 日期始终使用灰色
|
||||
}
|
||||
|
||||
def __init__(self, use_colors=True):
|
||||
super().__init__()
|
||||
# 强制启用颜色
|
||||
self.use_colors = use_colors
|
||||
|
||||
def format(self, record):
|
||||
# 检查是否有自定义堆栈信息
|
||||
if hasattr(record, "custom_stack_info") and record.custom_stack_info: # type: ignore
|
||||
r = cast(CustomRecord, record)
|
||||
frame_info = r.custom_stack_info
|
||||
record.filename = frame_info[0]
|
||||
record.lineno = frame_info[1]
|
||||
record.funcName = frame_info[2]
|
||||
if len(frame_info) > 3:
|
||||
record.name = frame_info[3]
|
||||
if not self.use_colors:
|
||||
return self._format_basic(record)
|
||||
|
||||
level_color = self.COLORS.get(f"{record.levelname}_LEVEL", self.COLORS["WHITE"])
|
||||
text_color = self.COLORS.get(f"{record.levelname}_TEXT", self.COLORS["WHITE"])
|
||||
date_color = self.COLORS["DATE"]
|
||||
reset = self.COLORS["RESET"]
|
||||
|
||||
# 日期格式
|
||||
datetime_str = datetime.fromtimestamp(record.created).strftime("%y-%m-%d [%H:%M:%S,%f")[:-3] + "]"
|
||||
|
||||
# 模块和函数信息
|
||||
filename = record.filename.replace(".py", "").split("\\")[-1] # 提取文件名(不含路径和扩展名)
|
||||
if "/" in filename:
|
||||
filename = filename.split("/")[-1]
|
||||
module_path = f"{record.name}.{filename}"
|
||||
func_line = f"{record.funcName}:{record.lineno}"
|
||||
right_info = f" [{func_line}] [{module_path}]"
|
||||
|
||||
# 主要消息
|
||||
main_msg = record.getMessage()
|
||||
|
||||
# 构建基本消息格式
|
||||
formatted_message = (
|
||||
f"{date_color}{datetime_str}{reset} "
|
||||
f"{level_color}[{record.levelname}]{reset} "
|
||||
f"{text_color}{main_msg}"
|
||||
f"{date_color}{right_info}{reset}"
|
||||
)
|
||||
|
||||
# 处理异常信息
|
||||
if record.exc_info:
|
||||
exc_text = self.formatException(record.exc_info)
|
||||
if formatted_message[-1:] != "\n":
|
||||
formatted_message = formatted_message + "\n"
|
||||
formatted_message = formatted_message + text_color + exc_text + reset
|
||||
elif record.stack_info:
|
||||
if formatted_message[-1:] != "\n":
|
||||
formatted_message = formatted_message + "\n"
|
||||
formatted_message = formatted_message + text_color + self.formatStack(record.stack_info) + reset
|
||||
|
||||
return formatted_message
|
||||
|
||||
def _format_basic(self, record):
|
||||
"""基本格式化,不包含颜色"""
|
||||
datetime_str = datetime.fromtimestamp(record.created).strftime("%y-%m-%d [%H:%M:%S,%f")[:-3] + "]"
|
||||
filename = record.filename.replace(".py", "").split("\\")[-1] # 提取文件名(不含路径和扩展名)
|
||||
if "/" in filename:
|
||||
filename = filename.split("/")[-1]
|
||||
module_path = f"{record.name}.{filename}"
|
||||
func_line = f"{record.funcName}:{record.lineno}"
|
||||
right_info = f" [{func_line}] [{module_path}]"
|
||||
|
||||
formatted_message = f"{datetime_str} [{record.levelname}] {record.getMessage()}{right_info}"
|
||||
|
||||
if record.exc_info:
|
||||
exc_text = self.formatException(record.exc_info)
|
||||
if formatted_message[-1:] != "\n":
|
||||
formatted_message = formatted_message + "\n"
|
||||
formatted_message = formatted_message + exc_text
|
||||
elif record.stack_info:
|
||||
if formatted_message[-1:] != "\n":
|
||||
formatted_message = formatted_message + "\n"
|
||||
formatted_message = formatted_message + self.formatStack(record.stack_info)
|
||||
|
||||
return formatted_message
|
||||
|
||||
def formatException(self, exc_info):
|
||||
"""重写异常格式化,确保异常信息保持正确的格式和颜色"""
|
||||
# 获取标准的异常格式化文本
|
||||
formatted_exc = super().formatException(exc_info)
|
||||
return formatted_exc
|
||||
|
||||
|
||||
# 配置日志处理器
|
||||
def configure_logger(loglevel=None, working_dir=None):
|
||||
"""配置日志记录器
|
||||
|
||||
Args:
|
||||
loglevel: 日志级别,可以是字符串('TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
|
||||
或logging模块的常量(如logging.DEBUG)或TRACE_LEVEL
|
||||
"""
|
||||
# 获取根日志记录器
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(TRACE_LEVEL)
|
||||
# 设置日志级别
|
||||
numeric_level = logging.DEBUG
|
||||
if loglevel is not None:
|
||||
if isinstance(loglevel, str):
|
||||
# 将字符串转换为logging级别
|
||||
if loglevel.upper() == "TRACE":
|
||||
numeric_level = TRACE_LEVEL
|
||||
else:
|
||||
numeric_level = getattr(logging, loglevel.upper(), None)
|
||||
if not isinstance(numeric_level, int):
|
||||
print(f"警告: 无效的日志级别 '{loglevel}',使用默认级别 DEBUG")
|
||||
else:
|
||||
numeric_level = loglevel
|
||||
|
||||
# 移除已存在的处理器
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
|
||||
# 创建控制台处理器
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(numeric_level) # 使用与根记录器相同的级别
|
||||
|
||||
# 使用自定义的颜色格式化器
|
||||
color_formatter = ColoredFormatter()
|
||||
console_handler.setFormatter(color_formatter)
|
||||
|
||||
# 添加处理器到根日志记录器
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
# 如果指定了工作目录,添加文件处理器
|
||||
if working_dir is not None:
|
||||
logs_dir = os.path.join(working_dir, "logs")
|
||||
os.makedirs(logs_dir, exist_ok=True)
|
||||
|
||||
# 生成日志文件名:日期 时间.log
|
||||
log_filename = datetime.now().strftime("%Y-%m-%d %H-%M-%S") + ".log"
|
||||
log_filepath = os.path.join(logs_dir, log_filename)
|
||||
|
||||
# 创建文件处理器
|
||||
file_handler = logging.FileHandler(log_filepath, encoding="utf-8")
|
||||
file_handler.setLevel(TRACE_LEVEL)
|
||||
|
||||
# 使用不带颜色的格式化器
|
||||
file_formatter = ColoredFormatter(use_colors=False)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
|
||||
root_logger.addHandler(file_handler)
|
||||
|
||||
logging.getLogger("asyncio").setLevel(logging.INFO)
|
||||
logging.getLogger("urllib3").setLevel(logging.INFO)
|
||||
|
||||
|
||||
|
||||
# 配置日志系统
|
||||
configure_logger()
|
||||
|
||||
# 获取日志记录器
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 获取调用栈信息的工具函数
|
||||
def _get_caller_info(stack_level=0) -> Tuple[str, int, str, str]:
|
||||
"""
|
||||
获取调用者的信息
|
||||
|
||||
Args:
|
||||
stack_level: 堆栈回溯的级别,0表示当前函数,1表示调用者,依此类推
|
||||
|
||||
Returns:
|
||||
(filename, line_number, function_name, module_name) 元组
|
||||
"""
|
||||
# 堆栈级别需要加3:
|
||||
# +1 因为这个函数本身占一层
|
||||
# +1 因为日志函数(debug, info等)占一层
|
||||
# +1 因为下面调用 inspect.stack() 也占一层
|
||||
frame = inspect.currentframe()
|
||||
try:
|
||||
# 跳过适当的堆栈帧
|
||||
for _ in range(stack_level + 3):
|
||||
if frame and frame.f_back:
|
||||
frame = frame.f_back
|
||||
else:
|
||||
break
|
||||
|
||||
if frame:
|
||||
filename = frame.f_code.co_filename if frame.f_code else "unknown"
|
||||
line_number = frame.f_lineno if hasattr(frame, "f_lineno") else 0
|
||||
function_name = frame.f_code.co_name if frame.f_code else "unknown"
|
||||
|
||||
# 获取模块名称
|
||||
module_name = "unknown"
|
||||
if frame.f_globals and "__name__" in frame.f_globals:
|
||||
module_name = frame.f_globals["__name__"].rsplit(".", 1)[0]
|
||||
|
||||
return (filename, line_number, function_name, module_name)
|
||||
return ("unknown", 0, "unknown", "unknown")
|
||||
finally:
|
||||
del frame # 避免循环引用
|
||||
|
||||
|
||||
# 便捷日志记录函数
|
||||
def debug(msg, *args, stack_level=0, **kwargs):
|
||||
"""
|
||||
记录DEBUG级别日志
|
||||
|
||||
Args:
|
||||
msg: 日志消息
|
||||
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
|
||||
*args, **kwargs: 传递给logger.debug的其他参数
|
||||
"""
|
||||
# 获取调用者信息
|
||||
if stack_level > 0:
|
||||
caller_info = _get_caller_info(stack_level)
|
||||
extra = kwargs.get("extra", {})
|
||||
extra["custom_stack_info"] = caller_info
|
||||
kwargs["extra"] = extra
|
||||
logger.debug(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def info(msg, *args, stack_level=0, **kwargs):
|
||||
"""
|
||||
记录INFO级别日志
|
||||
|
||||
Args:
|
||||
msg: 日志消息
|
||||
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
|
||||
*args, **kwargs: 传递给logger.info的其他参数
|
||||
"""
|
||||
if stack_level > 0:
|
||||
caller_info = _get_caller_info(stack_level)
|
||||
extra = kwargs.get("extra", {})
|
||||
extra["custom_stack_info"] = caller_info
|
||||
kwargs["extra"] = extra
|
||||
logger.info(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def warning(msg, *args, stack_level=0, **kwargs):
|
||||
"""
|
||||
记录WARNING级别日志
|
||||
|
||||
Args:
|
||||
msg: 日志消息
|
||||
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
|
||||
*args, **kwargs: 传递给logger.warning的其他参数
|
||||
"""
|
||||
if stack_level > 0:
|
||||
caller_info = _get_caller_info(stack_level)
|
||||
extra = kwargs.get("extra", {})
|
||||
extra["custom_stack_info"] = caller_info
|
||||
kwargs["extra"] = extra
|
||||
logger.warning(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def error(msg, *args, stack_level=0, **kwargs):
|
||||
"""
|
||||
记录ERROR级别日志
|
||||
|
||||
Args:
|
||||
msg: 日志消息
|
||||
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
|
||||
*args, **kwargs: 传递给logger.error的其他参数
|
||||
"""
|
||||
if stack_level > 0:
|
||||
caller_info = _get_caller_info(stack_level)
|
||||
extra = kwargs.get("extra", {})
|
||||
extra["custom_stack_info"] = caller_info
|
||||
kwargs["extra"] = extra
|
||||
logger.error(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def critical(msg, *args, stack_level=0, **kwargs):
|
||||
"""
|
||||
记录CRITICAL级别日志
|
||||
|
||||
Args:
|
||||
msg: 日志消息
|
||||
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
|
||||
*args, **kwargs: 传递给logger.critical的其他参数
|
||||
"""
|
||||
if stack_level > 0:
|
||||
caller_info = _get_caller_info(stack_level)
|
||||
extra = kwargs.get("extra", {})
|
||||
extra["custom_stack_info"] = caller_info
|
||||
kwargs["extra"] = extra
|
||||
logger.critical(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def trace(msg, *args, stack_level=0, **kwargs):
|
||||
"""
|
||||
记录TRACE级别日志(比DEBUG级别更低)
|
||||
|
||||
Args:
|
||||
msg: 日志消息
|
||||
stack_level: 堆栈回溯级别,用于定位日志的实际调用位置
|
||||
*args, **kwargs: 传递给logger.log的其他参数
|
||||
"""
|
||||
if stack_level > 0:
|
||||
caller_info = _get_caller_info(stack_level)
|
||||
extra = kwargs.get("extra", {})
|
||||
extra["custom_stack_info"] = caller_info
|
||||
kwargs["extra"] = extra
|
||||
logger.log(TRACE_LEVEL, msg, *args, **kwargs)
|
||||
|
||||
|
||||
logger.trace = trace
|
||||
|
||||
# 测试日志输出(如果直接运行此文件)
|
||||
if __name__ == "__main__":
|
||||
print("测试不同日志级别的颜色输出:")
|
||||
trace("这是一条跟踪日志 (TRACE级别显示为深灰色,其他文本也为深灰色)")
|
||||
debug("这是一条调试日志 (DEBUG级别显示为蓝色,其他文本为灰色)")
|
||||
info("这是一条信息日志 (INFO级别显示为绿色,其他文本为白色)")
|
||||
warning("这是一条警告日志 (WARNING级别显示为黄色,其他文本也为黄色)")
|
||||
error("这是一条错误日志 (ERROR级别显示为红色,其他文本也为红色)")
|
||||
critical("这是一条严重错误日志 (CRITICAL级别显示为紫色,其他文本也为紫色)")
|
||||
# 测试异常输出
|
||||
try:
|
||||
1 / 0
|
||||
except Exception as e:
|
||||
error(f"发生错误: {e}", exc_info=True)
|
||||
@@ -191,9 +191,23 @@ def configure_logger(loglevel=None, working_dir=None):
|
||||
|
||||
# 添加处理器到根日志记录器
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
# 降低第三方库的日志级别,避免过多输出
|
||||
# pymodbus 库的日志太详细,设置为 WARNING
|
||||
logging.getLogger('pymodbus').setLevel(logging.WARNING)
|
||||
logging.getLogger('pymodbus.logging').setLevel(logging.WARNING)
|
||||
logging.getLogger('pymodbus.logging.base').setLevel(logging.WARNING)
|
||||
logging.getLogger('pymodbus.logging.decoders').setLevel(logging.WARNING)
|
||||
|
||||
# websockets 库的日志输出较多,设置为 WARNING
|
||||
logging.getLogger('websockets').setLevel(logging.WARNING)
|
||||
logging.getLogger('websockets.client').setLevel(logging.WARNING)
|
||||
logging.getLogger('websockets.server').setLevel(logging.WARNING)
|
||||
|
||||
# ROS 节点的状态更新日志过于频繁,设置为 INFO
|
||||
logging.getLogger('unilabos.ros.nodes.presets.host_node').setLevel(logging.INFO)
|
||||
|
||||
# 如果指定了工作目录,添加文件处理器
|
||||
log_filepath = None
|
||||
if working_dir is not None:
|
||||
logs_dir = os.path.join(working_dir, "logs")
|
||||
os.makedirs(logs_dir, exist_ok=True)
|
||||
@@ -214,7 +228,6 @@ def configure_logger(loglevel=None, working_dir=None):
|
||||
|
||||
logging.getLogger("asyncio").setLevel(logging.INFO)
|
||||
logging.getLogger("urllib3").setLevel(logging.INFO)
|
||||
return log_filepath
|
||||
|
||||
|
||||
# 配置日志系统
|
||||
|
||||
@@ -17,6 +17,14 @@ try:
|
||||
default=json_default,
|
||||
)
|
||||
|
||||
def fast_loads(data) -> dict:
|
||||
"""JSON 反序列化,优先使用 orjson。接受 str / bytes。"""
|
||||
return orjson.loads(data)
|
||||
|
||||
def fast_dumps_str(obj, **kwargs) -> str:
|
||||
"""JSON 序列化为 str,优先使用 orjson。用于需要 str 而非 bytes 的场景(如 ROS msg)。"""
|
||||
return orjson.dumps(obj, option=orjson.OPT_NON_STR_KEYS, default=json_default).decode("utf-8")
|
||||
|
||||
def normalize_json(info: dict) -> dict:
|
||||
"""经 JSON 序列化/反序列化一轮来清理非标准类型。"""
|
||||
return orjson.loads(orjson.dumps(info, default=json_default))
|
||||
@@ -29,6 +37,14 @@ except ImportError:
|
||||
def fast_dumps_pretty(obj, **kwargs) -> bytes: # type: ignore[misc]
|
||||
return json.dumps(obj, indent=2, ensure_ascii=False, cls=TypeEncoder).encode("utf-8")
|
||||
|
||||
def fast_loads(data) -> dict: # type: ignore[misc]
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode("utf-8")
|
||||
return json.loads(data)
|
||||
|
||||
def fast_dumps_str(obj, **kwargs) -> str: # type: ignore[misc]
|
||||
return json.dumps(obj, ensure_ascii=False, cls=TypeEncoder)
|
||||
|
||||
def normalize_json(info: dict) -> dict: # type: ignore[misc]
|
||||
return json.loads(json.dumps(info, ensure_ascii=False, cls=TypeEncoder))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user