Merge branch 'dev' into pr/169

# Conflicts:
#	unilabos/device_comms/opcua_client/client.py
#	unilabos/device_comms/opcua_client/node/uniopcua.py
#	unilabos/registry/devices/post_process_station.yaml
This commit is contained in:
ZiWei
2025-12-25 13:55:22 +08:00
70 changed files with 233242 additions and 712 deletions

View File

@@ -388,6 +388,10 @@ def main():
for ind, i in enumerate(resource_edge_info[::-1]):
source_node: ResourceDict = nodes[i["source"]]
target_node: ResourceDict = nodes[i["target"]]
if "sourceHandle" not in source_node:
continue
if "targetHandle" not in target_node:
continue
source_handle = i["sourceHandle"]
target_handle = i["targetHandle"]
source_handler_keys = [

View File

@@ -300,6 +300,10 @@ class HTTPClient:
)
if response.status_code not in [200, 201]:
logger.error(f"注册资源失败: {response.status_code}, {response.text}")
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
logger.error(f"注册资源失败: {response.text}")
return response
def request_startup_json(self) -> Optional[Dict[str, Any]]:

View File

@@ -5,7 +5,6 @@ from typing import Any, Union, List, Dict, Callable, Optional, Tuple
from pydantic import BaseModel
from opcua import Client, ua
from opcua.ua import NodeClass
import pandas as pd
import os
@@ -13,7 +12,7 @@ from unilabos.device_comms.opcua_client.node.uniopcua import Base as OpcUaNodeBa
from unilabos.device_comms.opcua_client.node.uniopcua import Variable, Method, NodeType, DataType
from unilabos.device_comms.universal_driver import UniversalDriver
from unilabos.utils.log import logger
from unilabos.devices.workstation.post_process.decks import post_process_deck
class OpcUaNode(BaseModel):
name: str
@@ -117,6 +116,8 @@ class BaseClient(UniversalDriver):
_variables_to_find: Dict[str, Dict[str, Any]] = {}
_name_mapping: Dict[str, str] = {} # 英文名到中文名的映射
_reverse_mapping: Dict[str, str] = {} # 中文名到英文名的映射
# 直接缓存已找到的 ua.Node 对象,避免因字符串 NodeId 格式导致订阅失败
_found_node_objects: Dict[str, Any] = {}
def __init__(self):
super().__init__()
@@ -210,6 +211,8 @@ class BaseClient(UniversalDriver):
if node_type == NodeType.VARIABLE:
self._node_registry[node_name] = Variable(self.client, node_name, node_id_str, data_type)
logger.info(f"✓ 找到变量节点: '{node_name}', NodeId: {node_id_str}, DataType: {data_type}")
# 缓存真实的 ua.Node 对象用于订阅
self._found_node_objects[node_name] = node
elif node_type == NodeType.METHOD:
# 对于方法节点需要获取父节点ID
parent_node = node.get_parent()
@@ -499,6 +502,8 @@ class BaseClient(UniversalDriver):
err = result_dict.get("error")
print(f"读取 {node_name} 返回值 = {val} (类型: {type(val).__name__}), 错误 = {err}")
print(f"读取 {node_name} 返回值 = {val} (类型: {type(val).__name__}, 错误 = {err}")
return val, err
except Exception as e:
print(f"解析读取结果失败: {e}, 原始结果: {result_str}")
@@ -1171,13 +1176,51 @@ class BaseClient(UniversalDriver):
class OpcUaClient(BaseClient):
def __init__(self, url: str, config_path: str = None, username: str = None, password: str = None, refresh_interval: float = 1.0):
def __init__(
self,
url: str,
deck: Optional[Union[post_process_deck, Dict[str, Any]]] = None,
config_path: str = None,
username: str = None,
password: str = None,
use_subscription: bool = True,
cache_timeout: float = 5.0,
subscription_interval: int = 500,
*args,
**kwargs,
):
# 降低OPCUA库的日志级别
import logging
logging.getLogger("opcua").setLevel(logging.WARNING)
super().__init__()
# ===== 关键修改:参照 BioyondWorkstation 处理 deck =====
super().__init__()
# 处理 deck 参数
if deck is None:
self.deck = post_process_deck(setup=True)
elif isinstance(deck, dict):
self.deck = post_process_deck(setup=True)
elif hasattr(deck, 'children'):
self.deck = deck
else:
raise ValueError(f"deck 参数类型不支持: {type(deck)}")
if self.deck is None:
raise ValueError("Deck 配置不能为空")
# 统计仓库信息
warehouse_count = 0
if hasattr(self.deck, 'children'):
warehouse_count = len(self.deck.children)
logger.info(f"Deck 初始化完成,加载 {warehouse_count} 个资源")
# OPC UA 客户端初始化
client = Client(url)
if username and password:
@@ -1185,92 +1228,152 @@ class OpcUaClient(BaseClient):
client.set_password(password)
self._set_client(client)
self._connect()
# 节点值缓存和刷新相关属性
self._node_values = {} # 缓存节点值
self._refresh_interval = refresh_interval # 刷新间隔(秒)
self._refresh_running = False
self._refresh_thread = None
# 订阅相关属性
self._use_subscription = use_subscription
self._subscription = None
self._subscription_handles = {}
self._subscription_interval = subscription_interval
# 缓存相关属性
self._node_values = {} # 修改为支持时间戳的缓存结构
self._cache_timeout = cache_timeout
# 连接状态监控
self._connection_check_interval = 30.0 # 连接检查间隔(秒)
self._connection_monitor_running = False
self._connection_monitor_thread = None
# 添加线程锁保护OPC UA客户端的并发访问
import threading
self._client_lock = threading.RLock()
# 连接到服务器
self._connect()
# 如果提供了配置文件路径,则加载配置并注册工作流
if config_path:
self.load_config(config_path)
# 延迟启动节点值刷新线程,确保节点查找完成
# 注意刷新线程会在所有节点注册后由load_config调用
# 暂时不在这里启动,避免在节点未找到时就开始刷新
# self.start_node_refresh()
# 启动连接监控
self._start_connection_monitor()
def _register_nodes_as_attributes(self):
"""将所有节点注册为实例属性可以通过self.node_name访问"""
for node_name, node in self._node_registry.items():
# 检查node_id是否有效
if not node.node_id or node.node_id == "":
logger.warning(f"⚠ 节点 '{node_name}' 的 node_id 为空,跳过注册为属性")
continue
# 检查是否有对应的英文名称
eng_name = self._reverse_mapping.get(node_name)
if eng_name:
# 如果有对应的英文名称,使用英文名称作为属性名
attr_name = eng_name
else:
# 如果没有对应的英文名称,使用原始名称,但替换空格和特殊字符
attr_name = node_name.replace(' ', '_').replace('-', '_')
def _connect(self) -> None:
"""连接到OPC UA服务器"""
logger.info('尝试连接到 OPC UA 服务器...')
if self.client:
try:
self.client.connect()
logger.info('✓ 客户端已连接!')
# 创建获取节点值的属性方法,使用中文名称获取节点值
def create_property_getter(node_key):
def getter(self):
# 优先从缓存获取值
if node_key in self._node_values:
return self._node_values[node_key]
# 缓存中没有则直接读取
value, _ = self.use_node(node_key).read()
return value
return getter
# 连接后开始查找节点
if self._variables_to_find:
self._find_nodes()
# 使用property装饰器将方法注册为类属性
setattr(OpcUaClient, attr_name, property(create_property_getter(node_name)))
logger.debug(f"已注册节点 '{node_name}' 为属性 '{attr_name}', NodeId: {node.node_id}")
# 如果启用订阅模式,设置订阅
if self._use_subscription:
self._setup_subscriptions()
else:
logger.info("订阅模式已禁用,将使用按需读取模式")
def refresh_node_values(self):
"""刷新所有节点的值到缓存"""
if not self.client:
logger.warning("客户端未初始化,无法刷新节点值")
except Exception as e:
logger.error(f'客户端连接失败: {e}')
raise
else:
raise ValueError('客户端未初始化')
class SubscriptionHandler:
"""freeopcua订阅处理器必须实现 datachange_notification 方法"""
def __init__(self, outer):
self.outer = outer
def datachange_notification(self, node, val, data):
# 委托给外层类的处理函数
try:
self.outer._on_subscription_datachange(node, val, data)
except Exception as e:
logger.error(f"订阅数据回调处理失败: {e}")
# 可选:事件通知占位,避免库调用时报缺失
def event_notification(self, event):
pass
def _setup_subscriptions(self):
"""设置 OPC UA 订阅"""
if not self.client or not self._use_subscription:
return
# 使用锁保护客户端访问
with self._client_lock:
try:
# 简单检查连接状态,如果不连接会抛出异常
self.client.get_namespace_array()
logger.info(f"开始设置订阅 (发布间隔: {self._subscription_interval}ms)...")
# 创建订阅
handler = OpcUaClient.SubscriptionHandler(self)
self._subscription = self.client.create_subscription(
self._subscription_interval,
handler
)
# 为所有变量节点创建监控项
subscribed_count = 0
skipped_count = 0
for node_name, node in self._node_registry.items():
# 只为变量节点创建订阅
if node.type == NodeType.VARIABLE and node.node_id:
try:
# 优先使用在查找阶段缓存的真实 ua.Node 对象
ua_node = self._found_node_objects.get(node_name)
if ua_node is None:
ua_node = self.client.get_node(node.node_id)
handle = self._subscription.subscribe_data_change(ua_node)
self._subscription_handles[node_name] = handle
subscribed_count += 1
logger.debug(f"✓ 已订阅节点: {node_name}")
except Exception as e:
skipped_count += 1
logger.warning(f"✗ 订阅节点 {node_name} 失败: {e}")
else:
skipped_count += 1
logger.info(f"订阅设置完成: 成功 {subscribed_count} 个, 跳过 {skipped_count}")
except Exception as e:
logger.warning(f"客户端连接异常,无法刷新节点值: {e}")
return
logger.error(f"设置订阅失败: {e}")
traceback.print_exc()
# 订阅失败时回退到按需读取模式
self._use_subscription = False
logger.warning("订阅模式设置失败,已自动切换到按需读取模式")
for node_name, node in self._node_registry.items():
try:
# 跳过node_id为空的节点
if not node.node_id or node.node_id == "":
continue
def _on_subscription_datachange(self, node, val, data):
"""订阅数据变化处理器(供内部 SubscriptionHandler 调用)"""
try:
node_id = str(node.nodeid)
current_time = time.time()
# 查找对应的节点名称
for node_name, node_obj in self._node_registry.items():
if node_obj.node_id == node_id:
self._node_values[node_name] = {
'value': val,
'timestamp': current_time,
'source': 'subscription'
}
logger.debug(f"订阅更新: {node_name} = {val}")
break
except Exception as e:
logger.error(f"处理订阅数据失败: {e}")
if hasattr(node, 'read'):
value, error = node.read()
if not error:
self._node_values[node_name] = value
#logger.debug(f"已刷新节点 '{node_name}' 的值: {value}")
except Exception as e:
# 降低日志级别,避免刷新线程产生大量错误日志
logger.debug(f"刷新节点 '{node_name}' 失败: {e}")
def get_node_value(self, name, use_cache=True, force_read=False):
"""
获取节点值(智能缓存版本)
def get_node_value(self, name):
"""获取节点值,支持中文名英文名"""
# 如果提供的是英文名,转换为中文名
参数:
name: 节点名称(支持中文名英文名
use_cache: 是否使用缓存
force_read: 是否强制从服务器读取(忽略缓存)
"""
# 处理名称映射
if name in self._name_mapping:
chinese_name = self._name_mapping[name]
# 优先从缓存获取值
@@ -1290,15 +1393,63 @@ class OpcUaClient(BaseClient):
else:
raise ValueError(f"未找到名称为 '{name}' 的节点")
elif name in self._node_registry:
chinese_name = name
else:
raise ValueError(f"未找到名称为 '{name}' 的节点")
# 如果强制读取,直接从服务器读取
if force_read:
with self._client_lock:
value, _ = self.use_node(chinese_name).read()
# 更新缓存
self._node_values[chinese_name] = {
'value': value,
'timestamp': time.time(),
'source': 'forced_read'
}
return value
# 检查缓存
if use_cache and chinese_name in self._node_values:
cache_entry = self._node_values[chinese_name]
cache_age = time.time() - cache_entry['timestamp']
# 如果是订阅模式,缓存永久有效(由订阅更新)
# 如果是按需读取模式,检查缓存超时
if cache_entry.get('source') == 'subscription' or cache_age < self._cache_timeout:
logger.debug(f"从缓存读取: {chinese_name} = {cache_entry['value']} (age: {cache_age:.2f}s, source: {cache_entry.get('source', 'unknown')})")
return cache_entry['value']
# 缓存过期或不存在,从服务器读取
with self._client_lock:
try:
value, error = self.use_node(chinese_name).read()
if not error:
# 更新缓存
self._node_values[chinese_name] = {
'value': value,
'timestamp': time.time(),
'source': 'on_demand_read'
}
return value
else:
logger.warning(f"读取节点 {chinese_name} 失败")
return None
except Exception as e:
logger.error(f"读取节点 {chinese_name} 出错: {e}")
return None
def set_node_value(self, name, value):
"""设置节点值,支持中文名和英文名"""
# 如果提供的是英文名,转换为中文名
"""
设置节点值
写入成功后会立即更新本地缓存
"""
# 处理名称映射
if name in self._name_mapping:
chinese_name = self._name_mapping[name]
node = self.use_node(chinese_name)
# 如果提供的是中文名,直接使用
elif name in self._node_registry:
node = self.use_node(name)
chinese_name = name
else:
raise ValueError(f"未找到名称为 '{name}' 的节点")
@@ -1317,6 +1468,50 @@ class OpcUaClient(BaseClient):
logger.info(f"节点值刷新线程已启动,刷新间隔: {self._refresh_interval}")
while self._refresh_running:
with self._client_lock:
try:
node = self.use_node(chinese_name)
error = node.write(value)
if not error:
# 写入成功,立即更新缓存
self._node_values[chinese_name] = {
'value': value,
'timestamp': time.time(),
'source': 'write'
}
logger.debug(f"写入成功: {chinese_name} = {value}")
return True
else:
logger.warning(f"写入节点 {chinese_name} 失败")
return False
except Exception as e:
logger.error(f"写入节点 {chinese_name} 出错: {e}")
return False
def _check_connection(self) -> bool:
"""检查连接状态"""
try:
with self._client_lock:
if self.client:
# 尝试获取命名空间数组来验证连接
self.client.get_namespace_array()
return True
except Exception as e:
logger.warning(f"连接检查失败: {e}")
return False
return False
def _connection_monitor_worker(self):
"""连接监控线程工作函数"""
self._connection_monitor_running = True
logger.info(f"连接监控线程已启动 (检查间隔: {self._connection_check_interval}秒)")
reconnect_attempts = 0
max_reconnect_attempts = 5
while self._connection_monitor_running:
try:
self.refresh_node_values()
except Exception as e:
@@ -1329,6 +1524,49 @@ class OpcUaClient(BaseClient):
"""启动节点值刷新线程"""
if self._refresh_thread is not None and self._refresh_thread.is_alive():
logger.warning("节点值刷新线程已在运行")
# 检查连接状态
if not self._check_connection():
logger.warning("检测到连接断开,尝试重新连接...")
reconnect_attempts += 1
if reconnect_attempts <= max_reconnect_attempts:
try:
# 尝试重新连接
with self._client_lock:
if self.client:
try:
self.client.disconnect()
except:
pass
self.client.connect()
logger.info("✓ 重新连接成功")
# 重新设置订阅
if self._use_subscription:
self._setup_subscriptions()
reconnect_attempts = 0
except Exception as e:
logger.error(f"重新连接失败 (尝试 {reconnect_attempts}/{max_reconnect_attempts}): {e}")
time.sleep(5) # 重连失败后等待5秒
else:
logger.error(f"达到最大重连次数 ({max_reconnect_attempts}),停止重连")
self._connection_monitor_running = False
else:
# 连接正常,重置重连计数
reconnect_attempts = 0
except Exception as e:
logger.error(f"连接监控出错: {e}")
# 等待下次检查
time.sleep(self._connection_check_interval)
def _start_connection_monitor(self):
"""启动连接监控线程"""
if self._connection_monitor_thread is not None and self._connection_monitor_thread.is_alive():
logger.warning("连接监控线程已在运行")
return
import threading
@@ -1342,6 +1580,94 @@ class OpcUaClient(BaseClient):
self._refresh_thread.join(timeout=2.0)
logger.info("节点值刷新线程已停止")
self._connection_monitor_thread = threading.Thread(
target=self._connection_monitor_worker,
daemon=True,
name="OpcUaConnectionMonitor"
)
self._connection_monitor_thread.start()
def _stop_connection_monitor(self):
"""停止连接监控线程"""
self._connection_monitor_running = False
if self._connection_monitor_thread and self._connection_monitor_thread.is_alive():
self._connection_monitor_thread.join(timeout=2.0)
logger.info("连接监控线程已停止")
def read_node(self, node_name: str) -> str:
"""
读取节点值的便捷方法(使用缓存)
返回JSON格式字符串
"""
try:
# 使用get_node_value方法自动处理缓存
value = self.get_node_value(node_name, use_cache=True)
# 获取缓存信息
chinese_name = self._name_mapping.get(node_name, node_name)
cache_info = self._node_values.get(chinese_name, {})
result = {
"value": value,
"error": False,
"node_name": node_name,
"timestamp": time.time(),
"cache_age": time.time() - cache_info.get('timestamp', time.time()),
"source": cache_info.get('source', 'unknown')
}
return json.dumps(result)
except Exception as e:
logger.error(f"读取节点 {node_name} 失败: {e}")
result = {
"value": None,
"error": True,
"node_name": node_name,
"error_message": str(e),
"timestamp": time.time()
}
return json.dumps(result)
def get_cache_stats(self) -> Dict[str, Any]:
"""获取缓存统计信息"""
current_time = time.time()
stats = {
'total_cached_nodes': len(self._node_values),
'subscription_nodes': 0,
'on_demand_nodes': 0,
'expired_nodes': 0,
'cache_timeout': self._cache_timeout,
'using_subscription': self._use_subscription
}
for node_name, cache_entry in self._node_values.items():
source = cache_entry.get('source', 'unknown')
cache_age = current_time - cache_entry['timestamp']
if source == 'subscription':
stats['subscription_nodes'] += 1
elif source in ['on_demand_read', 'forced_read', 'write']:
stats['on_demand_nodes'] += 1
if cache_age > self._cache_timeout:
stats['expired_nodes'] += 1
return stats
def print_cache_stats(self):
"""打印缓存统计信息"""
stats = self.get_cache_stats()
print("\n" + "="*80)
print("缓存统计信息")
print("="*80)
print(f"总缓存节点数: {stats['total_cached_nodes']}")
print(f"订阅模式: {'启用' if stats['using_subscription'] else '禁用'}")
print(f" - 订阅更新节点: {stats['subscription_nodes']}")
print(f" - 按需读取节点: {stats['on_demand_nodes']}")
print(f" - 已过期节点: {stats['expired_nodes']}")
print(f"缓存超时时间: {stats['cache_timeout']}")
print("="*80 + "\n")
def load_config(self, config_path: str) -> None:
"""从JSON配置文件加载并注册工作流"""
try:
@@ -1350,46 +1676,43 @@ class OpcUaClient(BaseClient):
# 处理节点注册
if "register_node_list_from_csv_path" in config_data:
# 获取配置文件所在目录
config_dir = os.path.dirname(os.path.abspath(config_path))
# 处理CSV路径如果是相对路径则相对于配置文件所在目录
if "path" in config_data["register_node_list_from_csv_path"]:
csv_path = config_data["register_node_list_from_csv_path"]["path"]
if not os.path.isabs(csv_path):
# 转换为绝对路径
csv_path = os.path.join(config_dir, csv_path)
config_data["register_node_list_from_csv_path"]["path"] = csv_path
# 直接使用字典
self.register_node_list_from_csv_path(**config_data["register_node_list_from_csv_path"])
# 立即执行节点查找(如果客户端已连接)
if self.client and self._variables_to_find:
logger.info("CSV加载完成开始查找服务器节点...")
self._find_nodes()
# 处理工作流创建
if "create_flow" in config_data:
# 直接传递字典列表
self.create_workflow_from_json(config_data["create_flow"])
# 将工作流注册为实例方法
self.register_workflows_as_methods()
# 将所有节点注册为属性(只注册已找到的节点)
self._register_nodes_as_attributes()
# 打印节点注册统计
# 打印统计信息
found_count = len(self._node_registry)
total_count = len(self._variables_to_find)
if found_count < total_count:
logger.warning(f"节点查找完成:找到 {found_count}/{total_count} 个节点")
logger.warning(f"{total_count - found_count} 个节点未找到,这些节点的操作将会失败")
else:
logger.info(f"✓ 节点查找完成:所有 {found_count} 个节点均已找到")
# 现在启动节点值刷新线程
# self.start_node_refresh()
# 如果使用订阅模式,重新设置订阅(确保新节点被订阅)
if self._use_subscription and found_count > 0:
self._setup_subscriptions()
logger.info(f"成功从 {config_path} 加载配置")
except Exception as e:
@@ -1488,9 +1811,71 @@ class OpcUaClient(BaseClient):
# 停止刷新线程
self.stop_node_refresh()
"""断开连接并清理资源"""
logger.info("正在断开连接...")
# 停止连接监控
self._stop_connection_monitor()
# 删除订阅
if self._subscription:
try:
with self._client_lock:
self._subscription.delete()
logger.info("订阅已删除")
except Exception as e:
logger.warning(f"删除订阅失败: {e}")
# 断开客户端连接
if self.client:
self.client.disconnect()
logger.info("OPC UA client disconnected")
try:
with self._client_lock:
self.client.disconnect()
logger.info("✓ OPC UA 客户端已断开连接")
except Exception as e:
logger.error(f"断开连接失败: {e}")
def _register_nodes_as_attributes(self):
"""将所有节点注册为实例属性"""
for node_name, node in self._node_registry.items():
if not node.node_id or node.node_id == "":
logger.warning(f"⚠ 节点 '{node_name}' 的 node_id 为空,跳过注册为属性")
continue
eng_name = self._reverse_mapping.get(node_name)
attr_name = eng_name if eng_name else node_name.replace(' ', '_').replace('-', '_')
def create_property_getter(node_key):
def getter(self):
return self.get_node_value(node_key, use_cache=True)
return getter
setattr(OpcUaClient, attr_name, property(create_property_getter(node_name)))
logger.debug(f"已注册节点 '{node_name}' 为属性 '{attr_name}'")
def post_init(self, ros_node):
"""ROS2 节点就绪后的初始化"""
if not (hasattr(self, 'deck') and self.deck):
return
if not (hasattr(ros_node, 'resource_tracker') and ros_node.resource_tracker):
logger.warning("resource_tracker 不存在,无法注册 deck")
return
# 1. 本地注册(必需)
ros_node.resource_tracker.add_resource(self.deck)
# 2. 上传云端
try:
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode
ROS2DeviceNode.run_async_func(
ros_node.update_resource,
True,
resources=[self.deck]
)
logger.info("Deck 已上传到云端")
except Exception as e:
logger.error(f"上传失败: {e}")
if __name__ == '__main__':
@@ -1523,6 +1908,7 @@ if __name__ == '__main__':
grab_complete = client.get_node_value("grab_complete")
reaction_tank = client.get_node_value("reaction_tank_number")
raw_tank = client.get_node_value("raw_tank_number")
print(f"\n执行后状态检查 (使用英文节点名):")
print(f" - 抓取完成状态: {grab_complete}")
print(f" - 当前反应罐号码: {reaction_tank}")

View File

View File

@@ -0,0 +1,712 @@
#!/usr/bin/env python3
import asyncio
import json
import subprocess
import sys
import threading
from typing import Optional, Dict, Any
import logging
import requests
import websockets
logging.getLogger("zeep").setLevel(logging.WARNING)
logging.getLogger("zeep.xsd.schema").setLevel(logging.WARNING)
logging.getLogger("zeep.xsd.schema.schema").setLevel(logging.WARNING)
from onvif import ONVIFCamera # 新增ONVIF PTZ 控制
# ======================= 独立的 PTZController =======================
class PTZController:
def __init__(self, host: str, port: int, user: str, password: str):
"""
:param host: 摄像机 IP 或域名(和 RTSP 的一样即可)
:param port: ONVIF 端口(多数为 80看你的设备
:param user: 摄像机用户名
:param password: 摄像机密码
"""
self.host = host
self.port = port
self.user = user
self.password = password
self.cam: Optional[ONVIFCamera] = None
self.media_service = None
self.ptz_service = None
self.profile = None
def connect(self) -> bool:
"""
建立 ONVIF 连接并初始化 PTZ 能力,失败返回 False不抛异常
Note: 首先 pip install onvif-zeep
"""
try:
self.cam = ONVIFCamera(self.host, self.port, self.user, self.password)
self.media_service = self.cam.create_media_service()
self.ptz_service = self.cam.create_ptz_service()
profiles = self.media_service.GetProfiles()
if not profiles:
print("[PTZ] No media profiles found on camera.", file=sys.stderr)
return False
self.profile = profiles[0]
return True
except Exception as e:
print(f"[PTZ] Failed to init ONVIF PTZ: {e}", file=sys.stderr)
return False
def _continuous_move(self, pan: float, tilt: float, zoom: float, duration: float) -> bool:
"""
连续移动一段时间(秒),之后自动停止。
此函数为阻塞模式:只有在 Stop 调用结束后,才返回 True/False。
"""
if not self.ptz_service or not self.profile:
print("[PTZ] _continuous_move: ptz_service or profile not ready", file=sys.stderr)
return False
# 进入前先强行停一下,避免前一次残留动作
self._force_stop()
req = self.ptz_service.create_type("ContinuousMove")
req.ProfileToken = self.profile.token
req.Velocity = {
"PanTilt": {"x": pan, "y": tilt},
"Zoom": {"x": zoom},
}
try:
print(f"[PTZ] ContinuousMove start: pan={pan}, tilt={tilt}, zoom={zoom}, duration={duration}", file=sys.stderr)
self.ptz_service.ContinuousMove(req)
except Exception as e:
print(f"[PTZ] ContinuousMove failed: {e}", file=sys.stderr)
return False
# 阻塞等待:这里决定“运动时间”
import time
wait_seconds = max(2 * duration, 0.0)
time.sleep(wait_seconds)
# 运动完成后强制停止
return self._force_stop()
def stop(self) -> bool:
"""
阻塞调用 Stop带重试成功 True失败 False。
"""
return self._force_stop()
# ------- 对外动作接口(给 CameraController 调用) -------
# 所有接口都为“阻塞模式”:只有在运动 + Stop 完成后才返回 True/False
def move_up(self, speed: float = 0.5, duration: float = 1.0) -> bool:
print(f"[PTZ] move_up called, speed={speed}, duration={duration}", file=sys.stderr)
return self._continuous_move(pan=0.0, tilt=+speed, zoom=0.0, duration=duration)
def move_down(self, speed: float = 0.5, duration: float = 1.0) -> bool:
print(f"[PTZ] move_down called, speed={speed}, duration={duration}", file=sys.stderr)
return self._continuous_move(pan=0.0, tilt=-speed, zoom=0.0, duration=duration)
def move_left(self, speed: float = 0.2, duration: float = 1.0) -> bool:
print(f"[PTZ] move_left called, speed={speed}, duration={duration}", file=sys.stderr)
return self._continuous_move(pan=-speed, tilt=0.0, zoom=0.0, duration=duration)
def move_right(self, speed: float = 0.2, duration: float = 1.0) -> bool:
print(f"[PTZ] move_right called, speed={speed}, duration={duration}", file=sys.stderr)
return self._continuous_move(pan=+speed, tilt=0.0, zoom=0.0, duration=duration)
# ------- 占位的变倍接口(当前设备不支持) -------
def zoom_in(self, speed: float = 0.2, duration: float = 1.0) -> bool:
"""
当前设备不支持变倍;保留方法只是避免上层调用时报错。
"""
print("[PTZ] zoom_in is disabled for this device.", file=sys.stderr)
return False
def zoom_out(self, speed: float = 0.2, duration: float = 1.0) -> bool:
"""
当前设备不支持变倍;保留方法只是避免上层调用时报错。
"""
print("[PTZ] zoom_out is disabled for this device.", file=sys.stderr)
return False
def _force_stop(self, retries: int = 3, delay: float = 0.1) -> bool:
"""
尝试多次调用 Stop作为“强制停止”手段。
:param retries: 重试次数
:param delay: 每次重试间隔(秒)
"""
if not self.ptz_service or not self.profile:
print("[PTZ] _force_stop: ptz_service or profile not ready", file=sys.stderr)
return False
import time
last_error = None
for i in range(retries):
try:
print(f"[PTZ] _force_stop: calling Stop(), attempt={i+1}", file=sys.stderr)
self.ptz_service.Stop({"ProfileToken": self.profile.token})
print("[PTZ] _force_stop: Stop() returned OK", file=sys.stderr)
return True
except Exception as e:
last_error = e
print(f"[PTZ] _force_stop: Stop() failed at attempt {i+1}: {e}", file=sys.stderr)
time.sleep(delay)
print(f"[PTZ] _force_stop: all {retries} attempts failed, last error: {last_error}", file=sys.stderr)
return False
# ======================= CameraController加入 PTZ =======================
class CameraController:
"""
Uni-Lab-OS 摄像头驱动driver 形式)
启动 Uni-Lab-OS 后,立即开始推流
- WebSocket 信令:通过 signal_backend_url 连接到后端
例如: wss://sciol.ac.cn/api/realtime/signal/host/<host_id>
- 媒体服务器:通过 rtmp_url / webrtc_api / webrtc_stream_url
当前配置为 SRS与独立 HostSimulator 独立运行脚本保持一致。
"""
def __init__(
self,
host_id: str = "demo-host",
# 1信令后端WebSocket
signal_backend_url: str = "wss://sciol.ac.cn/api/realtime/signal/host",
# 2媒体后端RTMP + WebRTC API
rtmp_url: str = "rtmp://srs.sciol.ac.cn:4499/live/camera-01",
webrtc_api: str = "https://srs.sciol.ac.cn/rtc/v1/play/",
webrtc_stream_url: str = "webrtc://srs.sciol.ac.cn:4500/live/camera-01",
camera_rtsp_url: str = "",
# 3PTZ 控制相关ONVIF
ptz_host: str = "", # 一般就是摄像头 IP比如 "192.168.31.164"
ptz_port: int = 80, # ONVIF 端口,不一定是 80按实际情况改
ptz_user: str = "", # admin
ptz_password: str = "", # admin123
):
self.host_id = host_id
self.camera_rtsp_url = camera_rtsp_url
# 拼接最终的 WebSocket URL.../host/<host_id>
signal_backend_url = signal_backend_url.rstrip("/")
if not signal_backend_url.endswith("/host"):
signal_backend_url = signal_backend_url + "/host"
self.signal_backend_url = f"{signal_backend_url}/{host_id}"
# 媒体服务器配置
self.rtmp_url = rtmp_url
self.webrtc_api = webrtc_api
self.webrtc_stream_url = webrtc_stream_url
# PTZ 控制
self.ptz_host = ptz_host
self.ptz_port = ptz_port
self.ptz_user = ptz_user
self.ptz_password = ptz_password
self._ptz: Optional[PTZController] = None
self._init_ptz_if_possible()
# 运行时状态
self._ws: Optional[object] = None
self._ffmpeg_process: Optional[subprocess.Popen] = None
self._running = False
self._loop_task: Optional[asyncio.Future] = None
# 事件循环 & 线程
self._loop: Optional[asyncio.AbstractEventLoop] = None
self._loop_thread: Optional[threading.Thread] = None
try:
self.start()
except Exception as e:
print(f"[CameraController] __init__ auto start failed: {e}", file=sys.stderr)
# ------------------------ PTZ 初始化 ------------------------
# ------------------------ PTZ 公开动作方法(一个动作一个函数) ------------------------
def ptz_move_up(self, speed: float = 0.5, duration: float = 1.0) -> bool:
print(f"[CameraController] ptz_move_up called, speed={speed}, duration={duration}")
return self._ptz.move_up(speed=speed, duration=duration)
def ptz_move_down(self, speed: float = 0.5, duration: float = 1.0) -> bool:
print(f"[CameraController] ptz_move_down called, speed={speed}, duration={duration}")
return self._ptz.move_down(speed=speed, duration=duration)
def ptz_move_left(self, speed: float = 0.2, duration: float = 1.0) -> bool:
print(f"[CameraController] ptz_move_left called, speed={speed}, duration={duration}")
return self._ptz.move_left(speed=speed, duration=duration)
def ptz_move_right(self, speed: float = 0.2, duration: float = 1.0) -> bool:
print(f"[CameraController] ptz_move_right called, speed={speed}, duration={duration}")
return self._ptz.move_right(speed=speed, duration=duration)
def zoom_in(self, speed: float = 0.2, duration: float = 1.0) -> bool:
"""
当前设备不支持变倍;保留方法只是避免上层调用时报错。
"""
print("[PTZ] zoom_in is disabled for this device.", file=sys.stderr)
return False
def zoom_out(self, speed: float = 0.2, duration: float = 1.0) -> bool:
"""
当前设备不支持变倍;保留方法只是避免上层调用时报错。
"""
print("[PTZ] zoom_out is disabled for this device.", file=sys.stderr)
return False
def ptz_stop(self):
if self._ptz is None:
print("[CameraController] PTZ not initialized.", file=sys.stderr)
return
self._ptz.stop()
def _init_ptz_if_possible(self):
"""
根据 ptz_host / user / password 初始化 PTZ
如果配置信息不全则不启用 PTZ静默
"""
if not (self.ptz_host and self.ptz_user and self.ptz_password):
return
ctrl = PTZController(
host=self.ptz_host,
port=self.ptz_port,
user=self.ptz_user,
password=self.ptz_password,
)
if ctrl.connect():
self._ptz = ctrl
else:
self._ptz = None
# ---------------------------------------------------------------------
# 对外暴露的方法:供 Uni-Lab-OS 调用
# ---------------------------------------------------------------------
def start(self, config: Optional[Dict[str, Any]] = None):
"""
启动 Camera 连接 & 消息循环,并在启动时就开启 FFmpeg 推流,
"""
if self._running:
return {"status": "already_running", "host_id": self.host_id}
# 应用 config 覆盖(如果有)
if config:
self.camera_rtsp_url = config.get("camera_rtsp_url", self.camera_rtsp_url)
cfg_host_id = config.get("host_id")
if cfg_host_id:
self.host_id = cfg_host_id
signal_backend_url = config.get("signal_backend_url")
if signal_backend_url:
signal_backend_url = signal_backend_url.rstrip("/")
if not signal_backend_url.endswith("/host"):
signal_backend_url = signal_backend_url + "/host"
self.signal_backend_url = f"{signal_backend_url}/{self.host_id}"
self.rtmp_url = config.get("rtmp_url", self.rtmp_url)
self.webrtc_api = config.get("webrtc_api", self.webrtc_api)
self.webrtc_stream_url = config.get(
"webrtc_stream_url", self.webrtc_stream_url
)
# PTZ 相关配置也允许通过 config 注入
self.ptz_host = config.get("ptz_host", self.ptz_host)
self.ptz_port = int(config.get("ptz_port", self.ptz_port))
self.ptz_user = config.get("ptz_user", self.ptz_user)
self.ptz_password = config.get("ptz_password", self.ptz_password)
self._init_ptz_if_possible()
self._running = True
# === start 时启动 FFmpeg 推流 ===
self._start_ffmpeg()
# 创建新的事件循环和线程(用于 WebSocket 信令)
self._loop = asyncio.new_event_loop()
def loop_runner(loop: asyncio.AbstractEventLoop):
asyncio.set_event_loop(loop)
try:
loop.run_forever()
except Exception as e:
print(f"[CameraController] event loop error: {e}", file=sys.stderr)
self._loop_thread = threading.Thread(
target=loop_runner, args=(self._loop,), daemon=True
)
self._loop_thread.start()
self._loop_task = asyncio.run_coroutine_threadsafe(
self._run_main_loop(), self._loop
)
return {
"status": "started",
"host_id": self.host_id,
"signal_backend_url": self.signal_backend_url,
"rtmp_url": self.rtmp_url,
"webrtc_api": self.webrtc_api,
"webrtc_stream_url": self.webrtc_stream_url,
}
def stop(self) -> Dict[str, Any]:
"""
停止推流 & 断开 WebSocket并关闭事件循环线程。
"""
self._running = False
self._stop_ffmpeg()
if self._ws and self._loop is not None:
async def close_ws():
try:
await self._ws.close()
except Exception as e:
print(
f"[CameraController] error when closing WebSocket: {e}",
file=sys.stderr,
)
asyncio.run_coroutine_threadsafe(close_ws(), self._loop)
if self._loop_task is not None:
if not self._loop_task.done():
self._loop_task.cancel()
try:
self._loop_task.result()
except asyncio.CancelledError:
pass
except Exception as e:
print(
f"[CameraController] main loop task error in stop(): {e}",
file=sys.stderr,
)
finally:
self._loop_task = None
if self._loop is not None:
try:
self._loop.call_soon_threadsafe(self._loop.stop)
except Exception as e:
print(
f"[CameraController] error when stopping event loop: {e}",
file=sys.stderr,
)
if self._loop_thread is not None:
try:
self._loop_thread.join(timeout=5)
except Exception as e:
print(
f"[CameraController] error when joining loop thread: {e}",
file=sys.stderr,
)
finally:
self._loop_thread = None
self._ws = None
self._loop = None
return {"status": "stopped", "host_id": self.host_id}
def get_status(self) -> Dict[str, Any]:
"""
查询当前状态,方便在 Uni-Lab-OS 中做监控。
"""
ws_closed = None
if self._ws is not None:
ws_closed = getattr(self._ws, "closed", None)
if ws_closed is None:
websocket_connected = self._ws is not None
else:
websocket_connected = (self._ws is not None) and (not ws_closed)
return {
"host_id": self.host_id,
"running": self._running,
"websocket_connected": websocket_connected,
"ffmpeg_running": bool(
self._ffmpeg_process and self._ffmpeg_process.poll() is None
),
"signal_backend_url": self.signal_backend_url,
"rtmp_url": self.rtmp_url,
}
# ---------------------------------------------------------------------
# 内部实现逻辑WebSocket 循环 / FFmpeg / WebRTC Offer 处理
# ---------------------------------------------------------------------
async def _run_main_loop(self):
try:
while self._running:
try:
async with websockets.connect(self.signal_backend_url) as ws:
self._ws = ws
await self._recv_loop()
except asyncio.CancelledError:
raise
except Exception as e:
if self._running:
print(
f"[CameraController] WebSocket connection error: {e}",
file=sys.stderr,
)
await asyncio.sleep(3)
except asyncio.CancelledError:
pass
async def _recv_loop(self):
assert self._ws is not None
ws = self._ws
async for message in ws:
try:
data = json.loads(message)
except json.JSONDecodeError:
print(
f"[CameraController] received non-JSON message: {message}",
file=sys.stderr,
)
continue
try:
await self._handle_message(data)
except Exception as e:
print(
f"[CameraController] error while handling message {data}: {e}",
file=sys.stderr,
)
async def _handle_message(self, data: Dict[str, Any]):
"""
处理来自信令后端的消息:
- command: start_stream / stop_stream / ptz_xxx
- type: offer (WebRTC)
"""
cmd = data.get("command")
# ---------- 推流控制 ----------
if cmd == "start_stream":
try:
self._start_ffmpeg()
except Exception as e:
print(
f"[CameraController] error when starting FFmpeg on start_stream: {e}",
file=sys.stderr,
)
return
if cmd == "stop_stream":
try:
self._stop_ffmpeg()
except Exception as e:
print(
f"[CameraController] error when stopping FFmpeg on stop_stream: {e}",
file=sys.stderr,
)
return
# # ---------- PTZ 控制 ----------
# # 例如信令可以发:
# # {"command": "ptz_move", "direction": "down", "speed": 0.5, "duration": 0.5}
# if cmd == "ptz_move":
# if self._ptz is None:
# # 没有初始化 PTZ静默忽略或打印一条
# print("[CameraController] PTZ not initialized.", file=sys.stderr)
# return
# direction = data.get("direction", "")
# speed = float(data.get("speed", 0.5))
# duration = float(data.get("duration", 0.5))
# try:
# if direction == "up":
# self._ptz.move_up(speed=speed, duration=duration)
# elif direction == "down":
# self._ptz.move_down(speed=speed, duration=duration)
# elif direction == "left":
# self._ptz.move_left(speed=speed, duration=duration)
# elif direction == "right":
# self._ptz.move_right(speed=speed, duration=duration)
# elif direction == "zoom_in":
# self._ptz.zoom_in(speed=speed, duration=duration)
# elif direction == "zoom_out":
# self._ptz.zoom_out(speed=speed, duration=duration)
# elif direction == "stop":
# self._ptz.stop()
# else:
# # 未知方向,忽略
# pass
# except Exception as e:
# print(
# f"[CameraController] error when handling PTZ move: {e}",
# file=sys.stderr,
# )
# return
# ---------- WebRTC Offer ----------
if data.get("type") == "offer":
offer_sdp = data.get("sdp", "")
camera_id = data.get("cameraId", "camera-01")
try:
answer_sdp = await self._handle_webrtc_offer(offer_sdp)
except Exception as e:
print(
f"[CameraController] error when handling WebRTC offer: {e}",
file=sys.stderr,
)
return
if self._ws:
answer_payload = {
"type": "answer",
"sdp": answer_sdp,
"cameraId": camera_id,
"hostId": self.host_id,
}
try:
await self._ws.send(json.dumps(answer_payload))
except Exception as e:
print(
f"[CameraController] error when sending WebRTC answer: {e}",
file=sys.stderr,
)
# ------------------------ FFmpeg 相关 ------------------------
def _start_ffmpeg(self):
if self._ffmpeg_process and self._ffmpeg_process.poll() is None:
return
cmd = [
"ffmpeg",
"-rtsp_transport", "tcp",
"-i", self.camera_rtsp_url,
"-c:v", "libx264",
"-preset", "ultrafast",
"-tune", "zerolatency",
"-profile:v", "baseline",
"-b:v", "1M",
"-maxrate", "1M",
"-bufsize", "2M",
"-g", "10",
"-keyint_min", "10",
"-sc_threshold", "0",
"-pix_fmt", "yuv420p",
"-x264-params", "bframes=0",
"-c:a", "aac",
"-ar", "44100",
"-ac", "1",
"-b:a", "64k",
"-f", "flv",
self.rtmp_url,
]
try:
self._ffmpeg_process = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
shell=False,
)
except Exception as e:
print(f"[CameraController] failed to start FFmpeg: {e}", file=sys.stderr)
self._ffmpeg_process = None
raise
def _stop_ffmpeg(self):
proc = self._ffmpeg_process
if proc and proc.poll() is None:
try:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
try:
proc.kill()
try:
proc.wait(timeout=2)
except subprocess.TimeoutExpired:
print(
f"[CameraController] FFmpeg process did not exit even after kill (pid={proc.pid})",
file=sys.stderr,
)
except Exception as e:
print(
f"[CameraController] failed to kill FFmpeg process: {e}",
file=sys.stderr,
)
except Exception as e:
print(
f"[CameraController] error when stopping FFmpeg: {e}",
file=sys.stderr,
)
self._ffmpeg_process = None
# ------------------------ WebRTC Offer 相关 ------------------------
async def _handle_webrtc_offer(self, offer_sdp: str) -> str:
payload = {
"api": self.webrtc_api,
"streamurl": self.webrtc_stream_url,
"sdp": offer_sdp,
}
headers = {"Content-Type": "application/json"}
def _do_request():
return requests.post(
self.webrtc_api,
json=payload,
headers=headers,
timeout=10,
)
try:
loop = asyncio.get_running_loop()
resp = await loop.run_in_executor(None, _do_request)
except Exception as e:
print(
f"[CameraController] failed to send offer to media server: {e}",
file=sys.stderr,
)
raise
try:
resp.raise_for_status()
except Exception as e:
print(
f"[CameraController] media server HTTP error: {e}, "
f"status={resp.status_code}, body={resp.text[:200]}",
file=sys.stderr,
)
raise
try:
data = resp.json()
except Exception as e:
print(
f"[CameraController] failed to parse media server JSON: {e}, "
f"raw={resp.text[:200]}",
file=sys.stderr,
)
raise
answer_sdp = data.get("sdp", "")
if not answer_sdp:
msg = f"empty SDP from media server: {data}"
print(f"[CameraController] {msg}", file=sys.stderr)
raise RuntimeError(msg)
return answer_sdp

View File

@@ -0,0 +1,401 @@
#!/usr/bin/env python3
import asyncio
import json
import subprocess
import sys
import threading
from typing import Optional, Dict, Any
import requests
import websockets
class CameraController:
"""
Uni-Lab-OS 摄像头驱动Linux USB 摄像头版,无 PTZ
- WebSocket 信令signal_backend_url 连接到后端
例如: wss://sciol.ac.cn/api/realtime/signal/host/<host_id>
- 媒体服务器RTMP 推流到 rtmp_urlWebRTC offer 转发到 SRS 的 webrtc_api
- 视频源:本地 USB 摄像头V4L2默认 /dev/video0
"""
def __init__(
self,
host_id: str = "demo-host",
signal_backend_url: str = "wss://sciol.ac.cn/api/realtime/signal/host",
rtmp_url: str = "rtmp://srs.sciol.ac.cn:4499/live/camera-01",
webrtc_api: str = "https://srs.sciol.ac.cn/rtc/v1/play/",
webrtc_stream_url: str = "webrtc://srs.sciol.ac.cn:4500/live/camera-01",
video_device: str = "/dev/video0",
width: int = 1280,
height: int = 720,
fps: int = 30,
video_bitrate: str = "1500k",
audio_device: Optional[str] = None, # 比如 "hw:1,0",没有音频就保持 None
audio_bitrate: str = "64k",
):
self.host_id = host_id
# 拼接最终 WebSocket URL.../host/<host_id>
signal_backend_url = signal_backend_url.rstrip("/")
if not signal_backend_url.endswith("/host"):
signal_backend_url = signal_backend_url + "/host"
self.signal_backend_url = f"{signal_backend_url}/{host_id}"
# 媒体服务器配置
self.rtmp_url = rtmp_url
self.webrtc_api = webrtc_api
self.webrtc_stream_url = webrtc_stream_url
# 本地采集配置
self.video_device = video_device
self.width = int(width)
self.height = int(height)
self.fps = int(fps)
self.video_bitrate = video_bitrate
self.audio_device = audio_device
self.audio_bitrate = audio_bitrate
# 运行时状态
self._ws: Optional[object] = None
self._ffmpeg_process: Optional[subprocess.Popen] = None
self._running = False
self._loop_task: Optional[asyncio.Future] = None
# 事件循环 & 线程
self._loop: Optional[asyncio.AbstractEventLoop] = None
self._loop_thread: Optional[threading.Thread] = None
try:
self.start()
except Exception as e:
print(f"[CameraController] __init__ auto start failed: {e}", file=sys.stderr)
# ---------------------------------------------------------------------
# 对外方法
# ---------------------------------------------------------------------
def start(self, config: Optional[Dict[str, Any]] = None):
if self._running:
return {"status": "already_running", "host_id": self.host_id}
# 应用 config 覆盖(如果有)
if config:
cfg_host_id = config.get("host_id")
if cfg_host_id:
self.host_id = cfg_host_id
signal_backend_url = config.get("signal_backend_url")
if signal_backend_url:
signal_backend_url = signal_backend_url.rstrip("/")
if not signal_backend_url.endswith("/host"):
signal_backend_url = signal_backend_url + "/host"
self.signal_backend_url = f"{signal_backend_url}/{self.host_id}"
self.rtmp_url = config.get("rtmp_url", self.rtmp_url)
self.webrtc_api = config.get("webrtc_api", self.webrtc_api)
self.webrtc_stream_url = config.get("webrtc_stream_url", self.webrtc_stream_url)
self.video_device = config.get("video_device", self.video_device)
self.width = int(config.get("width", self.width))
self.height = int(config.get("height", self.height))
self.fps = int(config.get("fps", self.fps))
self.video_bitrate = config.get("video_bitrate", self.video_bitrate)
self.audio_device = config.get("audio_device", self.audio_device)
self.audio_bitrate = config.get("audio_bitrate", self.audio_bitrate)
self._running = True
print("[CameraController] start(): starting FFmpeg streaming...", file=sys.stderr)
self._start_ffmpeg()
self._loop = asyncio.new_event_loop()
def loop_runner(loop: asyncio.AbstractEventLoop):
asyncio.set_event_loop(loop)
try:
loop.run_forever()
except Exception as e:
print(f"[CameraController] event loop error: {e}", file=sys.stderr)
self._loop_thread = threading.Thread(target=loop_runner, args=(self._loop,), daemon=True)
self._loop_thread.start()
self._loop_task = asyncio.run_coroutine_threadsafe(self._run_main_loop(), self._loop)
return {
"status": "started",
"host_id": self.host_id,
"signal_backend_url": self.signal_backend_url,
"rtmp_url": self.rtmp_url,
"webrtc_api": self.webrtc_api,
"webrtc_stream_url": self.webrtc_stream_url,
"video_device": self.video_device,
"width": self.width,
"height": self.height,
"fps": self.fps,
"video_bitrate": self.video_bitrate,
"audio_device": self.audio_device,
}
def stop(self) -> Dict[str, Any]:
self._running = False
# 先取消主任务(让 ws connect/sleep 尽快退出)
if self._loop_task is not None and not self._loop_task.done():
self._loop_task.cancel()
# 停止推流
self._stop_ffmpeg()
# 关闭 WebSocket在 loop 中执行)
if self._ws and self._loop is not None:
async def close_ws():
try:
await self._ws.close()
except Exception as e:
print(f"[CameraController] error closing WebSocket: {e}", file=sys.stderr)
try:
asyncio.run_coroutine_threadsafe(close_ws(), self._loop)
except Exception:
pass
# 停止事件循环
if self._loop is not None:
try:
self._loop.call_soon_threadsafe(self._loop.stop)
except Exception as e:
print(f"[CameraController] error stopping loop: {e}", file=sys.stderr)
# 等待线程退出
if self._loop_thread is not None:
try:
self._loop_thread.join(timeout=5)
except Exception as e:
print(f"[CameraController] error joining loop thread: {e}", file=sys.stderr)
self._ws = None
self._loop_task = None
self._loop = None
self._loop_thread = None
return {"status": "stopped", "host_id": self.host_id}
def get_status(self) -> Dict[str, Any]:
ws_closed = None
if self._ws is not None:
ws_closed = getattr(self._ws, "closed", None)
if ws_closed is None:
websocket_connected = self._ws is not None
else:
websocket_connected = (self._ws is not None) and (not ws_closed)
return {
"host_id": self.host_id,
"running": self._running,
"websocket_connected": websocket_connected,
"ffmpeg_running": bool(self._ffmpeg_process and self._ffmpeg_process.poll() is None),
"signal_backend_url": self.signal_backend_url,
"rtmp_url": self.rtmp_url,
"video_device": self.video_device,
"width": self.width,
"height": self.height,
"fps": self.fps,
"video_bitrate": self.video_bitrate,
}
# ---------------------------------------------------------------------
# WebSocket / 信令
# ---------------------------------------------------------------------
async def _run_main_loop(self):
print("[CameraController] main loop started", file=sys.stderr)
try:
while self._running:
try:
async with websockets.connect(self.signal_backend_url) as ws:
self._ws = ws
print(f"[CameraController] WebSocket connected: {self.signal_backend_url}", file=sys.stderr)
await self._recv_loop()
except asyncio.CancelledError:
raise
except Exception as e:
if self._running:
print(f"[CameraController] WebSocket connection error: {e}", file=sys.stderr)
await asyncio.sleep(3)
except asyncio.CancelledError:
pass
finally:
print("[CameraController] main loop exited", file=sys.stderr)
async def _recv_loop(self):
assert self._ws is not None
ws = self._ws
async for message in ws:
try:
data = json.loads(message)
except json.JSONDecodeError:
print(f"[CameraController] non-JSON message: {message}", file=sys.stderr)
continue
try:
await self._handle_message(data)
except Exception as e:
print(f"[CameraController] error handling message {data}: {e}", file=sys.stderr)
async def _handle_message(self, data: Dict[str, Any]):
cmd = data.get("command")
if cmd == "start_stream":
self._start_ffmpeg()
return
if cmd == "stop_stream":
self._stop_ffmpeg()
return
if data.get("type") == "offer":
offer_sdp = data.get("sdp", "")
camera_id = data.get("cameraId", "camera-01")
answer_sdp = await self._handle_webrtc_offer(offer_sdp)
if self._ws:
answer_payload = {
"type": "answer",
"sdp": answer_sdp,
"cameraId": camera_id,
"hostId": self.host_id,
}
await self._ws.send(json.dumps(answer_payload))
# ---------------------------------------------------------------------
# FFmpeg 推流V4L2 USB 摄像头)
# ---------------------------------------------------------------------
def _start_ffmpeg(self):
if self._ffmpeg_process and self._ffmpeg_process.poll() is None:
return
# 兼容性优先:不强制输入像素格式;失败再通过外部调整 width/height/fps
video_size = f"{self.width}x{self.height}"
cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel",
"warning",
# video input
"-f", "v4l2",
"-framerate", str(self.fps),
"-video_size", video_size,
"-i", self.video_device,
]
# optional audio input
if self.audio_device:
cmd += [
"-f", "alsa",
"-i", self.audio_device,
"-c:a", "aac",
"-b:a", self.audio_bitrate,
"-ar", "44100",
"-ac", "1",
]
else:
cmd += ["-an"]
# video encode + rtmp out
cmd += [
"-c:v", "libx264",
"-preset", "ultrafast",
"-tune", "zerolatency",
"-profile:v", "baseline",
"-pix_fmt", "yuv420p",
"-b:v", self.video_bitrate,
"-maxrate", self.video_bitrate,
"-bufsize", "2M",
"-g", str(max(self.fps, 10)),
"-keyint_min", str(max(self.fps, 10)),
"-sc_threshold", "0",
"-x264-params", "bframes=0",
"-f", "flv",
self.rtmp_url,
]
print(f"[CameraController] starting FFmpeg: {' '.join(cmd)}", file=sys.stderr)
try:
# 不再丢弃日志,至少能看到 ffmpeg 报错(调试很关键)
self._ffmpeg_process = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=sys.stderr,
shell=False,
)
except Exception as e:
self._ffmpeg_process = None
print(f"[CameraController] failed to start FFmpeg: {e}", file=sys.stderr)
def _stop_ffmpeg(self):
proc = self._ffmpeg_process
if proc and proc.poll() is None:
try:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
except Exception as e:
print(f"[CameraController] error stopping FFmpeg: {e}", file=sys.stderr)
self._ffmpeg_process = None
# ---------------------------------------------------------------------
# WebRTC offer -> SRS
# ---------------------------------------------------------------------
async def _handle_webrtc_offer(self, offer_sdp: str) -> str:
payload = {
"api": self.webrtc_api,
"streamurl": self.webrtc_stream_url,
"sdp": offer_sdp,
}
headers = {"Content-Type": "application/json"}
def _do_post():
return requests.post(self.webrtc_api, json=payload, headers=headers, timeout=10)
loop = asyncio.get_running_loop()
resp = await loop.run_in_executor(None, _do_post)
resp.raise_for_status()
data = resp.json()
answer_sdp = data.get("sdp", "")
if not answer_sdp:
raise RuntimeError(f"empty SDP from media server: {data}")
return answer_sdp
if __name__ == "__main__":
# 直接运行用于手动测试
c = CameraController(
host_id="demo-host",
video_device="/dev/video0",
width=1280,
height=720,
fps=30,
video_bitrate="1500k",
audio_device=None,
)
try:
while True:
asyncio.sleep(1)
except KeyboardInterrupt:
c.stop()

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
import time
import json
from cameraUSB import CameraController
def main():
# 按你的实际情况改
cfg = dict(
host_id="demo-host",
signal_backend_url="wss://sciol.ac.cn/api/realtime/signal/host",
rtmp_url="rtmp://srs.sciol.ac.cn:4499/live/camera-01",
webrtc_api="https://srs.sciol.ac.cn/rtc/v1/play/",
webrtc_stream_url="webrtc://srs.sciol.ac.cn:4500/live/camera-01",
video_device="/dev/video7",
width=1280,
height=720,
fps=30,
video_bitrate="1500k",
audio_device=None,
)
c = CameraController(**cfg)
# 可选:如果你不想依赖 __init__ 自动 start可以这样显式调用
# c = CameraController(host_id=cfg["host_id"])
# c.start(cfg)
run_seconds = 30 # 测试运行时长
t0 = time.time()
try:
while True:
st = c.get_status()
print(json.dumps(st, ensure_ascii=False, indent=2))
if time.time() - t0 >= run_seconds:
break
time.sleep(2)
except KeyboardInterrupt:
print("Interrupted, stopping...")
finally:
print("Stopping controller...")
c.stop()
print("Done.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,36 @@
import cv2
# 推荐把 @ 进行 URL 编码:@ -> %40
RTSP_URL = "rtsp://admin:admin123@192.168.31.164:554/stream1"
OUTPUT_IMAGE = "rtsp_test_frame.jpg"
def main():
print(f"尝试连接 RTSP 流: {RTSP_URL}")
cap = cv2.VideoCapture(RTSP_URL)
if not cap.isOpened():
print("错误:无法打开 RTSP 流,请检查:")
print(" 1. IP/端口是否正确")
print(" 2. 账号密码(尤其是 @ 是否已转成 %40是否正确")
print(" 3. 摄像头是否允许当前主机访问(同一网段、防火墙等)")
return
print("连接成功,开始读取一帧...")
ret, frame = cap.read()
if not ret or frame is None:
print("错误:已连接但未能读取到帧数据(可能是码流未开启或网络抖动)")
cap.release()
return
# 保存当前帧
success = cv2.imwrite(OUTPUT_IMAGE, frame)
cap.release()
if success:
print(f"成功截取一帧并保存为: {OUTPUT_IMAGE}")
else:
print("错误:写入图片失败,请检查磁盘权限/路径")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,21 @@
# run_camera_push.py
import time
from cameraDriver import CameraController # 这里根据你的文件名调整
if __name__ == "__main__":
controller = CameraController(
host_id="demo-host",
signal_backend_url="wss://sciol.ac.cn/api/realtime/signal/host",
rtmp_url="rtmp://srs.sciol.ac.cn:4499/live/camera-01",
webrtc_api="https://srs.sciol.ac.cn/rtc/v1/play/",
webrtc_stream_url="webrtc://srs.sciol.ac.cn:4500/live/camera-01",
camera_rtsp_url="rtsp://admin:admin123@192.168.31.164:554/stream1",
)
try:
while True:
status = controller.get_status()
print(status)
time.sleep(5)
except KeyboardInterrupt:
controller.stop()

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
使用 CameraController 来测试 PTZ
让摄像头按顺序向下、向上、向左、向右运动几次。
"""
import time
import sys
# 根据你的工程结构修改导入路径:
# 假设 CameraController 定义在 cameraController.py 里
from cameraDriver import CameraController
def main():
# === 根据你的实际情况填 IP、端口、账号密码 ===
ptz_host = "192.168.31.164"
ptz_port = 2020 # 注意要和你单独测试 PTZController 时保持一致
ptz_user = "admin"
ptz_password = "admin123"
# 1. 创建 CameraController 实例
cam = CameraController(
# 其他摄像机相关参数按你类的 __init__ 来补充
ptz_host=ptz_host,
ptz_port=ptz_port,
ptz_user=ptz_user,
ptz_password=ptz_password,
)
# 2. 启动 / 初始化(如果你的 CameraController 有 start(config) 之类的接口)
# 这里给一个最小的 config重点是 PTZ 相关字段
config = {
"ptz_host": ptz_host,
"ptz_port": ptz_port,
"ptz_user": ptz_user,
"ptz_password": ptz_password,
}
try:
cam.start(config)
except Exception as e:
print(f"[TEST] CameraController start() 失败: {e}", file=sys.stderr)
return
# 这里可以判断一下内部 _ptz 是否初始化成功(如果你对 CameraController 做了封装)
if getattr(cam, "_ptz", None) is None:
print("[TEST] CameraController 内部 PTZ 未初始化成功,请检查 ptz_host/port/user/password 配置。", file=sys.stderr)
return
# 3. 依次调用 CameraController 的 PTZ 方法
# 这里假设你在 CameraController 中提供了这几个对外方法:
# ptz_move_down / ptz_move_up / ptz_move_left / ptz_move_right
# 如果你命名不一样,把下面调用名改成你的即可。
print("向下移动(通过 CameraController...")
cam.ptz_move_down(speed=0.5, duration=1.0)
time.sleep(1)
print("向上移动(通过 CameraController...")
cam.ptz_move_up(speed=0.5, duration=1.0)
time.sleep(1)
print("向左移动(通过 CameraController...")
cam.ptz_move_left(speed=0.5, duration=1.0)
time.sleep(1)
print("向右移动(通过 CameraController...")
cam.ptz_move_right(speed=0.5, duration=1.0)
time.sleep(1)
print("测试结束。")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,50 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
测试 cameraDriver.py中的 PTZController 类,让摄像头按顺序运动几次
"""
import time
from cameraDriver import PTZController
def main():
# 根据你的实际情况填 IP、端口、账号密码
host = "192.168.31.164"
port = 80
user = "admin"
password = "admin123"
ptz = PTZController(host=host, port=port, user=user, password=password)
# 1. 连接摄像头
if not ptz.connect():
print("连接 PTZ 失败,检查 IP/用户名/密码/端口。")
return
# 2. 依次测试几个动作
# 每个动作之间 sleep 一下方便观察
print("向下移动...")
ptz.move_down(speed=0.5, duration=1.0)
time.sleep(1)
print("向上移动...")
ptz.move_up(speed=0.5, duration=1.0)
time.sleep(1)
print("向左移动...")
ptz.move_left(speed=0.5, duration=1.0)
time.sleep(1)
print("向右移动...")
ptz.move_right(speed=0.5, duration=1.0)
time.sleep(1)
print("测试结束。")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,954 @@
[
{
"uuid": "3b6f33ffbf734014bcc20e3c63e124d4",
"Code": "ZX-58-1250",
"Name": "Tip头适配器 1250uL",
"SummaryName": "Tip头适配器 1250uL",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 128,
"WidthNum": 85,
"HeightNum": 20,
"DepthNum": 4,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 1,
"HoleRow": 1,
"HoleDiameter": 0,
"Volume": 1250,
"ImagePath": "/images/20220624015044.jpg",
"QRCode": null,
"Qty": 10,
"CreateName": null,
"CreateTime": "2021-12-30 16:03:52.6583727",
"UpdateName": null,
"UpdateTime": "2022-06-24 13:50:44.8123474",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "7c822592b360451fb59690e49ac6b181",
"Code": "ZX-58-300",
"Name": "ZHONGXI 适配器 300uL",
"SummaryName": "ZHONGXI 适配器 300uL",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 127,
"WidthNum": 85,
"HeightNum": 81,
"DepthNum": 4,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 1,
"HoleRow": 1,
"HoleDiameter": 0,
"Volume": 300,
"ImagePath": "/images/20220623102838.jpg",
"QRCode": null,
"Qty": 10,
"CreateName": null,
"CreateTime": "2021-12-30 16:07:53.7453351",
"UpdateName": null,
"UpdateTime": "2022-06-23 10:28:38.6190575",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "8cc3dce884ac41c09f4570d0bcbfb01c",
"Code": "ZX-58-10",
"Name": "吸头10ul 适配器",
"SummaryName": "吸头10ul 适配器",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 128,
"WidthNum": 85,
"HeightNum": 72.3,
"DepthNum": 0,
"StandardHeight": 0,
"PipetteHeight": 0,
"HoleColum": 1,
"HoleRow": 1,
"HoleDiameter": 127,
"Volume": 1000,
"ImagePath": "",
"QRCode": null,
"Qty": 10,
"CreateName": null,
"CreateTime": "2021-12-30 16:37:40.7073733",
"UpdateName": null,
"UpdateTime": "2025-05-30 15:17:01.8231737",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 0,
"YSpacing": 0,
"materialEnum": 0,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "7960f49ddfe9448abadda89bd1556936",
"Code": "ZX-001-1250",
"Name": "1250μL Tip头",
"SummaryName": "1250μL Tip头",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 118.09,
"WidthNum": 80.7,
"HeightNum": 107.67,
"DepthNum": 100,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 7.95,
"Volume": 1250,
"ImagePath": "/images/20220623102536.jpg",
"QRCode": null,
"Qty": 96,
"CreateName": null,
"CreateTime": "2021-12-30 20:53:27.8591195",
"UpdateName": null,
"UpdateTime": "2022-06-23 10:25:36.2592442",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "45f2ed3ad925484d96463d675a0ebf66",
"Code": "ZX-001-10",
"Name": "10μL Tip头",
"SummaryName": "10μL Tip头",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 120.98,
"WidthNum": 82.12,
"HeightNum": 67,
"DepthNum": 39.1,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 5,
"Volume": 10,
"ImagePath": "/images/20221119041031.jpg",
"QRCode": null,
"Qty": -21,
"CreateName": null,
"CreateTime": "2021-12-30 20:56:53.462015",
"UpdateName": null,
"UpdateTime": "2022-11-19 16:10:31.126801",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "068b3815e36b4a72a59bae017011b29f",
"Code": "ZX-001-10+",
"Name": "10μL加长 Tip头",
"SummaryName": "10μL加长 Tip头",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 122.11,
"WidthNum": 80.05,
"HeightNum": 58.23,
"DepthNum": 45.1,
"StandardHeight": 0,
"PipetteHeight": 60,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 7,
"Volume": 10,
"ImagePath": "",
"QRCode": null,
"Qty": 42,
"CreateName": null,
"CreateTime": "2021-12-30 20:57:57.331211",
"UpdateName": null,
"UpdateTime": "2025-09-17 17:02:51.2070383",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": 1,
"Margins_X": 7.97,
"Margins_Y": 5
},
{
"uuid": "80652665f6a54402b2408d50b40398df",
"Code": "ZX-001-1000",
"Name": "1000μL Tip头",
"SummaryName": "1000μL Tip头",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 128.09,
"WidthNum": 85.8,
"HeightNum": 98,
"DepthNum": 88,
"StandardHeight": 0,
"PipetteHeight": 100,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 7.95,
"Volume": 1000,
"ImagePath": "",
"QRCode": null,
"Qty": 47,
"CreateName": null,
"CreateTime": "2021-12-30 20:59:20.5534915",
"UpdateName": null,
"UpdateTime": "2025-05-30 14:49:53.639727",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": 1,
"Margins_X": 14.5,
"Margins_Y": 11.4
},
{
"uuid": "076250742950465b9d6ea29a225dfb00",
"Code": "ZX-001-300",
"Name": "300μL Tip头",
"SummaryName": "300μL Tip头",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 122.11,
"WidthNum": 80.05,
"HeightNum": 58.23,
"DepthNum": 45.1,
"StandardHeight": 0,
"PipetteHeight": 60,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 7,
"Volume": 300,
"ImagePath": "",
"QRCode": null,
"Qty": 11,
"CreateName": null,
"CreateTime": "2021-12-30 21:00:24.7266192",
"UpdateName": null,
"UpdateTime": "2025-09-17 17:02:40.6676947",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": 1,
"Margins_X": 7.97,
"Margins_Y": 5
},
{
"uuid": "7a73bb9e5c264515a8fcbe88aed0e6f7",
"Code": "ZX-001-200",
"Name": "200μL Tip头",
"SummaryName": "200μL Tip头",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 120.98,
"WidthNum": 82.12,
"HeightNum": 66.9,
"DepthNum": 52,
"StandardHeight": 0,
"PipetteHeight": 30,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 5.5,
"Volume": 200,
"ImagePath": "",
"QRCode": null,
"Qty": 19,
"CreateName": null,
"CreateTime": "2021-12-30 21:01:17.626704",
"UpdateName": null,
"UpdateTime": "2025-05-27 11:42:24.6021522",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": 0,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "73bb9b10bc394978b70e027bf45ce2d3",
"Code": "ZX-023-0.2",
"Name": "0.2ml PCR板",
"SummaryName": "0.2ml PCR板",
"SupplyType": 1,
"Factory": "中析",
"LengthNum": 126,
"WidthNum": 86,
"HeightNum": 21.2,
"DepthNum": 15.17,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 6,
"Volume": 1000,
"ImagePath": "",
"QRCode": null,
"Qty": -12,
"CreateName": null,
"CreateTime": "2021-12-30 21:06:02.7746392",
"UpdateName": null,
"UpdateTime": "2024-02-20 16:17:16.7921748",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "ca877b8b114a4310b429d1de4aae96ee",
"Code": "ZX-019-2.2",
"Name": "2.2ml 深孔板",
"SummaryName": "2.2ml 深孔板",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 127.3,
"WidthNum": 85.35,
"HeightNum": 44,
"DepthNum": 42,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 8.2,
"Volume": 2200,
"ImagePath": "",
"QRCode": null,
"Qty": 34,
"CreateName": null,
"CreateTime": "2021-12-30 21:07:16.4538022",
"UpdateName": null,
"UpdateTime": "2023-08-12 13:11:26.3993472",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "04211a2dc93547fe9bf6121eac533650",
"Code": "ZX-58-10000",
"Name": "储液槽",
"SummaryName": "储液槽",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 125.02,
"WidthNum": 82.97,
"HeightNum": 31.2,
"DepthNum": 24,
"StandardHeight": 0,
"PipetteHeight": 0,
"HoleColum": 1,
"HoleRow": 1,
"HoleDiameter": 99.33,
"Volume": 1250,
"ImagePath": "",
"QRCode": null,
"Qty": -172,
"CreateName": null,
"CreateTime": "2021-12-31 18:37:56.7949909",
"UpdateName": null,
"UpdateTime": "2025-09-17 17:22:22.8543991",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": 0,
"Margins_X": 8.5,
"Margins_Y": 5.5
},
{
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
"Code": "ZX-58-0001",
"Name": "全裙边 PCR适配器",
"SummaryName": "全裙边 PCR适配器",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 125.42,
"WidthNum": 83.13,
"HeightNum": 15.69,
"DepthNum": 13.41,
"StandardHeight": 0,
"PipetteHeight": 0,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 5.1,
"Volume": 1250,
"ImagePath": "",
"QRCode": null,
"Qty": 100,
"CreateName": null,
"CreateTime": "2022-01-02 19:21:35.8664843",
"UpdateName": null,
"UpdateTime": "2025-09-17 17:14:36.1210193",
"IsStright": 1,
"IsGeneral": 1,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": 3,
"Margins_X": 9.78,
"Margins_Y": 7.72
},
{
"uuid": "6bdfdd7069df453896b0806df50f2f4d",
"Code": "ZX-ADP-001",
"Name": "储液槽 适配器",
"SummaryName": "储液槽 适配器",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 133,
"WidthNum": 91.8,
"HeightNum": 70,
"DepthNum": 4,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 1,
"HoleRow": 1,
"HoleDiameter": 1,
"Volume": 1250,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2022-02-16 17:31:26.413594",
"UpdateName": null,
"UpdateTime": "2023-08-12 13:10:58.786996",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 0,
"YSpacing": 0,
"materialEnum": null,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "9a439bed8f3344549643d6b3bc5a5eb4",
"Code": "ZX-002-300",
"Name": "300ul深孔板适配器",
"SummaryName": "300ul深孔板适配器",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 136.4,
"WidthNum": 93.8,
"HeightNum": 96,
"DepthNum": 7,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 8.1,
"Volume": 300,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2022-06-18 15:17:42.7917763",
"UpdateName": null,
"UpdateTime": "2023-08-12 13:10:46.1526635",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "4dc8d6ecfd0449549683b8ef815a861b",
"Code": "ZX-002-10",
"Name": "10ul专用深孔板适配器",
"SummaryName": "10ul专用深孔板适配器",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 136.5,
"WidthNum": 93.8,
"HeightNum": 121.5,
"DepthNum": 7,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 8.1,
"Volume": 10,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2022-06-30 09:37:31.0451435",
"UpdateName": null,
"UpdateTime": "2023-08-12 13:10:38.5409878",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "b01627718d3341aba649baa81c2c083c",
"Code": "Sd155",
"Name": "爱津",
"SummaryName": "爱津",
"SupplyType": 1,
"Factory": "中析",
"LengthNum": 125,
"WidthNum": 85,
"HeightNum": 64,
"DepthNum": 45.5,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 4,
"Volume": 20,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2022-11-07 08:56:30.1794274",
"UpdateName": null,
"UpdateTime": "2022-11-07 09:00:29.5496845",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "adfabfffa8f24af5abfbba67b8d0f973",
"Code": "Fhh478",
"Name": "适配器",
"SummaryName": "适配器",
"SupplyType": 2,
"Factory": "中析",
"LengthNum": 120,
"WidthNum": 90,
"HeightNum": 86,
"DepthNum": 4,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 1,
"HoleRow": 1,
"HoleDiameter": 4,
"Volume": 1000,
"ImagePath": null,
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2022-11-07 09:00:10.7579131",
"UpdateName": null,
"UpdateTime": "2022-11-07 09:00:10.7579134",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "730067cf07ae43849ddf4034299030e9",
"Code": "q1",
"Name": "废弃槽",
"SummaryName": "废弃槽",
"SupplyType": 1,
"Factory": "中析",
"LengthNum": 126.59,
"WidthNum": 84.87,
"HeightNum": 103.17,
"DepthNum": 80,
"StandardHeight": 0,
"PipetteHeight": 0,
"HoleColum": 1,
"HoleRow": 1,
"HoleDiameter": 1,
"Volume": 1250,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2023-10-14 13:15:45.8172852",
"UpdateName": null,
"UpdateTime": "2025-09-17 17:06:18.3331101",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 1,
"YSpacing": 1,
"materialEnum": 0,
"Margins_X": 2.29,
"Margins_Y": 2.64
},
{
"uuid": "57b1e4711e9e4a32b529f3132fc5931f",
"Code": "q2",
"Name": "96深孔板",
"SummaryName": "96深孔板",
"SupplyType": 1,
"Factory": "中析",
"LengthNum": 127.3,
"WidthNum": 85.35,
"HeightNum": 44,
"DepthNum": 42,
"StandardHeight": 0,
"PipetteHeight": 1,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 8.2,
"Volume": 1250,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2023-10-14 13:19:55.7225524",
"UpdateName": null,
"UpdateTime": "2025-07-03 17:28:59.0082394",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": 0,
"Margins_X": 15,
"Margins_Y": 10
},
{
"uuid": "853dcfb6226f476e8b23c250217dc7da",
"Code": "q3",
"Name": "384板",
"SummaryName": "384板",
"SupplyType": 1,
"Factory": "中析",
"LengthNum": 126.6,
"WidthNum": 84,
"HeightNum": 9.4,
"DepthNum": 8,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 24,
"HoleRow": 16,
"HoleDiameter": 3,
"Volume": 1250,
"ImagePath": null,
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2023-10-14 13:22:34.779818",
"UpdateName": null,
"UpdateTime": "2023-10-14 13:22:34.7798181",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 4.5,
"YSpacing": 4.5,
"materialEnum": null,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "01953864f6f140ccaa8ddffd4f3e46f5",
"Code": "sdfrth654",
"Name": "4道储液槽",
"SummaryName": "4道储液槽",
"SupplyType": 1,
"Factory": "中析",
"LengthNum": 100,
"WidthNum": 40,
"HeightNum": 30,
"DepthNum": 10,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 4,
"HoleRow": 8,
"HoleDiameter": 4,
"Volume": 1000,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2024-02-20 14:44:25.0021372",
"UpdateName": null,
"UpdateTime": "2025-03-31 15:09:30.7392062",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 27,
"YSpacing": 9,
"materialEnum": 0,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "026c5d5cf3d94e56b4e16b7fb53a995b",
"Code": "22",
"Name": "48孔深孔板",
"SummaryName": "48孔深孔板",
"SupplyType": 1,
"Factory": "",
"LengthNum": null,
"WidthNum": null,
"HeightNum": null,
"DepthNum": null,
"StandardHeight": null,
"PipetteHeight": null,
"HoleColum": 6,
"HoleRow": 8,
"HoleDiameter": null,
"Volume": 23,
"ImagePath": null,
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2025-03-19 09:38:09.8535874",
"UpdateName": null,
"UpdateTime": "2025-03-19 09:38:09.8536386",
"IsStright": null,
"IsGeneral": null,
"IsControl": null,
"ArmCode": null,
"XSpacing": 18.5,
"YSpacing": 9,
"materialEnum": 2,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "0f1639987b154e1fac78f4fb29a1f7c1",
"Code": "12道储液槽",
"Name": "12道储液槽",
"SummaryName": "12道储液槽",
"SupplyType": 1,
"Factory": "",
"LengthNum": 129.5,
"WidthNum": 83.047,
"HeightNum": 30.6,
"DepthNum": 26.7,
"StandardHeight": null,
"PipetteHeight": 0,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 8.04,
"Volume": 12,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2025-05-21 13:10:53.2735971",
"UpdateName": null,
"UpdateTime": "2025-09-17 17:20:40.4460256",
"IsStright": null,
"IsGeneral": null,
"IsControl": null,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": 0,
"Margins_X": 8.7,
"Margins_Y": 5.35
},
{
"uuid": "548bbc3df0d4447586f2c19d2c0c0c55",
"Code": "HPLC01",
"Name": "HPLC料盘",
"SummaryName": "HPLC料盘",
"SupplyType": 1,
"Factory": "",
"LengthNum": 0,
"WidthNum": 0,
"HeightNum": 0,
"DepthNum": 0,
"StandardHeight": null,
"PipetteHeight": 0,
"HoleColum": 7,
"HoleRow": 15,
"HoleDiameter": 0,
"Volume": 1,
"ImagePath": null,
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2025-07-12 17:10:43.2660127",
"UpdateName": null,
"UpdateTime": "2025-07-12 17:10:43.2660131",
"IsStright": null,
"IsGeneral": null,
"IsControl": null,
"ArmCode": null,
"XSpacing": 12.5,
"YSpacing": 16.5,
"materialEnum": 0,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "e146697c395e4eabb3d6b74f0dd6aaf7",
"Code": "1",
"Name": "ep适配器",
"SummaryName": "ep适配器",
"SupplyType": 1,
"Factory": "",
"LengthNum": 128.04,
"WidthNum": 85.8,
"HeightNum": 42.66,
"DepthNum": 38.08,
"StandardHeight": null,
"PipetteHeight": 0,
"HoleColum": 6,
"HoleRow": 4,
"HoleDiameter": 10.6,
"Volume": 1,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2025-09-03 13:31:54.1541015",
"UpdateName": null,
"UpdateTime": "2025-09-17 17:18:03.8051993",
"IsStright": null,
"IsGeneral": null,
"IsControl": null,
"ArmCode": null,
"XSpacing": 21,
"YSpacing": 18,
"materialEnum": 0,
"Margins_X": 3.54,
"Margins_Y": 10.5
},
{
"uuid": "a0757a90d8e44e81a68f306a608694f2",
"Code": "ZX-58-30",
"Name": "30mm适配器",
"SummaryName": "30mm适配器",
"SupplyType": 2,
"Factory": "",
"LengthNum": 132,
"WidthNum": 93.5,
"HeightNum": 30,
"DepthNum": 7,
"StandardHeight": null,
"PipetteHeight": 0,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 8.1,
"Volume": 30,
"ImagePath": null,
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2025-09-15 14:02:30.8094658",
"UpdateName": null,
"UpdateTime": "2025-09-15 14:02:30.8098183",
"IsStright": null,
"IsGeneral": null,
"IsControl": null,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": 0,
"Margins_X": 0,
"Margins_Y": 0
},
{
"uuid": "b05b3b2aafd94ec38ea0cd3215ecea8f",
"Code": "ZX-78-096",
"Name": "细菌培养皿",
"SummaryName": "细菌培养皿",
"SupplyType": 1,
"Factory": "",
"LengthNum": 124.09,
"WidthNum": 81.89,
"HeightNum": 13.67,
"DepthNum": 11.2,
"StandardHeight": null,
"PipetteHeight": 0,
"HoleColum": 12,
"HoleRow": 8,
"HoleDiameter": 6.58,
"Volume": 78,
"ImagePath": null,
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2025-09-17 17:10:54.1859566",
"UpdateName": null,
"UpdateTime": "2025-09-17 17:10:54.1859568",
"IsStright": null,
"IsGeneral": null,
"IsControl": null,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": 4,
"Margins_X": 9.28,
"Margins_Y": 6.19
}
]

View File

@@ -156,7 +156,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300TipRack",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -4323,7 +4323,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -8297,7 +8297,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -8425,7 +8425,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -12496,7 +12496,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300TipRack",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -16664,7 +16664,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -20640,7 +20640,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -20671,7 +20671,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -20799,7 +20799,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -24872,7 +24872,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -28848,7 +28848,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -28879,7 +28879,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -29007,7 +29007,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -33080,7 +33080,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -37153,7 +37153,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 50,
"size_z": 10,
@@ -41151,6 +41151,5 @@
"uuid": "730067cf07ae43849ddf4034299030e9"
}
}
],
"links": []
}
}
]

View File

@@ -0,0 +1,607 @@
[
{
"Id": "1853794d-8cc1-4268-94b8-fc83e8be3ecc",
"StartDosage": 1.0,
"EndDosage": 55.0,
"Aspiration": 0.0,
"Dispensing": 0.0,
"K": 2126.89990234375,
"B": 2085.300048828125,
"compensateEnum": 7,
"materialVolume": 10
},
{
"Id": "37a31398-499c-4df3-9bfe-ff92e6bc1427",
"StartDosage": 1.0,
"EndDosage": 303.0,
"Aspiration": -1.0,
"Dispensing": 0.0,
"K": 2229.6,
"B": 3082.7,
"compensateEnum": 7,
"materialVolume": 1000
},
{
"Id": "e602c693-e51c-4485-8788-beb3560e0599",
"StartDosage": 303.0,
"EndDosage": 400.0,
"Aspiration": -0.8,
"Dispensing": 0.0,
"K": 2156.6,
"B": 9582.1,
"compensateEnum": 7,
"materialVolume": 1000
},
{
"Id": "d7cdf777-ae58-46ab-b1ec-a5e59496bb8a",
"StartDosage": 400.0,
"EndDosage": 501.0,
"Aspiration": -1.5,
"Dispensing": 0.0,
"K": 2087.9,
"B": 37256.0,
"compensateEnum": 7,
"materialVolume": 1000
},
{
"Id": "6149a3a7-98fb-4270-83b4-4f21b5c4e8d8",
"StartDosage": 501.0,
"EndDosage": 600.0,
"Aspiration": -1.5,
"Dispensing": 0.0,
"K": 2185.0,
"B": -12375.0,
"compensateEnum": 7,
"materialVolume": 1000
},
{
"Id": "039f5735-a598-482d-b21d-b265d5e7436a",
"StartDosage": 600.0,
"EndDosage": 700.0,
"Aspiration": -6.0,
"Dispensing": 0.0,
"K": 2222.0,
"B": -30370.0,
"compensateEnum": 7,
"materialVolume": 1000
},
{
"Id": "80875977-ee0f-49f4-b10d-de429e57c5b8",
"StartDosage": 700.0,
"EndDosage": 800.0,
"Aspiration": -6.0,
"Dispensing": 0.0,
"K": 1705.0,
"B": 324436.0,
"compensateEnum": 7,
"materialVolume": 1000
},
{
"Id": "a38afc7c-9c86-4014-a669-a7d159fb0c70",
"StartDosage": 800.0,
"EndDosage": 900.0,
"Aspiration": 0.0,
"Dispensing": 0.0,
"K": 2068.0,
"B": 61331.0,
"compensateEnum": 7,
"materialVolume": 1000
},
{
"Id": "a5ce0671-8767-4752-a04c-fdbdc3c7dc91",
"StartDosage": 900.0,
"EndDosage": 1001.0,
"Aspiration": 3.0,
"Dispensing": 0.0,
"K": 2047.2,
"B": 78417.0,
"compensateEnum": 7,
"materialVolume": 1000
},
{
"Id": "14daba17-0a35-474f-9f8a-e9ea6c355eb0",
"StartDosage": 1.0,
"EndDosage": 303.0,
"Aspiration": -1.0,
"Dispensing": 0.0,
"K": 2229.6,
"B": 3082.7,
"compensateEnum": 6,
"materialVolume": 1000
},
{
"Id": "82c2439c-79f6-4f61-9518-1b1205e44027",
"StartDosage": 303.0,
"EndDosage": 400.0,
"Aspiration": -0.8,
"Dispensing": 0.0,
"K": 2156.6,
"B": 9582.1,
"compensateEnum": 6,
"materialVolume": 1000
},
{
"Id": "7981db10-4005-4c62-a22d-fac90875e91c",
"StartDosage": 400.0,
"EndDosage": 501.0,
"Aspiration": -1.5,
"Dispensing": 0.0,
"K": 2087.9,
"B": 37256.0,
"compensateEnum": 6,
"materialVolume": 1000
},
{
"Id": "ae7606fd-98fa-4236-bec4-a4d60018dbea",
"StartDosage": 501.0,
"EndDosage": 600.0,
"Aspiration": -1.5,
"Dispensing": 0.0,
"K": 2185.0,
"B": -12375.0,
"compensateEnum": 6,
"materialVolume": 1000
},
{
"Id": "ed2a2db0-77b6-4a0a-ac36-7184f0b2c2c8",
"StartDosage": 600.0,
"EndDosage": 700.0,
"Aspiration": -6.0,
"Dispensing": 0.0,
"K": 2222.0,
"B": -30370.0,
"compensateEnum": 6,
"materialVolume": 1000
},
{
"Id": "ed639da4-b02f-4d2a-825d-b47cebdfbf1b",
"StartDosage": 700.0,
"EndDosage": 800.0,
"Aspiration": -6.0,
"Dispensing": 0.0,
"K": 1705.0,
"B": 324436.0,
"compensateEnum": 6,
"materialVolume": 1000
},
{
"Id": "7e740c8a-1043-4db1-820f-2e6e77386d7f",
"StartDosage": 800.0,
"EndDosage": 900.0,
"Aspiration": 0.0,
"Dispensing": 0.0,
"K": 2068.0,
"B": 61331.0,
"compensateEnum": 6,
"materialVolume": 1000
},
{
"Id": "49b6c4fe-e11a-4056-8de7-fd9a2b81bc90",
"StartDosage": 900.0,
"EndDosage": 1001.0,
"Aspiration": 3.0,
"Dispensing": 0.0,
"K": 2047.2,
"B": 78417.0,
"compensateEnum": 6,
"materialVolume": 1000
},
{
"Id": "67dee69d-a2a9-4598-8d8d-98b211a58821",
"StartDosage": 1.0,
"EndDosage": 6.0,
"Aspiration": 0.0,
"Dispensing": 0.0,
"K": 20211.0,
"B": 10779.0,
"compensateEnum": 5,
"materialVolume": 50
},
{
"Id": "d5c1b2b0-f897-4873-86bf-0ce5f443dfd3",
"StartDosage": 6.0,
"EndDosage": 25.0,
"Aspiration": 0.0,
"Dispensing": 0.0,
"K": 20211.0,
"B": 10779.0,
"compensateEnum": 5,
"materialVolume": 50
},
{
"Id": "b2789b53-6e0e-4b83-9932-f41c83d10da8",
"StartDosage": 25.0,
"EndDosage": 50.0,
"Aspiration": 0.0,
"Dispensing": 0.0,
"K": 20015.0,
"B": 17507.0,
"compensateEnum": 5,
"materialVolume": 50
},
{
"Id": "1f0d0bbb-6ea2-4d19-8452-6824fa1f474c",
"StartDosage": 0.1,
"EndDosage": 5.0,
"Aspiration": -1.1,
"Dispensing": 0.0,
"K": 1981.1,
"B": 3498.1,
"compensateEnum": 5,
"materialVolume": 300
},
{
"Id": "c58111db-dadc-43bd-97b3-a596f441d704",
"StartDosage": 5.0,
"EndDosage": 10.0,
"Aspiration": -1.1,
"Dispensing": 0.0,
"K": 2113.3,
"B": 2810.8,
"compensateEnum": 5,
"materialVolume": 300
},
{
"Id": "a15fd33d-28cd-4bca-bd6c-018e3bafcb65",
"StartDosage": 10.0,
"EndDosage": 50.0,
"Aspiration": -0.8,
"Dispensing": 0.0,
"K": 2113.3,
"B": 2810.8,
"compensateEnum": 5,
"materialVolume": 300
},
{
"Id": "ab957383-d83d-4fcc-8373-9d8f415c3023",
"StartDosage": 50.0,
"EndDosage": 100.0,
"Aspiration": -0.1,
"Dispensing": 0.0,
"K": 2093.7,
"B": 2969.2,
"compensateEnum": 5,
"materialVolume": 300
},
{
"Id": "be6b6f79-222f-4f6f-ae73-e537f397a11e",
"StartDosage": 100.0,
"EndDosage": 150.0,
"Aspiration": 1.7,
"Dispensing": 0.0,
"K": 2093.7,
"B": 2969.2,
"compensateEnum": 5,
"materialVolume": 300
},
{
"Id": "0ab3fc05-8f9f-4dc0-a2ce-918ade17810c",
"StartDosage": 150.0,
"EndDosage": 200.0,
"Aspiration": 0.0,
"Dispensing": 0.0,
"K": 2085.0,
"B": 3548.3,
"compensateEnum": 5,
"materialVolume": 300
},
{
"Id": "43b82710-37df-4039-9513-aa49bc5bc607",
"StartDosage": 200.0,
"EndDosage": 250.0,
"Aspiration": 4.0,
"Dispensing": 0.0,
"K": 2085.0,
"B": 3548.3,
"compensateEnum": 5,
"materialVolume": 300
},
{
"Id": "2f208ffc-808f-4bf9-b443-14dbf0338d83",
"StartDosage": 250.0,
"EndDosage": 310.0,
"Aspiration": 5.3,
"Dispensing": 0.0,
"K": 2085.0,
"B": 3548.3,
"compensateEnum": 5,
"materialVolume": 300
},
{
"Id": "84bb5356-481d-41b9-a563-917e64b5e20c",
"StartDosage": 1.0,
"EndDosage": 10.0,
"Aspiration": 0.0,
"Dispensing": 0.0,
"K": 964.19,
"B": 1207.7,
"compensateEnum": 5,
"materialVolume": 1000
},
{
"Id": "67463c2c-a520-4d33-831f-e0c3cdcdec60",
"StartDosage": 10.0,
"EndDosage": 50.0,
"Aspiration": 0.5,
"Dispensing": 0.0,
"K": 964.19,
"B": 1207.7,
"compensateEnum": 5,
"materialVolume": 1000
},
{
"Id": "a752d77e-7c5d-450a-8b54-e87513facda0",
"StartDosage": 50.0,
"EndDosage": 100.0,
"Aspiration": 0.0,
"Dispensing": 0.0,
"K": 964.19,
"B": 1207.7,
"compensateEnum": 5,
"materialVolume": 1000
},
{
"Id": "d30f522a-5992-4be4-984d-0c27b9e8f410",
"StartDosage": 100.0,
"EndDosage": 300.0,
"Aspiration": 1.8,
"Dispensing": 0.0,
"K": 937.8,
"B": 3550.1,
"compensateEnum": 5,
"materialVolume": 1000
},
{
"Id": "29914cbe-ad35-4712-80b1-8c4e54f9fc15",
"StartDosage": 300.0,
"EndDosage": 500.0,
"Aspiration": 2.5,
"Dispensing": 0.0,
"K": 937.8,
"B": 3550.1,
"compensateEnum": 5,
"materialVolume": 1000
},
{
"Id": "b75b1d6d-9b53-4b5c-b6ab-640cb23491d8",
"StartDosage": 500.0,
"EndDosage": 800.0,
"Aspiration": 50.0,
"Dispensing": 0.0,
"K": 928.69,
"B": 8253.7,
"compensateEnum": 5,
"materialVolume": 1000
},
{
"Id": "1658a9de-bb62-4dd6-9715-0e8e71b27f97",
"StartDosage": 800.0,
"EndDosage": 900.0,
"Aspiration": 4.0,
"Dispensing": 0.0,
"K": 928.69,
"B": 8253.7,
"compensateEnum": 5,
"materialVolume": 1000
},
{
"Id": "4d0fec65-983d-47f6-82fe-723bb9efd42a",
"StartDosage": 900.0,
"EndDosage": 1050.0,
"Aspiration": 5.0,
"Dispensing": 0.0,
"K": 928.69,
"B": 8253.7,
"compensateEnum": 5,
"materialVolume": 1000
},
{
"Id": "f194ad17-3be3-4684-bf21-d458693e640c",
"StartDosage": 1.0,
"EndDosage": 2.0,
"Aspiration": 0.0,
"Dispensing": 0.0,
"K": 62616.0,
"B": 106.49,
"compensateEnum": 5,
"materialVolume": 10
},
{
"Id": "fa43155c-8220-4ead-bc8f-6984a25711bf",
"StartDosage": 2.0,
"EndDosage": 7.0,
"Aspiration": -0.1,
"Dispensing": 0.0,
"K": 52421.0,
"B": 20977.0,
"compensateEnum": 5,
"materialVolume": 10
},
{
"Id": "9b05eebb-ba5d-427c-bd4f-1b6745bab932",
"StartDosage": 7.0,
"EndDosage": 11.0,
"Aspiration": 0.1,
"Dispensing": 0.0,
"K": 51942.0,
"B": 21434.0,
"compensateEnum": 5,
"materialVolume": 10
},
{
"Id": "d4715f09-e24a-4ed2-b784-09256640bcf7",
"StartDosage": 0.5,
"EndDosage": 5.0,
"Aspiration": -1.1,
"Dispensing": 0.0,
"K": 1981.1,
"B": 3498.1,
"compensateEnum": 7,
"materialVolume": 300
},
{
"Id": "e37e2fad-954d-4a17-8312-e08bbde00902",
"StartDosage": 5.0,
"EndDosage": 10.0,
"Aspiration": -1.1,
"Dispensing": -0.8,
"K": 2113.3,
"B": 2810.8,
"compensateEnum": 7,
"materialVolume": 300
},
{
"Id": "642714bd-22c6-46b5-9a48-2f0bcd91d555",
"StartDosage": 10.0,
"EndDosage": 50.0,
"Aspiration": -0.8,
"Dispensing": -2.0,
"K": 2113.3,
"B": 2810.8,
"compensateEnum": 7,
"materialVolume": 300
},
{
"Id": "2fccf79f-52e5-4b6c-be6e-bdac167dd40c",
"StartDosage": 50.0,
"EndDosage": 100.0,
"Aspiration": -0.1,
"Dispensing": 0.0,
"K": 2093.7,
"B": 2969.2,
"compensateEnum": 7,
"materialVolume": 300
},
{
"Id": "34555f2c-2e11-4c45-b733-83a8185727da",
"StartDosage": 100.0,
"EndDosage": 150.0,
"Aspiration": 1.7,
"Dispensing": 0.0,
"K": 2093.7,
"B": 2969.2,
"compensateEnum": 7,
"materialVolume": 300
},
{
"Id": "9353ac79-b710-49da-a423-4bfe651ac16a",
"StartDosage": 150.0,
"EndDosage": 200.0,
"Aspiration": 0.0,
"Dispensing": 0.0,
"K": 2085.0,
"B": 3548.3,
"compensateEnum": 7,
"materialVolume": 300
},
{
"Id": "1628da53-8c86-4eff-b119-07cb7a859bb6",
"StartDosage": 200.0,
"EndDosage": 250.0,
"Aspiration": 4.0,
"Dispensing": 0.0,
"K": 2085.0,
"B": 3548.3,
"compensateEnum": 7,
"materialVolume": 300
},
{
"Id": "658913c3-2c3e-4e14-9eb3-0489b5fdee7f",
"StartDosage": 250.0,
"EndDosage": 310.0,
"Aspiration": -11.0,
"Dispensing": 0.0,
"K": 2085.0,
"B": 3548.3,
"compensateEnum": 7,
"materialVolume": 300
},
{
"Id": "f736e716-ec13-432c-ac2e-4905753ac6f9",
"StartDosage": 0.1,
"EndDosage": 5.0,
"Aspiration": -1.1,
"Dispensing": 0.0,
"K": 1981.1,
"B": 3498.1,
"compensateEnum": 6,
"materialVolume": 300
},
{
"Id": "7595eda8-f2d8-491f-bdac-69d169308ab5",
"StartDosage": 5.0,
"EndDosage": 10.0,
"Aspiration": -1.1,
"Dispensing": 0.0,
"K": 2113.3,
"B": 2810.8,
"compensateEnum": 6,
"materialVolume": 300
},
{
"Id": "42eddd0a-8394-4245-8ad3-49573b25286e",
"StartDosage": 10.0,
"EndDosage": 50.0,
"Aspiration": -0.8,
"Dispensing": 0.0,
"K": 2113.3,
"B": 2810.8,
"compensateEnum": 6,
"materialVolume": 300
},
{
"Id": "713eadfe-25c0-4ec0-acfd-900df9e12396",
"StartDosage": 50.0,
"EndDosage": 100.0,
"Aspiration": -0.1,
"Dispensing": 0.0,
"K": 2093.7,
"B": 2969.2,
"compensateEnum": 6,
"materialVolume": 300
},
{
"Id": "f602c7bd-bdcf-4be0-9d77-a16d409bc64b",
"StartDosage": 100.0,
"EndDosage": 150.0,
"Aspiration": 1.7,
"Dispensing": 0.0,
"K": 2093.7,
"B": 2969.2,
"compensateEnum": 6,
"materialVolume": 300
},
{
"Id": "b91867e5-f0a2-4bbe-b37e-aec9837b019e",
"StartDosage": 150.0,
"EndDosage": 200.0,
"Aspiration": 0.0,
"Dispensing": 0.0,
"K": 2085.0,
"B": 3548.3,
"compensateEnum": 6,
"materialVolume": 300
},
{
"Id": "bd2e39d7-eb93-4d40-b0b4-2aac6b5678f3",
"StartDosage": 200.0,
"EndDosage": 250.0,
"Aspiration": 4.0,
"Dispensing": 0.0,
"K": 2085.0,
"B": 3548.3,
"compensateEnum": 6,
"materialVolume": 300
},
{
"Id": "52e20b7f-f519-434f-86bb-a48238c290d1",
"StartDosage": 250.0,
"EndDosage": 310.0,
"Aspiration": 5.3,
"Dispensing": 0.0,
"K": 2085.0,
"B": 3548.3,
"compensateEnum": 6,
"materialVolume": 300
}
]

View File

@@ -0,0 +1,794 @@
[
{
"uuid": "3b6f33ffbf734014bcc20e3c63e124d4",
"Code": "ZX-58-1250",
"Name": "Tip头适配器 1250uL",
"SummaryName": "Tip头适配器 1250uL",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 128,
"WidthNum": 85,
"HeightNum": 20,
"DepthNum": 4,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 1,
"HoleRow": 1,
"ChannelNum": 1,
"HoleDiameter": 0,
"Volume": 1250,
"ImagePath": "/images/20220624015044.jpg",
"QRCode": null,
"Qty": 10,
"CreateName": null,
"CreateTime": "2021-12-30 16:03:52.6583727",
"UpdateName": null,
"UpdateTime": "2022-06-24 13:50:44.8123474",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null
},
{
"uuid": "7c822592b360451fb59690e49ac6b181",
"Code": "ZX-58-300",
"Name": "ZHONGXI 适配器 300uL",
"SummaryName": "ZHONGXI 适配器 300uL",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 127,
"WidthNum": 85,
"HeightNum": 81,
"DepthNum": 4,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 1,
"HoleRow": 1,
"ChannelNum": 1,
"HoleDiameter": 0,
"Volume": 300,
"ImagePath": "/images/20220623102838.jpg",
"QRCode": null,
"Qty": 10,
"CreateName": null,
"CreateTime": "2021-12-30 16:07:53.7453351",
"UpdateName": null,
"UpdateTime": "2022-06-23 10:28:38.6190575",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null
},
{
"uuid": "8cc3dce884ac41c09f4570d0bcbfb01c",
"Code": "ZX-58-10",
"Name": "吸头10ul 适配器",
"SummaryName": "吸头10ul 适配器",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 128,
"WidthNum": 85,
"HeightNum": 81,
"DepthNum": 4,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 1,
"HoleRow": 1,
"ChannelNum": 1,
"HoleDiameter": 127,
"Volume": 1000,
"ImagePath": "/images/20221115010348.jpg",
"QRCode": null,
"Qty": 10,
"CreateName": null,
"CreateTime": "2021-12-30 16:37:40.7073733",
"UpdateName": null,
"UpdateTime": "2022-11-15 13:03:48.1679642",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 1,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null
},
{
"uuid": "7960f49ddfe9448abadda89bd1556936",
"Code": "ZX-001-1250",
"Name": "1250μL Tip头",
"SummaryName": "1250μL Tip头",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 118.09,
"WidthNum": 80.7,
"HeightNum": 107.67,
"DepthNum": 100,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"ChannelNum": 8,
"HoleDiameter": 7.95,
"Volume": 1250,
"ImagePath": "/images/20220623102536.jpg",
"QRCode": null,
"Qty": 96,
"CreateName": null,
"CreateTime": "2021-12-30 20:53:27.8591195",
"UpdateName": null,
"UpdateTime": "2022-06-23 10:25:36.2592442",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null
},
{
"uuid": "45f2ed3ad925484d96463d675a0ebf66",
"Code": "ZX-001-10",
"Name": "10μL Tip头",
"SummaryName": "10μL Tip头",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 120.98,
"WidthNum": 82.12,
"HeightNum": 67,
"DepthNum": 39.1,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"ChannelNum": 8,
"HoleDiameter": 5,
"Volume": 1000,
"ImagePath": "/images/20221119041031.jpg",
"QRCode": null,
"Qty": -21,
"CreateName": null,
"CreateTime": "2021-12-30 20:56:53.462015",
"UpdateName": null,
"UpdateTime": "2022-11-19 16:10:31.126801",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 1,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null
},
{
"uuid": "068b3815e36b4a72a59bae017011b29f",
"Code": "ZX-001-10+",
"Name": "10μL加长 Tip头",
"SummaryName": "10μL加长 Tip头",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 120.98,
"WidthNum": 82.12,
"HeightNum": 50.3,
"DepthNum": 45.8,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"ChannelNum": 8,
"HoleDiameter": 5,
"Volume": 20,
"ImagePath": "/images/20220718120113.jpg",
"QRCode": null,
"Qty": 42,
"CreateName": null,
"CreateTime": "2021-12-30 20:57:57.331211",
"UpdateName": null,
"UpdateTime": "2022-07-18 12:01:13.2131453",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null
},
{
"uuid": "80652665f6a54402b2408d50b40398df",
"Code": "ZX-001-1000",
"Name": "1000μL Tip头",
"SummaryName": "1000μL Tip头",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 118.09,
"WidthNum": 80.7,
"HeightNum": 107.67,
"DepthNum": 88,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"ChannelNum": 8,
"HoleDiameter": 7.95,
"Volume": 1000,
"ImagePath": "",
"QRCode": null,
"Qty": 47,
"CreateName": null,
"CreateTime": "2021-12-30 20:59:20.5534915",
"UpdateName": null,
"UpdateTime": "2023-08-12 13:11:44.8670189",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null
},
{
"uuid": "076250742950465b9d6ea29a225dfb00",
"Code": "ZX-001-300",
"Name": "300μL Tip头",
"SummaryName": "300μL Tip头",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 120.98,
"WidthNum": 82.12,
"HeightNum": 40,
"DepthNum": 59.3,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"ChannelNum": 8,
"HoleDiameter": 5.5,
"Volume": 300,
"ImagePath": "",
"QRCode": null,
"Qty": 11,
"CreateName": null,
"CreateTime": "2021-12-30 21:00:24.7266192",
"UpdateName": null,
"UpdateTime": "2024-02-01 15:48:02.1562734",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null
},
{
"uuid": "7a73bb9e5c264515a8fcbe88aed0e6f7",
"Code": "ZX-001-200",
"Name": "200μL Tip头",
"SummaryName": "200μL Tip头",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 120.98,
"WidthNum": 82.12,
"HeightNum": 66.9,
"DepthNum": 52,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"ChannelNum": 8,
"HoleDiameter": 5.5,
"Volume": 200,
"ImagePath": "",
"QRCode": null,
"Qty": 19,
"CreateName": null,
"CreateTime": "2021-12-30 21:01:17.626704",
"UpdateName": null,
"UpdateTime": "2023-10-14 13:44:41.5428946",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null
},
{
"uuid": "73bb9b10bc394978b70e027bf45ce2d3",
"Code": "ZX-023-0.2",
"Name": "0.2ml PCR板",
"SummaryName": "0.2ml PCR板",
"SupplyType": 1,
"Factory": "中析",
"LengthNum": 126,
"WidthNum": 86,
"HeightNum": 21.2,
"DepthNum": 15.17,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"ChannelNum": 96,
"HoleDiameter": 6,
"Volume": 1000,
"ImagePath": "",
"QRCode": null,
"Qty": -12,
"CreateName": null,
"CreateTime": "2021-12-30 21:06:02.7746392",
"UpdateName": null,
"UpdateTime": "2024-02-20 16:17:16.7921748",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null
},
{
"uuid": "ca877b8b114a4310b429d1de4aae96ee",
"Code": "ZX-019-2.2",
"Name": "2.2ml 深孔板",
"SummaryName": "2.2ml 深孔板",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 127.3,
"WidthNum": 85.35,
"HeightNum": 44,
"DepthNum": 42,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"ChannelNum": 8,
"HoleDiameter": 8.2,
"Volume": 2200,
"ImagePath": "",
"QRCode": null,
"Qty": 34,
"CreateName": null,
"CreateTime": "2021-12-30 21:07:16.4538022",
"UpdateName": null,
"UpdateTime": "2023-08-12 13:11:26.3993472",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 1,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null
},
{
"uuid": "04211a2dc93547fe9bf6121eac533650",
"Code": "ZX-58-10000",
"Name": "储液槽",
"SummaryName": "储液槽",
"SupplyType": 1,
"Factory": "宁静致远",
"LengthNum": 127,
"WidthNum": 85,
"HeightNum": 31.2,
"DepthNum": 24,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 1,
"HoleRow": 1,
"ChannelNum": 1,
"HoleDiameter": 127,
"Volume": 1250,
"ImagePath": "/images/20220623103134.jpg",
"QRCode": null,
"Qty": -172,
"CreateName": null,
"CreateTime": "2021-12-31 18:37:56.7949909",
"UpdateName": null,
"UpdateTime": "2022-06-23 10:31:34.4261358",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 1,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null
},
{
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
"Code": "ZX-58-0001",
"Name": "半裙边 PCR适配器",
"SummaryName": "半裙边 PCR适配器",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 127,
"WidthNum": 85,
"HeightNum": 88,
"DepthNum": 5,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"ChannelNum": 96,
"HoleDiameter": 9,
"Volume": 1250,
"ImagePath": "/images/20221123051800.jpg",
"QRCode": null,
"Qty": 100,
"CreateName": null,
"CreateTime": "2022-01-02 19:21:35.8664843",
"UpdateName": null,
"UpdateTime": "2022-11-23 17:18:00.8826719",
"IsStright": 1,
"IsGeneral": 1,
"IsControl": 1,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null
},
{
"uuid": "6bdfdd7069df453896b0806df50f2f4d",
"Code": "ZX-ADP-001",
"Name": "储液槽 适配器",
"SummaryName": "储液槽 适配器",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 133,
"WidthNum": 91.8,
"HeightNum": 70,
"DepthNum": 4,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 1,
"HoleRow": 1,
"ChannelNum": 8,
"HoleDiameter": 1,
"Volume": 1250,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2022-02-16 17:31:26.413594",
"UpdateName": null,
"UpdateTime": "2023-08-12 13:10:58.786996",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 0,
"YSpacing": 0,
"materialEnum": null
},
{
"uuid": "9a439bed8f3344549643d6b3bc5a5eb4",
"Code": "ZX-002-300",
"Name": "300ul深孔板适配器",
"SummaryName": "300ul深孔板适配器",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 136.4,
"WidthNum": 93.8,
"HeightNum": 96,
"DepthNum": 7,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"ChannelNum": 96,
"HoleDiameter": 8.1,
"Volume": 300,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2022-06-18 15:17:42.7917763",
"UpdateName": null,
"UpdateTime": "2023-08-12 13:10:46.1526635",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null
},
{
"uuid": "4dc8d6ecfd0449549683b8ef815a861b",
"Code": "ZX-002-10",
"Name": "10ul专用深孔板适配器",
"SummaryName": "10ul专用深孔板适配器",
"SupplyType": 2,
"Factory": "宁静致远",
"LengthNum": 136.5,
"WidthNum": 93.8,
"HeightNum": 121.5,
"DepthNum": 7,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"ChannelNum": 96,
"HoleDiameter": 8.1,
"Volume": 10,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2022-06-30 09:37:31.0451435",
"UpdateName": null,
"UpdateTime": "2023-08-12 13:10:38.5409878",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null
},
{
"uuid": "b01627718d3341aba649baa81c2c083c",
"Code": "Sd155",
"Name": "爱津",
"SummaryName": "爱津",
"SupplyType": 1,
"Factory": "中析",
"LengthNum": 125,
"WidthNum": 85,
"HeightNum": 64,
"DepthNum": 45.5,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"ChannelNum": 1,
"HoleDiameter": 4,
"Volume": 20,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2022-11-07 08:56:30.1794274",
"UpdateName": null,
"UpdateTime": "2022-11-07 09:00:29.5496845",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null
},
{
"uuid": "adfabfffa8f24af5abfbba67b8d0f973",
"Code": "Fhh478",
"Name": "适配器",
"SummaryName": "适配器",
"SupplyType": 2,
"Factory": "中析",
"LengthNum": 120,
"WidthNum": 90,
"HeightNum": 86,
"DepthNum": 4,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 1,
"HoleRow": 1,
"ChannelNum": 1,
"HoleDiameter": 4,
"Volume": 1000,
"ImagePath": null,
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2022-11-07 09:00:10.7579131",
"UpdateName": null,
"UpdateTime": "2022-11-07 09:00:10.7579134",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": null,
"YSpacing": null,
"materialEnum": null
},
{
"uuid": "1592e84a07f74668af155588867f2da7",
"Code": "12",
"Name": "12",
"SummaryName": "12",
"SupplyType": 1,
"Factory": "12",
"LengthNum": 1,
"WidthNum": 1,
"HeightNum": 1,
"DepthNum": 100,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 8,
"HoleRow": 12,
"ChannelNum": 12,
"HoleDiameter": 7,
"Volume": 12,
"ImagePath": null,
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2023-10-08 09:35:19.281766",
"UpdateName": null,
"UpdateTime": "2023-10-08 09:35:19.2817667",
"IsStright": 0,
"IsGeneral": 0,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null
},
{
"uuid": "730067cf07ae43849ddf4034299030e9",
"Code": "q1",
"Name": "废弃槽",
"SummaryName": "废弃槽",
"SupplyType": 1,
"Factory": "中析",
"LengthNum": 190,
"WidthNum": 135,
"HeightNum": 75,
"DepthNum": 1,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 1,
"HoleRow": 1,
"ChannelNum": 1,
"HoleDiameter": 1,
"Volume": 1250,
"ImagePath": null,
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2023-10-14 13:15:45.8172852",
"UpdateName": null,
"UpdateTime": "2023-10-14 13:15:45.8172869",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 1,
"YSpacing": 1,
"materialEnum": null
},
{
"uuid": "57b1e4711e9e4a32b529f3132fc5931f",
"Code": "q2",
"Name": "96深孔板",
"SummaryName": "96深孔板",
"SupplyType": 1,
"Factory": "中析",
"LengthNum": 126.5,
"WidthNum": 84.5,
"HeightNum": 41.4,
"DepthNum": 38.4,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 12,
"HoleRow": 8,
"ChannelNum": 96,
"HoleDiameter": 8.3,
"Volume": 1250,
"ImagePath": null,
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2023-10-14 13:19:55.7225524",
"UpdateName": null,
"UpdateTime": "2023-10-14 13:19:55.7225525",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 9,
"YSpacing": 9,
"materialEnum": null
},
{
"uuid": "853dcfb6226f476e8b23c250217dc7da",
"Code": "q3",
"Name": "384板",
"SummaryName": "384板",
"SupplyType": 1,
"Factory": "中析",
"LengthNum": 126.6,
"WidthNum": 84,
"HeightNum": 9.4,
"DepthNum": 8,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 24,
"HoleRow": 16,
"ChannelNum": 384,
"HoleDiameter": 3,
"Volume": 1250,
"ImagePath": null,
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2023-10-14 13:22:34.779818",
"UpdateName": null,
"UpdateTime": "2023-10-14 13:22:34.7798181",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 4.5,
"YSpacing": 4.5,
"materialEnum": null
},
{
"uuid": "e201e206fcfc4e8ab51946a22e8cd1bc",
"Code": "1",
"Name": "ep",
"SummaryName": "ep",
"SupplyType": 1,
"Factory": "中析",
"LengthNum": 504,
"WidthNum": 337,
"HeightNum": 160,
"DepthNum": 163,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 6,
"HoleRow": 4,
"ChannelNum": 24,
"HoleDiameter": 41.2,
"Volume": 1,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2024-01-20 13:14:38.0308919",
"UpdateName": null,
"UpdateTime": "2024-02-05 16:27:07.2582693",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 21,
"YSpacing": 18,
"materialEnum": null
},
{
"uuid": "01953864f6f140ccaa8ddffd4f3e46f5",
"Code": "sdfrth654",
"Name": "4道储液槽",
"SummaryName": "4道储液槽",
"SupplyType": 1,
"Factory": "中析",
"LengthNum": 100,
"WidthNum": 40,
"HeightNum": 30,
"DepthNum": 10,
"StandardHeight": 0,
"PipetteHeight": null,
"HoleColum": 4,
"HoleRow": 8,
"ChannelNum": 4,
"HoleDiameter": 4,
"Volume": 1000,
"ImagePath": "",
"QRCode": null,
"Qty": null,
"CreateName": null,
"CreateTime": "2024-02-20 14:44:25.0021372",
"UpdateName": null,
"UpdateTime": "2024-02-20 15:28:21.3881302",
"IsStright": 0,
"IsGeneral": 1,
"IsControl": 0,
"ArmCode": null,
"XSpacing": 27,
"YSpacing": 9,
"materialEnum": null
}
]

View File

@@ -0,0 +1,602 @@
[
{
"uuid": "87ea11eeb24b43648ce294654b561fe7",
"PlanName": "2341",
"PlanCode": "2980eb",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-05-15 18:24:00.8445073",
"MatrixId": "34ba3f02-6fcd-48e6-bb8e-3b0ce1d54ed5"
},
{
"uuid": "0a977d6ebc4244739793b0b6f8b3f815",
"PlanName": "384测试方案300模块",
"PlanCode": "9336ee",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-06-13 10:34:52.5310959",
"MatrixId": "74ed84ea-0b5d-4307-a966-ceb83fcaefe7"
},
{
"uuid": "aff2cd213ad34072b370f44acb5ab658",
"PlanName": "96孔吸300方案单放",
"PlanCode": "9932fc",
"PlanTarget": "测试用",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-06-13 09:57:38.422353",
"MatrixId": "bacd78be-b86d-49d6-973a-dd522834e4c4"
},
{
"uuid": "97816d94f99a48409379013d19f0ab66",
"PlanName": "384测试方案50模块",
"PlanCode": "3964de",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-06-13 10:32:22.8918817",
"MatrixId": "74ed84ea-0b5d-4307-a966-ceb83fcaefe7"
},
{
"uuid": "c3d86e9d7eed4ddb8c32e9234da659de",
"PlanName": "96吸50方案单放",
"PlanCode": "6994aa",
"PlanTarget": "测试用",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-08-08 11:50:14.6850189",
"MatrixId": "bacd78be-b86d-49d6-973a-dd522834e4c4"
},
{
"uuid": "59a97f77718d4bbba6bed1ddbf959772",
"PlanName": "test12",
"PlanCode": "8630fa",
"PlanTarget": "12通道",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-10-08 09:36:14.2536629",
"MatrixId": "517c836e-56c6-4c06-a897-7074886061bd"
},
{
"uuid": "84d50e4cf3034aa6a3de505a92b30812",
"PlanName": "test001",
"PlanCode": "9013fe",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-10-08 16:37:57.2302499",
"MatrixId": "ed9b1ceb-b879-4b8c-a246-2d4f54fbe970"
},
{
"uuid": "d052b893c6324ae38d301a58614a5663",
"PlanName": "test01",
"PlanCode": "8524cf",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-10-09 11:00:21.4973895",
"MatrixId": "bacd78be-b86d-49d6-973a-dd522834e4c4"
},
{
"uuid": "875a6eaa00e548b99318fd0be310e879",
"PlanName": "test002",
"PlanCode": "2477fe",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-10-09 11:02:01.2027308",
"MatrixId": "7374dc89-d425-42aa-b252-1b1338d3c2f2"
},
{
"uuid": "ecb3cb37f603495d95a93522a6b611e3",
"PlanName": "test02",
"PlanCode": "5126cb",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-10-09 11:02:14.7987877",
"MatrixId": "7374dc89-d425-42aa-b252-1b1338d3c2f2"
},
{
"uuid": "705edabbcbd645d0925e4e581643247c",
"PlanName": "test003",
"PlanCode": "4994cc",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-10-09 11:41:04.1715458",
"MatrixId": "4c126841-5c37-49c7-b4e8-539983bc9cc4"
},
{
"uuid": "6c58136d7de54a6abb7b51e6327eacac",
"PlanName": "test04",
"PlanCode": "9704dd",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-10-09 11:51:59.1752071",
"MatrixId": "4c126841-5c37-49c7-b4e8-539983bc9cc4"
},
{
"uuid": "208f00a911b846d9922b2e72bdda978c",
"PlanName": "96版位 50ul量程",
"PlanCode": "7595be",
"PlanTarget": "213213",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-10-18 19:12:17.4641981",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "40bd0ca25ffb4be6b246353db6ebefc9",
"PlanName": "96版位 300ul量程",
"PlanCode": "7421fc",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-10-14 14:47:03.8105699",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "30b838bb7d124ec885b506df29ee7860",
"PlanName": "300版位 50ul量程",
"PlanCode": "6364cc",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-10-14 14:48:05.2235254",
"MatrixId": "f8c70333-b717-4ca0-9306-c40fd5f156fb"
},
{
"uuid": "e53c591c86334c6f92d3b1afa107bcf8",
"PlanName": "384版位 300ul量程",
"PlanCode": "4029be",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-10-14 14:47:48.9478679",
"MatrixId": "f8c70333-b717-4ca0-9306-c40fd5f156fb"
},
{
"uuid": "1d26d1ab45c6431990ba0e00cc1f78d2",
"PlanName": "96版位梯度稀释 50ul量程",
"PlanCode": "3502cf",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-10-14 14:48:12.8676989",
"MatrixId": "916bbd00-e66c-4237-9843-e049b70b740a"
},
{
"uuid": "7a0383b4fbb543339723513228365451",
"PlanName": "96版位梯度稀释 300ul量程",
"PlanCode": "9345fe",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-10-14 14:50:02.0250566",
"MatrixId": "916bbd00-e66c-4237-9843-e049b70b740a"
},
{
"uuid": "69d4882f0f024fb5a3b91010f149ff89",
"PlanName": "测试",
"PlanCode": "3941bf",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2023-12-11 15:24:30.1371824",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "3603f89f4e0945f68353a33e8017ba6e",
"PlanName": "测试111",
"PlanCode": "8056eb",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-16 09:29:12.1441631",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "b44be8260740460598816c40f13fd6b4",
"PlanName": "测试12",
"PlanCode": "8272fb",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-16 10:40:54.2543702",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "f189a50122d54a568f3d39dc1f996167",
"PlanName": "0.5",
"PlanCode": "2093ec",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-16 13:06:37.8280696",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "b48218c8f2274b108e278d019c9b5126",
"PlanName": "3",
"PlanCode": "9493bb",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-16 14:20:42.4761092",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "41d2ebc5ab5b4b2da3e203937c5cbe70",
"PlanName": "6",
"PlanCode": "5586de",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-16 15:21:03.4440875",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "49ec03499aa646b9b8069a783dbeca1c",
"PlanName": "7",
"PlanCode": "1162bc",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-16 15:31:33.7359724",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "a9c6d149cdf04636ac43cfb7623e4e7f",
"PlanName": "8",
"PlanCode": "7354eb",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-16 15:39:32.2399414",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "0e3a36cabefa4f5497e35193db48b559",
"PlanName": "9",
"PlanCode": "4453ba",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-16 15:49:31.5830134",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "d0a0d926e2034abc94b4d883951a78f7",
"PlanName": "10",
"PlanCode": "5797ab",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-16 16:00:25.4439315",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "22ac523a47e7421e80f401baf1526daf",
"PlanName": "50",
"PlanCode": "2507ca",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-16 16:23:13.8022807",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "fdea60f535ee4bc39c02c602a64f46bd",
"PlanName": "11",
"PlanCode": "1574ae",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-18 09:14:59.8230591",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "6650f7df6b8944f98476da92ce81d688",
"PlanName": "12",
"PlanCode": "2145bd",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-18 09:45:34.137906",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "9415a69280c042a09d6836f5eeddf40f",
"PlanName": "100",
"PlanCode": "2073fd",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-18 10:12:29.9998926",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "d9740fea94a04c2db44b1364a336b338",
"PlanName": "250",
"PlanCode": "2601ea",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-18 11:15:54.2583401",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "1d80c1fff5af442595c21963e6ca9fee",
"PlanName": "160",
"PlanCode": "6612ea",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-18 11:18:59.0457638",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "36889fb926aa480cb42de97700522bbf",
"PlanName": "200",
"PlanCode": "3174dc",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-18 11:20:15.7676326",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "bd90ae2846c14e708854938158fd3443",
"PlanName": "300",
"PlanCode": "2665df",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-18 13:00:16.9242256",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "9df4857d2bef45bcad14cc13055e9f7b",
"PlanName": "500",
"PlanCode": "4771ab",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-18 13:26:32.3910805",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "d2f6e63cf1ff41a4a8d03f4444a2aeac",
"PlanName": "800",
"PlanCode": "4560bc",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-18 13:42:35.5153947",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "f40a6f4326a346d39d5a82f6262aba47",
"PlanName": "测试12345",
"PlanCode": "3402ab",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-18 14:37:29.8890777",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "4248035f01e943faa6d71697ed386e19",
"PlanName": "995",
"PlanCode": "2688dc",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-18 14:39:23.5292196",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "a73bc780e4d04099bf54c2b90fa7b974",
"PlanName": "1000",
"PlanCode": "2889bf",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 09:16:37.7818522",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "4d97363a0a334094a1ff24494a902d02",
"PlanName": "2.。",
"PlanCode": "6527ff",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 11:38:00.0672017",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "6eec360c74464769967ebefa43b7aec1",
"PlanName": "2222222",
"PlanCode": "8763ce",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 11:40:42.7038484",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "986049c83b054171a1b34dd49b3ca9cf",
"PlanName": "9ul",
"PlanCode": "1945fd",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 13:33:06.6556398",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "462eed73962142c2bd3b8fe717caceb6",
"PlanName": "8ul",
"PlanCode": "6912fc",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 15:16:17.4254316",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "b2f0c7ab462f4cf1bae56ee59a49a253",
"PlanName": "11.",
"PlanCode": "6190ba",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 15:21:57.6729366",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "b9768a1d91444d4a86b7a013467bee95",
"PlanName": "8ulll",
"PlanCode": "6899be",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 15:29:03.2029069",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "98621898cd514bc9a1ac0c92362284f4",
"PlanName": "7u",
"PlanCode": "7651fe",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 15:57:16.4898686",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "4d03142fd86844db8e23c19061b3d505",
"PlanName": "55555",
"PlanCode": "7963fe",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 16:23:37.7271107",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "c78c3f38a59748c3aef949405e434b05",
"PlanName": "44443",
"PlanCode": "4564dd",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 16:29:26.6765074",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "0fc4ffd86091451db26162af4f7b235e",
"PlanName": "u",
"PlanCode": "9246de",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 16:34:15.4217796",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "a08748982b934daab8752f55796e1b0c",
"PlanName": "666y",
"PlanCode": "5492ce",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 16:38:55.6092122",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "2317611bdb614e45b61a5118e58e3a2a",
"PlanName": "8ull、",
"PlanCode": "4641de",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 16:46:26.6184295",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "62cb45ac3af64a46aa6d450ba56963e7",
"PlanName": "33333",
"PlanCode": "1270aa",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 16:49:19.6115492",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "321f717a3a2640a3bfc9515aee7d1052",
"PlanName": "999",
"PlanCode": "7597ed",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-01-19 16:58:22.6149002",
"MatrixId": "b3da2b21-875b-4ae6-8077-ec951730201b"
},
{
"uuid": "6c3246ac0f974a6abc24c83bf45e1cf4",
"PlanName": "QPCR",
"PlanCode": "7297ad",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-02-19 13:03:44.3456134",
"MatrixId": "f02830f3-ed67-49fb-9865-c31828ba3a48"
},
{
"uuid": "1d307a2c095b461abeec6e8521565ad3",
"PlanName": "绝对定量",
"PlanCode": "8540af",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-02-19 13:35:14.2243691",
"MatrixId": "739ddf78-e04c-4d43-9293-c35d31f36f51"
},
{
"uuid": "bbd6dc765867466ca2a415525f5bdbdd",
"PlanName": "血凝",
"PlanCode": "6513ee",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-02-20 16:14:25.0364174",
"MatrixId": "20e70dcb-63f6-4bac-82e3-29e88eb6a7ab"
},
{
"uuid": "f7282ecbfee44e91b05cefbc1beac1ae",
"PlanName": "血凝抑制",
"PlanCode": "1431ba",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-02-21 10:00:05.8661038",
"MatrixId": "1c948beb-4c32-494f-b226-14bb84b3e144"
},
{
"uuid": "196e0d757c574020932b64b69e88fac9",
"PlanName": "测试杀杀杀",
"PlanCode": "9833df",
"PlanTarget": "",
"Annotate": "",
"CreateName": "",
"CreateDate": "2024-02-21 10:54:19.3136491",
"MatrixId": "3667ead7-9044-46ad-b73e-655b57c8c6b9"
}
]

View File

@@ -0,0 +1,302 @@
[
{
"id": "630a9ca9-dfbf-40f9-b90b-6df73e6a1d7f",
"number": 1,
"name": "T1",
"row": 0,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
},
{
"id": "db955443-1397-4a7a-a0cc-185eb6422c27",
"number": 2,
"name": "T2",
"row": 0,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
},
{
"id": "635e8265-e2b9-430e-8a4e-ddf94256266f",
"number": 3,
"name": "T3",
"row": 0,
"col": 2,
"row_span": 2,
"col_span": 1,
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
},
{
"id": "6de1521d-a249-4a7e-800f-1d49b5c7b56f",
"number": 4,
"name": "T4",
"row": 1,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
},
{
"id": "4f9f2527-0f71-4ec4-a0ac-e546407e2960",
"number": 5,
"name": "T5",
"row": 1,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
},
{
"id": "55ecff40-453f-4a5f-9ed3-1267b0a03cae",
"number": 1,
"name": "T1",
"row": 0,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "7dcd9c87-6702-4659-b28a-f6565b27f8e3",
"number": 2,
"name": "T2",
"row": 0,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "67e51bd6-6eee-46e4-931c-73d9e07397eb",
"number": 3,
"name": "T3",
"row": 0,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "e1289406-4f5e-4966-a1e6-fb29be6cd4bd",
"number": 4,
"name": "T4",
"row": 0,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "4ecb9ef7-cbd4-44bc-a6a9-fdbbefdc01d6",
"number": 5,
"name": "T5",
"row": 1,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "c7bcaeeb-7ce7-479d-8dae-e82f4023a2b6",
"number": 6,
"name": "T6",
"row": 1,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "e502d5ee-3197-4f60-8ac4-3bc005349dfd",
"number": 7,
"name": "T7",
"row": 1,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "829c78b0-9e05-448f-9531-6d19c094c83f",
"number": 8,
"name": "T8",
"row": 1,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "d0fd64d6-360d-4f5e-9451-21a332e247f5",
"number": 9,
"name": "T9",
"row": 2,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "7f3da25d-0be0-4e07-885f-fbbbfa952f9f",
"number": 10,
"name": "T10",
"row": 2,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "491d396d-7264-43d6-9ad4-60bffbe66c26",
"number": 11,
"name": "T11",
"row": 2,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "a8853b6d-639d-46f9-a4bf-9153c0c22461",
"number": 12,
"name": "T12",
"row": 2,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "b7beb8d0-0003-471d-bd8d-a9c0e09b07d5",
"number": 1,
"name": "T1",
"row": 0,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "306e3f96-a6d7-484a-83ef-722e3710d5c4",
"number": 2,
"name": "T2",
"row": 0,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "4e7bb617-ac1a-4360-b379-7ac4197089c4",
"number": 3,
"name": "T3",
"row": 0,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "af583180-c29d-418e-9061-9e030f77cf57",
"number": 4,
"name": "T4",
"row": 0,
"col": 3,
"row_span": 2,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "24a85ce8-e9e3-44f5-9d08-25116173ba75",
"number": 5,
"name": "T5",
"row": 1,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "7bf61a40-f65a-4d2f-bb19-d42bfd80e2e9",
"number": 6,
"name": "T6",
"row": 1,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "a3177806-3c02-4c4f-86d6-604a38c2ba2a",
"number": 7,
"name": "T7",
"row": 1,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "8ccaad5a-8588-4ff3-b0d7-17e7fd5ac6cc",
"number": 1,
"name": "T1",
"row": 0,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
},
{
"id": "93ae7707-b6b8-4bc4-8700-c500c3d7b165",
"number": 2,
"name": "T2",
"row": 0,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
},
{
"id": "3591a07b-4922-4882-996f-7bebee843be1",
"number": 3,
"name": "T3",
"row": 0,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
},
{
"id": "669fdba9-b20c-4bd2-8352-8fe5682e3e0c",
"number": 4,
"name": "T4",
"row": 1,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
},
{
"id": "8bf3333e-4a73-4e4c-959a-8ae44e1038a2",
"number": 5,
"name": "T5",
"row": 1,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
},
{
"id": "2837bf69-273a-4cbb-a74c-0af1b362f609",
"number": 6,
"name": "T6",
"row": 1,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
}
]

View File

@@ -0,0 +1,74 @@
[
{
"uuid": "9a3007baa748457b8d5162f5c5918553",
"ArmCode": "SC10",
"ArmName": "单道-10uL",
"CmdCode": "SC10",
"ChannelNum": 1,
"Dosage": 10,
"CreateName": "admin",
"CreateTime": "2021-11-13 14:04:02.000",
"UpdateName": "admin",
"UpdateTime": "2021-11-13 14:04:12.000"
},
{
"uuid": "8f57a4cc859d4c02bffbeeadcfb2b661",
"ArmCode": "SC300",
"ArmName": "单道-300uL",
"CmdCode": "SC300",
"ChannelNum": 1,
"Dosage": 300,
"CreateName": "admin",
"CreateTime": "2021-11-11 11:11:11.000",
"UpdateName": "admin",
"UpdateTime": "2021-11-11 11:11:11.000"
},
{
"uuid": "8fe0320823de49a99bfa5060ce1aaa28",
"ArmCode": "SC1250",
"ArmName": "单道-1250",
"CmdCode": "SC1250",
"ChannelNum": 1,
"Dosage": 1250,
"CreateName": "admin",
"CreateTime": "2021-11-12 10:10:10.000",
"UpdateName": "admin",
"UpdateTime": "2021-11-12 11:11:11.000"
},
{
"uuid": "88f22c5384e94dbbad60961d4d2b5e91",
"ArmCode": "MC10",
"ArmName": "八道-10uL",
"CmdCode": "MC10",
"ChannelNum": 8,
"Dosage": 10,
"CreateName": "admin",
"CreateTime": "2021-11-12 10:10:10.000",
"UpdateName": "admin",
"UpdateTime": "2021-11-13 12:12:12.000"
},
{
"uuid": "09206ff90e64466f90ce6a785a24bad8",
"ArmCode": "MC300",
"ArmName": "八道-300uL",
"CmdCode": "MC300",
"ChannelNum": 8,
"Dosage": 300,
"CreateName": "admin",
"CreateTime": "2021-11-12 12:12:12.000",
"UpdateName": "admin",
"UpdateTime": "2021-11-12 10:10:10.000"
},
{
"uuid": "5afcbd7d1d6749079d1c94f8c2e68f06",
"ArmCode": "MC1250",
"ArmName": "八道-1250uL",
"CmdCode": "MC1250",
"ChannelNum": 8,
"Dosage": 1250,
"CreateName": "admin",
"CreateTime": "2021-11-12 12:12:10.000",
"UpdateName": "admin",
"UpdateTime": "2021-11-12 12:11:11.000"
}
]

View File

@@ -0,0 +1,10 @@
[
{
"uuid": "bd52d6566534441ea523265814dc06e8",
"uuidMaterial": "01bdeb95a1314dc78b8f25667b08d531",
"ChannelNum": 8,
"HoleNo": 96,
"HoleCenterXYZ": "300",
"uuidLayoutMaster": "4f35adc958c540fcb40d6f9dd51e40fa"
}
]

View File

@@ -0,0 +1,20 @@
[
{
"uuid": "4f35adc958c540fcb40d6f9dd51e40fa",
"BoardCode": 34,
"BoardNum": 1,
"BoardLength": 500,
"BoardWidth": 400,
"BoardColum": 4,
"BoardRow": 3,
"TotalColum": 4,
"TotalRow": 3,
"BoardCenterXY": "300",
"HoleQty": 96,
"Version": 1,
"CreateTime": "2021-11-15",
"CreateName": "admin",
"UpdateTime": "2021-11-15",
"UpdateName": "admin"
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,98 @@
[
{
"id": "ef121889-2724-4b3d-a786-bbf0bd213c3d",
"name": "9300_V02",
"row": 2,
"col": 3,
"create_name": "",
"create_time": "2023-08-12 16:02:20.994",
"update_name": null,
"update_time": null,
"remark": "9300_V02",
"isUse": 0
},
{
"id": "9af15efc-29d2-4c44-8533-bbaf24913be6",
"name": "9310",
"row": 3,
"col": 4,
"create_name": "",
"create_time": "2023-08-12 16:23:07.472",
"update_name": null,
"update_time": null,
"remark": "9310",
"isUse": 0
},
{
"id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546",
"name": "6版位",
"row": 2,
"col": 4,
"create_name": "",
"create_time": "2023-10-09 11:05:57.244",
"update_name": null,
"update_time": null,
"remark": "6版位",
"isUse": 0
},
{
"id": "77673540-92c4-4404-b659-4257034a9c5e",
"name": "9300_V03",
"row": 2,
"col": 3,
"create_name": "",
"create_time": "2024-01-20 08:49:09.620",
"update_name": null,
"update_time": null,
"remark": "9300_V03",
"isUse": 0
},
{
"id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e",
"name": "9320",
"row": 4,
"col": 7,
"create_name": "",
"create_time": "2025-03-10 13:44:17.994",
"update_name": null,
"update_time": null,
"remark": "9320",
"isUse": 0
},
{
"id": "54092457-a8b8-4457-bccd-e8c251e83ebd",
"name": "7.17演示",
"row": 4,
"col": 4,
"create_name": "",
"create_time": "2025-07-12 17:08:38.336",
"update_name": null,
"update_time": null,
"remark": "7.17演示",
"isUse": 0
},
{
"id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc",
"name": "北京大学 16版位",
"row": 4,
"col": 4,
"create_name": "",
"create_time": "2025-09-03 13:23:51.781",
"update_name": null,
"update_time": null,
"remark": "北京大学 16版位",
"isUse": 1
},
{
"id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a",
"name": "TEST",
"row": 4,
"col": 4,
"create_name": "",
"create_time": "2025-10-27 14:36:03.266",
"update_name": null,
"update_time": null,
"remark": "TEST",
"isUse": 0
}
]

View File

@@ -0,0 +1,872 @@
[
{
"id": "630a9ca9-dfbf-40f9-b90b-6df73e6a1d7f",
"number": 1,
"name": "T1",
"row": 0,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
},
{
"id": "db955443-1397-4a7a-a0cc-185eb6422c27",
"number": 2,
"name": "T2",
"row": 0,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
},
{
"id": "635e8265-e2b9-430e-8a4e-ddf94256266f",
"number": 3,
"name": "T3",
"row": 0,
"col": 2,
"row_span": 2,
"col_span": 1,
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
},
{
"id": "6de1521d-a249-4a7e-800f-1d49b5c7b56f",
"number": 4,
"name": "T4",
"row": 1,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
},
{
"id": "4f9f2527-0f71-4ec4-a0ac-e546407e2960",
"number": 5,
"name": "T5",
"row": 1,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "ef121889-2724-4b3d-a786-bbf0bd213c3d"
},
{
"id": "55ecff40-453f-4a5f-9ed3-1267b0a03cae",
"number": 1,
"name": "T1",
"row": 0,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "7dcd9c87-6702-4659-b28a-f6565b27f8e3",
"number": 2,
"name": "T2",
"row": 0,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "67e51bd6-6eee-46e4-931c-73d9e07397eb",
"number": 3,
"name": "T3",
"row": 0,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "e1289406-4f5e-4966-a1e6-fb29be6cd4bd",
"number": 4,
"name": "T4",
"row": 0,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "4ecb9ef7-cbd4-44bc-a6a9-fdbbefdc01d6",
"number": 5,
"name": "T5",
"row": 1,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "c7bcaeeb-7ce7-479d-8dae-e82f4023a2b6",
"number": 6,
"name": "T6",
"row": 1,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "e502d5ee-3197-4f60-8ac4-3bc005349dfd",
"number": 7,
"name": "T7",
"row": 1,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "829c78b0-9e05-448f-9531-6d19c094c83f",
"number": 8,
"name": "T8",
"row": 1,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "d0fd64d6-360d-4f5e-9451-21a332e247f5",
"number": 9,
"name": "T9",
"row": 2,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "7f3da25d-0be0-4e07-885f-fbbbfa952f9f",
"number": 10,
"name": "T10",
"row": 2,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "491d396d-7264-43d6-9ad4-60bffbe66c26",
"number": 11,
"name": "T11",
"row": 2,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "a8853b6d-639d-46f9-a4bf-9153c0c22461",
"number": 12,
"name": "T12",
"row": 2,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "9af15efc-29d2-4c44-8533-bbaf24913be6"
},
{
"id": "b7beb8d0-0003-471d-bd8d-a9c0e09b07d5",
"number": 1,
"name": "T1",
"row": 0,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "306e3f96-a6d7-484a-83ef-722e3710d5c4",
"number": 2,
"name": "T2",
"row": 0,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "4e7bb617-ac1a-4360-b379-7ac4197089c4",
"number": 3,
"name": "T3",
"row": 0,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "af583180-c29d-418e-9061-9e030f77cf57",
"number": 4,
"name": "T4",
"row": 0,
"col": 3,
"row_span": 2,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "24a85ce8-e9e3-44f5-9d08-25116173ba75",
"number": 5,
"name": "T5",
"row": 1,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "7bf61a40-f65a-4d2f-bb19-d42bfd80e2e9",
"number": 6,
"name": "T6",
"row": 1,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "a3177806-3c02-4c4f-86d6-604a38c2ba2a",
"number": 7,
"name": "T7",
"row": 1,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "6ed12532-eeae-4c16-a9ae-18f0b0cfc546"
},
{
"id": "8ccaad5a-8588-4ff3-b0d7-17e7fd5ac6cc",
"number": 1,
"name": "T1",
"row": 0,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
},
{
"id": "93ae7707-b6b8-4bc4-8700-c500c3d7b165",
"number": 2,
"name": "T2",
"row": 0,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
},
{
"id": "3591a07b-4922-4882-996f-7bebee843be1",
"number": 3,
"name": "T3",
"row": 0,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
},
{
"id": "669fdba9-b20c-4bd2-8352-8fe5682e3e0c",
"number": 4,
"name": "T4",
"row": 1,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
},
{
"id": "8bf3333e-4a73-4e4c-959a-8ae44e1038a2",
"number": 5,
"name": "T5",
"row": 1,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
},
{
"id": "2837bf69-273a-4cbb-a74c-0af1b362f609",
"number": 6,
"name": "T6",
"row": 1,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "77673540-92c4-4404-b659-4257034a9c5e"
},
{
"id": "e9d352fa-816a-4c01-a9e2-f52bce8771f1",
"number": 1,
"name": "T1",
"row": 0,
"col": 0,
"row_span": 4,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "713f1d85-b671-49f1-a2f9-11a64e5bb545",
"number": 2,
"name": "T2",
"row": 0,
"col": 1,
"row_span": 4,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "ba2d8fd6-e2fa-4dd3-8afc-13472ca12afb",
"number": 3,
"name": "T3",
"row": 0,
"col": 2,
"row_span": 4,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "68137a87-ae26-4e27-8953-4b1335ed957c",
"number": 4,
"name": "T4",
"row": 0,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "182b2814-9c89-4a75-8456-9a82e774f876",
"number": 5,
"name": "T5",
"row": 0,
"col": 4,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "bc149d3c-9d54-45f0-8c33-23a5d4b70aff",
"number": 6,
"name": "T6",
"row": 0,
"col": 5,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "7d9ce812-c39c-42fe-9b73-f35364a7b01f",
"number": 7,
"name": "T7",
"row": 0,
"col": 6,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "4907b17d-c3f8-40a6-a8a2-e874f66195b1",
"number": 8,
"name": "T8",
"row": 1,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "f858fdb5-649f-4cb2-8e95-06a1b2d97113",
"number": 9,
"name": "T9",
"row": 1,
"col": 4,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "cc5f91d2-494a-4991-9dda-3b82ae61556b",
"number": 10,
"name": "T10",
"row": 1,
"col": 5,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "afed9a1f-2f48-4ca9-ae14-eb1ae4e80181",
"number": 11,
"name": "T11",
"row": 1,
"col": 6,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "1d39cacd-7828-4318-9d4f-5bf8fc21d77d",
"number": 12,
"name": "T12",
"row": 2,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "086912ac-4f33-4214-a2c8-22acb5291bfe",
"number": 13,
"name": "T13",
"row": 2,
"col": 4,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "89d43ea4-93f6-4cbf-aba4-564b0067295f",
"number": 14,
"name": "T14",
"row": 2,
"col": 5,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "866b12a8-5ef6-426d-a65b-b0583a3d8f16",
"number": 15,
"name": "T15",
"row": 2,
"col": 6,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "6c5969a9-e763-48f4-97f4-a9027e3ea7ef",
"number": 16,
"name": "T16",
"row": 3,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "af8370be-076d-455d-b0b3-dd246f76d930",
"number": 17,
"name": "T17",
"row": 3,
"col": 4,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "abf2b8c7-79ef-4fd1-9f9b-14e7e6a128c7",
"number": 18,
"name": "T18",
"row": 3,
"col": 5,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "ca92a1e9-eb7d-4f9a-a42c-9bae461da797",
"number": 19,
"name": "T19",
"row": 3,
"col": 6,
"row_span": 1,
"col_span": 1,
"plate_position_id": "c08591fe-bc7e-42a8-bfa1-a27a4967058e"
},
{
"id": "4a4df4fd-ea0b-461c-aad4-032bfda5abab",
"number": 1,
"name": "T1",
"row": 0,
"col": 0,
"row_span": 4,
"col_span": 1,
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
},
{
"id": "dba90870-4b7a-4fbd-b33f-948bbb594703",
"number": 2,
"name": "T2",
"row": 0,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
},
{
"id": "fddc5c2b-157f-4554-8b39-2c9e338f4d3a",
"number": 3,
"name": "T3",
"row": 0,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
},
{
"id": "2569a396-2cd8-4cac-8b78-a8af1313c993",
"number": 4,
"name": "T4",
"row": 0,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
},
{
"id": "f0f693c7-a45f-4dd3-b629-621461ca9992",
"number": 5,
"name": "T5",
"row": 1,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
},
{
"id": "9dcba2bf-8a48-4bc6-a9b1-88f51ffaa8af",
"number": 6,
"name": "T6",
"row": 1,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
},
{
"id": "08449a38-0dca-48c4-a156-6f1055cf74c4",
"number": 7,
"name": "T7",
"row": 1,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
},
{
"id": "6ec7343f-12b9-42ae-86d1-3894758e69b4",
"number": 8,
"name": "T8",
"row": 2,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
},
{
"id": "b5f02dbc-ffc6-452a-ad9f-2d1ff3db2064",
"number": 9,
"name": "T9",
"row": 2,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
},
{
"id": "7635380a-4f96-4894-9a54-37c2bd27f148",
"number": 10,
"name": "T10",
"row": 2,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
},
{
"id": "b4b6b063-5a0b-45a2-aa47-f427d4cd06f6",
"number": 11,
"name": "T11",
"row": 3,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
},
{
"id": "af02c689-7bca-476b-bd05-ce21d3e83f27",
"number": 12,
"name": "T12",
"row": 3,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
},
{
"id": "52a42e58-c0d6-420c-bc0b-575f749c7e3b",
"number": 13,
"name": "T13",
"row": 3,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "54092457-a8b8-4457-bccd-e8c251e83ebd"
},
{
"id": "169c12fe-e2f4-465e-9fd3-e58eac83a502",
"number": 1,
"name": "T1",
"row": 0,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "b6072651-1df5-4946-a5b4-fbff3fa54e6a",
"number": 2,
"name": "T2",
"row": 0,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "d0b8ea7c-f06e-4d94-98a8-70ffcba73c47",
"number": 3,
"name": "T3",
"row": 0,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "a7a8eb69-63f6-494e-a441-b7aef0f7c8a4",
"number": 4,
"name": "T4",
"row": 0,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "21966669-6761-4e37-947c-12fec82173fb",
"number": 5,
"name": "T5",
"row": 1,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "2227b825-fe1d-4fa3-bcb2-6e4b3c10ea53",
"number": 6,
"name": "T6",
"row": 1,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "b799da88-c2d9-4ec4-81ec-bc0991a50fe5",
"number": 7,
"name": "T7",
"row": 1,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "adaaa00a-ff6b-4bd8-b8f1-bb100488f306",
"number": 8,
"name": "T8",
"row": 1,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "3bc98311-b548-46d3-a0e0-4f1edcf10e24",
"number": 9,
"name": "T9",
"row": 2,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "81befc70-d249-49af-93dd-2efbe88c0211",
"number": 10,
"name": "T10",
"row": 2,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "45dd5535-0293-4d27-beab-1e486657b148",
"number": 11,
"name": "T11",
"row": 2,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "12ccf33a-6fe7-44a4-8643-b0b0ac6dd181",
"number": 12,
"name": "T12",
"row": 2,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "900272dd-23fd-41a4-a366-254999a30487",
"number": 13,
"name": "T13",
"row": 3,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "c366710d-2b81-4cee-8667-2b86e77e5c34",
"number": 14,
"name": "T14",
"row": 3,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "e18a9271-bc66-4c2b-8bc1-0fb129b5cc2f",
"number": 15,
"name": "T15",
"row": 3,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "6737cba0-de84-4c1f-992d-645e7f159b0c",
"number": 16,
"name": "T16",
"row": 3,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "e3855307-d91f-4ddc-9caf-565c0fd8adfc"
},
{
"id": "8ace38ab-dbc7-48a1-8226-0fe92d176e07",
"number": 1,
"name": "T1",
"row": 0,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
},
{
"id": "033fec53-c52d-4b59-aec6-2135ae0e18b9",
"number": 2,
"name": "T2",
"row": 0,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
},
{
"id": "fa730930-8709-4250-928f-f757fce57b60",
"number": 3,
"name": "T3",
"row": 0,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
},
{
"id": "e279d6f1-5243-4224-8953-1033dbea25ac",
"number": 4,
"name": "T4",
"row": 0,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
},
{
"id": "76bd9426-6324-4af2-b12f-6ec0ff8c416e",
"number": 5,
"name": "T5",
"row": 1,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
},
{
"id": "3f4ff652-3d87-4254-a235-bafde3359dae",
"number": 6,
"name": "T6",
"row": 1,
"col": 1,
"row_span": 1,
"col_span": 1,
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
},
{
"id": "a38e94af-e91e-4e7a-b49d-8668001bb356",
"number": 7,
"name": "T7",
"row": 1,
"col": 2,
"row_span": 1,
"col_span": 1,
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
},
{
"id": "9e45da24-1346-4886-a303-932880a79954",
"number": 8,
"name": "T8",
"row": 1,
"col": 3,
"row_span": 1,
"col_span": 1,
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
},
{
"id": "1ac46e58-86ae-42d9-b230-d476b984507a",
"number": 9,
"name": "T9",
"row": 2,
"col": 0,
"row_span": 1,
"col_span": 1,
"plate_position_id": "a25563ec-8a2a-4de8-9ca2-a59c1c71427a"
}
]

View File

@@ -0,0 +1,58 @@
[
{
"uuid": "4034fa042e7f418db42ab80b0044a8cd",
"Code": "MDHC-001-10",
"Key": "c28ae2cb",
"Value": "MDHC-001-1000522001001612db9dc",
"CreateTime": "2022-01-22 17:07:00.8651386"
},
{
"uuid": "8fb6d7589fdd42df93c1e1989ff13a62",
"Code": "MDHC-001-10",
"Key": "52980979",
"Value": "MDHC-001-100052200100119bb6731",
"CreateTime": "2022-01-22 20:19:20.9444209"
},
{
"uuid": "efc4c92b40a94de6b0662c64486c18d1",
"Code": "MDHC-001-10",
"Key": "79da8402",
"Value": "MDHC-001-1000522001001e24ea780",
"CreateTime": "2022-01-22 20:19:26.8107506"
},
{
"uuid": "3b81b1a9eabc4449b4dcbbbde47cb17f",
"Code": "MDHC-001-10",
"Key": "daa51755",
"Value": "MDHC-001-100052200100185dd22e2",
"CreateTime": "2022-01-22 20:19:36.1581374"
},
{
"uuid": "d005a70801544e42ab9d216ad68dbf50",
"Code": "MDHC-023-0.2",
"Key": "992bbdab",
"Value": "MDHC-023-0.2005220010014871a385",
"CreateTime": "2022-02-16 15:49:53.760377"
},
{
"uuid": "222315afb8e04320b0fcff10e3ddb8ae",
"Code": "MDHC-023-0.2",
"Key": "76d23270",
"Value": "MDHC-023-0.200522001001e61547ee",
"CreateTime": "2022-02-16 15:50:05.1932055"
},
{
"uuid": "31e2a5d4f884419aa9ba96cef98b7385",
"Code": "MDHC-023-0.2",
"Key": "ba2b8a46",
"Value": "MDHC-023-0.2005220010013bfed6cf",
"CreateTime": "2022-02-16 17:26:20.0024235"
},
{
"uuid": "9ccb8e0c5ca64ef09b8aced680395335",
"Code": "MDHC-023-0.2",
"Key": "1d1276d0",
"Value": "MDHC-023-0.2005220010015c039a9c",
"CreateTime": "2022-02-16 17:26:31.8479966"
}
]

View File

@@ -0,0 +1,22 @@
[
{
"uuid": "f3932aeae93533f19c0519c4c14702aa",
"RoleCode": "admin",
"RoleName": "管理员",
"RoleMenu": "all",
"CreateTime": "2022-02-26 00:00:00.000",
"CreateName": "admin",
"UpdateTime": "2022-02-26 14:50:10.000",
"UpdateName": "admin"
},
{
"uuid": "8c822592b360345fb59690e49ac6b181",
"RoleCode": "user",
"RoleName": "实验员",
"RoleMenu": "nosetting",
"CreateTime": "2022-02-26 14:54:16.000",
"CreateName": "admin",
"UpdateTime": "2022-02-26 14:54:19.000",
"UpdateName": "admin"
}
]

View File

@@ -0,0 +1,54 @@
[
{
"uuid": "f3932aeae93533f19c0519c4c14702dd",
"UserName": "admin",
"Password": "NuGlByx4NZBm7XcV9f89qA==",
"RealName": "管理员",
"IsEnable": 1,
"uuidRole": "f3932aeae93533f19c0519c4c14702aa",
"IsDel": 0,
"CreateTime": "2022-02-26 14:51:41.000",
"CreateName": "admin",
"UpdateTime": "2022-02-26 14:51:49.000",
"UpdateName": "admin"
},
{
"uuid": "5c522592b366645fb55690e49ac6b166",
"UserName": "user",
"Password": "4QrcOUm6Wau+VuBX8g+IPg==",
"RealName": "实验员",
"IsEnable": 1,
"uuidRole": "8c822592b360345fb59690e49ac6b181",
"IsDel": 0,
"CreateTime": "2022-02-26 14:56:57.000",
"CreateName": "admin",
"UpdateTime": "2022-02-26 14:58:39.000",
"UpdateName": "admin"
},
{
"uuid": "ju0514zjhi9267mz8s0buspq8b9s0bgb",
"UserName": "Administrator",
"Password": "3J17Il4KOR+wKPszf/0cHQ==",
"RealName": "超级管理员",
"IsEnable": 1,
"uuidRole": "f3932aeae93533f19c0519c4c14702aa",
"IsDel": 0,
"CreateTime": "2023-08-12 00:00:00.000",
"CreateName": "admin",
"UpdateTime": "2023-08-12 00:00:00.000",
"UpdateName": "admin"
},
{
"uuid": "2",
"UserName": "shortcut",
"Password": "4QrcOUm6Wau+VuBX8g+IPg==",
"RealName": "实验员",
"IsEnable": 1,
"uuidRole": "8c822592b360345fb59690e49ac6b181",
"IsDel": 0,
"CreateTime": null,
"CreateName": "admin",
"UpdateTime": "2023-10-23 00:00:00.000",
"UpdateName": null
}
]

View File

@@ -6,7 +6,7 @@ import os
import socket
import time
import uuid
from typing import Any, List, Dict, Optional, Tuple, TypedDict, Union, Sequence, Iterator, Literal
from typing import Any, List, Dict, Optional, OrderedDict, Tuple, TypedDict, Union, Sequence, Iterator, Literal
from pylabrobot.liquid_handling import (
LiquidHandlerBackend,
@@ -28,7 +28,7 @@ from pylabrobot.liquid_handling.standard import (
ResourceMove,
ResourceDrop,
)
from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash
from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash, TubeRack, PlateAdapter
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
@@ -70,50 +70,129 @@ class PRCXI9300Deck(Deck):
super().__init__(name, size_x, size_y, size_z)
self.slots = [None] * 6 # PRCXI 9300 有 6 个槽位
class PRCXI9300Container(Plate, TipRack):
"""PRCXI 9300 的专用 Container 类,继承自 Plate和TipRack。
该类定义了 PRCXI 9300 的工作台布局和槽位信息
class PRCXI9300Plate(Plate):
"""
专用孔板类:
1. 继承自 PLR 原生 Plate保留所有物理特性。
2. 增加 material_info 参数,用于在初始化时直接绑定 Unilab UUID
"""
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
category: str,
ordering: collections.OrderedDict,
model: Optional[str] = None,
**kwargs,
):
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
category: str = "plate",
ordered_items: collections.OrderedDict = None,
ordering: Optional[collections.OrderedDict] = None,
model: Optional[str] = None,
material_info: Optional[Dict[str, Any]] = None,
**kwargs):
items = ordered_items if ordered_items is not None else ordering
super().__init__(name, size_x, size_y, size_z,
ordered_items=items,
category=category,
model=model, **kwargs)
self._unilabos_state = {}
if material_info:
self._unilabos_state["Material"] = material_info
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]]:
try:
data = super().serialize_state()
except AttributeError:
data = {}
if hasattr(self, '_unilabos_state') and self._unilabos_state:
safe_state = {}
for k, v in self._unilabos_state.items():
# 如果是 Material 字典,深入检查
if k == "Material" and isinstance(v, dict):
safe_material = {}
for mk, mv in v.items():
# 只保留基本数据类型 (字符串, 数字, 布尔值, 列表, 字典)
if isinstance(mv, (str, int, float, bool, list, dict, type(None))):
safe_material[mk] = mv
else:
# 打印日志提醒(可选)
# print(f"Warning: Removing non-serializable key {mk} from {self.name}")
pass
safe_state[k] = safe_material
# 其他顶层属性也进行类型检查
elif isinstance(v, (str, int, float, bool, list, dict, type(None))):
safe_state[k] = v
data.update(safe_state)
return data
class PRCXI9300TipRack(TipRack):
""" 专用吸头盒类 """
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
category: str = "tip_rack",
ordered_items: collections.OrderedDict = None,
ordering: Optional[collections.OrderedDict] = None,
model: Optional[str] = None,
material_info: Optional[Dict[str, Any]] = None,
**kwargs):
items = ordered_items if ordered_items is not None else ordering
super().__init__(name, size_x, size_y, size_z,
ordered_items=items,
category=category,
model=model, **kwargs)
self._unilabos_state = {}
if material_info:
self._unilabos_state["Material"] = material_info
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)
try:
data = super().serialize_state()
except AttributeError:
data = {}
if hasattr(self, '_unilabos_state') and self._unilabos_state:
safe_state = {}
for k, v in self._unilabos_state.items():
# 如果是 Material 字典,深入检查
if k == "Material" and isinstance(v, dict):
safe_material = {}
for mk, mv in v.items():
# 只保留基本数据类型 (字符串, 数字, 布尔值, 列表, 字典)
if isinstance(mv, (str, int, float, bool, list, dict, type(None))):
safe_material[mk] = mv
else:
# 打印日志提醒(可选)
# print(f"Warning: Removing non-serializable key {mk} from {self.name}")
pass
safe_state[k] = safe_material
# 其他顶层属性也进行类型检查
elif isinstance(v, (str, int, float, bool, list, dict, type(None))):
safe_state[k] = v
data.update(safe_state)
return data
class PRCXI9300Trash(Trash):
"""PRCXI 9300 的专用 Trash 类,继承自 Trash。
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
"""
def __init__(self, name: str, size_x: float, size_y: float, size_z: float, category: str, **kwargs):
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
category: str = "trash",
material_info: Optional[Dict[str, Any]] = None,
**kwargs):
if name != "trash":
name = "trash"
print("PRCXI9300Trash name must be 'trash', using 'trash' instead.")
super().__init__(name, size_x, size_y, size_z, category=category, **kwargs)
print(f"Warning: PRCXI9300Trash usually expects name='trash' for backend logic, but got '{name}'.")
super().__init__(name, size_x, size_y, size_z, **kwargs)
self._unilabos_state = {}
# 初始化时注入 UUID
if material_info:
self._unilabos_state["Material"] = material_info
def load_state(self, state: Dict[str, Any]) -> None:
"""从给定的状态加载工作台信息。"""
@@ -121,10 +200,152 @@ class PRCXI9300Trash(Trash):
self._unilabos_state = state
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
data = super().serialize_state()
data.update(self._unilabos_state)
try:
data = super().serialize_state()
except AttributeError:
data = {}
if hasattr(self, '_unilabos_state') and self._unilabos_state:
safe_state = {}
for k, v in self._unilabos_state.items():
# 如果是 Material 字典,深入检查
if k == "Material" and isinstance(v, dict):
safe_material = {}
for mk, mv in v.items():
# 只保留基本数据类型 (字符串, 数字, 布尔值, 列表, 字典)
if isinstance(mv, (str, int, float, bool, list, dict, type(None))):
safe_material[mk] = mv
else:
# 打印日志提醒(可选)
# print(f"Warning: Removing non-serializable key {mk} from {self.name}")
pass
safe_state[k] = safe_material
# 其他顶层属性也进行类型检查
elif isinstance(v, (str, int, float, bool, list, dict, type(None))):
safe_state[k] = v
data.update(safe_state)
return data
class PRCXI9300TubeRack(TubeRack):
"""
专用管架类:用于 EP 管架、试管架等。
继承自 PLR 的 TubeRack并支持注入 material_info (UUID)。
"""
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
category: str = "tube_rack",
items: Optional[Dict[str, Any]] = None,
ordered_items: Optional[OrderedDict] = None,
model: Optional[str] = None,
material_info: Optional[Dict[str, Any]] = None,
**kwargs):
# 兼容处理PLR 的 TubeRack 构造函数可能接受 items 或 ordered_items
items_to_pass = items if items is not None else ordered_items
super().__init__(name, size_x, size_y, size_z,
ordered_items=ordered_items,
model=model,
**kwargs)
self._unilabos_state = {}
if material_info:
self._unilabos_state["Material"] = material_info
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
try:
data = super().serialize_state()
except AttributeError:
data = {}
if hasattr(self, '_unilabos_state') and self._unilabos_state:
safe_state = {}
for k, v in self._unilabos_state.items():
# 如果是 Material 字典,深入检查
if k == "Material" and isinstance(v, dict):
safe_material = {}
for mk, mv in v.items():
# 只保留基本数据类型 (字符串, 数字, 布尔值, 列表, 字典)
if isinstance(mv, (str, int, float, bool, list, dict, type(None))):
safe_material[mk] = mv
else:
# 打印日志提醒(可选)
# print(f"Warning: Removing non-serializable key {mk} from {self.name}")
pass
safe_state[k] = safe_material
# 其他顶层属性也进行类型检查
elif isinstance(v, (str, int, float, bool, list, dict, type(None))):
safe_state[k] = v
data.update(safe_state)
return data
class PRCXI9300PlateAdapter(PlateAdapter):
"""
专用板式适配器类:用于承载 Plate 的底座(如 PCR 适配器、磁吸架等)。
支持注入 material_info (UUID)。
"""
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
category: str = "plate_adapter",
model: Optional[str] = None,
material_info: Optional[Dict[str, Any]] = None,
# 参数给予默认值 (标准96孔板尺寸)
adapter_hole_size_x: float = 127.76,
adapter_hole_size_y: float = 85.48,
adapter_hole_size_z: float = 10.0, # 假设凹槽深度或板子放置高度
dx: Optional[float] = None,
dy: Optional[float] = None,
dz: float = 0.0, # 默认Z轴偏移
**kwargs):
# 自动居中计算:如果未指定 dx/dy则根据适配器尺寸和孔尺寸计算居中位置
if dx is None:
dx = (size_x - adapter_hole_size_x) / 2
if dy is None:
dy = (size_y - adapter_hole_size_y) / 2
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
dx=dx,
dy=dy,
dz=dz,
adapter_hole_size_x=adapter_hole_size_x,
adapter_hole_size_y=adapter_hole_size_y,
adapter_hole_size_z=adapter_hole_size_z,
model=model,
**kwargs
)
self._unilabos_state = {}
if material_info:
self._unilabos_state["Material"] = material_info
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
try:
data = super().serialize_state()
except AttributeError:
data = {}
if hasattr(self, '_unilabos_state') and self._unilabos_state:
safe_state = {}
for k, v in self._unilabos_state.items():
# 如果是 Material 字典,深入检查
if k == "Material" and isinstance(v, dict):
safe_material = {}
for mk, mv in v.items():
# 只保留基本数据类型 (字符串, 数字, 布尔值, 列表, 字典)
if isinstance(mv, (str, int, float, bool, list, dict, type(None))):
safe_material[mk] = mv
else:
# 打印日志提醒(可选)
# print(f"Warning: Removing non-serializable key {mk} from {self.name}")
pass
safe_state[k] = safe_material
# 其他顶层属性也进行类型检查
elif isinstance(v, (str, int, float, bool, list, dict, type(None))):
safe_state[k] = v
data.update(safe_state)
return data
class PRCXI9300Handler(LiquidHandlerAbstract):
support_touch_tip = False
@@ -154,10 +375,15 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
tablets_info = []
count = 0
for child in deck.children:
if "Material" in child._unilabos_state:
child_state = getattr(child, "_unilabos_state", {})
if "Material" in child_state:
count += 1
tablets_info.append(
WorkTablets(Number=count, Code=f"T{count}", Material=child._unilabos_state["Material"])
WorkTablets(
Number=count,
Code=f"T{count}",
Material=child_state["Material"]
)
)
if is_9320:
print("当前设备是9320")
@@ -434,7 +660,6 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
async def move_to(self, well: Well, dis_to_top: float = 0, channel: int = 0):
return await super().move_to(well, dis_to_top, channel)
class PRCXI9300Backend(LiquidHandlerBackend):
"""PRCXI 9300 的后端实现,继承自 LiquidHandlerBackend。
@@ -1533,31 +1758,31 @@ if __name__ == "__main__":
from pylabrobot.resources.opentrons.tip_racks import tipone_96_tiprack_200ul, opentrons_96_tiprack_10ul
from pylabrobot.resources.opentrons.plates import corning_96_wellplate_360ul_flat, nest_96_wellplate_2ml_deep
def get_well_container(name: str) -> PRCXI9300Container:
def get_well_container(name: str) -> PRCXI9300Plate:
well_containers = corning_96_wellplate_360ul_flat(name).serialize()
plate = PRCXI9300Container(
name=name, size_x=50, size_y=50, size_z=10, category="plate", ordering=well_containers["ordering"]
plate = PRCXI9300Plate(
name=name, size_x=50, size_y=50, size_z=10, category="plate", ordered_items=well_containers["ordering"]
)
plate_serialized = plate.serialize()
plate_serialized["parent_name"] = deck.name
well_containers.update({k: v for k, v in plate_serialized.items() if k not in ["children"]})
new_plate: PRCXI9300Container = PRCXI9300Container.deserialize(well_containers)
new_plate: PRCXI9300Plate = PRCXI9300Plate.deserialize(well_containers)
return new_plate
def get_tip_rack(name: str, child_prefix: str = "tip") -> PRCXI9300Container:
def get_tip_rack(name: str, child_prefix: str = "tip") -> PRCXI9300TipRack:
tip_racks = opentrons_96_tiprack_10ul(name).serialize()
tip_rack = PRCXI9300Container(
tip_rack = PRCXI9300TipRack(
name=name,
size_x=50,
size_y=50,
size_z=10,
category="tip_rack",
ordering=collections.OrderedDict({k: f"{child_prefix}_{k}" for k, v in tip_racks["ordering"].items()}),
ordered_items=collections.OrderedDict({k: f"{child_prefix}_{k}" for k, v in tip_racks["ordering"].items()}),
)
tip_rack_serialized = tip_rack.serialize()
tip_rack_serialized["parent_name"] = deck.name
tip_racks.update({k: v for k, v in tip_rack_serialized.items() if k not in ["children"]})
new_tip_rack: PRCXI9300Container = PRCXI9300Container.deserialize(tip_racks)
new_tip_rack: PRCXI9300TipRack = PRCXI9300TipRack.deserialize(tip_racks)
return new_tip_rack
plate1 = get_tip_rack("RackT1")
@@ -1604,8 +1829,8 @@ if __name__ == "__main__":
}
}
)
plate7 = PRCXI9300Container(
name="plateT7", size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict()
plate7 = PRCXI9300Plate(
name="plateT7", size_x=50, size_y=50, size_z=10, category="plate", ordered_items=collections.OrderedDict()
)
plate7.load_state({"Material": {"uuid": "04211a2dc93547fe9bf6121eac533650"}})
plate8 = get_tip_rack("PlateT8")
@@ -1679,13 +1904,13 @@ if __name__ == "__main__":
deck.assign_child_resource(plate1, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate2, location=Coordinate(0, 0, 0))
deck.assign_child_resource(
PRCXI9300Container(
PRCXI9300Plate(
name="container_for_nothin3",
size_x=50,
size_y=50,
size_z=10,
category="plate",
ordering=collections.OrderedDict(),
ordered_items=collections.OrderedDict(),
),
location=Coordinate(0, 0, 0),
)
@@ -1693,48 +1918,48 @@ if __name__ == "__main__":
deck.assign_child_resource(plate5, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate6, location=Coordinate(0, 0, 0))
deck.assign_child_resource(
PRCXI9300Container(
PRCXI9300Plate(
name="container_for_nothing7",
size_x=50,
size_y=50,
size_z=10,
category="plate",
ordering=collections.OrderedDict(),
ordered_items=collections.OrderedDict(),
),
location=Coordinate(0, 0, 0),
)
deck.assign_child_resource(
PRCXI9300Container(
PRCXI9300Plate(
name="container_for_nothing8",
size_x=50,
size_y=50,
size_z=10,
category="plate",
ordering=collections.OrderedDict(),
ordered_items=collections.OrderedDict(),
),
location=Coordinate(0, 0, 0),
)
deck.assign_child_resource(plate9, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate10, location=Coordinate(0, 0, 0))
deck.assign_child_resource(
PRCXI9300Container(
PRCXI9300Plate(
name="container_for_nothing11",
size_x=50,
size_y=50,
size_z=10,
category="plate",
ordering=collections.OrderedDict(),
ordered_items=collections.OrderedDict(),
),
location=Coordinate(0, 0, 0),
)
deck.assign_child_resource(
PRCXI9300Container(
PRCXI9300Plate(
name="container_for_nothing12",
size_x=50,
size_y=50,
size_z=10,
category="plate",
ordering=collections.OrderedDict(),
ordered_items=collections.OrderedDict(),
),
location=Coordinate(0, 0, 0),
)

View File

@@ -0,0 +1,841 @@
from typing import Optional
from pylabrobot.resources import Tube, Coordinate
from pylabrobot.resources.well import Well, WellBottomType, CrossSectionType
from pylabrobot.resources.tip import Tip, TipCreator
from pylabrobot.resources.tip_rack import TipRack, TipSpot
from pylabrobot.resources.utils import create_ordered_items_2d
from pylabrobot.resources.height_volume_functions import (
compute_height_from_volume_rectangle,
compute_volume_from_height_rectangle,
)
from .prcxi import PRCXI9300Plate, PRCXI9300TipRack, PRCXI9300Trash, PRCXI9300TubeRack, PRCXI9300PlateAdapter
def _make_tip_helper(volume: float, length: float, depth: float) -> Tip:
"""
PLR 的 Tip 类参数名为: maximal_volume, total_tip_length, fitting_depth
"""
return Tip(
has_filter=False, # 默认无滤芯
maximal_volume=volume,
total_tip_length=length,
fitting_depth=depth
)
# =========================================================================
# 标准品 参照 PLR 标准库的参数,但是用 PRCXI9300Plate 实例化,并注入 UUID
# =========================================================================
def PRCXI_BioER_96_wellplate(name: str) -> PRCXI9300Plate:
"""
对应 JSON Code: ZX-019-2.2 (2.2ml 深孔板)
原型: pylabrobot.resources.bioer.BioER_96_wellplate_Vb_2200uL
"""
return PRCXI9300Plate(
name=name,
size_x=127.1,
size_y=85.0,
size_z=44.2,
lid=None,
model="PRCXI_BioER_96_wellplate",
category="plate",
material_info={
"uuid": "ca877b8b114a4310b429d1de4aae96ee",
"Code": "ZX-019-2.2",
"Name": "2.2ml 深孔板",
"materialEnum": 0,
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
Well,
size_x=8.25,
size_y=8.25,
size_z=39.3, # 修改过
dx=9.5,
dy=7.5,
dz=6,
material_z_thickness=0.8,
item_dx=9.0,
item_dy=9.0,
num_items_x=12,
num_items_y=8,
cross_section_type=CrossSectionType.RECTANGLE,
bottom_type=WellBottomType.V, # 是否需要修改?
max_volume=2200,
),
)
def PRCXI_nest_1_troughplate(name: str) -> PRCXI9300Plate:
"""
对应 JSON Code: ZX-58-10000 (储液槽)
原型: pylabrobot.resources.nest.nest_1_troughplate_195000uL_Vb
"""
well_size_x = 127.76 - (14.38 - 9 / 2) * 2
well_size_y = 85.48 - (11.24 - 9 / 2) * 2
well_kwargs = {
"size_x": well_size_x,
"size_y": well_size_y,
"size_z": 26.85,
"bottom_type": WellBottomType.V,
"compute_height_from_volume": lambda liquid_volume: compute_height_from_volume_rectangle(
liquid_volume=liquid_volume, well_length=well_size_x, well_width=well_size_y
),
"compute_volume_from_height": lambda liquid_height: compute_volume_from_height_rectangle(
liquid_height=liquid_height, well_length=well_size_x, well_width=well_size_y
),
"material_z_thickness": 31.4 - 26.85 - 3.55,
}
return PRCXI9300Plate(
name=name,
size_x=127.76,
size_y=85.48,
size_z=31.4,
lid=None,
model="PRCXI_Nest_1_troughplate",
category="plate",
material_info={
"uuid": "04211a2dc93547fe9bf6121eac533650",
"Code": "ZX-58-10000",
"Name": "储液槽",
"materialEnum": 0,
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
Well,
num_items_x=1,
num_items_y=1,
dx=14.38 - 9 / 2,
dy=11.24 - 9 / 2,
dz=3.55,
item_dx=9.0,
item_dy=9.0,
**well_kwargs, # 传入上面计算好的孔参数
),
)
def PRCXI_BioRad_384_wellplate(name: str) -> PRCXI9300Plate:
"""
对应 JSON Code: q3 (384板)
原型: pylabrobot.resources.biorad.BioRad_384_wellplate_50uL_Vb
"""
return PRCXI9300Plate(
name=name,
# 直接抄录 PLR 标准品的物理尺寸
size_x=127.76,
size_y=85.48,
size_z=10.40,
model="BioRad_384_wellplate_50uL_Vb",
category="plate",
# 2. 注入 Unilab 必须的 UUID 信息
material_info={
"uuid": "853dcfb6226f476e8b23c250217dc7da",
"Code": "q3",
"Name": "384板",
"SupplyType": 1,
},
# 3. 定义孔的排列 (抄录标准参数)
ordered_items=create_ordered_items_2d(
Well,
num_items_x=24,
num_items_y=16,
dx=10.58, # A1 左边缘距离板子左边缘 需要进一步测量
dy=7.44, # P1 下边缘距离板子下边缘 需要进一步测量
dz=1.05,
item_dx=4.5,
item_dy=4.5,
size_x=3.10,
size_y=3.10,
size_z=9.35,
max_volume=50,
material_z_thickness=1,
bottom_type=WellBottomType.V,
cross_section_type=CrossSectionType.CIRCLE,
)
)
def PRCXI_AGenBio_4_troughplate(name: str) -> PRCXI9300Plate:
"""
对应 JSON Code: sdfrth654 (4道储液槽)
原型: pylabrobot.resources.agenbio.AGenBio_4_troughplate_75000uL_Vb
"""
INNER_WELL_WIDTH = 26.1
INNER_WELL_LENGTH = 71.2
well_kwargs = {
"size_x": 26,
"size_y": 71.2,
"size_z": 42.55,
"bottom_type": WellBottomType.FLAT,
"cross_section_type": CrossSectionType.RECTANGLE,
"compute_height_from_volume": lambda liquid_volume: compute_height_from_volume_rectangle(
liquid_volume,
INNER_WELL_LENGTH,
INNER_WELL_WIDTH,
),
"compute_volume_from_height": lambda liquid_height: compute_volume_from_height_rectangle(
liquid_height,
INNER_WELL_LENGTH,
INNER_WELL_WIDTH,
),
"material_z_thickness": 1,
}
return PRCXI9300Plate(
name=name,
size_x=127.76,
size_y=85.48,
size_z=43.80,
model="PRCXI_AGenBio_4_troughplate",
category="plate",
material_info={
"uuid": "01953864f6f140ccaa8ddffd4f3e46f5",
"Code": "sdfrth654",
"Name": "4道储液槽",
"materialEnum": 0,
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
Well,
num_items_x=4,
num_items_y=1,
dx=9.8,
dy=7.2,
dz=0.9,
item_dx=INNER_WELL_WIDTH + 1, # 1 mm wall thickness
item_dy=INNER_WELL_LENGTH,
**well_kwargs,
),
)
def PRCXI_nest_12_troughplate(name: str) -> PRCXI9300Plate:
"""
对应 JSON Code: 12道储液槽 (12道储液槽)
原型: pylabrobot.resources.nest.nest_12_troughplate_15000uL_Vb
"""
well_size_x = 8.2
well_size_y = 71.2
well_kwargs = {
"size_x": well_size_x,
"size_y": well_size_y,
"size_z": 26.85,
"bottom_type": WellBottomType.V,
"compute_height_from_volume": lambda liquid_volume: compute_height_from_volume_rectangle(
liquid_volume=liquid_volume, well_length=well_size_x, well_width=well_size_y
),
"compute_volume_from_height": lambda liquid_height: compute_volume_from_height_rectangle(
liquid_height=liquid_height, well_length=well_size_x, well_width=well_size_y
),
"material_z_thickness": 31.4 - 26.85 - 3.55,
}
return PRCXI9300Plate(
name=name,
size_x=127.76,
size_y=85.48,
size_z=31.4,
lid=None,
model="PRCXI_nest_12_troughplate",
category="plate",
material_info={
"uuid": "0f1639987b154e1fac78f4fb29a1f7c1",
"Code": "12道储液槽",
"Name": "12道储液槽",
"materialEnum": 0,
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
Well,
num_items_x=12,
num_items_y=1,
dx=14.38 - 8.2 / 2,
dy=(85.48 - 71.2) / 2,
dz=3.55,
item_dx=9.0,
item_dy=9.0,
**well_kwargs,
),
)
def PRCXI_CellTreat_96_wellplate(name: str) -> PRCXI9300Plate:
"""
对应 JSON Code: ZX-78-096 (细菌培养皿)
原型: pylabrobot.resources.celltreat.CellTreat_96_wellplate_350ul_Fb
"""
well_kwargs = {
"size_x": 6.96,
"size_y": 6.96,
"size_z": 10.04,
"bottom_type": WellBottomType.FLAT,
"material_z_thickness": 1.75,
"cross_section_type": CrossSectionType.CIRCLE,
"max_volume": 300,
}
return PRCXI9300Plate(
name=name,
size_x=127.61,
size_y=85.24,
size_z=14.30,
lid=None,
model="PRCXI_CellTreat_96_wellplate",
category="plate",
material_info={
"uuid": "b05b3b2aafd94ec38ea0cd3215ecea8f",
"Code": "ZX-78-096",
"Name": "细菌培养皿",
"materialEnum": 4,
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
Well,
num_items_x=12,
num_items_y=8,
dx=10.83,
dy=7.67,
dz=4.05,
item_dx=9,
item_dy=9,
**well_kwargs,
),
)
# =========================================================================
# 自定义/需测量品 (Custom Measurement)
# =========================================================================
def PRCXI_10ul_eTips(name: str) -> PRCXI9300TipRack:
"""
对应 JSON Code: ZX-001-10+
"""
return PRCXI9300TipRack(
name=name,
size_x=122.11,
size_y=85.48, #修改
size_z=58.23,
model="PRCXI_10ul_eTips",
material_info={
"uuid": "068b3815e36b4a72a59bae017011b29f",
"Code": "ZX-001-10+",
"Name": "10μL加长 Tip头",
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
TipSpot,
num_items_x=12,
num_items_y=8,
dx=7.97, #需要修改
dy=5.0, #需修改
dz=2.0, #需修改
item_dx=9.0,
item_dy=9.0,
size_x=7.0,
size_y=7.0,
size_z=0,
make_tip=lambda: _make_tip_helper(volume=10, length=52.0, depth=45.1)
)
)
def PRCXI_300ul_Tips(name: str) -> PRCXI9300TipRack:
"""
对应 JSON Code: ZX-001-300
吸头盒通常比较特殊,需要定义 Tip 对象
"""
return PRCXI9300TipRack(
name=name,
size_x=122.11,
size_y=85.48, #修改
size_z=58.23,
model="PRCXI_300ul_Tips",
material_info={
"uuid": "076250742950465b9d6ea29a225dfb00",
"Code": "ZX-001-300",
"Name": "300μL Tip头",
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
TipSpot,
num_items_x=12,
num_items_y=8,
dx=7.97, #需要修改
dy=5.0, #需修改
dz=2.0, #需修改
item_dx=9.0,
item_dy=9.0,
size_x=7.0,
size_y=7.0,
size_z=0,
make_tip=lambda: _make_tip_helper(volume=300, length=60.0, depth=51.0)
)
)
def PRCXI_PCR_Plate_200uL_nonskirted(name: str) -> PRCXI9300Plate:
"""
对应 JSON Code: ZX-023-0.2 (0.2ml PCR 板)
"""
return PRCXI9300Plate(
name=name,
size_x=119.5,
size_y=80.0,
size_z=26.0,
model="PRCXI_PCR_Plate_200uL_nonskirted",
plate_type="non-skirted",
category="plate",
material_info={
"uuid": "73bb9b10bc394978b70e027bf45ce2d3",
"Code": "ZX-023-0.2",
"Name": "0.2ml PCR 板",
"materialEnum": 0,
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
Well,
num_items_x=12,
num_items_y=8,
dx=7,
dy=5,
dz=0.0,
item_dx=9,
item_dy=9,
size_x=6,
size_y=6,
size_z=15.17,
bottom_type=WellBottomType.V,
cross_section_type=CrossSectionType.CIRCLE,
),
)
def PRCXI_PCR_Plate_200uL_semiskirted(name: str) -> PRCXI9300Plate:
"""
对应 JSON Code: ZX-023-0.2 (0.2ml PCR 板)
"""
return PRCXI9300Plate(
name=name,
size_x=126,
size_y=86,
size_z=21.2,
model="PRCXI_PCR_Plate_200uL_semiskirted",
plate_type="semi-skirted",
category="plate",
material_info={
"uuid": "73bb9b10bc394978b70e027bf45ce2d3",
"Code": "ZX-023-0.2",
"Name": "0.2ml PCR 板",
"materialEnum": 0,
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
Well,
num_items_x=12,
num_items_y=8,
dx=11,
dy=8,
dz=0.0,
item_dx=9,
item_dy=9,
size_x=6,
size_y=6,
size_z=15.17,
bottom_type=WellBottomType.V,
cross_section_type=CrossSectionType.CIRCLE,
),
)
def PRCXI_PCR_Plate_200uL_skirted(name: str) -> PRCXI9300Plate:
"""
对应 JSON Code: ZX-023-0.2 (0.2ml PCR 板)
"""
return PRCXI9300Plate(
name=name,
size_x=127.76,
size_y=86,
size_z=16.1,
model="PRCXI_PCR_Plate_200uL_skirted",
plate_type="skirted",
category="plate",
material_info={
"uuid": "73bb9b10bc394978b70e027bf45ce2d3",
"Code": "ZX-023-0.2",
"Name": "0.2ml PCR 板",
"materialEnum": 0,
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
Well,
num_items_x=12,
num_items_y=8,
dx=11,
dy=8.49,
dz=0.8,
item_dx=9,
item_dy=9,
size_x=6,
size_y=6,
size_z=15.1,
bottom_type=WellBottomType.V,
cross_section_type=CrossSectionType.CIRCLE,
),
)
def PRCXI_trash(name: str = "trash") -> PRCXI9300Trash:
"""
对应 JSON Code: q1 (废弃槽)
"""
return PRCXI9300Trash(
name="trash",
size_x=126.59,
size_y=84.87,
size_z=89.5, # 修改
category="trash",
model="PRCXI_trash",
material_info={
"uuid": "730067cf07ae43849ddf4034299030e9",
"Code": "q1",
"Name": "废弃槽",
"materialEnum": 0,
"SupplyType": 1
}
)
def PRCXI_96_DeepWell(name: str) -> PRCXI9300Plate:
"""
对应 JSON Code: q2 (96深孔板)
"""
return PRCXI9300Plate(
name=name,
size_x=127.3,
size_y=85.35,
size_z=45.0, #修改
model="PRCXI_96_DeepWell",
material_info={
"uuid": "57b1e4711e9e4a32b529f3132fc5931f", # 对应 q2 uuid
"Code": "q2",
"Name": "96深孔板",
"materialEnum": 0
},
ordered_items=create_ordered_items_2d(
Well,
num_items_x=12,
num_items_y=8,
dx=10.9,
dy=8.25,
dz=2.0,
item_dx=9.0,
item_dy=9.0,
size_x=8.2,
size_y=8.2,
size_z=42.0,
max_volume=2200
)
)
def PRCXI_EP_Adapter(name: str) -> PRCXI9300TubeRack:
"""
对应 JSON Code: 1 (ep适配器)
这是一个 4x6 的 EP 管架,适配 1.5mL/2.0mL 离心管
"""
ep_tube_prototype = Tube(
name="EP_Tube_1.5mL",
size_x=10.6,
size_y=10.6,
size_z=40.0, # 管子本身的高度,通常比架子孔略高或持平
max_volume=1500,
model="EP_Tube_1.5mL"
)
# 计算 PRCXI9300TubeRack 中孔的起始位置 dx, dy
dy_calc = 85.8 - 10.5 - (3 * 18) - 10.6
dx_calc = 3.54
return PRCXI9300TubeRack(
name=name,
size_x=128.04,
size_y=85.8,
size_z=42.66,
model="PRCXI_EP_Adapter",
category="tube_rack",
material_info={
"uuid": "e146697c395e4eabb3d6b74f0dd6aaf7",
"Code": "1",
"Name": "ep适配器",
"materialEnum": 0,
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
Tube,
num_items_x=6,
num_items_y=4,
dx=dx_calc,
dy=dy_calc,
dz=42.66 - 38.08, # 架高 - 孔深
item_dx=21.0,
item_dy=18.0,
size_x=10.6,
size_y=10.6,
size_z=40.0,
max_volume=1500
)
)
# =========================================================================
# 无实物,需要测量
# =========================================================================
def PRCXI_Tip1250_Adapter(name: str) -> PRCXI9300PlateAdapter:
""" Code: ZX-58-1250 """
return PRCXI9300PlateAdapter(
name=name,
size_x=128,
size_y=85,
size_z=20,
material_info={
"uuid": "3b6f33ffbf734014bcc20e3c63e124d4",
"Code": "ZX-58-1250",
"Name": "Tip头适配器 1250uL",
"SupplyType": 2
}
)
def PRCXI_Tip300_Adapter(name: str) -> PRCXI9300PlateAdapter:
""" Code: ZX-58-300 """
return PRCXI9300PlateAdapter(
name=name,
size_x=127,
size_y=85,
size_z=81,
material_info={
"uuid": "7c822592b360451fb59690e49ac6b181",
"Code": "ZX-58-300",
"Name": "ZHONGXI 适配器 300uL",
"SupplyType": 2
}
)
def PRCXI_Tip10_Adapter(name: str) -> PRCXI9300PlateAdapter:
""" Code: ZX-58-10 """
return PRCXI9300PlateAdapter(
name=name,
size_x=128,
size_y=85,
size_z=72.3,
material_info={
"uuid": "8cc3dce884ac41c09f4570d0bcbfb01c",
"Code": "ZX-58-10",
"Name": "吸头10ul 适配器",
"SupplyType": 2
}
)
def PRCXI_1250uL_Tips(name: str) -> PRCXI9300TipRack:
""" Code: ZX-001-1250 """
return PRCXI9300TipRack(
name=name,
size_x=118.09,
size_y=80.7,
size_z=107.67,
model="PRCXI_1250uL_Tips",
material_info={
"uuid": "7960f49ddfe9448abadda89bd1556936",
"Code": "ZX-001-1250",
"Name": "1250μL Tip头",
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
TipSpot,
num_items_x=12,
num_items_y=8,
dx=9.545 - 7.95/2,
dy=8.85 - 7.95/2,
dz=2.0,
item_dx=9,
item_dy=9,
size_x=7.0,
size_y=7.0,
size_z=0,
make_tip=lambda: _make_tip_helper(volume=1250, length=107.67, depth=8)
)
)
def PRCXI_10uL_Tips(name: str) -> PRCXI9300TipRack:
""" Code: ZX-001-10 """
return PRCXI9300TipRack(
name=name,
size_x=120.98,
size_y=82.12,
size_z=67,
model="PRCXI_10uL_Tips",
material_info={
"uuid": "45f2ed3ad925484d96463d675a0ebf66",
"Code": "ZX-001-10",
"Name": "10μL Tip头",
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
TipSpot,
num_items_x=12,
num_items_y=8,
dx=10.99 - 5/2,
dy=9.56 - 5/2,
dz=2.0,
item_dx=9,
item_dy=9,
size_x=7.0,
size_y=7.0,
size_z=0,
make_tip=lambda: _make_tip_helper(volume=1250, length=52.0, depth=5)
)
)
def PRCXI_1000uL_Tips(name: str) -> PRCXI9300TipRack:
""" Code: ZX-001-1000 """
return PRCXI9300TipRack(
name=name,
size_x=128.09,
size_y=85.8,
size_z=98,
model="PRCXI_1000uL_Tips",
material_info={
"uuid": "80652665f6a54402b2408d50b40398df",
"Code": "ZX-001-1000",
"Name": "1000μL Tip头",
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
TipSpot,
num_items_x=12,
num_items_y=8,
dx=14.5 - 7.95/2,
dy=7.425,
dz=2.0,
item_dx=9,
item_dy=9,
size_x=7.0,
size_y=7.0,
size_z=0,
make_tip=lambda: _make_tip_helper(volume=1000, length=55.0, depth=8)
)
)
def PRCXI_200uL_Tips(name: str) -> PRCXI9300TipRack:
""" Code: ZX-001-200 """
return PRCXI9300TipRack(
name=name,
size_x=120.98,
size_y=82.12,
size_z=66.9,
model="PRCXI_200uL_Tips",
material_info={
"uuid": "7a73bb9e5c264515a8fcbe88aed0e6f7",
"Code": "ZX-001-200",
"Name": "200μL Tip头",
"SupplyType": 1},
ordered_items=create_ordered_items_2d(
TipSpot,
num_items_x=12,
num_items_y=8,
dx=10.99 - 5.5/2,
dy=9.56 - 5.5/2,
dz=2.0,
item_dx=9,
item_dy=9,
size_x=7.0,
size_z=0,
size_y=7.0,
make_tip=lambda: _make_tip_helper(volume=200, length=52.0, depth=5)
)
)
def PRCXI_PCR_Adapter(name: str) -> PRCXI9300PlateAdapter:
"""
对应 JSON Code: ZX-58-0001 (全裙边 PCR适配器)
"""
return PRCXI9300PlateAdapter(
name=name,
size_x=127.76,
size_y=85.48,
size_z=21.69,
model="PRCXI_PCR_Adapter",
material_info={
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
"Code": "ZX-58-0001",
"Name": "全裙边 PCR适配器",
"materialEnum": 3,
"SupplyType": 2
}
)
def PRCXI_Reservoir_Adapter(name: str) -> PRCXI9300PlateAdapter:
""" Code: ZX-ADP-001 """
return PRCXI9300PlateAdapter(
name=name,
size_x=133,
size_y=91.8,
size_z=70,
material_info={
"uuid": "6bdfdd7069df453896b0806df50f2f4d",
"Code": "ZX-ADP-001",
"Name": "储液槽 适配器",
"SupplyType": 2
}
)
def PRCXI_Deep300_Adapter(name: str) -> PRCXI9300PlateAdapter:
""" Code: ZX-002-300 """
return PRCXI9300PlateAdapter(
name=name,
size_x=136.4,
size_y=93.8,
size_z=96,
material_info={
"uuid": "9a439bed8f3344549643d6b3bc5a5eb4",
"Code": "ZX-002-300",
"Name": "300ul深孔板适配器",
"SupplyType": 2
}
)
def PRCXI_Deep10_Adapter(name: str) -> PRCXI9300PlateAdapter:
""" Code: ZX-002-10 """
return PRCXI9300PlateAdapter(
name=name,
size_x=136.5,
size_y=93.8,
size_z=121.5,
material_info={
"uuid": "4dc8d6ecfd0449549683b8ef815a861b",
"Code": "ZX-002-10",
"Name": "10ul专用深孔板适配器",
"SupplyType": 2
}
)
def PRCXI_Adapter(name: str) -> PRCXI9300PlateAdapter:
""" Code: Fhh478 """
return PRCXI9300PlateAdapter(
name=name,
size_x=120,
size_y=90,
size_z=86,
material_info={
"uuid": "adfabfffa8f24af5abfbba67b8d0f973",
"Code": "Fhh478",
"Name": "适配器",
"SupplyType": 2
}
)
def PRCXI_48_DeepWell(name: str) -> PRCXI9300Plate:
""" Code: 22 (48孔深孔板) """
print("Warning: Code '22' (48孔深孔板) dimensions are null in JSON.")
return PRCXI9300Plate(
name=name,
size_x=127,
size_y=85,
size_z=44,
model="PRCXI_48_DeepWell",
material_info={
"uuid": "026c5d5cf3d94e56b4e16b7fb53a995b",
"Code": "22",
"Name": "48孔深孔板",
"SupplyType": 1
},
ordered_items=create_ordered_items_2d(
Well,
num_items_x=6,
num_items_y=8,
dx=10,
dy=10,
dz=1,
item_dx=18.5,
item_dy=9,
size_x=8,
size_y=8,
size_z=40
)
)
def PRCXI_30mm_Adapter(name: str) -> PRCXI9300PlateAdapter:
""" Code: ZX-58-30 """
return PRCXI9300PlateAdapter(
name=name,
size_x=132,
size_y=93.5,
size_z=30,
material_info={
"uuid": "a0757a90d8e44e81a68f306a608694f2",
"Code": "ZX-58-30",
"Name": "30mm适配器",
"SupplyType": 2
}
)

View File

@@ -1,31 +0,0 @@
{
"Tip头适配器 1250uL": {"uuid": "3b6f33ffbf734014bcc20e3c63e124d4", "materialEnum": "0"},
"ZHONGXI 适配器 300uL": {"uuid": "7c822592b360451fb59690e49ac6b181", "materialEnum": "0"},
"吸头10ul 适配器": {"uuid": "8cc3dce884ac41c09f4570d0bcbfb01c", "materialEnum": "0"},
"1250μL Tip头": {"uuid": "7960f49ddfe9448abadda89bd1556936", "materialEnum": "0"},
"10μL Tip头": {"uuid": "45f2ed3ad925484d96463d675a0ebf66", "materialEnum": "0"},
"10μL加长 Tip头": {"uuid": "068b3815e36b4a72a59bae017011b29f", "materialEnum": "1"},
"1000μL Tip头": {"uuid": "80652665f6a54402b2408d50b40398df", "materialEnum": "1"},
"300μL Tip头": {"uuid": "076250742950465b9d6ea29a225dfb00", "materialEnum": "1"},
"200μL Tip头": {"uuid": "7a73bb9e5c264515a8fcbe88aed0e6f7", "materialEnum": "0"},
"0.2ml PCR板": {"uuid": "73bb9b10bc394978b70e027bf45ce2d3", "materialEnum": "0"},
"2.2ml 深孔板": {"uuid": "ca877b8b114a4310b429d1de4aae96ee", "materialEnum": "0"},
"储液槽": {"uuid": "04211a2dc93547fe9bf6121eac533650", "materialEnum": "0"},
"全裙边 PCR适配器": {"uuid": "4a043a07c65a4f9bb97745e1f129b165", "materialEnum": "3"},
"储液槽 适配器": {"uuid": "6bdfdd7069df453896b0806df50f2f4d", "materialEnum": "0"},
"300ul深孔板适配器": {"uuid": "9a439bed8f3344549643d6b3bc5a5eb4", "materialEnum": "0"},
"10ul专用深孔板适配器": {"uuid": "4dc8d6ecfd0449549683b8ef815a861b", "materialEnum": "0"},
"爱津": {"uuid": "b01627718d3341aba649baa81c2c083c", "materialEnum": "0"},
"适配器": {"uuid": "adfabfffa8f24af5abfbba67b8d0f973", "materialEnum": "0"},
"废弃槽": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": "0"},
"96深孔板": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": "0"},
"384板": {"uuid": "853dcfb6226f476e8b23c250217dc7da", "materialEnum": "0"},
"4道储液槽": {"uuid": "01953864f6f140ccaa8ddffd4f3e46f5", "materialEnum": "0"},
"48孔深孔板": {"uuid": "026c5d5cf3d94e56b4e16b7fb53a995b", "materialEnum": "2"},
"12道储液槽": {"uuid": "0f1639987b154e1fac78f4fb29a1f7c1", "materialEnum": "0"},
"HPLC料盘": {"uuid": "548bbc3df0d4447586f2c19d2c0c0c55", "materialEnum": "0"},
"ep适配器": {"uuid": "e146697c395e4eabb3d6b74f0dd6aaf7", "materialEnum": "0"},
"30mm适配器": {"uuid": "a0757a90d8e44e81a68f306a608694f2", "materialEnum": "0"},
"细菌培养皿": {"uuid": "b05b3b2aafd94ec38ea0cd3215ecea8f", "materialEnum": "4"},
"96 细胞培养皿":{ "uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": "0"}
}

View File

@@ -1,21 +0,0 @@
import collections
import json
from pathlib import Path
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Container
prcxi_materials_path = str(Path(__file__).parent / "prcxi_material.json")
with open(prcxi_materials_path, mode="r", encoding="utf-8") as f:
prcxi_materials = json.loads(f.read())
def tip_adaptor_1250ul(name="Tip头适配器 1250uL") -> PRCXI9300Container: # 必须传入一个name参数是plr的规范要求
# tip_rack = PRCXI9300Container(name, prcxi_materials["name"]["Height"])
tip_rack = PRCXI9300Container(name, 1000,400,800, "tip_rack", collections.OrderedDict())
tip_rack.load_state({
"Materials": {"uuid": "7960f49ddfe9448abadda89bd1556936", "materialEnum": "0"}
})
return tip_rack

View File

@@ -1,44 +0,0 @@
import collections
from pylabrobot.resources import opentrons_96_tiprack_10ul
from pylabrobot.resources.opentrons.plates import corning_96_wellplate_360ul_flat, nest_96_wellplate_2ml_deep
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Container, PRCXI9300Trash
def get_well_container(name: str) -> PRCXI9300Container:
well_containers = corning_96_wellplate_360ul_flat(name).serialize()
plate = PRCXI9300Container(name=name, size_x=50, size_y=50, size_z=10, category="plate",
ordering=collections.OrderedDict())
plate_serialized = plate.serialize()
well_containers.update({k: v for k, v in plate_serialized.items() if k not in ["children"]})
new_plate: PRCXI9300Container = PRCXI9300Container.deserialize(well_containers)
return new_plate
def get_tip_rack(name: str) -> PRCXI9300Container:
tip_racks = opentrons_96_tiprack_10ul("name").serialize()
tip_rack = PRCXI9300Container(name=name, size_x=50, size_y=50, size_z=10, category="tip_rack",
ordering=collections.OrderedDict())
tip_rack_serialized = tip_rack.serialize()
tip_racks.update({k: v for k, v in tip_rack_serialized.items() if k not in ["children"]})
new_tip_rack: PRCXI9300Container = PRCXI9300Container.deserialize(tip_racks)
return new_tip_rack
def prcxi_96_wellplate_360ul_flat(name: str):
return get_well_container(name)
def prcxi_opentrons_96_tiprack_10ul(name: str):
return get_tip_rack(name)
def prcxi_trash(name: str = None):
return PRCXI9300Trash(name="trash", size_x=50, size_y=50, size_z=10, category="trash")
if __name__ == "__main__":
# Example usage
test_plate = prcxi_96_wellplate_360ul_flat("test_plate")
test_rack = prcxi_opentrons_96_tiprack_10ul("test_rack")
tash = prcxi_trash("trash")
print(test_plate)
print(test_rack)
print(tash)
# Output will be a dictionary representation of the PRCXI9300Container with well details

View File

@@ -0,0 +1,560 @@
# 新威电池测试系统 - OSS 上传功能说明
## 功能概述
本次更新为新威电池测试系统添加了**阿里云 OSS 文件上传功能**,采用统一的 API 方式,允许将测试数据备份文件上传到云端存储。
## 版本更新说明
### ⚠️ 重大变更2025-12-17
本次更新将 OSS 上传方式从 **`oss2` 库** 改为 **统一 API 方式**,实现与团队其他系统的统一。
**主要变化**
- ✅ 用 `requests`
- ✅ 通过统一 API 获取预签名 URL 进行上传
- ✅ 简化环境变量配置(仅需要 JWT Token
- ✅ 返回文件访问 URL
## 主要改动
### 1. OSS 上传工具函数重构第30-200行
#### 新增函数
- **`get_upload_token(base_url, auth_token, scene, filename)`**
从统一 API 获取文件上传的预签名 URL
- **`upload_file_with_presigned_url(upload_info, file_path)`**
使用预签名 URL 上传文件到 OSS
#### 更新的函数
- **`upload_file_to_oss(local_file_path, oss_object_name)`**
上传单个文件到阿里云 OSS使用统一 API 方式)
- 返回值变更:成功时返回文件访问 URL失败时返回 `False`
- **`upload_files_to_oss(file_paths, oss_prefix)`**
批量上传文件列表
- `oss_prefix` 参数保留但暂不使用(接口兼容性)
- **`upload_directory_to_oss(local_dir, oss_prefix)`**
上传整个目录
- 简化实现,直接使用文件名上传
### 2. 环境变量配置简化
#### 新方式(推荐)
```bash
# ✅ 必需
UNI_LAB_AUTH_TOKEN # API Key 格式: "Api xxxxxx"
# ✅ 可选(有默认值)
UNI_LAB_BASE_URL (默认: https://uni-lab.test.bohrium.com)
UNI_LAB_UPLOAD_SCENE (默认: job其他值会被改成 default)
```
### 3. 初始化方法(保持不变)
`__init__` 方法中的 OSS 相关配置参数:
```python
# OSS 上传配置
self.oss_upload_enabled = False # 默认不启用 OSS 上传
self.oss_prefix = "neware_backup" # OSS 对象路径前缀
self._last_backup_dir = None # 记录最近一次的 backup_dir
```
**默认行为**OSS 上传功能默认关闭(`oss_upload_enabled=False`),不影响现有系统。
### 4. upload_backup_to_oss 方法(保持不变)
```python
def upload_backup_to_oss(
self,
backup_dir: str = None,
file_pattern: str = "*",
oss_prefix: str = None
) -> dict
```
## 使用说明
### 前置条件
#### 1. 安装依赖
```bash
# requests 库(通常已安装)
pip install requests
```
#### 2. 配置环境变量
根据您使用的终端类型配置环境变量:
##### PowerShell推荐
```powershell
# 必需:设置认证 TokenAPI Key 格式)
$env:UNI_LAB_AUTH_TOKEN = "Api xxxx"
# 可选:自定义服务器地址(默认为 test 环境)
$env:UNI_LAB_BASE_URL = "https://uni-lab.test.bohrium.com"
# 可选:自定义上传场景(默认为 job
$env:UNI_LAB_UPLOAD_SCENE = "job"
# 验证是否设置成功
echo $env:UNI_LAB_AUTH_TOKEN
```
##### CMD / 命令提示符
```cmd
REM 必需:设置认证 TokenAPI Key 格式)
set UNI_LAB_AUTH_TOKEN=Api xxxx
REM 可选:自定义配置
set UNI_LAB_BASE_URL=https://uni-lab.test.bohrium.com
set UNI_LAB_UPLOAD_SCENE=job
REM 验证是否设置成功
echo %UNI_LAB_AUTH_TOKEN%
```
##### Linux/Mac
```bash
# 必需:设置认证 TokenAPI Key 格式)
export UNI_LAB_AUTH_TOKEN="Api xxxx"
# 可选:自定义配置
export UNI_LAB_BASE_URL="https://uni-lab.test.bohrium.com"
export UNI_LAB_UPLOAD_SCENE="job"
# 验证是否设置成功
echo $UNI_LAB_AUTH_TOKEN
```
#### 3. 获取认证 Token
> **重要**:从 Uni-Lab 主页 → 账号安全 中获取 API Key。
**获取步骤**
1. 登录 Uni-Lab 系统
2. 进入主页 → 账号安全
3. 复制 API Key
Token 格式示例:
```
Api 48ccxx336fba44f39e1e37db93xxxxx
```
> **提示**
> - 如果 Token 已经包含 `Api ` 前缀,直接使用
> - 如果没有前缀,代码会自动添加 `Api ` 前缀
> - 旧版 `Bearer` JWT Token 格式仍然兼容
#### 4. 持久化配置(可选)
**临时配置**:上述命令设置的环境变量只在当前终端会话中有效。
**持久化方式 1PowerShell 配置文件**
```powershell
# 编辑 PowerShell 配置文件
notepad $PROFILE
# 在打开的文件中添加:
$env:UNI_LAB_AUTH_TOKEN = "Api 你的API_Key"
```
**持久化方式 2Windows 系统环境变量**
- 右键"此电脑" → "属性" → "高级系统设置" → "环境变量"
- 添加用户变量或系统变量:
- 变量名:`UNI_LAB_AUTH_TOKEN`
- 变量值:`Api 你的API_Key`
### 使用流程
#### 步骤 1启用 OSS 上传功能
**推荐方式:在 `device.json` 中配置**
编辑设备配置文件 `unilabos/devices/neware_battery_test_system/device.json`,在 `config` 中添加:
```json
{
"nodes": [
{
"id": "NEWARE_BATTERY_TEST_SYSTEM",
"config": {
"ip": "127.0.0.1",
"port": 502,
"machine_id": 1,
"oss_upload_enabled": true,
"oss_prefix": "neware_backup/2025-12"
}
}
]
}
```
**参数说明**
- `oss_upload_enabled`: 设置为 `true` 启用 OSS 上传
- `oss_prefix`: OSS 文件路径前缀,建议按日期或项目组织(暂时未使用,保留接口兼容性)
**其他方式:通过初始化参数**
```python
device = NewareBatteryTestSystem(
ip="127.0.0.1",
port=502,
oss_upload_enabled=True, # 启用 OSS 上传
oss_prefix="neware_backup/2025-12" # 可选:自定义路径前缀
)
```
**配置完成后,重启 ROS 节点使配置生效。**
#### 步骤 2提交测试任务
使用 `submit_from_csv` 提交测试任务:
```python
result = device.submit_from_csv(
csv_path="test_data.csv",
output_dir="D:/neware_output"
)
```
此时会创建以下目录结构:
```
D:/neware_output/
├── xml_dir/ # XML 配置文件
└── backup_dir/ # 测试数据备份(由新威设备生成)
```
#### 步骤 3等待测试完成
等待新威设备完成测试,备份文件会生成到 `backup_dir` 中。
#### 步骤 4上传备份文件到 OSS
**方法 A使用默认设置推荐**
```python
# 自动使用最近一次的 backup_dir上传所有文件
result = device.upload_backup_to_oss()
```
**方法 B指定备份目录**
```python
# 手动指定备份目录
result = device.upload_backup_to_oss(
backup_dir="D:/neware_output/backup_dir"
)
```
**方法 C筛选特定文件**
```python
# 仅上传 CSV 文件
result = device.upload_backup_to_oss(
backup_dir="D:/neware_output/backup_dir",
file_pattern="*.csv"
)
# 仅上传特定电池编号的文件
result = device.upload_backup_to_oss(
file_pattern="Battery_A001_*.nda"
)
```
### 返回结果示例
**成功上传所有文件**
```python
{
"return_info": "全部上传成功: 15/15 个文件",
"success": True,
"uploaded_count": 15,
"total_count": 15,
"failed_files": [],
"uploaded_files": [
{
"filename": "Battery_A001.ndax",
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A001.ndax"
},
{
"filename": "Battery_A002.ndax",
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A002.ndax"
}
# ... 其他 13 个文件
]
}
```
**部分上传成功**
```python
{
"return_info": "部分上传成功: 12/15 个文件,失败 3 个",
"success": True,
"uploaded_count": 12,
"total_count": 15,
"failed_files": ["Battery_A003.csv", "Battery_A007.csv", "test.log"],
"uploaded_files": [
{
"filename": "Battery_A001.ndax",
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A001.ndax"
},
{
"filename": "Battery_A002.ndax",
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A002.ndax"
}
# ... 其他 10 个成功上传的文件
]
}
```
> **说明**`uploaded_files` 字段包含所有成功上传文件的详细信息:
> - `filename`: 文件名(不含路径)
> - `url`: 文件在 OSS 上的完整访问 URL
## 错误处理
### OSS 上传未启用
如果 `oss_upload_enabled=False`,调用 `upload_backup_to_oss` 会返回:
```python
{
"return_info": "OSS 上传未启用 (oss_upload_enabled=False),跳过上传。备份目录: ...",
"success": False,
"uploaded_count": 0,
"total_count": 0,
"failed_files": []
}
```
**解决方法**:设置 `device.oss_upload_enabled = True`
### 环境变量未配置
如果缺少 `UNI_LAB_AUTH_TOKEN`,会返回:
```python
{
"return_info": "OSS 环境变量配置错误: 请设置环境变量: UNI_LAB_AUTH_TOKEN",
"success": False,
...
}
```
**解决方法**:按照前置条件配置环境变量
### 备份目录不存在
如果指定的备份目录不存在,会返回:
```python
{
"return_info": "备份目录不存在: D:/neware_output/backup_dir",
"success": False,
...
}
```
**解决方法**:检查目录路径是否正确,或等待测试生成备份文件
### API 认证失败
如果 Token 无效或过期,会返回:
```python
{
"return_info": "获取凭证失败: 认证失败",
"success": False,
...
}
```
**解决方法**:检查 Token 是否正确,或联系开发团队获取新 Token
## 技术细节
### OSS 上传流程(新方式)
```mermaid
flowchart TD
A[开始上传] --> B[验证配置和环境变量]
B --> C[扫描备份目录]
C --> D[筛选符合 pattern 的文件]
D --> E[遍历每个文件]
E --> F[调用 API 获取预签名 URL]
F --> G{获取成功?}
G -->|是| H[使用预签名 URL 上传文件]
G -->|否| I[记录失败]
H --> J{上传成功?}
J -->|是| K[记录成功 + 文件 URL]
J -->|否| I
I --> L{还有文件?}
K --> L
L -->|是| E
L -->|否| M[返回统计结果]
```
### 上传 API 流程
1. **获取预签名 URL**
- 请求:`GET /api/v1/applications/token?scene={scene}&filename={filename}`
- 认证:`Authorization: Bearer {token}`
- 响应:`{code: 0, data: {url: "预签名URL", path: "文件路径"}}`
2. **上传文件**
- 请求:`PUT {预签名URL}`
- 内容:文件二进制数据
- 响应HTTP 200 表示成功
3. **生成访问 URL**
- 格式:`https://{OSS_PUBLIC_HOST}/{path}`
- 示例:`https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/20251217/battery_data.csv`
### 日志记录
所有上传操作都会通过 ROS 日志系统记录:
- `INFO` 级别:上传进度和成功信息
- `WARNING` 级别:空目录、未启用等警告
- `ERROR` 级别:上传失败、配置错误
## 注意事项
1. **上传时机**`backup_dir` 中的文件是在新威设备执行测试过程中实时生成的,请确保测试已完成再上传。
2. **文件命名**:上传到 OSS 的文件会保留原始文件名,路径由统一 API 分配。
3. **网络要求**:上传需要稳定的网络连接到阿里云 OSS 服务。
4. **Token 有效期**JWT Token 有过期时间,过期后需要重新获取。
5. **成本考虑**OSS 存储和流量会产生费用,请根据需要合理设置文件筛选规则。
6. **并发上传**:当前实现为串行上传,大量文件上传可能需要较长时间。
7. **文件大小限制**:请注意单个文件大小是否有上传限制(由统一 API 控制)。
## 兼容性
-**向后兼容**:默认 `oss_upload_enabled=False`,不影响现有系统
-**可选功能**:仅在需要时启用
-**独立操作**:上传失败不会影响测试任务的提交和执行
- ⚠️ **环境变量变更**:需要更新环境变量配置(从 OSS AK/SK 改为 JWT Token
## 迁移指南
如果您之前使用 `oss2` 库方式,请按以下步骤迁移:
### 1. 卸载旧依赖(可选)
```bash
pip uninstall oss2
```
### 2. 删除旧环境变量
```powershell
# PowerShell
Remove-Item Env:\OSS_ACCESS_KEY_ID
Remove-Item Env:\OSS_ACCESS_KEY_SECRET
Remove-Item Env:\OSS_BUCKET_NAME
Remove-Item Env:\OSS_ENDPOINT
```
### 3. 设置新环境变量
```powershell
# PowerShell
$env:UNI_LAB_AUTH_TOKEN = "Bearer 你的token..."
```
### 4. 测试上传功能
```python
# 验证上传是否正常工作
result = device.upload_backup_to_oss(backup_dir="测试目录")
print(result)
```
## 常见问题
**Q: 为什么要从 `oss2` 改为统一 API**
A: 为了与团队其他系统保持一致,简化配置,并统一认证方式。
**Q: Token 在哪里获取?**
A: 请联系开发团队获取有效的 JWT Token。
**Q: Token 过期了怎么办?**
A: 重新获取新的 Token 并更新环境变量 `UNI_LAB_AUTH_TOKEN`
**Q: 可以自定义上传路径吗?**
A: 当前版本路径由统一 API 自动分配,`oss_prefix` 参数暂不使用(保留接口兼容性)。
**Q: 为什么不在 `submit_from_csv` 中自动上传?**
A: 因为备份文件在测试进行中逐步生成,方法返回时可能文件尚未完全生成,因此提供独立的上传方法更灵活。
**Q: 上传后如何访问文件?**
A: 上传成功后会返回文件访问 URL格式为 `https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/{path}`
**Q: 如何删除已上传的文件?**
A: 需要通过 OSS 控制台或 API 操作,本功能仅负责上传。
## 验证上传结果
### 方法1通过阿里云控制台查看
1. 登录 [阿里云 OSS 控制台](https://oss.console.aliyun.com/)
2. 点击左侧 **Bucket列表**
3. 选择 `uni-lab-test` Bucket
4. 点击 **文件管理**
5. 查看上传的文件列表
### 方法2使用返回的文件 URL
上传成功后,`upload_file_to_oss()` 会返回文件访问 URL
```python
url = upload_file_to_oss("local_file.csv")
print(f"文件访问 URL: {url}")
# 输出示例https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/20251217/local_file.csv
```
> **注意**OSS 文件默认为私有访问,直接访问 URL 可能需要签名认证。
### 方法3使用 ossutil 命令行工具
安装 [ossutil](https://help.aliyun.com/document_detail/120075.html) 后:
```bash
# 列出文件
ossutil ls oss://uni-lab-test/job/
# 下载文件到本地
ossutil cp oss://uni-lab-test/job/20251217/文件名 ./本地路径
# 生成签名URL有效期1小时
ossutil sign oss://uni-lab-test/job/20251217/文件名 --timeout 3600
```
## 更新日志
- **2025-12-17**: v2.0(重大更新)
- ⚠️ 从 `oss2` 库改为统一 API 方式
- 简化环境变量配置(仅需 JWT Token
- 新增 `get_upload_token()``upload_file_with_presigned_url()` 函数
- `upload_file_to_oss()` 返回值改为文件访问 URL
- 更新文档和迁移指南
- **2025-12-15**: v1.1
- 添加初始化参数 `oss_upload_enabled``oss_prefix`
- 支持在 `device.json` 中配置 OSS 上传
- 更新使用说明,添加验证方法
- **2025-12-13**: v1.0 初始版本
- 添加 OSS 上传工具函数(基于 `oss2` 库)
- 创建 `upload_backup_to_oss` 动作方法
- 支持文件筛选和自定义 OSS 路径
## 参考资料
- [Uni-Lab 统一文件上传 API 文档](https://uni-lab.test.bohrium.com/api/docs)(如有)
- [阿里云 OSS 控制台](https://oss.console.aliyun.com/)
- [ossutil 工具文档](https://help.aliyun.com/document_detail/120075.html)

View File

@@ -0,0 +1,574 @@
# Neware Battery Test System - OSS Upload Feature
## Overview
This update adds **Aliyun OSS file upload functionality** to the Neware Battery Test System using a unified API approach, allowing test data backup files to be uploaded to cloud storage.
## Version Updates
### ⚠️ Breaking Changes (2025-12-17)
This update changes the OSS upload method from **`oss2` library** to **unified API approach** to align with other team systems.
**Main Changes**:
- ✅ Use `requests` library
- ✅ Upload via presigned URLs obtained through unified API
- ✅ Simplified environment variable configuration (only API Key required)
- ✅ Returns file access URLs
## Main Changes
### 1. OSS Upload Functions Refactored (Lines 30-200)
#### New Functions
- **`get_upload_token(base_url, auth_token, scene, filename)`**
Obtain presigned URL for file upload from unified API
- **`upload_file_with_presigned_url(upload_info, file_path)`**
Upload file to OSS using presigned URL
#### Updated Functions
- **`upload_file_to_oss(local_file_path, oss_object_name)`**
Upload single file to Aliyun OSS (using unified API approach)
- Return value changed: returns file access URL on success, `False` on failure
- **`upload_files_to_oss(file_paths, oss_prefix)`**
Batch upload file list
- `oss_prefix` parameter retained but not used (interface compatibility)
- **`upload_directory_to_oss(local_dir, oss_prefix)`**
Upload entire directory
- Simplified implementation, uploads using filenames directly
### 2. Simplified Environment Variable Configuration
#### Old Method (Deprecated)
```bash
# ❌ No longer used
OSS_ACCESS_KEY_ID
OSS_ACCESS_KEY_SECRET
OSS_BUCKET_NAME
OSS_ENDPOINT
```
#### New Method (Recommended)
```bash
# ✅ Required
UNI_LAB_AUTH_TOKEN # API Key format: "Api xxxxxx"
# ✅ Optional (with defaults)
UNI_LAB_BASE_URL (default: https://uni-lab.test.bohrium.com)
UNI_LAB_UPLOAD_SCENE (default: job, other values will be changed to default)
```
### 3. Initialization Method (Unchanged)
OSS-related configuration parameters in `__init__` method:
```python
# OSS upload configuration
self.oss_upload_enabled = False # OSS upload disabled by default
self.oss_prefix = "neware_backup" # OSS object path prefix
self._last_backup_dir = None # Record last backup_dir
```
**Default Behavior**: OSS upload is disabled by default (`oss_upload_enabled=False`), does not affect existing systems.
### 4. upload_backup_to_oss Method (Unchanged)
```python
def upload_backup_to_oss(
self,
backup_dir: str = None,
file_pattern: str = "*",
oss_prefix: str = None
) -> dict
```
## Usage Guide
### Prerequisites
#### 1. Install Dependencies
```bash
# requests library (usually pre-installed)
pip install requests
```
> **Note**: No longer need to install `oss2` library
#### 2. Configure Environment Variables
Configure environment variables based on your terminal type:
##### PowerShell (Recommended)
```powershell
# Required: Set authentication Token (API Key format)
$env:UNI_LAB_AUTH_TOKEN = "Api xxxx"
# Optional: Custom server URL (defaults to test environment)
$env:UNI_LAB_BASE_URL = "https://uni-lab.test.bohrium.com"
# Optional: Custom upload scene (defaults to job)
$env:UNI_LAB_UPLOAD_SCENE = "job"
# Verify if set successfully
echo $env:UNI_LAB_AUTH_TOKEN
```
##### CMD / Command Prompt
```cmd
REM Required: Set authentication Token (API Key format)
set UNI_LAB_AUTH_TOKEN=Api xxxx
REM Optional: Custom configuration
set UNI_LAB_BASE_URL=https://uni-lab.test.bohrium.com
set UNI_LAB_UPLOAD_SCENE=job
REM Verify if set successfully
echo %UNI_LAB_AUTH_TOKEN%
```
##### Linux/Mac
```bash
# Required: Set authentication Token (API Key format)
export UNI_LAB_AUTH_TOKEN="Api xxxx"
# Optional: Custom configuration
export UNI_LAB_BASE_URL="https://uni-lab.test.bohrium.com"
export UNI_LAB_UPLOAD_SCENE="job"
# Verify if set successfully
echo $UNI_LAB_AUTH_TOKEN
```
#### 3. Obtain Authentication Token
> **Important**: Obtain API Key from Uni-Lab Homepage → Account Security.
**Steps to Obtain**:
1. Login to Uni-Lab system
2. Go to Homepage → Account Security
3. Copy your API Key
Token format example:
```
Api 48ccxx336fba44f39e1e37db93xxxxx
```
> **Tips**:
> - If Token already includes `Api ` prefix, use directly
> - If no prefix, code will automatically add `Api ` prefix
> - Old `Bearer` JWT Token format is still compatible
#### 4. Persistent Configuration (Optional)
**Temporary Configuration**: Environment variables set with the above commands are only valid for the current terminal session.
**Persistence Method 1: PowerShell Profile**
```powershell
# Edit PowerShell profile
notepad $PROFILE
# Add to the opened file:
$env:UNI_LAB_AUTH_TOKEN = "Api your_API_Key"
```
**Persistence Method 2: Windows System Environment Variables**
- Right-click "This PC" → "Properties" → "Advanced system settings" → "Environment Variables"
- Add user or system variable:
- Variable name: `UNI_LAB_AUTH_TOKEN`
- Variable value: `Api your_API_Key`
### Usage Workflow
#### Step 1: Enable OSS Upload Feature
**Recommended: Configure in `device.json`**
Edit device configuration file `unilabos/devices/neware_battery_test_system/device.json`, add to `config`:
```json
{
"nodes": [
{
"id": "NEWARE_BATTERY_TEST_SYSTEM",
"config": {
"ip": "127.0.0.1",
"port": 502,
"machine_id": 1,
"oss_upload_enabled": true,
"oss_prefix": "neware_backup/2025-12"
}
}
]
}
```
**Parameter Description**:
- `oss_upload_enabled`: Set to `true` to enable OSS upload
- `oss_prefix`: OSS file path prefix, recommended to organize by date or project (currently unused, retained for interface compatibility)
**Alternative: Via Initialization Parameters**
```python
device = NewareBatteryTestSystem(
ip="127.0.0.1",
port=502,
oss_upload_enabled=True, # Enable OSS upload
oss_prefix="neware_backup/2025-12" # Optional: custom path prefix
)
```
**After configuration, restart the ROS node for changes to take effect.**
#### Step 2: Submit Test Tasks
Use `submit_from_csv` to submit test tasks:
```python
result = device.submit_from_csv(
csv_path="test_data.csv",
output_dir="D:/neware_output"
)
```
This creates the following directory structure:
```
D:/neware_output/
├── xml_dir/ # XML configuration files
└── backup_dir/ # Test data backup (generated by Neware device)
```
#### Step 3: Wait for Test Completion
Wait for the Neware device to complete testing. Backup files will be generated in the `backup_dir`.
#### Step 4: Upload Backup Files to OSS
**Method A: Use Default Settings (Recommended)**
```python
# Automatically uses the last backup_dir, uploads all files
result = device.upload_backup_to_oss()
```
**Method B: Specify Backup Directory**
```python
# Manually specify backup directory
result = device.upload_backup_to_oss(
backup_dir="D:/neware_output/backup_dir"
)
```
**Method C: Filter Specific Files**
```python
# Upload only CSV files
result = device.upload_backup_to_oss(
backup_dir="D:/neware_output/backup_dir",
file_pattern="*.csv"
)
# Upload files for specific battery IDs
result = device.upload_backup_to_oss(
file_pattern="Battery_A001_*.nda"
)
```
### Return Result Examples
**All Files Uploaded Successfully**:
```python
{
"return_info": "All uploads successful: 15/15 files",
"success": True,
"uploaded_count": 15,
"total_count": 15,
"failed_files": [],
"uploaded_files": [
{
"filename": "Battery_A001.ndax",
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A001.ndax"
},
{
"filename": "Battery_A002.ndax",
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A002.ndax"
}
# ... other 13 files
]
}
```
**Partial Upload Success**:
```python
{
"return_info": "Partial upload success: 12/15 files, 3 failed",
"success": True,
"uploaded_count": 12,
"total_count": 15,
"failed_files": ["Battery_A003.csv", "Battery_A007.csv", "test.log"],
"uploaded_files": [
{
"filename": "Battery_A001.ndax",
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A001.ndax"
},
{
"filename": "Battery_A002.ndax",
"url": "https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/abc123.../Battery_A002.ndax"
}
# ... other 10 successfully uploaded files
]
}
```
> **Note**: The `uploaded_files` field contains detailed information for all successfully uploaded files:
> - `filename`: Filename (without path)
> - `url`: Complete OSS access URL for the file
## Error Handling
### OSS Upload Not Enabled
If `oss_upload_enabled=False`, calling `upload_backup_to_oss` returns:
```python
{
"return_info": "OSS upload not enabled (oss_upload_enabled=False), skipping upload. Backup directory: ...",
"success": False,
"uploaded_count": 0,
"total_count": 0,
"failed_files": []
}
```
**Solution**: Set `device.oss_upload_enabled = True`
### Environment Variables Not Configured
If `UNI_LAB_AUTH_TOKEN` is missing, returns:
```python
{
"return_info": "OSS environment variable configuration error: Please set environment variable: UNI_LAB_AUTH_TOKEN",
"success": False,
...
}
```
**Solution**: Configure environment variables as per prerequisites
### Backup Directory Does Not Exist
If specified backup directory doesn't exist, returns:
```python
{
"return_info": "Backup directory does not exist: D:/neware_output/backup_dir",
"success": False,
...
}
```
**Solution**: Check if directory path is correct, or wait for test to generate backup files
### API Authentication Failed
If Token is invalid or expired, returns:
```python
{
"return_info": "Failed to get credentials: Authentication failed",
"success": False,
...
}
```
**Solution**: Check if Token is correct, or contact development team for new Token
## Technical Details
### OSS Upload Process (New Method)
```mermaid
flowchart TD
A[Start Upload] --> B[Verify Configuration and Environment Variables]
B --> C[Scan Backup Directory]
C --> D[Filter Files Matching Pattern]
D --> E[Iterate Each File]
E --> F[Call API to Get Presigned URL]
F --> G{Success?}
G -->|Yes| H[Upload File Using Presigned URL]
G -->|No| I[Record Failure]
H --> J{Upload Success?}
J -->|Yes| K[Record Success + File URL]
J -->|No| I
I --> L{More Files?}
K --> L
L -->|Yes| E
L -->|No| M[Return Statistics]
```
### Upload API Flow
1. **Get Presigned URL**
- Request: `GET /api/v1/lab/storage/token?scene={scene}&filename={filename}&path={path}`
- Authentication: `Authorization: Api {api_key}` or `Authorization: Bearer {token}`
- Response: `{code: 0, data: {url: "presigned_url", path: "file_path"}}`
2. **Upload File**
- Request: `PUT {presigned_url}`
- Content: File binary data
- Response: HTTP 200 indicates success
3. **Generate Access URL**
- Format: `https://{OSS_PUBLIC_HOST}/{path}`
- Example: `https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/20251217/battery_data.csv`
### Logging
All upload operations are logged through ROS logging system:
- `INFO` level: Upload progress and success information
- `WARNING` level: Empty directory, not enabled warnings
- `ERROR` level: Upload failures, configuration errors
## Important Notes
1. **Upload Timing**: Files in `backup_dir` are generated in real-time during test execution. Ensure testing is complete before uploading.
2. **File Naming**: Files uploaded to OSS retain original filenames. Paths are assigned by unified API.
3. **Network Requirements**: Upload requires stable network connection to Aliyun OSS service.
4. **Token Expiration**: JWT Tokens have expiration time. Need to obtain new token after expiration.
5. **Cost Considerations**: OSS storage and traffic incur costs. Set file filtering rules appropriately.
6. **Concurrent Upload**: Current implementation uses serial upload. Large number of files may take considerable time.
7. **File Size Limits**: Note single file size upload limits (controlled by unified API).
## Compatibility
-**Backward Compatible**: Default `oss_upload_enabled=False`, does not affect existing systems
-**Optional Feature**: Enable only when needed
-**Independent Operation**: Upload failures do not affect test task submission and execution
- ⚠️ **Environment Variable Changes**: Need to update environment variable configuration (from OSS AK/SK to API Key)
## Migration Guide
If you previously used the `oss2` library method, follow these steps to migrate:
### 1. Uninstall Old Dependencies (Optional)
```bash
pip uninstall oss2
```
### 2. Remove Old Environment Variables
```powershell
# PowerShell
Remove-Item Env:\OSS_ACCESS_KEY_ID
Remove-Item Env:\OSS_ACCESS_KEY_SECRET
Remove-Item Env:\OSS_BUCKET_NAME
Remove-Item Env:\OSS_ENDPOINT
```
### 3. Set New Environment Variables
```powershell
# PowerShell
$env:UNI_LAB_AUTH_TOKEN = "Api your_API_Key"
```
### 4. Test Upload Functionality
```python
# Verify upload works correctly
result = device.upload_backup_to_oss(backup_dir="test_directory")
print(result)
```
## FAQ
**Q: Why change from `oss2` to unified API?**
A: To maintain consistency with other team systems, simplify configuration, and unify authentication methods.
**Q: Where to get the Token?**
A: Obtain API Key from Uni-Lab Homepage → Account Security.
**Q: What if Token expires?**
A: Obtain a new API Key and update the `UNI_LAB_AUTH_TOKEN` environment variable.
**Q: Can I customize upload paths?**
A: Current version has paths automatically assigned by unified API. `oss_prefix` parameter is currently unused (retained for interface compatibility).
**Q: Why not auto-upload in `submit_from_csv`?**
A: Because backup files are generated progressively during testing, they may not be fully generated when the method returns. A separate upload method provides more flexibility.
**Q: How to access files after upload?**
A: Upload success returns file access URL in format `https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/{path}`
**Q: How to delete uploaded files?**
A: Need to operate through OSS console or API. This feature only handles uploads.
## Verifying Upload Results
### Method 1: Via Aliyun Console
1. Login to [Aliyun OSS Console](https://oss.console.aliyun.com/)
2. Click **Bucket List** on the left
3. Select the `uni-lab-test` Bucket
4. Click **File Management**
5. View uploaded file list
### Method 2: Using Returned File URL
After successful upload, `upload_file_to_oss()` returns file access URL:
```python
url = upload_file_to_oss("local_file.csv")
print(f"File access URL: {url}")
# Example output: https://uni-lab-test.oss-cn-zhangjiakou.aliyuncs.com/job/20251217/local_file.csv
```
> **Note**: OSS files are private by default, direct URL access may require signature authentication.
### Method 3: Using ossutil CLI Tool
After installing [ossutil](https://help.aliyun.com/document_detail/120075.html):
```bash
# List files
ossutil ls oss://uni-lab-test/job/
# Download file to local
ossutil cp oss://uni-lab-test/job/20251217/filename ./local_path
# Generate signed URL (valid for 1 hour)
ossutil sign oss://uni-lab-test/job/20251217/filename --timeout 3600
```
## Changelog
- **2025-12-17**: v2.0 (Major Update)
- ⚠️ Changed from `oss2` library to unified API approach
- Simplified environment variable configuration (only API Key required)
- Added `get_upload_token()` and `upload_file_with_presigned_url()` functions
- `upload_file_to_oss()` return value changed to file access URL
- Updated documentation and migration guide
- Token format: Support both `Api Key` and `Bearer JWT`
- API endpoint: `/api/v1/lab/storage/token`
- Scene parameter: Fixed to `job` (other values changed to `default`)
- **2025-12-15**: v1.1
- Added initialization parameters `oss_upload_enabled` and `oss_prefix`
- Support OSS upload configuration in `device.json`
- Updated usage guide, added verification methods
- **2025-12-13**: v1.0 Initial Version
- Added OSS upload utility functions (based on `oss2` library)
- Created `upload_backup_to_oss` action method
- Support file filtering and custom OSS paths
## References
- [Uni-Lab Unified File Upload API Documentation](https://uni-lab.test.bohrium.com/api/docs) (if available)
- [Aliyun OSS Console](https://oss.console.aliyun.com/)
- [ossutil Tool Documentation](https://help.aliyun.com/document_detail/120075.html)

View File

@@ -0,0 +1,35 @@
{
"nodes": [
{
"id": "NEWARE_BATTERY_TEST_SYSTEM",
"name": "Neware Battery Test System",
"parent": null,
"type": "device",
"class": "neware_battery_test_system",
"position": {
"x": 620.0,
"y": 200.0,
"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,
"oss_upload_enabled": true,
"oss_prefix": "neware_backup/2025-12"
},
"data": {
"功能说明": "新威电池测试系统提供720通道监控和CSV批量提交功能",
"监控功能": "支持720个通道的实时状态监控、2盘电池物料管理、状态导出等",
"提交功能": "通过submit_from_csv action从CSV文件批量提交测试任务。CSV必须包含: Battery_Code, Pole_Weight, 集流体质量, 活性物质含量, 克容量mah/g, 电池体系, 设备号, 排号, 通道号"
},
"children": []
}
],
"links": []
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,282 +1,649 @@
import sys
import threading
import serial
import serial.tools.list_ports
import re
import time
from typing import Optional, List, Dict, Tuple
# -*- coding: utf-8 -*-
"""
Contains drivers for:
1. SyringePump: Runze Fluid SY-03B (ASCII)
2. EmmMotor: Emm V5.0 Closed-loop Stepper (Modbus-RTU variant)
3. XKCSensor: XKC Non-contact Level Sensor (Modbus-RTU)
"""
class ChinweDevice:
import socket
import serial
import time
import threading
import struct
import re
import traceback
import queue
from typing import Optional, Dict, List, Any
try:
from unilabos.device_comms.universal_driver import UniversalDriver
except ImportError:
import logging
class UniversalDriver:
def __init__(self):
self.logger = logging.getLogger(self.__class__.__name__)
def execute_command_from_outer(self, command: str):
pass
# ==============================================================================
# 1. Transport Layer (通信层)
# ==============================================================================
class TransportManager:
"""
ChinWe设备控制类
提供串口通信、电机控制、传感器数据读取等功能
统一通信管理类。
自动识别 串口 (Serial) 或 网络 (TCP) 连接。
"""
def __init__(self, port: str, baudrate: int = 115200, debug: bool = False):
"""
初始化ChinWe设备
Args:
port: 串口名称如果为None则自动检测
baudrate: 波特率默认115200
"""
self.debug = debug
def __init__(self, port: str, baudrate: int = 9600, timeout: float = 3.0, logger=None):
self.port = port
self.baudrate = baudrate
self.serial_port: Optional[serial.Serial] = None
self._voltage: float = 0.0
self._ec_value: float = 0.0
self._ec_adc_value: int = 0
self.timeout = timeout
self.logger = logger
self.lock = threading.RLock() # 线程锁,确保多设备共用一个连接时不冲突
self.is_tcp = False
self.serial = None
self.socket = None
# 简单判断: 如果包含 ':' (如 192.168.1.1:8899) 或者看起来像 IP则认为是 TCP
if ':' in self.port or (self.port.count('.') == 3 and not self.port.startswith('/')):
self.is_tcp = True
self._connect_tcp()
else:
self._connect_serial()
def _log(self, msg):
if self.logger:
pass
# self.logger.debug(f"[Transport] {msg}")
def _connect_tcp(self):
try:
if ':' in self.port:
host, p = self.port.split(':')
self.tcp_host = host
self.tcp_port = int(p)
else:
self.tcp_host = self.port
self.tcp_port = 8899 # 默认端口
# if self.logger: self.logger.info(f"Connecting TCP {self.tcp_host}:{self.tcp_port} ...")
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(self.timeout)
self.socket.connect((self.tcp_host, self.tcp_port))
except Exception as e:
raise ConnectionError(f"TCP connection failed: {e}")
def _connect_serial(self):
try:
# if self.logger: self.logger.info(f"Opening Serial {self.port} (Baud: {self.baudrate}) ...")
self.serial = serial.Serial(
port=self.port,
baudrate=self.baudrate,
timeout=self.timeout
)
except Exception as e:
raise ConnectionError(f"Serial open failed: {e}")
def close(self):
"""关闭连接"""
if self.is_tcp and self.socket:
try: self.socket.close()
except: pass
elif not self.is_tcp and self.serial and self.serial.is_open:
self.serial.close()
def clear_buffer(self):
"""清空缓冲区 (Thread-safe)"""
with self.lock:
if self.is_tcp:
self.socket.setblocking(False)
try:
while True:
if not self.socket.recv(1024): break
except: pass
finally: self.socket.settimeout(self.timeout)
else:
self.serial.reset_input_buffer()
def write(self, data: bytes):
"""发送原始字节"""
with self.lock:
if self.is_tcp:
self.socket.sendall(data)
else:
self.serial.write(data)
def read(self, size: int) -> bytes:
"""读取指定长度字节"""
if self.is_tcp:
data = b''
start = time.time()
while len(data) < size:
if time.time() - start > self.timeout: break
try:
chunk = self.socket.recv(size - len(data))
if not chunk: break
data += chunk
except socket.timeout: break
return data
else:
return self.serial.read(size)
def send_ascii_command(self, command: str) -> str:
"""
发送 ASCII 字符串命令 (如注射泵指令),读取直到 '\r'
"""
with self.lock:
data = command.encode('ascii') if isinstance(command, str) else command
self.clear_buffer()
self.write(data)
# Read until \r
if self.is_tcp:
resp = b''
start = time.time()
while True:
if time.time() - start > self.timeout: break
try:
char = self.socket.recv(1)
if not char: break
resp += char
if char == b'\r': break
except: break
return resp.decode('ascii', errors='ignore').strip()
else:
return self.serial.read_until(b'\r').decode('ascii', errors='ignore').strip()
# ==============================================================================
# 2. Syringe Pump Driver (注射泵)
# ==============================================================================
class SyringePump:
"""SY-03B 注射泵驱动 (ASCII协议)"""
CMD_INITIALIZE = "Z{speed},{drain_port},{output_port}R"
CMD_SWITCH_VALVE = "I{port}R"
CMD_ASPIRATE = "P{vol}R"
CMD_DISPENSE = "D{vol}R"
CMD_DISPENSE_ALL = "A0R"
CMD_STOP = "TR"
CMD_QUERY_STATUS = "Q"
CMD_QUERY_PLUNGER = "?0"
def __init__(self, device_id: int, transport: TransportManager):
if not 1 <= device_id <= 15:
pass # Allow all IDs for now
self.id = str(device_id)
self.transport = transport
def _send(self, template: str, **kwargs) -> str:
cmd = f"/{self.id}" + template.format(**kwargs) + "\r"
return self.transport.send_ascii_command(cmd)
def is_busy(self) -> bool:
"""查询繁忙状态"""
resp = self._send(self.CMD_QUERY_STATUS)
# 响应如 /0` (Ready, 0x60) 或 /0@ (Busy, 0x40)
if len(resp) >= 3:
status_byte = ord(resp[2])
# Bit 5: 1=Ready, 0=Busy
return (status_byte & 0x20) == 0
return False
def wait_until_idle(self, timeout=30):
"""阻塞等待直到空闲"""
start = time.time()
while time.time() - start < timeout:
if not self.is_busy(): return
time.sleep(0.5)
# raise TimeoutError(f"Pump {self.id} wait idle timeout")
pass
def initialize(self, drain_port=0, output_port=0, speed=10):
"""初始化"""
self._send(self.CMD_INITIALIZE, speed=speed, drain_port=drain_port, output_port=output_port)
def switch_valve(self, port: int):
"""切换阀门 (1-8)"""
self._send(self.CMD_SWITCH_VALVE, port=port)
def aspirate(self, steps: int):
"""吸液 (相对步数)"""
self._send(self.CMD_ASPIRATE, vol=steps)
def dispense(self, steps: int):
"""排液 (相对步数)"""
self._send(self.CMD_DISPENSE, vol=steps)
def stop(self):
"""停止"""
self._send(self.CMD_STOP)
def get_position(self) -> int:
"""获取柱塞位置 (步数)"""
resp = self._send(self.CMD_QUERY_PLUNGER)
m = re.search(r'\d+', resp)
return int(m.group()) if m else -1
# ==============================================================================
# 3. Stepper Motor Driver (步进电机)
# ==============================================================================
class EmmMotor:
"""Emm V5.0 闭环步进电机驱动"""
def __init__(self, device_id: int, transport: TransportManager):
self.id = device_id
self.transport = transport
def _send(self, func_code: int, payload: list) -> bytes:
with self.transport.lock:
self.transport.clear_buffer()
# 格式: [ID] [Func] [Data...] [Check=0x6B]
body = [self.id, func_code] + payload
body.append(0x6B) # Checksum
self.transport.write(bytes(body))
# 根据指令不同,读取不同长度响应
read_len = 10 if func_code in [0x31, 0x32, 0x35, 0x24, 0x27] else 4
return self.transport.read(read_len)
def enable(self, on=True):
"""使能 (True=锁轴, False=松轴)"""
state = 1 if on else 0
self._send(0xF3, [0xAB, state, 0])
def run_speed(self, speed_rpm: int, direction=0, acc=10):
"""速度模式运行"""
sp = struct.pack('>H', int(speed_rpm))
self._send(0xF6, [direction, sp[0], sp[1], acc, 0])
def run_position(self, pulses: int, speed_rpm: int, direction=0, acc=10, absolute=False):
"""位置模式运行"""
sp = struct.pack('>H', int(speed_rpm))
pl = struct.pack('>I', int(pulses))
is_abs = 1 if absolute else 0
self._send(0xFD, [direction, sp[0], sp[1], acc, pl[0], pl[1], pl[2], pl[3], is_abs, 0])
def stop(self):
"""停止"""
self._send(0xFE, [0x98, 0])
def set_zero(self):
"""清零位置"""
self._send(0x0A, [])
def get_position(self) -> int:
"""获取当前脉冲位置"""
resp = self._send(0x32, [])
if len(resp) >= 8:
sign = resp[2]
val = struct.unpack('>I', resp[3:7])[0]
return -val if sign == 1 else val
return 0
# ==============================================================================
# 4. Liquid Sensor Driver (液位传感器)
# ==============================================================================
class XKCSensor:
"""XKC RS485 液位传感器 (Modbus RTU)"""
def __init__(self, device_id: int, transport: TransportManager, threshold: int = 300):
self.id = device_id
self.transport = transport
self.threshold = threshold
def _crc(self, data: bytes) -> bytes:
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 0x0001: crc = (crc >> 1) ^ 0xA001
else: crc >>= 1
return struct.pack('<H', crc)
def read_level(self) -> Optional[Dict[str, Any]]:
"""
读取液位。
返回: {'level': bool, 'rssi': int}
"""
with self.transport.lock:
self.transport.clear_buffer()
# Modbus Read Registers: 01 03 00 01 00 02 CRC
payload = struct.pack('>HH', 0x0001, 0x0002)
msg = struct.pack('BB', self.id, 0x03) + payload
msg += self._crc(msg)
self.transport.write(msg)
# Read header
h = self.transport.read(3) # Addr, Func, Len
if len(h) < 3: return None
length = h[2]
# Read body + CRC
body = self.transport.read(length + 2)
if len(body) < length + 2:
# Firmware bug fix specific to some modules
if len(body) == 4 and length == 4:
pass
else:
return None
data = body[:-2]
if len(data) == 2:
rssi = data[1]
elif len(data) >= 4:
rssi = (data[2] << 8) | data[3]
else:
return None
return {
'level': rssi > self.threshold,
'rssi': rssi
}
# ==============================================================================
# 5. Main Device Class (ChinweDevice)
# ==============================================================================
class ChinweDevice(UniversalDriver):
"""
ChinWe 工作站主驱动
继承自 UniversalDriver管理所有子设备泵、电机、传感器
"""
def __init__(self, port: str = "192.168.1.200:8899", baudrate: int = 9600,
pump_ids: List[int] = None, motor_ids: List[int] = None,
sensor_id: int = 6, sensor_threshold: int = 300,
timeout: float = 10.0):
"""
初始化 ChinWe 工作站
:param port: 串口号 或 IP:Port
:param baudrate: 串口波特率
:param pump_ids: 注射泵 ID列表 (默认 [1, 2, 3])
:param motor_ids: 步进电机 ID列表 (默认 [4, 5])
:param sensor_id: 液位传感器 ID (默认 6)
:param sensor_threshold: 传感器液位判定阈值
:param timeout: 通信超时时间 (默认 10秒)
"""
super().__init__()
self.port = port
self.baudrate = baudrate
self.timeout = timeout
self.mgr = None
self._is_connected = False
self.connect()
# 默认配置
if pump_ids is None: pump_ids = [1, 2, 3]
if motor_ids is None: motor_ids = [4, 5]
# 配置信息
self.pump_ids = pump_ids
self.motor_ids = motor_ids
self.sensor_id = sensor_id
self.sensor_threshold = sensor_threshold
# 子设备实例容器
self.pumps: Dict[int, SyringePump] = {}
self.motors: Dict[int, EmmMotor] = {}
self.sensor: Optional[XKCSensor] = None
# 轮询线程控制
self._stop_event = threading.Event()
self._poll_thread = None
# 实时状态缓存
self.status_cache = {
"sensor_rssi": 0,
"sensor_level": False,
"connected": False
}
# 自动连接
if self.port:
self.connect()
def connect(self) -> bool:
if self._is_connected: return True
try:
self.logger.info(f"Connecting to {self.port} (timeout={self.timeout})...")
self.mgr = TransportManager(self.port, baudrate=self.baudrate, timeout=self.timeout, logger=self.logger)
# 初始化所有泵
for pid in self.pump_ids:
self.pumps[pid] = SyringePump(pid, self.mgr)
# 初始化所有电机
for mid in self.motor_ids:
self.motors[mid] = EmmMotor(mid, self.mgr)
# 初始化传感器
self.sensor = XKCSensor(self.sensor_id, self.mgr, self.sensor_threshold)
self._is_connected = True
self.status_cache["connected"] = True
# 启动轮询线程
self._start_polling()
return True
except Exception as e:
self.logger.error(f"Connection failed: {e}")
self._is_connected = False
self.status_cache["connected"] = False
return False
def disconnect(self):
self._stop_event.set()
if self._poll_thread:
self._poll_thread.join(timeout=2.0)
if self.mgr:
self.mgr.close()
self._is_connected = False
self.status_cache["connected"] = False
self.logger.info("Disconnected.")
def _start_polling(self):
"""启动传感器轮询线程"""
if self._poll_thread and self._poll_thread.is_alive():
return
self._stop_event.clear()
self._poll_thread = threading.Thread(target=self._polling_loop, daemon=True, name="ChinwePoll")
self._poll_thread.start()
def _polling_loop(self):
"""轮询主循环"""
self.logger.info("Sensor polling started.")
error_count = 0
while not self._stop_event.is_set():
if not self._is_connected or not self.sensor:
time.sleep(1)
continue
try:
# 获取传感器数据
data = self.sensor.read_level()
if data:
self.status_cache["sensor_rssi"] = data['rssi']
self.status_cache["sensor_level"] = data['level']
error_count = 0
else:
error_count += 1
# 降低轮询频率防止总线拥塞
time.sleep(0.2)
except Exception as e:
error_count += 1
if error_count > 10: # 连续错误记录日志
# self.logger.error(f"Polling error: {e}")
error_count = 0
time.sleep(1)
# --- 对外暴露属性 (Properties) ---
@property
def sensor_level(self) -> bool:
return self.status_cache["sensor_level"]
@property
def sensor_rssi(self) -> int:
return self.status_cache["sensor_rssi"]
@property
def is_connected(self) -> bool:
"""获取连接状态"""
return self._is_connected and self.serial_port and self.serial_port.is_open
@property
def voltage(self) -> float:
"""获取电源电压值"""
return self._voltage
@property
def ec_value(self) -> float:
"""获取电导率值 (ms/cm)"""
return self._ec_value
return self._is_connected
@property
def ec_adc_value(self) -> int:
"""获取EC ADC原始值"""
return self._ec_adc_value
# --- 对外功能指令 (Actions) ---
@property
def device_status(self) -> Dict[str, any]:
"""
获取设备状态信息
Returns:
包含设备状态的字典
"""
return {
"connected": self.is_connected,
"port": self.port,
"baudrate": self.baudrate,
"voltage": self.voltage,
"ec_value": self.ec_value,
"ec_adc_value": self.ec_adc_value
}
def connect(self, port: Optional[str] = None, baudrate: Optional[int] = None) -> bool:
"""
连接到串口设备
Args:
port: 串口名称如果为None则使用初始化时的port或自动检测
baudrate: 波特率如果为None则使用初始化时的baudrate
Returns:
连接是否成功
"""
if self.is_connected:
def pump_initialize(self, pump_id: int, drain_port=0, output_port=0, speed=10):
"""指定泵初始化"""
pump_id = int(pump_id)
if pump_id in self.pumps:
self.pumps[pump_id].initialize(drain_port, output_port, speed)
self.pumps[pump_id].wait_until_idle()
return True
target_port = port or self.port
target_baudrate = baudrate or self.baudrate
try:
self.serial_port = serial.Serial(target_port, target_baudrate, timeout=0.5)
self._is_connected = True
self.port = target_port
self.baudrate = target_baudrate
connect_allow_times = 5
while not self.serial_port.is_open and connect_allow_times > 0:
time.sleep(0.5)
connect_allow_times -= 1
print(f"尝试连接到 {target_port} @ {target_baudrate},剩余尝试次数: {connect_allow_times}", self.debug)
raise ValueError("串口未打开,请检查设备连接")
print(f"已连接到 {target_port} @ {target_baudrate}", self.debug)
threading.Thread(target=self._read_data, daemon=True).start()
return False
def pump_aspirate(self, pump_id: int, volume: int, valve_port: int):
"""
泵吸液 (阻塞)
:param valve_port: 阀门端口 (1-8)
"""
pump_id = int(pump_id)
valve_port = int(valve_port)
if pump_id in self.pumps:
pump = self.pumps[pump_id]
# 1. 切换阀门
pump.switch_valve(valve_port)
pump.wait_until_idle()
# 2. 吸液
pump.aspirate(volume)
pump.wait_until_idle()
return True
except Exception as e:
print(f"ChinweDevice连接失败: {e}")
self._is_connected = False
return False
def disconnect(self) -> bool:
return False
def pump_dispense(self, pump_id: int, volume: int, valve_port: int):
"""
断开串口连接
Returns:
断开是否成功
泵排液 (阻塞)
:param valve_port: 阀门端口 (1-8)
"""
if self.serial_port and self.serial_port.is_open:
try:
self.serial_port.close()
self._is_connected = False
print("已断开串口连接")
return True
except Exception as e:
print(f"断开连接失败: {e}")
return False
pump_id = int(pump_id)
valve_port = int(valve_port)
if pump_id in self.pumps:
pump = self.pumps[pump_id]
# 1. 切换阀门
pump.switch_valve(valve_port)
pump.wait_until_idle()
# 2. 排液
pump.dispense(volume)
pump.wait_until_idle()
return True
return False
def pump_valve(self, pump_id: int, port: int):
"""泵切换阀门 (阻塞)"""
pump_id = int(pump_id)
port = int(port)
if pump_id in self.pumps:
pump = self.pumps[pump_id]
pump.switch_valve(port)
pump.wait_until_idle()
return True
return False
def motor_run_continuous(self, motor_id: int, speed: int, direction: str = "顺时针"):
"""
电机一直旋转 (速度模式)
:param direction: "顺时针" or "逆时针"
"""
motor_id = int(motor_id)
if motor_id not in self.motors: return False
dir_val = 0 if direction == "顺时针" else 1
self.motors[motor_id].run_speed(speed, dir_val)
return True
def _send_motor_command(self, command: str) -> bool:
def motor_rotate_quarter(self, motor_id: int, speed: int = 60, direction: str = "顺时针"):
"""
发送电机控制命令
Args:
command: 电机命令字符串,例如 "M 1 CW 1.5"
Returns:
发送是否成功
电机旋转1/4圈 (阻塞)
假设电机设置为 3200 脉冲/圈1/4圈 = 800脉冲
"""
if not self.is_connected:
print("设备未连接")
return False
try:
self.serial_port.write((command + "\n").encode('utf-8'))
print(f"发送命令: {command}")
motor_id = int(motor_id)
if motor_id not in self.motors: return False
pulses = 800
dir_val = 0 if direction == "顺时针" else 1
self.motors[motor_id].run_position(pulses, speed, dir_val, absolute=False)
# 预估时间阻塞 (单位: 分钟 -> 秒)
# Time(s) = revs / (RPM/60). revs = 0.25. time = 15 / RPM.
estimated_time = 15.0 / max(1, speed)
time.sleep(estimated_time + 0.5)
return True
def motor_stop(self, motor_id: int):
"""电机停止"""
motor_id = int(motor_id)
if motor_id in self.motors:
self.motors[motor_id].stop()
return True
except Exception as e:
print(f"发送命令失败: {e}")
return False
def rotate_motor(self, motor_id: int, turns: float, clockwise: bool = True) -> bool:
"""
使电机转动指定圈数
Args:
motor_id: 电机ID1, 2, 3...
turns: 转动圈数,支持小数
clockwise: True为顺时针False为逆时针
Returns:
命令发送是否成功
"""
if clockwise:
command = f"M {motor_id} CW {turns}"
else:
command = f"M {motor_id} CCW {turns}"
return self._send_motor_command(command)
return False
def set_motor_speed(self, motor_id: int, speed: float) -> bool:
def wait_sensor_level(self, target_state: str = "有液", timeout: int = 30) -> bool:
"""
设置电机转速(如果设备支持)
Args:
motor_id: 电机ID1, 2, 3...
speed: 转速值
Returns:
命令发送是否成功
等待传感器达到指定电平
:param target_state: "有液" or "无液"
"""
command = f"M {motor_id} SPEED {speed}"
return self._send_motor_command(command)
target_bool = True if target_state == "有液" else False
def _read_data(self) -> List[str]:
"""
读取串口数据并解析
Returns:
读取到的数据行列表
"""
print("开始读取串口数据...")
if not self.is_connected:
return []
data_lines = []
try:
while self.serial_port.in_waiting:
time.sleep(0.1) # 等待数据稳定
try:
line = self.serial_port.readline().decode('utf-8', errors='ignore').strip()
if line:
data_lines.append(line)
self._parse_sensor_data(line)
except Exception as ex:
print(f"解码数据错误: {ex}")
except Exception as e:
print(f"读取串口数据错误: {e}")
return data_lines
def _parse_sensor_data(self, line: str) -> None:
"""
解析传感器数据
Args:
line: 接收到的数据行
"""
# 解析电源电压
if "电源电压" in line:
try:
val = float(line.split("")[1].replace("V", "").strip())
self._voltage = val
if self.debug:
print(f"电源电压更新: {val}V")
except Exception:
pass
self.logger.info(f"Wait sensor: {target_state} ({target_bool}), timeout: {timeout}")
start = time.time()
while time.time() - start < timeout:
if self.sensor_level == target_bool:
return True
time.sleep(0.1)
self.logger.warning("Wait sensor level timeout")
return False
# 解析电导率和ADC原始值支持两种格式
if "电导率" in line and "ADC原始值" in line:
try:
# 支持格式如电导率2.50ms/cm, ADC原始值2052
ec_match = re.search(r"电导率[:]\s*([\d\.]+)", line)
adc_match = re.search(r"ADC原始值[:]\s*(\d+)", line)
if ec_match:
ec_val = float(ec_match.group(1))
self._ec_value = ec_val
if self.debug:
print(f"电导率更新: {ec_val:.2f} ms/cm")
if adc_match:
adc_val = int(adc_match.group(1))
self._ec_adc_value = adc_val
if self.debug:
print(f"EC ADC原始值更新: {adc_val}")
except Exception:
pass
# 仅电导率无ADC原始值
elif "电导率" in line:
try:
val = float(line.split("")[1].replace("ms/cm", "").strip())
self._ec_value = val
if self.debug:
print(f"电导率更新: {val:.2f} ms/cm")
except Exception:
pass
# 仅ADC原始值如有分开回传场景
elif "ADC原始值" in line:
try:
adc_val = int(line.split("")[1].strip())
self._ec_adc_value = adc_val
if self.debug:
print(f"EC ADC原始值更新: {adc_val}")
except Exception:
pass
def spin_when_ec_ge_0():
pass
def wait_time(self, duration: int) -> bool:
"""
等待指定时间 (秒)
:param duration: 秒
"""
self.logger.info(f"Waiting for {duration} seconds...")
time.sleep(duration)
return True
def execute_command_from_outer(self, command_dict: Dict[str, Any]) -> bool:
"""支持标准 JSON 指令调用"""
return super().execute_command_from_outer(command_dict)
def main():
"""测试函数"""
print("=== ChinWe设备测试 ===")
# 创建设备实例
device = ChinweDevice("/dev/tty.usbserial-A5069RR4", debug=True)
try:
# 测试5: 发送电机命令
print("\n5. 发送电机命令测试:")
print(" 5.3 使用通用函数控制电机20顺时针转2圈:")
device.rotate_motor(2, 20.0, clockwise=True)
time.sleep(0.5)
finally:
time.sleep(10)
# 测试7: 断开连接
print("\n7. 断开连接:")
device.disconnect()
if __name__ == "__main__":
main()
# Test
logging.basicConfig(level=logging.INFO)
dev = ChinweDevice(port="192.168.31.201:8899")
try:
if dev.is_connected:
print(f"Status: Level={dev.sensor_level}, RSSI={dev.sensor_rssi}")
# Test pump 1
# dev.pump_valve(1, 1)
# dev.pump_move(1, 1000, "aspirate")
# Test motor 4
# dev.motor_run(4, 60, 0, 2)
for _ in range(5):
print(f"Level={dev.sensor_level}, RSSI={dev.sensor_rssi}")
time.sleep(1)
finally:
dev.disconnect()

View File

@@ -0,0 +1,93 @@
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
from unilabos.resources.itemized_carrier import BottleCarrier
from unilabos.devices.workstation.post_process.bottles import POST_PROCESS_PolymerStation_Reagent_Bottle
# 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask,小瓶-Vial
# ============================================================================
# 聚合站PolymerStation载体定义统一入口
# ============================================================================
def POST_PROCESS_Raw_1BottleCarrier(name: str) -> BottleCarrier:
"""聚合站-单试剂瓶载架
参数:
- name: 载架名称前缀
"""
# 载架尺寸 (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="POST_PROCESS_Raw_1BottleCarrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
# 统一后缀采用 "flask_1" 命名(可按需调整)
carrier[0] = POST_PROCESS_PolymerStation_Reagent_Bottle(f"{name}_flask_1")
return carrier
def POST_PROCESS_Reaction_1BottleCarrier(name: str) -> BottleCarrier:
"""聚合站-单试剂瓶载架
参数:
- name: 载架名称前缀
"""
# 载架尺寸 (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="POST_PROCESS_Reaction_1BottleCarrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
# 统一后缀采用 "flask_1" 命名(可按需调整)
carrier[0] = POST_PROCESS_PolymerStation_Reagent_Bottle(f"{name}_flask_1")
return carrier

View File

@@ -0,0 +1,20 @@
from unilabos.resources.itemized_carrier import Bottle
def POST_PROCESS_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="POST_PROCESS_PolymerStation_Reagent_Bottle",
)

View File

@@ -0,0 +1,46 @@
from os import name
from pylabrobot.resources import Deck, Coordinate, Rotation
from unilabos.devices.workstation.post_process.warehouses import (
post_process_warehouse_4x3x1,
post_process_warehouse_4x3x1_2,
)
class post_process_deck(Deck):
def __init__(
self,
name: str = "post_process_deck",
size_x: float = 2000.0,
size_y: float = 1000.0,
size_z: float = 2670.0,
category: str = "deck",
setup: bool = True,
) -> None:
super().__init__(name=name, size_x=1700.0, size_y=1350.0, size_z=2670.0)
if setup:
self.setup()
def setup(self) -> None:
# 添加仓库
self.warehouses = {
"原料罐堆栈": post_process_warehouse_4x3x1("原料罐堆栈"),
"反应罐堆栈": post_process_warehouse_4x3x1_2("反应罐堆栈"),
}
# warehouse 的位置
self.warehouse_locations = {
"原料罐堆栈": Coordinate(350.0, 55.0, 0.0),
"反应罐堆栈": Coordinate(1000.0, 55.0, 0.0),
}
for warehouse_name, warehouse in self.warehouses.items():
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])

View File

@@ -0,0 +1,157 @@
{
"register_node_list_from_csv_path": {
"path": "opcua_nodes_huairou.csv"
},
"create_flow": [
{
"name": "trigger_grab_action",
"description": "触发反应罐及原料罐抓取动作",
"parameters": ["reaction_tank_number", "raw_tank_number"],
"action": [
{
"init_function": {
"func_name": "init_grab_params",
"write_nodes": ["reaction_tank_number", "raw_tank_number"]
},
"start_function": {
"func_name": "start_grab",
"write_nodes": {"grab_trigger": true},
"condition_nodes": ["grab_complete"],
"stop_condition_expression": "grab_complete == True",
"timeout_seconds": 999999.0
},
"stop_function": {
"func_name": "stop_grab",
"write_nodes": {"grab_trigger": false}
}
}
]
},
{
"name": "trigger_post_processing",
"description": "触发后处理动作",
"parameters": ["atomization_fast_speed", "wash_slow_speed","injection_pump_suction_speed",
"injection_pump_push_speed","raw_liquid_suction_count","first_wash_water_amount",
"second_wash_water_amount","first_powder_mixing_time","second_powder_mixing_time",
"first_powder_wash_count","second_powder_wash_count","initial_water_amount",
"pre_filtration_mixing_time","atomization_pressure_kpa"],
"action": [
{
"init_function": {
"func_name": "init_post_processing_params",
"write_nodes": ["atomization_fast_speed", "wash_slow_speed","injection_pump_suction_speed",
"injection_pump_push_speed","raw_liquid_suction_count","first_wash_water_amount",
"second_wash_water_amount","first_powder_mixing_time","second_powder_mixing_time",
"first_powder_wash_count","second_powder_wash_count","initial_water_amount",
"pre_filtration_mixing_time","atomization_pressure_kpa"]
},
"start_function": {
"func_name": "start_post_processing",
"write_nodes": {"post_process_trigger": true},
"condition_nodes": ["post_process_complete"],
"stop_condition_expression": "post_process_complete == True",
"timeout_seconds": 999999.0
},
"stop_function": {
"func_name": "stop_post_processing",
"write_nodes": {"post_process_trigger": false}
}
}
]
},
{
"name": "trigger_cleaning_action",
"description": "触发清洗及管路吹气动作",
"parameters": ["nmp_outer_wall_cleaning_injection", "nmp_outer_wall_cleaning_count","nmp_outer_wall_cleaning_wait_time",
"nmp_outer_wall_cleaning_waste_time","nmp_inner_wall_cleaning_injection","nmp_inner_wall_cleaning_count",
"nmp_pump_cleaning_suction_count",
"nmp_inner_wall_cleaning_waste_time",
"nmp_stirrer_cleaning_injection",
"nmp_stirrer_cleaning_count",
"nmp_stirrer_cleaning_wait_time",
"nmp_stirrer_cleaning_waste_time",
"water_outer_wall_cleaning_injection",
"water_outer_wall_cleaning_count",
"water_outer_wall_cleaning_wait_time",
"water_outer_wall_cleaning_waste_time",
"water_inner_wall_cleaning_injection",
"water_inner_wall_cleaning_count",
"water_pump_cleaning_suction_count",
"water_inner_wall_cleaning_waste_time",
"water_stirrer_cleaning_injection",
"water_stirrer_cleaning_count",
"water_stirrer_cleaning_wait_time",
"water_stirrer_cleaning_waste_time",
"acetone_outer_wall_cleaning_injection",
"acetone_outer_wall_cleaning_count",
"acetone_outer_wall_cleaning_wait_time",
"acetone_outer_wall_cleaning_waste_time",
"acetone_inner_wall_cleaning_injection",
"acetone_inner_wall_cleaning_count",
"acetone_pump_cleaning_suction_count",
"acetone_inner_wall_cleaning_waste_time",
"acetone_stirrer_cleaning_injection",
"acetone_stirrer_cleaning_count",
"acetone_stirrer_cleaning_wait_time",
"acetone_stirrer_cleaning_waste_time",
"pipe_blowing_time",
"injection_pump_forward_empty_suction_count",
"injection_pump_reverse_empty_suction_count",
"filtration_liquid_selection"],
"action": [
{
"init_function": {
"func_name": "init_cleaning_params",
"write_nodes": ["nmp_outer_wall_cleaning_injection", "nmp_outer_wall_cleaning_count","nmp_outer_wall_cleaning_wait_time",
"nmp_outer_wall_cleaning_waste_time","nmp_inner_wall_cleaning_injection","nmp_inner_wall_cleaning_count",
"nmp_pump_cleaning_suction_count",
"nmp_inner_wall_cleaning_waste_time",
"nmp_stirrer_cleaning_injection",
"nmp_stirrer_cleaning_count",
"nmp_stirrer_cleaning_wait_time",
"nmp_stirrer_cleaning_waste_time",
"water_outer_wall_cleaning_injection",
"water_outer_wall_cleaning_count",
"water_outer_wall_cleaning_wait_time",
"water_outer_wall_cleaning_waste_time",
"water_inner_wall_cleaning_injection",
"water_inner_wall_cleaning_count",
"water_pump_cleaning_suction_count",
"water_inner_wall_cleaning_waste_time",
"water_stirrer_cleaning_injection",
"water_stirrer_cleaning_count",
"water_stirrer_cleaning_wait_time",
"water_stirrer_cleaning_waste_time",
"acetone_outer_wall_cleaning_injection",
"acetone_outer_wall_cleaning_count",
"acetone_outer_wall_cleaning_wait_time",
"acetone_outer_wall_cleaning_waste_time",
"acetone_inner_wall_cleaning_injection",
"acetone_inner_wall_cleaning_count",
"acetone_pump_cleaning_suction_count",
"acetone_inner_wall_cleaning_waste_time",
"acetone_stirrer_cleaning_injection",
"acetone_stirrer_cleaning_count",
"acetone_stirrer_cleaning_wait_time",
"acetone_stirrer_cleaning_waste_time",
"pipe_blowing_time",
"injection_pump_forward_empty_suction_count",
"injection_pump_reverse_empty_suction_count",
"filtration_liquid_selection"]
},
"start_function": {
"func_name": "start_cleaning",
"write_nodes": {"cleaning_and_pipe_blowing_trigger": true},
"condition_nodes": ["cleaning_complete"],
"stop_condition_expression": "cleaning_complete == True",
"timeout_seconds": 999999.0
},
"stop_function": {
"func_name": "stop_cleaning",
"write_nodes": {"cleaning_and_pipe_blowing_trigger": false}
}
}
]
}
]
}

View File

@@ -0,0 +1,70 @@
Name,EnglishName,NodeType,DataType,NodeLanguage,NodeId
原料罐号码,raw_tank_number,VARIABLE,INT16,Chinese,ns=4;s=OPC|原料罐号码
反应罐号码,reaction_tank_number,VARIABLE,INT16,Chinese,ns=4;s=OPC|反应罐号码
反应罐及原料罐抓取触发,grab_trigger,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|反应罐及原料罐抓取触发
后处理动作触发,post_process_trigger,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|后处理动作触发
搅拌桨雾化快速,atomization_fast_speed,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|搅拌桨雾化快速
搅拌桨洗涤慢速,wash_slow_speed,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|搅拌桨洗涤慢速
注射泵抽液速度,injection_pump_suction_speed,VARIABLE,INT16,Chinese,ns=4;s=OPC|注射泵抽液速度
注射泵推液速度,injection_pump_push_speed,VARIABLE,INT16,Chinese,ns=4;s=OPC|注射泵推液速度
抽原液次数,raw_liquid_suction_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|抽原液次数
第1次洗涤加水量,first_wash_water_amount,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|第1次洗涤加水量
第2次洗涤加水量,second_wash_water_amount,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|第2次洗涤加水量
第1次粉末搅拌时间,first_powder_mixing_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|第1次粉末搅拌时间
第2次粉末搅拌时间,second_powder_mixing_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|第2次粉末搅拌时间
第1次粉末洗涤次数,first_powder_wash_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|第1次粉末洗涤次数
第2次粉末洗涤次数,second_powder_wash_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|第2次粉末洗涤次数
最开始加水量,initial_water_amount,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|最开始加水量
抽滤前搅拌时间,pre_filtration_mixing_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|抽滤前搅拌时间
雾化压力Kpa,atomization_pressure_kpa,VARIABLE,INT16,Chinese,ns=4;s=OPC|雾化压力Kpa
清洗及管路吹气触发,cleaning_and_pipe_blowing_trigger,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|清洗及管路吹气触发
废液桶满报警,waste_tank_full_alarm,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|废液桶满报警
清水桶空报警,water_tank_empty_alarm,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|清水桶空报警
NMP桶空报警,nmp_tank_empty_alarm,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|NMP桶空报警
丙酮桶空报警,acetone_tank_empty_alarm,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|丙酮桶空报警
门开报警,door_open_alarm,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|门开报警
反应罐及原料罐抓取完成PLCtoPC,grab_complete,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|反应罐及原料罐抓取完成PLCtoPC
后处理动作完成PLCtoPC,post_process_complete,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|后处理动作完成PLCtoPC
清洗及管路吹气完成PLCtoPC,cleaning_complete,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|清洗及管路吹气完成PLCtoPC
远程模式PLCtoPC,remote_mode,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|远程模式PLCtoPC
设备准备就绪PLCtoPC,device_ready,VARIABLE,BOOLEAN,Chinese,ns=4;s=OPC|设备准备就绪PLCtoPC
NMP外壁清洗加注,nmp_outer_wall_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|NMP外壁清洗加注
NMP外壁清洗次数,nmp_outer_wall_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|NMP外壁清洗次数
NMP外壁清洗等待时间,nmp_outer_wall_cleaning_wait_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|NMP外壁清洗等待时间
NMP外壁清洗抽废时间,nmp_outer_wall_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|NMP外壁清洗抽废时间
NMP内壁清洗加注,nmp_inner_wall_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|NMP内壁清洗加注
NMP内壁清洗次数,nmp_inner_wall_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|NMP内壁清洗次数
NMP泵清洗抽次数,nmp_pump_cleaning_suction_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|NMP泵清洗抽次数
NMP内壁清洗抽废时间,nmp_inner_wall_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|NMP内壁清洗抽废时间
NMP搅拌桨清洗加注,nmp_stirrer_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|NMP搅拌桨清洗加注
NMP搅拌桨清洗次数,nmp_stirrer_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|NMP搅拌桨清洗次数
NMP搅拌桨清洗等待时间,nmp_stirrer_cleaning_wait_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|NMP搅拌桨清洗等待时间
NMP搅拌桨清洗抽废时间,nmp_stirrer_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|NMP搅拌桨清洗抽废时间
清水外壁清洗加注,water_outer_wall_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|清水外壁清洗加注
清水外壁清洗次数,water_outer_wall_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|清水外壁清洗次数
清水外壁清洗等待时间,water_outer_wall_cleaning_wait_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|清水外壁清洗等待时间
清水外壁清洗抽废时间,water_outer_wall_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|清水外壁清洗抽废时间
清水内壁清洗加注,water_inner_wall_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|清水内壁清洗加注
清水内壁清洗次数,water_inner_wall_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|清水内壁清洗次数
清水泵清洗抽次数,water_pump_cleaning_suction_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|清水泵清洗抽次数
清水内壁清洗抽废时间,water_inner_wall_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|清水内壁清洗抽废时间
清水搅拌桨清洗加注,water_stirrer_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|清水搅拌桨清洗加注
清水搅拌桨清洗次数,water_stirrer_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|清水搅拌桨清洗次数
清水搅拌桨清洗等待时间,water_stirrer_cleaning_wait_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|清水搅拌桨清洗等待时间
清水搅拌桨清洗抽废时间,water_stirrer_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|清水搅拌桨清洗抽废时间
丙酮外壁清洗加注,acetone_outer_wall_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|丙酮外壁清洗加注
丙酮外壁清洗次数,acetone_outer_wall_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|丙酮外壁清洗次数
丙酮外壁清洗等待时间,acetone_outer_wall_cleaning_wait_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|丙酮外壁清洗等待时间
丙酮外壁清洗抽废时间,acetone_outer_wall_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|丙酮外壁清洗抽废时间
丙酮内壁清洗加注,acetone_inner_wall_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|丙酮内壁清洗加注
丙酮内壁清洗次数,acetone_inner_wall_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|丙酮内壁清洗次数
丙酮泵清洗抽次数,acetone_pump_cleaning_suction_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|丙酮泵清洗抽次数
丙酮内壁清洗抽废时间,acetone_inner_wall_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|丙酮内壁清洗抽废时间
丙酮搅拌桨清洗加注,acetone_stirrer_cleaning_injection,VARIABLE,FLOAT,Chinese,ns=4;s=OPC|丙酮搅拌桨清洗加注
丙酮搅拌桨清洗次数,acetone_stirrer_cleaning_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|丙酮搅拌桨清洗次数
丙酮搅拌桨清洗等待时间,acetone_stirrer_cleaning_wait_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|丙酮搅拌桨清洗等待时间
丙酮搅拌桨清洗抽废时间,acetone_stirrer_cleaning_waste_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|丙酮搅拌桨清洗抽废时间
管道吹气时间,pipe_blowing_time,VARIABLE,INT32,Chinese,ns=4;s=OPC|管道吹气时间
注射泵正向空抽次数,injection_pump_forward_empty_suction_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|注射泵正向空抽次数
注射泵反向空抽次数,injection_pump_reverse_empty_suction_count,VARIABLE,INT16,Chinese,ns=4;s=OPC|注射泵反向空抽次数
抽滤液选择0水1丙酮,filtration_liquid_selection,VARIABLE,INT16,Chinese,ns=4;s=OPC|抽滤液选择0水1丙酮
1 Name EnglishName NodeType DataType NodeLanguage NodeId
2 原料罐号码 raw_tank_number VARIABLE INT16 Chinese ns=4;s=OPC|原料罐号码
3 反应罐号码 reaction_tank_number VARIABLE INT16 Chinese ns=4;s=OPC|反应罐号码
4 反应罐及原料罐抓取触发 grab_trigger VARIABLE BOOLEAN Chinese ns=4;s=OPC|反应罐及原料罐抓取触发
5 后处理动作触发 post_process_trigger VARIABLE BOOLEAN Chinese ns=4;s=OPC|后处理动作触发
6 搅拌桨雾化快速 atomization_fast_speed VARIABLE FLOAT Chinese ns=4;s=OPC|搅拌桨雾化快速
7 搅拌桨洗涤慢速 wash_slow_speed VARIABLE FLOAT Chinese ns=4;s=OPC|搅拌桨洗涤慢速
8 注射泵抽液速度 injection_pump_suction_speed VARIABLE INT16 Chinese ns=4;s=OPC|注射泵抽液速度
9 注射泵推液速度 injection_pump_push_speed VARIABLE INT16 Chinese ns=4;s=OPC|注射泵推液速度
10 抽原液次数 raw_liquid_suction_count VARIABLE INT16 Chinese ns=4;s=OPC|抽原液次数
11 第1次洗涤加水量 first_wash_water_amount VARIABLE FLOAT Chinese ns=4;s=OPC|第1次洗涤加水量
12 第2次洗涤加水量 second_wash_water_amount VARIABLE FLOAT Chinese ns=4;s=OPC|第2次洗涤加水量
13 第1次粉末搅拌时间 first_powder_mixing_time VARIABLE INT32 Chinese ns=4;s=OPC|第1次粉末搅拌时间
14 第2次粉末搅拌时间 second_powder_mixing_time VARIABLE INT32 Chinese ns=4;s=OPC|第2次粉末搅拌时间
15 第1次粉末洗涤次数 first_powder_wash_count VARIABLE INT16 Chinese ns=4;s=OPC|第1次粉末洗涤次数
16 第2次粉末洗涤次数 second_powder_wash_count VARIABLE INT16 Chinese ns=4;s=OPC|第2次粉末洗涤次数
17 最开始加水量 initial_water_amount VARIABLE FLOAT Chinese ns=4;s=OPC|最开始加水量
18 抽滤前搅拌时间 pre_filtration_mixing_time VARIABLE INT32 Chinese ns=4;s=OPC|抽滤前搅拌时间
19 雾化压力Kpa atomization_pressure_kpa VARIABLE INT16 Chinese ns=4;s=OPC|雾化压力Kpa
20 清洗及管路吹气触发 cleaning_and_pipe_blowing_trigger VARIABLE BOOLEAN Chinese ns=4;s=OPC|清洗及管路吹气触发
21 废液桶满报警 waste_tank_full_alarm VARIABLE BOOLEAN Chinese ns=4;s=OPC|废液桶满报警
22 清水桶空报警 water_tank_empty_alarm VARIABLE BOOLEAN Chinese ns=4;s=OPC|清水桶空报警
23 NMP桶空报警 nmp_tank_empty_alarm VARIABLE BOOLEAN Chinese ns=4;s=OPC|NMP桶空报警
24 丙酮桶空报警 acetone_tank_empty_alarm VARIABLE BOOLEAN Chinese ns=4;s=OPC|丙酮桶空报警
25 门开报警 door_open_alarm VARIABLE BOOLEAN Chinese ns=4;s=OPC|门开报警
26 反应罐及原料罐抓取完成PLCtoPC grab_complete VARIABLE BOOLEAN Chinese ns=4;s=OPC|反应罐及原料罐抓取完成PLCtoPC
27 后处理动作完成PLCtoPC post_process_complete VARIABLE BOOLEAN Chinese ns=4;s=OPC|后处理动作完成PLCtoPC
28 清洗及管路吹气完成PLCtoPC cleaning_complete VARIABLE BOOLEAN Chinese ns=4;s=OPC|清洗及管路吹气完成PLCtoPC
29 远程模式PLCtoPC remote_mode VARIABLE BOOLEAN Chinese ns=4;s=OPC|远程模式PLCtoPC
30 设备准备就绪PLCtoPC device_ready VARIABLE BOOLEAN Chinese ns=4;s=OPC|设备准备就绪PLCtoPC
31 NMP外壁清洗加注 nmp_outer_wall_cleaning_injection VARIABLE FLOAT Chinese ns=4;s=OPC|NMP外壁清洗加注
32 NMP外壁清洗次数 nmp_outer_wall_cleaning_count VARIABLE INT16 Chinese ns=4;s=OPC|NMP外壁清洗次数
33 NMP外壁清洗等待时间 nmp_outer_wall_cleaning_wait_time VARIABLE INT32 Chinese ns=4;s=OPC|NMP外壁清洗等待时间
34 NMP外壁清洗抽废时间 nmp_outer_wall_cleaning_waste_time VARIABLE INT32 Chinese ns=4;s=OPC|NMP外壁清洗抽废时间
35 NMP内壁清洗加注 nmp_inner_wall_cleaning_injection VARIABLE FLOAT Chinese ns=4;s=OPC|NMP内壁清洗加注
36 NMP内壁清洗次数 nmp_inner_wall_cleaning_count VARIABLE INT16 Chinese ns=4;s=OPC|NMP内壁清洗次数
37 NMP泵清洗抽次数 nmp_pump_cleaning_suction_count VARIABLE INT16 Chinese ns=4;s=OPC|NMP泵清洗抽次数
38 NMP内壁清洗抽废时间 nmp_inner_wall_cleaning_waste_time VARIABLE INT32 Chinese ns=4;s=OPC|NMP内壁清洗抽废时间
39 NMP搅拌桨清洗加注 nmp_stirrer_cleaning_injection VARIABLE FLOAT Chinese ns=4;s=OPC|NMP搅拌桨清洗加注
40 NMP搅拌桨清洗次数 nmp_stirrer_cleaning_count VARIABLE INT16 Chinese ns=4;s=OPC|NMP搅拌桨清洗次数
41 NMP搅拌桨清洗等待时间 nmp_stirrer_cleaning_wait_time VARIABLE INT32 Chinese ns=4;s=OPC|NMP搅拌桨清洗等待时间
42 NMP搅拌桨清洗抽废时间 nmp_stirrer_cleaning_waste_time VARIABLE INT32 Chinese ns=4;s=OPC|NMP搅拌桨清洗抽废时间
43 清水外壁清洗加注 water_outer_wall_cleaning_injection VARIABLE FLOAT Chinese ns=4;s=OPC|清水外壁清洗加注
44 清水外壁清洗次数 water_outer_wall_cleaning_count VARIABLE INT16 Chinese ns=4;s=OPC|清水外壁清洗次数
45 清水外壁清洗等待时间 water_outer_wall_cleaning_wait_time VARIABLE INT32 Chinese ns=4;s=OPC|清水外壁清洗等待时间
46 清水外壁清洗抽废时间 water_outer_wall_cleaning_waste_time VARIABLE INT32 Chinese ns=4;s=OPC|清水外壁清洗抽废时间
47 清水内壁清洗加注 water_inner_wall_cleaning_injection VARIABLE FLOAT Chinese ns=4;s=OPC|清水内壁清洗加注
48 清水内壁清洗次数 water_inner_wall_cleaning_count VARIABLE INT16 Chinese ns=4;s=OPC|清水内壁清洗次数
49 清水泵清洗抽次数 water_pump_cleaning_suction_count VARIABLE INT16 Chinese ns=4;s=OPC|清水泵清洗抽次数
50 清水内壁清洗抽废时间 water_inner_wall_cleaning_waste_time VARIABLE INT32 Chinese ns=4;s=OPC|清水内壁清洗抽废时间
51 清水搅拌桨清洗加注 water_stirrer_cleaning_injection VARIABLE FLOAT Chinese ns=4;s=OPC|清水搅拌桨清洗加注
52 清水搅拌桨清洗次数 water_stirrer_cleaning_count VARIABLE INT16 Chinese ns=4;s=OPC|清水搅拌桨清洗次数
53 清水搅拌桨清洗等待时间 water_stirrer_cleaning_wait_time VARIABLE INT32 Chinese ns=4;s=OPC|清水搅拌桨清洗等待时间
54 清水搅拌桨清洗抽废时间 water_stirrer_cleaning_waste_time VARIABLE INT32 Chinese ns=4;s=OPC|清水搅拌桨清洗抽废时间
55 丙酮外壁清洗加注 acetone_outer_wall_cleaning_injection VARIABLE FLOAT Chinese ns=4;s=OPC|丙酮外壁清洗加注
56 丙酮外壁清洗次数 acetone_outer_wall_cleaning_count VARIABLE INT16 Chinese ns=4;s=OPC|丙酮外壁清洗次数
57 丙酮外壁清洗等待时间 acetone_outer_wall_cleaning_wait_time VARIABLE INT32 Chinese ns=4;s=OPC|丙酮外壁清洗等待时间
58 丙酮外壁清洗抽废时间 acetone_outer_wall_cleaning_waste_time VARIABLE INT32 Chinese ns=4;s=OPC|丙酮外壁清洗抽废时间
59 丙酮内壁清洗加注 acetone_inner_wall_cleaning_injection VARIABLE FLOAT Chinese ns=4;s=OPC|丙酮内壁清洗加注
60 丙酮内壁清洗次数 acetone_inner_wall_cleaning_count VARIABLE INT16 Chinese ns=4;s=OPC|丙酮内壁清洗次数
61 丙酮泵清洗抽次数 acetone_pump_cleaning_suction_count VARIABLE INT16 Chinese ns=4;s=OPC|丙酮泵清洗抽次数
62 丙酮内壁清洗抽废时间 acetone_inner_wall_cleaning_waste_time VARIABLE INT32 Chinese ns=4;s=OPC|丙酮内壁清洗抽废时间
63 丙酮搅拌桨清洗加注 acetone_stirrer_cleaning_injection VARIABLE FLOAT Chinese ns=4;s=OPC|丙酮搅拌桨清洗加注
64 丙酮搅拌桨清洗次数 acetone_stirrer_cleaning_count VARIABLE INT16 Chinese ns=4;s=OPC|丙酮搅拌桨清洗次数
65 丙酮搅拌桨清洗等待时间 acetone_stirrer_cleaning_wait_time VARIABLE INT32 Chinese ns=4;s=OPC|丙酮搅拌桨清洗等待时间
66 丙酮搅拌桨清洗抽废时间 acetone_stirrer_cleaning_waste_time VARIABLE INT32 Chinese ns=4;s=OPC|丙酮搅拌桨清洗抽废时间
67 管道吹气时间 pipe_blowing_time VARIABLE INT32 Chinese ns=4;s=OPC|管道吹气时间
68 注射泵正向空抽次数 injection_pump_forward_empty_suction_count VARIABLE INT16 Chinese ns=4;s=OPC|注射泵正向空抽次数
69 注射泵反向空抽次数 injection_pump_reverse_empty_suction_count VARIABLE INT16 Chinese ns=4;s=OPC|注射泵反向空抽次数
70 抽滤液选择0水1丙酮 filtration_liquid_selection VARIABLE INT16 Chinese ns=4;s=OPC|抽滤液选择0水1丙酮

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
{
"nodes": [
{
"id": "post_process_station",
"name": "post_process_station",
"children": [
"post_process_deck"
],
"parent": null,
"type": "device",
"class": "post_process_station",
"config": {
"url": "opc.tcp://LAPTOP-AN6QGCSD:53530/OPCUA/SimulationServer",
"config_path": "C:\\Users\\Roy\\Desktop\\DPLC\\Uni-Lab-OS\\unilabos\\devices\\workstation\\post_process\\opcua_huairou.json",
"deck": {
"data": {
"_resource_child_name": "post_process_deck",
"_resource_type": "unilabos.devices.workstation.post_process.decks:post_process_deck"
}
}
},
"data": {
}
},
{
"id": "post_process_deck",
"name": "post_process_deck",
"sample_id": null,
"children": [],
"parent": "post_process_station",
"type": "deck",
"class": "post_process_deck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "post_process_deck",
"setup": true
},
"data": {}
}
]
}

View File

@@ -0,0 +1,160 @@
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
LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
def warehouse_factory(
name: str,
num_items_x: int = 1,
num_items_y: int = 4,
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,
resource_size_x: float = 127.0,
resource_size_y: float = 86.0,
resource_size_z: float = 25.0,
removed_positions: Optional[List[int]] = None,
empty: bool = False,
category: str = "warehouse",
model: Optional[str] = None,
col_offset: int = 0, # 列起始偏移量用于生成5-8等命名
layout: str = "col-major", # 新增:排序方式,"col-major"=列优先,"row-major"=行优先
):
# 创建位置坐标
locations = []
for layer in range(num_items_z): # 层
for row in range(num_items_y): # 行
for col in range(num_items_x): # 列
# 计算位置
x = dx + col * item_dx
# 根据 layout 决定 y 坐标计算
if layout == "row-major":
# 行优先row=0(第1行) 应该显示在上方y 值最小
y = dy + row * item_dy
else:
# 列优先:保持原逻辑
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=resource_size_x,
resource_size_y=resource_size_y,
resource_size_z=resource_size_z,
name_prefix=name,
)
len_x, len_y = (num_items_x, num_items_y) if num_items_z == 1 else (num_items_y, num_items_z) if num_items_x == 1 else (num_items_x, num_items_z)
# 🔑 修改使用数字命名最上面是4321最下面是12,11,10,9
# 命名顺序必须与坐标生成顺序一致:层 → 行 → 列
keys = []
for layer in range(num_items_z): # 遍历每一层
for row in range(num_items_y): # 遍历每一行
for col in range(num_items_x): # 遍历每一列
# 倒序计算全局行号row=0 应该对应 global_row=0第1行4321
# row=1 应该对应 global_row=1第2行8765
# row=2 应该对应 global_row=2第3行12,11,10,9
# 但前端显示时 row=2 在最上面,所以需要反转
reversed_row = (num_items_y - 1 - row) # row=0→reversed_row=2, row=1→reversed_row=1, row=2→reversed_row=0
global_row = layer * num_items_y + reversed_row
# 每行的最大数字 = (global_row + 1) * num_items_x + col_offset
base_num = (global_row + 1) * num_items_x + col_offset
# 从右到左递减4,3,2,1
key = str(base_num - col)
keys.append(key)
sites = {i: site for i, site in zip(keys, _sites.values())}
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,
ordering_layout=layout, # 传递排序方式到 ordering_layout
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,
layout: str = "x-y",
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
category: str = "warehouse",
model: Optional[str] = None,
ordering_layout: str = "col-major",
**kwargs
):
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,
layout=layout,
sites=sites,
category=category,
model=model,
)
# 保存排序方式供graphio.py的坐标映射使用
# 使用独立属性避免与父类的layout冲突
self.ordering_layout = ordering_layout
def serialize(self) -> dict:
"""序列化时保存 ordering_layout 属性"""
data = super().serialize()
data['ordering_layout'] = self.ordering_layout
return data
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

View File

@@ -0,0 +1,38 @@
from unilabos.devices.workstation.post_process.post_process_warehouse import WareHouse, warehouse_factory
# =================== Other ===================
def post_process_warehouse_4x3x1(name: str) -> WareHouse:
"""创建post_process 4x3x1仓库"""
return warehouse_factory(
name=name,
num_items_x=4,
num_items_y=3,
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",
)
def post_process_warehouse_4x3x1_2(name: str) -> WareHouse:
"""已弃用创建post_process 4x3x1仓库"""
return warehouse_factory(
name=name,
num_items_x=4,
num_items_y=3,
num_items_z=1,
dx=12.0,
dy=12.0,
dz=12.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)

View File

@@ -0,0 +1,105 @@
cameracontroller_device:
category:
- cameraSII
class:
action_value_mappings:
auto-start:
feedback: {}
goal: {}
goal_default:
config: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
config:
type: string
required: []
type: object
result: {}
required:
- goal
title: start参数
type: object
type: UniLabJsonCommand
auto-stop:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: stop参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.cameraSII.cameraUSB:CameraController
status_types:
status: dict
type: python
config_info: []
description: Uni-Lab-OS 摄像头驱动Linux USB 摄像头版,无 PTZ
handles: []
icon: ''
init_param_schema:
config:
properties:
audio_bitrate:
default: 64k
type: string
audio_device:
type: string
fps:
default: 30
type: integer
height:
default: 720
type: integer
host_id:
default: demo-host
type: string
rtmp_url:
default: rtmp://srs.sciol.ac.cn:4499/live/camera-01
type: string
signal_backend_url:
default: wss://sciol.ac.cn/api/realtime/signal/host
type: string
video_bitrate:
default: 1500k
type: string
video_device:
default: /dev/video0
type: string
webrtc_api:
default: https://srs.sciol.ac.cn/rtc/v1/play/
type: string
webrtc_stream_url:
default: webrtc://srs.sciol.ac.cn:4500/live/camera-01
type: string
width:
default: 1280
type: integer
required: []
type: object
data:
properties:
status:
type: object
required:
- status
type: object
registry_type: device
version: 1.0.0

View File

@@ -0,0 +1,344 @@
separator.chinwe:
category:
- separator
- chinwe
class:
action_value_mappings:
motor_rotate_quarter:
goal:
direction: 顺时针
motor_id: 4
speed: 60
handles: {}
schema:
description: 电机旋转 1/4 圈
properties:
goal:
properties:
direction:
default: 顺时针
description: 旋转方向
enum:
- 顺时针
- 逆时针
type: string
motor_id:
default: '4'
description: 选择电机 (4:搅拌, 5:旋钮)
enum:
- '4'
- '5'
type: string
speed:
default: 60
description: 速度 (RPM)
type: integer
required:
- motor_id
- speed
type: object
type: UniLabJsonCommand
motor_run_continuous:
goal:
direction: 顺时针
motor_id: 4
speed: 60
handles: {}
schema:
description: 电机一直旋转 (速度模式)
properties:
goal:
properties:
direction:
default: 顺时针
description: 旋转方向
enum:
- 顺时针
- 逆时针
type: string
motor_id:
default: '4'
description: 选择电机 (4:搅拌, 5:旋钮)
enum:
- '4'
- '5'
type: string
speed:
default: 60
description: 速度 (RPM)
type: integer
required:
- motor_id
- speed
type: object
type: UniLabJsonCommand
motor_stop:
goal:
motor_id: 4
handles: {}
schema:
description: 停止指定步进电机
properties:
goal:
properties:
motor_id:
default: '4'
description: 选择电机
enum:
- '4'
- '5'
title: '注: 4=搅拌, 5=旋钮'
type: string
required:
- motor_id
type: object
type: UniLabJsonCommand
pump_aspirate:
goal:
pump_id: 1
valve_port: 1
volume: 1000
handles: {}
schema:
description: 注射泵吸液
properties:
goal:
properties:
pump_id:
default: '1'
description: 选择泵
enum:
- '1'
- '2'
- '3'
type: string
valve_port:
default: '1'
description: 阀门端口
enum:
- '1'
- '2'
- '3'
- '4'
- '5'
- '6'
- '7'
- '8'
type: string
volume:
default: 1000
description: 吸液步数
type: integer
required:
- pump_id
- volume
- valve_port
type: object
type: UniLabJsonCommand
pump_dispense:
goal:
pump_id: 1
valve_port: 1
volume: 1000
handles: {}
schema:
description: 注射泵排液
properties:
goal:
properties:
pump_id:
default: '1'
description: 选择泵
enum:
- '1'
- '2'
- '3'
type: string
valve_port:
default: '1'
description: 阀门端口
enum:
- '1'
- '2'
- '3'
- '4'
- '5'
- '6'
- '7'
- '8'
type: string
volume:
default: 1000
description: 排液步数
type: integer
required:
- pump_id
- volume
- valve_port
type: object
type: UniLabJsonCommand
pump_initialize:
goal:
drain_port: 0
output_port: 0
pump_id: 1
speed: 10
handles: {}
schema:
description: 初始化指定注射泵
properties:
goal:
properties:
drain_port:
default: 0
description: 排液口索引
type: integer
output_port:
default: 0
description: 输出口索引
type: integer
pump_id:
default: '1'
description: 选择泵
enum:
- '1'
- '2'
- '3'
title: '注: 1号泵, 2号泵, 3号泵'
type: string
speed:
default: 10
description: 运动速度
type: integer
required:
- pump_id
type: object
type: UniLabJsonCommand
pump_valve:
goal:
port: 1
pump_id: 1
handles: {}
schema:
description: 切换指定泵的阀门端口
properties:
goal:
properties:
port:
default: '1'
description: 阀门端口号 (1-8)
enum:
- '1'
- '2'
- '3'
- '4'
- '5'
- '6'
- '7'
- '8'
type: string
pump_id:
default: '1'
description: 选择泵
enum:
- '1'
- '2'
- '3'
type: string
required:
- pump_id
- port
type: object
type: UniLabJsonCommand
wait_sensor_level:
goal:
target_state: 有液
timeout: 30
handles: {}
schema:
description: 等待传感器液位条件
properties:
goal:
properties:
target_state:
default: 有液
description: 目标液位状态
enum:
- 有液
- 无液
type: string
timeout:
default: 30
description: 超时时间 (秒)
type: integer
required:
- target_state
type: object
type: UniLabJsonCommand
wait_time:
goal:
duration: 10
handles: {}
schema:
description: 等待指定时间
properties:
goal:
properties:
duration:
default: 10
description: 等待时间 (秒)
type: integer
required:
- duration
type: object
type: UniLabJsonCommand
module: unilabos.devices.separator.chinwe:ChinweDevice
status_types:
is_connected: bool
sensor_level: bool
sensor_rssi: int
type: python
config_info: []
description: ChinWe 简易工作站控制器 (3泵, 2电机, 1传感器)
handles: []
icon: ''
init_param_schema:
goal:
baudrate:
default: 9600
description: 串口波特率
type: integer
motor_ids:
default:
- 4
- 5
description: 步进电机ID列表
items:
type: integer
type: array
port:
default: 192.168.1.200:8899
description: 串口号或 IP:Port
type: string
pump_ids:
default:
- 1
- 2
- 3
description: 注射泵ID列表
items:
type: integer
type: array
sensor_id:
default: 6
description: XKC传感器ID
type: integer
sensor_threshold:
default: 300
description: 传感器液位判定阈值
type: integer
timeout:
default: 10
description: 通信超时时间 (秒)
type: integer
version: 2.1.0

View File

@@ -1,73 +1,40 @@
neware_battery_test_system:
category:
- neware_battery_test_system
- neware
- battery_test
class:
action_value_mappings:
auto-post_init:
debug_resource_names:
feedback: {}
goal: {}
goal_default:
ros_node: null
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
result:
return_info: return_info
success: success
schema:
description: ''
description: 调试方法:显示所有资源的实际名称
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result:
properties:
ros_node:
return_info:
description: 资源调试信息
type: string
success:
description: 是否成功
type: boolean
required:
- ros_node
- return_info
- success
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-print_status_summary:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
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: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: test_connection参数
type: object
type: UniLabJsonCommand
export_status_json:
@@ -145,29 +112,32 @@ neware_battery_test_system:
goal:
plate_num: plate_num
goal_default:
plate_num: 1
plate_num: null
handles: {}
result:
plate_data: plate_data
return_info: return_info
success: success
schema:
description: 获取指定盘(1或2)的电池状态信息
description: 获取指定盘或所有盘的状态信息
properties:
feedback: {}
goal:
properties:
plate_num:
description: 盘号 (1 或 2)
description: 盘号 (1 或 2)如果为null则返回所有盘的状态
maximum: 2
minimum: 1
type: integer
required:
- plate_num
required: []
type: object
result:
properties:
plate_data:
description: 盘状态数据(单盘或所有盘)
type: object
return_info:
description: 盘状态信息JSON格式
description: 操作结果信息
type: string
success:
description: 查询是否成功
@@ -175,6 +145,7 @@ neware_battery_test_system:
required:
- return_info
- success
- plate_data
type: object
required:
- goal
@@ -219,7 +190,9 @@ neware_battery_test_system:
goal_default:
string: ''
handles: {}
result: {}
result:
return_info: return_info
success: success
schema:
description: ''
properties:
@@ -252,6 +225,56 @@ neware_battery_test_system:
title: StrSingleInput
type: object
type: StrSingleInput
submit_from_csv:
feedback: {}
goal:
csv_path: string
output_dir: string
goal_default:
csv_path: ''
output_dir: .
handles: {}
result:
return_info: return_info
submitted_count: submitted_count
success: success
schema:
description: 从CSV文件批量提交Neware测试任务
properties:
feedback: {}
goal:
properties:
csv_path:
description: 输入CSV文件的绝对路径
type: string
output_dir:
description: 输出目录用于存储XML和备份文件默认当前目录
type: string
required:
- csv_path
type: object
result:
properties:
return_info:
description: 执行结果详细信息
type: string
submitted_count:
description: 成功提交的任务数量
type: integer
success:
description: 是否成功
type: boolean
total_count:
description: CSV文件中的总行数
type: integer
required:
- return_info
- success
type: object
required:
- goal
type: object
type: UniLabJsonCommand
test_connection_action:
feedback: {}
goal: {}
@@ -284,30 +307,135 @@ neware_battery_test_system:
- goal
type: object
type: UniLabJsonCommand
module: unilabos.devices.battery.neware_battery_test_system:NewareBatteryTestSystem
upload_backup_to_oss:
feedback: {}
goal:
backup_dir: backup_dir
file_pattern: file_pattern
oss_prefix: oss_prefix
goal_default:
backup_dir: null
file_pattern: '*'
oss_prefix: null
handles:
output:
- data_key: uploaded_files
data_source: executor
data_type: array
handler_key: uploaded_files
io_type: sink
label: Uploaded Files (with standard flow info)
result:
failed_files: failed_files
return_info: return_info
success: success
total_count: total_count
uploaded_count: uploaded_count
schema:
description: 上传备份文件到阿里云OSS
properties:
feedback: {}
goal:
properties:
backup_dir:
description: 备份目录路径默认使用最近一次submit_from_csv的backup_dir
type: string
file_pattern:
default: '*'
description: 文件通配符模式,例如 *.csv 或 Battery_*.nda
type: string
oss_prefix:
description: OSS对象路径前缀默认使用self.oss_prefix
type: string
required: []
type: object
result:
properties:
failed_files:
description: 上传失败的文件名列表
items:
type: string
type: array
return_info:
description: 上传操作结果信息
type: string
success:
description: 上传是否成功
type: boolean
total_count:
description: 总文件数
type: integer
uploaded_count:
description: 成功上传的文件数
type: integer
uploaded_files:
description: 成功上传的文件详情列表
items:
properties:
Battery_Code:
description: 电池编码
type: string
Electrolyte_Code:
description: 电解液编码
type: string
filename:
description: 文件名
type: string
url:
description: OSS下载链接
type: string
required:
- filename
- url
- Battery_Code
- Electrolyte_Code
type: object
type: array
required:
- return_info
- success
- uploaded_count
- total_count
- failed_files
- uploaded_files
type: object
required:
- goal
type: object
type: UniLabJsonCommand
module: unilabos.devices.neware_battery_test_system.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盘电池状态映射和监控
description: 新威电池测试系统驱动,提供720个通道的电池测试状态监控、物料管理和CSV批量提交功能。支持TCP通信实现远程控制包含完整的物料管理系统2盘电池状态映射以及从CSV文件批量提交测试任务的能力
handles: []
icon: ''
init_param_schema:
config:
properties:
devtype:
default: '27'
type: string
ip:
default: 127.0.0.1
type: string
machine_id:
default: 1
type: integer
oss_prefix:
default: neware_backup
description: OSS对象路径前缀
type: string
oss_upload_enabled:
default: false
description: 是否启用OSS上传功能
type: boolean
port:
default: 502
type: integer
size_x:
default: 500.0
@@ -319,6 +447,7 @@ neware_battery_test_system:
default: 2000.0
type: number
timeout:
default: 20
type: integer
required: []
type: object
@@ -330,8 +459,6 @@ neware_battery_test_system:
type: object
device_summary:
type: object
plate_status:
type: object
status:
type: string
total_channels:
@@ -341,7 +468,6 @@ neware_battery_test_system:
- channel_status
- connection_info
- total_channels
- plate_status
- device_summary
type: object
version: 1.0.0

View File

@@ -136,46 +136,46 @@ post_process_station:
water_stirrer_cleaning_wait_time: water_stirrer_cleaning_wait_time
water_stirrer_cleaning_waste_time: water_stirrer_cleaning_waste_time
goal_default:
nmp_outer_wall_cleaning_injection: 240.0
nmp_outer_wall_cleaning_count: 3
nmp_outer_wall_cleaning_wait_time: 30
nmp_outer_wall_cleaning_waste_time: 20
nmp_inner_wall_cleaning_injection: 300.0
nmp_inner_wall_cleaning_count: 2
nmp_pump_cleaning_suction_count: 8
nmp_inner_wall_cleaning_waste_time: 20
nmp_stirrer_cleaning_injection: 500.0
nmp_stirrer_cleaning_count: 1
nmp_stirrer_cleaning_wait_time: 30
nmp_stirrer_cleaning_waste_time: 30
water_outer_wall_cleaning_injection: 200.0
water_outer_wall_cleaning_count: 2
water_outer_wall_cleaning_wait_time: 20
water_outer_wall_cleaning_waste_time: 15
water_inner_wall_cleaning_injection: 200.0
water_inner_wall_cleaning_count: 1
water_pump_cleaning_suction_count: 3
water_inner_wall_cleaning_waste_time: 15
water_stirrer_cleaning_injection: 200.0
water_stirrer_cleaning_count: 1
water_stirrer_cleaning_wait_time: 20
water_stirrer_cleaning_waste_time: 15
acetone_outer_wall_cleaning_injection: 240.0
acetone_outer_wall_cleaning_count: 1
acetone_outer_wall_cleaning_wait_time: 30
acetone_outer_wall_cleaning_waste_time: 10
acetone_inner_wall_cleaning_injection: 200.0
acetone_inner_wall_cleaning_count: 1
acetone_pump_cleaning_suction_count: 3
acetone_inner_wall_cleaning_waste_time: 10
acetone_stirrer_cleaning_injection: 500.0
acetone_stirrer_cleaning_count: 1
acetone_stirrer_cleaning_wait_time: 30
acetone_stirrer_cleaning_waste_time: 30
pipe_blowing_time: 15
injection_pump_forward_empty_suction_count: 15
injection_pump_reverse_empty_suction_count: 60
acetone_inner_wall_cleaning_count: 0
acetone_inner_wall_cleaning_injection: 0.0
acetone_inner_wall_cleaning_waste_time: 0
acetone_outer_wall_cleaning_count: 0
acetone_outer_wall_cleaning_injection: 0.0
acetone_outer_wall_cleaning_wait_time: 0
acetone_outer_wall_cleaning_waste_time: 0
acetone_pump_cleaning_suction_count: 0
acetone_stirrer_cleaning_count: 0
acetone_stirrer_cleaning_injection: 0.0
acetone_stirrer_cleaning_wait_time: 0
acetone_stirrer_cleaning_waste_time: 0
filtration_liquid_selection: 0
injection_pump_forward_empty_suction_count: 0
injection_pump_reverse_empty_suction_count: 0
nmp_inner_wall_cleaning_count: 0
nmp_inner_wall_cleaning_injection: 0.0
nmp_inner_wall_cleaning_waste_time: 0
nmp_outer_wall_cleaning_count: 0
nmp_outer_wall_cleaning_injection: 0.0
nmp_outer_wall_cleaning_wait_time: 0
nmp_outer_wall_cleaning_waste_time: 0
nmp_pump_cleaning_suction_count: 0
nmp_stirrer_cleaning_count: 0
nmp_stirrer_cleaning_injection: 0.0
nmp_stirrer_cleaning_wait_time: 0
nmp_stirrer_cleaning_waste_time: 0
pipe_blowing_time: 0
water_inner_wall_cleaning_count: 0
water_inner_wall_cleaning_injection: 0.0
water_inner_wall_cleaning_waste_time: 0
water_outer_wall_cleaning_count: 0
water_outer_wall_cleaning_injection: 0.0
water_outer_wall_cleaning_wait_time: 0
water_outer_wall_cleaning_waste_time: 0
water_pump_cleaning_suction_count: 0
water_stirrer_cleaning_count: 0
water_stirrer_cleaning_injection: 0.0
water_stirrer_cleaning_wait_time: 0
water_stirrer_cleaning_waste_time: 0
handles: {}
result:
return_info: return_info
@@ -438,34 +438,34 @@ post_process_station:
feedback: {}
goal:
atomization_fast_speed: atomization_fast_speed
wash_slow_speed: wash_slow_speed
injection_pump_suction_speed: injection_pump_suction_speed
injection_pump_push_speed: injection_pump_push_speed
raw_liquid_suction_count: raw_liquid_suction_count
first_wash_water_amount: first_wash_water_amount
second_wash_water_amount: second_wash_water_amount
first_powder_mixing_time: first_powder_mixing_time
second_powder_mixing_time: second_powder_mixing_time
first_powder_wash_count: first_powder_wash_count
second_powder_wash_count: second_powder_wash_count
initial_water_amount: initial_water_amount
pre_filtration_mixing_time: pre_filtration_mixing_time
atomization_pressure_kpa: atomization_pressure_kpa
first_powder_mixing_tim: first_powder_mixing_tim
first_powder_wash_count: first_powder_wash_count
first_wash_water_amount: first_wash_water_amount
initial_water_amount: initial_water_amount
injection_pump_push_speed: injection_pump_push_speed
injection_pump_suction_speed: injection_pump_suction_speed
pre_filtration_mixing_time: pre_filtration_mixing_time
raw_liquid_suction_count: raw_liquid_suction_count
second_powder_mixing_time: second_powder_mixing_time
second_powder_wash_count: second_powder_wash_count
second_wash_water_amount: second_wash_water_amount
wash_slow_speed: wash_slow_speed
goal_default:
atomization_fast_speed: 17.0
wash_slow_speed: 17.0
injection_pump_suction_speed: 300
injection_pump_push_speed: 100
raw_liquid_suction_count: 1
first_wash_water_amount: 550.0
second_wash_water_amount: 550.0
first_powder_mixing_time: 300
second_powder_mixing_time: 300
first_powder_wash_count: 1
second_powder_wash_count: 1
initial_water_amount: 551.0
pre_filtration_mixing_time: 300
atomization_pressure_kpa: 74
atomization_fast_speed: 0.0
atomization_pressure_kpa: 0
first_powder_mixing_tim: 0
first_powder_wash_count: 0
first_wash_water_amount: 0.0
initial_water_amount: 0.0
injection_pump_push_speed: 0
injection_pump_suction_speed: 0
pre_filtration_mixing_time: 0
raw_liquid_suction_count: 0
second_powder_mixing_time: 0
second_powder_wash_count: 0
second_wash_water_amount: 0.0
wash_slow_speed: 0.0
handles: {}
result:
return_info: return_info
@@ -485,7 +485,7 @@ post_process_station:
maximum: 2147483647
minimum: -2147483648
type: integer
first_powder_mixing_time:
first_powder_mixing_tim:
maximum: 2147483647
minimum: -2147483648
type: integer
@@ -526,20 +526,20 @@ post_process_station:
wash_slow_speed:
type: number
required:
- atomization_fast_speed
- wash_slow_speed
- injection_pump_suction_speed
- injection_pump_push_speed
- raw_liquid_suction_count
- first_wash_water_amount
- second_wash_water_amount
- first_powder_mixing_time
- second_powder_mixing_time
- first_powder_wash_count
- second_powder_wash_count
- initial_water_amount
- pre_filtration_mixing_time
- atomization_pressure_kpa
- atomization_fast_speed
- wash_slow_speed
- injection_pump_suction_speed
- injection_pump_push_speed
- raw_liquid_suction_count
- first_wash_water_amount
- second_wash_water_amount
- first_powder_mixing_tim
- second_powder_mixing_time
- first_powder_wash_count
- second_powder_wash_count
- initial_water_amount
- pre_filtration_mixing_time
- atomization_pressure_kpa
title: PostProcessTriggerPostPro_Goal
type: object
result:
@@ -600,7 +600,7 @@ post_process_station:
title: SendCmd
type: object
type: SendCmd
module: unilabos.device_comms.opcua_client.client:OpcUaClient
module: unilabos.devices.workstation.post_process.post_process:OpcUaClient
status_types:
acetone_tank_empty_alarm: Bool
atomization_fast_speed: Float64
@@ -625,6 +625,6 @@ post_process_station:
config_info: []
description: 后处理站
handles: []
icon: post_process_station.png
icon: post_process_station.webp
init_param_schema: {}
version: 1.0.0

View File

@@ -237,6 +237,8 @@ class Registry:
resource_info["category"] = [file.stem]
elif file.stem not in resource_info["category"]:
resource_info["category"].append(file.stem)
elif not isinstance(resource_info.get("category"), list):
resource_info["category"] = [resource_info["category"]]
if "config_info" not in resource_info:
resource_info["config_info"] = []
if "icon" not in resource_info:

View File

@@ -0,0 +1,25 @@
POST_PROCESS_Raw_1BottleCarrier:
category:
- bottle_carriers
class:
module: unilabos.devices.workstation.post_process.bottle_carriers:POST_PROCESS_Raw_1BottleCarrier
type: pylabrobot
description: POST_PROCESS_Raw_1BottleCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
POST_PROCESS_Reaction_1BottleCarrier:
category:
- bottle_carriers
class:
module: unilabos.devices.workstation.post_process.bottle_carriers:POST_PROCESS_Reaction_1BottleCarrier
type: pylabrobot
description: POST_PROCESS_Reaction_1BottleCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -0,0 +1,11 @@
POST_PROCESS_PolymerStation_Reagent_Bottle:
category:
- bottles
class:
module: unilabos.devices.workstation.post_process.bottles:POST_PROCESS_PolymerStation_Reagent_Bottle
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0

View File

@@ -0,0 +1,12 @@
post_process_deck:
category:
- post_process_deck
class:
module: unilabos.devices.workstation.post_process.decks:post_process_deck
type: pylabrobot
description: post_process_deck
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -0,0 +1,108 @@
PRCXI_30mm_Adapter:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_30mm_Adapter
type: pylabrobot
description: '30mm适配器 (Code: ZX-58-30)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Adapter:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Adapter
type: pylabrobot
description: '适配器 (Code: Fhh478)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Deep10_Adapter:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Deep10_Adapter
type: pylabrobot
description: '10ul专用深孔板适配器 (Code: ZX-002-10)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Deep300_Adapter:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Deep300_Adapter
type: pylabrobot
description: '300ul深孔板适配器 (Code: ZX-002-300)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_PCR_Adapter:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_PCR_Adapter
type: pylabrobot
description: '全裙边 PCR适配器 (Code: ZX-58-0001)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Reservoir_Adapter:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Reservoir_Adapter
type: pylabrobot
description: '储液槽 适配器 (Code: ZX-ADP-001)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Tip10_Adapter:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Tip10_Adapter
type: pylabrobot
description: '吸头10ul 适配器 (Code: ZX-58-10)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Tip1250_Adapter:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Tip1250_Adapter
type: pylabrobot
description: 'Tip头适配器 1250uL (Code: ZX-58-1250)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_Tip300_Adapter:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_Tip300_Adapter
type: pylabrobot
description: 'ZHONGXI 适配器 300uL (Code: ZX-58-300)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -1,10 +1,130 @@
prcxi_96_wellplate_360ul_flat:
category:
- plates
PRCXI_48_DeepWell:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_res:prcxi_96_wellplate_360ul_flat
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_48_DeepWell
type: pylabrobot
description: prcxi_96_wellplate_360ul_flat
description: '48孔深孔板 (Code: 22)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_96_DeepWell:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_96_DeepWell
type: pylabrobot
description: '96深孔板 (Code: q2)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_AGenBio_4_troughplate:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_AGenBio_4_troughplate
type: pylabrobot
description: '4道储液槽 (Code: sdfrth654)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_BioER_96_wellplate:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_BioER_96_wellplate
type: pylabrobot
description: '2.2ml 深孔板 (Code: ZX-019-2.2)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_BioRad_384_wellplate:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_BioRad_384_wellplate
type: pylabrobot
description: '384板 (Code: q3)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_CellTreat_96_wellplate:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_CellTreat_96_wellplate
type: pylabrobot
description: '细菌培养皿 (Code: ZX-78-096)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_PCR_Plate_200uL_nonskirted:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_PCR_Plate_200uL_nonskirted
type: pylabrobot
description: '0.2ml PCR 板 (Code: ZX-023-0.2)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_PCR_Plate_200uL_semiskirted:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_PCR_Plate_200uL_semiskirted
type: pylabrobot
description: '0.2ml PCR 板 (Code: ZX-023-0.2)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_PCR_Plate_200uL_skirted:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_PCR_Plate_200uL_skirted
type: pylabrobot
description: '0.2ml PCR 板 (Code: ZX-023-0.2)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_nest_12_troughplate:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_nest_12_troughplate
type: pylabrobot
description: '12道储液槽 (Code: 12道储液槽)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_nest_1_troughplate:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_nest_1_troughplate
type: pylabrobot
description: '储液槽 (Code: ZX-58-10000)'
handles: []
icon: ''
init_param_schema: {}

View File

@@ -1,23 +1,70 @@
prcxi_opentrons_96_tiprack_10ul:
category:
- tip_racks
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_res:prcxi_opentrons_96_tiprack_10ul
type: pylabrobot
description: prcxi_opentrons_96_tiprack_10ul
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
tip_adaptor_1250ul_2:
category:
PRCXI_1000uL_Tips:
category:
- prcxi
- tip_racks
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_materials:tip_adaptor_1250ul
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_1000uL_Tips
type: pylabrobot
description: Tip头适配器 1250uL
description: '1000μL Tip头 (Code: ZX-001-1000)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_10uL_Tips:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_10uL_Tips
type: pylabrobot
description: '10μL Tip头 (Code: ZX-001-10)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_10ul_eTips:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_10ul_eTips
type: pylabrobot
description: '10μL加长 Tip头 (Code: ZX-001-10+)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_1250uL_Tips:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_1250uL_Tips
type: pylabrobot
description: '1250μL Tip头 (Code: ZX-001-1250)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_200uL_Tips:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_200uL_Tips
type: pylabrobot
description: '200μL Tip头 (Code: ZX-001-200)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
PRCXI_300ul_Tips:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_300ul_Tips
type: pylabrobot
description: '300μL Tip头 (Code: ZX-001-300)'
handles: []
icon: ''
init_param_schema: {}

View File

@@ -1,10 +1,10 @@
prcxi_trash:
category:
- trash
PRCXI_trash:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_res:prcxi_trash
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_trash
type: pylabrobot
description: prcxi_trash
description: '废弃槽 (Code: q1)'
handles: []
icon: ''
init_param_schema: {}

View File

@@ -0,0 +1,12 @@
PRCXI_EP_Adapter:
category:
- prcxi
class:
module: unilabos.devices.liquid_handling.prcxi.prcxi_labware:PRCXI_EP_Adapter
type: pylabrobot
description: 'ep适配器 (Code: 1)'
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -284,10 +284,18 @@ def modify_to_backend_format(data: list[dict[str, Any]]) -> list[dict[str, Any]]
edge["sourceHandle"] = port[source]
elif "source_port" in edge:
edge["sourceHandle"] = edge.pop("source_port")
else:
typ = edge.get("type")
if typ == "communication":
continue
if target in port:
edge["targetHandle"] = port[target]
elif "target_port" in edge:
edge["targetHandle"] = edge.pop("target_port")
else:
typ = edge.get("type")
if typ == "communication":
continue
edge["id"] = f"reactflow__edge-{source}-{edge['sourceHandle']}-{target}-{edge['targetHandle']}"
for key in ["source_port", "target_port"]:
if key in edge:

View File

@@ -4,7 +4,11 @@ def register():
# noinspection PyUnresolvedReferences
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Deck
# noinspection PyUnresolvedReferences
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Container
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Plate
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300PlateAdapter
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300TipRack
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Trash
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300TubeRack
# noinspection PyUnresolvedReferences
from unilabos.devices.workstation.workstation_base import WorkStationContainer

View File

@@ -69,7 +69,7 @@
"children": [],
"parent": "YugongStation",
"type": "device",
"class": "syringepump.runze",
"class": "syringe_pump_with_valve.runze.SY03B-T08",
"position": {
"x": 620.6111111111111,
"y": 171,
@@ -93,7 +93,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 430.4087301587302,
"y": 428,
@@ -117,7 +117,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 295.36944444444447,
"y": 428,
@@ -141,7 +141,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 165.36944444444444,
"y": 428,
@@ -165,7 +165,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 165.36944444444444,
"y": 428,
@@ -189,7 +189,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 35,
"y": 428,
@@ -213,7 +213,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 698.1111111111111,
"y": 428,
@@ -255,7 +255,7 @@
"children": [],
"parent": "YugongStation",
"type": "device",
"class": "syringepump.runze",
"class": "syringe_pump_with_valve.runze.SY03B-T08",
"position": {
"x": 1195.611507936508,
"y": 686,
@@ -279,7 +279,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 1587.703373015873,
"y": 1172.5,
@@ -299,7 +299,7 @@
"children": [],
"parent": "YugongStation",
"type": "device",
"class": "separator_controller",
"class": "separator.homemade",
"position": {
"x": 1624.4027777777778,
"y": 665.5,
@@ -320,7 +320,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 1614.404365079365,
"y": 948,
@@ -340,7 +340,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 1915.7035714285714,
"y": 665.5,
@@ -360,7 +360,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 1785.7035714285714,
"y": 665.5,
@@ -384,7 +384,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 2054.0650793650793,
"y": 665.5,
@@ -408,7 +408,7 @@
"children": [],
"parent": "YugongStation",
"type": "device",
"class": "syringepump.runze",
"class": "syringe_pump_with_valve.runze.SY03B-T08",
"position": {
"x": 1630.6527777777778,
"y": 448.5,
@@ -432,7 +432,7 @@
"children": [],
"parent": "YugongStation",
"type": "device",
"class": "rotavap",
"class": "rotavap.one",
"position": {
"x": 1339.7031746031746,
"y": 968.5,
@@ -453,7 +453,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 1339.7031746031746,
"y": 1152,
@@ -473,7 +473,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 909.722619047619,
"y": 948,
@@ -493,7 +493,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 867.972619047619,
"y": 1152,
@@ -513,7 +513,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 742.722619047619,
"y": 948,
@@ -533,7 +533,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 1206.722619047619,
"y": 948,
@@ -553,7 +553,7 @@
"children": [],
"parent": "YugongStation",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 1148.222619047619,
"y": 1152,
@@ -573,7 +573,7 @@
"children": [],
"parent": "YugongStation",
"type": "device",
"class": "syringepump.runze",
"class": "syringe_pump_with_valve.runze.SY03B-T08",
"position": {
"x": 1469.7031746031746,
"y": 968.5,

View File

@@ -0,0 +1,34 @@
{
"nodes": [
{
"id": "ChinWeStation",
"name": "分液工作站",
"children": [],
"parent": null,
"type": "device",
"class": "separator.chinwe",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"port": "192.168.31.13:8899",
"baudrate": 9600,
"pump_ids": [
1,
2,
3
],
"motor_ids": [
4,
5
],
"sensor_id": 6,
"sensor_threshold": 300
},
"data": {}
}
],
"links": []
}

View File

@@ -178,7 +178,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300TipRack",
"size_x": 50,
"size_y": 40,
"size_z": 30,
@@ -4248,7 +4248,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 40,
"size_z": 30,
@@ -9415,7 +9415,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 40,
"size_z": 30,
@@ -13389,7 +13389,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 40,
"size_z": 30,
@@ -17363,7 +17363,7 @@
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"type": "PRCXI9300Plate",
"size_x": 50,
"size_y": 40,
"size_z": 30,

View File

@@ -111,7 +111,7 @@ def upload_workflow(
if result.get("code") == 0:
data = result.get("data", {})
print_status("工作流上传成功!", "success")
print_status(f"工作流上传成功!{data}", "success")
print_status(f" - UUID: {data.get('uuid', 'N/A')}", "info")
print_status(f" - 名称: {data.get('name', 'N/A')}", "info")
else: