From 467f0b1115eae2575697a4861ef77cfa2fa0f94b Mon Sep 17 00:00:00 2001 From: Andy6M Date: Wed, 25 Mar 2026 23:31:06 +0800 Subject: [PATCH] feat: update coin cell assembly, bioyond cell workstation, and resource configs --- CHANGES_2026_03_24.md | 168 ++++++++++++++++++ unilabos/app/main.py | 2 + .../bioyond_cell/20260323-1.xlsx | Bin 13112 -> 13083 bytes .../bioyond_studio/bioyond_cell/20260323.xlsx | Bin 13352 -> 13324 bytes .../bioyond_cell/bioyond_cell_workstation.py | 50 ++++-- .../bioyond_cell/material_template5.xlsx | Bin 0 -> 10771 bytes .../workstation/bioyond_studio/station.py | 14 +- .../coin_cell_assembly/YB_YH_materials.py | 24 ++- .../coin_cell_assembly/coin_cell_assembly.py | 7 +- .../coin_cell_assembly/date_20260325.csv | 29 +++ unilabos/registry/devices/bioyond_cell.yaml | 28 +-- unilabos/resources/bioyond/decks.py | 3 + unilabos/resources/graphio.py | 14 +- 13 files changed, 290 insertions(+), 49 deletions(-) create mode 100644 CHANGES_2026_03_24.md create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template5.xlsx create mode 100644 unilabos/devices/workstation/coin_cell_assembly/date_20260325.csv diff --git a/CHANGES_2026_03_24.md b/CHANGES_2026_03_24.md new file mode 100644 index 00000000..a514d165 --- /dev/null +++ b/CHANGES_2026_03_24.md @@ -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 个资源全部无法放置到对应仓库位,前端不显示。 diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 6c097682..546e9594 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -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"]) diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323-1.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323-1.xlsx index d2c41c5a1a9fb3d31d5439dc4eca3b0c5120eac5..6905ad87f3d440c30a248ec0dab25802cc907244 100644 GIT binary patch delta 3624 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 QE|3|pO(HG_=%3Ml0J&4mU;qFB delta 3613 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 Q0<6Ps!Nlm{(BFXn00rHzv;Y7A diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323.xlsx index 7c56216a4ffa5567e39c53a4d9e16a1a8096dd0a..ef9bca4027ed52dc52dceb13b45f0f24c7908a4f 100644 GIT binary patch delta 4964 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 pkRjp*C`5hLPR({d+ZyK;-x8zW`6)CCUH* delta 4939 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 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py index c03bf776..0e577abc 100644 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py @@ -979,10 +979,10 @@ class BioyondCellWorkstation(BioyondWorkstation): formulation: List[Dict[str, Any]], batch_id: str = "", bottle_type: str = "配液小瓶", - mix_time: int = 0, - load_shedding_info: float = 0.0, - pouch_cell_info: float = 0.0, - conductivity_info: float = 0.0, + mix_time: List[int] = [], + coin_cell_volume: float = 0.0, + pouch_cell_volume: float = 0.0, + conductivity_volume: float = 0.0, conductivity_bottle_count: int = 0, ) -> Dict[str, Any]: """ @@ -1003,10 +1003,10 @@ class BioyondCellWorkstation(BioyondWorkstation): ] batch_id: 批次ID,若为空则用当前时间戳 bottle_type: 配液瓶类型,默认 "配液小瓶" - mix_time: 混匀时间(秒) - load_shedding_info: 扣电组装分液体积 - pouch_cell_info: 软包组装分液体积 - conductivity_info: 电导测试分液体积 + mix_time: 混匀时间列表(秒),与 formulation 一一对应,不足则补 0 + coin_cell_volume: 纽扣电池组装分液体积 + pouch_cell_volume: 软包电池注液组装分液体积 + conductivity_volume: 电导率测试分液体积 conductivity_bottle_count: 电导测试分液瓶数 Returns: @@ -1039,9 +1039,10 @@ class BioyondCellWorkstation(BioyondWorkstation): logger.warning(f"[create_orders_formulation] 第 {idx + 1} 个配方无有效物料,跳过") continue + item_mix_time = mix_time[idx] if idx < len(mix_time) else 0 logger.info(f"[create_orders_formulation] 第 {idx + 1} 个配方: orderName={order_name}, " - f"loadShedding={load_shedding_info}, pouchCell={pouch_cell_info}, " - f"conductivity={conductivity_info}, totalMass={total_mass}, " + f"coinCellVolume={coin_cell_volume}, pouchCellVolume={pouch_cell_volume}, " + f"conductivityVolume={conductivity_volume}, totalMass={total_mass}, " f"material_count={len(mats)}") orders.append({ @@ -1049,10 +1050,10 @@ class BioyondCellWorkstation(BioyondWorkstation): "orderName": order_name, "createTime": create_time, "bottleType": bottle_type, - "mixTime": mix_time, - "loadSheddingInfo": load_shedding_info, - "pouchCellInfo": pouch_cell_info, - "conductivityInfo": conductivity_info, + "mixTime": item_mix_time, + "loadSheddingInfo": coin_cell_volume, + "pouchCellInfo": pouch_cell_volume, + "conductivityInfo": conductivity_volume, "conductivityBottleCount": conductivity_bottle_count, "materialInfos": mats, "totalMass": round(total_mass, 4), @@ -1650,18 +1651,31 @@ class BioyondCellWorkstation(BioyondWorkstation): Args: device_id: 目标设备 ID(如 "BatteryStation") - resource_name: 资源名称(如 "electrolyte_buffer") + resource_name: 资源名称(如 "bottle_rack_6x2") Returns: 找到的 PLR Resource 对象,未找到则返回 None """ + # 优先:通过全局设备注册表直接访问目标设备的 deck + # DeviceInfoType 是 TypedDict(即普通 dict),必须用 dict.get() 而非 getattr() try: - 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") + if driver is not None: + deck = getattr(driver, "deck", None) + if deck is not None and hasattr(deck, "get_resource"): + try: + res = deck.get_resource(resource_name) + if res is not None: + return res + except Exception: + pass except Exception: pass - # 降级:遍历 workstation 已注册的 plr_resources 列表 + # 降级:遍历 workstation 已注册的 plr_resources 列表(仅当前设备) try: for res in getattr(self, "_plr_resources", []): if res.name == resource_name: diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template5.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template5.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..27a311d72b619a51571ca8401c54e7524dddfe8c GIT binary patch literal 10771 zcmeHN^;=u%)(!41#T^O+cXv-I6p9vyAjRF?o#H`?OR*LW6eteGy*Lyv?hfT+=HBm{ zna+HF!M*2)ljKR(&N=5&>{ZGw);p+%$&dUp zX4q)4R93Yeq{hn-?Q8l`iPfV$qS7*E1nSvtB@yS*;cotd{`m|O1e|(zUQ$gs&445W zEB=pjefvc%76^$BN<>Gw=A;sQ#D<2k=9vKPLc4HY>&Q;HZN@jBV1bnFMcKXbK*ptB zK`be@ZnEnGBZs~sclbD^U7lHwbNEY*W)Q6Xt)@czy2oikxuAaVJv_ zq-wxsn;{WO9i%!7o4~7K00h=8+=;c1_Iy3#YWei;@S)}yhcfvK|FMtgid3o~tjs)7 zJ1AZ>nt~Qe6XaErp6KWZ*H({pqp)pG;@h5_i(H~&O!=;=8C?;X#_AeHamDzKy7)X> z)a}P?r7wdZ<-+fthB)hF$iDve^R+C*;A1ACge``Qb`V z(j~8f!Yk{e`+HBwg?UYZM*+6C8*ZvBN>G;hWV2=`=%KL@2`9-(E_Pp`GmyrnY_I%+ zJC=DfZ}vhgs$wlagpK|&fXG!|vnNQjNzY0^JH?4e_go39j9xEglRRvNM0J>_XG8&6 z*cmQg?7HQqg^yqPqt&37eSZ-xFripxBthpoVNPoIj5l91uZXVWli$Ej#K0YmDICRf z-G`hXN7mzNYI5|U8F1NKH~RtzbuERm&UpTd5ILW1jutMg5^X#7{`@P>YEjb>tx#4(f0Rri+7Ih9oedKG@fTB?5fYw^1A zJ9G79L1Q@kc^OXi>k4?rd5;nw!6a0BIZ2~nwh%#MU; z1fV5DbVG6HIaQRCNxP4JwVeIf?izMuMrmJzBbp9BfYTxivW$5H7+}7nEqL0-_#w~b3(|+if(x{@cIqAcz--}%Rc1?rM(;UX^s$MY*~VT1$?I9}R&?+O)KEx(&LkD?97vmH?~w}j;5 z*lZ?#A(Q#~%9h)T7|9AdJ_yVx>98L)2yGdU7zsGWUfUu@Pzl`w+LDG{2f&JxAuQqVwG4;K{# zF-c8R+yY39qO}^Ro7RJ-d={rVQKtr(4Q5Wwtc=^WIbUB~0=NCgAozT$6f$L3#fFb0 zybenAqyq-To9(wE%>i}#NzWB%REzXxt_96dunuMOaD5&H$>QZD1->o{2n;VeV8<+7 z+w3-bZeZPG=Y+i?^n|w=zY|fVZkOp1$-K>4-8Lm6= zVv8oW!EAoO2lbe0%5Kfzs-aN6CG1FXkE?-bDwnGzu5q3(w|A7{Pw{Y#0vNox*zL)7;xgX*AAsthLc&BrF`g-gVoAWzEiGh!4)5e!ej_>U=S;B|-WCGu&@y}7dI zh;>K7E&3)|If$Z+SJWrWg)023Jf%NVqVr}xq*>!=)kW%VfT?Zalx6Vz$b6%}Z~C7L zo2Jx7YJEfiV2}&|AbR4$zq7QnB^d1D%>Mfe=WppcOUE|8fDikp!G}kf6JKfc{zC-Y z%VdD!<&=`f(Y&8|WUVBU0K%nm$%EQgX-?9-u`oa(;!)xM86qRKH^@1IqF z-C`h(RSvdzH`_aRws^m{u)CFHg%3`*?UFI`vGY6byuUx*W52Sec{JD;WXJfFK;Z0b zn2I!06JUlo*>XB@%7-I6jQ@}D>V|uxETEO*|#A-uK%z_3h2lx0c zTe^PV>qZZ@OF zKMpsOuVMdN`dL!19jR$ev(pv*^2jjd`g$9w2iFa!o%qXH|E;bEzA&6r17g9I;b8>l zHi4l!ionx2i^ki){fnuLOY7Te0U%gxEcdINmzPO9*E<-&RcQPGxUj?ao(cC;=PQ7aq zBwdB_4eho_AjGMGE3MO8s#BtrIU6*q7Z7XPH+$`oxAq_tl5cm3R5VoC&7P{u@_YYK#OEXM} zBt0pG&mWQ|LF63iA7>^JS5x_7

JvT%!Wz9O-wm3ovR=yymGECm8U0)FqYPln`ZU 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 literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index 60c18e1e..6a493ced 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -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) diff --git a/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py b/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py index 9a1cb2ff..41ccd1f0 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py +++ b/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py @@ -169,23 +169,28 @@ class MaterialPlate(ItemizedResource[MaterialHole]): model: Optional[str] = None, ) -> "MaterialPlate": """工厂方法:创建带 4x4 洞位的料板(仅用于初始 setup,不在反序列化路径调用)""" - plate = cls(name=name, size_x=size_x, size_y=size_y, size_z=size_z, category=category, model=model) + # 默认洞位间距(与 _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 * plate._unilabos_state["hole_spacing_x"]) / 2, - dy=(size_y - 4 * plate._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=plate._unilabos_state["hole_spacing_x"], - item_dy=plate._unilabos_state["hole_spacing_y"], + item_dx=hole_spacing_x, + item_dy=hole_spacing_y, size_x=16, size_y=16, size_z=16, ) - for hole_name, hole in holes.items(): - plate.assign_child_resource(hole, location=hole.location) - return plate + return cls( + name=name, size_x=size_x, size_y=size_y, size_z=size_z, + ordered_items=holes, category=category, model=model, + ) def update_locations(self): # TODO:调多次相加 @@ -542,6 +547,7 @@ class YihuaCoinCellDeck(Deck): size_z: float = 100.0, origin: Coordinate = Coordinate(-2200, 0, 0), category: str = "coin_cell_deck", + setup: bool = False, ): super().__init__( name=name, @@ -550,6 +556,8 @@ class YihuaCoinCellDeck(Deck): size_z=100.0, origin=origin, ) + if setup: + self.setup() def setup(self) -> None: """设置工作站的标准布局 - 包含子弹夹、料盘、瓶架等完整配置""" 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 dbb05e8c..b4333a57 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -193,7 +193,12 @@ class CoinCellAssemblyWorkstation(WorkstationBase): 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] }) diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20260325.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20260325.csv new file mode 100644 index 00000000..4fc794b4 --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/date_20260325.csv @@ -0,0 +1,29 @@ +Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code,formulation_order_code,formulation_ratio +20260325_132011,0.0,12.119999885559082,405.0,3189,20,7,test0008,13163721,, +20260325_132301,0.0,12.079999923706055,153.0,3172,20,7,test0008,13200631,, +20260325_132516,0.0,12.119999885559082,153.0,3205,20,7,test0008,13224031,, +20260325_132758,0.0,12.309999465942383,161.0,3221,20,7,test0008,13251351,, +20260325_133215,0.0,12.520000457763672,257.0,3318,20,7,NoRead88,13293861,, +20260325_133820,0.0,12.15999984741211,363.0,3269,20,7,NoRead88,13321291,, +20260325_134049,0.0,12.100000381469727,149.0,3383,20,7,NoRead88,13381641,, +20260325_134327,0.0,12.369999885559082,157.0,3237,20,7,NoRead88,13404651,, +20260325_160512,0.0,12.299999237060547,238.0,3577,20,7,NoRead88,16022161,, +20260325_160734,0.0,12.40000057220459,155.0,3464,20,7,NoRead88,16045481,, +20260325_161010,0.0,12.269999504089355,155.0,3609,20,7,NoRead88,60731181,, +20260325_161252,0.0,12.579999923706055,162.0,3496,20,7,NoRead88,16100671,, +20260325_161636,0.0,12.619999885559082,223.0,3399,20,7,NoRead88,16135951,, +20260325_161909,0.0,12.039999961853027,153.0,3302,20,7,NoRead88,16163351,, +20260325_162145,0.0,12.00999927520752,155.0,3350,20,7,NoRead88,16190731,, +20260325_162429,0.0,12.329998970031738,163.0,3561,20,7,NoRead88,16214361,, +20260325_162841,0.0,12.579999923706055,251.0,3593,20,7,NoRead88,16260311,, +20260325_163118,0.0,12.25999927520752,156.0,3545,20,7,NoRead88,16283921,, +20260325_163356,0.0,12.220000267028809,157.0,3464,20,7,NoRead88,16311611,, +20260325_163641,0.0,12.199999809265137,165.0,3674,20,7,NoRead88,16335401,, +20260325_164046,0.0,12.25,244.0,3512,20,7,NoRead88,16380881,, +20260325_164321,0.0,12.079999923706055,154.0,3609,20,7,NoRead88,16404401,, +20260325_164556,0.0,12.029999732971191,155.0,3593,20,7,NoRead88,16431851,, +20260325_164840,0.0,12.100000381469727,163.0,3496,20,7,NoRead88,16455451,, +20260325_172206,0.0,12.00999927520752,245.0,3011,20,7,NoRead88,17193041,BSO2026032500006,"{""EMC"": 0.949, ""LiFSI"": 0.051}" +20260325_172608,0.0,12.0,242.0,3253,20,7,NoRead88,17233491,BSO2026032500007,"{""EMC"": 0.9582, ""LiFSI"": 0.0418}" +20260325_183415,0.0,12.690000534057617,1226.0,3528,20,7,NoRead88,18150131,BSO2026032500011,"{""EMC"": 0.949, ""LiFSI"": 0.051}" +20260325_190044,0.0,12.130000114440918,1586.0,3528,20,7,NoRead88,18355771,BSO2026032500012,"{""EMC"": 0.9582, ""LiFSI"": 0.0418}" diff --git a/unilabos/registry/devices/bioyond_cell.yaml b/unilabos/registry/devices/bioyond_cell.yaml index aa6abd96..9b3f293b 100644 --- a/unilabos/registry/devices/bioyond_cell.yaml +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -196,11 +196,11 @@ bioyond_cell: batch_id: '' bottle_type: 配液小瓶 conductivity_bottle_count: 0 - conductivity_info: 0.0 + conductivity_volume: 0.0 formulation: null - load_shedding_info: 0.0 - mix_time: 0 - pouch_cell_info: 0.0 + coin_cell_volume: 0.0 + mix_time: [] + pouch_cell_volume: 0.0 handles: output: - data_key: total_orders @@ -239,9 +239,9 @@ bioyond_cell: default: 0 description: 电导测试分液瓶数 type: integer - conductivity_info: + conductivity_volume: default: 0.0 - description: 电导测试分液体积 + description: 电导率测试分液体积 type: number formulation: description: 配方列表,每个元素代表一个订单(一瓶) @@ -269,17 +269,19 @@ bioyond_cell: - materials type: object type: array - load_shedding_info: + coin_cell_volume: default: 0.0 - description: 扣电组装分液体积 + description: 纽扣电池组装分液体积 type: number mix_time: - default: 0 - description: 混匀时间(秒) - type: integer - pouch_cell_info: + default: [] + description: 混匀时间列表(秒),与 formulation 一一对应 + items: + type: integer + type: array + pouch_cell_volume: default: 0.0 - description: 软包组装分液体积 + description: 软包电池注液组装分液体积 type: number required: - formulation diff --git a/unilabos/resources/bioyond/decks.py b/unilabos/resources/bioyond/decks.py index fdc470e4..f711b1d1 100644 --- a/unilabos/resources/bioyond/decks.py +++ b/unilabos/resources/bioyond/decks.py @@ -104,8 +104,11 @@ class BioyondElectrolyteDeck(Deck): size_y: float = 1400.0, size_z: float = 2670.0, category: str = "deck", + setup: bool = False, ) -> None: super().__init__(name=name, size_x=4150.0, size_y=1400.0, size_z=2670.0) + if setup: + self.setup() def setup(self) -> None: # 添加仓库 diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index c8f1cc2c..67f594db 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -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)