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