mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-04-26 03:09:54 +00:00
chore: 鏈湴淇敼瀛樻。 - 0.10.18 鍩虹鐗堟湰澶囦唤 (2026-03-24)
Made-with: Cursor
This commit is contained in:
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` 目标查找方式 |
|
||||
| 验证计划 | 简述目标 | 提供具体测试命令和逐步手动验证流程 |
|
||||
Reference in New Issue
Block a user