diff --git a/test/experiments/comprehensive_protocol/comprehensive_slim.json b/test/experiments/comprehensive_protocol/comprehensive_slim.json index f533d22b..83e15679 100644 --- a/test/experiments/comprehensive_protocol/comprehensive_slim.json +++ b/test/experiments/comprehensive_protocol/comprehensive_slim.json @@ -41,7 +41,7 @@ "HydrogenateProtocol", "RecrystallizeProtocol" ], - "station_resource": { + "deck": { "data": { "_resource_child_name": "deck", "_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck" diff --git a/test/experiments/reaction_station_bioyond_test.json b/test/experiments/reaction_station_bioyond_test.json new file mode 100644 index 00000000..8446373a --- /dev/null +++ b/test/experiments/reaction_station_bioyond_test.json @@ -0,0 +1,69 @@ +{ + "nodes": [ + { + "id": "reaction_station_bioyond", + "name": "reaction_station_bioyond", + "parent": null, + "children": [ + "Bioyond_Deck" + ], + "type": "device", + "class": "workstation.bioyond", + "config": { + "bioyond_config": { + "api_key": "DE9BDDA0", + "api_host": "http://192.168.1.200:44388", + "workflow_mappings": { + "reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1", + "reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6", + "Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6", + "Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47", + "Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046", + "Liquid_feeding(titration)": "3a160824-0665-01ed-285a-51ef817a9046", + "Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784", + "Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a" + }, + "material_type_mappings": { + "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier", + "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier", + "样品板": "BIOYOND_PolymerStation_6VialCarrier" + } + }, + "deck": { + "data": { + "_resource_child_name": "Bioyond_Deck", + "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck" + } + }, + "protocol_type": [] + }, + "data": {} + }, + { + "id": "Bioyond_Deck", + "name": "Bioyond_Deck", + "sample_id": null, + "children": [ + ], + "parent": "reaction_station_bioyond", + "type": "deck", + "class": "BIOYOND_PolymerReactionStation_Deck", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "BIOYOND_PolymerReactionStation_Deck", + "setup": true, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + } + }, + "data": {} + } + ] +} \ No newline at end of file diff --git a/test/resources/bioyond_materials.json b/test/resources/bioyond_materials.json new file mode 100644 index 00000000..9e6d7f6d --- /dev/null +++ b/test/resources/bioyond_materials.json @@ -0,0 +1,198 @@ +{ + "data": [ + { + "id": "3a1c67a9-aed7-b94d-9e24-bfdf10c8baa9", + "typeName": "烧杯", + "code": "0006-00160", + "barCode": "", + "name": "ODA", + "quantity": 120000.00000000000000000000000, + "lockQuantity": 695374.00000000000000000000000, + "unit": "微升", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a14aa17-0d49-11d7-a6e1-f236b3e5e5a3", + "whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366", + "whName": "堆栈1", + "code": "0001-0001", + "x": 1, + "y": 1, + "z": 1, + "quantity": 0 + } + ], + "detail": [] + }, + { + "id": "3a1c67a9-aed9-1ade-5fe1-cc04b24b171c", + "typeName": "烧杯", + "code": "0006-00161", + "barCode": "", + "name": "MPDA", + "quantity": 120000.00000000000000000000000, + "lockQuantity": 681618.00000000000000000000000, + "unit": "", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a14aa17-0d49-4bc5-8836-517b75473f5f", + "whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366", + "whName": "堆栈1", + "code": "0001-0002", + "x": 1, + "y": 2, + "z": 1, + "quantity": 0 + } + ], + "detail": [] + }, + { + "id": "3a1c67a9-aed9-2864-6783-2cee4e701ba6", + "typeName": "试剂瓶", + "code": "0004-00041", + "barCode": "", + "name": "NMP", + "quantity": 300000.00000000000000000000000, + "lockQuantity": 380000.00000000000000000000000, + "unit": "微升", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a14aa3b-9fab-adac-7b9c-e1ee446b51d5", + "whid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c", + "whName": "站内试剂存放堆栈", + "code": "0003-0001", + "x": 1, + "y": 1, + "z": 1, + "quantity": 0 + } + ], + "detail": [] + }, + { + "id": "3a1c67a9-aed9-32c7-5809-3ba1b8db1aa1", + "typeName": "试剂瓶", + "code": "0004-00042", + "barCode": "", + "name": "PGME", + "quantity": 300000.00000000000000000000000, + "lockQuantity": 337892.00000000000000000000000, + "unit": "", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a14aa3b-9fab-ca72-febc-b7c304476c78", + "whid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c", + "whName": "站内试剂存放堆栈", + "code": "0003-0002", + "x": 1, + "y": 2, + "z": 1, + "quantity": 0 + } + ], + "detail": [] + }, + { + "id": "3a1c68c8-0574-d748-725e-97a2e549f085", + "typeName": "样品板", + "code": "0001-00004", + "barCode": "", + "name": "0917", + "quantity": 1.0000000000000000000000000000, + "lockQuantity": 4.0000000000000000000000000000, + "unit": "块", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a14aa17-0d49-f49c-6b66-b27f185a3b32", + "whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366", + "whName": "堆栈1", + "code": "0001-0009", + "x": 2, + "y": 1, + "z": 1, + "quantity": 0 + } + ], + "detail": [ + { + "id": "3a1c68c8-0574-69a1-9858-4637e0193451", + "detailMaterialId": "3a1c68c8-0574-3630-bd42-bbf3623c5208", + "code": null, + "name": "SIDA", + "quantity": "300000", + "lockQuantity": "4", + "unit": "微升", + "x": 1, + "y": 2, + "z": 1, + "associateId": null + }, + { + "id": "3a1c68c8-0574-8d51-3191-a31f5be421e5", + "detailMaterialId": "3a1c68c8-0574-3b20-9ad7-90755f123d53", + "code": null, + "name": "BTDA-2", + "quantity": "300000", + "lockQuantity": "4", + "unit": "微升", + "x": 2, + "y": 2, + "z": 1, + "associateId": null + }, + { + "id": "3a1c68c8-0574-da80-735b-53ae2197a360", + "detailMaterialId": "3a1c68c8-0574-f2e4-33b3-90d813567939", + "code": null, + "name": "BTDA-DD", + "quantity": "300000", + "lockQuantity": "28", + "unit": "微升", + "x": 1, + "y": 1, + "z": 1, + "associateId": null + }, + { + "id": "3a1c68c8-0574-e717-1b1b-99891f875455", + "detailMaterialId": "3a1c68c8-0574-a0ef-e636-68cdc98960e2", + "code": null, + "name": "BTDA-3", + "quantity": "300000", + "lockQuantity": "4", + "unit": "微升", + "x": 2, + "y": 3, + "z": 1, + "associateId": null + }, + { + "id": "3a1c68c8-0574-e9bd-6cca-5e261b4f89cb", + "detailMaterialId": "3a1c68c8-0574-9d11-5115-283e8e5510b1", + "code": null, + "name": "BTDA-1", + "quantity": "300000", + "lockQuantity": "4", + "unit": "微升", + "x": 2, + "y": 1, + "z": 1, + "associateId": null + } + ] + } + ], + "code": 1, + "message": "", + "timestamp": 1758560573511 +} \ No newline at end of file diff --git a/test/resources/test_bottle_carrier.py b/test/resources/test_bottle_carrier.py new file mode 100644 index 00000000..c981eeeb --- /dev/null +++ b/test/resources/test_bottle_carrier.py @@ -0,0 +1,48 @@ +import pytest + +from unilabos.resources.bioyond.bottle_carriers import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier +from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle + + +def test_bottle_carrier() -> "BottleCarrier": + print("创建载架...") + + # 创建6瓶载架 + bottle_carrier = BIOYOND_Electrolyte_6VialCarrier("powder_carrier_01") + print(f"6瓶载架: {bottle_carrier.name}, 位置数: {len(bottle_carrier.sites)}") + + # 创建1烧杯载架 + beaker_carrier = BIOYOND_Electrolyte_1BottleCarrier("solution_carrier_01") + print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}") + + # 创建瓶子和烧杯 + powder_bottle = BIOYOND_PolymerStation_Solid_Vial("powder_bottle_01") + solution_beaker = BIOYOND_PolymerStation_Solution_Beaker("solution_beaker_01") + reagent_bottle = BIOYOND_PolymerStation_Reagent_Bottle("reagent_bottle_01") + + print(f"\n创建的物料:") + print(f"粉末瓶: {powder_bottle.name} - {powder_bottle.diameter}mm x {powder_bottle.height}mm, {powder_bottle.max_volume}μL") + print(f"溶液烧杯: {solution_beaker.name} - {solution_beaker.diameter}mm x {solution_beaker.height}mm, {solution_beaker.max_volume}μL") + print(f"试剂瓶: {reagent_bottle.name} - {reagent_bottle.diameter}mm x {reagent_bottle.height}mm, {reagent_bottle.max_volume}μL") + + # 测试放置容器 + print(f"\n测试放置容器...") + + # 通过载架的索引操作来放置容器 + # bottle_carrier[0] = powder_bottle # 放置粉末瓶到第一个位置 + print(f"粉末瓶已放置到6瓶载架的位置 0") + + # beaker_carrier[0] = solution_beaker # 放置烧杯到第一个位置 + print(f"溶液烧杯已放置到1烧杯载架的位置 0") + + # 验证放置结果 + print(f"\n验证放置结果:") + bottle_at_0 = bottle_carrier[0].resource + beaker_at_0 = beaker_carrier[0].resource + + if bottle_at_0: + print(f"位置 0 的瓶子: {bottle_at_0.name}") + if beaker_at_0: + print(f"位置 0 的烧杯: {beaker_at_0.name}") + + print("\n载架设置完成!") \ No newline at end of file diff --git a/test/resources/test_converter_bioyond.py b/test/resources/test_converter_bioyond.py new file mode 100644 index 00000000..1044a8d0 --- /dev/null +++ b/test/resources/test_converter_bioyond.py @@ -0,0 +1,35 @@ +import pytest +import json +import os + +from unilabos.resources.graphio import resource_bioyond_to_plr +from unilabos.registry.registry import lab_registry + +from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck + +lab_registry.setup() + + +type_mapping = { + "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier", + "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier", + "样品板": "BIOYOND_PolymerStation_6VialCarrier", +} + +@pytest.fixture +def bioyond_materials() -> list[dict]: + print("加载 BioYond 物料数据...") + print(os.getcwd()) + with open("bioyond_materials.json", "r", encoding="utf-8") as f: + data = json.load(f)["data"] + print(f"加载了 {len(data)} 条物料数据") + return data + + +def test_bioyond_to_plr(bioyond_materials) -> list[dict]: + deck = BIOYOND_PolymerReactionStation_Deck("test_deck") + print("将 BioYond 物料数据转换为 PLR 格式...") + output = resource_bioyond_to_plr(bioyond_materials, type_mapping=type_mapping, deck=deck) + print(deck.summary()) + print([resource.serialize() for resource in output]) + print([resource.serialize_all_state() for resource in output]) diff --git a/unilabos/devices/battery/battery.json b/unilabos/devices/battery/battery.json new file mode 100644 index 00000000..105ba5bd --- /dev/null +++ b/unilabos/devices/battery/battery.json @@ -0,0 +1,29 @@ +{ + "nodes": [ + { + "id": "NEWARE_BATTERY_TEST_SYSTEM", + "name": "Neware Battery Test System", + "parent": null, + "type": "device", + "class": "neware_battery_test_system", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "ip": "127.0.0.1", + "port": 502, + "machine_id": 1, + "devtype": "27", + "timeout": 20, + "size_x": 500.0, + "size_y": 500.0, + "size_z": 2000.0 + }, + "data": {}, + "children": [] + } + ], + "links": [] +} \ No newline at end of file diff --git a/unilabos/devices/battery/neware_battery_test_system.py b/unilabos/devices/battery/neware_battery_test_system.py new file mode 100644 index 00000000..317e8fe5 --- /dev/null +++ b/unilabos/devices/battery/neware_battery_test_system.py @@ -0,0 +1,1042 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +新威电池测试系统设备类 +- 提供TCP通信接口查询电池通道状态 +- 支持720个通道(devid 1-7, 8, 86) +- 兼容BTSAPI getchlstatus协议 + +设备特点: +- TCP连接: 默认127.0.0.1:502 +- 通道映射: devid->subdevid->chlid 三级结构 +- 状态类型: working/stop/finish/protect/pause/false/unknown +""" + +import socket +import xml.etree.ElementTree as ET +import json +import time +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, TypedDict + +from pylabrobot.resources import ResourceHolder, Coordinate, create_ordered_items_2d, Deck, Plate + +from unilabos.ros.nodes.base_device_node import ROS2DeviceNode +from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode + + +# ======================== +# 内部数据类和结构 +# ======================== + +@dataclass(frozen=True) +class ChannelKey: + devid: int + subdevid: int + chlid: int + + +@dataclass +class ChannelStatus: + state: str # working/stop/finish/protect/pause/false/unknown + color: str # 状态对应颜色 + current_A: float # 电流 (A) + voltage_V: float # 电压 (V) + totaltime_s: float # 总时间 (s) + + +class BatteryTestPositionState(TypedDict): + voltage: float # 电压 (V) + current: float # 电流 (A) + time: float # 时间 (s) - 使用totaltime + capacity: float # 容量 (Ah) + energy: float # 能量 (Wh) + + status: str # 通道状态 + color: str # 状态对应颜色 + + # 额外的inquire协议字段 + relativetime: float # 相对时间 (s) + open_or_close: int # 0=关闭, 1=打开 + step_type: str # 步骤类型 + cycle_id: int # 循环ID + step_id: int # 步骤ID + log_code: str # 日志代码 + + +class BatteryTestPosition(ResourceHolder): + def __init__( + self, + name, + size_x=60, + size_y=60, + size_z=60, + rotation=None, + category="resource_holder", + model=None, + child_location: Coordinate = Coordinate.zero(), + ): + super().__init__(name, size_x, size_y, size_z, rotation, category, model, child_location=child_location) + self._unilabos_state: Dict[str, Any] = {} + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) + return data + + +class NewareBatteryTestSystem: + """ + 新威电池测试系统设备类 + + 提供电池测试通道状态查询、控制等功能。 + 支持720个通道的状态监控和数据导出。 + 包含完整的物料管理系统,支持2盘电池的状态映射。 + + Attributes: + ip (str): TCP服务器IP地址,默认127.0.0.1 + port (int): TCP端口,默认502 + devtype (str): 设备类型,默认"27" + timeout (int): 通信超时时间(秒),默认20 + """ + + # ======================== + # 基本通信与协议参数 + # ======================== + BTS_IP = "127.0.0.1" + BTS_PORT = 502 + DEVTYPE = "27" + TIMEOUT = 20 # 秒 + REQ_END = b"#\r\n" # 常见实现以 "#\\r\\n" 作为报文结束 + + # ======================== + # 状态与颜色映射(前端可直接使用) + # ======================== + STATUS_SET = {"working", "stop", "finish", "protect", "pause", "false"} + STATUS_COLOR = { + "working": "#22c55e", # 绿 + "stop": "#6b7280", # 灰 + "finish": "#3b82f6", # 蓝 + "protect": "#ef4444", # 红 + "pause": "#f59e0b", # 橙 + "false": "#9ca3af", # 不存在/无效 + "unknown": "#a855f7", # 未知 + } + + # 字母常量 + ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz' + ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + LETTERS = ascii_uppercase + ascii_lowercase + + def __init__(self, + ip: str = None, + port: int = None, + machine_id: int = 1, + devtype: str = None, + timeout: int = None, + + size_x: float = 500.0, + size_y: float = 500.0, + size_z: float = 2000.0, + ): + """ + 初始化新威电池测试系统 + + Args: + ip: TCP服务器IP地址 + port: TCP端口 + devtype: 设备类型标识 + timeout: 通信超时时间(秒) + machine_id: 机器ID + size_x, size_y, size_z: 设备物理尺寸 + """ + self.ip = ip or self.BTS_IP + self.port = port or self.BTS_PORT + self.machine_id = machine_id + self.devtype = devtype or self.DEVTYPE + self.timeout = timeout or self.TIMEOUT + self._last_status_update = None + self._cached_status = {} + self._ros_node: Optional[ROS2WorkstationNode] = None # ROS节点引用,由框架设置 + + + def post_init(self, ros_node): + """ + ROS节点初始化后的回调方法,用于建立设备连接 + + Args: + ros_node: ROS节点实例 + """ + self._ros_node = ros_node + # 创建2盘电池的物料管理系统 + self._setup_material_management() + # 初始化通道映射 + self._channels = self._build_channel_map() + try: + # 测试设备连接 + if self.test_connection(): + ros_node.lab_logger().info(f"新威电池测试系统连接成功: {self.ip}:{self.port}") + else: + ros_node.lab_logger().warning(f"新威电池测试系统连接失败: {self.ip}:{self.port}") + except Exception as e: + ros_node.lab_logger().error(f"新威电池测试系统初始化失败: {e}") + # 不抛出异常,允许节点继续运行,后续可以重试连接 + + def _setup_material_management(self): + """设置物料管理系统""" + # 第1盘:5行8列网格 (A1-E8) - 5行对应subdevid 1-5,8列对应chlid 1-8 + # 先给物料设置一个最大的Deck + deck_main = Deck("ADeckName", 200, 200, 200) + + plate1_resources: Dict[str, BatteryTestPosition] = create_ordered_items_2d( + BatteryTestPosition, + num_items_x=8, # 8列(对应chlid 1-8) + num_items_y=5, # 5行(对应subdevid 1-5,即A-E) + dx=10, + dy=10, + dz=0, + item_dx=45, + item_dy=45 + ) + plate1 = Plate("P1", 400, 300, 50, ordered_items=plate1_resources) + deck_main.assign_child_resource(plate1, location=Coordinate(0, 0, 0)) + + # 只有在真实ROS环境下才调用update_resource + if hasattr(self._ros_node, 'update_resource') and callable(getattr(self._ros_node, 'update_resource')): + try: + ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + "resources": [deck_main] + }) + except Exception as e: + if hasattr(self._ros_node, 'lab_logger'): + self._ros_node.lab_logger().warning(f"更新资源失败: {e}") + # 在非ROS环境下忽略此错误 + + # 为第1盘资源添加P1_前缀 + self.station_resources_plate1 = {} + for name, resource in plate1_resources.items(): + new_name = f"P1_{name}" + self.station_resources_plate1[new_name] = resource + + # 第2盘:5行8列网格 (A1-E8),在Z轴上偏移 - 5行对应subdevid 6-10,8列对应chlid 1-8 + plate2_resources = create_ordered_items_2d( + BatteryTestPosition, + num_items_x=8, # 8列(对应chlid 1-8) + num_items_y=5, # 5行(对应subdevid 6-10,即A-E) + dx=10, + dy=10, + dz=100, # Z轴偏移100mm + item_dx=65, + item_dy=65 + ) + + # 为第2盘资源添加P2_前缀 + self.station_resources_plate2 = {} + for name, resource in plate2_resources.items(): + new_name = f"P2_{name}" + self.station_resources_plate2[new_name] = resource + + # 合并两盘资源为统一的station_resources + self.station_resources = {} + self.station_resources.update(self.station_resources_plate1) + self.station_resources.update(self.station_resources_plate2) + + # ======================== + # 核心属性(Uni-Lab标准) + # ======================== + + @property + def status(self) -> str: + """设备状态属性 - 会被自动识别并定时广播""" + try: + if self.test_connection(): + return "Connected" + else: + return "Disconnected" + except: + return "Error" + + @property + def channel_status(self) -> Dict[int, Dict]: + """ + 获取所有通道状态(按设备ID分组) + + 这个属性会执行实际的TCP查询并返回格式化的状态数据。 + 结果按设备ID分组,包含统计信息和详细状态。 + + Returns: + Dict[int, Dict]: 按设备ID分组的通道状态统计 + """ + status_map = self._query_all_channels() + status_processed = {} if not status_map else self._group_by_devid(status_map) + + # 修复数据过滤逻辑:如果machine_id对应的数据不存在,尝试使用第一个可用的设备数据 + status_current_machine = status_processed.get(self.machine_id, {}) + + if not status_current_machine and status_processed: + # 如果machine_id没有匹配到数据,使用第一个可用的设备数据 + first_devid = next(iter(status_processed.keys())) + status_current_machine = status_processed[first_devid] + if self._ros_node: + self._ros_node.lab_logger().warning( + f"machine_id {self.machine_id} 没有匹配到数据,使用设备ID {first_devid} 的数据" + ) + + # 确保有默认的数据结构 + if not status_current_machine: + status_current_machine = { + "stats": {s: 0 for s in self.STATUS_SET | {"unknown"}}, + "subunits": {} + } + + # 确保subunits存在 + subunits = status_current_machine.get("subunits", {}) + + # 处理2盘电池的状态映射 + self._update_plate_resources(subunits) + + return status_current_machine + + def _update_plate_resources(self, subunits: Dict): + """更新两盘电池资源的状态""" + # 第1盘:subdevid 1-5 映射到 P1_A1-P1_E8 (5行8列) + for subdev_id in range(1, 6): # subdevid 1-5 + status_row = subunits.get(subdev_id, {}) + + for chl_id in range(1, 9): # chlid 1-8 + try: + # 计算在5×8网格中的位置 + row_idx = (subdev_id - 1) # 0-4 (对应A-E) + col_idx = (chl_id - 1) # 0-7 (对应1-8) + resource_name = f"P1_{self.LETTERS[row_idx]}{col_idx + 1}" + + r = self.station_resources.get(resource_name) + if r: + status_channel = status_row.get(chl_id, {}) + channel_state = { + "status": status_channel.get("state", "unknown"), + "color": status_channel.get("color", self.STATUS_COLOR["unknown"]), + "voltage": status_channel.get("voltage_V", 0.0), + "current": status_channel.get("current_A", 0.0), + "time": status_channel.get("totaltime_s", 0.0), + } + r.load_state(channel_state) + except (KeyError, IndexError): + continue + + # 第2盘:subdevid 6-10 映射到 P2_A1-P2_E8 (5行8列) + for subdev_id in range(6, 11): # subdevid 6-10 + status_row = subunits.get(subdev_id, {}) + + for chl_id in range(1, 9): # chlid 1-8 + try: + # 计算在5×8网格中的位置 + row_idx = (subdev_id - 6) # 0-4 (subdevid 6->0, 7->1, ..., 10->4) (对应A-E) + col_idx = (chl_id - 1) # 0-7 (对应1-8) + resource_name = f"P2_{self.LETTERS[row_idx]}{col_idx + 1}" + + r = self.station_resources.get(resource_name) + if r: + status_channel = status_row.get(chl_id, {}) + channel_state = { + "status": status_channel.get("state", "unknown"), + "color": status_channel.get("color", self.STATUS_COLOR["unknown"]), + "voltage": status_channel.get("voltage_V", 0.0), + "current": status_channel.get("current_A", 0.0), + "time": status_channel.get("totaltime_s", 0.0), + } + r.load_state(channel_state) + except (KeyError, IndexError): + continue + + @property + def connection_info(self) -> Dict[str, str]: + """获取连接信息""" + return { + "ip": self.ip, + "port": str(self.port), + "devtype": self.devtype, + "timeout": f"{self.timeout}s" + } + + @property + def total_channels(self) -> int: + """获取总通道数""" + return len(self._channels) + + # ======================== + # 设备动作方法(Uni-Lab标准) + # ======================== + + def export_status_json(self, filepath: str = "bts_status.json") -> dict: + """ + 导出当前状态到JSON文件(ROS2动作) + + Args: + filepath: 输出文件路径 + + Returns: + dict: ROS2动作结果格式 {"return_info": str, "success": bool} + """ + try: + grouped_status = self.channel_status + payload = { + "timestamp": time.time(), + "device_info": { + "ip": self.ip, + "port": self.port, + "devtype": self.devtype, + "total_channels": self.total_channels + }, + "data": grouped_status, + "color_mapping": self.STATUS_COLOR + } + + with open(filepath, "w", encoding="utf-8") as f: + json.dump(payload, f, ensure_ascii=False, indent=2) + + success_msg = f"状态数据已成功导出到: {filepath}" + if self._ros_node: + self._ros_node.lab_logger().info(success_msg) + return {"return_info": success_msg, "success": True} + + except Exception as e: + error_msg = f"导出JSON失败: {str(e)}" + if self._ros_node: + self._ros_node.lab_logger().error(error_msg) + return {"return_info": error_msg, "success": False} + + @property + def plate_status(self) -> Dict[str, Any]: + """ + 获取所有盘的状态信息(属性) + + Returns: + 包含所有盘状态信息的字典 + """ + try: + # 确保先更新所有资源的状态数据 + _ = self.channel_status # 这会触发状态更新并调用load_state + + # 手动计算两盘的状态,避免调用需要参数的get_plate_status方法 + plate1_stats = {s: 0 for s in self.STATUS_SET | {"unknown"}} + plate1_active = [] + + for name, resource in self.station_resources_plate1.items(): + state = getattr(resource, '_unilabos_state', {}) + status = state.get('status', 'unknown') + plate1_stats[status] += 1 + + if status != 'unknown': + plate1_active.append({ + 'name': name, + 'status': status, + 'color': state.get('color', self.STATUS_COLOR['unknown']), + 'voltage': state.get('voltage', 0.0), + 'current': state.get('current', 0.0), + }) + + plate2_stats = {s: 0 for s in self.STATUS_SET | {"unknown"}} + plate2_active = [] + + for name, resource in self.station_resources_plate2.items(): + state = getattr(resource, '_unilabos_state', {}) + status = state.get('status', 'unknown') + plate2_stats[status] += 1 + + if status != 'unknown': + plate2_active.append({ + 'name': name, + 'status': status, + 'color': state.get('color', self.STATUS_COLOR['unknown']), + 'voltage': state.get('voltage', 0.0), + 'current': state.get('current', 0.0), + }) + + return { + "plate1": { + 'plate_num': 1, + 'stats': plate1_stats, + 'total_positions': len(self.station_resources_plate1), + 'active_positions': len(plate1_active), + 'resources': plate1_active + }, + "plate2": { + 'plate_num': 2, + 'stats': plate2_stats, + 'total_positions': len(self.station_resources_plate2), + 'active_positions': len(plate2_active), + 'resources': plate2_active + }, + "total_plates": 2 + } + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"获取盘状态失败: {e}") + return { + "plate1": {"error": str(e)}, + "plate2": {"error": str(e)}, + "total_plates": 2 + } + + + + + + # ======================== + # 辅助方法 + # ======================== + + def test_connection(self) -> bool: + """ + 测试TCP连接是否正常 + + Returns: + bool: 连接是否成功 + """ + try: + with socket.create_connection((self.ip, self.port), timeout=5) as sock: + return True + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().debug(f"连接测试失败: {e}") + return False + + def print_status_summary(self) -> None: + """ + 打印通道状态摘要信息(支持2盘电池) + """ + try: + status_data = self.channel_status + if not status_data: + print(" 未获取到状态数据") + return + + print(f" 状态统计:") + total_channels = 0 + + # 从channel_status获取stats字段 + stats = status_data.get("stats", {}) + for state, count in stats.items(): + if isinstance(count, int) and count > 0: + color = self.STATUS_COLOR.get(state, "#000000") + print(f" {state}: {count} 个通道 ({color})") + total_channels += count + + print(f" 总计: {total_channels} 个通道") + print(f" 第1盘资源数: {len(self.station_resources_plate1)}") + print(f" 第2盘资源数: {len(self.station_resources_plate2)}") + print(f" 总资源数: {len(self.station_resources)}") + + except Exception as e: + print(f" 获取状态失败: {e}") + + def get_device_summary(self) -> dict: + """ + 获取设备级别的摘要统计(设备动作) + + Returns: + dict: ROS2动作结果格式 {"return_info": str, "success": bool} + """ + try: + # 确保_channels已初始化 + if not hasattr(self, '_channels') or not self._channels: + self._channels = self._build_channel_map() + + summary = {} + for channel in self._channels: + devid = channel.devid + summary[devid] = summary.get(devid, 0) + 1 + + result_info = json.dumps(summary, ensure_ascii=False) + success_msg = f"设备摘要统计: {result_info}" + if self._ros_node: + self._ros_node.lab_logger().info(success_msg) + return {"return_info": result_info, "success": True} + + except Exception as e: + error_msg = f"获取设备摘要失败: {str(e)}" + if self._ros_node: + self._ros_node.lab_logger().error(error_msg) + return {"return_info": error_msg, "success": False} + + def test_connection_action(self) -> dict: + """ + 测试TCP连接(设备动作) + + Returns: + dict: ROS2动作结果格式 {"return_info": str, "success": bool} + """ + try: + is_connected = self.test_connection() + if is_connected: + success_msg = f"TCP连接测试成功: {self.ip}:{self.port}" + if self._ros_node: + self._ros_node.lab_logger().info(success_msg) + return {"return_info": success_msg, "success": True} + else: + error_msg = f"TCP连接测试失败: {self.ip}:{self.port}" + if self._ros_node: + self._ros_node.lab_logger().warning(error_msg) + return {"return_info": error_msg, "success": False} + + except Exception as e: + error_msg = f"连接测试异常: {str(e)}" + if self._ros_node: + self._ros_node.lab_logger().error(error_msg) + return {"return_info": error_msg, "success": False} + + def print_status_summary_action(self) -> dict: + """ + 打印状态摘要(设备动作) + + Returns: + dict: ROS2动作结果格式 {"return_info": str, "success": bool} + """ + try: + self.print_status_summary() + success_msg = "状态摘要已打印到控制台" + if self._ros_node: + self._ros_node.lab_logger().info(success_msg) + return {"return_info": success_msg, "success": True} + + except Exception as e: + error_msg = f"打印状态摘要失败: {str(e)}" + if self._ros_node: + self._ros_node.lab_logger().error(error_msg) + return {"return_info": error_msg, "success": False} + + def query_plate_action(self, plate_id: str = "P1") -> dict: + """ + 查询指定盘的详细信息(设备动作) + + Args: + plate_id: 盘号标识,如"P1"或"P2" + + Returns: + dict: ROS2动作结果格式,包含指定盘的详细通道信息 + """ + try: + # 解析盘号 + if plate_id.upper() == "P1": + plate_num = 1 + elif plate_id.upper() == "P2": + plate_num = 2 + else: + error_msg = f"无效的盘号: {plate_id},仅支持P1或P2" + if self._ros_node: + self._ros_node.lab_logger().warning(error_msg) + return {"return_info": error_msg, "success": False} + + # 获取指定盘的详细信息 + plate_detail = self._get_plate_detail_info(plate_num) + + success_msg = f"成功获取{plate_id}盘详细信息,包含{len(plate_detail['channels'])}个通道" + if self._ros_node: + self._ros_node.lab_logger().info(success_msg) + + return { + "return_info": success_msg, + "success": True, + "plate_data": plate_detail + } + + except Exception as e: + error_msg = f"查询盘{plate_id}详细信息失败: {str(e)}" + if self._ros_node: + self._ros_node.lab_logger().error(error_msg) + return {"return_info": error_msg, "success": False} + + def _get_plate_detail_info(self, plate_num: int) -> dict: + """ + 获取指定盘的详细信息,包含设备ID、子设备ID、通道ID映射 + + Args: + plate_num: 盘号 (1 或 2) + + Returns: + dict: 包含详细通道信息的字典 + """ + # 获取最新的通道状态数据 + channel_status_data = self.channel_status + subunits = channel_status_data.get('subunits', {}) + + if plate_num == 1: + devid = 1 + subdevid_range = range(1, 6) # 子设备ID 1-5 + elif plate_num == 2: + devid = 1 + subdevid_range = range(6, 11) # 子设备ID 6-10 + else: + raise ValueError("盘号必须是1或2") + + channels = [] + + # 直接从subunits数据构建通道信息,而不依赖资源状态 + for subdev_id in subdevid_range: + status_row = subunits.get(subdev_id, {}) + + for chl_id in range(1, 9): # chlid 1-8 + try: + # 计算在5×8网格中的位置 + if plate_num == 1: + row_idx = (subdev_id - 1) # 0-4 (对应A-E) + else: # plate_num == 2 + row_idx = (subdev_id - 6) # 0-4 (subdevid 6->0, 7->1, ..., 10->4) (对应A-E) + + col_idx = (chl_id - 1) # 0-7 (对应1-8) + position = f"{self.LETTERS[row_idx]}{col_idx + 1}" + name = f"P{plate_num}_{position}" + + # 从subunits直接获取通道状态数据 + status_channel = status_row.get(chl_id, {}) + + # 提取metrics数据(如果存在) + metrics = status_channel.get('metrics', {}) + + channel_info = { + 'name': name, + 'devid': devid, + 'subdevid': subdev_id, + 'chlid': chl_id, + 'position': position, + 'status': status_channel.get('state', 'unknown'), + 'color': status_channel.get('color', self.STATUS_COLOR['unknown']), + 'voltage': metrics.get('voltage_V', 0.0), + 'current': metrics.get('current_A', 0.0), + 'time': metrics.get('totaltime_s', 0.0) + } + + channels.append(channel_info) + + except (ValueError, IndexError, KeyError): + # 如果解析失败,跳过该通道 + continue + + # 按位置排序(先按行,再按列) + channels.sort(key=lambda x: (x['subdevid'], x['chlid'])) + + # 统计状态 + stats = {s: 0 for s in self.STATUS_SET | {"unknown"}} + for channel in channels: + stats[channel['status']] += 1 + + return { + 'plate_id': f"P{plate_num}", + 'plate_num': plate_num, + 'devid': devid, + 'subdevid_range': list(subdevid_range), + 'total_channels': len(channels), + 'stats': stats, + 'channels': channels + } + + # ======================== + # TCP通信和协议处理 + # ======================== + + def _build_channel_map(self) -> List['ChannelKey']: + """构建全量通道映射(720个通道)""" + channels = [] + + # devid 1-7: subdevid 1-10, chlid 1-8 + for devid in range(1, 8): + for sub in range(1, 11): + for ch in range(1, 9): + channels.append(ChannelKey(devid, sub, ch)) + + # devid 8: subdevid 11-20, chlid 1-8 + for sub in range(11, 21): + for ch in range(1, 9): + channels.append(ChannelKey(8, sub, ch)) + + # devid 86: subdevid 1-10, chlid 1-8 + for sub in range(1, 11): + for ch in range(1, 9): + channels.append(ChannelKey(86, sub, ch)) + + return channels + + def _query_all_channels(self) -> Dict['ChannelKey', dict]: + """执行TCP查询获取所有通道状态""" + try: + req_xml = self._build_inquire_xml() + + with socket.create_connection((self.ip, self.port), timeout=self.timeout) as sock: + sock.settimeout(self.timeout) + sock.sendall(req_xml) + response = self._recv_until(sock) + + return self._parse_inquire_resp(response) + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"查询通道状态失败: {e}") + else: + print(f"查询通道状态失败: {e}") + return {} + + def _build_inquire_xml(self) -> bytes: + """构造inquire请求XML""" + lines = [ + '', + '', + 'inquire', + f'' + ] + + for c in self._channels: + lines.append( + f'true' + ) + + lines.extend(['', '']) + xml_text = "\n".join(lines) + return xml_text.encode("utf-8") + self.REQ_END + + def _recv_until(self, sock: socket.socket, end_token: bytes = None, + alt_close_tag: bytes = b"") -> bytes: + """接收TCP响应数据""" + if end_token is None: + end_token = self.REQ_END + + buf = bytearray() + while True: + chunk = sock.recv(8192) + if not chunk: + break + buf.extend(chunk) + if end_token in buf: + cut = buf.rfind(end_token) + return bytes(buf[:cut]) + if alt_close_tag in buf: + cut = buf.rfind(alt_close_tag) + len(alt_close_tag) + return bytes(buf[:cut]) + return bytes(buf) + + def _parse_inquire_resp(self, xml_bytes: bytes) -> Dict['ChannelKey', dict]: + """解析inquire_resp响应XML""" + mapping = {} + + try: + xml_text = xml_bytes.decode("utf-8", errors="ignore").strip() + if not xml_text: + return mapping + + root = ET.fromstring(xml_text) + cmd = root.findtext("cmd", default="").strip() + + if cmd != "inquire_resp": + return mapping + + list_node = root.find("list") + if list_node is None: + return mapping + + for node in list_node.findall("inquire"): + # 解析 dev="27-1-1-1-0" + dev = node.get("dev", "") + parts = dev.split("-") + # 容错:至少需要 5 段 + if len(parts) < 5: + continue + try: + devtype = int(parts[0]) # 未使用,但解析以校验正确性 + devid = int(parts[1]) + subdevid = int(parts[2]) + chlid = int(parts[3]) + aux = int(parts[4]) + except ValueError: + continue + + key = ChannelKey(devid, subdevid, chlid) + + # 提取属性,带类型转换与缺省值 + def fget(name: str, cast, default): + v = node.get(name) + if v is None or v == "": + return default + try: + return cast(v) + except Exception: + return default + + workstatus = (node.get("workstatus", "") or "").lower() + if workstatus not in self.STATUS_SET: + workstatus = "unknown" + + current = fget("current", float, 0.0) + voltage = fget("voltage", float, 0.0) + capacity = fget("capacity", float, 0.0) + energy = fget("energy", float, 0.0) + totaltime = fget("totaltime", float, 0.0) + relativetime = fget("relativetime", float, 0.0) + open_close = fget("open_or_close", int, 0) + cycle_id = fget("cycle_id", int, 0) + step_id = fget("step_id", int, 0) + step_type = node.get("step_type", "") or "" + log_code = node.get("log_code", "") or "" + barcode = node.get("barcode") + + mapping[key] = { + "state": workstatus, + "color": self.STATUS_COLOR.get(workstatus, self.STATUS_COLOR["unknown"]), + "current_A": current, + "voltage_V": voltage, + "capacity_Ah": capacity, + "energy_Wh": energy, + "totaltime_s": totaltime, + "relativetime_s": relativetime, + "open_or_close": open_close, + "step_type": step_type, + "cycle_id": cycle_id, + "step_id": step_id, + "log_code": log_code, + **({"barcode": barcode} if barcode is not None else {}), + } + + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"解析XML响应失败: {e}") + else: + print(f"解析XML响应失败: {e}") + + return mapping + + def _group_by_devid(self, status_map: Dict['ChannelKey', dict]) -> Dict[int, Dict]: + """按设备ID分组状态数据""" + result = {} + + for key, val in status_map.items(): + if key.devid not in result: + result[key.devid] = { + "stats": {s: 0 for s in self.STATUS_SET | {"unknown"}}, + "subunits": {} + } + + dev = result[key.devid] + state = val.get("state", "unknown") + dev["stats"][state] = dev["stats"].get(state, 0) + 1 + + subunits = dev["subunits"] + if key.subdevid not in subunits: + subunits[key.subdevid] = {} + + subunits[key.subdevid][key.chlid] = { + "state": state, + "color": val.get("color", self.STATUS_COLOR["unknown"]), + "open_or_close": val.get("open_or_close", 0), + "metrics": { + "voltage_V": val.get("voltage_V", 0.0), + "current_A": val.get("current_A", 0.0), + "capacity_Ah": val.get("capacity_Ah", 0.0), + "energy_Wh": val.get("energy_Wh", 0.0), + "totaltime_s": val.get("totaltime_s", 0.0), + "relativetime_s": val.get("relativetime_s", 0.0) + }, + "meta": { + "step_type": val.get("step_type", ""), + "cycle_id": val.get("cycle_id", 0), + "step_id": val.get("step_id", 0), + "log_code": val.get("log_code", "") + } + } + + return result + + +# ======================== +# 示例和测试代码 +# ======================== +def main(): + """测试和演示设备类的使用(支持2盘80颗电池)""" + print("=== 新威电池测试系统设备类演示(2盘80颗电池) ===") + + # 创建设备实例 + bts = NewareBatteryTestSystem() + + # 创建一个模拟的ROS节点用于初始化 + class MockRosNode: + def lab_logger(self): + import logging + return logging.getLogger(__name__) + + def update_resource(self, *args, **kwargs): + pass # 空实现,避免ROS调用错误 + + # 调用post_init进行正确的初始化 + mock_ros_node = MockRosNode() + bts.post_init(mock_ros_node) + + # 测试连接 + print(f"\n1. 连接测试:") + print(f" 连接信息: {bts.connection_info}") + if bts.test_connection(): + print(" ✓ TCP连接正常") + else: + print(" ✗ TCP连接失败") + return + + # 获取设备摘要 + print(f"\n2. 设备摘要:") + print(f" 总通道数: {bts.total_channels}") + summary_result = bts.get_device_summary() + if summary_result["success"]: + # 直接解析return_info,因为它就是JSON字符串 + summary = json.loads(summary_result["return_info"]) + for devid, count in summary.items(): + print(f" 设备ID {devid}: {count} 个通道") + else: + print(f" 获取设备摘要失败: {summary_result['return_info']}") + + # 显示物料管理系统信息 + print(f"\n3. 物料管理系统:") + print(f" 第1盘资源数: {len(bts.station_resources_plate1)}") + print(f" 第2盘资源数: {len(bts.station_resources_plate2)}") + print(f" 总资源数: {len(bts.station_resources)}") + + # 获取实时状态 + print(f"\n4. 获取通道状态:") + try: + bts.print_status_summary() + except Exception as e: + print(f" 获取状态失败: {e}") + + # 分别获取两盘的状态 + print(f"\n5. 分盘状态统计:") + try: + plate_status_data = bts.plate_status + for plate_num in [1, 2]: + plate_key = f"plate{plate_num}" # 修正键名格式:plate1, plate2 + if plate_key in plate_status_data: + plate_info = plate_status_data[plate_key] + print(f" 第{plate_num}盘:") + print(f" 总位置数: {plate_info['total_positions']}") + print(f" 活跃位置数: {plate_info['active_positions']}") + for state, count in plate_info['stats'].items(): + if count > 0: + print(f" {state}: {count} 个位置") + else: + print(f" 第{plate_num}盘: 无数据") + except Exception as e: + print(f" 获取分盘状态失败: {e}") + + # 导出JSON + print(f"\n6. 导出状态数据:") + result = bts.export_status_json("demo_2plate_status.json") + if result["success"]: + print(" ✓ 状态数据已导出到 demo_2plate_status.json") + else: + print(" ✗ 导出失败") + + +if __name__ == "__main__": + main() diff --git a/unilabos/devices/workstation/bioyond_studio/__init__.py b/unilabos/devices/workstation/bioyond_studio/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py new file mode 100644 index 00000000..f545a2ec --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py @@ -0,0 +1,1058 @@ +# bioyond_rpc.py +""" +BioyondV1RPC类定义 - 包含所有RPC接口和业务逻辑 +""" + +from enum import Enum +from datetime import datetime, timezone +from unilabos.device_comms.rpc import BaseRequest +from typing import Optional, List, Dict, Any +import json +from unilabos.devices.workstation.bioyond_studio.config import WORKFLOW_TO_SECTION_MAP, WORKFLOW_STEP_IDS, LOCATION_MAPPING + + +class SimpleLogger: + """简单的日志记录器""" + def info(self, msg): print(f"[INFO] {msg}") + def error(self, msg): print(f"[ERROR] {msg}") + def debug(self, msg): print(f"[DEBUG] {msg}") + def warning(self, msg): print(f"[WARNING] {msg}") + def critical(self, msg): print(f"[CRITICAL] {msg}") + + +class MachineState(Enum): + INITIAL = 0 + STOPPED = 1 + RUNNING = 2 + PAUSED = 3 + ERROR_PAUSED = 4 + ERROR_STOPPED = 5 + + +class MaterialType(Enum): + Consumables = 0 + Sample = 1 + Reagent = 2 + Product = 3 + + +class BioyondV1RPC(BaseRequest): + def __init__(self, config): + super().__init__() + print("开始初始化") + self.config = config + self.api_key = config["api_key"] + self.host = config["api_host"] + self._logger = SimpleLogger() + self.is_running = False + self.workflow_mappings = {} + self.workflow_sequence = [] + self.pending_task_params = [] + self.material_cache = {} + self._load_material_cache() + + if "workflow_mappings" in config: + self._set_workflow_mappings(config["workflow_mappings"]) + + def _set_workflow_mappings(self, mappings: Dict[str, str]): + self.workflow_mappings = mappings + print(f"设置工作流映射配置: {mappings}") + + def _get_workflow(self, web_workflow_name: str) -> str: + if web_workflow_name not in self.workflow_mappings: + print(f"未找到工作流映射配置: {web_workflow_name}") + return "" + workflow_id = self.workflow_mappings[web_workflow_name] + print(f"获取工作流: {web_workflow_name} -> {workflow_id}") + return workflow_id + + def process_web_workflows(self, json_str: str) -> Dict[str, str]: + try: + data = json.loads(json_str) + web_workflow_list = data.get("web_workflow_list", []) + except json.JSONDecodeError: + print(f"无效的JSON字符串: {json_str}") + return {} + + result = {} + self.workflow_sequence = [] + + for web_name in web_workflow_list: + workflow_id = self._get_workflow(web_name) + if workflow_id: + result[web_name] = workflow_id + self.workflow_sequence.append(workflow_id) + else: + print(f"无法获取工作流ID: {web_name}") + + print(f"工作流执行顺序: {self.workflow_sequence}") + return result + + def get_workflow_sequence(self) -> List[str]: + id_to_name = {workflow_id: name for name, workflow_id in self.workflow_mappings.items()} + workflow_names = [] + for workflow_id in self.workflow_sequence: + workflow_names.append(id_to_name.get(workflow_id, workflow_id)) + return workflow_names + + def append_to_workflow_sequence(self, json_str: str) -> bool: + try: + data = json.loads(json_str) + web_workflow_name = data.get("web_workflow_name", "") + except: + return False + + workflow_id = self._get_workflow(web_workflow_name) + if workflow_id: + self.workflow_sequence.append(workflow_id) + print(f"添加工作流到执行顺序: {web_workflow_name} -> {workflow_id}") + + def set_workflow_sequence(self, json_str: str) -> List[str]: + try: + data = json.loads(json_str) + web_workflow_names = data.get("web_workflow_names", []) + except: + return [] + + sequence = [] + for web_name in web_workflow_names: + workflow_id = self._get_workflow(web_name) + if workflow_id: + sequence.append(workflow_id) + + self.workflow_sequence = sequence + print(f"设置工作流执行顺序: {self.workflow_sequence}") + return self.workflow_sequence.copy() + + def get_all_workflows(self) -> Dict[str, str]: + return self.workflow_mappings.copy() + + def clear_workflows(self): + self.workflow_sequence = [] + print("清空工作流执行顺序") + + def get_current_time_iso8601(self) -> str: + current_time = datetime.now(timezone.utc).isoformat(timespec='milliseconds') + return current_time.replace("+00:00", "Z") + + # 物料查询接口 + def stock_material(self, json_str: str) -> list: + try: + params = json.loads(json_str) + except: + return [] + + response = self.post( + url=f'{self.host}/api/lims/storage/stock-material', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response or response['code'] != 1: + return [] + return response.get("data", []) + + # 工作流列表查询 + def query_workflow(self, json_str: str) -> dict: + try: + params = json.loads(json_str) + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/workflow/work-flow-list', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 工作流步骤查询接口 + def workflow_step_query(self, json_str: str) -> dict: + try: + data = json.loads(json_str) + workflow_id = data.get("workflow_id", "") + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/workflow/sub-workflow-step-parameters', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": workflow_id, + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 任务推送接口 + def create_order(self, json_str: str) -> dict: + try: + params = json.loads(json_str) + except Exception as e: + result = str({"success": False, "error": f"create_order:处理JSON时出错: {str(e)}", "method": "create_order"}) + return result + + print('===============', json.dumps(params)) + + request_params = { + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + } + + response = self.post( + url=f'{self.host}/api/lims/order/order', + params=request_params) + + if response['code'] != 1: + print(f"create order error: {response.get('message')}") + + print(f"create order data: {response['data']}") + result = str(response.get("data", {})) + return result + + # 查询任务列表 + def order_query(self, json_str: str) -> dict: + try: + params = json.loads(json_str) + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/order/order-list', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 任务明细查询 + def order_report(self, json_str: str) -> dict: + try: + data = json.loads(json_str) + order_id = data.get("order_id", "") + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/order/order-report', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": order_id, + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 任务取出接口 + def order_takeout(self, json_str: str) -> int: + try: + data = json.loads(json_str) + params = { + "orderId": data.get("order_id", ""), + "preintakeId": data.get("preintake_id", "") + } + except: + return 0 + + response = self.post( + url=f'{self.host}/api/lims/order/order-takeout', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params, + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 设备列表查询 + def device_list(self, json_str: str = "") -> list: + device_no = None + if json_str: + try: + data = json.loads(json_str) + device_no = data.get("device_no", None) + except: + pass + + url = f'{self.host}/api/lims/device/device-list' + if device_no: + url += f'/{device_no}' + + response = self.post( + url=url, + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return [] + return response.get("data", []) + + # 设备操作 + def device_operation(self, json_str: str) -> int: + try: + data = json.loads(json_str) + params = { + "deviceNo": data.get("device_no", ""), + "operationType": data.get("operation_type", 0), + "operationParams": data.get("operation_params", {}) + } + except: + return 0 + + response = self.post( + url=f'{self.host}/api/lims/device/device-operation', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params, + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 调度器状态查询 + def scheduler_status(self) -> dict: + response = self.post( + url=f'{self.host}/api/lims/scheduler/scheduler-status', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 调度器启动 + def scheduler_start(self) -> int: + response = self.post( + url=f'{self.host}/api/lims/scheduler/start', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 调度器暂停 + def scheduler_pause(self) -> int: + response = self.post( + url=f'{self.host}/api/lims/scheduler/scheduler-pause', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 调度器继续 + def scheduler_continue(self) -> int: + response = self.post( + url=f'{self.host}/api/lims/scheduler/scheduler-continue', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 调度器停止 + def scheduler_stop(self) -> int: + response = self.post( + url=f'{self.host}/api/lims/scheduler/scheduler-stop', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 调度器重置 + def scheduler_reset(self) -> int: + response = self.post( + url=f'{self.host}/api/lims/scheduler/scheduler-reset', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 取消任务 + def cancel_order(self, json_str: str) -> bool: + try: + data = json.loads(json_str) + order_id = data.get("order_id", "") + except: + return False + + response = self.post( + url=f'{self.host}/api/lims/order/cancel-order', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": order_id, + }) + + if not response or response['code'] != 1: + return False + return True + + # 获取可拼接工作流 + def query_split_workflow(self) -> list: + response = self.post( + url=f'{self.host}/api/lims/workflow/split-workflow-list', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return [] + return str(response.get("data", {})) + + # 合并工作流 + def merge_workflow(self, json_str: str) -> dict: + try: + data = json.loads(json_str) + params = { + "name": data.get("name", ""), + "workflowIds": data.get("workflow_ids", []) + } + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/workflow/merge-workflow', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params, + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 合并当前工作流序列 + def merge_sequence_workflow(self, json_str: str) -> dict: + try: + data = json.loads(json_str) + name = data.get("name", "合并工作流") + except: + return {} + + if not self.workflow_sequence: + print("工作流序列为空,无法合并") + return {} + + params = { + "name": name, + "workflowIds": self.workflow_sequence + } + + response = self.post( + url=f'{self.host}/api/lims/workflow/merge-workflow', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params, + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 发布任务 + def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict: + web_workflow_list = self.get_workflow_sequence() + workflow_name = workflow_name + + pending_params_backup = self.pending_task_params.copy() + print(f"保存pending_task_params副本,共{len(pending_params_backup)}个参数") + + # 1. 处理网页工作流列表 + print(f"处理网页工作流列表: {web_workflow_list}") + web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list}) + workflows_result = self.process_web_workflows(web_workflow_json) + + if not workflows_result: + error_msg = "处理网页工作流列表失败" + print(error_msg) + result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "process_web_workflows"}) + return result + + # 2. 合并工作流序列 + print(f"合并工作流序列,名称: {workflow_name}") + merge_json = json.dumps({"name": workflow_name}) + merged_workflow = self.merge_sequence_workflow(merge_json) + print(f"合并工作流序列结果: {merged_workflow}") + + if not merged_workflow: + error_msg = "合并工作流序列失败" + print(error_msg) + result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "merge_sequence_workflow"}) + return result + + # 3. 合并所有参数并创建任务 + workflow_name = merged_workflow.get("name", "") + workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "") + print(f"使用工作流创建任务: {workflow_name} (ID: {workflow_id})") + + workflow_query_json = json.dumps({"workflow_id": workflow_id}) + workflow_params_structure = self.workflow_step_query(workflow_query_json) + + self.pending_task_params = pending_params_backup + print(f"恢复pending_task_params,共{len(self.pending_task_params)}个参数") + + param_values = self.generate_task_param_values(workflow_params_structure) + + task_params = [{ + "orderCode": f"BSO{self.get_current_time_iso8601().replace('-', '').replace('T', '').replace(':', '').replace('.', '')[:14]}", + "orderName": f"实验-{self.get_current_time_iso8601()[:10].replace('-', '')}", + "workFlowId": workflow_id, + "borderNumber": 1, + "paramValues": param_values, + "extendProperties": "" + }] + + task_json = json.dumps(task_params) + print(f"创建任务参数: {type(task_json)}") + result = self.create_order(task_json) + + if not result: + error_msg = "创建任务失败" + print(error_msg) + result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "create_order"}) + return result + + print(f"任务创建成功: {result}") + self.pending_task_params.clear() + print("已清空pending_task_params") + + return { + "success": True, + "workflow": {"name": workflow_name, "id": workflow_id}, + "task": result, + "method": "process_and_execute_workflow" + } + + # 生成任务参数 + def generate_task_param_values(self, workflow_params_structure): + if not workflow_params_structure: + print("workflow_params_structure为空") + return {} + + data = workflow_params_structure + + # 从pending_task_params中提取实际参数值,按DisplaySectionName和Key组织 + pending_params_by_section = {} + print(f"开始处理pending_task_params,共{len(self.pending_task_params)}个任务参数组") + + # 获取工作流执行顺序,用于按顺序匹配参数 + workflow_sequence = self.get_workflow_sequence() + print(f"工作流执行顺序: {workflow_sequence}") + + workflow_index = 0 + + for i, task_param in enumerate(self.pending_task_params): + if 'param_values' in task_param: + print(f"处理第{i+1}个任务参数组,包含{len(task_param['param_values'])}个步骤") + + if workflow_index < len(workflow_sequence): + current_workflow = workflow_sequence[workflow_index] + section_name = WORKFLOW_TO_SECTION_MAP.get(current_workflow) + print(f" 匹配到工作流: {current_workflow} -> {section_name}") + workflow_index += 1 + else: + print(f" 警告: 参数组{i+1}超出了工作流序列范围") + continue + + if not section_name: + print(f" 警告: 工作流{current_workflow}没有对应的DisplaySectionName") + continue + + if section_name not in pending_params_by_section: + pending_params_by_section[section_name] = {} + + for step_id, param_list in task_param['param_values'].items(): + print(f" 步骤ID: {step_id},参数数量: {len(param_list)}") + + for param_item in param_list: + key = param_item.get('Key', '') + value = param_item.get('Value', '') + m = param_item.get('m', 0) + n = param_item.get('n', 0) + print(f" 参数: {key} = {value} (m={m}, n={n}) -> 分组到{section_name}") + + param_key = f"{section_name}.{key}" + if param_key not in pending_params_by_section[section_name]: + pending_params_by_section[section_name][param_key] = [] + + pending_params_by_section[section_name][param_key].append({ + 'value': value, + 'm': m, + 'n': n + }) + + print(f"pending_params_by_section构建完成,包含{len(pending_params_by_section)}个分组") + + # 收集所有参数,过滤TaskDisplayable为0的项 + filtered_params = [] + + for step_id, step_info in data.items(): + if isinstance(step_info, list): + for step_item in step_info: + param_list = step_item.get("parameterList", []) + for param in param_list: + if param.get("TaskDisplayable") == 0: + continue + + param_with_step = param.copy() + param_with_step['step_id'] = step_id + param_with_step['step_name'] = step_item.get("name", "") + param_with_step['step_m'] = step_item.get("m", 0) + param_with_step['step_n'] = step_item.get("n", 0) + filtered_params.append(param_with_step) + + # 按DisplaySectionIndex排序 + filtered_params.sort(key=lambda x: x.get('DisplaySectionIndex', 0)) + + # 生成参数映射 + param_mapping = {} + step_params = {} + for param in filtered_params: + step_id = param['step_id'] + if step_id not in step_params: + step_params[step_id] = [] + step_params[step_id].append(param) + + # 为每个步骤生成参数 + for step_id, params in step_params.items(): + param_list = [] + for param in params: + key = param.get('Key', '') + display_section_index = param.get('DisplaySectionIndex', 0) + step_m = param.get('step_m', 0) + step_n = param.get('step_n', 0) + + section_name = param.get('DisplaySectionName', '') + param_key = f"{section_name}.{key}" + + if section_name in pending_params_by_section and param_key in pending_params_by_section[section_name]: + pending_param_list = pending_params_by_section[section_name][param_key] + if pending_param_list: + pending_param = pending_param_list[0] + value = pending_param['value'] + m = step_m + n = step_n + print(f" 匹配成功: {section_name}.{key} = {value} (m={m}, n={n})") + pending_param_list.pop(0) + else: + value = "1" + m = step_m + n = step_n + print(f" 匹配失败: {section_name}.{key},参数列表为空,使用默认值 = {value}") + else: + value = "1" + m = display_section_index + n = step_n + print(f" 匹配失败: {section_name}.{key},使用默认值 = {value} (m={m}, n={n})") + + param_item = { + "m": m, + "n": n, + "key": key, + "value": str(value).strip() + } + param_list.append(param_item) + + if param_list: + param_mapping[step_id] = param_list + + print(f"生成任务参数值,包含 {len(param_mapping)} 个步骤") + return param_mapping + + # 工作流方法 + def reactor_taken_out(self): + """反应器取出""" + self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_out"}') + reactor_taken_out_params = {"param_values": {}} + self.pending_task_params.append(reactor_taken_out_params) + print(f"成功添加反应器取出工作流") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + def reactor_taken_in(self, assign_material_name: str, cutoff: str = "900000", temperature: float = -10.00): + """反应器放入""" + self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_in"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + step_id = WORKFLOW_STEP_IDS["reactor_taken_in"]["config"] + reactor_taken_in_params = { + "param_values": { + step_id: [ + {"m": 0, "n": 3, "Key": "cutoff", "Value": cutoff}, + {"m": 0, "n": 3, "Key": "temperature", "Value": f"{temperature:.2f}"}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} + ] + } + } + + self.pending_task_params.append(reactor_taken_in_params) + print(f"成功添加反应器放入参数: material={assign_material_name}->ID:{material_id}, cutoff={cutoff}, temp={temperature:.2f}") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + def solid_feeding_vials(self, material_id: str, time: str = "0", torque_variation: str = "1", + assign_material_name: str = None, temperature: float = 25.00): + """固体进料小瓶""" + self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}') + material_id_m = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + feeding_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"] + observe_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"] + + solid_feeding_vials_params = { + "param_values": { + feeding_id: [ + {"m": 0, "n": 3, "Key": "materialId", "Value": material_id}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id_m} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(solid_feeding_vials_params) + print(f"成功添加固体进料小瓶参数: material_id={material_id}, time={time}min, temp={temperature:.2f}°C") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + def liquid_feeding_vials_non_titration(self, volumeFormula: str, assign_material_name: str, + titration_type: str = "1", time: str = "0", + torque_variation: str = "1", temperature: float = 25.00): + """液体进料小瓶(非滴定)""" + self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"] + observe_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"] + + params = { + "param_values": { + liquid_id: [ + {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volumeFormula}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id}, + {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(params) + print(f"成功添加液体进料小瓶(非滴定)参数: volume={volumeFormula}μL, material={assign_material_name}->ID:{material_id}") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + def liquid_feeding_solvents(self, assign_material_name: str, volume: str, titration_type: str = "1", + time: str = "360", torque_variation: str = "2", temperature: float = 25.00): + """液体进料溶剂""" + self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"] + observe_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"] + + params = { + "param_values": { + liquid_id: [ + {"m": 0, "n": 1, "Key": "titrationType", "Value": titration_type}, + {"m": 0, "n": 1, "Key": "volume", "Value": volume}, + {"m": 0, "n": 1, "Key": "assignMaterialName", "Value": material_id} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(params) + print(f"成功添加液体进料溶剂参数: material={assign_material_name}->ID:{material_id}, volume={volume}μL") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + def liquid_feeding_titration(self, volume_formula: str, assign_material_name: str, titration_type: str = "1", + time: str = "90", torque_variation: int = 2, temperature: float = 25.00): + """液体进料(滴定)""" + self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"] + observe_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"] + + params = { + "param_values": { + liquid_id: [ + {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volume_formula}, + {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(params) + print(f"成功添加液体进料滴定参数: volume={volume_formula}μL, material={assign_material_name}->ID:{material_id}") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + def liquid_feeding_beaker(self, volume: str = "35000", assign_material_name: str = "BAPP", + time: str = "0", torque_variation: str = "1", titrationType: str = "1", + temperature: float = 25.00): + """液体进料烧杯""" + self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["liquid"] + observe_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["observe"] + + params = { + "param_values": { + liquid_id: [ + {"m": 0, "n": 2, "Key": "volume", "Value": volume}, + {"m": 0, "n": 2, "Key": "assignMaterialName", "Value": material_id}, + {"m": 0, "n": 2, "Key": "titrationType", "Value": titrationType} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(params) + print(f"成功添加液体进料烧杯参数: volume={volume}μL, material={assign_material_name}->ID:{material_id}") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + # 辅助方法 + def _load_material_cache(self): + """预加载材料列表到缓存中""" + try: + print("正在加载材料列表缓存...") + stock_query = '{"typeMode": 2, "includeDetail": true}' + stock_result = self.stock_material(stock_query) + + if isinstance(stock_result, str): + stock_data = json.loads(stock_result) + else: + stock_data = stock_result + + materials = stock_data + for material in materials: + material_name = material.get("name") + material_id = material.get("id") + if material_name and material_id: + self.material_cache[material_name] = material_id + + print(f"材料列表缓存加载完成,共加载 {len(self.material_cache)} 个材料") + + except Exception as e: + print(f"加载材料列表缓存时出错: {e}") + self.material_cache = {} + + def _get_material_id_by_name(self, material_name_or_id: str) -> str: + """根据材料名称获取材料ID""" + if len(material_name_or_id) > 20 and '-' in material_name_or_id: + return material_name_or_id + + if material_name_or_id in self.material_cache: + material_id = self.material_cache[material_name_or_id] + print(f"从缓存找到材料: {material_name_or_id} -> ID: {material_id}") + return material_id + + print(f"警告: 未在缓存中找到材料名称 '{material_name_or_id}',将使用原值") + return material_name_or_id + + def refresh_material_cache(self): + """刷新材料列表缓存""" + print("正在刷新材料列表缓存...") + self._load_material_cache() + + def get_available_materials(self): + """获取所有可用的材料名称列表""" + return list(self.material_cache.keys()) + + # 物料管理接口 + def add_material(self, json_str: str) -> dict: + """添加新的物料""" + try: + params = json.loads(json_str) + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/storage/material', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + def query_matial_type_id(self, data) -> list: + """查找物料typeid""" + response = self.post( + url=f'{self.host}/api/lims/storage/material-types', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": data + }) + + if not response or response['code'] != 1: + return [] + return str(response.get("data", {})) + + def query_warehouse_by_material_type(self, type_id: str) -> dict: + """查询物料类型可以入库的库位""" + params = {"typeId": type_id} + + response = self.post( + url=f'{self.host}/api/lims/storage/warehouse-info-by-mat-type-id', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + def material_inbound(self, material_id: str, location_name: str) -> dict: + """指定库位入库一个物料""" + location_id = LOCATION_MAPPING.get(location_name, location_name) + + params = { + "materialId": material_id, + "locationId": location_id + } + + response = self.post( + url=f'{self.host}/api/lims/storage/inbound', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + def delete_material(self, material_id: str) -> dict: + """删除尚未入库的物料""" + response = self.post( + url=f'{self.host}/api/lims/storage/delete-material', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": material_id + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict: + """指定库位出库物料""" + location_id = LOCATION_MAPPING.get(location_name, location_name) + + 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 {} + return response + + def get_logger(self): + return self._logger diff --git a/unilabos/devices/workstation/bioyond_studio/experiment.py b/unilabos/devices/workstation/bioyond_studio/experiment.py new file mode 100644 index 00000000..ae3111b8 --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/experiment.py @@ -0,0 +1,398 @@ +# experiment_workflow.py +""" +实验流程主程序 +""" + +import json +from bioyond_rpc import BioyondV1RPC +from config import API_CONFIG, WORKFLOW_MAPPINGS + + +def run_experiment(): + """运行实验流程""" + + # 初始化Bioyond客户端 + config = { + **API_CONFIG, + "workflow_mappings": WORKFLOW_MAPPINGS + } + + Bioyond = BioyondV1RPC(config) + + print("\n============= 多工作流参数测试(简化接口+材料缓存)=============") + + # 显示可用的材料名称(前20个) + available_materials = Bioyond.get_available_materials() + print(f"可用材料名称(前20个): {available_materials[:20]}") + print(f"总共有 {len(available_materials)} 个材料可用\n") + + # 1. 反应器放入 + print("1. 添加反应器放入工作流,带参数...") + Bioyond.reactor_taken_in( + assign_material_name="BTDA-DD", + cutoff="10000", + temperature="-10" + ) + + # 2. 液体投料-烧杯 (第一个) + print("2. 添加液体投料-烧杯,带参数...") + Bioyond.liquid_feeding_beaker( + volume="34768.7", + assign_material_name="ODA", + time="0", + torque_variation="1", + titrationType="1", + temperature=-10 + ) + + # 3. 液体投料-烧杯 (第二个) + print("3. 添加液体投料-烧杯,带参数...") + Bioyond.liquid_feeding_beaker( + volume="34080.9", + assign_material_name="MPDA", + time="5", + torque_variation="2", + titrationType="1", + temperature=0 + ) + + # 4. 液体投料-小瓶非滴定 + print("4. 添加液体投料-小瓶非滴定,带参数...") + Bioyond.liquid_feeding_vials_non_titration( + volumeFormula="639.5", + assign_material_name="SIDA", + titration_type="1", + time="0", + torque_variation="1", + temperature=-10 + ) + + # 5. 液体投料溶剂 + print("5. 添加液体投料溶剂,带参数...") + Bioyond.liquid_feeding_solvents( + assign_material_name="NMP", + volume="19000", + titration_type="1", + time="5", + torque_variation="2", + temperature=-10 + ) + + # 6-8. 固体进料小瓶 (三个) + print("6. 添加固体进料小瓶,带参数...") + Bioyond.solid_feeding_vials( + material_id="3", + time="180", + torque_variation="2", + assign_material_name="BTDA-1", + temperature=-10.00 + ) + + print("7. 添加固体进料小瓶,带参数...") + Bioyond.solid_feeding_vials( + material_id="3", + time="180", + torque_variation="2", + assign_material_name="BTDA-2", + temperature=25.00 + ) + + print("8. 添加固体进料小瓶,带参数...") + Bioyond.solid_feeding_vials( + material_id="3", + time="480", + torque_variation="2", + assign_material_name="BTDA-3", + temperature=25.00 + ) + + # 液体投料滴定(第一个) + print("9. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="1000", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + # 液体投料滴定(第二个) + print("10. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + # 液体投料滴定(第三个) + print("11. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + print("12. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + print("13. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + print("14. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + + + print("15. 添加液体投料溶剂,带参数...") + Bioyond.liquid_feeding_solvents( + assign_material_name="PGME", + volume="16894.6", + titration_type="1", + time="360", + torque_variation="2", + temperature=25.00 + ) + + # 16. 反应器取出 + print("16. 添加反应器取出工作流...") + Bioyond.reactor_taken_out() + + # 显示当前工作流序列 + sequence = Bioyond.get_workflow_sequence() + print("\n当前工作流执行顺序:") + print(sequence) + + # 执行process_and_execute_workflow,合并工作流并创建任务 + print("\n4. 执行process_and_execute_workflow...") + + result = Bioyond.process_and_execute_workflow( + workflow_name="test3_86", + task_name="实验3_86" + ) + + # 显示执行结果 + print("\n5. 执行结果:") + if isinstance(result, str): + try: + result_dict = json.loads(result) + if result_dict.get("success"): + print("任务创建成功!") + print(f"- 工作流: {result_dict.get('workflow', {}).get('name')}") + print(f"- 工作流ID: {result_dict.get('workflow', {}).get('id')}") + print(f"- 任务结果: {result_dict.get('task')}") + else: + print(f"任务创建失败: {result_dict.get('error')}") + except: + print(f"结果解析失败: {result}") + else: + if result.get("success"): + print("任务创建成功!") + print(f"- 工作流: {result.get('workflow', {}).get('name')}") + print(f"- 工作流ID: {result.get('workflow', {}).get('id')}") + print(f"- 任务结果: {result.get('task')}") + else: + print(f"任务创建失败: {result.get('error')}") + + # 可选:启动调度器 + # Bioyond.scheduler_start() + + return Bioyond + + +def prepare_materials(bioyond): + """准备实验材料(可选)""" + + # 样品板材料数据定义 + material_data_yp_1 = { + "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", + "name": "样品板-1", + "unit": "个", + "quantity": 1, + "details": [ + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-DD-1", + "quantity": 1, + "x": 1, + "y": 1, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "PEPA", + "quantity": 1, + "x": 1, + "y": 2, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-DD-2", + "quantity": 1, + "x": 1, + "y": 3, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-1", + "quantity": 1, + "x": 2, + "y": 1, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "PMDA", + "quantity": 1, + "x": 2, + "y": 2, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-2", + "quantity": 1, + "x": 2, + "y": 3, + "Parameters": "{\"molecular\": 1}" + } + ], + "Parameters": "{}" + } + + material_data_yp_2 = { + "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", + "name": "样品板-2", + "unit": "个", + "quantity": 1, + "details": [ + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-DD", + "quantity": 1, + "x": 1, + "y": 1, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "SIDA", + "quantity": 1, + "x": 1, + "y": 2, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BTDA-1", + "quantity": 1, + "x": 2, + "y": 1, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BTDA-2", + "quantity": 1, + "x": 2, + "y": 2, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BTDA-3", + "quantity": 1, + "x": 2, + "y": 3, + "Parameters": "{\"molecular\": 1}" + } + ], + "Parameters": "{}" + } + + # 烧杯材料数据定义 + beaker_materials = [ + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "PDA-1", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + }, + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "TFDB", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + }, + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "ODA", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + }, + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "MPDA", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + }, + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "PDA-2", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + } + ] + + # 如果需要,可以在这里调用add_material方法添加材料 + # 例如: + # result = bioyond.add_material(json.dumps(material_data_yp_1)) + # print(f"添加材料结果: {result}") + + return { + "sample_plates": [material_data_yp_1, material_data_yp_2], + "beakers": beaker_materials + } + + +if __name__ == "__main__": + # 运行主实验流程 + bioyond_client = run_experiment() + + # 可选:准备材料数据 + # materials = prepare_materials(bioyond_client) + # print(f"\n准备的材料数据: {materials}") diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py new file mode 100644 index 00000000..3685910a --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -0,0 +1,400 @@ +""" +Bioyond工作站实现 +Bioyond Workstation Implementation + +集成Bioyond物料管理的工作站示例 +""" +import traceback +from typing import Dict, Any, List, Optional, Union +import json + +from unilabos.devices.workstation.workstation_base import WorkstationBase, ResourceSynchronizer +from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC +from unilabos.resources.warehouse import WareHouse +from unilabos.utils.log import logger +from unilabos.resources.graphio import resource_bioyond_to_plr + +from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode +from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode + +from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS + + +class BioyondResourceSynchronizer(ResourceSynchronizer): + """Bioyond资源同步器 + + 负责与Bioyond系统进行物料数据的同步 + """ + + def __init__(self, workstation: 'BioyondWorkstation'): + super().__init__(workstation) + self.bioyond_api_client = None + self.sync_interval = 60 # 默认60秒同步一次 + self.last_sync_time = 0 + self.initialize() + + def initialize(self) -> bool: + """初始化Bioyond资源同步器""" + try: + self.bioyond_api_client = self.workstation.hardware_interface + if self.bioyond_api_client is None: + logger.error("Bioyond API客户端未初始化") + return False + + # 设置同步间隔 + self.sync_interval = self.workstation.bioyond_config.get("sync_interval", 600) + + logger.info("Bioyond资源同步器初始化完成") + return True + except Exception as e: + logger.error(f"Bioyond资源同步器初始化失败: {e}") + return False + + def sync_from_external(self) -> bool: + """从Bioyond系统同步物料数据""" + try: + if self.bioyond_api_client is None: + logger.error("Bioyond API客户端未初始化") + return False + + bioyond_data = self.bioyond_api_client.stock_material('{"typeMode": 2, "includeDetail": true}') + if not bioyond_data: + logger.warning("从Bioyond获取的物料数据为空") + return False + + # 转换为UniLab格式 + unilab_resources = resource_bioyond_to_plr(bioyond_data, type_mapping=self.workstation.bioyond_config["material_type_mappings"], deck=self.workstation.deck) + + logger.info(f"从Bioyond同步了 {len(unilab_resources)} 个资源") + return True + except Exception as e: + logger.error(f"从Bioyond同步物料数据失败: {e}") + traceback.print_exc() + return False + + def sync_to_external(self, resource: Any) -> bool: + """将本地物料数据变更同步到Bioyond系统""" + try: + if self.bioyond_api_client is None: + logger.error("Bioyond API客户端未初始化") + return False + + # 调用入库、出库操作 + # bioyond_format_data = self._convert_resource_to_bioyond_format(resource) + # success = await self.bioyond_api_client.update_material(bioyond_format_data) + # + # if success + except: + pass + + def handle_external_change(self, change_info: Dict[str, Any]) -> bool: + """处理Bioyond系统的变更通知""" + try: + # 这里可以实现对Bioyond变更的处理逻辑 + logger.info(f"处理Bioyond变更通知: {change_info}") + + return True + except Exception as e: + logger.error(f"处理Bioyond变更通知失败: {e}") + return False + + +class BioyondWorkstation(WorkstationBase): + """Bioyond工作站 + + 集成Bioyond物料管理的工作站实现 + """ + + def __init__( + self, + bioyond_config: Optional[Dict[str, Any]] = None, + deck: Optional[Any] = None, + *args, + **kwargs, + ): + # 初始化父类 + super().__init__( + # 桌子 + deck=deck, + *args, + **kwargs, + ) + self.deck.warehouses = {} + for resource in self.deck.children: + if isinstance(resource, WareHouse): + self.deck.warehouses[resource.name] = resource + + self._create_communication_module(bioyond_config) + self.resource_synchronizer = BioyondResourceSynchronizer(self) + self.resource_synchronizer.sync_from_external() + + # TODO: self._ros_node里面拿属性 + logger.info(f"Bioyond工作站初始化完成") + + def post_init(self, ros_node: ROS2WorkstationNode): + self._ros_node = ros_node + #self.deck = create_a_coin_cell_deck() + ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + "resources": [self.deck] + }) + + def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None: + """创建Bioyond通信模块""" + self.bioyond_config = config or { + **API_CONFIG, + "workflow_mappings": WORKFLOW_MAPPINGS, + "material_type_mappings": MATERIAL_TYPE_MAPPINGS + } + self.hardware_interface = BioyondV1RPC(self.bioyond_config) + + return None + + def _register_supported_workflows(self): + """注册Bioyond支持的工作流""" + from unilabos.devices.workstation.workstation_base import WorkflowInfo + + # Bioyond物料同步工作流 + self.supported_workflows["bioyond_sync"] = WorkflowInfo( + name="bioyond_sync", + description="从Bioyond系统同步物料", + parameters={ + "sync_type": {"type": "string", "default": "full", "options": ["full", "incremental"]}, + "force_sync": {"type": "boolean", "default": False} + } + ) + + # Bioyond物料更新工作流 + self.supported_workflows["bioyond_update"] = WorkflowInfo( + name="bioyond_update", + description="将本地物料变更同步到Bioyond", + parameters={ + "material_ids": {"type": "list", "default": []}, + "sync_all": {"type": "boolean", "default": True} + } + ) + + logger.info(f"注册了 {len(self.supported_workflows)} 个Bioyond工作流") + + async def execute_bioyond_sync_workflow(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """执行Bioyond同步工作流""" + try: + sync_type = parameters.get("sync_type", "full") + force_sync = parameters.get("force_sync", False) + + logger.info(f"开始执行Bioyond同步工作流: {sync_type}") + + # 获取物料管理模块 + material_manager = self.material_management + + if sync_type == "full": + # 全量同步 + success = await material_manager.sync_from_bioyond() + else: + # 增量同步(这里可以实现增量同步逻辑) + success = await material_manager.sync_from_bioyond() + + if success: + result = { + "status": "success", + "message": f"Bioyond同步完成: {sync_type}", + "synced_resources": len(material_manager.plr_resources) + } + else: + result = { + "status": "failed", + "message": "Bioyond同步失败" + } + + logger.info(f"Bioyond同步工作流执行完成: {result['status']}") + return result + + except Exception as e: + logger.error(f"Bioyond同步工作流执行失败: {e}") + return { + "status": "error", + "message": str(e) + } + + async def execute_bioyond_update_workflow(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """执行Bioyond更新工作流""" + try: + material_ids = parameters.get("material_ids", []) + sync_all = parameters.get("sync_all", True) + + logger.info(f"开始执行Bioyond更新工作流: sync_all={sync_all}") + + # 获取物料管理模块 + material_manager = self.material_management + + if sync_all: + # 同步所有物料 + success_count = 0 + for resource in material_manager.plr_resources.values(): + success = await material_manager.sync_to_bioyond(resource) + if success: + success_count += 1 + else: + # 同步指定物料 + success_count = 0 + for material_id in material_ids: + resource = material_manager.find_material_by_id(material_id) + if resource: + success = await material_manager.sync_to_bioyond(resource) + if success: + success_count += 1 + + result = { + "status": "success", + "message": f"Bioyond更新完成", + "updated_resources": success_count, + "total_resources": len(material_ids) if not sync_all else len(material_manager.plr_resources) + } + + logger.info(f"Bioyond更新工作流执行完成: {result['status']}") + return result + + except Exception as e: + logger.error(f"Bioyond更新工作流执行失败: {e}") + return { + "status": "error", + "message": str(e) + } + + def load_bioyond_data_from_file(self, file_path: str) -> bool: + """从文件加载Bioyond数据(用于测试)""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + bioyond_data = json.load(f) + + # 获取物料管理模块 + material_manager = self.material_management + + # 转换为UniLab格式 + if isinstance(bioyond_data, dict) and "data" in bioyond_data: + unilab_resources = material_manager.resource_bioyond_container_to_ulab(bioyond_data) + else: + unilab_resources = material_manager.resource_bioyond_to_ulab(bioyond_data) + + # 分配到Deck + import asyncio + asyncio.create_task(material_manager._assign_resources_to_deck(unilab_resources)) + + logger.info(f"从文件 {file_path} 加载了 {len(unilab_resources)} 个Bioyond资源") + return True + + except Exception as e: + logger.error(f"从文件加载Bioyond数据失败: {e}") + return False + + +# 使用示例 +def create_bioyond_workstation_example(): + """创建Bioyond工作站示例""" + + # 配置参数 + device_id = "bioyond_workstation_001" + + # 子资源配置 + children = { + "plate_1": { + "name": "plate_1", + "type": "plate", + "position": {"x": 100, "y": 100, "z": 0}, + "config": { + "size_x": 127.76, + "size_y": 85.48, + "size_z": 14.35, + "model": "Generic 96 Well Plate" + } + } + } + + # Bioyond配置 + bioyond_config = { + "base_url": "http://bioyond.example.com/api", + "api_key": "your_api_key_here", + "sync_interval": 60, # 60秒同步一次 + "timeout": 30 + } + + # Deck配置 + deck_config = { + "size_x": 1000.0, + "size_y": 1000.0, + "size_z": 100.0, + "model": "BioyondDeck" + } + + # 创建工作站 + workstation = BioyondWorkstation( + station_resource=deck_config, + bioyond_config=bioyond_config, + deck_config=deck_config, + ) + + return workstation + + +if __name__ == "__main__": + # 创建示例工作站 + #workstation = create_bioyond_workstation_example() + + # 从文件加载测试数据 + #workstation.load_bioyond_data_from_file("bioyond_test_yibin.json") + + # 获取状态 + #status = workstation.get_bioyond_status() + #print("Bioyond工作站状态:", status) + + # 创建测试数据 - 使用resource_bioyond_container_to_ulab函数期望的格式 + + # 读取 bioyond_resources_unilab_output3 copy.json 文件 + from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type + from Bioyond_wuliao import * + from typing import List + from pylabrobot.resources import Resource as PLRResource + import json + from pylabrobot.resources.deck import Deck + from pylabrobot.resources.coordinate import Coordinate + + with open("./bioyond_test_yibin3_unilab_result_corr.json", "r", encoding="utf-8") as f: + bioyond_resources_unilab = json.load(f) + print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源") + ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource]) + print(f"转换结果类型: {type(ulab_resources)}") + print(f"转换结果长度: {len(ulab_resources) if ulab_resources else 0}") + deck = Deck(size_x=2000, + size_y=653.5, + size_z=900) + + Stack0 = Stack(name="Stack0", location=Coordinate(0, 100, 0)) + Stack1 = Stack(name="Stack1", location=Coordinate(100, 100, 0)) + Stack2 = Stack(name="Stack2", location=Coordinate(200, 100, 0)) + Stack3 = Stack(name="Stack3", location=Coordinate(300, 100, 0)) + Stack4 = Stack(name="Stack4", location=Coordinate(400, 100, 0)) + Stack5 = Stack(name="Stack5", location=Coordinate(500, 100, 0)) + + deck.assign_child_resource(Stack1, Stack1.location) + deck.assign_child_resource(Stack2, Stack2.location) + deck.assign_child_resource(Stack3, Stack3.location) + deck.assign_child_resource(Stack4, Stack4.location) + deck.assign_child_resource(Stack5, Stack5.location) + + Stack0.assign_child_resource(ulab_resources[0], Stack0.location) + Stack1.assign_child_resource(ulab_resources[1], Stack1.location) + Stack2.assign_child_resource(ulab_resources[2], Stack2.location) + Stack3.assign_child_resource(ulab_resources[3], Stack3.location) + Stack4.assign_child_resource(ulab_resources[4], Stack4.location) + Stack5.assign_child_resource(ulab_resources[5], Stack5.location) + + from unilabos.resources.graphio import convert_resources_from_type + from unilabos.app.web.client import http_client + + resources = convert_resources_from_type([deck], [PLRResource]) + + + print(resources) + http_client.remote_addr = "https://uni-lab.bohrium.com/api/v1" + #http_client.auth = "9F05593C" + http_client.auth = "ED634D1C" + http_client.resource_add(resources, database_process_later=False) \ No newline at end of file diff --git a/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py b/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py new file mode 100644 index 00000000..ffce04be --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py @@ -0,0 +1,1292 @@ +""" +纽扣电池组装工作站物料类定义 +Button Battery Assembly Station Resource Classes +""" + +from __future__ import annotations + +from collections import OrderedDict +from typing import Any, Dict, List, Optional, TypedDict, Union, cast + +from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources.container import Container +from pylabrobot.resources.deck import Deck +from pylabrobot.resources.itemized_resource import ItemizedResource +from pylabrobot.resources.resource import Resource +from pylabrobot.resources.resource_stack import ResourceStack +from pylabrobot.resources.tip_rack import TipRack, TipSpot +from pylabrobot.resources.trash import Trash +from pylabrobot.resources.utils import create_ordered_items_2d + + +class ElectrodeSheetState(TypedDict): + diameter: float # 直径 (mm) + thickness: float # 厚度 (mm) + mass: float # 质量 (g) + material_type: str # 材料类型(正极、负极、隔膜、弹片、垫片、铝箔等) + info: Optional[str] # 附加信息 + +class ElectrodeSheet(Resource): + """极片类 - 包含正负极片、隔膜、弹片、垫片、铝箔等所有片状材料""" + + def __init__( + self, + name: str = "极片", + size_x=10, + size_y=10, + size_z=10, + category: str = "electrode_sheet", + model: Optional[str] = None, + ): + """初始化极片 + + Args: + name: 极片名称 + category: 类别 + model: 型号 + """ + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category=category, + model=model, + ) + self._unilabos_state: ElectrodeSheetState = ElectrodeSheetState( + diameter=14, + thickness=0.1, + mass=0.5, + material_type="copper", + info=None + ) + + # TODO: 这个还要不要?给self._unilabos_state赋值的? + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + #序列化 + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + +# TODO: 这个应该只能放一个极片 +class MaterialHoleState(TypedDict): + diameter: int + depth: int + max_sheets: int + info: Optional[str] # 附加信息 + +class MaterialHole(Resource): + """料板洞位类""" + children: List[ElectrodeSheet] = [] + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + category: str = "material_hole", + **kwargs + ): + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category=category, + ) + self._unilabos_state: MaterialHoleState = MaterialHoleState( + diameter=20, + depth=10, + max_sheets=1, + info=None + ) + + def get_all_sheet_info(self): + info_list = [] + for sheet in self.children: + info_list.append(sheet._unilabos_state["info"]) + return info_list + + #这个函数函数好像没用,一般不会集中赋值质量 + def set_all_sheet_mass(self): + for sheet in self.children: + sheet._unilabos_state["mass"] = 0.5 # 示例:设置质量为0.5g + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + #移动极片前先取出对象 + def get_sheet_with_name(self, name: str) -> Optional[ElectrodeSheet]: + for sheet in self.children: + if sheet.name == name: + return sheet + return None + + def has_electrode_sheet(self) -> bool: + """检查洞位是否有极片""" + return len(self.children) > 0 + + def assign_child_resource( + self, + resource: ElectrodeSheet, + location: Optional[Coordinate], + reassign: bool = True, + ): + """放置极片""" + # TODO: 这里要改,diameter找不到,加入._unilabos_state后应该没问题 + if resource._unilabos_state["diameter"] > self._unilabos_state["diameter"]: + raise ValueError(f"极片直径 {resource._unilabos_state['diameter']} 超过洞位直径 {self._unilabos_state['diameter']}") + if len(self.children) >= self._unilabos_state["max_sheets"]: + raise ValueError(f"洞位已满,无法放置更多极片") + super().assign_child_resource(resource, location, reassign) + + # 根据children的编号取物料对象。 + def get_electrode_sheet_info(self, index: int) -> ElectrodeSheet: + return self.children[index] + + + +class MaterialPlateState(TypedDict): + hole_spacing_x: float + hole_spacing_y: float + hole_diameter: float + info: Optional[str] # 附加信息 + + + +class MaterialPlate(ItemizedResource[MaterialHole]): + """料板类 - 4x4个洞位,每个洞位放1个极片""" + + children: List[MaterialHole] + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + ordered_items: Optional[Dict[str, MaterialHole]] = None, + 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: 型号 + """ + self._unilabos_state: MaterialPlateState = MaterialPlateState( + hole_spacing_x=24.0, + hole_spacing_y=24.0, + hole_diameter=20.0, + info="", + ) + # 创建4x4的洞位 + # TODO: 这里要改,对应不同形状 + 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, # 居中 + 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, + ) + 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:调多次相加 + holes = create_ordered_items_2d( + klass=MaterialHole, + num_items_x=4, + num_items_y=4, + dx=(self._size_x - 3 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中 + dy=(self._size_y - 3 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中 + dz=self._size_z, + item_dx=self._unilabos_state["hole_spacing_x"], + item_dy=self._unilabos_state["hole_spacing_y"], + size_x = 1, + size_y = 1, + size_z = 1, + ) + for item, original_item in zip(holes.items(), self.children): + original_item.location = item[1].location + + +class PlateSlot(ResourceStack): + """板槽位类 - 1个槽上能堆放8个板,移板只能操作最上方的板""" + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + max_plates: int = 8, + category: str = "plate_slot", + model: Optional[str] = None + ): + """初始化板槽位 + + Args: + name: 槽位名称 + max_plates: 最大板数量 + category: 类别 + """ + super().__init__( + name=name, + direction="z", # Z方向堆叠 + resources=[], + ) + self.max_plates = max_plates + self.category = category + + def can_add_plate(self) -> bool: + """检查是否可以添加板""" + return len(self.children) < self.max_plates + + def add_plate(self, plate: MaterialPlate) -> None: + """添加料板""" + if not self.can_add_plate(): + raise ValueError(f"槽位 {self.name} 已满,无法添加更多板") + self.assign_child_resource(plate) + + def get_top_plate(self) -> MaterialPlate: + """获取最上方的板""" + if len(self.children) == 0: + raise ValueError(f"槽位 {self.name} 为空") + return cast(MaterialPlate, self.get_top_item()) + + def take_top_plate(self) -> MaterialPlate: + """取出最上方的板""" + top_plate = self.get_top_plate() + self.unassign_child_resource(top_plate) + return top_plate + + def can_access_for_picking(self) -> bool: + """检查是否可以进行取料操作(只有最上方的板能进行取料操作)""" + return len(self.children) > 0 + + def serialize(self) -> dict: + return { + **super().serialize(), + "max_plates": self.max_plates, + } + + +class ClipMagazineHole(Container): + """子弹夹洞位类""" + children: List[ElectrodeSheet] = [] + def __init__( + self, + name: str, + diameter: float, + depth: float, + category: str = "clip_magazine_hole", + ): + """初始化子弹夹洞位 + + Args: + name: 洞位名称 + diameter: 洞直径 (mm) + depth: 洞深度 (mm) + category: 类别 + """ + super().__init__( + name=name, + size_x=diameter, + size_y=diameter, + size_z=depth, + category=category, + ) + self.diameter = diameter + self.depth = depth + + def can_add_sheet(self, sheet: ElectrodeSheet) -> bool: + """检查是否可以添加极片 + + 根据洞的深度和极片的厚度来判断是否可以添加极片 + """ + # 检查极片直径是否适合洞的直径 + if sheet._unilabos_state["diameter"] > self.diameter: + return False + + # 计算当前已添加极片的总厚度 + current_thickness = sum(s._unilabos_state["thickness"] for s in self.children) + + # 检查添加新极片后总厚度是否超过洞的深度 + if current_thickness + sheet._unilabos_state["thickness"] > self.depth: + return False + + return True + + + def assign_child_resource( + self, + resource: ElectrodeSheet, + location: Optional[Coordinate] = None, + reassign: bool = True, + ): + """放置极片到洞位中 + + Args: + resource: 要放置的极片 + location: 极片在洞位中的位置(对于洞位,通常为None) + reassign: 是否允许重新分配 + """ + # 检查是否可以添加极片 + if not self.can_add_sheet(resource): + raise ValueError(f"无法向洞位 {self.name} 添加极片:直径或厚度不匹配") + + # 调用父类方法实际执行分配 + super().assign_child_resource(resource, location, reassign) + + def unassign_child_resource(self, resource: ElectrodeSheet): + """从洞位中移除极片 + + Args: + resource: 要移除的极片 + """ + if resource not in self.children: + raise ValueError(f"极片 {resource.name} 不在洞位 {self.name} 中") + + # 调用父类方法实际执行移除 + super().unassign_child_resource(resource) + + + + def serialize_state(self) -> Dict[str, Any]: + return { + "sheet_count": len(self.children), + "sheets": [sheet.serialize() for sheet in self.children], + } +class ClipMagazine_four(ItemizedResource[ClipMagazineHole]): + """子弹夹类 - 有4个洞位,每个洞位放多个极片""" + children: List[ClipMagazineHole] + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + hole_spacing: float = 25.0, + max_sheets_per_hole: int = 100, + category: str = "clip_magazine_four", + model: Optional[str] = None, + ): + """初始化子弹夹 + + Args: + name: 子弹夹名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + hole_diameter: 洞直径 (mm) + hole_depth: 洞深度 (mm) + hole_spacing: 洞位间距 (mm) + max_sheets_per_hole: 每个洞位最大极片数量 + category: 类别 + model: 型号 + """ + # 创建4个洞位,排成2x2布局 + holes = create_ordered_items_2d( + klass=ClipMagazineHole, + num_items_x=2, + num_items_y=2, + dx=(size_x - 2 * hole_spacing) / 2, # 居中 + dy=(size_y - hole_spacing) / 2, # 居中 + dz=size_z - 0, + item_dx=hole_spacing, + item_dy=hole_spacing, + diameter=hole_diameter, + depth=hole_depth, + ) + + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + ordered_items=holes, + category=category, + model=model, + ) + + # 保存洞位的直径和深度 + self.hole_diameter = hole_diameter + self.hole_depth = hole_depth + self.max_sheets_per_hole = max_sheets_per_hole + + def serialize(self) -> dict: + return { + **super().serialize(), + "hole_diameter": self.hole_diameter, + "hole_depth": self.hole_depth, + "max_sheets_per_hole": self.max_sheets_per_hole, + } +# TODO: 这个要改 +class ClipMagazine(ItemizedResource[ClipMagazineHole]): + """子弹夹类 - 有6个洞位,每个洞位放多个极片""" + children: List[ClipMagazineHole] + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + hole_spacing: float = 25.0, + max_sheets_per_hole: int = 100, + category: str = "clip_magazine", + model: Optional[str] = None, + ): + """初始化子弹夹 + + Args: + name: 子弹夹名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + hole_diameter: 洞直径 (mm) + hole_depth: 洞深度 (mm) + hole_spacing: 洞位间距 (mm) + max_sheets_per_hole: 每个洞位最大极片数量 + category: 类别 + model: 型号 + """ + # 创建6个洞位,排成2x3布局 + holes = create_ordered_items_2d( + klass=ClipMagazineHole, + num_items_x=3, + num_items_y=2, + dx=(size_x - 2 * hole_spacing) / 2, # 居中 + dy=(size_y - hole_spacing) / 2, # 居中 + dz=size_z - 0, + item_dx=hole_spacing, + item_dy=hole_spacing, + diameter=hole_diameter, + depth=hole_depth, + ) + + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + ordered_items=holes, + category=category, + model=model, + ) + + # 保存洞位的直径和深度 + self.hole_diameter = hole_diameter + self.hole_depth = hole_depth + self.max_sheets_per_hole = max_sheets_per_hole + + def serialize(self) -> dict: + return { + **super().serialize(), + "hole_diameter": self.hole_diameter, + "hole_depth": self.hole_depth, + "max_sheets_per_hole": self.max_sheets_per_hole, + } +#是一种类型注解,不用self +class BatteryState(TypedDict): + """电池状态字典""" + diameter: float + height: float + + electrolyte_name: str + electrolyte_volume: float + +class Battery(Resource): + """电池类 - 可容纳极片""" + children: List[ElectrodeSheet] = [] + + def __init__( + self, + name: str, + category: str = "battery", + ): + """初始化电池 + + Args: + name: 电池名称 + diameter: 直径 (mm) + height: 高度 (mm) + max_volume: 最大容量 (μL) + barcode: 二维码编号 + category: 类别 + model: 型号 + """ + super().__init__( + name=name, + size_x=1, + size_y=1, + size_z=1, + category=category, + ) + self._unilabos_state: BatteryState = BatteryState() + + def add_electrolyte_with_bottle(self, bottle: Bottle) -> bool: + to_add_name = bottle._unilabos_state["electrolyte_name"] + if bottle.aspirate_electrolyte(10): + if self.add_electrolyte(to_add_name, 10): + pass + else: + bottle._unilabos_state["electrolyte_volume"] += 10 + + def set_electrolyte(self, name: str, volume: float) -> None: + """设置电解液信息""" + self._unilabos_state["electrolyte_name"] = name + self._unilabos_state["electrolyte_volume"] = volume + #这个应该没用,不会有加了后再加的事情 + def add_electrolyte(self, name: str, volume: float) -> bool: + """添加电解液信息""" + if name != self._unilabos_state["electrolyte_name"]: + return False + self._unilabos_state["electrolyte_volume"] += volume + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + +# 电解液作为属性放进去 + +class BatteryPressSlotState(TypedDict): + """电池状态字典""" + diameter: float =20.0 + depth: float = 4.0 + +class BatteryPressSlot(Resource): + """电池压制槽类 - 设备,可容纳一个电池""" + children: List[Battery] = [] + + def __init__( + self, + name: str = "BatteryPressSlot", + category: str = "battery_press_slot", + ): + """初始化电池压制槽 + + Args: + name: 压制槽名称 + diameter: 直径 (mm) + depth: 深度 (mm) + category: 类别 + model: 型号 + """ + super().__init__( + name=name, + size_x=10, + size_y=12, + size_z=13, + category=category, + ) + self._unilabos_state: BatteryPressSlotState = BatteryPressSlotState() + + def has_battery(self) -> bool: + """检查是否有电池""" + return len(self.children) > 0 + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + + def assign_child_resource( + self, + resource: Battery, + location: Optional[Coordinate], + reassign: bool = True, + ): + """放置极片""" + # TODO: 让高京看下槽位只有一个电池时是否这么写。 + if self.has_battery(): + raise ValueError(f"槽位已含有一个电池,无法再放置其他电池") + super().assign_child_resource(resource, location, reassign) + + # 根据children的编号取物料对象。 + def get_battery_info(self, index: int) -> Battery: + return self.children[0] + +# TODO:这个移液枪架子看一下从哪继承 +class TipBox64State(TypedDict): + """电池状态字典""" + tip_diameter: float = 5.0 + tip_length: float = 50.0 + with_tips: bool = True + +class TipBox64(TipRack): + """64孔枪头盒类""" + + children: List[TipSpot] = [] + def __init__( + self, + name: str, + size_x: float = 127.8, + size_y: float = 85.5, + size_z: float = 60.0, + category: str = "tip_box_64", + model: Optional[str] = None, + ): + """初始化64孔枪头盒 + + Args: + name: 枪头盒名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + tip_diameter: 枪头直径 (mm) + tip_length: 枪头长度 (mm) + category: 类别 + model: 型号 + with_tips: 是否带枪头 + """ + from pylabrobot.resources.tip import Tip + + # 创建8x8=64个枪头位 + def make_tip(): + return Tip( + has_filter=False, + total_tip_length=20.0, + maximal_volume=1000, # 1mL + fitting_depth=8.0, + ) + + tip_spots = create_ordered_items_2d( + klass=TipSpot, + num_items_x=8, + num_items_y=8, + dx=8.0, + dy=8.0, + dz=0.0, + item_dx=9.0, + item_dy=9.0, + size_x=10, + size_y=10, + size_z=0.0, + make_tip=make_tip, + ) + self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate() + # 记录网格参数用于前端渲染 + self._grid_params = { + "num_items_x": 8, + "num_items_y": 8, + "dx": 8.0, + "dy": 8.0, + "item_dx": 9.0, + "item_dy": 9.0, + } + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + ordered_items=tip_spots, + category=category, + model=model, + with_tips=True, + ) + + def serialize(self) -> dict: + return { + **super().serialize(), + **self._grid_params, + } + + + +class WasteTipBoxstate(TypedDict): + """"废枪头盒状态字典""" + max_tips: int = 100 + tip_count: int = 0 + +#枪头不是一次性的(同一溶液则反复使用),根据寄存器判断 +class WasteTipBox(Trash): + """废枪头盒类 - 100个枪头容量""" + + def __init__( + self, + name: str, + size_x: float = 127.8, + size_y: float = 85.5, + size_z: float = 60.0, + category: str = "waste_tip_box", + model: Optional[str] = None, + ): + """初始化废枪头盒 + + Args: + name: 废枪头盒名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + max_tips: 最大枪头容量 + category: 类别 + model: 型号 + """ + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category=category, + model=model, + ) + self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate() + + def add_tip(self) -> None: + """添加废枪头""" + if self._unilabos_state["tip_count"] >= self._unilabos_state["max_tips"]: + raise ValueError(f"废枪头盒 {self.name} 已满") + self._unilabos_state["tip_count"] += 1 + + def get_tip_count(self) -> int: + """获取枪头数量""" + return self._unilabos_state["tip_count"] + + def empty(self) -> None: + """清空废枪头盒""" + self._unilabos_state["tip_count"] = 0 + + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + + +class BottleRackState(TypedDict): + """ bottle_diameter: 瓶子直径 (mm) + bottle_height: 瓶子高度 (mm) + position_spacing: 位置间距 (mm)""" + bottle_diameter: float + bottle_height: float + name_to_index: dict + + +class BottleRackState(TypedDict): + """ bottle_diameter: 瓶子直径 (mm) + bottle_height: 瓶子高度 (mm) + position_spacing: 位置间距 (mm)""" + bottle_diameter: float + bottle_height: float + position_spacing: float + name_to_index: dict + + +class BottleRack(Resource): + """瓶架类 - 12个待配位置+12个已配位置""" + children: List[Resource] = [] + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + category: str = "bottle_rack", + model: Optional[str] = None, + num_items_x: int = 3, + num_items_y: int = 4, + position_spacing: float = 35.0, + orientation: str = "horizontal", + padding_x: float = 20.0, + padding_y: float = 20.0, + ): + """初始化瓶架 + + Args: + name: 瓶架名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + category: 类别 + model: 型号 + """ + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category=category, + model=model, + ) + # 初始化状态 + self._unilabos_state: BottleRackState = BottleRackState( + bottle_diameter=30.0, + bottle_height=100.0, + position_spacing=position_spacing, + name_to_index={}, + ) + # 基于网格生成瓶位坐标映射(居中摆放) + # 使用内边距,避免点跑到容器外(前端渲染不按mm等比缩放时更稳妥) + origin_x = padding_x + origin_y = padding_y + self.index_to_pos = {} + for j in range(num_items_y): + for i in range(num_items_x): + idx = j * num_items_x + i + if orientation == "vertical": + # 纵向:沿 y 方向优先排列 + self.index_to_pos[idx] = Coordinate( + x=origin_x + j * position_spacing, + y=origin_y + i * position_spacing, + z=0, + ) + else: + # 横向(默认):沿 x 方向优先排列 + self.index_to_pos[idx] = Coordinate( + x=origin_x + i * position_spacing, + y=origin_y + j * position_spacing, + z=0, + ) + self.name_to_index = {} + self.name_to_pos = {} + self.num_items_x = num_items_x + self.num_items_y = num_items_y + self.orientation = orientation + self.padding_x = padding_x + self.padding_y = padding_y + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update( + self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + + # TODO: 这里有些问题要重新写一下 + def assign_child_resource_old(self, resource: Resource, location=Coordinate.zero(), reassign=True): + capacity = self.num_items_x * self.num_items_y + assert len(self.children) < capacity, "瓶架已满,无法添加更多瓶子" + index = len(self.children) + location = self.index_to_pos.get(index, Coordinate.zero()) + self.name_to_pos[resource.name] = location + self.name_to_index[resource.name] = index + return super().assign_child_resource(resource, location, reassign) + + def assign_child_resource(self, resource: Resource, index: int): + capacity = self.num_items_x * self.num_items_y + assert 0 <= index < capacity, "无效的瓶子索引" + self.name_to_index[resource.name] = index + location = self.index_to_pos[index] + return super().assign_child_resource(resource, location) + + def unassign_child_resource(self, resource: Bottle): + super().unassign_child_resource(resource) + self.index_to_pos.pop(self.name_to_index.pop(resource.name, None), None) + + def serialize(self) -> dict: + return { + **super().serialize(), + "num_items_x": self.num_items_x, + "num_items_y": self.num_items_y, + "position_spacing": self._unilabos_state.get("position_spacing", 35.0), + "orientation": self.orientation, + "padding_x": self.padding_x, + "padding_y": self.padding_y, + } + + +class BottleState(TypedDict): + diameter: float + height: float + electrolyte_name: str + electrolyte_volume: float + max_volume: float + +class Bottle(Resource): + """瓶子类 - 容纳电解液""" + + def __init__( + self, + name: str, + category: str = "bottle", + ): + """初始化瓶子 + + Args: + name: 瓶子名称 + diameter: 直径 (mm) + height: 高度 (mm) + max_volume: 最大体积 (μL) + barcode: 二维码 + category: 类别 + model: 型号 + """ + super().__init__( + name=name, + size_x=1, + size_y=1, + size_z=1, + category=category, + ) + self._unilabos_state: BottleState = BottleState() + + def aspirate_electrolyte(self, volume: float) -> bool: + current_volume = self._unilabos_state["electrolyte_volume"] + assert current_volume > volume, f"Cannot aspirate {volume}μL, only {current_volume}μL available." + self._unilabos_state["electrolyte_volume"] -= volume + return True + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + +class CoincellDeck(Deck): + """纽扣电池组装工作站台面类""" + + def __init__( + self, + name: str = "coin_cell_deck", + size_x: float = 1620.0, # 3.66m + size_y: float = 1270.0, # 1.23m + size_z: float = 500.0, + origin: Coordinate = Coordinate(0, 0, 0), + category: str = "coin_cell_deck", + ): + """初始化纽扣电池组装工作站台面 + + Args: + name: 台面名称 + size_x: 长度 (mm) - 3.66m + size_y: 宽度 (mm) - 1.23m + size_z: 高度 (mm) + origin: 原点坐标 + category: 类别 + """ + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + origin=origin, + category=category, + ) + +#if __name__ == "__main__": +# # 转移极片的测试代码 +# deck = CoincellDeck("coin_cell_deck") +# ban_cao_wei = PlateSlot("ban_cao_wei", max_plates=8) +# deck.assign_child_resource(ban_cao_wei, Coordinate(x=0, y=0, z=0)) +# +# plate_1 = MaterialPlate("plate_1", 1,1,1, fill=True) +# for i, hole in enumerate(plate_1.children): +# sheet = ElectrodeSheet(f"hole_{i}_sheet_1") +# sheet._unilabos_state = { +# "diameter": 14, +# "info": "NMC", +# "mass": 5.0, +# "material_type": "positive_electrode", +# "thickness": 0.1 +# } +# hole._unilabos_state = { +# "depth": 1.0, +# "diameter": 14, +# "info": "", +# "max_sheets": 1 +# } +# hole.assign_child_resource(sheet, Coordinate.zero()) +# plate_1._unilabos_state = { +# "hole_spacing_x": 20.0, +# "hole_spacing_y": 20.0, +# "hole_diameter": 5, +# "info": "这是第一块料板" +# } +# plate_1.update_locations() +# ban_cao_wei.assign_child_resource(plate_1, Coordinate.zero()) +# # zi_dan_jia = ClipMagazine("zi_dan_jia", 1, 1, 1) +# # deck.assign_child_resource(ban_cao_wei, Coordinate(x=200, y=200, z=0)) +# +# from unilabos.resources.graphio import * +# A = tree_to_list([resource_plr_to_ulab(deck)]) +# with open("test.json", "w") as f: +# json.dump(A, f) +# +# +#def get_plate_with_14mm_hole(name=""): +# plate = MaterialPlate(name=name) +# for i in range(4): +# for j in range(4): +# hole = MaterialHole(f"{i+1}x{j+1}") +# hole._unilabos_state["diameter"] = 14 +# hole._unilabos_state["max_sheets"] = 1 +# plate.assign_child_resource(hole) +# return plate + +def create_a_liaopan(): + liaopan = MaterialPlate(name="liaopan", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) + for i in range(16): + jipian = ElectrodeSheet(name=f"jipian_{i}", size_x= 12, size_y=12, size_z=0.1) + liaopan1.children[i].assign_child_resource(jipian, location=None) + return liaopan + +def create_a_coin_cell_deck(): + deck = Deck(size_x=1200, + size_y=800, + size_z=900) + + #liaopan = TipBox64(name="liaopan") + + #创建一个4*4的物料板 + liaopan1 = MaterialPlate(name="liaopan1", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) + #把物料板放到桌子上 + deck.assign_child_resource(liaopan1, Coordinate(x=0, y=0, z=0)) + #创建一个极片 + for i in range(16): + jipian = ElectrodeSheet(name=f"jipian_{i}", size_x= 12, size_y=12, size_z=0.1) + liaopan1.children[i].assign_child_resource(jipian, location=None) + #创建一个4*4的物料板 + liaopan2 = MaterialPlate(name="liaopan2", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) + #把物料板放到桌子上 + deck.assign_child_resource(liaopan2, Coordinate(x=500, y=0, z=0)) + + #创建一个4*4的物料板 + liaopan3 = MaterialPlate(name="liaopan3", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) + #把物料板放到桌子上 + deck.assign_child_resource(liaopan3, Coordinate(x=1000, y=0, z=0)) + + print(deck) + + return deck + + +import json + +if __name__ == "__main__": + electrode1 = BatteryPressSlot() + #print(electrode1.get_size_x()) + #print(electrode1.get_size_y()) + #print(electrode1.get_size_z()) + #jipian = ElectrodeSheet() + #jipian._unilabos_state["diameter"] = 18 + #print(jipian.serialize()) + #print(jipian.serialize_state()) + + deck = CoincellDeck() + """======================================子弹夹============================================""" + zip_dan_jia = ClipMagazine_four("zi_dan_jia", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia, Coordinate(x=1400, y=50, z=0)) + zip_dan_jia2 = ClipMagazine_four("zi_dan_jia2", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia2, Coordinate(x=1600, y=200, z=0)) + zip_dan_jia3 = ClipMagazine("zi_dan_jia3", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia3, Coordinate(x=1500, y=200, z=0)) + zip_dan_jia4 = ClipMagazine("zi_dan_jia4", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia4, Coordinate(x=1500, y=300, z=0)) + zip_dan_jia5 = ClipMagazine("zi_dan_jia5", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia5, Coordinate(x=1600, y=300, z=0)) + zip_dan_jia6 = ClipMagazine("zi_dan_jia6", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia6, Coordinate(x=1530, y=500, z=0)) + zip_dan_jia7 = ClipMagazine("zi_dan_jia7", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia7, Coordinate(x=1180, y=400, z=0)) + zip_dan_jia8 = ClipMagazine("zi_dan_jia8", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia8, Coordinate(x=1280, y=400, z=0)) + for i in range(4): + jipian = ElectrodeSheet(name=f"zi_dan_jia_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia2.children[i].assign_child_resource(jipian, location=None) + for i in range(4): + jipian2 = ElectrodeSheet(name=f"zi_dan_jia2_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia.children[i].assign_child_resource(jipian2, location=None) + for i in range(6): + jipian3 = ElectrodeSheet(name=f"zi_dan_jia3_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia3.children[i].assign_child_resource(jipian3, location=None) + for i in range(6): + jipian4 = ElectrodeSheet(name=f"zi_dan_jia4_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia4.children[i].assign_child_resource(jipian4, location=None) + for i in range(6): + jipian5 = ElectrodeSheet(name=f"zi_dan_jia5_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia5.children[i].assign_child_resource(jipian5, location=None) + for i in range(6): + jipian6 = ElectrodeSheet(name=f"zi_dan_jia6_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia6.children[i].assign_child_resource(jipian6, location=None) + for i in range(6): + jipian7 = ElectrodeSheet(name=f"zi_dan_jia7_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia7.children[i].assign_child_resource(jipian7, location=None) + for i in range(6): + jipian8 = ElectrodeSheet(name=f"zi_dan_jia8_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia8.children[i].assign_child_resource(jipian8, location=None) + """======================================子弹夹============================================""" + #liaopan = TipBox64(name="liaopan") + """======================================物料板============================================""" + #创建一个4*4的物料板 + liaopan1 = MaterialPlate(name="liaopan1", size_x=120, size_y=100, size_z=10.0, fill=True) + deck.assign_child_resource(liaopan1, Coordinate(x=1010, y=50, z=0)) + for i in range(16): + jipian_1 = ElectrodeSheet(name=f"{liaopan1.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + liaopan1.children[i].assign_child_resource(jipian_1, location=None) + + liaopan2 = MaterialPlate(name="liaopan2", size_x=120, size_y=100, size_z=10.0, fill=True) + deck.assign_child_resource(liaopan2, Coordinate(x=1130, y=50, z=0)) + + liaopan3 = MaterialPlate(name="liaopan3", size_x=120, size_y=100, size_z=10.0, fill=True) + deck.assign_child_resource(liaopan3, Coordinate(x=1250, y=50, z=0)) + + liaopan4 = MaterialPlate(name="liaopan4", size_x=120, size_y=100, size_z=10.0, fill=True) + deck.assign_child_resource(liaopan4, Coordinate(x=1010, y=150, z=0)) + for i in range(16): + jipian_4 = ElectrodeSheet(name=f"{liaopan4.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + liaopan4.children[i].assign_child_resource(jipian_4, location=None) + liaopan5 = MaterialPlate(name="liaopan5", size_x=120, size_y=100, size_z=10.0, fill=True) + deck.assign_child_resource(liaopan5, Coordinate(x=1130, y=150, z=0)) + liaopan6 = MaterialPlate(name="liaopan6", size_x=120, size_y=100, size_z=10.0, fill=True) + deck.assign_child_resource(liaopan6, Coordinate(x=1250, y=150, z=0)) + #liaopan.children[3].assign_child_resource(jipian, location=None) + """======================================物料板============================================""" + """======================================瓶架,移液枪============================================""" + # 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒 + bottle_rack_3x4 = BottleRack( + name="bottle_rack_3x4", + size_x=210.0, + size_y=140.0, + size_z=100.0, + num_items_x=3, + num_items_y=4, + position_spacing=35.0, + orientation="vertical", + ) + deck.assign_child_resource(bottle_rack_3x4, Coordinate(x=100, y=200, z=0)) + + bottle_rack_6x2 = BottleRack( + name="bottle_rack_6x2", + size_x=120.0, + size_y=250.0, + size_z=100.0, + num_items_x=6, + num_items_y=2, + position_spacing=35.0, + orientation="vertical", + ) + deck.assign_child_resource(bottle_rack_6x2, Coordinate(x=300, y=300, z=0)) + + bottle_rack_6x2_2 = BottleRack( + name="bottle_rack_6x2_2", + size_x=120.0, + size_y=250.0, + size_z=100.0, + num_items_x=6, + num_items_y=2, + position_spacing=35.0, + orientation="vertical", + ) + deck.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=430, y=300, z=0)) + + + # 将 ElectrodeSheet 放满 3x4 与 6x2 的所有孔位 + for idx in range(bottle_rack_3x4.num_items_x * bottle_rack_3x4.num_items_y): + sheet = ElectrodeSheet(name=f"sheet_3x4_{idx}", size_x=12, size_y=12, size_z=0.1) + bottle_rack_3x4.assign_child_resource(sheet, index=idx) + + for idx in range(bottle_rack_6x2.num_items_x * bottle_rack_6x2.num_items_y): + sheet = ElectrodeSheet(name=f"sheet_6x2_{idx}", size_x=12, size_y=12, size_z=0.1) + bottle_rack_6x2.assign_child_resource(sheet, index=idx) + + tip_box = TipBox64(name="tip_box_64") + deck.assign_child_resource(tip_box, Coordinate(x=300, y=100, z=0)) + + waste_tip_box = WasteTipBox(name="waste_tip_box") + deck.assign_child_resource(waste_tip_box, Coordinate(x=300, y=200, z=0)) + """======================================瓶架,移液枪============================================""" + print(deck) + + + from unilabos.resources.graphio import convert_resources_from_type + from unilabos.config.config import BasicConfig + BasicConfig.ak = "56bbed5b-6e30-438c-b06d-f69eaa63bb45" + BasicConfig.sk = "238222fe-0bf7-4350-a426-e5ced8011dcf" + from unilabos.app.web.client import http_client + + resources = convert_resources_from_type([deck], [Resource]) + + # 检查序列化后的资源 + + json.dump({"nodes": resources, "links": []}, open("button_battery_decks_unilab.json", "w"), indent=2) + + + #print(resources) + http_client.remote_addr = "https://uni-lab.test.bohrium.com/api/v1" + + http_client.resource_add(resources) \ No newline at end of file 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 ee88e602..4b8ba73d 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -1,39 +1,1142 @@ +import csv +import json +import os +import threading +import time +from datetime import datetime from typing import Any, Dict, Optional from pylabrobot.resources import Resource as PLRResource +from unilabos_msgs.msg import Resource from unilabos.device_comms.modbus_plc.client import ModbusTcpClient -from unilabos.devices.workstation.workstation_base import ResourceSynchronizer, WorkstationBase +from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import MaterialHole, MaterialPlate +from unilabos.devices.workstation.workstation_base import WorkstationBase +from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusNode, PLCWorkflow, ModbusWorkflow, WorkflowAction, BaseClient +from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNodeBase, DataType, WorderOrder +from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import * +from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode +from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode +#构建物料系统 class CoinCellAssemblyWorkstation(WorkstationBase): def __init__( self, - device_id: str, - deck_config: Dict[str, Any], - children: Optional[Dict[str, Any]] = None, - resource_synchronizer: Optional[ResourceSynchronizer] = None, - host: str = "192.168.0.0", - port: str = "", + deck: CoincellDeck, + address: str = "192.168.1.20", + port: str = "502", + debug_mode: bool = True, *args, **kwargs, ): super().__init__( - device_id=device_id, - deck_config=deck_config, - children=children, - resource_synchronizer=resource_synchronizer, + #桌子 + deck=deck, *args, **kwargs, ) + self.debug_mode = debug_mode + self.deck = deck + """ 连接初始化 """ + modbus_client = TCPClient(addr=address, port=port) + print("modbus_client", modbus_client) + if not debug_mode: + modbus_client.client.connect() + count = 100 + while count >0: + count -=1 + if modbus_client.client.is_socket_open(): + break + time.sleep(2) + if not modbus_client.client.is_socket_open(): + raise ValueError('modbus tcp connection failed') + else: + print("测试模式,跳过连接") + + """ 工站的配置 """ + self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_a.csv')) + self.client = modbus_client.register_node_list(self.nodes) + self.success = False + self.allow_data_read = False #允许读取函数运行标志位 + self.csv_export_thread = None + self.csv_export_running = False + self.csv_export_file = None + #创建一个物料台面,包含两个极片板 + #self.deck = create_a_coin_cell_deck() - self.hardware_interface = ModbusTcpClient(host=host, port=port) - - def run_assembly(self, wf_name: str, resource: PLRResource, params: str = "\{\}"): - """启动工作流""" - self.current_workflow_status = WorkflowStatus.RUNNING - logger.info(f"工作站 {self.device_id} 启动工作流: {wf_name}") - - # TODO: 实现工作流逻辑 - - anode_sheet = self.deck.get_resource("anode_sheet") + #self._ros_node.update_resource(self.deck) - \ No newline at end of file + #ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + # "resources": [self.deck] + #}) + + + def post_init(self, ros_node: ROS2WorkstationNode): + self._ros_node = ros_node + #self.deck = create_a_coin_cell_deck() + ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + "resources": [self.deck] + }) + + # 批量操作在这里写 + async def change_hole_sheet_to_2(self, hole: MaterialHole): + hole._unilabos_state["max_sheets"] = 2 + return await self._ros_node.update_resource(hole) + + + async def fill_plate(self): + plate_1: MaterialPlate = self.deck.children[0].children[0] + #plate_1 + return await self._ros_node.update_resource(plate_1) + + #def run_assembly(self, wf_name: str, resource: PLRResource, params: str = "\{\}"): + # """启动工作流""" + # self.current_workflow_status = WorkflowStatus.RUNNING + # logger.info(f"工作站 {self.device_id} 启动工作流: {wf_name}") +# + # # TODO: 实现工作流逻辑 +# + # anode_sheet = self.deck.get_resource("anode_sheet") + + """ Action逻辑代码 """ + def _sys_start_cmd(self, cmd=None): + """设备启动命令 (可读写)""" + if cmd is not None: # 写入模式 + self.success = False + node = self.client.use_node('COIL_SYS_START_CMD') + ret = node.write(cmd) + print(ret) + self.success = True + return self.success + else: # 读取模式 + cmd_feedback, read_err = self.client.use_node('COIL_SYS_START_CMD').read(1) + return cmd_feedback[0] + + def _sys_stop_cmd(self, cmd=None): + """设备停止命令 (可读写)""" + if cmd is not None: # 写入模式 + self.success = False + node = self.client.use_node('COIL_SYS_STOP_CMD') + node.write(cmd) + self.success = True + return self.success + else: # 读取模式 + cmd_feedback, read_err = self.client.use_node('COIL_SYS_STOP_CMD').read(1) + return cmd_feedback[0] + + def _sys_reset_cmd(self, cmd=None): + """设备复位命令 (可读写)""" + if cmd is not None: + self.success = False + self.client.use_node('COIL_SYS_RESET_CMD').write(cmd) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('COIL_SYS_RESET_CMD').read(1) + return cmd_feedback[0] + + def _sys_hand_cmd(self, cmd=None): + """手动模式命令 (可读写)""" + if cmd is not None: + self.success = False + self.client.use_node('COIL_SYS_HAND_CMD').write(cmd) + self.success = True + print("步骤0") + return self.success + else: + cmd_feedback, read_err = self.client.use_node('COIL_SYS_HAND_CMD').read(1) + return cmd_feedback[0] + + def _sys_auto_cmd(self, cmd=None): + """自动模式命令 (可读写)""" + if cmd is not None: + self.success = False + self.client.use_node('COIL_SYS_AUTO_CMD').write(cmd) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('COIL_SYS_AUTO_CMD').read(1) + return cmd_feedback[0] + + def _sys_init_cmd(self, cmd=None): + """初始化命令 (可读写)""" + if cmd is not None: + self.success = False + self.client.use_node('COIL_SYS_INIT_CMD').write(cmd) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('COIL_SYS_INIT_CMD').read(1) + return cmd_feedback[0] + + def _unilab_send_msg_succ_cmd(self, cmd=None): + """UNILAB发送配方完毕 (可读写)""" + if cmd is not None: + self.success = False + self.client.use_node('COIL_UNILAB_SEND_MSG_SUCC_CMD').write(cmd) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('COIL_UNILAB_SEND_MSG_SUCC_CMD').read(1) + return cmd_feedback[0] + + def _unilab_rec_msg_succ_cmd(self, cmd=None): + """UNILAB接收测试电池数据完毕 (可读写)""" + if cmd is not None: + self.success = False + self.client.use_node('COIL_UNILAB_REC_MSG_SUCC_CMD').write(cmd) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('COIL_UNILAB_REC_MSG_SUCC_CMD').read(1) + return cmd_feedback + + + # ====================== 命令类指令(REG_x_) ====================== + def _unilab_send_msg_electrolyte_num(self, num=None): + """UNILAB写电解液使用瓶数(可读写)""" + if num is not None: + self.success = False + ret = self.client.use_node('REG_MSG_ELECTROLYTE_NUM').write(num) + print(ret) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('REG_MSG_ELECTROLYTE_NUM').read(1) + return cmd_feedback[0] + + def _unilab_send_msg_electrolyte_use_num(self, use_num=None): + """UNILAB写单次电解液使用瓶数(可读写)""" + if use_num is not None: + self.success = False + self.client.use_node('REG_MSG_ELECTROLYTE_USE_NUM').write(use_num) + self.success = True + return self.success + else: + return False + + def _unilab_send_msg_assembly_type(self, num=None): + """UNILAB写组装参数""" + if num is not None: + self.success = False + self.client.use_node('REG_MSG_ASSEMBLY_TYPE').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('REG_MSG_ASSEMBLY_TYPE').read(1) + return cmd_feedback[0] + + def _unilab_send_msg_electrolyte_vol(self, vol=None): + """UNILAB写电解液吸取量参数""" + if vol is not None: + self.success = False + self.client.use_node('REG_MSG_ELECTROLYTE_VOLUME').write(vol, data_type=DataType.FLOAT32, word_order=WorderOrder.LITTLE) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('REG_MSG_ELECTROLYTE_VOLUME').read(2, word_order=WorderOrder.LITTLE) + return cmd_feedback[0] + + def _unilab_send_msg_assembly_pressure(self, vol=None): + """UNILAB写电池压制力""" + if vol is not None: + self.success = False + self.client.use_node('REG_MSG_ASSEMBLY_PRESSURE').write(vol, data_type=DataType.FLOAT32, word_order=WorderOrder.LITTLE) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('REG_MSG_ASSEMBLY_PRESSURE').read(2, word_order=WorderOrder.LITTLE) + return cmd_feedback[0] + + # ==================== 0905新增内容(COIL_x_STATUS) ==================== + def _unilab_send_electrolyte_bottle_num(self, num=None): + """UNILAB发送电解液瓶数完毕""" + if num is not None: + self.success = False + self.client.use_node('UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM').read(1) + return cmd_feedback[0] + + def _unilab_rece_electrolyte_bottle_num(self, num=None): + """设备请求接受电解液瓶数""" + if num is not None: + self.success = False + self.client.use_node('UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM').read(1) + return cmd_feedback[0] + + def _reg_msg_electrolyte_num(self, num=None): + """电解液已使用瓶数""" + if num is not None: + self.success = False + self.client.use_node('REG_MSG_ELECTROLYTE_NUM').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('REG_MSG_ELECTROLYTE_NUM').read(1) + return cmd_feedback[0] + + def _reg_data_electrolyte_use_num(self, num=None): + """单瓶电解液完成组装数""" + if num is not None: + self.success = False + self.client.use_node('REG_DATA_ELECTROLYTE_USE_NUM').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_USE_NUM').read(1) + return cmd_feedback[0] + + def _unilab_send_finished_cmd(self, num=None): + """Unilab发送已知一组组装完成信号""" + if num is not None: + self.success = False + self.client.use_node('UNILAB_SEND_FINISHED_CMD').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('UNILAB_SEND_FINISHED_CMD').read(1) + return cmd_feedback[0] + + def _unilab_rece_finished_cmd(self, num=None): + """Unilab接收已知一组组装完成信号""" + if num is not None: + self.success = False + self.client.use_node('UNILAB_RECE_FINISHED_CMD').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('UNILAB_RECE_FINISHED_CMD').read(1) + return cmd_feedback[0] + + + + # ==================== 状态类属性(COIL_x_STATUS) ==================== + def _sys_start_status(self) -> bool: + """设备启动中( BOOL)""" + status, read_err = self.client.use_node('COIL_SYS_START_STATUS').read(1) + return status[0] + + def _sys_stop_status(self) -> bool: + """设备停止中( BOOL)""" + status, read_err = self.client.use_node('COIL_SYS_STOP_STATUS').read(1) + return status[0] + + def _sys_reset_status(self) -> bool: + """设备复位中( BOOL)""" + status, read_err = self.client.use_node('COIL_SYS_RESET_STATUS').read(1) + return status[0] + + def _sys_init_status(self) -> bool: + """设备初始化完成( BOOL)""" + status, read_err = self.client.use_node('COIL_SYS_INIT_STATUS').read(1) + return status[0] + + # 查找资源 + def modify_deck_name(self, resource_name: str): + # figure_res = self._ros_node.resource_tracker.figure_resource({"name": resource_name}) + # print(f"!!! figure_res: {type(figure_res)}") + self.deck.children[1] + return + + @property + def sys_status(self) -> str: + if self.debug_mode: + return "设备调试模式" + if self._sys_start_status(): + return "设备启动中" + elif self._sys_stop_status(): + return "设备停止中" + elif self._sys_reset_status(): + return "设备复位中" + elif self._sys_init_status(): + return "设备初始化中" + else: + return "未知状态" + + def _sys_hand_status(self) -> bool: + """设备手动模式( BOOL)""" + status, read_err = self.client.use_node('COIL_SYS_HAND_STATUS').read(1) + return status[0] + + def _sys_auto_status(self) -> bool: + """设备自动模式( BOOL)""" + status, read_err = self.client.use_node('COIL_SYS_AUTO_STATUS').read(1) + return status[0] + + @property + def sys_mode(self) -> str: + if self.debug_mode: + return "设备调试模式" + if self._sys_hand_status(): + return "设备手动模式" + elif self._sys_auto_status(): + return "设备自动模式" + else: + return "未知模式" + + @property + def request_rec_msg_status(self) -> bool: + """设备请求接受配方( BOOL)""" + if self.debug_mode: + return True + status, read_err = self.client.use_node('COIL_REQUEST_REC_MSG_STATUS').read(1) + return status[0] + + @property + def request_send_msg_status(self) -> bool: + """设备请求发送测试数据( BOOL)""" + if self.debug_mode: + return True + status, read_err = self.client.use_node('COIL_REQUEST_SEND_MSG_STATUS').read(1) + return status[0] + + # ======================= 其他属性(特殊功能) ======================== + ''' + @property + def warning_1(self) -> bool: + status, read_err = self.client.use_node('COIL_WARNING_1').read(1) + return status[0] + ''' + # ===================== 生产数据区 ====================== + + @property + def data_assembly_coin_cell_num(self) -> int: + """已完成电池数量 (INT16)""" + if self.debug_mode: + return 0 + num, read_err = self.client.use_node('REG_DATA_ASSEMBLY_COIN_CELL_NUM').read(1) + return num + + @property + def data_assembly_time(self) -> float: + """单颗电池组装时间 (秒, REAL/FLOAT32)""" + if self.debug_mode: + return 0 + time, read_err = self.client.use_node('REG_DATA_ASSEMBLY_PER_TIME').read(2, word_order=WorderOrder.LITTLE) + return time + + @property + def data_open_circuit_voltage(self) -> float: + """开路电压值 (FLOAT32)""" + if self.debug_mode: + return 0 + vol, read_err = self.client.use_node('REG_DATA_OPEN_CIRCUIT_VOLTAGE').read(2, word_order=WorderOrder.LITTLE) + return vol + + @property + def data_axis_x_pos(self) -> float: + """分液X轴当前位置 (FLOAT32)""" + if self.debug_mode: + return 0 + pos, read_err = self.client.use_node('REG_DATA_AXIS_X_POS').read(2, word_order=WorderOrder.LITTLE) + return pos + + @property + def data_axis_y_pos(self) -> float: + """分液Y轴当前位置 (FLOAT32)""" + if self.debug_mode: + return 0 + pos, read_err = self.client.use_node('REG_DATA_AXIS_Y_POS').read(2, word_order=WorderOrder.LITTLE) + return pos + + @property + def data_axis_z_pos(self) -> float: + """分液Z轴当前位置 (FLOAT32)""" + if self.debug_mode: + return 0 + pos, read_err = self.client.use_node('REG_DATA_AXIS_Z_POS').read(2, word_order=WorderOrder.LITTLE) + return pos + + @property + def data_pole_weight(self) -> float: + """当前电池正极片称重数据 (FLOAT32)""" + if self.debug_mode: + return 0 + weight, read_err = self.client.use_node('REG_DATA_POLE_WEIGHT').read(2, word_order=WorderOrder.LITTLE) + return weight + + @property + def data_assembly_pressure(self) -> int: + """当前电池压制力 (INT16)""" + if self.debug_mode: + return 0 + pressure, read_err = self.client.use_node('REG_DATA_ASSEMBLY_PRESSURE').read(1) + return pressure + + @property + def data_electrolyte_volume(self) -> int: + """当前电解液加注量 (INT16)""" + if self.debug_mode: + return 0 + vol, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_VOLUME').read(1) + return vol + + @property + def data_coin_num(self) -> int: + """当前电池数量 (INT16)""" + if self.debug_mode: + return 0 + num, read_err = self.client.use_node('REG_DATA_COIN_NUM').read(1) + return num + + @property + def data_coin_cell_code(self) -> str: + """电池二维码序列号 (STRING)""" + try: + # 尝试不同的字节序读取 + code_little, read_err = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(10, word_order=WorderOrder.LITTLE) + print(code_little) + clean_code = code_little[-8:][::-1] + return clean_code + except Exception as e: + print(f"读取电池二维码失败: {e}") + return "N/A" + + + @property + def data_electrolyte_code(self) -> str: + try: + # 尝试不同的字节序读取 + code_little, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(10, word_order=WorderOrder.LITTLE) + print(code_little) + clean_code = code_little[-8:][::-1] + return clean_code + except Exception as e: + print(f"读取电解液二维码失败: {e}") + return "N/A" + + # ===================== 环境监控区 ====================== + @property + def data_glove_box_pressure(self) -> float: + """手套箱压力 (bar, FLOAT32)""" + if self.debug_mode: + return 0 + status, read_err = self.client.use_node('REG_DATA_GLOVE_BOX_PRESSURE').read(2, word_order=WorderOrder.LITTLE) + return status + + @property + def data_glove_box_o2_content(self) -> float: + """手套箱氧含量 (ppm, FLOAT32)""" + if self.debug_mode: + return 0 + value, read_err = self.client.use_node('REG_DATA_GLOVE_BOX_O2_CONTENT').read(2, word_order=WorderOrder.LITTLE) + return value + + @property + def data_glove_box_water_content(self) -> float: + """手套箱水含量 (ppm, FLOAT32)""" + if self.debug_mode: + return 0 + value, read_err = self.client.use_node('REG_DATA_GLOVE_BOX_WATER_CONTENT').read(2, word_order=WorderOrder.LITTLE) + return value + +# @property +# def data_stack_vision_code(self) -> int: +# """物料堆叠复检图片编码 (INT16)""" +# if self.debug_mode: +# return 0 +# code, read_err = self.client.use_node('REG_DATA_STACK_VISON_CODE').read(1) +# #code, _ = self.client.use_node('REG_DATA_STACK_VISON_CODE').read(1).type +# print(f"读取物料堆叠复检图片编码", {code}, "error", type(code)) +# #print(code.type) +# # print(read_err) +# return int(code) + + def func_pack_device_init(self): + #切换手动模式 + print("切换手动模式") + self._sys_hand_cmd(True) + time.sleep(1) + while (self._sys_hand_status()) == False: + print("waiting for hand_cmd") + time.sleep(1) + #设备初始化 + self._sys_init_cmd(True) + time.sleep(1) + #sys_init_status为bool值,不加括号 + while (self._sys_init_status())== False: + print("waiting for init_cmd") + time.sleep(1) + #手动按钮置回False + self._sys_hand_cmd(False) + time.sleep(1) + while (self._sys_hand_cmd()) == True: + print("waiting for hand_cmd to False") + time.sleep(1) + #初始化命令置回False + self._sys_init_cmd(False) + time.sleep(1) + while (self._sys_init_cmd()) == True: + print("waiting for init_cmd to False") + time.sleep(1) + + def func_pack_device_auto(self): + #切换自动 + print("切换自动模式") + self._sys_auto_cmd(True) + time.sleep(1) + while (self._sys_auto_status()) == False: + print("waiting for auto_status") + time.sleep(1) + #自动按钮置False + self._sys_auto_cmd(False) + time.sleep(1) + while (self._sys_auto_cmd()) == True: + print("waiting for auto_cmd") + time.sleep(1) + + def func_pack_device_start(self): + #切换自动 + print("启动") + self._sys_start_cmd(True) + time.sleep(1) + while (self._sys_start_status()) == False: + print("waiting for start_status") + time.sleep(1) + #自动按钮置False + self._sys_start_cmd(False) + time.sleep(1) + while (self._sys_start_cmd()) == True: + print("waiting for start_cmd") + time.sleep(1) + + def func_pack_send_bottle_num(self, bottle_num: int): + #发送电解液平台数 + print("启动") + while (self._unilab_rece_electrolyte_bottle_num()) == False: + print("waiting for rece_electrolyte_bottle_num to True") + # self.client.use_node('8520').write(True) + time.sleep(1) + #发送电解液瓶数为2 + self._reg_msg_electrolyte_num(bottle_num) + time.sleep(1) + #完成信号置True + self._unilab_send_electrolyte_bottle_num(True) + time.sleep(1) + #检测到依华已接收 + while (self._unilab_rece_electrolyte_bottle_num()) == True: + print("waiting for rece_electrolyte_bottle_num to False") + time.sleep(1) + #完成信号置False + self._unilab_send_electrolyte_bottle_num(False) + time.sleep(1) + #自动按钮置False + + + # 下发参数 + #def func_pack_send_msg_cmd(self, elec_num: int, elec_use_num: int, elec_vol: float, assembly_type: int, assembly_pressure: int) -> bool: + # """UNILAB写参数""" + # while (self.request_rec_msg_status) == False: + # print("wait for res_msg") + # time.sleep(1) + # self.success = False + # self._unilab_send_msg_electrolyte_num(elec_num) + # time.sleep(1) + # self._unilab_send_msg_electrolyte_use_num(elec_use_num) + # time.sleep(1) + # self._unilab_send_msg_electrolyte_vol(elec_vol) + # time.sleep(1) + # self._unilab_send_msg_assembly_type(assembly_type) + # time.sleep(1) + # self._unilab_send_msg_assembly_pressure(assembly_pressure) + # time.sleep(1) + # self._unilab_send_msg_succ_cmd(True) + # time.sleep(1) + # self._unilab_send_msg_succ_cmd(False) + # #将允许读取标志位置True + # self.allow_data_read = True + # self.success = True + # return self.success + + def func_pack_send_msg_cmd(self, elec_use_num) -> bool: + """UNILAB写参数""" + while (self.request_rec_msg_status) == False: + print("wait for request_rec_msg_status to True") + time.sleep(1) + self.success = False + #self._unilab_send_msg_electrolyte_num(elec_num) + time.sleep(1) + self._unilab_send_msg_electrolyte_use_num(elec_use_num) + time.sleep(1) + self._unilab_send_msg_succ_cmd(True) + time.sleep(1) + while (self.request_rec_msg_status) == True: + print("wait for request_rec_msg_status to False") + time.sleep(1) + self._unilab_send_msg_succ_cmd(False) + #将允许读取标志位置True + self.allow_data_read = True + self.success = True + return self.success + + def func_pack_get_msg_cmd(self, file_path: str="D:\\coin_cell_data") -> bool: + """UNILAB读参数""" + while self.request_send_msg_status == False: + print("waiting for send_read_msg_status to True") + time.sleep(1) + data_open_circuit_voltage = self.data_open_circuit_voltage + data_pole_weight = self.data_pole_weight + 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_electrolyte_code = self.data_electrolyte_code + data_coin_cell_code = self.data_coin_cell_code + print("data_open_circuit_voltage", data_open_circuit_voltage) + print("data_pole_weight", data_pole_weight) + print("data_assembly_time", data_assembly_time) + print("data_assembly_pressure", data_assembly_pressure) + print("data_electrolyte_volume", data_electrolyte_volume) + print("data_coin_num", data_coin_num) + print("data_electrolyte_code", data_electrolyte_code) + print("data_coin_cell_code", data_coin_cell_code) + #接收完信息后,读取完毕标志位置True + self._unilab_rec_msg_succ_cmd(True) + time.sleep(1) + #等待允许读取标志位置False + while self.request_send_msg_status == True: + print("waiting for send_msg_status to False") + time.sleep(1) + self._unilab_rec_msg_succ_cmd(False) + time.sleep(1) + #将允许读取标志位置True + time_date = datetime.now().strftime("%Y%m%d") + #秒级时间戳用于标记每一行电池数据 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + #生成输出文件的变量 + self.csv_export_file = os.path.join(file_path, f"date_{time_date}.csv") + #将数据存入csv文件 + if not os.path.exists(self.csv_export_file): + #创建一个表头 + with open(self.csv_export_file, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile) + writer.writerow([ + 'Time', 'open_circuit_voltage', 'pole_weight', + 'assembly_time', 'assembly_pressure', 'electrolyte_volume', + 'coin_num', 'electrolyte_code', 'coin_cell_code' + ]) + #立刻写入磁盘 + csvfile.flush() + #开始追加电池信息 + with open(self.csv_export_file, 'a', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile) + 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 + ]) + #立刻写入磁盘 + csvfile.flush() + self.success = True + return self.success + + + + def func_pack_send_finished_cmd(self) -> bool: + """UNILAB写参数""" + while (self._unilab_rece_finished_cmd()) == False: + print("wait for rece_finished_cmd to True") + time.sleep(1) + self.success = False + self._unilab_send_finished_cmd(True) + time.sleep(1) + while (self._unilab_rece_finished_cmd()) == True: + print("wait for rece_finished_cmd to False") + time.sleep(1) + self._unilab_send_finished_cmd(False) + #将允许读取标志位置True + self.success = True + return self.success + + + + def func_allpack_cmd(self, elec_num, elec_use_num, file_path: str="D:\\coin_cell_data") -> bool: + summary_csv_file = os.path.join(file_path, "duandian.csv") + # 如果断点文件存在,先读取之前的进度 + if os.path.exists(summary_csv_file): + read_status_flag = True + with open(summary_csv_file, 'r', newline='', encoding='utf-8') as csvfile: + reader = csv.reader(csvfile) + header = next(reader) # 跳过标题行 + data_row = next(reader) # 读取数据行 + if len(data_row) >= 2: + elec_num_r = int(data_row[0]) + elec_use_num_r = int(data_row[1]) + elec_num_N = int(data_row[2]) + elec_use_num_N = int(data_row[3]) + coin_num_N = int(data_row[4]) + if elec_num_r == elec_num and elec_use_num_r == elec_use_num: + print("断点文件与当前任务匹配,继续") + else: + print("断点文件中elec_num、elec_use_num与当前任务不匹配,请检查任务下发参数或修改断点文件") + return False + print(f"从断点文件读取进度: elec_num_N={elec_num_N}, elec_use_num_N={elec_use_num_N}, coin_num_N={coin_num_N}") + + else: + read_status_flag = False + print("未找到断点文件,从头开始") + elec_num_N = 0 + elec_use_num_N = 0 + coin_num_N = 0 + + print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}") + + + #如果是第一次运行,则进行初始化、切换自动、启动, 如果是断点重启则跳过。 + if read_status_flag == False: + #初始化 + self.func_pack_device_init() + #切换自动 + self.func_pack_device_auto() + #启动,小车收回 + self.func_pack_device_start() + #发送电解液瓶数量,启动搬运,多搬运没事 + self.func_pack_send_bottle_num(elec_num) + last_i = elec_num_N + last_j = elec_use_num_N + for i in range(last_i, elec_num): + print(f"开始第{last_i+i+1}瓶电解液的组装") + #第一个循环从上次断点继续,后续循环从0开始 + j_start = last_j if i == last_i else 0 + self.func_pack_send_msg_cmd(elec_use_num-j_start) + + for j in range(j_start, elec_use_num): + print(f"开始第{last_i+i+1}瓶电解液的第{j+j_start+1}个电池组装") + #读取电池组装数据并存入csv + self.func_pack_get_msg_cmd(file_path) + time.sleep(1) + + #这里定义物料系统 + # TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑 + liaopan1 = self.deck.get_resource("liaopan1") + liaopan4 = self.deck.get_resource("liaopan4") + jipian1 = liaopan1.children[coin_num_N].children[0] + jipian4 = liaopan4.children[coin_num_N].children[0] + #print(jipian1) + #从料盘上去物料解绑后放到另一盘上 + jipian1.parent.unassign_child_resource(jipian1) + jipian4.parent.unassign_child_resource(jipian4) + + #print(jipian2.parent) + battery = Battery(name = f"battery_{coin_num_N}") + battery.assign_child_resource(jipian1, location=None) + battery.assign_child_resource(jipian4, location=None) + + zidanjia6 = self.deck.get_resource("zi_dan_jia6") + + zidanjia6.children[0].assign_child_resource(battery, location=None) + + + # 生成断点文件 + # 生成包含elec_num_N、coin_num_N、timestamp的CSV文件 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + with open(summary_csv_file, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile) + writer.writerow(['elec_num','elec_use_num', 'elec_num_N', 'elec_use_num_N', 'coin_num_N', 'timestamp']) + writer.writerow([elec_num, elec_use_num, elec_num_N, elec_use_num_N, coin_num_N, timestamp]) + csvfile.flush() + coin_num_N += 1 + elec_use_num_N += 1 + elec_num_N += 1 + elec_use_num_N = 0 + + #循环正常结束,则删除断点文件 + os.remove(summary_csv_file) + #全部完成后等待依华发送完成信号 + self.func_pack_send_finished_cmd() + + + def func_pack_device_stop(self) -> bool: + """打包指令:设备停止""" + for i in range(3): + time.sleep(2) + print(f"输出{i}") + #print("_sys_hand_cmd", self._sys_hand_cmd()) + #time.sleep(1) + #print("_sys_hand_status", self._sys_hand_status()) + #time.sleep(1) + #print("_sys_init_cmd", self._sys_init_cmd()) + #time.sleep(1) + #print("_sys_init_status", self._sys_init_status()) + #time.sleep(1) + #print("_sys_auto_status", self._sys_auto_status()) + #time.sleep(1) + #print("data_axis_y_pos", self.data_axis_y_pos) + #time.sleep(1) + #self.success = False + #with open('action_device_stop.json', 'r', encoding='utf-8') as f: + # action_json = json.load(f) + #self.client.execute_procedure_from_json(action_json) + #self.success = True + #return self.success + + def fun_wuliao_test(self) -> bool: + #找到data_init中构建的2个物料盘 + #liaopan1 = self.deck.get_resource("liaopan1") + #liaopan4 = self.deck.get_resource("liaopan4") + #for coin_num_N in range(16): + # liaopan1 = self.deck.get_resource("liaopan1") + # liaopan4 = self.deck.get_resource("liaopan4") + # jipian1 = liaopan1.children[coin_num_N].children[0] + # jipian4 = liaopan4.children[coin_num_N].children[0] + # #print(jipian1) + # #从料盘上去物料解绑后放到另一盘上 + # jipian1.parent.unassign_child_resource(jipian1) + # jipian4.parent.unassign_child_resource(jipian4) + # + # #print(jipian2.parent) + # battery = Battery(name = f"battery_{coin_num_N}") + # battery.assign_child_resource(jipian1, location=None) + # battery.assign_child_resource(jipian4, location=None) + # + # zidanjia6 = self.deck.get_resource("zi_dan_jia6") + # zidanjia6.children[0].assign_child_resource(battery, location=None) + # ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + # "resources": [self.deck] + # }) + # time.sleep(2) + for i in range(20): + print(f"输出{i}") + time.sleep(2) + + + # 数据读取与输出 + def func_read_data_and_output(self, file_path: str="D:\\coin_cell_data"): + # 检查CSV导出是否正在运行,已运行则跳出,防止同时启动两个while循环 + if self.csv_export_running: + return False, "读取已在运行中" + + #若不存在该目录则创建 + if not os.path.exists(file_path): + os.makedirs(file_path) + print(f"创建目录: {file_path}") + + # 只要允许读取标志位为true,就持续运行该函数,直到触发停止条件 + while self.allow_data_read: + + #函数运行标志位,确保只同时启动一个导出函数 + self.csv_export_running = True + + #等待接收结果标志位置True + while self.request_send_msg_status == False: + print("waiting for send_msg_status to True") + time.sleep(1) + #日期时间戳用于按天存放csv文件 + time_date = datetime.now().strftime("%Y%m%d") + #秒级时间戳用于标记每一行电池数据 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + #生成输出文件的变量 + self.csv_export_file = os.path.join(file_path, f"date_{time_date}.csv") + + #接收信息 + data_open_circuit_voltage = self.data_open_circuit_voltage + data_pole_weight = self.data_pole_weight + 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_electrolyte_code = self.data_electrolyte_code + data_coin_cell_code = self.data_coin_cell_code + # 电解液瓶位置 + elec_bottle_site = 2 + # 极片夹取位置(应当通过寄存器读光标) + Pos_elec_site = 0 + Al_elec_site = 0 + Gasket_site = 0 + + #接收完信息后,读取完毕标志位置True + self._unilab_rec_msg_succ_cmd()# = True + #等待允许读取标志位置False + while self.request_send_msg_status == True: + print("waiting for send_msg_status to False") + time.sleep(1) + self._unilab_rec_msg_succ_cmd()# = False + + #此处操作物料信息(如果中途报错停止,如何) + #报错怎么办(加个判断标志位,如果发生错误,则根据停止位置扣除物料) + #根据物料光标判断取哪个物料(人工摆盘,电解液瓶,移液枪头都有光标位置,寄存器读即可) + + #物料读取操作写在这里 + #在这里进行物料调取 + #转移物料瓶,elec_bottle_site对应第几瓶电解液(从依华寄存器读取) + # transfer_bottle(deck, elec_bottle_site) + # #找到电解液瓶的对象 + # electrolyte_rack = deck.get_resource("electrolyte_rack") + # pending_positions = electrolyte_rack.get_pending_positions()[elec_bottle_site] + # # TODO: 瓶子取液体操作需要加入 +# +# + # #找到压制工站对应的对象 + # battery_press_slot = deck.get_resource("battery_press_1") + # #创建一个新电池 + # test_battery = Battery( + # name=f"test_battery_{data_coin_num}", + # diameter=20.0, # 与压制槽直径匹配 + # height=3.0, # 电池高度 + # max_volume=100.0, # 100μL容量 + # barcode=data_coin_cell_code, # 电池条码 + # ) + # if battery_press_slot.has_battery(): + # return False, "压制工站已有电池,无法放置新电池" + # #在压制位放置电池 + # battery_press_slot.place_battery(test_battery) + # #从第一个子弹夹中取料 + # clip_magazine_1_hole = self.deck.get_resource("clip_magazine_1").get_item(Pos_elec_site) + # clip_magazine_2_hole = self.deck.get_resource("clip_magazine_2").get_item(Al_elec_site) + # clip_magazine_3_hole = self.deck.get_resource("clip_magazine_3").get_item(Gasket_site) + # + # if clip_magazine_1_hole.get_sheet_count() > 0: # 检查洞位是否有极片 + # electrode_sheet_1 = clip_magazine_1_hole.take_sheet() # 从洞位取出极片 + # test_battery.add_electrode_sheet(electrode_sheet_1) # 添加到电池中 + # print(f"已将极片 {electrode_sheet_1.name} 从子弹夹转移到电池") + # else: + # print("子弹夹洞位0没有极片") +# + # if clip_magazine_2_hole.get_sheet_count() > 0: # 检查洞位是否有极片 + # electrode_sheet_2 = clip_magazine_2_hole.take_sheet() # 从洞位取出极片 + # test_battery.add_electrode_sheet(electrode_sheet_2) # 添加到电池中 + # print(f"已将极片 {electrode_sheet_2.name} 从子弹夹转移到电池") + # else: + # print("子弹夹洞位0没有极片") +# + # if clip_magazine_3_hole.get_sheet_count() > 0: # 检查洞位是否有极片 + # electrode_sheet_3 = clip_magazine_3_hole.take_sheet() # 从洞位取出极片 + # test_battery.add_electrode_sheet(electrode_sheet_3) # 添加到电池中 + # print(f"已将极片 {electrode_sheet_3.name} 从子弹夹转移到电池") + # else: + # print("子弹夹洞位0没有极片") + # + # # TODO:#把电解液从瓶中取到电池夹子中 + # battery_site = deck.get_resource("battery_press_1") + # clip_magazine_battery = deck.get_resource("clip_magazine_battery") + # if battery_site.has_battery(): + # battery = battery_site.take_battery() #从压制槽取出电池 + # clip_magazine_battery.add_battery(battery) #从压制槽取出电池 +# +# +# +# + # # 保存配置到文件 + # self.deck.save("button_battery_station_layout.json", indent=2) + # print("\n台面配置已保存到: button_battery_station_layout.json") + # + # # 保存状态到文件 + # self.deck.save_state_to_file("button_battery_station_state.json", indent=2) + # print("台面状态已保存到: button_battery_station_state.json") + + + + + + + #将数据写入csv中 + #如当前目录下无同名文件则新建一个csv用于存放数据 + if not os.path.exists(self.csv_export_file): + #创建一个表头 + with open(self.csv_export_file, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile) + writer.writerow([ + 'Time', 'open_circuit_voltage', 'pole_weight', + 'assembly_time', 'assembly_pressure', 'electrolyte_volume', + 'coin_num', 'electrolyte_code', 'coin_cell_code' + ]) + #立刻写入磁盘 + csvfile.flush() + #开始追加电池信息 + with open(self.csv_export_file, 'a', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile) + 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 + ]) + #立刻写入磁盘 + csvfile.flush() + + # 只要不在自动模式运行中,就将允许标志位置False + if self.sys_auto_status == False or self.sys_start_status == False: + self.allow_data_read = False + self.csv_export_running = False + time.sleep(1) + + def func_stop_read_data(self): + """停止CSV导出""" + if not self.csv_export_running: + return False, "read data未在运行" + + self.csv_export_running = False + self.allow_data_read = False + + if self.csv_export_thread and self.csv_export_thread.is_alive(): + self.csv_export_thread.join(timeout=5) + + def func_get_csv_export_status(self): + """获取CSV导出状态""" + return { + 'allow_read': self.allow_data_read, + 'running': self.csv_export_running, + 'thread_alive': self.csv_export_thread.is_alive() if self.csv_export_thread else False + } + + + ''' + # ===================== 物料管理区 ====================== + @property + def data_material_inventory(self) -> int: + """主物料库存 (数量, INT16)""" + inventory, read_err = self.client.use_node('REG_DATA_MATERIAL_INVENTORY').read(1) + return inventory + + @property + def data_tips_inventory(self) -> int: + """移液枪头库存 (数量, INT16)""" + inventory, read_err = self.client.register_node_list(self.nodes).use_node('REG_DATA_TIPS_INVENTORY').read(1) + return inventory + + ''' + + +if __name__ == "__main__": + from pylabrobot.resources import Resource + Coin_Cell = CoinCellAssemblyWorkstation(Resource("1", 1, 1, 1), debug_mode=True) + #Coin_Cell.func_pack_device_init() + #Coin_Cell.func_pack_device_auto() + #Coin_Cell.func_pack_device_start() + #Coin_Cell.func_pack_send_bottle_num(2) + #Coin_Cell.func_pack_send_msg_cmd(2) + #Coin_Cell.func_pack_get_msg_cmd() + #Coin_Cell.func_pack_get_msg_cmd() + #Coin_Cell.func_pack_send_finished_cmd() +# + #Coin_Cell.func_allpack_cmd(3, 2) + #print(Coin_Cell.data_stack_vision_code) + #print("success") + #创建一个物料台面 + + #deck = create_a_coin_cell_deck() + + ##在台面上找到料盘和极片 + #liaopan1 = deck.get_resource("liaopan1") + #liaopan2 = deck.get_resource("liaopan2") + #jipian1 = liaopan1.children[1].children[0] +# + ##print(jipian1) + ##把物料解绑后放到另一盘上 + #jipian1.parent.unassign_child_resource(jipian1) + #liaopan2.children[1].assign_child_resource(jipian1, location=None) + ##print(jipian2.parent) + from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type + + with open("./button_battery_decks_unilab.json", "r", encoding="utf-8") as f: + bioyond_resources_unilab = json.load(f) + print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源") + ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource]) + print(f"转换结果类型: {type(ulab_resources)}") + print(ulab_resources) + diff --git a/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json b/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json new file mode 100644 index 00000000..7e371327 --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json @@ -0,0 +1,14472 @@ +{ + "nodes": [ + { + "id": "BatteryStation", + "name": "扣电工作站", + "children": [ + "coin_cell_deck" + ], + "parent": null, + "type": "device", + "class": "bettery_station_registry", + "position": { + "x": 600, + "y": 400, + "z": 0 + }, + "config": { + "debug_mode": false, + "_comment": "protocol_type接外部工站固定写法字段,一般为空,deck写法也固定", + "protocol_type": [], + "deck": { + "data": { + "_resource_child_name": "coin_cell_deck", + "_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck" + } + }, + + "address": "192.168.1.20", + "port": 502 + }, + "data": {} + }, + { + "id": "coin_cell_deck", + "name": "coin_cell_deck", + "sample_id": null, + "children": [ + "zi_dan_jia", + "zi_dan_jia2", + "zi_dan_jia3", + "zi_dan_jia4", + "zi_dan_jia5", + "zi_dan_jia6", + "zi_dan_jia7", + "zi_dan_jia8", + "liaopan1", + "liaopan2", + "liaopan3", + "liaopan4", + "liaopan5", + "liaopan6", + "bottle_rack_3x4", + "bottle_rack_6x2", + "bottle_rack_6x2_2", + "tip_box_64", + "waste_tip_box" + ], + "parent": null, + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "CoincellDeck", + "size_x": 1620.0, + "size_y": 1270.0, + "size_z": 500.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "coin_cell_deck", + "barcode": null + }, + "data": {} + }, + { + "id": "zi_dan_jia", + "name": "zi_dan_jia", + "sample_id": null, + "children": [ + "zi_dan_jia_clipmagazinehole_0_0", + "zi_dan_jia_clipmagazinehole_0_1", + "zi_dan_jia_clipmagazinehole_1_0", + "zi_dan_jia_clipmagazinehole_1_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1400, + "y": 50, + "z": 0 + }, + "config": { + "type": "ClipMagazine_four", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_four", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia_clipmagazinehole_0_0", + "B1": "zi_dan_jia_clipmagazinehole_0_1", + "A2": "zi_dan_jia_clipmagazinehole_1_0", + "B2": "zi_dan_jia_clipmagazinehole_1_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia_clipmagazinehole_0_0", + "name": "zi_dan_jia_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia2_jipian_0" + ], + "parent": "zi_dan_jia", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia2_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia2_jipian_0", + "name": "zi_dan_jia2_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia_clipmagazinehole_0_1", + "name": "zi_dan_jia_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia2_jipian_1" + ], + "parent": "zi_dan_jia", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia2_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia2_jipian_1", + "name": "zi_dan_jia2_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia_clipmagazinehole_1_0", + "name": "zi_dan_jia_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia2_jipian_2" + ], + "parent": "zi_dan_jia", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia2_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia2_jipian_2", + "name": "zi_dan_jia2_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia_clipmagazinehole_1_1", + "name": "zi_dan_jia_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia2_jipian_3" + ], + "parent": "zi_dan_jia", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia2_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia2_jipian_3", + "name": "zi_dan_jia2_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia2", + "name": "zi_dan_jia2", + "sample_id": null, + "children": [ + "zi_dan_jia2_clipmagazinehole_0_0", + "zi_dan_jia2_clipmagazinehole_0_1", + "zi_dan_jia2_clipmagazinehole_1_0", + "zi_dan_jia2_clipmagazinehole_1_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1600, + "y": 200, + "z": 0 + }, + "config": { + "type": "ClipMagazine_four", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_four", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia2_clipmagazinehole_0_0", + "B1": "zi_dan_jia2_clipmagazinehole_0_1", + "A2": "zi_dan_jia2_clipmagazinehole_1_0", + "B2": "zi_dan_jia2_clipmagazinehole_1_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia2_clipmagazinehole_0_0", + "name": "zi_dan_jia2_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia_jipian_0" + ], + "parent": "zi_dan_jia2", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia2_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia_jipian_0", + "name": "zi_dan_jia_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia2_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia2_clipmagazinehole_0_1", + "name": "zi_dan_jia2_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia_jipian_1" + ], + "parent": "zi_dan_jia2", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia2_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia_jipian_1", + "name": "zi_dan_jia_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia2_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia2_clipmagazinehole_1_0", + "name": "zi_dan_jia2_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia_jipian_2" + ], + "parent": "zi_dan_jia2", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia2_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia_jipian_2", + "name": "zi_dan_jia_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia2_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia2_clipmagazinehole_1_1", + "name": "zi_dan_jia2_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia_jipian_3" + ], + "parent": "zi_dan_jia2", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia2_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia_jipian_3", + "name": "zi_dan_jia_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia2_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia3", + "name": "zi_dan_jia3", + "sample_id": null, + "children": [ + "zi_dan_jia3_clipmagazinehole_0_0", + "zi_dan_jia3_clipmagazinehole_0_1", + "zi_dan_jia3_clipmagazinehole_1_0", + "zi_dan_jia3_clipmagazinehole_1_1", + "zi_dan_jia3_clipmagazinehole_2_0", + "zi_dan_jia3_clipmagazinehole_2_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1500, + "y": 200, + "z": 0 + }, + "config": { + "type": "ClipMagazine", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia3_clipmagazinehole_0_0", + "B1": "zi_dan_jia3_clipmagazinehole_0_1", + "A2": "zi_dan_jia3_clipmagazinehole_1_0", + "B2": "zi_dan_jia3_clipmagazinehole_1_1", + "A3": "zi_dan_jia3_clipmagazinehole_2_0", + "B3": "zi_dan_jia3_clipmagazinehole_2_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia3_clipmagazinehole_0_0", + "name": "zi_dan_jia3_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia3_jipian_0" + ], + "parent": "zi_dan_jia3", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia3_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia3_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia3_jipian_0", + "name": "zi_dan_jia3_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia3_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia3_clipmagazinehole_0_1", + "name": "zi_dan_jia3_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia3_jipian_1" + ], + "parent": "zi_dan_jia3", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia3_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia3_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia3_jipian_1", + "name": "zi_dan_jia3_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia3_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia3_clipmagazinehole_1_0", + "name": "zi_dan_jia3_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia3_jipian_2" + ], + "parent": "zi_dan_jia3", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia3_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia3_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia3_jipian_2", + "name": "zi_dan_jia3_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia3_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia3_clipmagazinehole_1_1", + "name": "zi_dan_jia3_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia3_jipian_3" + ], + "parent": "zi_dan_jia3", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia3_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia3_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia3_jipian_3", + "name": "zi_dan_jia3_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia3_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia3_clipmagazinehole_2_0", + "name": "zi_dan_jia3_clipmagazinehole_2_0", + "sample_id": null, + "children": [ + "zi_dan_jia3_jipian_4" + ], + "parent": "zi_dan_jia3", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia3_jipian_4", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia3_clipmagazinehole_2_0" + } + ] + } + }, + { + "id": "zi_dan_jia3_jipian_4", + "name": "zi_dan_jia3_jipian_4", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia3_clipmagazinehole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia3_clipmagazinehole_2_1", + "name": "zi_dan_jia3_clipmagazinehole_2_1", + "sample_id": null, + "children": [ + "zi_dan_jia3_jipian_5" + ], + "parent": "zi_dan_jia3", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia3_jipian_5", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia3_clipmagazinehole_2_1" + } + ] + } + }, + { + "id": "zi_dan_jia3_jipian_5", + "name": "zi_dan_jia3_jipian_5", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia3_clipmagazinehole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia4", + "name": "zi_dan_jia4", + "sample_id": null, + "children": [ + "zi_dan_jia4_clipmagazinehole_0_0", + "zi_dan_jia4_clipmagazinehole_0_1", + "zi_dan_jia4_clipmagazinehole_1_0", + "zi_dan_jia4_clipmagazinehole_1_1", + "zi_dan_jia4_clipmagazinehole_2_0", + "zi_dan_jia4_clipmagazinehole_2_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1500, + "y": 300, + "z": 0 + }, + "config": { + "type": "ClipMagazine", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia4_clipmagazinehole_0_0", + "B1": "zi_dan_jia4_clipmagazinehole_0_1", + "A2": "zi_dan_jia4_clipmagazinehole_1_0", + "B2": "zi_dan_jia4_clipmagazinehole_1_1", + "A3": "zi_dan_jia4_clipmagazinehole_2_0", + "B3": "zi_dan_jia4_clipmagazinehole_2_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia4_clipmagazinehole_0_0", + "name": "zi_dan_jia4_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia4_jipian_0" + ], + "parent": "zi_dan_jia4", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia4_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia4_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia4_jipian_0", + "name": "zi_dan_jia4_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia4_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia4_clipmagazinehole_0_1", + "name": "zi_dan_jia4_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia4_jipian_1" + ], + "parent": "zi_dan_jia4", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia4_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia4_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia4_jipian_1", + "name": "zi_dan_jia4_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia4_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia4_clipmagazinehole_1_0", + "name": "zi_dan_jia4_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia4_jipian_2" + ], + "parent": "zi_dan_jia4", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia4_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia4_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia4_jipian_2", + "name": "zi_dan_jia4_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia4_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia4_clipmagazinehole_1_1", + "name": "zi_dan_jia4_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia4_jipian_3" + ], + "parent": "zi_dan_jia4", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia4_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia4_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia4_jipian_3", + "name": "zi_dan_jia4_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia4_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia4_clipmagazinehole_2_0", + "name": "zi_dan_jia4_clipmagazinehole_2_0", + "sample_id": null, + "children": [ + "zi_dan_jia4_jipian_4" + ], + "parent": "zi_dan_jia4", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia4_jipian_4", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia4_clipmagazinehole_2_0" + } + ] + } + }, + { + "id": "zi_dan_jia4_jipian_4", + "name": "zi_dan_jia4_jipian_4", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia4_clipmagazinehole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia4_clipmagazinehole_2_1", + "name": "zi_dan_jia4_clipmagazinehole_2_1", + "sample_id": null, + "children": [ + "zi_dan_jia4_jipian_5" + ], + "parent": "zi_dan_jia4", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia4_jipian_5", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia4_clipmagazinehole_2_1" + } + ] + } + }, + { + "id": "zi_dan_jia4_jipian_5", + "name": "zi_dan_jia4_jipian_5", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia4_clipmagazinehole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia5", + "name": "zi_dan_jia5", + "sample_id": null, + "children": [ + "zi_dan_jia5_clipmagazinehole_0_0", + "zi_dan_jia5_clipmagazinehole_0_1", + "zi_dan_jia5_clipmagazinehole_1_0", + "zi_dan_jia5_clipmagazinehole_1_1", + "zi_dan_jia5_clipmagazinehole_2_0", + "zi_dan_jia5_clipmagazinehole_2_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1600, + "y": 300, + "z": 0 + }, + "config": { + "type": "ClipMagazine", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia5_clipmagazinehole_0_0", + "B1": "zi_dan_jia5_clipmagazinehole_0_1", + "A2": "zi_dan_jia5_clipmagazinehole_1_0", + "B2": "zi_dan_jia5_clipmagazinehole_1_1", + "A3": "zi_dan_jia5_clipmagazinehole_2_0", + "B3": "zi_dan_jia5_clipmagazinehole_2_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia5_clipmagazinehole_0_0", + "name": "zi_dan_jia5_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia5_jipian_0" + ], + "parent": "zi_dan_jia5", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia5_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia5_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia5_jipian_0", + "name": "zi_dan_jia5_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia5_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia5_clipmagazinehole_0_1", + "name": "zi_dan_jia5_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia5_jipian_1" + ], + "parent": "zi_dan_jia5", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia5_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia5_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia5_jipian_1", + "name": "zi_dan_jia5_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia5_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia5_clipmagazinehole_1_0", + "name": "zi_dan_jia5_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia5_jipian_2" + ], + "parent": "zi_dan_jia5", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia5_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia5_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia5_jipian_2", + "name": "zi_dan_jia5_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia5_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia5_clipmagazinehole_1_1", + "name": "zi_dan_jia5_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia5_jipian_3" + ], + "parent": "zi_dan_jia5", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia5_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia5_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia5_jipian_3", + "name": "zi_dan_jia5_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia5_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia5_clipmagazinehole_2_0", + "name": "zi_dan_jia5_clipmagazinehole_2_0", + "sample_id": null, + "children": [ + "zi_dan_jia5_jipian_4" + ], + "parent": "zi_dan_jia5", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia5_jipian_4", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia5_clipmagazinehole_2_0" + } + ] + } + }, + { + "id": "zi_dan_jia5_jipian_4", + "name": "zi_dan_jia5_jipian_4", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia5_clipmagazinehole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia5_clipmagazinehole_2_1", + "name": "zi_dan_jia5_clipmagazinehole_2_1", + "sample_id": null, + "children": [ + "zi_dan_jia5_jipian_5" + ], + "parent": "zi_dan_jia5", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia5_jipian_5", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia5_clipmagazinehole_2_1" + } + ] + } + }, + { + "id": "zi_dan_jia5_jipian_5", + "name": "zi_dan_jia5_jipian_5", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia5_clipmagazinehole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia6", + "name": "zi_dan_jia6", + "sample_id": null, + "children": [ + "zi_dan_jia6_clipmagazinehole_0_0", + "zi_dan_jia6_clipmagazinehole_0_1", + "zi_dan_jia6_clipmagazinehole_1_0", + "zi_dan_jia6_clipmagazinehole_1_1", + "zi_dan_jia6_clipmagazinehole_2_0", + "zi_dan_jia6_clipmagazinehole_2_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1530, + "y": 500, + "z": 0 + }, + "config": { + "type": "ClipMagazine", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia6_clipmagazinehole_0_0", + "B1": "zi_dan_jia6_clipmagazinehole_0_1", + "A2": "zi_dan_jia6_clipmagazinehole_1_0", + "B2": "zi_dan_jia6_clipmagazinehole_1_1", + "A3": "zi_dan_jia6_clipmagazinehole_2_0", + "B3": "zi_dan_jia6_clipmagazinehole_2_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia6_clipmagazinehole_0_0", + "name": "zi_dan_jia6_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia6_jipian_0" + ], + "parent": "zi_dan_jia6", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia6_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia6_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia6_jipian_0", + "name": "zi_dan_jia6_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia6_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia6_clipmagazinehole_0_1", + "name": "zi_dan_jia6_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia6_jipian_1" + ], + "parent": "zi_dan_jia6", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia6_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia6_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia6_jipian_1", + "name": "zi_dan_jia6_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia6_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia6_clipmagazinehole_1_0", + "name": "zi_dan_jia6_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia6_jipian_2" + ], + "parent": "zi_dan_jia6", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia6_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia6_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia6_jipian_2", + "name": "zi_dan_jia6_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia6_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia6_clipmagazinehole_1_1", + "name": "zi_dan_jia6_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia6_jipian_3" + ], + "parent": "zi_dan_jia6", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia6_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia6_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia6_jipian_3", + "name": "zi_dan_jia6_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia6_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia6_clipmagazinehole_2_0", + "name": "zi_dan_jia6_clipmagazinehole_2_0", + "sample_id": null, + "children": [ + "zi_dan_jia6_jipian_4" + ], + "parent": "zi_dan_jia6", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia6_jipian_4", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia6_clipmagazinehole_2_0" + } + ] + } + }, + { + "id": "zi_dan_jia6_jipian_4", + "name": "zi_dan_jia6_jipian_4", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia6_clipmagazinehole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia6_clipmagazinehole_2_1", + "name": "zi_dan_jia6_clipmagazinehole_2_1", + "sample_id": null, + "children": [ + "zi_dan_jia6_jipian_5" + ], + "parent": "zi_dan_jia6", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia6_jipian_5", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia6_clipmagazinehole_2_1" + } + ] + } + }, + { + "id": "zi_dan_jia6_jipian_5", + "name": "zi_dan_jia6_jipian_5", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia6_clipmagazinehole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia7", + "name": "zi_dan_jia7", + "sample_id": null, + "children": [ + "zi_dan_jia7_clipmagazinehole_0_0", + "zi_dan_jia7_clipmagazinehole_0_1", + "zi_dan_jia7_clipmagazinehole_1_0", + "zi_dan_jia7_clipmagazinehole_1_1", + "zi_dan_jia7_clipmagazinehole_2_0", + "zi_dan_jia7_clipmagazinehole_2_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1180, + "y": 400, + "z": 0 + }, + "config": { + "type": "ClipMagazine", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia7_clipmagazinehole_0_0", + "B1": "zi_dan_jia7_clipmagazinehole_0_1", + "A2": "zi_dan_jia7_clipmagazinehole_1_0", + "B2": "zi_dan_jia7_clipmagazinehole_1_1", + "A3": "zi_dan_jia7_clipmagazinehole_2_0", + "B3": "zi_dan_jia7_clipmagazinehole_2_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia7_clipmagazinehole_0_0", + "name": "zi_dan_jia7_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia7_jipian_0" + ], + "parent": "zi_dan_jia7", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia7_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia7_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia7_jipian_0", + "name": "zi_dan_jia7_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia7_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia7_clipmagazinehole_0_1", + "name": "zi_dan_jia7_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia7_jipian_1" + ], + "parent": "zi_dan_jia7", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia7_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia7_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia7_jipian_1", + "name": "zi_dan_jia7_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia7_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia7_clipmagazinehole_1_0", + "name": "zi_dan_jia7_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia7_jipian_2" + ], + "parent": "zi_dan_jia7", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia7_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia7_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia7_jipian_2", + "name": "zi_dan_jia7_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia7_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia7_clipmagazinehole_1_1", + "name": "zi_dan_jia7_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia7_jipian_3" + ], + "parent": "zi_dan_jia7", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia7_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia7_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia7_jipian_3", + "name": "zi_dan_jia7_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia7_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia7_clipmagazinehole_2_0", + "name": "zi_dan_jia7_clipmagazinehole_2_0", + "sample_id": null, + "children": [ + "zi_dan_jia7_jipian_4" + ], + "parent": "zi_dan_jia7", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia7_jipian_4", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia7_clipmagazinehole_2_0" + } + ] + } + }, + { + "id": "zi_dan_jia7_jipian_4", + "name": "zi_dan_jia7_jipian_4", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia7_clipmagazinehole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia7_clipmagazinehole_2_1", + "name": "zi_dan_jia7_clipmagazinehole_2_1", + "sample_id": null, + "children": [ + "zi_dan_jia7_jipian_5" + ], + "parent": "zi_dan_jia7", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia7_jipian_5", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia7_clipmagazinehole_2_1" + } + ] + } + }, + { + "id": "zi_dan_jia7_jipian_5", + "name": "zi_dan_jia7_jipian_5", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia7_clipmagazinehole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia8", + "name": "zi_dan_jia8", + "sample_id": null, + "children": [ + "zi_dan_jia8_clipmagazinehole_0_0", + "zi_dan_jia8_clipmagazinehole_0_1", + "zi_dan_jia8_clipmagazinehole_1_0", + "zi_dan_jia8_clipmagazinehole_1_1", + "zi_dan_jia8_clipmagazinehole_2_0", + "zi_dan_jia8_clipmagazinehole_2_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1280, + "y": 400, + "z": 0 + }, + "config": { + "type": "ClipMagazine", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia8_clipmagazinehole_0_0", + "B1": "zi_dan_jia8_clipmagazinehole_0_1", + "A2": "zi_dan_jia8_clipmagazinehole_1_0", + "B2": "zi_dan_jia8_clipmagazinehole_1_1", + "A3": "zi_dan_jia8_clipmagazinehole_2_0", + "B3": "zi_dan_jia8_clipmagazinehole_2_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia8_clipmagazinehole_0_0", + "name": "zi_dan_jia8_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia8_jipian_0" + ], + "parent": "zi_dan_jia8", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia8_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia8_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia8_jipian_0", + "name": "zi_dan_jia8_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia8_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia8_clipmagazinehole_0_1", + "name": "zi_dan_jia8_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia8_jipian_1" + ], + "parent": "zi_dan_jia8", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia8_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia8_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia8_jipian_1", + "name": "zi_dan_jia8_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia8_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia8_clipmagazinehole_1_0", + "name": "zi_dan_jia8_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia8_jipian_2" + ], + "parent": "zi_dan_jia8", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia8_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia8_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia8_jipian_2", + "name": "zi_dan_jia8_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia8_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia8_clipmagazinehole_1_1", + "name": "zi_dan_jia8_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia8_jipian_3" + ], + "parent": "zi_dan_jia8", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia8_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia8_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia8_jipian_3", + "name": "zi_dan_jia8_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia8_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia8_clipmagazinehole_2_0", + "name": "zi_dan_jia8_clipmagazinehole_2_0", + "sample_id": null, + "children": [ + "zi_dan_jia8_jipian_4" + ], + "parent": "zi_dan_jia8", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia8_jipian_4", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia8_clipmagazinehole_2_0" + } + ] + } + }, + { + "id": "zi_dan_jia8_jipian_4", + "name": "zi_dan_jia8_jipian_4", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia8_clipmagazinehole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia8_clipmagazinehole_2_1", + "name": "zi_dan_jia8_clipmagazinehole_2_1", + "sample_id": null, + "children": [ + "zi_dan_jia8_jipian_5" + ], + "parent": "zi_dan_jia8", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia8_jipian_5", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia8_clipmagazinehole_2_1" + } + ] + } + }, + { + "id": "zi_dan_jia8_jipian_5", + "name": "zi_dan_jia8_jipian_5", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia8_clipmagazinehole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1", + "name": "liaopan1", + "sample_id": null, + "children": [ + "liaopan1_materialhole_0_0", + "liaopan1_materialhole_0_1", + "liaopan1_materialhole_0_2", + "liaopan1_materialhole_0_3", + "liaopan1_materialhole_1_0", + "liaopan1_materialhole_1_1", + "liaopan1_materialhole_1_2", + "liaopan1_materialhole_1_3", + "liaopan1_materialhole_2_0", + "liaopan1_materialhole_2_1", + "liaopan1_materialhole_2_2", + "liaopan1_materialhole_2_3", + "liaopan1_materialhole_3_0", + "liaopan1_materialhole_3_1", + "liaopan1_materialhole_3_2", + "liaopan1_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1010, + "y": 50, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120, + "size_y": 100, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan1_materialhole_0_0", + "B1": "liaopan1_materialhole_0_1", + "C1": "liaopan1_materialhole_0_2", + "D1": "liaopan1_materialhole_0_3", + "A2": "liaopan1_materialhole_1_0", + "B2": "liaopan1_materialhole_1_1", + "C2": "liaopan1_materialhole_1_2", + "D2": "liaopan1_materialhole_1_3", + "A3": "liaopan1_materialhole_2_0", + "B3": "liaopan1_materialhole_2_1", + "C3": "liaopan1_materialhole_2_2", + "D3": "liaopan1_materialhole_2_3", + "A4": "liaopan1_materialhole_3_0", + "B4": "liaopan1_materialhole_3_1", + "C4": "liaopan1_materialhole_3_2", + "D4": "liaopan1_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan1_materialhole_0_0", + "name": "liaopan1_materialhole_0_0", + "sample_id": null, + "children": [ + "liaopan1_jipian_0" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_0", + "name": "liaopan1_jipian_0", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_0_1", + "name": "liaopan1_materialhole_0_1", + "sample_id": null, + "children": [ + "liaopan1_jipian_1" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_1", + "name": "liaopan1_jipian_1", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_0_2", + "name": "liaopan1_materialhole_0_2", + "sample_id": null, + "children": [ + "liaopan1_jipian_2" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_2", + "name": "liaopan1_jipian_2", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_0_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_0_3", + "name": "liaopan1_materialhole_0_3", + "sample_id": null, + "children": [ + "liaopan1_jipian_3" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_3", + "name": "liaopan1_jipian_3", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_0_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_1_0", + "name": "liaopan1_materialhole_1_0", + "sample_id": null, + "children": [ + "liaopan1_jipian_4" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_4", + "name": "liaopan1_jipian_4", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_1_1", + "name": "liaopan1_materialhole_1_1", + "sample_id": null, + "children": [ + "liaopan1_jipian_5" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_5", + "name": "liaopan1_jipian_5", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_1_2", + "name": "liaopan1_materialhole_1_2", + "sample_id": null, + "children": [ + "liaopan1_jipian_6" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_6", + "name": "liaopan1_jipian_6", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_1_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_1_3", + "name": "liaopan1_materialhole_1_3", + "sample_id": null, + "children": [ + "liaopan1_jipian_7" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_7", + "name": "liaopan1_jipian_7", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_1_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_2_0", + "name": "liaopan1_materialhole_2_0", + "sample_id": null, + "children": [ + "liaopan1_jipian_8" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_8", + "name": "liaopan1_jipian_8", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_2_1", + "name": "liaopan1_materialhole_2_1", + "sample_id": null, + "children": [ + "liaopan1_jipian_9" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_9", + "name": "liaopan1_jipian_9", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_2_2", + "name": "liaopan1_materialhole_2_2", + "sample_id": null, + "children": [ + "liaopan1_jipian_10" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_10", + "name": "liaopan1_jipian_10", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_2_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_2_3", + "name": "liaopan1_materialhole_2_3", + "sample_id": null, + "children": [ + "liaopan1_jipian_11" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_11", + "name": "liaopan1_jipian_11", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_2_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_3_0", + "name": "liaopan1_materialhole_3_0", + "sample_id": null, + "children": [ + "liaopan1_jipian_12" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_12", + "name": "liaopan1_jipian_12", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_3_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_3_1", + "name": "liaopan1_materialhole_3_1", + "sample_id": null, + "children": [ + "liaopan1_jipian_13" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_13", + "name": "liaopan1_jipian_13", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_3_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_3_2", + "name": "liaopan1_materialhole_3_2", + "sample_id": null, + "children": [ + "liaopan1_jipian_14" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_14", + "name": "liaopan1_jipian_14", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_3_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_3_3", + "name": "liaopan1_materialhole_3_3", + "sample_id": null, + "children": [ + "liaopan1_jipian_15" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_15", + "name": "liaopan1_jipian_15", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_3_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2", + "name": "liaopan2", + "sample_id": null, + "children": [ + "liaopan2_materialhole_0_0", + "liaopan2_materialhole_0_1", + "liaopan2_materialhole_0_2", + "liaopan2_materialhole_0_3", + "liaopan2_materialhole_1_0", + "liaopan2_materialhole_1_1", + "liaopan2_materialhole_1_2", + "liaopan2_materialhole_1_3", + "liaopan2_materialhole_2_0", + "liaopan2_materialhole_2_1", + "liaopan2_materialhole_2_2", + "liaopan2_materialhole_2_3", + "liaopan2_materialhole_3_0", + "liaopan2_materialhole_3_1", + "liaopan2_materialhole_3_2", + "liaopan2_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1130, + "y": 50, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120, + "size_y": 100, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan2_materialhole_0_0", + "B1": "liaopan2_materialhole_0_1", + "C1": "liaopan2_materialhole_0_2", + "D1": "liaopan2_materialhole_0_3", + "A2": "liaopan2_materialhole_1_0", + "B2": "liaopan2_materialhole_1_1", + "C2": "liaopan2_materialhole_1_2", + "D2": "liaopan2_materialhole_1_3", + "A3": "liaopan2_materialhole_2_0", + "B3": "liaopan2_materialhole_2_1", + "C3": "liaopan2_materialhole_2_2", + "D3": "liaopan2_materialhole_2_3", + "A4": "liaopan2_materialhole_3_0", + "B4": "liaopan2_materialhole_3_1", + "C4": "liaopan2_materialhole_3_2", + "D4": "liaopan2_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan2_materialhole_0_0", + "name": "liaopan2_materialhole_0_0", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_0_1", + "name": "liaopan2_materialhole_0_1", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_0_2", + "name": "liaopan2_materialhole_0_2", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_0_3", + "name": "liaopan2_materialhole_0_3", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_1_0", + "name": "liaopan2_materialhole_1_0", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_1_1", + "name": "liaopan2_materialhole_1_1", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_1_2", + "name": "liaopan2_materialhole_1_2", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_1_3", + "name": "liaopan2_materialhole_1_3", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_2_0", + "name": "liaopan2_materialhole_2_0", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_2_1", + "name": "liaopan2_materialhole_2_1", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_2_2", + "name": "liaopan2_materialhole_2_2", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_2_3", + "name": "liaopan2_materialhole_2_3", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_3_0", + "name": "liaopan2_materialhole_3_0", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_3_1", + "name": "liaopan2_materialhole_3_1", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_3_2", + "name": "liaopan2_materialhole_3_2", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_3_3", + "name": "liaopan2_materialhole_3_3", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3", + "name": "liaopan3", + "sample_id": null, + "children": [ + "liaopan3_materialhole_0_0", + "liaopan3_materialhole_0_1", + "liaopan3_materialhole_0_2", + "liaopan3_materialhole_0_3", + "liaopan3_materialhole_1_0", + "liaopan3_materialhole_1_1", + "liaopan3_materialhole_1_2", + "liaopan3_materialhole_1_3", + "liaopan3_materialhole_2_0", + "liaopan3_materialhole_2_1", + "liaopan3_materialhole_2_2", + "liaopan3_materialhole_2_3", + "liaopan3_materialhole_3_0", + "liaopan3_materialhole_3_1", + "liaopan3_materialhole_3_2", + "liaopan3_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1250, + "y": 50, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120, + "size_y": 100, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan3_materialhole_0_0", + "B1": "liaopan3_materialhole_0_1", + "C1": "liaopan3_materialhole_0_2", + "D1": "liaopan3_materialhole_0_3", + "A2": "liaopan3_materialhole_1_0", + "B2": "liaopan3_materialhole_1_1", + "C2": "liaopan3_materialhole_1_2", + "D2": "liaopan3_materialhole_1_3", + "A3": "liaopan3_materialhole_2_0", + "B3": "liaopan3_materialhole_2_1", + "C3": "liaopan3_materialhole_2_2", + "D3": "liaopan3_materialhole_2_3", + "A4": "liaopan3_materialhole_3_0", + "B4": "liaopan3_materialhole_3_1", + "C4": "liaopan3_materialhole_3_2", + "D4": "liaopan3_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan3_materialhole_0_0", + "name": "liaopan3_materialhole_0_0", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_0_1", + "name": "liaopan3_materialhole_0_1", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_0_2", + "name": "liaopan3_materialhole_0_2", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_0_3", + "name": "liaopan3_materialhole_0_3", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_1_0", + "name": "liaopan3_materialhole_1_0", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_1_1", + "name": "liaopan3_materialhole_1_1", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_1_2", + "name": "liaopan3_materialhole_1_2", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_1_3", + "name": "liaopan3_materialhole_1_3", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_2_0", + "name": "liaopan3_materialhole_2_0", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_2_1", + "name": "liaopan3_materialhole_2_1", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_2_2", + "name": "liaopan3_materialhole_2_2", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_2_3", + "name": "liaopan3_materialhole_2_3", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_3_0", + "name": "liaopan3_materialhole_3_0", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_3_1", + "name": "liaopan3_materialhole_3_1", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_3_2", + "name": "liaopan3_materialhole_3_2", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_3_3", + "name": "liaopan3_materialhole_3_3", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4", + "name": "liaopan4", + "sample_id": null, + "children": [ + "liaopan4_materialhole_0_0", + "liaopan4_materialhole_0_1", + "liaopan4_materialhole_0_2", + "liaopan4_materialhole_0_3", + "liaopan4_materialhole_1_0", + "liaopan4_materialhole_1_1", + "liaopan4_materialhole_1_2", + "liaopan4_materialhole_1_3", + "liaopan4_materialhole_2_0", + "liaopan4_materialhole_2_1", + "liaopan4_materialhole_2_2", + "liaopan4_materialhole_2_3", + "liaopan4_materialhole_3_0", + "liaopan4_materialhole_3_1", + "liaopan4_materialhole_3_2", + "liaopan4_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1010, + "y": 150, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120, + "size_y": 100, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan4_materialhole_0_0", + "B1": "liaopan4_materialhole_0_1", + "C1": "liaopan4_materialhole_0_2", + "D1": "liaopan4_materialhole_0_3", + "A2": "liaopan4_materialhole_1_0", + "B2": "liaopan4_materialhole_1_1", + "C2": "liaopan4_materialhole_1_2", + "D2": "liaopan4_materialhole_1_3", + "A3": "liaopan4_materialhole_2_0", + "B3": "liaopan4_materialhole_2_1", + "C3": "liaopan4_materialhole_2_2", + "D3": "liaopan4_materialhole_2_3", + "A4": "liaopan4_materialhole_3_0", + "B4": "liaopan4_materialhole_3_1", + "C4": "liaopan4_materialhole_3_2", + "D4": "liaopan4_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan4_materialhole_0_0", + "name": "liaopan4_materialhole_0_0", + "sample_id": null, + "children": [ + "liaopan4_jipian_0" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_0", + "name": "liaopan4_jipian_0", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_0_1", + "name": "liaopan4_materialhole_0_1", + "sample_id": null, + "children": [ + "liaopan4_jipian_1" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_1", + "name": "liaopan4_jipian_1", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_0_2", + "name": "liaopan4_materialhole_0_2", + "sample_id": null, + "children": [ + "liaopan4_jipian_2" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_2", + "name": "liaopan4_jipian_2", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_0_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_0_3", + "name": "liaopan4_materialhole_0_3", + "sample_id": null, + "children": [ + "liaopan4_jipian_3" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_3", + "name": "liaopan4_jipian_3", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_0_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_1_0", + "name": "liaopan4_materialhole_1_0", + "sample_id": null, + "children": [ + "liaopan4_jipian_4" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_4", + "name": "liaopan4_jipian_4", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_1_1", + "name": "liaopan4_materialhole_1_1", + "sample_id": null, + "children": [ + "liaopan4_jipian_5" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_5", + "name": "liaopan4_jipian_5", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_1_2", + "name": "liaopan4_materialhole_1_2", + "sample_id": null, + "children": [ + "liaopan4_jipian_6" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_6", + "name": "liaopan4_jipian_6", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_1_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_1_3", + "name": "liaopan4_materialhole_1_3", + "sample_id": null, + "children": [ + "liaopan4_jipian_7" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_7", + "name": "liaopan4_jipian_7", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_1_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_2_0", + "name": "liaopan4_materialhole_2_0", + "sample_id": null, + "children": [ + "liaopan4_jipian_8" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_8", + "name": "liaopan4_jipian_8", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_2_1", + "name": "liaopan4_materialhole_2_1", + "sample_id": null, + "children": [ + "liaopan4_jipian_9" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_9", + "name": "liaopan4_jipian_9", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_2_2", + "name": "liaopan4_materialhole_2_2", + "sample_id": null, + "children": [ + "liaopan4_jipian_10" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_10", + "name": "liaopan4_jipian_10", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_2_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_2_3", + "name": "liaopan4_materialhole_2_3", + "sample_id": null, + "children": [ + "liaopan4_jipian_11" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_11", + "name": "liaopan4_jipian_11", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_2_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_3_0", + "name": "liaopan4_materialhole_3_0", + "sample_id": null, + "children": [ + "liaopan4_jipian_12" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_12", + "name": "liaopan4_jipian_12", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_3_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_3_1", + "name": "liaopan4_materialhole_3_1", + "sample_id": null, + "children": [ + "liaopan4_jipian_13" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_13", + "name": "liaopan4_jipian_13", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_3_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_3_2", + "name": "liaopan4_materialhole_3_2", + "sample_id": null, + "children": [ + "liaopan4_jipian_14" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_14", + "name": "liaopan4_jipian_14", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_3_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_3_3", + "name": "liaopan4_materialhole_3_3", + "sample_id": null, + "children": [ + "liaopan4_jipian_15" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_15", + "name": "liaopan4_jipian_15", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_3_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan5", + "name": "liaopan5", + "sample_id": null, + "children": [ + "liaopan5_materialhole_0_0", + "liaopan5_materialhole_0_1", + "liaopan5_materialhole_0_2", + "liaopan5_materialhole_0_3", + "liaopan5_materialhole_1_0", + "liaopan5_materialhole_1_1", + "liaopan5_materialhole_1_2", + "liaopan5_materialhole_1_3", + "liaopan5_materialhole_2_0", + "liaopan5_materialhole_2_1", + "liaopan5_materialhole_2_2", + "liaopan5_materialhole_2_3", + "liaopan5_materialhole_3_0", + "liaopan5_materialhole_3_1", + "liaopan5_materialhole_3_2", + "liaopan5_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1130, + "y": 150, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120, + "size_y": 100, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan5_materialhole_0_0", + "B1": "liaopan5_materialhole_0_1", + "C1": "liaopan5_materialhole_0_2", + "D1": "liaopan5_materialhole_0_3", + "A2": "liaopan5_materialhole_1_0", + "B2": "liaopan5_materialhole_1_1", + "C2": "liaopan5_materialhole_1_2", + "D2": "liaopan5_materialhole_1_3", + "A3": "liaopan5_materialhole_2_0", + "B3": "liaopan5_materialhole_2_1", + "C3": "liaopan5_materialhole_2_2", + "D3": "liaopan5_materialhole_2_3", + "A4": "liaopan5_materialhole_3_0", + "B4": "liaopan5_materialhole_3_1", + "C4": "liaopan5_materialhole_3_2", + "D4": "liaopan5_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan5_materialhole_0_0", + "name": "liaopan5_materialhole_0_0", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_0_1", + "name": "liaopan5_materialhole_0_1", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_0_2", + "name": "liaopan5_materialhole_0_2", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_0_3", + "name": "liaopan5_materialhole_0_3", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_1_0", + "name": "liaopan5_materialhole_1_0", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_1_1", + "name": "liaopan5_materialhole_1_1", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_1_2", + "name": "liaopan5_materialhole_1_2", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_1_3", + "name": "liaopan5_materialhole_1_3", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_2_0", + "name": "liaopan5_materialhole_2_0", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_2_1", + "name": "liaopan5_materialhole_2_1", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_2_2", + "name": "liaopan5_materialhole_2_2", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_2_3", + "name": "liaopan5_materialhole_2_3", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_3_0", + "name": "liaopan5_materialhole_3_0", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_3_1", + "name": "liaopan5_materialhole_3_1", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_3_2", + "name": "liaopan5_materialhole_3_2", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_3_3", + "name": "liaopan5_materialhole_3_3", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6", + "name": "liaopan6", + "sample_id": null, + "children": [ + "liaopan6_materialhole_0_0", + "liaopan6_materialhole_0_1", + "liaopan6_materialhole_0_2", + "liaopan6_materialhole_0_3", + "liaopan6_materialhole_1_0", + "liaopan6_materialhole_1_1", + "liaopan6_materialhole_1_2", + "liaopan6_materialhole_1_3", + "liaopan6_materialhole_2_0", + "liaopan6_materialhole_2_1", + "liaopan6_materialhole_2_2", + "liaopan6_materialhole_2_3", + "liaopan6_materialhole_3_0", + "liaopan6_materialhole_3_1", + "liaopan6_materialhole_3_2", + "liaopan6_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1250, + "y": 150, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120, + "size_y": 100, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan6_materialhole_0_0", + "B1": "liaopan6_materialhole_0_1", + "C1": "liaopan6_materialhole_0_2", + "D1": "liaopan6_materialhole_0_3", + "A2": "liaopan6_materialhole_1_0", + "B2": "liaopan6_materialhole_1_1", + "C2": "liaopan6_materialhole_1_2", + "D2": "liaopan6_materialhole_1_3", + "A3": "liaopan6_materialhole_2_0", + "B3": "liaopan6_materialhole_2_1", + "C3": "liaopan6_materialhole_2_2", + "D3": "liaopan6_materialhole_2_3", + "A4": "liaopan6_materialhole_3_0", + "B4": "liaopan6_materialhole_3_1", + "C4": "liaopan6_materialhole_3_2", + "D4": "liaopan6_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan6_materialhole_0_0", + "name": "liaopan6_materialhole_0_0", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_0_1", + "name": "liaopan6_materialhole_0_1", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_0_2", + "name": "liaopan6_materialhole_0_2", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_0_3", + "name": "liaopan6_materialhole_0_3", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_1_0", + "name": "liaopan6_materialhole_1_0", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_1_1", + "name": "liaopan6_materialhole_1_1", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_1_2", + "name": "liaopan6_materialhole_1_2", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_1_3", + "name": "liaopan6_materialhole_1_3", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_2_0", + "name": "liaopan6_materialhole_2_0", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_2_1", + "name": "liaopan6_materialhole_2_1", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_2_2", + "name": "liaopan6_materialhole_2_2", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_2_3", + "name": "liaopan6_materialhole_2_3", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_3_0", + "name": "liaopan6_materialhole_3_0", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_3_1", + "name": "liaopan6_materialhole_3_1", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_3_2", + "name": "liaopan6_materialhole_3_2", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_3_3", + "name": "liaopan6_materialhole_3_3", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "bottle_rack_3x4", + "name": "bottle_rack_3x4", + "sample_id": null, + "children": [ + "sheet_3x4_0", + "sheet_3x4_1", + "sheet_3x4_2", + "sheet_3x4_3", + "sheet_3x4_4", + "sheet_3x4_5", + "sheet_3x4_6", + "sheet_3x4_7", + "sheet_3x4_8", + "sheet_3x4_9", + "sheet_3x4_10", + "sheet_3x4_11" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 100, + "y": 200, + "z": 0 + }, + "config": { + "type": "BottleRack", + "size_x": 210.0, + "size_y": 140.0, + "size_z": 100.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "bottle_rack", + "model": null, + "barcode": null, + "num_items_x": 3, + "num_items_y": 4, + "position_spacing": 35.0, + "orientation": "vertical", + "padding_x": 20.0, + "padding_y": 20.0 + }, + "data": { + "bottle_diameter": 30.0, + "bottle_height": 100.0, + "position_spacing": 35.0, + "name_to_index": {} + } + }, + { + "id": "sheet_3x4_0", + "name": "sheet_3x4_0", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 20.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_1", + "name": "sheet_3x4_1", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 55.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_2", + "name": "sheet_3x4_2", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 90.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_3", + "name": "sheet_3x4_3", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 20.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_4", + "name": "sheet_3x4_4", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 55.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_5", + "name": "sheet_3x4_5", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 90.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_6", + "name": "sheet_3x4_6", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 90.0, + "y": 20.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_7", + "name": "sheet_3x4_7", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 90.0, + "y": 55.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_8", + "name": "sheet_3x4_8", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 90.0, + "y": 90.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_9", + "name": "sheet_3x4_9", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 125.0, + "y": 20.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_10", + "name": "sheet_3x4_10", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 125.0, + "y": 55.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_11", + "name": "sheet_3x4_11", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 125.0, + "y": 90.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "bottle_rack_6x2", + "name": "bottle_rack_6x2", + "sample_id": null, + "children": [ + "sheet_6x2_0", + "sheet_6x2_1", + "sheet_6x2_2", + "sheet_6x2_3", + "sheet_6x2_4", + "sheet_6x2_5", + "sheet_6x2_6", + "sheet_6x2_7", + "sheet_6x2_8", + "sheet_6x2_9", + "sheet_6x2_10", + "sheet_6x2_11" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 300, + "y": 300, + "z": 0 + }, + "config": { + "type": "BottleRack", + "size_x": 120.0, + "size_y": 250.0, + "size_z": 100.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "bottle_rack", + "model": null, + "barcode": null, + "num_items_x": 6, + "num_items_y": 2, + "position_spacing": 35.0, + "orientation": "vertical", + "padding_x": 20.0, + "padding_y": 20.0 + }, + "data": { + "bottle_diameter": 30.0, + "bottle_height": 100.0, + "position_spacing": 35.0, + "name_to_index": {} + } + }, + { + "id": "sheet_6x2_0", + "name": "sheet_6x2_0", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 20.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_1", + "name": "sheet_6x2_1", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 55.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_2", + "name": "sheet_6x2_2", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 90.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_3", + "name": "sheet_6x2_3", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 125.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_4", + "name": "sheet_6x2_4", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 160.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_5", + "name": "sheet_6x2_5", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 195.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_6", + "name": "sheet_6x2_6", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 20.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_7", + "name": "sheet_6x2_7", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 55.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_8", + "name": "sheet_6x2_8", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 90.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_9", + "name": "sheet_6x2_9", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 125.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_10", + "name": "sheet_6x2_10", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 160.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_11", + "name": "sheet_6x2_11", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 195.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "bottle_rack_6x2_2", + "name": "bottle_rack_6x2_2", + "sample_id": null, + "children": [], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 430, + "y": 300, + "z": 0 + }, + "config": { + "type": "BottleRack", + "size_x": 120.0, + "size_y": 250.0, + "size_z": 100.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "bottle_rack", + "model": null, + "barcode": null, + "num_items_x": 6, + "num_items_y": 2, + "position_spacing": 35.0, + "orientation": "vertical", + "padding_x": 20.0, + "padding_y": 20.0 + }, + "data": { + "bottle_diameter": 30.0, + "bottle_height": 100.0, + "position_spacing": 35.0, + "name_to_index": {} + } + }, + { + "id": "tip_box_64", + "name": "tip_box_64", + "sample_id": null, + "children": [ + "tip_box_64_tipspot_0_0", + "tip_box_64_tipspot_0_1", + "tip_box_64_tipspot_0_2", + "tip_box_64_tipspot_0_3", + "tip_box_64_tipspot_0_4", + "tip_box_64_tipspot_0_5", + "tip_box_64_tipspot_0_6", + "tip_box_64_tipspot_0_7", + "tip_box_64_tipspot_1_0", + "tip_box_64_tipspot_1_1", + "tip_box_64_tipspot_1_2", + "tip_box_64_tipspot_1_3", + "tip_box_64_tipspot_1_4", + "tip_box_64_tipspot_1_5", + "tip_box_64_tipspot_1_6", + "tip_box_64_tipspot_1_7", + "tip_box_64_tipspot_2_0", + "tip_box_64_tipspot_2_1", + "tip_box_64_tipspot_2_2", + "tip_box_64_tipspot_2_3", + "tip_box_64_tipspot_2_4", + "tip_box_64_tipspot_2_5", + "tip_box_64_tipspot_2_6", + "tip_box_64_tipspot_2_7", + "tip_box_64_tipspot_3_0", + "tip_box_64_tipspot_3_1", + "tip_box_64_tipspot_3_2", + "tip_box_64_tipspot_3_3", + "tip_box_64_tipspot_3_4", + "tip_box_64_tipspot_3_5", + "tip_box_64_tipspot_3_6", + "tip_box_64_tipspot_3_7", + "tip_box_64_tipspot_4_0", + "tip_box_64_tipspot_4_1", + "tip_box_64_tipspot_4_2", + "tip_box_64_tipspot_4_3", + "tip_box_64_tipspot_4_4", + "tip_box_64_tipspot_4_5", + "tip_box_64_tipspot_4_6", + "tip_box_64_tipspot_4_7", + "tip_box_64_tipspot_5_0", + "tip_box_64_tipspot_5_1", + "tip_box_64_tipspot_5_2", + "tip_box_64_tipspot_5_3", + "tip_box_64_tipspot_5_4", + "tip_box_64_tipspot_5_5", + "tip_box_64_tipspot_5_6", + "tip_box_64_tipspot_5_7", + "tip_box_64_tipspot_6_0", + "tip_box_64_tipspot_6_1", + "tip_box_64_tipspot_6_2", + "tip_box_64_tipspot_6_3", + "tip_box_64_tipspot_6_4", + "tip_box_64_tipspot_6_5", + "tip_box_64_tipspot_6_6", + "tip_box_64_tipspot_6_7", + "tip_box_64_tipspot_7_0", + "tip_box_64_tipspot_7_1", + "tip_box_64_tipspot_7_2", + "tip_box_64_tipspot_7_3", + "tip_box_64_tipspot_7_4", + "tip_box_64_tipspot_7_5", + "tip_box_64_tipspot_7_6", + "tip_box_64_tipspot_7_7" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 300, + "y": 100, + "z": 0 + }, + "config": { + "type": "TipBox64", + "size_x": 127.8, + "size_y": 85.5, + "size_z": 60.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_box_64", + "model": null, + "barcode": null, + "ordering": { + "A1": "tip_box_64_tipspot_0_0", + "B1": "tip_box_64_tipspot_0_1", + "C1": "tip_box_64_tipspot_0_2", + "D1": "tip_box_64_tipspot_0_3", + "E1": "tip_box_64_tipspot_0_4", + "F1": "tip_box_64_tipspot_0_5", + "G1": "tip_box_64_tipspot_0_6", + "H1": "tip_box_64_tipspot_0_7", + "A2": "tip_box_64_tipspot_1_0", + "B2": "tip_box_64_tipspot_1_1", + "C2": "tip_box_64_tipspot_1_2", + "D2": "tip_box_64_tipspot_1_3", + "E2": "tip_box_64_tipspot_1_4", + "F2": "tip_box_64_tipspot_1_5", + "G2": "tip_box_64_tipspot_1_6", + "H2": "tip_box_64_tipspot_1_7", + "A3": "tip_box_64_tipspot_2_0", + "B3": "tip_box_64_tipspot_2_1", + "C3": "tip_box_64_tipspot_2_2", + "D3": "tip_box_64_tipspot_2_3", + "E3": "tip_box_64_tipspot_2_4", + "F3": "tip_box_64_tipspot_2_5", + "G3": "tip_box_64_tipspot_2_6", + "H3": "tip_box_64_tipspot_2_7", + "A4": "tip_box_64_tipspot_3_0", + "B4": "tip_box_64_tipspot_3_1", + "C4": "tip_box_64_tipspot_3_2", + "D4": "tip_box_64_tipspot_3_3", + "E4": "tip_box_64_tipspot_3_4", + "F4": "tip_box_64_tipspot_3_5", + "G4": "tip_box_64_tipspot_3_6", + "H4": "tip_box_64_tipspot_3_7", + "A5": "tip_box_64_tipspot_4_0", + "B5": "tip_box_64_tipspot_4_1", + "C5": "tip_box_64_tipspot_4_2", + "D5": "tip_box_64_tipspot_4_3", + "E5": "tip_box_64_tipspot_4_4", + "F5": "tip_box_64_tipspot_4_5", + "G5": "tip_box_64_tipspot_4_6", + "H5": "tip_box_64_tipspot_4_7", + "A6": "tip_box_64_tipspot_5_0", + "B6": "tip_box_64_tipspot_5_1", + "C6": "tip_box_64_tipspot_5_2", + "D6": "tip_box_64_tipspot_5_3", + "E6": "tip_box_64_tipspot_5_4", + "F6": "tip_box_64_tipspot_5_5", + "G6": "tip_box_64_tipspot_5_6", + "H6": "tip_box_64_tipspot_5_7", + "A7": "tip_box_64_tipspot_6_0", + "B7": "tip_box_64_tipspot_6_1", + "C7": "tip_box_64_tipspot_6_2", + "D7": "tip_box_64_tipspot_6_3", + "E7": "tip_box_64_tipspot_6_4", + "F7": "tip_box_64_tipspot_6_5", + "G7": "tip_box_64_tipspot_6_6", + "H7": "tip_box_64_tipspot_6_7", + "A8": "tip_box_64_tipspot_7_0", + "B8": "tip_box_64_tipspot_7_1", + "C8": "tip_box_64_tipspot_7_2", + "D8": "tip_box_64_tipspot_7_3", + "E8": "tip_box_64_tipspot_7_4", + "F8": "tip_box_64_tipspot_7_5", + "G8": "tip_box_64_tipspot_7_6", + "H8": "tip_box_64_tipspot_7_7" + }, + "num_items_x": 8, + "num_items_y": 8, + "dx": 8.0, + "dy": 8.0, + "item_dx": 9.0, + "item_dy": 9.0 + }, + "data": {} + }, + { + "id": "tip_box_64_tipspot_0_0", + "name": "tip_box_64_tipspot_0_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_1", + "name": "tip_box_64_tipspot_0_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_2", + "name": "tip_box_64_tipspot_0_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_3", + "name": "tip_box_64_tipspot_0_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_4", + "name": "tip_box_64_tipspot_0_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_5", + "name": "tip_box_64_tipspot_0_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_6", + "name": "tip_box_64_tipspot_0_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_7", + "name": "tip_box_64_tipspot_0_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_0", + "name": "tip_box_64_tipspot_1_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_1", + "name": "tip_box_64_tipspot_1_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_2", + "name": "tip_box_64_tipspot_1_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_3", + "name": "tip_box_64_tipspot_1_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_4", + "name": "tip_box_64_tipspot_1_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_5", + "name": "tip_box_64_tipspot_1_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_6", + "name": "tip_box_64_tipspot_1_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_7", + "name": "tip_box_64_tipspot_1_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_0", + "name": "tip_box_64_tipspot_2_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_1", + "name": "tip_box_64_tipspot_2_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_2", + "name": "tip_box_64_tipspot_2_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_3", + "name": "tip_box_64_tipspot_2_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_4", + "name": "tip_box_64_tipspot_2_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_5", + "name": "tip_box_64_tipspot_2_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_6", + "name": "tip_box_64_tipspot_2_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_7", + "name": "tip_box_64_tipspot_2_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_0", + "name": "tip_box_64_tipspot_3_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_1", + "name": "tip_box_64_tipspot_3_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_2", + "name": "tip_box_64_tipspot_3_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_3", + "name": "tip_box_64_tipspot_3_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_4", + "name": "tip_box_64_tipspot_3_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_5", + "name": "tip_box_64_tipspot_3_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_6", + "name": "tip_box_64_tipspot_3_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_7", + "name": "tip_box_64_tipspot_3_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_0", + "name": "tip_box_64_tipspot_4_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_1", + "name": "tip_box_64_tipspot_4_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_2", + "name": "tip_box_64_tipspot_4_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_3", + "name": "tip_box_64_tipspot_4_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_4", + "name": "tip_box_64_tipspot_4_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_5", + "name": "tip_box_64_tipspot_4_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_6", + "name": "tip_box_64_tipspot_4_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_7", + "name": "tip_box_64_tipspot_4_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_0", + "name": "tip_box_64_tipspot_5_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_1", + "name": "tip_box_64_tipspot_5_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_2", + "name": "tip_box_64_tipspot_5_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_3", + "name": "tip_box_64_tipspot_5_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_4", + "name": "tip_box_64_tipspot_5_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_5", + "name": "tip_box_64_tipspot_5_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_6", + "name": "tip_box_64_tipspot_5_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_7", + "name": "tip_box_64_tipspot_5_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_0", + "name": "tip_box_64_tipspot_6_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_1", + "name": "tip_box_64_tipspot_6_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_2", + "name": "tip_box_64_tipspot_6_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_3", + "name": "tip_box_64_tipspot_6_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_4", + "name": "tip_box_64_tipspot_6_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_5", + "name": "tip_box_64_tipspot_6_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_6", + "name": "tip_box_64_tipspot_6_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_7", + "name": "tip_box_64_tipspot_6_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_0", + "name": "tip_box_64_tipspot_7_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_1", + "name": "tip_box_64_tipspot_7_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_2", + "name": "tip_box_64_tipspot_7_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_3", + "name": "tip_box_64_tipspot_7_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_4", + "name": "tip_box_64_tipspot_7_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_5", + "name": "tip_box_64_tipspot_7_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_6", + "name": "tip_box_64_tipspot_7_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_7", + "name": "tip_box_64_tipspot_7_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "waste_tip_box", + "name": "waste_tip_box", + "sample_id": null, + "children": [], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 300, + "y": 200, + "z": 0 + }, + "config": { + "type": "WasteTipBox", + "size_x": 127.8, + "size_y": 85.5, + "size_z": 60.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "waste_tip_box", + "model": null, + "barcode": null, + "max_volume": "Infinity", + "material_z_thickness": 0, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/unilabos/devices/workstation/workstation_base.py b/unilabos/devices/workstation/workstation_base.py index f8cea028..1988249f 100644 --- a/unilabos/devices/workstation/workstation_base.py +++ b/unilabos/devices/workstation/workstation_base.py @@ -112,17 +112,17 @@ class ResourceSynchronizer(ABC): self.workstation = workstation @abstractmethod - async def sync_from_external(self) -> bool: + def sync_from_external(self) -> bool: """从外部系统同步物料到本地deck""" pass @abstractmethod - async def sync_to_external(self, plr_resource: PLRResource) -> bool: + def sync_to_external(self, plr_resource: PLRResource) -> bool: """将本地物料同步到外部系统""" pass @abstractmethod - async def handle_external_change(self, change_info: Dict[str, Any]) -> bool: + def handle_external_change(self, change_info: Dict[str, Any]) -> bool: """处理外部系统的变更通知""" pass @@ -147,17 +147,15 @@ class WorkstationBase(ABC): def __init__( self, - station_resource: PLRResource, + deck: Deck, *args, **kwargs, # 必须有kwargs ): - # 基本配置 - print(station_resource) - self.deck_config = station_resource - # PLR 物料系统 - self.deck: Optional[Deck] = None + self.deck: Optional[Deck] = deck self.plr_resources: Dict[str, PLRResource] = {} + + self.resource_synchronizer = None # type: Optional[ResourceSynchronizer] # 硬件接口 self.hardware_interface: Union[Any, str] = None @@ -173,46 +171,7 @@ class WorkstationBase(ABC): def post_init(self, ros_node: ROS2WorkstationNode) -> None: # 初始化物料系统 self._ros_node = ros_node - self._initialize_material_system() - - def _initialize_material_system(self): - """初始化物料系统 - 使用 graphio 转换""" - pass - - def _create_complete_resource_config(self) -> Dict[str, Any]: - """创建完整的资源配置 - 合并 deck_config 和 children""" - # 创建主 deck 配置 - return {} - - def _normalize_child_resource(self, resource_id: str, config: Dict[str, Any], parent_id: str) -> Dict[str, Any]: - """标准化子资源配置""" - return { - "id": resource_id, - "name": config.get("name", resource_id), - "type": config.get("type", "container"), - "position": self._normalize_position(config.get("position", {})), - "config": config.get("config", {}), - "data": config.get("data", {}), - "children": [], # 简化版本:只支持一层子资源 - "parent": parent_id, - } - - def _normalize_position(self, position: Any) -> Dict[str, float]: - """标准化位置信息""" - if isinstance(position, dict): - return { - "x": float(position.get("x", 0)), - "y": float(position.get("y", 0)), - "z": float(position.get("z", 0)), - } - elif isinstance(position, (list, tuple)) and len(position) >= 2: - return { - "x": float(position[0]), - "y": float(position[1]), - "z": float(position[2]) if len(position) > 2 else 0.0, - } - else: - return {"x": 0.0, "y": 0.0, "z": 0.0} + self._ros_node.update_resource([self.deck]) def _build_resource_mappings(self, deck: Deck): """递归构建资源映射""" @@ -296,14 +255,14 @@ class WorkstationBase(ABC): """按类型查找资源""" return [res for res in self.plr_resources.values() if isinstance(res, resource_type)] - async def sync_with_external_system(self) -> bool: + def sync_with_external_system(self) -> bool: """与外部物料系统同步""" if not self.resource_synchronizer: logger.info(f"工作站 {self._ros_node.device_id} 没有配置资源同步器") return True try: - success = await self.resource_synchronizer.sync_from_external() + success = self.resource_synchronizer.sync_from_external() if success: logger.info(f"工作站 {self._ros_node.device_id} 外部同步成功") else: @@ -391,5 +350,5 @@ class WorkstationBase(ABC): class ProtocolNode(WorkstationBase): - def __init__(self, station_resource: Optional[PLRResource], *args, **kwargs): - super().__init__(station_resource, *args, **kwargs) + def __init__(self, deck: Optional[PLRResource], *args, **kwargs): + super().__init__(deck, *args, **kwargs) diff --git a/unilabos/devices/workstation/workstation_http_service.py b/unilabos/devices/workstation/workstation_http_service.py index 3805d2ce..4565edea 100644 --- a/unilabos/devices/workstation/workstation_http_service.py +++ b/unilabos/devices/workstation/workstation_http_service.py @@ -149,6 +149,22 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler): ) self._send_response(error_response) + def do_OPTIONS(self): + """处理OPTIONS请求 - CORS预检请求""" + try: + # 发送CORS响应头 + self.send_response(200) + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization') + self.send_header('Access-Control-Max-Age', '86400') + self.end_headers() + + except Exception as e: + logger.error(f"OPTIONS请求处理失败: {e}") + self.send_response(500) + self.end_headers() + def _handle_step_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse: """处理步骤完成报送(统一LIMS协议规范)""" try: @@ -206,7 +222,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler): # 验证data字段内容 data = request_data['data'] - data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'Status'] + data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'status'] if data_missing_fields := [field for field in data_required_fields if field not in data]: return HttpResponse( success=False, @@ -227,7 +243,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler): "0": "待生产", "2": "进样", "10": "开始", "20": "完成", "-2": "异常停止", "-3": "人工停止" } - status_desc = status_names.get(str(data['Status']), f"状态{data['Status']}") + status_desc = status_names.get(str(data['status']), f"状态{data['status']}") return HttpResponse( success=True, @@ -380,6 +396,21 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler): """处理物料变更报送""" try: # 验证必需字段 + if 'brand' in request_data: + if request_data['brand'] == "bioyond": # 奔曜 + error_msg = request_data["text"] + logger.info(f"收到奔曜错误处理报送: {error_msg}") + return HttpResponse( + success=True, + message=f"错误处理报送已收到: {error_msg}", + acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_msg.get('action_id', 'unknown')}", + data=None + ) + else: + return HttpResponse( + success=False, + message=f"缺少厂家信息(brand字段)" + ) required_fields = ['workstation_id', 'timestamp', 'resource_id', 'change_type'] if missing_fields := [field for field in required_fields if field not in request_data]: return HttpResponse( @@ -407,23 +438,45 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler): def _handle_error_handling_report(self, request_data: Dict[str, Any]) -> HttpResponse: """处理错误处理报送""" try: - # 验证必需字段 - required_fields = ['workstation_id', 'timestamp', 'error_type', 'error_message'] - if missing_fields := [field for field in required_fields if field not in request_data]: + # 检查是否为奔曜格式的错误报送 + if 'brand' in request_data and str(request_data['brand']).lower() == "bioyond": + # 奔曜格式处理 + if 'text' not in request_data: + return HttpResponse( + success=False, + message="奔曜格式缺少text字段" + ) + + error_data = request_data["text"] + logger.info(f"收到奔曜错误处理报送: {error_data}") + + # 调用工作站的处理方法 + result = self.workstation.handle_external_error(error_data) + return HttpResponse( - success=False, - message=f"缺少必要字段: {', '.join(missing_fields)}" + success=True, + message=f"错误处理报送已收到: 任务{error_data.get('task', 'unknown')}, 错误代码{error_data.get('code', 'unknown')}", + acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_data.get('task', 'unknown')}", + data=result + ) + else: + # 标准格式处理 + required_fields = ['workstation_id', 'timestamp', 'error_type', 'error_message'] + if missing_fields := [field for field in required_fields if field not in request_data]: + return HttpResponse( + success=False, + message=f"缺少必要字段: {', '.join(missing_fields)}" + ) + + # 调用工作站的处理方法 + result = self.workstation.handle_external_error(request_data) + + return HttpResponse( + success=True, + message=f"错误处理报送已处理: {request_data['error_type']} - {request_data['error_message']}", + acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{request_data.get('action_id', 'unknown')}", + data=result ) - - # 调用工作站的处理方法 - result = self.workstation.handle_external_error(request_data) - - return HttpResponse( - success=True, - message=f"错误处理报送已处理: {request_data['error_type']} - {request_data['error_message']}", - acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{request_data.get('action_id', 'unknown')}", - data=result - ) except Exception as e: logger.error(f"处理错误处理报送失败: {e}") @@ -548,13 +601,19 @@ class WorkstationHTTPService: """停止HTTP服务""" try: if self.running and self.server: + logger.info("正在停止工作站HTTP报送服务...") self.running = False - self.server.shutdown() - self.server.server_close() + # 停止serve_forever循环 + self.server.shutdown() + + # 等待服务器线程结束 if self.server_thread and self.server_thread.is_alive(): self.server_thread.join(timeout=5.0) + # 关闭服务器套接字 + self.server.server_close() + logger.info("工作站HTTP报送服务已停止") except Exception as e: @@ -563,11 +622,13 @@ class WorkstationHTTPService: def _run_server(self): """运行HTTP服务器""" try: - while self.running: - self.server.handle_request() + # 使用serve_forever()让服务持续运行 + self.server.serve_forever() except Exception as e: if self.running: # 只在非正常停止时记录错误 logger.error(f"HTTP服务运行错误: {e}") + finally: + logger.info("HTTP服务器线程已退出") @property def is_running(self) -> bool: @@ -603,3 +664,49 @@ __all__ = [ 'MaterialChangeReport', 'TaskExecutionReport' ] + + +if __name__ == "__main__": + # 简单测试HTTP服务 + class DummyWorkstation: + device_id = "WS-001" + + def process_step_finish_report(self, report_request): + return {"processed": True} + + def process_sample_finish_report(self, report_request): + return {"processed": True} + + def process_order_finish_report(self, report_request, used_materials): + return {"processed": True} + + def process_material_change_report(self, report_data): + return {"processed": True} + + def handle_external_error(self, error_data): + return {"handled": True} + + workstation = DummyWorkstation() + http_service = WorkstationHTTPService(workstation) + + try: + http_service.start() + print(f"测试服务器已启动: {http_service.service_url}") + print("按 Ctrl+C 停止服务器") + print("服务将持续运行,等待接收HTTP请求...") + + # 保持服务器运行 - 使用更好的等待机制 + try: + while http_service.is_running: + time.sleep(1) + except KeyboardInterrupt: + print("\n接收到停止信号...") + + except KeyboardInterrupt: + print("\n正在停止服务器...") + http_service.stop() + print("服务器已停止") + except Exception as e: + print(f"服务器运行错误: {e}") + http_service.stop() + diff --git a/unilabos/registry/devices/neware_battery_test_system.yaml b/unilabos/registry/devices/neware_battery_test_system.yaml new file mode 100644 index 00000000..3dd7568d --- /dev/null +++ b/unilabos/registry/devices/neware_battery_test_system.yaml @@ -0,0 +1,344 @@ +neware_battery_test_system: + category: + - neware_battery_test_system + class: + action_value_mappings: + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: string + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + auto-print_status_summary: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: print_status_summary参数 + type: object + type: UniLabJsonCommand + auto-test_connection: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: test_connection参数 + type: object + type: UniLabJsonCommand + export_status_json: + feedback: {} + goal: + filepath: filepath + goal_default: + filepath: bts_status.json + handles: {} + result: + return_info: return_info + success: success + schema: + description: 导出当前状态数据到JSON文件 + properties: + feedback: {} + goal: + properties: + filepath: + default: bts_status.json + description: 输出JSON文件路径 + type: string + required: [] + type: object + result: + properties: + return_info: + description: 导出操作结果信息 + type: string + success: + description: 导出是否成功 + type: boolean + required: + - return_info + - success + type: object + required: + - goal + type: object + type: UniLabJsonCommand + get_device_summary: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: + return_info: return_info + success: success + schema: + description: 获取设备级别的摘要统计信息 + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: + properties: + return_info: + description: 设备摘要信息JSON格式 + type: string + success: + description: 查询是否成功 + type: boolean + required: + - return_info + - success + type: object + required: + - goal + type: object + type: UniLabJsonCommand + get_plate_status: + feedback: {} + goal: + plate_num: plate_num + goal_default: + plate_num: 1 + handles: {} + result: + return_info: return_info + success: success + schema: + description: 获取指定盘(1或2)的电池状态信息 + properties: + feedback: {} + goal: + properties: + plate_num: + description: 盘号 (1 或 2) + maximum: 2 + minimum: 1 + type: integer + required: + - plate_num + type: object + result: + properties: + return_info: + description: 盘状态信息JSON格式 + type: string + success: + description: 查询是否成功 + type: boolean + required: + - return_info + - success + type: object + required: + - goal + type: object + type: UniLabJsonCommand + print_status_summary_action: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: + return_info: return_info + success: success + schema: + description: 打印通道状态摘要信息到控制台 + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: + properties: + return_info: + description: 打印操作结果信息 + type: string + success: + description: 打印是否成功 + type: boolean + required: + - return_info + - success + type: object + required: + - goal + type: object + type: UniLabJsonCommand + query_plate_action: + feedback: {} + goal: + string: plate_id + goal_default: + string: '' + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: StrSingleInput_Feedback + type: object + goal: + properties: + string: + type: string + required: + - string + title: StrSingleInput_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: StrSingleInput_Result + type: object + required: + - goal + title: StrSingleInput + type: object + type: StrSingleInput + test_connection_action: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: + return_info: return_info + success: success + schema: + description: 测试与电池测试系统的TCP连接 + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: + properties: + return_info: + description: 连接测试结果信息 + type: string + success: + description: 连接测试是否成功 + type: boolean + required: + - return_info + - success + type: object + required: + - goal + type: object + type: UniLabJsonCommand + module: unilabos.devices.battery.neware_battery_test_system:NewareBatteryTestSystem + status_types: + channel_status: dict + connection_info: dict + device_summary: dict + plate_status: dict + status: str + total_channels: int + type: python + config_info: [] + description: 新威电池测试系统驱动,支持720个通道的电池测试状态监控和数据导出。通过TCP通信实现远程控制,包含完整的物料管理系统,支持2盘电池的状态映射和监控。 + handles: [] + icon: '' + init_param_schema: + config: + properties: + devtype: + type: string + ip: + type: string + machine_id: + default: 1 + type: integer + port: + type: integer + size_x: + default: 500.0 + type: number + size_y: + default: 500.0 + type: number + size_z: + default: 2000.0 + type: number + timeout: + type: integer + required: [] + type: object + data: + properties: + channel_status: + type: object + connection_info: + type: object + device_summary: + type: object + plate_status: + type: object + status: + type: string + total_channels: + type: integer + required: + - status + - channel_status + - connection_info + - total_channels + - plate_status + - device_summary + type: object + version: 1.0.0 diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index c5e7fced..48054810 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -6034,10 +6034,108 @@ workstation: init_param_schema: config: properties: - station_resource: + deck: type: string required: - - station_resource + - deck + type: object + data: + properties: {} + required: [] + type: object + version: 1.0.0 +workstation.bioyond: + category: + - work_station + class: + action_value_mappings: + auto-execute_bioyond_sync_workflow: + feedback: {} + goal: {} + goal_default: + parameters: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + parameters: + type: object + required: + - parameters + type: object + result: {} + required: + - goal + title: execute_bioyond_sync_workflow参数 + type: object + type: UniLabJsonCommandAsync + auto-execute_bioyond_update_workflow: + feedback: {} + goal: {} + goal_default: + parameters: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + parameters: + type: object + required: + - parameters + type: object + result: {} + required: + - goal + title: execute_bioyond_update_workflow参数 + type: object + type: UniLabJsonCommandAsync + auto-load_bioyond_data_from_file: + feedback: {} + goal: {} + goal_default: + file_path: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + file_path: + type: string + required: + - file_path + type: object + result: {} + required: + - goal + title: load_bioyond_data_from_file参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation + status_types: {} + type: python + config_info: [] + description: '' + handles: [] + icon: 反应站.webp + init_param_schema: + config: + properties: + bioyond_config: + type: string + deck: + type: string + required: [] type: object data: properties: {} diff --git a/unilabos/registry/resources/bioyond/bottle_carriers.yaml b/unilabos/registry/resources/bioyond/bottle_carriers.yaml new file mode 100644 index 00000000..0eaba1e3 --- /dev/null +++ b/unilabos/registry/resources/bioyond/bottle_carriers.yaml @@ -0,0 +1,36 @@ +BIOYOND_PolymerStation_1BottleCarrier: + category: + - bottle_carriers + class: + module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1BottleCarrier + type: pylabrobot + description: BIOYOND_PolymerStation_1BottleCarrier + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +BIOYOND_PolymerStation_1FlaskCarrier: + category: + - bottle_carriers + class: + module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1FlaskCarrier + type: pylabrobot + description: BIOYOND_PolymerStation_1FlaskCarrier + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +BIOYOND_PolymerStation_6VialCarrier: + category: + - bottle_carriers + class: + module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6VialCarrier + type: pylabrobot + description: BIOYOND_PolymerStation_6VialCarrier + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 diff --git a/unilabos/registry/resources/bioyond/deck.yaml b/unilabos/registry/resources/bioyond/deck.yaml new file mode 100644 index 00000000..cb655126 --- /dev/null +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -0,0 +1,24 @@ +BIOYOND_PolymerReactionStation_Deck: + category: + - deck + class: + module: unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck + type: pylabrobot + description: BIOYOND PolymerReactionStation Deck + handles: [] + icon: '反应站.webp' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +BIOYOND_PolymerPreparationStation_Deck: + category: + - deck + class: + module: unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck + type: pylabrobot + description: BIOYOND PolymerPreparationStation Deck + handles: [] + icon: '配液站.webp' + init_param_schema: {} + registry_type: resource + version: 1.0.0 diff --git a/unilabos/resources/bioyond/__init__.py b/unilabos/resources/bioyond/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unilabos/resources/bioyond/bottle_carriers.py b/unilabos/resources/bioyond/bottle_carriers.py new file mode 100644 index 00000000..9f88b88d --- /dev/null +++ b/unilabos/resources/bioyond/bottle_carriers.py @@ -0,0 +1,217 @@ +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.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle +# 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial + + +def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier: + """6瓶载架 - 2x3布局""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 30.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=3, + num_items_y=2, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=bottle_spacing_x, + item_dy=bottle_spacing_y, + + size_x=bottle_diameter, + size_y=bottle_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="BIOYOND_Electrolyte_6VialCarrier", + ) + carrier.num_items_x = 3 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + for i in range(6): + carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_vial_{i+1}") + return carrier + + +def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier: + """1瓶载架 - 单个中央位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 100.0 + + # 烧杯尺寸 + beaker_diameter = 80.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="BIOYOND_Electrolyte_1BottleCarrier", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = BIOYOND_PolymerStation_Solution_Beaker(f"{name}_beaker_1") + return carrier + + +def BIOYOND_PolymerStation_6VialCarrier(name: str) -> BottleCarrier: + """6瓶载架 - 2x3布局""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 30.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=3, + num_items_y=2, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=bottle_spacing_x, + item_dy=bottle_spacing_y, + + size_x=bottle_diameter, + size_y=bottle_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="BIOYOND_PolymerStation_6VialCarrier", + ) + carrier.num_items_x = 3 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序 + for i in range(6): + carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_vial_{ordering[i]}") + return carrier + + +def BIOYOND_PolymerStation_1BottleCarrier(name: str) -> BottleCarrier: + """1瓶载架 - 单个中央位置""" + + # 载架尺寸 (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="BIOYOND_PolymerStation_1BottleCarrier", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = BIOYOND_PolymerStation_Reagent_Bottle(f"{name}_flask_1") + return carrier + + +def BIOYOND_PolymerStation_1FlaskCarrier(name: str) -> BottleCarrier: + """1瓶载架 - 单个中央位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 20.0 + + # 烧杯尺寸 + beaker_diameter = 70.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="BIOYOND_PolymerStation_1FlaskCarrier", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = BIOYOND_PolymerStation_Reagent_Bottle(f"{name}_bottle_1") + return carrier diff --git a/unilabos/resources/bioyond/bottles.py b/unilabos/resources/bioyond/bottles.py new file mode 100644 index 00000000..1afac18c --- /dev/null +++ b/unilabos/resources/bioyond/bottles.py @@ -0,0 +1,56 @@ +from unilabos.resources.itemized_carrier import Bottle, BottleCarrier +# 工厂函数 + + +def BIOYOND_PolymerStation_Solid_Vial( + name: str, + diameter: float = 20.0, + height: float = 100.0, + max_volume: float = 30000.0, # 30mL + barcode: str = None, +) -> Bottle: + """创建粉末瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="BIOYOND_PolymerStation_Solid_Vial", + ) + + +def BIOYOND_PolymerStation_Solution_Beaker( + name: str, + diameter: float = 60.0, + height: float = 70.0, + max_volume: float = 200000.0, # 200mL + barcode: str = None, +) -> Bottle: + """创建溶液烧杯""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="BIOYOND_PolymerStation_Solution_Beaker", + ) + + +def BIOYOND_PolymerStation_Reagent_Bottle( + name: str, + diameter: float = 70.0, + height: float = 120.0, + max_volume: float = 500000.0, # 500mL + barcode: str = None, +) -> Bottle: + """创建试剂瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="BIOYOND_PolymerStation_Reagent_Bottle", + ) diff --git a/unilabos/resources/bioyond/decks.py b/unilabos/resources/bioyond/decks.py new file mode 100644 index 00000000..e8c021fd --- /dev/null +++ b/unilabos/resources/bioyond/decks.py @@ -0,0 +1,68 @@ +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 + + +class BIOYOND_PolymerReactionStation_Deck(Deck): + def __init__( + self, + name: str = "PolymerReactionStation_Deck", + size_x: float = 2700.0, + 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: + # 添加仓库 + self.warehouses = { + "堆栈1": bioyond_warehouse_1x4x4("堆栈1"), + "堆栈2": bioyond_warehouse_1x4x4("堆栈2"), + "站内试剂存放堆栈": bioyond_warehouse_liquid_and_lid_handling("站内试剂存放堆栈"), + } + self.warehouse_locations = { + "堆栈1": Coordinate(0.0, 430.0, 0.0), + "堆栈2": Coordinate(2550.0, 430.0, 0.0), + "站内试剂存放堆栈": Coordinate(800.0, 475.0, 0.0), + } + self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90) + + 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, + name: str = "PolymerPreparationStation_Deck", + size_x: float = 2700.0, + 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: + # 添加仓库 + self.warehouses = { + "io_warehouse_left": bioyond_warehouse_1x4x4("io_warehouse_left"), + "io_warehouse_right": bioyond_warehouse_1x4x4("io_warehouse_right"), + "solutions": bioyond_warehouse_1x4x2("warehouse_solutions"), + "liquid_and_lid_handling": bioyond_warehouse_liquid_and_lid_handling("warehouse_liquid_and_lid_handling"), + } + self.warehouse_locations = { + "io_warehouse_left": Coordinate(0.0, 650.0, 0.0), + "io_warehouse_right": Coordinate(2550.0, 650.0, 0.0), + "solutions": Coordinate(1915.0, 900.0, 0.0), + "liquid_and_lid_handling": Coordinate(1330.0, 490.0, 0.0), + } + + for warehouse_name, warehouse in self.warehouses.items(): + self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name]) diff --git a/unilabos/resources/bioyond/warehouses.py b/unilabos/resources/bioyond/warehouses.py new file mode 100644 index 00000000..507b1f2f --- /dev/null +++ b/unilabos/resources/bioyond/warehouses.py @@ -0,0 +1,54 @@ +from unilabos.resources.warehouse import WareHouse, warehouse_factory + + +def bioyond_warehouse_1x4x4(name: str) -> WareHouse: + """创建BioYond 4x1x4仓库""" + return warehouse_factory( + name=name, + num_items_x=1, + num_items_y=4, + num_items_z=4, + 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_1x4x2(name: str) -> WareHouse: + """创建BioYond 4x1x2仓库""" + return warehouse_factory( + name=name, + num_items_x=1, + num_items_y=4, + num_items_z=2, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + removed_positions=None + ) + + +def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse: + """创建BioYond开关盖加液模块台面""" + return warehouse_factory( + name=name, + num_items_x=2, + num_items_y=5, + num_items_z=1, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + removed_positions=None + ) \ No newline at end of file diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 093aa7d5..0c55127a 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -4,6 +4,7 @@ import json from typing import Union, Any, Dict import numpy as np import networkx as nx +from pylabrobot.resources import ResourceHolder from unilabos_msgs.msg import Resource from unilabos.resources.container import RegularContainer @@ -480,7 +481,54 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w return r -def initialize_resource(resource_config: dict) -> list[dict]: +def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = {}, deck: Any = None) -> list[dict]: + """ + 将 bioyond 物料格式转换为 ulab 物料格式 + + Args: + bioyond_materials: bioyond 系统的物料查询结果列表 + type_mapping: 物料类型映射字典,格式 {bioyond_type: plr_class_name} + location_id_mapping: 库位 ID 到名称的映射字典,格式 {location_id: location_name} + + Returns: + pylabrobot 格式的物料列表 + """ + plr_materials = [] + + for material in bioyond_materials: + className = type_mapping.get(material.get("typeName"), "RegularContainer") if type_mapping else "RegularContainer" + + plr_material: ResourcePLR = initialize_resource({"name": material["name"], "class": className}, resource_type=ResourcePLR) + plr_material.code = material.get("code", "") and material.get("barCode", "") or "" + + # 处理子物料(detail) + if material.get("detail") and len(material["detail"]) > 0: + child_ids = [] + for detail in material["detail"]: + number = (detail.get("z", 0) - 1) * plr_material.num_items_x * plr_material.num_items_y + \ + (detail.get("x", 0) - 1) * plr_material.num_items_x + \ + (detail.get("y", 0) - 1) + bottle = plr_material[number] + bottle.code = detail.get("code", "") + bottle.tracker.liquids = [(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)] + + plr_materials.append(plr_material) + + if deck and hasattr(deck, "warehouses"): + for loc in material.get("locations", []): + if hasattr(deck, "warehouses") and loc.get("whName") in deck.warehouses: + warehouse = deck.warehouses[loc["whName"]] + idx = (loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y + \ + (loc.get("x", 0) - 1) * warehouse.num_items_x + \ + (loc.get("z", 0) - 1) + if 0 <= idx < warehouse.capacity: + if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder): + warehouse[idx] = plr_material + + return plr_materials + + +def initialize_resource(resource_config: dict, resource_type: Any = None) -> Union[list[dict], ResourcePLR]: """Initializes a resource based on its configuration. If the config is detailed, then do nothing; @@ -512,11 +560,14 @@ def initialize_resource(resource_config: dict) -> list[dict]: if resource_class_config["type"] == "pylabrobot": resource_plr = RESOURCE(name=resource_config["name"]) - r = resource_plr_to_ulab(resource_plr=resource_plr, parent_name=resource_config.get("parent", None)) - # r = resource_plr_to_ulab(resource_plr=resource_plr) - if resource_config.get("position") is not None: - r["position"] = resource_config["position"] - r = tree_to_list([r]) + if resource_type != ResourcePLR: + r = resource_plr_to_ulab(resource_plr=resource_plr, parent_name=resource_config.get("parent", None)) + # r = resource_plr_to_ulab(resource_plr=resource_plr) + if resource_config.get("position") is not None: + r["position"] = resource_config["position"] + r = tree_to_list([r]) + else: + r = resource_plr elif resource_class_config["type"] == "unilabos": res_instance: RegularContainer = RESOURCE(id=resource_config["name"]) res_instance.ulr_resource = convert_to_ros_msg(Resource, {k:v for k,v in resource_config.items() if k != "class"}) diff --git a/unilabos/resources/itemized_carrier.py b/unilabos/resources/itemized_carrier.py new file mode 100644 index 00000000..997ad068 --- /dev/null +++ b/unilabos/resources/itemized_carrier.py @@ -0,0 +1,357 @@ +""" +自动化液体处理工作站物料类定义 - 简化版 +Automated Liquid Handling Station Resource Classes - Simplified Version +""" + +from __future__ import annotations + +from typing import Dict, Optional + +from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources.container import Container +from pylabrobot.resources.resource_holder import ResourceHolder +from pylabrobot.resources import Resource as ResourcePLR + + +class Bottle(Container): + """瓶子类 - 简化版,不追踪瓶盖""" + + def __init__( + self, + name: str, + diameter: float, + height: float, + max_volume: float, + size_x: float = 0.0, + size_y: float = 0.0, + size_z: float = 0.0, + barcode: Optional[str] = "", + category: str = "container", + model: Optional[str] = None, + ): + super().__init__( + name=name, + size_x=diameter, + size_y=diameter, + size_z=height, + max_volume=max_volume, + category=category, + model=model, + ) + self.diameter = diameter + self.height = height + self.barcode = barcode + + def serialize(self) -> dict: + return { + **super().serialize(), + "diameter": self.diameter, + "height": self.height, + "barcode": self.barcode, + } + + +from string import ascii_uppercase as LETTERS +from typing import Dict, List, Optional, Type, TypeVar, Union, Sequence, Tuple + +import pylabrobot +from pylabrobot.resources.resource_holder import ResourceHolder + +T = TypeVar("T", bound=ResourceHolder) + +S = TypeVar("S", bound=ResourceHolder) + + +class ItemizedCarrier(ResourcePLR): + """Base class for all carriers.""" + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + num_items_x: int = 0, + num_items_y: int = 0, + num_items_z: int = 0, + sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None, + category: Optional[str] = "carrier", + model: Optional[str] = None, + ): + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category=category, + model=model, + ) + self.num_items = len(sites) + self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z + if isinstance(sites, dict): + sites = sites or {} + self.sites: List[Optional[ResourcePLR]] = list(sites.values()) + self._ordering = sites + self.child_locations: Dict[str, Coordinate] = {} + self.child_size: Dict[str, dict] = {} + for spot, resource in sites.items(): + if resource is not None and getattr(resource, "location", None) is None: + raise ValueError(f"resource {resource} has no location") + if resource is not None: + self.child_locations[spot] = resource.location + self.child_size[spot] = {"width": resource._size_x, "height": resource._size_y, "depth": resource._size_z} + else: + self.child_locations[spot] = Coordinate.zero() + self.child_size[spot] = {"width": 0, "height": 0, "depth": 0} + elif isinstance(sites, list): + # deserialize时走这里;还需要根据 self.sites 索引children + self.child_locations = {site["label"]: Coordinate(**site["position"]) for site in sites} + self.child_size = {site["label"]: site["size"] for site in sites} + self.sites = [site["occupied_by"] for site in sites] + self._ordering = {site["label"]: site["position"] for site in sites} + else: + print("sites:", sites) + + @property + def capacity(self): + """The number of sites on this carrier.""" + return len(self.sites) + + def __len__(self) -> int: + """Return the number of sites on this carrier.""" + return len(self.sites) + + def assign_child_resource( + self, + resource: ResourcePLR, + location: Optional[Coordinate], + reassign: bool = True, + spot: Optional[int] = None, + ): + idx = spot + # 如果只给 location,根据坐标和 deserialize 后的 self.sites(持有names)来寻找 resource 该摆放的位置 + if spot is not None: + idx = spot + else: + for i, site in enumerate(self.sites): + site_location = list(self.child_locations.values())[i] + if type(site) == str and site == resource.name: + idx = i + break + if site_location == location: + idx = i + break + + if not reassign and self.sites[idx] is not None: + raise ValueError(f"a site with index {idx} already exists") + super().assign_child_resource(resource, location=location, reassign=reassign) + self.sites[idx] = resource + + def assign_resource_to_site(self, resource: ResourcePLR, spot: int): + if self.sites[spot] is not None and not isinstance(self.sites[spot], ResourceHolder): + raise ValueError(f"spot {spot} already has a resource, {resource}") + self.assign_child_resource(resource, location=self.child_locations.get(str(spot)), spot=spot) + + def unassign_child_resource(self, resource: ResourcePLR): + found = False + for spot, res in enumerate(self.sites): + if res == resource: + self.sites[spot] = None + found = True + break + if not found: + raise ValueError(f"Resource {resource} is not assigned to this carrier") + if hasattr(resource, "unassign"): + resource.unassign() + + def __getitem__( + self, + identifier: Union[str, int, Sequence[int], Sequence[str], slice, range], + ) -> Union[List[T], T]: + """Get the items with the given identifier. + + This is a convenience method for getting the items with the given identifier. It is equivalent + to :meth:`get_items`, but adds support for slicing and supports single items in the same + functional call. Note that the return type will always be a list, even if a single item is + requested. + + Examples: + Getting the items with identifiers "A1" through "E1": + + >>> items["A1:E1"] + + [, , , , ] + + Getting the items with identifiers 0 through 4 (note that this is the same as above): + + >>> items[range(5)] + + [, , , , ] + + Getting items with a slice (note that this is the same as above): + + >>> items[0:5] + + [, , , , ] + + Getting a single item: + + >>> items[0] + + [] + """ + + if isinstance(identifier, str): + if ":" in identifier: # multiple # TODO: deprecate this, use `"A1":"E1"` instead (slice) + return self.get_items(identifier) + + return self.get_item(identifier) # single + + if isinstance(identifier, int): + return self.get_item(identifier) + + if isinstance(identifier, (slice, range)): + start, stop = identifier.start, identifier.stop + if isinstance(identifier.start, str): + start = list(self._ordering.keys()).index(identifier.start) + elif identifier.start is None: + start = 0 + if isinstance(identifier.stop, str): + stop = list(self._ordering.keys()).index(identifier.stop) + elif identifier.stop is None: + stop = self.num_items + identifier = list(range(start, stop, identifier.step or 1)) + return self.get_items(identifier) + + if isinstance(identifier, (list, tuple)): + return self.get_items(identifier) + + raise TypeError(f"Invalid identifier type: {type(identifier)}") + + def get_item(self, identifier: Union[str, int, Tuple[int, int]]) -> T: + """Get the item with the given identifier. + + Args: + identifier: The identifier of the item. Either a string, an integer, or a tuple. If an + integer, it is the index of the item in the list of items (counted from 0, top to bottom, left + to right). If a string, it uses transposed MS Excel style notation, e.g. "A1" for the first + item, "B1" for the item below that, etc. If a tuple, it is (row, column). + + Raises: + IndexError: If the identifier is out of range. The range is 0 to self.num_items-1 (inclusive). + """ + + if isinstance(identifier, tuple): + row, column = identifier + identifier = LETTERS[row] + str(column + 1) # standard transposed-Excel style notation + if isinstance(identifier, str): + try: + identifier = list(self._ordering.keys()).index(identifier) + except ValueError as e: + raise IndexError( + f"Item with identifier '{identifier}' does not exist on " f"resource '{self.name}'." + ) from e + + if not 0 <= identifier < self.capacity: + raise IndexError( + f"Item with identifier '{identifier}' does not exist on " f"resource '{self.name}'." + ) + + # Cast child to item type. Children will always be `T`, but the type checker doesn't know that. + return self.sites[identifier] + + def get_items(self, identifiers: Union[str, Sequence[int], Sequence[str]]) -> List[T]: + """Get the items with the given identifier. + + Args: + identifier: Deprecated. Use `identifiers` instead. # TODO(deprecate-ordered-items) + identifiers: The identifiers of the items. Either a string range or a list of integers. If a + string, it uses transposed MS Excel style notation. Regions of items can be specified using + a colon, e.g. "A1:H1" for the first column. If a list of integers, it is the indices of the + items in the list of items (counted from 0, top to bottom, left to right). + + Examples: + Getting the items with identifiers "A1" through "E1": + + >>> items.get_items("A1:E1") + + [, , , , ] + + Getting the items with identifiers 0 through 4: + + >>> items.get_items(range(5)) + + [, , , , ] + """ + + if isinstance(identifiers, str): + identifiers = pylabrobot.utils.expand_string_range(identifiers) + return [self.get_item(i) for i in identifiers] + + def __setitem__(self, idx: Union[int, str], resource: Optional[ResourcePLR]): + """Assign a resource to this carrier.""" + if resource is None: # setting to None + assigned_resource = self[idx] + if assigned_resource is not None: + self.unassign_child_resource(assigned_resource) + else: + idx = list(self._ordering.keys()).index(idx) if isinstance(idx, str) else idx + self.assign_resource_to_site(resource, spot=idx) + + def __delitem__(self, idx: int): + """Unassign a resource from this carrier.""" + assigned_resource = self[idx] + if assigned_resource is not None: + self.unassign_child_resource(assigned_resource) + + def get_resources(self) -> List[ResourcePLR]: + """Get all resources assigned to this carrier.""" + return [resource for resource in self.sites.values() if resource is not None] + + def __eq__(self, other): + return super().__eq__(other) and self.sites == other.sites + + def get_free_sites(self) -> List[int]: + return [spot for spot, resource in self.sites.items() if resource is None] + + def serialize(self): + return { + **super().serialize(), + "num_items_x": self.num_items_x, + "num_items_y": self.num_items_y, + "num_items_z": self.num_items_z, + "sites": [{ + "label": str(identifier), + "visible": True if self[identifier] is not None else False, + "occupied_by": self[identifier].name + if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else + self[identifier] if isinstance(self[identifier], str) else None, + "position": {"x": location.x, "y": location.y, "z": location.z}, + "size": self.child_size[identifier], + "content_type": ["bottle", "container", "tube", "bottle_carrier", "tip_rack"] + } for identifier, location in self.child_locations.items()] + } + + +class BottleCarrier(ItemizedCarrier): + """瓶载架 - 直接继承自 TubeCarrier""" + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + sites: Optional[Dict[Union[int, str], ResourceHolder]] = None, + category: str = "bottle_carrier", + model: Optional[str] = None, + ): + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + sites=sites, + category=category, + model=model, + ) diff --git a/unilabos/resources/warehouse.py b/unilabos/resources/warehouse.py new file mode 100644 index 00000000..775c55aa --- /dev/null +++ b/unilabos/resources/warehouse.py @@ -0,0 +1,104 @@ +from typing import Dict, Optional, List, Union +from pylabrobot.resources import Coordinate +from pylabrobot.resources.carrier import ResourceHolder, create_homogeneous_resources + +from unilabos.resources.itemized_carrier import ItemizedCarrier, ResourcePLR + + +def warehouse_factory( + name: str, + num_items_x: int = 4, + num_items_y: int = 1, + num_items_z: int = 4, + dx: float = 137.0, + dy: float = 96.0, + dz: float = 120.0, + item_dx: float = 10.0, + item_dy: float = 10.0, + item_dz: float = 10.0, + removed_positions: Optional[List[int]] = None, + empty: bool = False, + category: str = "warehouse", + model: Optional[str] = None, +): + # 创建16个板架位 (4层 x 4位置) + locations = [] + for layer in range(num_items_z): # 4层 + for row in range(num_items_y): # 4行 + for col in range(num_items_x): # 1列 (每层4x1=4个位置) + # 计算位置 + x = dx + col * item_dx + y = dy + (num_items_y - row - 1) * item_dy + z = dz + (num_items_z - layer - 1) * item_dz + locations.append(Coordinate(x, y, z)) + if removed_positions: + locations = [loc for i, loc in enumerate(locations) if i not in removed_positions] + sites = create_homogeneous_resources( + klass=ResourceHolder, + locations=locations, + resource_size_x=127.0, + resource_size_y=86.0, + name_prefix=name, + ) + + return WareHouse( + name=name, + size_x=dx + item_dx * num_items_x, + size_y=dy + item_dy * num_items_y, + size_z=dz + item_dz * num_items_z, + num_items_x = num_items_x, + num_items_y = num_items_y, + num_items_z = num_items_z, + # ordered_items=ordered_items, + # ordering=ordering, + sites=sites, + category=category, + model=model, + ) + + +class WareHouse(ItemizedCarrier): + """堆栈载体类 - 可容纳16个板位的载体(4层x4行x1列)""" + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + num_items_x: int, + num_items_y: int, + num_items_z: int, + sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None, + category: str = "warehouse", + model: Optional[str] = None, + ): + + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + # ordered_items=ordered_items, + # ordering=ordering, + num_items_x=num_items_x, + num_items_y=num_items_y, + num_items_z=num_items_z, + sites=sites, + category=category, + model=model, + ) + + def get_site_by_layer_position(self, row: int, col: int, layer: int) -> ResourceHolder: + if not (0 <= layer < 4 and 0 <= row < 4 and 0 <= col < 1): + raise ValueError("无效的位置: layer={}, row={}, col={}".format(layer, row, col)) + + site_index = layer * 4 + row * 1 + col + return self.sites[site_index] + + def add_rack_to_position(self, row: int, col: int, layer: int, rack) -> None: + site = self.get_site_by_layer_position(row, col, layer) + site.assign_child_resource(rack) + + def get_rack_at_position(self, row: int, col: int, layer: int): + site = self.get_site_by_layer_position(row, col, layer) + return site.resource \ No newline at end of file diff --git a/unilabos/ros/utils/driver_creator.py b/unilabos/ros/utils/driver_creator.py index 76c6f776..f0fdddd0 100644 --- a/unilabos/ros/utils/driver_creator.py +++ b/unilabos/ros/utils/driver_creator.py @@ -296,14 +296,14 @@ class WorkstationNodeCreator(DeviceClassCreator[T]): try: # 创建实例,额外补充一个给protocol node的字段,后面考虑取消 data["children"] = self.children - station_resource_dict = data.get("station_resource") - if station_resource_dict: + deck_dict = data.get("deck") + if deck_dict: from pylabrobot.resources import Deck, Resource plrc = PyLabRobotCreator(Deck, self.children, self.resource_tracker) - station_resource = plrc.create_instance(station_resource_dict) - data["station_resource"] = station_resource + deck = plrc.create_instance(deck_dict) + data["deck"] = deck else: - data["station_resource"] = None + data["deck"] = None self.device_instance = super(WorkstationNodeCreator, self).create_instance(data) self.post_create() return self.device_instance