mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 04:51:10 +00:00
bioyond_HR (#133)
* feat: Enhance Bioyond synchronization and resource management - Implemented synchronization for all material types (consumables, samples, reagents) from Bioyond, logging detailed information for each type. - Improved error handling and logging during synchronization processes. - Added functionality to save Bioyond material IDs in UniLab resources for future updates. - Enhanced the `sync_to_external` method to handle material movements correctly, including querying and creating materials in Bioyond. - Updated warehouse configurations to support new storage types and improved layout for better resource management. - Introduced new resource types such as reactors and tip boxes, with detailed specifications. - Modified warehouse factory to support column offsets for naming conventions (e.g., A05-D08). - Improved resource tracking by merging extra attributes instead of overwriting them. - Added a new method for updating resources in Bioyond, ensuring better synchronization of resource changes. * feat: 添加TipBox和Reactor的配置到bottles.yaml * fix: 修复液体投料方法中的volume参数处理逻辑
This commit is contained in:
@@ -24,13 +24,42 @@
|
|||||||
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
||||||
},
|
},
|
||||||
"material_type_mappings": {
|
"material_type_mappings": {
|
||||||
"烧杯": ["BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"],
|
"烧杯": [
|
||||||
"试剂瓶": ["BIOYOND_PolymerStation_1BottleCarrier", ""],
|
"BIOYOND_PolymerStation_1FlaskCarrier",
|
||||||
"样品板": ["BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"],
|
"3a14196b-24f2-ca49-9081-0cab8021bf1a"
|
||||||
"分装板": ["BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"],
|
],
|
||||||
"样品瓶": ["BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"],
|
"试剂瓶": [
|
||||||
"90%分装小瓶": ["BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"],
|
"BIOYOND_PolymerStation_1BottleCarrier",
|
||||||
"10%分装小瓶": ["BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"]
|
""
|
||||||
|
],
|
||||||
|
"样品板": [
|
||||||
|
"BIOYOND_PolymerStation_6StockCarrier",
|
||||||
|
"3a14196e-b7a0-a5da-1931-35f3000281e9"
|
||||||
|
],
|
||||||
|
"分装板": [
|
||||||
|
"BIOYOND_PolymerStation_6VialCarrier",
|
||||||
|
"3a14196e-5dfe-6e21-0c79-fe2036d052c4"
|
||||||
|
],
|
||||||
|
"样品瓶": [
|
||||||
|
"BIOYOND_PolymerStation_Solid_Stock",
|
||||||
|
"3a14196a-cf7d-8aea-48d8-b9662c7dba94"
|
||||||
|
],
|
||||||
|
"90%分装小瓶": [
|
||||||
|
"BIOYOND_PolymerStation_Solid_Vial",
|
||||||
|
"3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"
|
||||||
|
],
|
||||||
|
"10%分装小瓶": [
|
||||||
|
"BIOYOND_PolymerStation_Liquid_Vial",
|
||||||
|
"3a14196c-76be-2279-4e22-7310d69aed68"
|
||||||
|
],
|
||||||
|
"枪头盒": [
|
||||||
|
"BIOYOND_PolymerStation_TipBox",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"反应器": [
|
||||||
|
"BIOYOND_PolymerStation_Reactor",
|
||||||
|
""
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deck": {
|
"deck": {
|
||||||
@@ -46,8 +75,7 @@
|
|||||||
{
|
{
|
||||||
"id": "Bioyond_Deck",
|
"id": "Bioyond_Deck",
|
||||||
"name": "Bioyond_Deck",
|
"name": "Bioyond_Deck",
|
||||||
"children": [
|
"children": [],
|
||||||
],
|
|
||||||
"parent": "reaction_station_bioyond",
|
"parent": "reaction_station_bioyond",
|
||||||
"type": "deck",
|
"type": "deck",
|
||||||
"class": "BIOYOND_PolymerReactionStation_Deck",
|
"class": "BIOYOND_PolymerReactionStation_Deck",
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
||||||
"""指定库位出库物料"""
|
"""指定库位出库物料(通过库位名称)"""
|
||||||
location_id = LOCATION_MAPPING.get(location_name, location_name)
|
location_id = LOCATION_MAPPING.get(location_name, location_name)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
@@ -251,7 +251,36 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
})
|
})
|
||||||
|
|
||||||
if not response or response['code'] != 1:
|
if not response or response['code'] != 1:
|
||||||
return {}
|
return None
|
||||||
|
return response
|
||||||
|
|
||||||
|
def material_outbound_by_id(self, material_id: str, location_id: str, quantity: int) -> dict:
|
||||||
|
"""指定库位出库物料(直接使用location_id)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
material_id: 物料ID
|
||||||
|
location_id: 库位ID(不是库位名称,是UUID)
|
||||||
|
quantity: 数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: API响应,失败返回None
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"materialId": material_id,
|
||||||
|
"locationId": location_id,
|
||||||
|
"quantity": quantity
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
url=f'{self.host}/api/lims/storage/outbound',
|
||||||
|
params={
|
||||||
|
"apiKey": self.api_key,
|
||||||
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
|
"data": params
|
||||||
|
})
|
||||||
|
|
||||||
|
if not response or response['code'] != 1:
|
||||||
|
return None
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# ==================== 工作流查询相关接口 ====================
|
# ==================== 工作流查询相关接口 ====================
|
||||||
|
|||||||
@@ -48,3 +48,25 @@ BIOYOND_PolymerStation_Solution_Beaker:
|
|||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
BIOYOND_PolymerStation_TipBox:
|
||||||
|
category:
|
||||||
|
- bottles
|
||||||
|
- tip_boxes
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_TipBox
|
||||||
|
type: pylabrobot
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
BIOYOND_PolymerStation_Reactor:
|
||||||
|
category:
|
||||||
|
- bottles
|
||||||
|
- reactors
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Reactor
|
||||||
|
type: pylabrobot
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
|
|||||||
@@ -90,3 +90,89 @@ def BIOYOND_PolymerStation_Reagent_Bottle(
|
|||||||
barcode=barcode,
|
barcode=barcode,
|
||||||
model="BIOYOND_PolymerStation_Reagent_Bottle",
|
model="BIOYOND_PolymerStation_Reagent_Bottle",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def BIOYOND_PolymerStation_Reactor(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 30.0,
|
||||||
|
height: float = 80.0,
|
||||||
|
max_volume: float = 50000.0, # 50mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建反应器"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="BIOYOND_PolymerStation_Reactor",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def BIOYOND_PolymerStation_TipBox(
|
||||||
|
name: str,
|
||||||
|
size_x: float = 127.76, # 枪头盒宽度
|
||||||
|
size_y: float = 85.48, # 枪头盒长度
|
||||||
|
size_z: float = 100.0, # 枪头盒高度
|
||||||
|
barcode: str = None,
|
||||||
|
):
|
||||||
|
"""创建4×6枪头盒 (24个枪头)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 枪头盒名称
|
||||||
|
size_x: 枪头盒宽度 (mm)
|
||||||
|
size_y: 枪头盒长度 (mm)
|
||||||
|
size_z: 枪头盒高度 (mm)
|
||||||
|
barcode: 条形码
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TipBoxCarrier: 包含24个枪头孔位的枪头盒
|
||||||
|
"""
|
||||||
|
from pylabrobot.resources import Container, Coordinate
|
||||||
|
|
||||||
|
# 创建枪头盒容器
|
||||||
|
tip_box = Container(
|
||||||
|
name=name,
|
||||||
|
size_x=size_x,
|
||||||
|
size_y=size_y,
|
||||||
|
size_z=size_z,
|
||||||
|
category="tip_rack",
|
||||||
|
model="BIOYOND_PolymerStation_TipBox_4x6",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 设置自定义属性
|
||||||
|
tip_box.barcode = barcode
|
||||||
|
tip_box.tip_count = 24 # 4行×6列
|
||||||
|
tip_box.num_items_x = 6 # 6列
|
||||||
|
tip_box.num_items_y = 4 # 4行
|
||||||
|
|
||||||
|
# 创建24个枪头孔位 (4行×6列)
|
||||||
|
# 假设孔位间距为 9mm
|
||||||
|
tip_spacing_x = 9.0 # 列间距
|
||||||
|
tip_spacing_y = 9.0 # 行间距
|
||||||
|
start_x = 14.38 # 第一个孔位的x偏移
|
||||||
|
start_y = 11.24 # 第一个孔位的y偏移
|
||||||
|
|
||||||
|
for row in range(4): # A, B, C, D
|
||||||
|
for col in range(6): # 1-6
|
||||||
|
spot_name = f"{chr(65 + row)}{col + 1}" # A1, A2, ..., D6
|
||||||
|
x = start_x + col * tip_spacing_x
|
||||||
|
y = start_y + row * tip_spacing_y
|
||||||
|
|
||||||
|
# 创建枪头孔位容器
|
||||||
|
tip_spot = Container(
|
||||||
|
name=spot_name,
|
||||||
|
size_x=8.0, # 单个枪头孔位大小
|
||||||
|
size_y=8.0,
|
||||||
|
size_z=size_z - 10.0, # 略低于盒子高度
|
||||||
|
category="tip_spot",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加到枪头盒
|
||||||
|
tip_box.assign_child_resource(
|
||||||
|
tip_spot,
|
||||||
|
location=Coordinate(x=x, y=y, z=0)
|
||||||
|
)
|
||||||
|
|
||||||
|
return tip_box
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
from os import name
|
from os import name
|
||||||
from pylabrobot.resources import Deck, Coordinate, Rotation
|
from pylabrobot.resources import Deck, Coordinate, Rotation
|
||||||
|
|
||||||
from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling, bioyond_warehouse_1x2x2, bioyond_warehouse_1x3x3, bioyond_warehouse_10x1x1, bioyond_warehouse_3x3x1, bioyond_warehouse_3x3x1_2, bioyond_warehouse_5x1x1
|
from unilabos.resources.bioyond.warehouses import (
|
||||||
|
bioyond_warehouse_1x4x4,
|
||||||
|
bioyond_warehouse_1x4x4_right, # 新增:右侧仓库 (A05~D08)
|
||||||
|
bioyond_warehouse_1x4x2,
|
||||||
|
bioyond_warehouse_liquid_and_lid_handling,
|
||||||
|
bioyond_warehouse_1x2x2,
|
||||||
|
bioyond_warehouse_1x3x3,
|
||||||
|
bioyond_warehouse_10x1x1,
|
||||||
|
bioyond_warehouse_3x3x1,
|
||||||
|
bioyond_warehouse_3x3x1_2,
|
||||||
|
bioyond_warehouse_5x1x1,
|
||||||
|
bioyond_warehouse_1x8x4,
|
||||||
|
bioyond_warehouse_reagent_storage,
|
||||||
|
bioyond_warehouse_liquid_preparation,
|
||||||
|
bioyond_warehouse_tipbox_storage, # 新增:Tip盒堆栈
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BIOYOND_PolymerReactionStation_Deck(Deck):
|
class BIOYOND_PolymerReactionStation_Deck(Deck):
|
||||||
@@ -20,15 +35,22 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
|
|||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
# 添加仓库
|
# 添加仓库
|
||||||
|
# 说明: 堆栈1物理上分为左右两部分
|
||||||
|
# - 堆栈1左: A01~D04 (4行×4列, 位于反应站左侧)
|
||||||
|
# - 堆栈1右: A05~D08 (4行×4列, 位于反应站右侧)
|
||||||
self.warehouses = {
|
self.warehouses = {
|
||||||
"堆栈1": bioyond_warehouse_1x4x4("堆栈1"),
|
"堆栈1左": bioyond_warehouse_1x4x4("堆栈1左"), # 左侧堆栈: A01~D04
|
||||||
"堆栈2": bioyond_warehouse_1x4x4("堆栈2"),
|
"堆栈1右": bioyond_warehouse_1x4x4_right("堆栈1右"), # 右侧堆栈: A05~D08
|
||||||
"站内试剂存放堆栈": bioyond_warehouse_liquid_and_lid_handling("站内试剂存放堆栈"),
|
"站内试剂存放堆栈": bioyond_warehouse_reagent_storage("站内试剂存放堆栈"), # A01~A02
|
||||||
|
"移液站内10%分装液体准备仓库": bioyond_warehouse_liquid_preparation("移液站内10%分装液体准备仓库"), # A01~B04
|
||||||
|
"站内Tip盒堆栈": bioyond_warehouse_tipbox_storage("站内Tip盒堆栈"), # A01~B03, 存放枪头盒
|
||||||
}
|
}
|
||||||
self.warehouse_locations = {
|
self.warehouse_locations = {
|
||||||
"堆栈1": Coordinate(0.0, 430.0, 0.0),
|
"堆栈1左": Coordinate(0.0, 430.0, 0.0), # 左侧位置
|
||||||
"堆栈2": Coordinate(2550.0, 430.0, 0.0),
|
"堆栈1右": Coordinate(2500.0, 430.0, 0.0), # 右侧位置
|
||||||
"站内试剂存放堆栈": Coordinate(800.0, 475.0, 0.0),
|
"站内试剂存放堆栈": Coordinate(1100.0, 475.0, 0.0),
|
||||||
|
"移液站内10%分装液体准备仓库": Coordinate(1500.0, 300.0, 0.0),
|
||||||
|
"站内Tip盒堆栈": Coordinate(1800.0, 300.0, 0.0), # TODO: 根据实际位置调整坐标
|
||||||
}
|
}
|
||||||
self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90)
|
self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from unilabos.resources.warehouse import WareHouse, warehouse_factory
|
|||||||
|
|
||||||
|
|
||||||
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
||||||
"""创建BioYond 4x1x4仓库"""
|
"""创建BioYond 4x4x1仓库 (左侧堆栈: A01~D04)"""
|
||||||
return warehouse_factory(
|
return warehouse_factory(
|
||||||
name=name,
|
name=name,
|
||||||
num_items_x=4,
|
num_items_x=4,
|
||||||
@@ -15,6 +15,25 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
|||||||
item_dy=106.0,
|
item_dy=106.0,
|
||||||
item_dz=130.0,
|
item_dz=130.0,
|
||||||
category="warehouse",
|
category="warehouse",
|
||||||
|
col_offset=0, # 从01开始: A01, A02, A03, A04
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x4x4_right(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x4x1仓库 (右侧堆栈: A05~D08)"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=4,
|
||||||
|
num_items_y=4,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=147.0,
|
||||||
|
item_dy=106.0,
|
||||||
|
item_dz=130.0,
|
||||||
|
category="warehouse",
|
||||||
|
col_offset=4, # 从05开始: A05, A06, A07, A08
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -159,3 +178,71 @@ def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
|
|||||||
category="warehouse",
|
category="warehouse",
|
||||||
removed_positions=None
|
removed_positions=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def bioyond_warehouse_1x8x4(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 8x4x1反应站堆栈(A01~D08)"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=8, # 8列(01-08)
|
||||||
|
num_items_y=4, # 4行(A-D)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=147.0,
|
||||||
|
item_dy=106.0,
|
||||||
|
item_dz=130.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def bioyond_warehouse_reagent_storage(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond站内试剂存放堆栈(A01~A02, 1行×2列)"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=2, # 2列(01-02)
|
||||||
|
num_items_y=1, # 1行(A)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def bioyond_warehouse_liquid_preparation(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond移液站内10%分装液体准备仓库(A01~B04)"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=4, # 4列(01-04)
|
||||||
|
num_items_y=2, # 2行(A-B)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def bioyond_warehouse_tipbox_storage(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond站内Tip盒堆栈(A01~B03),用于存放枪头盒"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3, # 3列(01-03)
|
||||||
|
num_items_y=2, # 2行(A-B)
|
||||||
|
num_items_z=1, # 1层
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
@@ -580,6 +580,8 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w
|
|||||||
"trash": "trash",
|
"trash": "trash",
|
||||||
"deck": "deck",
|
"deck": "deck",
|
||||||
"tip_rack": "tip_rack",
|
"tip_rack": "tip_rack",
|
||||||
|
"warehouse": "warehouse",
|
||||||
|
"container": "container",
|
||||||
}
|
}
|
||||||
if source in replace_info:
|
if source in replace_info:
|
||||||
return replace_info[source]
|
return replace_info[source]
|
||||||
@@ -632,9 +634,24 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
type_mapping.get(material.get("typeName"), ("RegularContainer", ""))[0] if type_mapping else "RegularContainer"
|
type_mapping.get(material.get("typeName"), ("RegularContainer", ""))[0] if type_mapping else "RegularContainer"
|
||||||
)
|
)
|
||||||
|
|
||||||
plr_material: ResourcePLR = initialize_resource(
|
plr_material_result = initialize_resource(
|
||||||
{"name": material["name"], "class": className}, resource_type=ResourcePLR
|
{"name": material["name"], "class": className}, resource_type=ResourcePLR
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# initialize_resource 可能返回列表或单个对象
|
||||||
|
if isinstance(plr_material_result, list):
|
||||||
|
if len(plr_material_result) == 0:
|
||||||
|
logger.warning(f"物料 {material['name']} 初始化失败,跳过")
|
||||||
|
continue
|
||||||
|
plr_material = plr_material_result[0]
|
||||||
|
else:
|
||||||
|
plr_material = plr_material_result
|
||||||
|
|
||||||
|
# 确保 plr_material 是 ResourcePLR 实例
|
||||||
|
if not isinstance(plr_material, ResourcePLR):
|
||||||
|
logger.warning(f"物料 {material['name']} 不是有效的 ResourcePLR 实例,类型: {type(plr_material)}")
|
||||||
|
continue
|
||||||
|
|
||||||
plr_material.code = material.get("code", "") and material.get("barCode", "") or ""
|
plr_material.code = material.get("code", "") and material.get("barCode", "") or ""
|
||||||
plr_material.unilabos_uuid = str(uuid.uuid4())
|
plr_material.unilabos_uuid = str(uuid.uuid4())
|
||||||
|
|
||||||
@@ -659,6 +676,8 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
]
|
]
|
||||||
bottle.code = detail.get("code", "")
|
bottle.code = detail.get("code", "")
|
||||||
else:
|
else:
|
||||||
|
# 只对有 capacity 属性的容器(液体容器)处理液体追踪
|
||||||
|
if hasattr(plr_material, 'capacity'):
|
||||||
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
|
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
|
||||||
bottle.tracker.liquids = [
|
bottle.tracker.liquids = [
|
||||||
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
|
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
|
||||||
@@ -668,16 +687,55 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
|
|
||||||
if deck and hasattr(deck, "warehouses"):
|
if deck and hasattr(deck, "warehouses"):
|
||||||
for loc in material.get("locations", []):
|
for loc in material.get("locations", []):
|
||||||
if hasattr(deck, "warehouses") and loc.get("whName") in deck.warehouses:
|
wh_name = loc.get("whName")
|
||||||
warehouse = deck.warehouses[loc["whName"]]
|
|
||||||
idx = (
|
# 特殊处理: Bioyond的"堆栈1"需要映射到"堆栈1左"或"堆栈1右"
|
||||||
(loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y
|
# 根据列号(x)判断: 1-4映射到左侧, 5-8映射到右侧
|
||||||
+ (loc.get("x", 0) - 1) * warehouse.num_items_x
|
if wh_name == "堆栈1":
|
||||||
+ (loc.get("z", 0) - 1)
|
x_val = loc.get("x", 1)
|
||||||
)
|
if 1 <= x_val <= 4:
|
||||||
|
wh_name = "堆栈1左"
|
||||||
|
elif 5 <= x_val <= 8:
|
||||||
|
wh_name = "堆栈1右"
|
||||||
|
else:
|
||||||
|
logger.warning(f"物料 {material['name']} 的列号 x={x_val} 超出范围,无法映射到堆栈1左或堆栈1右")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if hasattr(deck, "warehouses") and wh_name in deck.warehouses:
|
||||||
|
warehouse = deck.warehouses[wh_name]
|
||||||
|
|
||||||
|
# Bioyond坐标映射 (重要!): x→行(1=A,2=B...), y→列(1=01,2=02...), z→层(通常=1)
|
||||||
|
# PyLabRobot warehouse是列优先存储: A01,B01,C01,D01, A02,B02,C02,D02, ...
|
||||||
|
x = loc.get("x", 1) # 行号 (1-based: 1=A, 2=B, 3=C, 4=D)
|
||||||
|
y = loc.get("y", 1) # 列号 (1-based: 1=01, 2=02, 3=03...)
|
||||||
|
z = loc.get("z", 1) # 层号 (1-based, 通常为1)
|
||||||
|
|
||||||
|
# 如果是右侧堆栈,需要调整列号 (5→1, 6→2, 7→3, 8→4)
|
||||||
|
if wh_name == "堆栈1右":
|
||||||
|
y = y - 4 # 将5-8映射到1-4
|
||||||
|
|
||||||
|
# 特殊处理:对于1行×N列的横向warehouse(如站内试剂存放堆栈)
|
||||||
|
# Bioyond的y坐标表示线性位置序号,而不是列号
|
||||||
|
if warehouse.num_items_y == 1:
|
||||||
|
# 1行warehouse: 直接用y作为线性索引
|
||||||
|
idx = y - 1
|
||||||
|
logger.debug(f"1行warehouse {wh_name}: y={y} → idx={idx}")
|
||||||
|
else:
|
||||||
|
# 多行warehouse: 使用列优先索引 (与Bioyond坐标系统一致)
|
||||||
|
# warehouse keys顺序: A01,B01,C01,D01, A02,B02,C02,D02, ...
|
||||||
|
# 索引计算: idx = (col-1) * num_rows + (row-1) + (layer-1) * (rows * cols)
|
||||||
|
row_idx = x - 1 # x表示行: 转为0-based
|
||||||
|
col_idx = y - 1 # y表示列: 转为0-based
|
||||||
|
layer_idx = z - 1 # 转为0-based
|
||||||
|
idx = layer_idx * (warehouse.num_items_x * warehouse.num_items_y) + col_idx * warehouse.num_items_y + row_idx
|
||||||
|
logger.debug(f"多行warehouse {wh_name}: x={x}(行),y={y}(列) → row={row_idx},col={col_idx} → idx={idx}")
|
||||||
|
|
||||||
if 0 <= idx < warehouse.capacity:
|
if 0 <= idx < warehouse.capacity:
|
||||||
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
|
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
|
||||||
warehouse[idx] = plr_material
|
warehouse[idx] = plr_material
|
||||||
|
logger.debug(f"✅ 物料 {material['name']} 放置到 {wh_name}[{idx}] (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')})")
|
||||||
|
else:
|
||||||
|
logger.warning(f"物料 {material['name']} 的索引 {idx} 超出仓库 {wh_name} 容量 {warehouse.capacity}")
|
||||||
|
|
||||||
return plr_materials
|
return plr_materials
|
||||||
|
|
||||||
@@ -714,8 +772,8 @@ def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict
|
|||||||
bottle = resource[0] if resource.capacity > 0 else resource
|
bottle = resource[0] if resource.capacity > 0 else resource
|
||||||
material = {
|
material = {
|
||||||
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
|
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
|
||||||
"name": resource.get("name", ""),
|
"name": resource.name if hasattr(resource, "name") else "",
|
||||||
"unit": "",
|
"unit": "个", # 修复:Bioyond API 要求 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": "{}"
|
||||||
}
|
}
|
||||||
@@ -759,6 +817,8 @@ def initialize_resource(resource_config: dict, resource_type: Any = None) -> Uni
|
|||||||
elif type(resource_class_config) == str:
|
elif type(resource_class_config) == str:
|
||||||
# Allow special resource class names to be used
|
# Allow special resource class names to be used
|
||||||
if resource_class_config not in lab_registry.resource_type_registry:
|
if resource_class_config not in lab_registry.resource_type_registry:
|
||||||
|
logger.warning(f"❌ 类 {resource_class_config} 不在 registry 中,返回原始配置")
|
||||||
|
logger.debug(f" 可用的类: {list(lab_registry.resource_type_registry.keys())[:10]}...")
|
||||||
return [resource_config]
|
return [resource_config]
|
||||||
# If the resource class is a string, look up the class in the
|
# If the resource class is a string, look up the class in the
|
||||||
# resource_type_registry and import it
|
# resource_type_registry and import it
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ def warehouse_factory(
|
|||||||
empty: bool = False,
|
empty: bool = False,
|
||||||
category: str = "warehouse",
|
category: str = "warehouse",
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
|
col_offset: int = 0, # 新增:列起始偏移量,用于生成A05-D08等命名
|
||||||
):
|
):
|
||||||
# 创建16个板架位 (4层 x 4位置)
|
# 创建16个板架位 (4层 x 4位置)
|
||||||
locations = []
|
locations = []
|
||||||
@@ -44,7 +45,9 @@ def warehouse_factory(
|
|||||||
name_prefix=name,
|
name_prefix=name,
|
||||||
)
|
)
|
||||||
len_x, len_y = (num_items_x, num_items_y) if num_items_z == 1 else (num_items_y, num_items_z) if num_items_x == 1 else (num_items_x, num_items_z)
|
len_x, len_y = (num_items_x, num_items_y) if num_items_z == 1 else (num_items_y, num_items_z) if num_items_x == 1 else (num_items_x, num_items_z)
|
||||||
keys = [f"{LETTERS[j]}{i + 1}" for i in range(len_x) for j in range(len_y)]
|
# 应用列偏移量,支持A05-D08等命名
|
||||||
|
# 使用列优先顺序生成keys (与Bioyond坐标系统一致): A01,B01,C01,D01, A02,B02,C02,D02, ...
|
||||||
|
keys = [f"{LETTERS[j]}{i + 1 + col_offset:02d}" for i in range(len_x) for j in range(len_y)]
|
||||||
sites = {i: site for i, site in zip(keys, _sites.values())}
|
sites = {i: site for i, site in zip(keys, _sites.values())}
|
||||||
|
|
||||||
return WareHouse(
|
return WareHouse(
|
||||||
|
|||||||
@@ -848,9 +848,15 @@ class DeviceNodeResourceTracker(object):
|
|||||||
extra: extra字典值
|
extra: extra字典值
|
||||||
"""
|
"""
|
||||||
if isinstance(resource, dict):
|
if isinstance(resource, dict):
|
||||||
resource["extra"] = extra
|
# ⭐ 修复:合并extra而不是覆盖
|
||||||
|
current_extra = resource.get("extra", {})
|
||||||
|
current_extra.update(extra)
|
||||||
|
resource["extra"] = current_extra
|
||||||
else:
|
else:
|
||||||
setattr(resource, "unilabos_extra", extra)
|
# ⭐ 修复:合并unilabos_extra而不是覆盖
|
||||||
|
current_extra = getattr(resource, "unilabos_extra", {})
|
||||||
|
current_extra.update(extra)
|
||||||
|
setattr(resource, "unilabos_extra", current_extra)
|
||||||
|
|
||||||
def _traverse_and_process(self, resource, process_func) -> int:
|
def _traverse_and_process(self, resource, process_func) -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user