From 865dd87556ec71176c86e09e99d5b85debb204b3 Mon Sep 17 00:00:00 2001 From: Andy6M Date: Fri, 22 May 2026 14:52:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=89=A3=E7=94=B5=E7=BB=84?= =?UTF-8?q?=E8=A3=85=E9=A9=B1=E5=8A=A8=20coin=5Fcell=5Fassembly.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../skills/yibin-electrolyte-submit/SKILL.md | 483 ++++++++++++++++++ .../yibin-electrolyte-submit/action-index.md | 295 +++++++++++ .../coin_cell_assembly/coin_cell_assembly.py | 135 +++-- 3 files changed, 876 insertions(+), 37 deletions(-) create mode 100644 .cursor/skills/yibin-electrolyte-submit/SKILL.md create mode 100644 .cursor/skills/yibin-electrolyte-submit/action-index.md diff --git a/.cursor/skills/yibin-electrolyte-submit/SKILL.md b/.cursor/skills/yibin-electrolyte-submit/SKILL.md new file mode 100644 index 00000000..b39eaac0 --- /dev/null +++ b/.cursor/skills/yibin-electrolyte-submit/SKILL.md @@ -0,0 +1,483 @@ +--- +name: yibin-electrolyte-submit +description: >- + 通过 Uni-Lab Notebook API 向宜宾电解液工站提交实验,覆盖配液分液(Bioyond LIMS)、 + 扣电组装(CoinCellAssembly)、扣电测试全流程。 + 包含 Excel 解析、formulation 构建、工作流节点参数填写、notebook 提交与状态轮询。 + Use when the user wants to submit electrolyte experiments, assemble or test coin cells, + parse experiment Excel files, build notebook payloads, or mentions + 宜宾/配液/分液/扣电/电解液实验/notebook提交/CoinCell/BioyondLIMS. +--- + +# 宜宾电解液产线 API 操作指南 + +本 skill 覆盖两个设备的完整操作流程: +1. **配液分液工站** (`bioyond_cell_workstation`) — Bioyond LIMS 配液/分液/转运 +2. **扣电组装站** (`BatteryStation`) — Modbus PLC 扣电组装/数据采集 + +## 设备信息 + +| 属性 | 配液分液工站 | 扣电组装站 | +|------|------------|-----------| +| device_id | `bioyond_cell_workstation` | `BatteryStation` | +| 显示名 | 配液分液工站 | 扣电工作站 | +| 源码 | `unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py` | `unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py` | +| 类名 | `BioyondCellWorkstation` | `CoinCellAssemblyWorkstation` | +| 通讯 | HTTP REST (Bioyond LIMS API) | Modbus TCP (PLC 寄存器) | + +## 前置条件 + +### 认证信息 + +``` +AUTH="Authorization: Lab OTdlY2FkNmUtZmZmMi00YjhiLThhOWEtNWM5ODAyOTJmOTUxOmU0OGM2YWJkLTA4ZmEtNDFjMy04NzhhLTc4M2FiODlhZjYxMw==" +BASE="https://uni-lab.test.bohrium.com" +``` + +来源:`--ak 97ecad6e-fff2-4b8b-8a9a-5c980292f951 --sk e48c6abd-08fa-41c3-878a-783ab89af613 --addr test` + +### 启动 unilab(云端模式) + +> **重要**:提交实验前必须确保 unilab 正在运行且已连接云端 WebSocket。 + +```powershell +$env:PYTHONIOENCODING="utf-8" +conda activate newunilab2603 +cd D:\UniLabdev\Uni-Lab-OS\unilabos\devices\workstation +unilab -g D:\UniLabdev\Uni-Lab-OS\yibin_electrolyte_config.json --ak 97ecad6e-fff2-4b8b-8a9a-5c980292f951 --sk e48c6abd-08fa-41c3-878a-783ab89af613 --upload_registry --addr test --disable_browser --skip_env_check +``` + +**启动要点**: +1. 必须先激活虚拟环境 `newunilab2603` +2. 工作目录切到 `unilabos/devices/workstation`(设备驱动所在目录) +3. `--upload_registry` 将 64 个设备 + 142 个资源注册到云端 +4. `--skip_env_check` + `PYTHONIOENCODING=utf-8` 避免 Windows GBK 编码崩溃 +5. 启动后后台运行,等待日志出现 `Application startup complete` 和 `Host node ready signal published with 3 devices` + +**验证连接成功的标志**: +- 日志出现 `[MessageProcessor] ... wss://uni-lab.test.bohrium.com/api/v1/ws/schedule` +- 日志出现 `[WebSocketClient] Host node ready signal published with 3 devices` +- 日志出现 `Resource tree add completed`(资源树同步完成) + +### 云端物料上架与入库(启动后必做) + +> **在提交实验之前,必须提醒用户完成以下云端操作,否则实验会因物料缺失而失败。** + +1. **拖拽上料**:在云端 UI(`$BASE/laboratory/`)的资源树视图中,将物料拖拽到对应的仓库/库位上。unilab 启动后资源树会自动同步到云端,但物料的**上架位置**需要用户在 UI 上手动确认或调整。 + +2. **确认配液物料入库**:确保所有配液实验需要的试剂(如 LiPF6、EC、DMC、EMC 等)已在 LIMS 系统中完成入库。可通过以下方式验证: + - 云端 UI 资源树中对应仓库(如"粉末加样头堆栈"、"配液站内试剂仓库")下有物料节点 + - 或通过 API #8 获取资源树后检查物料节点是否存在 + +3. **告知 AI 可以提交**:用户完成上述操作后,告知 AI "物料已上架,可以提交实验",AI 再执行 notebook 提交流程。 + +**提醒话术模板**(AI 应在启动成功后发送给用户): +``` +unilab 已成功启动并连接云端。提交实验前请完成以下操作: +1. 在云端 UI 上确认资源树中的物料位置,必要时拖拽调整上料位 +2. 确保配液所需的试剂(粉末、液体)已在 LIMS 中完成入库 +3. 完成后告诉我,我将为您提交实验 +``` + +### 生成 Action Schema(首次使用) + +启动 unilab 后,在 `unilabos_data/` 目录下会生成 `req_device_registry_upload.json`。运行以下命令提取两个设备的 action JSON: + +```bash +python .cursor/skills/create-device-skill/scripts/extract_device_actions.py --registry unilabos_data/req_device_registry_upload.json bioyond_cell_workstation .cursor/skills/yibin-electrolyte-submit/actions/ +python .cursor/skills/create-device-skill/scripts/extract_device_actions.py --registry unilabos_data/req_device_registry_upload.json BatteryStation .cursor/skills/yibin-electrolyte-submit/actions/ +``` + +## 请求约定 + +- Windows 平台**必须用 `curl.exe`**(非 PowerShell 的 curl 别名) +- 所有请求带 `$AUTH` 头 +- URL 格式:`$BASE/api/v1/` +- POST/PATCH 请求体写入临时 JSON 文件后用 `-d '@tmp.json'` 传参(避免 PowerShell 转义问题) +- 本地 API 基址:`http://127.0.0.1:8002/api/v1/` + +## Session State + +每次会话开始时,依次获取以下信息: + +```bash +# 1. lab_uuid +curl.exe -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH" +# → data.uuid → $lab_uuid + +# 2. project_uuid +curl.exe -s -X GET "$BASE/api/v1/lab/project/list?lab_uuid=$lab_uuid" -H "$AUTH" +# → data.items[].uuid/name → 让用户选择或取唯一项 → $project_uuid +``` + +## 工作流模板(重要) + +> **必须向用户索要已有的工作流模板 UUID 或 URL,不要自行创建。** +> +> 原因:通过 `edge/workflow/node` API 创建节点会报 `resource_node_template not found`—— +> 云端的工作流节点模板系统和设备注册表是独立的,需要用户在云端 UI 上预先配置好工作流模板。 + +**获取方式**: +- 用户提供工作流页面 URL,如 `$BASE/laboratory//workflow/` +- 从 URL 中提取 `workflow_uuid` +- 用 API 获取模板详情: + +``` +GET /api/v1/lab/workflow/template/detail/ +``` + +返回 `data.nodes[]`:每个节点的 uuid、name、param、device_name、handles、disabled。 + +**示例**: +``` +工作流 URL: https://uni-lab.test.bohrium.com/laboratory/e9ed9102-d709-4741-b7a0-d1e8578e2065/workflow/b49f80d9-58d6-4456-a521-56f4dd39cda0 +→ workflow_uuid = b49f80d9-58d6-4456-a521-56f4dd39cda0 +``` + +从模板详情中提取**未 disabled** 的节点的 `uuid` 和 `name`,后续提交 notebook 时使用。 + +## API Endpoints + +### #1 获取 lab_uuid + +``` +GET /api/v1/edge/lab/info +``` + +### #2 列出项目 + +``` +GET /api/v1/lab/project/list?lab_uuid=$lab_uuid +``` + +返回 `data.items[]`,取 `uuid` 和 `name`。 + +### #3 获取工作流模板详情 + +``` +GET /api/v1/lab/workflow/template/detail/ +``` + +返回 `data.nodes[]`:每个节点的 uuid、name、param、device_name、handles。 +提取活跃节点(`disabled != true`)的 `uuid` 用于构建 notebook 请求。 + +### #4 提交实验(创建 notebook)— 核心 API + +``` +POST /api/v1/lab/notebook +Body: { + "lab_uuid": "", + "project_uuid": "", + "workflow_uuid": "", + "name": "<实验名称>", + "node_params": [ + { + "sample_uuids": [], + "datas": [ + { + "node_uuid": "<模板中的节点UUID>", + "param": { <参数键值对> }, + "sample_params": [] + } + ] + } + ] +} +``` + +**关键注意事项**: +- `node_params` 是数组,每个元素代表一轮实验 +- `datas` 中每个节点对应模板中的一个活跃节点 +- `param` 中的字段名**必须使用 Python 函数参数名**,不能用模板中存储的 LIMS 字段名(见下方映射表) + +### #5 查询 notebook 状态 + +``` +GET /api/v1/lab/notebook/status?uuid= +``` + +| status | 含义 | +|--------|------| +| `running` | 执行中 | +| `success` | 成功 | +| `fail` | 失败 | + +### #6 运行设备单动作(本地 API) + +``` +POST http://127.0.0.1:8002/api/v1/job/add +Body: { + "device_id": "", + "action": "", + "action_args": { <参数键值对> }, + "sample_material": {} +} +``` + +本地 API 可自动解析 `action_type`,无需手动指定。适用于快速调试或云端未连接时。 + +### #7 查询本地任务状态 + +``` +GET http://127.0.0.1:8002/api/v1/job//status +``` + +| status | 含义 | +|--------|------| +| 0 | UNKNOWN | +| 1 | ACCEPTED | +| 2 | EXECUTING | +| 4 | SUCCEEDED | +| 5 | CANCELED | +| 6 | ABORTED | + +### #8 获取资源树 + +``` +GET /api/v1/lab/material/download/ +``` + +返回所有节点(`id`, `name`, `uuid`, `type`, `parent`)。填写 Slot 字段时用此接口筛选节点。 + +## Placeholder Slot 填写规则 + +action JSON 中 `placeholder_keys` 标记了哪些字段需要填 Slot: + +| placeholder 值 | Slot 类型 | 填写格式 | +|---------------|-----------|---------| +| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` | +| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` 路径字符串 | +| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` 路径字符串 | +| `unilabos_class` | ClassSlot | `"class_name"` 字符串 | +| `unilabos_formulation` | FormulationSlot | `[{well_name, liquids: [{name, volume}]}]` | + +### ResourceSlot 填写 + +从 API #8 资源树中筛选**物料**节点: + +```json +{"id": "/bioyond_cell_workstation/YB_Bioyond_Deck/自动堆栈-左", "name": "自动堆栈-左", "uuid": "3a19debc-..."} +``` + +数组字段:`[{id, name, uuid}, ...]` +特例:`create_resource` 的 `res_id` 允许填不存在的路径。 + +### DeviceSlot 填写 + +从资源树筛选 `type=device` 的节点,填路径字符串: + +``` +"/BatteryStation" +"/bioyond_cell_workstation" +``` + +### FormulationSlot 填写 + +```json +[ + { + "sample_uuid": "", + "well_name": "YB_PrepBottle_15mL_Carrier_bottle_A1", + "liquids": [ + { "name": "LiPF6", "mass": 12.5 }, + { "name": "EC", "mass": 50.0 } + ] + } +] +``` + +`well_name` 从资源树中取物料节点的 `name`。 + +## 参数名映射(重要的坑) + +> 工作流模板中存储的参数名和 Python 函数实际接受的参数名**不一定相同**。 +> 提交 notebook 时必须使用 **Python 函数参数名**。 + +### `create_orders_formulation` 参数映射 + +| 模板中的 param 键 | 实际 Python 参数名 | 说明 | +|-------------------|-------------------|------| +| `pouch_cell_info` | `pouch_cell_volume` | 软包组装分液体积 (mL) | +| `conductivity_info` | `conductivity_volume` | 电导测试分液体积 (mL) | +| `load_shedding_info` | `coin_cell_volume` | 扣电组装分液体积 (mL) | +| `formulation` | `formulation` | 配方数组(名称一致) | +| `batch_id` | `batch_id` | 批次号(名称一致) | +| `bottle_type` | `bottle_type` | 配液瓶类型(名称一致) | +| `mix_time` | `mix_time` | 混匀时间(秒)(名称一致) | +| `conductivity_bottle_count` | `conductivity_bottle_count` | 电导瓶数(名称一致) | + +当从模板中读到 `param` 包含 `pouch_cell_info` 等 LIMS 字段名时,提交 notebook 时要用右列的 Python 函数参数名。否则会报 `TypeError: got an unexpected keyword argument`。 + +## 典型工作流 + +### 方式一:通过 Notebook API 批量提交(推荐) + +**适用场景**:多组配方的批量实验,云端管理实验记录 + +``` +1. 向用户索要工作流模板 URL(不要自行创建) +2. 获取 lab_uuid(API #1)和 project_uuid(API #2) +3. 获取工作流模板详情(API #3),提取活跃节点 UUID +4. 解析用户提供的 Excel 文件,构建 formulation 数组 +5. 提交 notebook(API #4) +6. 轮询 notebook 状态(API #5)直到完成 +``` + +**Excel 解析规则**: +- 全局参数在第一个数据行:`batch_id`、`bottle_type`、`mix_time`、`coin_cell_volume`、`pouch_cell_volume`、`conductivity_volume`、`conductivity_bottle_count` +- 配方列从"试剂名1"开始,交替排列:试剂名列 + 质量列(以 `(g)` 结尾) +- 每行一个配方,`order_name` = 配方ID列 +- formulation 中每个配方的 materials 数组只包含 `mass > 0` 的试剂 + +**node_params 构建**:所有配方放入同一个 round 的同一个 datas 条目中,因为只有一个节点(`create_orders_formulation`)。 + +### 方式二:设备单步操作(本地 API) + +**适用场景**:调试、快速测试 + +``` +1. 确保 unilab 已在本地启动 +2. 通过 POST http://127.0.0.1:8002/api/v1/job/add 提交任务 +3. 通过 GET /api/v1/job//status 查询状态 +``` + +### 设备操作流程:配液 → 转运 → 扣电 + +``` +1. [配液站] scheduler_start_and_auto_feeding → 启动调度 + 上料 +2. [配液站] create_orders_formulation → 创建配液实验(配方输入) +3. [配液站] transfer_3_to_2_to_1_auto → 分液瓶板转运到扣电站 +4. [扣电站] func_pack_device_init_auto_start_combined → 初始化+自动+启动 +5. [扣电站] func_sendbottle_allpack_multi → 发送瓶数+批量组装 +``` + +## 云端使用心得 + +### 环境准备 +- Windows 必须设置 `$env:PYTHONIOENCODING="utf-8"` 防止编码崩溃 +- 使用 `--skip_env_check` 跳过依赖检查,加快启动 +- 工作目录建议在 `unilabos/devices/workstation` 下启动 + +### 连接与注册 +- `--upload_registry` 会自动将设备和资源注册到云端 +- WebSocket 连接建立后,本地和云端的资源树会自动同步 +- 注册成功后用户需在云端 UI 完成**物料拖放上架**操作 +- 如果 unilab 断开重连,资源树会重新同步 + +### 工作流模板 +- **不要自行调用 API 创建工作流或节点**——云端工作流节点模板需要预配置 +- 始终向用户索要已有的工作流模板 URL +- 从 URL 中提取 `workflow_uuid`,通过 API #3 获取详情 +- 模板中 `disabled: true` 的节点跳过,只处理活跃节点 + +### Notebook 实验提交 +- Notebook 是云端管理实验的标准方式 +- 一个 notebook 可包含多轮(`node_params` 数组),每轮可包含多组参数 +- 提交后通过 API #5 轮询状态,LIMS 配液流程通常需要较长时间(8 个配方约 30-60 分钟) +- 实验进度可在云端 UI 和本地 unilab 日志中同步查看 + +### 常见错误 +| 错误 | 原因 | 解决 | +|------|------|------| +| `edge not started error` | unilab 未连接云端 WebSocket | 检查 unilab 是否在运行、重启 | +| `resource_node_template not found` | 云端没有该设备的工作流模板 | 向用户索要已有模板,不要自行创建 | +| `got an unexpected keyword argument` | 参数名用了模板字段名而非 Python 函数参数名 | 参照上方映射表转换 | +| `UnicodeEncodeError: 'gbk'` | Windows 默认编码不支持特殊字符 | 设置 `PYTHONIOENCODING=utf-8` | +| `parse parameter error` | 云端 API 字段名错误 | `device_id` (非 `device_name`)、`action` (非 `action_name`)、必须带 `action_type` | + +## 渐进加载策略 + +1. 先读本文件了解 API 端点、参数映射和云端注意事项 +2. 需要具体 action 参数时,读 [action-index.md](action-index.md) 查找 action 名称和核心参数 +3. 需要完整 schema 时,读 `actions/.json`(需先运行提取命令生成) +4. 需要理解参数含义时,读设备源码 + +## 完整 Notebook 提交 Checklist + +``` +- [ ] 确认 unilab 已在本地启动并连接云端 WebSocket +- [ ] 提醒用户在云端 UI 拖拽上料、确认物料位置 +- [ ] 提醒用户确认配液所需试剂已在 LIMS 完成入库 +- [ ] 等待用户确认物料就绪后再继续 +- [ ] 向用户索要工作流模板 URL → 提取 workflow_uuid +- [ ] 获取 lab_uuid(API #1) +- [ ] 获取 project_uuid(API #2) +- [ ] 获取工作流模板详情(API #3),提取活跃节点 UUID +- [ ] 解析用户 Excel 文件 → 构建 formulation + 全局参数 +- [ ] 注意参数名映射(模板字段名 → Python 函数参数名) +- [ ] 提交 notebook(API #4) +- [ ] 轮询 notebook 状态(API #5)直到完成 +``` + +--- + +## 真实场景:宜宾产线 Excel 提交提示词模板 + +> 以下为已验证可用的标准提示词,适用于配液-分液-扣电全流程。 + +### 场景说明 + +- unilab 运行在本地 Windows 机器(miniforge 环境),连接云端 WebSocket +- AI(Cursor / OpenClaw)在任意设备上,通过云端 API 操作,**不需要本地 127.0.0.1** +- 工作流为 5 节点串联:`create_orders_formulation` → `transfer_3_to_2_to_1_auto` → `func_pack_device_init_auto_start_combined` → `func_sendbottle_allpack_multi` → `transfer_1_to_2` + +### 已知固定参数(宜宾产线) + +``` +BASE = https://uni-lab.test.bohrium.com +lab_uuid = e9ed9102-d709-4741-b7a0-d1e8578e2065 +project = YiBinElectrolyte (bc5224b4-8120-4765-9961-9dfc1802a1f6) +workflow = 配液分液formulation全流程 (2bc59938-db79-4415-ac2d-9897ef125f2f) +``` + +#### 工作流节点 UUID(固定,无需重新查询) + +| 顺序 | action | node_uuid | +|------|--------|-----------| +| Step1 | auto-create_orders_formulation | `ece6744a-81ac-4ae4-8cd1-1c8eeda1dab6` | +| Step2 | auto-transfer_3_to_2_to_1_auto | `1c37a8dd-5ba0-413d-81db-94b9c936a171` | +| Step3 | auto-func_pack_device_init_auto_start_combined | `97a676a2-d257-4479-9096-073b40300970` | +| Step4 | auto-func_sendbottle_allpack_multi | `cf69017a-d29c-4aad-a63b-309d63dac2e9` | +| Step5 | auto-transfer_1_to_2 | `80d1c1aa-dbc3-4601-86b7-5c22a992dd9e` | + +### 标准提示词 + +``` +请使用 yibin-electrolyte-submit skill,提交以下实验: + +工作流模板 URL:https://uni-lab.test.bohrium.com/laboratory/e9ed9102-d709-4741-b7a0-d1e8578e2065/workflow/2bc59938-db79-4415-ac2d-9897ef125f2f +Excel 文件路径:<粘贴或上传 xlsx 路径> + +注意事项: +- lab_uuid、project_uuid、workflow节点UUID均已固定,无需重新查询 +- 直接解析 Excel → 构建 payload → 提交 +- mix_time 传标量整数即可(已兼容) +- 试剂名以 Excel 为准,注意区分 LiDFOB / LiDOFB 等拼写 +- csv_export_path 取 Excel 中 csv_export_path 列的值 +- 提交后告知 notebook UUID,无需自动轮询(实验耗时较长) +``` + +### Excel 列结构说明(experment_template_0415sim-*.xlsx) + +| 列范围 | 内容 | +|--------|------| +| C | batch_id | +| D | bottle_type | +| E-H | coin_cell_volume / conductivity_bottle_count / conductivity_volume / csv_export_path | +| I-T | 试剂名+质量 交替排列(最多6对)| +| U | mix_time | +| V | order_name(每行配方的订单号)| +| W | pouch_cell_volume | +| X-Y | target_device / target_location(Step2参数)| +| AA | material_search_enable(Step3参数)| +| AB-AS | 扣电站参数(Step4)| + +### CSV 导出说明 + +每次 `create_orders_formulation` 完成后,在 `csv_export_path` 目录下生成: +``` +electrolyte_orders_.csv +``` +列:`orderCode, orderName, 配液瓶类型, 配液瓶二维码, 分液瓶类型, 分液瓶二维码, 目标配液质量比, 真实配液质量比, 时间` + +> **注意**:barCode 为 `null` 或 `"nullBarCode123456"` 是正常现象,表示 LIMS 中该物料尚未扫码。配液瓶缺失通常是因为物料未放在手动传递窗(`locationId` 前缀 `3a19deae-2c7a-`)。 diff --git a/.cursor/skills/yibin-electrolyte-submit/action-index.md b/.cursor/skills/yibin-electrolyte-submit/action-index.md new file mode 100644 index 00000000..2292242b --- /dev/null +++ b/.cursor/skills/yibin-electrolyte-submit/action-index.md @@ -0,0 +1,295 @@ +# Action 索引 + +> Action JSON 文件需运行提取命令生成,详见 [SKILL.md](SKILL.md) 中「生成 Action Schema」。 +> 以下描述和参数信息基于源码分析。 + +--- + +## 配液分液工站 (`bioyond_cell_workstation`) + +源码:`unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py` + +### 调度控制 + +#### `scheduler_start` + +启动 Bioyond LIMS 调度系统 + +- **核心参数**: 无(仅需 apiKey/requestTime,由设备内部处理) +- **返回**: LIMS 响应 `{code, message, data}` + +#### `scheduler_stop` + +停止调度 + +- **核心参数**: 无 + +#### `scheduler_continue` + +继续调度(从暂停状态恢复) + +- **核心参数**: 无 + +#### `scheduler_reset` + +复位调度 + +- **核心参数**: 无 + +#### `scheduler_start_and_auto_feeding` + +**组合操作**:启动调度 + 自动化上料(4号→3号手套箱) + +- **核心参数**: `xlsx_path`(Excel 物料模板路径,可选) +- **可选参数**: WH4 加样头面 12 个点位(materialName + quantity)、WH4 原液瓶面 9 个点位(materialName + quantity + materialType + targetWH)、WH3 人工堆栈 15 个点位(materialType + materialId + quantity) +- **流程**: 先 `scheduler_start()`,成功后执行 `auto_feeding4to3()` +- **备注**: 支持 Excel 模式和手动参数模式,Excel 路径存在时优先使用 Excel + +### 物料上料/下料 + +#### `auto_feeding4to3` + +自动化上料:从 4 号手套箱转运物料到 3 号手套箱 + +- **核心参数**: `xlsx_path`(Excel 物料模板路径) +- **可选参数**: 同 `scheduler_start_and_auto_feeding` 的 WH4/WH3 点位参数 +- **返回**: 等待上料任务完成后返回结果 + +#### `auto_batch_outbound_from_xlsx` + +自动化下料(从 Excel 读取下料信息) + +- **核心参数**: `xlsx_path`(Excel 下料模板) +- **Excel 列**: locationId, warehouseId, 数量, x, y, z + +### 物料管理 + +#### `create_and_inbound_materials` + +批量创建固体物料并入库 + +- **核心参数**: `material_names`(物料名称列表,默认 `["LiPF6", "LiDFOB", "DTD", "LiFSI", "LiPO2F2"]`) +- **可选参数**: `type_id`(物料类型ID), `warehouse_name`(目标仓库,默认 "粉末加样头堆栈") +- **流程**: 创建物料 → 批量入库 → 同步 + +#### `create_material` + +创建单个物料并可选入库 + +- **核心参数**: `material_name`, `type_id`, `warehouse_name` +- **可选参数**: `location_name_or_id`(库位编号如 "A01" 或 UUID) + +#### `create_sample` + +创建配液板物料(含子瓶)并入库 + +- **核心参数**: `name`, `board_type`(如 "5ml分液瓶板"), `bottle_type`(如 "5ml分液瓶"), `location_code`(如 "A01") +- **可选参数**: `warehouse_name`(默认 "手动堆栈") +- **备注**: 自动创建 2x4=8 个子瓶 + +#### `storage_inbound` + +单个物料入库 + +- **核心参数**: `material_id`, `location_id` + +#### `storage_batch_inbound` + +批量物料入库 + +- **核心参数**: `items`(`[{materialId, locationId}, ...]`) + +### 配液实验 + +#### `create_orders` + +从 Excel 文件创建配液实验订单 + +- **核心参数**: `xlsx_path`(Excel 文件路径) +- **Excel 列**: 配方ID, 创建日期, 配液瓶类型, 混匀时间(s), 扣电组装分液体积, 软包组装分液体积, 电导测试分液体积, 电导测试分液瓶数, 以及所有以 `(g)` 结尾的物料列 +- **流程**: 解析 Excel → 提交订单 → 等待全部完成 → 计算质量比 → 提取分液瓶板 → 创建资源树对象 +- **返回**: `{status, total_orders, bottle_count, reports, mass_ratios, vial_plates}` + +#### `create_orders_formulation` + +从配方列表创建配液实验订单(前端/API 输入版本) + +- **核心参数**: `formulation`(配方数组) +- **可选参数**: `batch_id`, `bottle_type`(默认 "配液小瓶"), `mix_time`(秒,列表), `coin_cell_volume`, `pouch_cell_volume`, `conductivity_volume`, `conductivity_bottle_count` +- **formulation 格式**: + ```json + [ + { + "order_name": "配方A", + "materials": [ + {"name": "LiPF6", "mass": 12.5}, + {"name": "EC", "mass": 50.0}, + {"name": "DMC", "mass": 37.5} + ] + } + ] + ``` +- **返回**: 同 `create_orders` + +### 物料转运 + +#### `transfer_3_to_2_to_1_auto` + +**自动转运**:从 create_orders 结果中自动定位分液瓶板并转运到目标设备 + +- **核心参数**: `vial_plates`(分液瓶板列表,来自 create_orders 返回的 `vial_plates`) +- **可选参数**: `target_device`(默认 "BatteryStation"), `target_location`(默认 "bottle_rack_6x2"), `mass_ratios`(配方信息) +- **流程**: 遍历瓶板 → 解析 locationId → 调用 LIMS 转运 API → 更新资源树 +- **返回**: `{total, success, failed, results}` + +#### `transfer_3_to_2_to_1` + +3→2→1 物料转运(手动指定坐标) + +- **核心参数**: `source_wh_id`, `source_x`, `source_y`, `source_z` + +#### `transfer_3_to_2` + +3→2 物料转运 + +- **核心参数**: `source_wh_id`, `source_x`, `source_y`, `source_z` + +#### `transfer_1_to_2` + +1→2 物料转运 + +- **核心参数**: 无 + +### 查询 + +#### `order_list_v2` + +批量查询实验报告 + +- **可选参数**: `timeType`, `beginTime`, `endTime`, `status`(60=运行中, 80=完成, 90=失败), `filter`, `skipCount`, `pageCount`, `sorting` + +--- + +## 扣电组装站 (`BatteryStation`) + +源码:`unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py` + +### 设备控制(组合操作) + +#### `func_pack_device_init_auto_start_combined` + +**组合操作**:设备初始化 → 物料搜寻确认 → 切换自动模式 → 启动 + +- **核心参数**: `material_search_enable`(是否启用物料搜寻,默认 `False`) +- **前置检查**: REG_UNILAB_INTERACT=False, COIL_GB_L_IGNORE_CMD=False, 所有握手寄存器无残留 +- **流程**: 手动模式 → 初始化命令 → 监测物料搜寻弹窗并自动处理 → 自动模式 → 启动 +- **返回**: `True`/`False` +- **备注**: 第一次运行必须调用此函数;后续批次调用 `func_sendbottle_allpack_multi` + +### 批量组装 + +#### `func_sendbottle_allpack_multi` + +**发送瓶数 + 批量组装**(适用于第二批次及后续批次) + +- **核心参数**: `elec_num`(电解液瓶数), `elec_use_num`(每瓶组装电池数), `elec_vol`(电解液吸液量 μL,默认 50) +- **可选参数**: + - 双滴模式:`dual_drop_mode`(bool), `dual_drop_first_volume`(μL), `dual_drop_suction_timing`(bool), `dual_drop_start_timing`(bool) + - 组装参数:`assembly_type`(7=不用铝箔垫/8=用), `assembly_pressure`(N,默认 4200) + - 物料参数:`fujipian_panshu`, `fujipian_juzhendianwei`, `gemopanshu`, `gemo_juzhendianwei`, `qiangtou_juzhendianwei` + - 开关:`lvbodian`(铝箔垫片), `battery_pressure_mode`(压力模式), `battery_clean_ignore`(忽略清洁) + - 其他:`file_path`(CSV保存路径), `formulations`(配方信息,用于CSV追溯) +- **流程**: 发送瓶数触发物料搬运 → 设置PLC参数 → 循环(等待PLC请求→下发参数→读取电池数据→写入CSV→更新资源树)→ 完成握手 +- **返回**: `{success, total_batteries, batteries, summary}` +- **备注**: 设备已初始化后直接调用;`formulations` 来自 create_orders 的 `mass_ratios` + +#### `func_allpack_cmd` + +全套组装(基础版本,含断点续传) + +- **核心参数**: `elec_num`, `elec_use_num`, `elec_vol`, `assembly_type`, `assembly_pressure`, `file_path` +- **返回**: `{success, total_batteries, batteries, summary}` + +#### `func_allpack_cmd_simp` + +增强版组装(含双滴模式 + 负极片/隔膜/枪头参数) + +- **核心参数**: 同 `func_sendbottle_allpack_multi` +- **备注**: 被 `func_sendbottle_allpack_multi` 内部调用 + +### 设备控制(单步操作) + +#### `func_pack_device_init` + +设备初始化(手动模式 → 初始化 → 复位标志) + +#### `func_pack_device_auto` + +切换自动模式 + +#### `func_pack_device_start` + +启动设备 + +#### `func_pack_device_stop` + +设备停止 + +#### `func_pack_send_bottle_num` + +发送电解液瓶数(触发物料搬运) + +- **核心参数**: `bottle_num`(瓶数) + +### PLC 参数设置 + +#### `qiming_coin_cell_code` + +设置组装物料参数 + +- **核心参数**: `fujipian_panshu`(负极片盘数) +- **可选参数**: `fujipian_juzhendianwei`, `gemopanshu`, `gemo_juzhendianwei`, `lvbodian`, `battery_pressure_mode`, `battery_pressure`, `battery_clean_ignore` + +### 数据采集 + +#### `func_read_data_and_output` + +持续数据采集并导出 CSV(后台循环运行) + +- **核心参数**: `file_path`(CSV 保存目录) +- **采集字段**: 开路电压, 极片质量, 组装时间, 压制力, 电解液加注量, 电池类型, 电解液二维码, 电池二维码 + +#### `func_stop_read_data` + +停止 CSV 数据采集 + +### 设备状态属性(只读) + +| 属性 | 类型 | 说明 | +|------|------|------| +| `sys_status` | str | 设备状态(启动中/停止中/复位中/初始化中) | +| `sys_mode` | str | 设备模式(手动/自动) | +| `data_assembly_coin_cell_num` | int | 已完成电池数量 | +| `data_assembly_time` | float | 单颗电池组装时间(秒) | +| `data_open_circuit_voltage` | float | 开路电压(V) | +| `data_pole_weight` | float | 正极片称重(g) | +| `data_glove_box_pressure` | float | 手套箱压力(mbar) | +| `data_glove_box_o2_content` | float | 手套箱氧含量(ppm) | +| `data_glove_box_water_content` | float | 手套箱水含量(ppm) | +| `data_coin_cell_code` | str | 电池二维码 | +| `data_electrolyte_code` | str | 电解液二维码 | + +--- + +## 配置参考 + +设备图文件 `yibin_electrolyte_config.json` 中的仓库映射(`warehouse_mapping`): + +| 仓库名称 | 说明 | 典型操作 | +|---------|------|---------| +| 粉末加样头堆栈 | 20 个点位 (A01-T01) | `create_and_inbound_materials` 入库目标 | +| 配液站内试剂仓库 | 9 个点位 (A01-C03) | 试剂存储 | +| 自动堆栈-左 | 4 个点位 | 分液瓶板存放,`transfer_3_to_2_to_1_auto` 的源位置 | +| 自动堆栈-右 | 4 个点位 | 分液瓶板存放 | +| 手动传递窗左/右 | 各 15 个点位 | 人工上料/下料 | +| 4号手套箱内部堆栈 | 12 个点位 | `auto_feeding4to3` 的源位置 | diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py index 67eeac83..dd2c5507 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -6,7 +6,7 @@ import threading import time import types from datetime import datetime -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional from functools import wraps from pylabrobot.resources import Deck, Resource as PLRResource from unilabos_msgs.msg import Resource @@ -1406,12 +1406,45 @@ class CoinCellAssemblyWorkstation(WorkstationBase): self._formulations_map = { f["orderCode"]: f for f in formulations } if formulations else {} - # ✅ 新增:存储配方列表(按接收顺序),用于索引访问 + # ✅ 新增:存储配方列表(按接收顺序),用于索引访问(兜底用) self._formulations_list = formulations + # ✅ 新增:按分液瓶条码(vial_bottle_barcodes)反向索引配方 + # 配液站夹爪取放顺序与扣电站夹取顺序可能不同,所以不能再依赖位置序号, + # 必须用扣电站扫码得到的 data_electrolyte_code 去对齐配液站登记的瓶条码。 + # vial_bottle_barcodes 字段可能形如 "LG100114"(单瓶)或 '["LG100114","LG100115"]'(多瓶)。 + self._formulations_by_vial_barcode: Dict[str, Dict] = {} + for f in formulations: + raw_barcodes = f.get("vial_bottle_barcodes", "") + if not raw_barcodes: + continue + barcodes: List[str] = [] + if isinstance(raw_barcodes, list): + barcodes = [str(b).strip() for b in raw_barcodes if b] + else: + s = str(raw_barcodes).strip() + if s.startswith("[") and s.endswith("]"): + try: + parsed = json.loads(s) + if isinstance(parsed, list): + barcodes = [str(b).strip() for b in parsed if b] + else: + barcodes = [str(parsed).strip()] + except Exception: + barcodes = [s] + else: + barcodes = [s] + for bc in barcodes: + if bc and bc not in self._formulations_by_vial_barcode: + self._formulations_by_vial_barcode[bc] = f + logger.info( + f"已建立分液瓶条码 → 配方索引: {len(self._formulations_by_vial_barcode)} 条 " + f"(条码: {list(self._formulations_by_vial_barcode.keys())})" + ) else: logger.warning("未接收到配方信息,CSV将不包含配方字段") self._formulations_map = {} self._formulations_list = [] + self._formulations_by_vial_barcode = {} # ✅ 新增:存储每瓶电池数,用于计算当前使用的瓶号 # ⚠️ 确保转换为整数(前端可能传递字符串) @@ -1680,50 +1713,78 @@ class CoinCellAssemblyWorkstation(WorkstationBase): # 从 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 + # ============================================================ + # ✅ 主方案:用扣电站扫码得到的电解液瓶条码 (data_electrolyte_code) + # 反查配液站登记的 vial_bottle_barcodes,避免依赖夹爪取放顺序。 + # 配液站和扣电站的瓶子顺序往往不一致(不同自动化设备的取放策略不同), + # 按位置序号匹配会错位;但每个瓶子的条码是唯一的,按条码匹配最可靠。 + # ============================================================ + formulation = None + match_method = "" + + barcode_map = getattr(self, "_formulations_by_vial_barcode", {}) or {} + scan_code = (data_electrolyte_code or "").strip() + if scan_code and scan_code != "N/A" and barcode_map: + formulation = barcode_map.get(scan_code) + if formulation is not None: + match_method = f"按条码({scan_code})精确匹配" 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] + logger.warning( + f"[CSV写入] 电池 {data_battery_number}: 扫码条码 {scan_code} " + f"在配方索引中找不到 (已登记条码: {list(barcode_map.keys())})" + ) + + # ============================================================ + # 🔁 降级方案:扫码失败 / 条码缺失时按瓶号位置兜底 + # 保留原有"每瓶电池数"或"二维码尾号"的位置推断逻辑, + # 确保在异常路径下仍能落盘(位置推断的结果可能不准,仅供回溯)。 + # ============================================================ + if formulation is None: + 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] + match_method = f"按位置兜底匹配[{current_bottle_index}]" + else: + logger.warning( + f"[CSV写入] 电池 {data_battery_number}: 瓶号索引 {current_bottle_index} " + f"超出配方列表范围 (共{len(self._formulations_list)}个配方)" + ) + + if formulation is not None: 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)}个配方)" + f"[CSV写入] 电池 {data_battery_number} ({match_method}): " + f"orderName={formulation_order_name}, 配液瓶={prep_bottle_barcode}, " + f"分液瓶={vial_bottle_barcodes}" ) else: logger.debug(f"[CSV写入] 电池 {data_battery_number}: 未找到配方信息数据")