mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-19 05:51:17 +00:00
* Cleanup registry to be easy-understanding (#76) * delete deprecated mock devices * rename categories * combine chromatographic devices * rename rviz simulation nodes * organic virtual devices * parse vessel_id * run registry completion before merge --------- Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com> * fix: workstation handlers and vessel_id parsing * fix: working dir error when input config path feat: report publish topic when error * modify default discovery_interval to 15s * feat: add trace log level * feat: 添加ChinWe设备控制类,支持串口通信和电机控制功能 (#79) * fix: drop_tips not using auto resource select * fix: discard_tips error * fix: discard_tips * fix: prcxi_res * add: prcxi res fix: startup slow * feat: workstation example * fix pumps and liquid_handler handle * feat: 优化protocol node节点运行日志 * fix all protocol_compilers and remove deprecated devices * feat: 新增use_remote_resource参数 * fix and remove redundant info * bugfixes on organic protocols * fix filter protocol * fix protocol node * 临时兼容错误的driver写法 * fix: prcxi import error * use call_async in all service to avoid deadlock * fix: figure_resource * Update recipe.yaml * add workstation template and battery example * feat: add sk & ak * update workstation base * Create workstation_architecture.md * refactor: workstation_base 重构为仅含业务逻辑,通信和子设备管理交给 ProtocolNode * refactor: ProtocolNode→WorkstationNode * Add:msgs.action (#83) * update: Workstation dev 将版本号从 0.10.3 更新为 0.10.4 (#84) * Add:msgs.action * update: 将版本号从 0.10.3 更新为 0.10.4 * simplify resource system * uncompleted refactor * example for use WorkstationBase * feat: websocket * feat: websocket test * feat: workstation example * feat: action status * fix: station自己的方法注册错误 * fix: 还原protocol node处理方法 * fix: build * fix: missing job_id key * ws test version 1 * ws test version 2 * ws protocol * 增加物料关系上传日志 * 增加物料关系上传日志 * 修正物料关系上传 * 修复工站的tracker实例追踪失效问题 * 增加handle检测,增加material edge关系上传 * 修复event loop错误 * 修复edge上报错误 * 修复async错误 * 更新schema的title字段 * 主机节点信息等支持自动刷新 * 注册表编辑器 * 修复status密集发送时,消息出错 * 增加addr参数 * fix: addr param * fix: addr param * 取消labid 和 强制config输入 * Add action definitions for LiquidHandlerSetGroup and LiquidHandlerTransferGroup - Created LiquidHandlerSetGroup.action with fields for group name, wells, and volumes. - Created LiquidHandlerTransferGroup.action with fields for source and target group names and unit volume. - Both actions include response fields for return information and success status. * Add LiquidHandlerSetGroup and LiquidHandlerTransferGroup actions to CMakeLists * Add set_group and transfer_group methods to PRCXI9300Handler and update liquid_handler.yaml * result_info改为字典类型 * 新增uat的地址替换 * runze multiple pump support (cherry picked from commit49354fcf39) * remove runze multiple software obtainer (cherry picked from commit8bcc92a394) * support multiple backbone (cherry picked from commit4771ff2347) * Update runze pump format * Correct runze multiple backbone * Update runze_multiple_backbone * Correct runze pump multiple receive method. * Correct runze pump multiple receive method. * 对于PRCXI9320的transfer_group,一对多和多对多 * 移除MQTT,更新launch文档,提供注册表示例文件,更新到0.10.5 * fix import error * fix dupe upload registry * refactor ws client * add server timeout * Fix: run-column with correct vessel id (#86) * fix run_column * Update run_column_protocol.py (cherry picked from commite5aa4d940a) * resource_update use resource_add * 新增版位推荐功能 * 重新规定了版位推荐的入参 * update registry with nested obj * fix protocol node log_message, added create_resource return value * fix protocol node log_message, added create_resource return value * try fix add protocol * fix resource_add * 修复移液站错误的aspirate注册表 * Feature/xprbalance-zhida (#80) * feat(devices): add Zhida GC/MS pretreatment automation workstation * feat(devices): add mettler_toledo xpr balance * balance * 重新补全zhida注册表 * PRCXI9320 json * PRCXI9320 json * PRCXI9320 json * fix resource download * remove class for resource * bump version to 0.10.6 * 更新所有注册表 * 修复protocolnode的兼容性 * 修复protocolnode的兼容性 * Update install md * Add Defaultlayout * 更新物料接口 * fix dict to tree/nested-dict converter * coin_cell_station draft * refactor: rename "station_resource" to "deck" * add standardized BIOYOND resources: bottle_carrier, bottle * refactor and add BIOYOND resources tests * add BIOYOND deck assignment and pass all tests * fix: update resource with correct structure; remove deprecated liquid_handler set_group action * feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92) * feat: 新威电池测试系统驱动与注册文件 * feat: bring neware driver & battery.json into workstation_dev_YB2 * add bioyond studio draft * bioyond station with communication init and resource sync * fix bioyond station and registry * fix: update resource with correct structure; remove deprecated liquid_handler set_group action * frontend_docs * create/update resources with POST/PUT for big amount/ small amount data * create/update resources with POST/PUT for big amount/ small amount data * refactor: add itemized_carrier instead of carrier consists of ResourceHolder * create warehouse by factory func * update bioyond launch json * add child_size for itemized_carrier * fix bioyond resource io * Workstation templates: Resources and its CRUD, and workstation tasks (#95) * coin_cell_station draft * refactor: rename "station_resource" to "deck" * add standardized BIOYOND resources: bottle_carrier, bottle * refactor and add BIOYOND resources tests * add BIOYOND deck assignment and pass all tests * fix: update resource with correct structure; remove deprecated liquid_handler set_group action * feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92) * feat: 新威电池测试系统驱动与注册文件 * feat: bring neware driver & battery.json into workstation_dev_YB2 * add bioyond studio draft * bioyond station with communication init and resource sync * fix bioyond station and registry * create/update resources with POST/PUT for big amount/ small amount data * refactor: add itemized_carrier instead of carrier consists of ResourceHolder * create warehouse by factory func * update bioyond launch json * add child_size for itemized_carrier * fix bioyond resource io --------- Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com> Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com> * 更新物料接口 * Workstation dev yb2 (#100) * Refactor and extend reaction station action messages * Refactor dispensing station tasks to enhance parameter clarity and add batch processing capabilities - Updated `create_90_10_vial_feeding_task` to include detailed parameters for 90%/10% vial feeding, improving clarity and usability. - Introduced `create_batch_90_10_vial_feeding_task` for batch processing of 90%/10% vial feeding tasks with JSON formatted input. - Added `create_batch_diamine_solution_task` for batch preparation of diamine solution, also utilizing JSON formatted input. - Refined `create_diamine_solution_task` to include additional parameters for better task configuration. - Enhanced schema descriptions and default values for improved user guidance. * 修复to_plr_resources * add update remove * 支持选择器注册表自动生成 支持转运物料 * 修复资源添加 * 修复transfer_resource_to_another生成 * 更新transfer_resource_to_another参数,支持spot入参 * 新增test_resource动作 * fix host_node error * fix host_node test_resource error * fix host_node test_resource error * 过滤本地动作 * 移动内部action以兼容host node * 修复同步任务报错不显示的bug * feat: 允许返回非本节点物料,后面可以通过decoration进行区分,就不进行warning了 * update todo * modify bioyond/plr converter, bioyond resource registry, and tests * pass the tests * update todo * add conda-pack-build.yml * add auto install script for conda-pack-build.yml (cherry picked from commit172599adcf) * update conda-pack-build.yml * update conda-pack-build.yml * update conda-pack-build.yml * update conda-pack-build.yml * update conda-pack-build.yml * Add version in __init__.py Update conda-pack-build.yml Add create_zip_archive.py * Update conda-pack-build.yml * Update conda-pack-build.yml (with mamba) * Update conda-pack-build.yml * Fix FileNotFoundError * Try fix 'charmap' codec can't encode characters in position 16-23: character maps to <undefined> * Fix unilabos msgs search error * Fix environment_check.py * Update recipe.yaml * Update registry. Update uuid loop figure method. Update install docs. * Fix nested conda pack * Fix one-key installation path error * Bump version to 0.10.7 * Workshop bj (#99) * Add LaiYu Liquid device integration and tests Introduce LaiYu Liquid device implementation, including backend, controllers, drivers, configuration, and resource files. Add hardware connection, tip pickup, and simplified test scripts, as well as experiment and registry configuration for LaiYu Liquid. Documentation and .gitignore for the device are also included. * feat(LaiYu_Liquid): 重构设备模块结构并添加硬件文档 refactor: 重新组织LaiYu_Liquid模块目录结构 docs: 添加SOPA移液器和步进电机控制指令文档 fix: 修正设备配置中的最大体积默认值 test: 新增工作台配置测试用例 chore: 删除过时的测试脚本和配置文件 * add * 重构: 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py 并更新所有导入引用 - 使用 git mv 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py - 更新所有相关文件中的导入引用 - 保持代码功能不变,仅改善命名一致性 - 测试确认所有导入正常工作 * 修复: 在 core/__init__.py 中添加 LaiYuLiquidBackend 导出 - 添加 LaiYuLiquidBackend 到导入列表 - 添加 LaiYuLiquidBackend 到 __all__ 导出列表 - 确保所有主要类都可以正确导入 * 修复大小写文件夹名字 * 电池装配工站二次开发教程(带目录)上传至dev (#94) * 电池装配工站二次开发教程 * Update intro.md * 物料教程 * 更新物料教程,json格式注释 * Update prcxi driver & fix transfer_liquid mix_times (#90) * Update prcxi driver & fix transfer_liquid mix_times * fix: correct mix_times type * Update liquid_handler registry * test: prcxi.py * Update registry from pr * fix ony-key script not exist * clean files --------- Co-authored-by: Junhan Chang <changjh@dp.tech> Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com> Co-authored-by: Guangxin Zhang <guangxin.zhang.bio@gmail.com> Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com> Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com> Co-authored-by: LccLink <1951855008@qq.com> Co-authored-by: lixinyu1011 <61094742+lixinyu1011@users.noreply.github.com> Co-authored-by: shiyubo0410 <shiyubo@dp.tech>
1822 lines
66 KiB
Python
1822 lines
66 KiB
Python
import asyncio
|
||
import collections
|
||
import contextlib
|
||
import json
|
||
import os
|
||
import socket
|
||
import time
|
||
from typing import Any, List, Dict, Optional, Tuple, TypedDict, Union, Sequence, Iterator, Literal
|
||
|
||
from pylabrobot.liquid_handling import (
|
||
LiquidHandlerBackend,
|
||
Pickup,
|
||
SingleChannelAspiration,
|
||
Drop,
|
||
SingleChannelDispense,
|
||
PickupTipRack,
|
||
DropTipRack,
|
||
MultiHeadAspirationPlate,
|
||
ChatterBoxBackend,
|
||
LiquidHandlerChatterboxBackend,
|
||
)
|
||
from pylabrobot.liquid_handling.standard import (
|
||
MultiHeadAspirationContainer,
|
||
MultiHeadDispenseContainer,
|
||
MultiHeadDispensePlate,
|
||
ResourcePickup,
|
||
ResourceMove,
|
||
ResourceDrop,
|
||
)
|
||
from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash
|
||
|
||
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
|
||
|
||
|
||
class PRCXIError(RuntimeError):
|
||
"""Lilith 返回 Success=false 时抛出的业务异常"""
|
||
|
||
|
||
class Material(TypedDict): # 和Plate同关系
|
||
uuid: str
|
||
Code: Optional[str]
|
||
Name: Optional[str]
|
||
SummaryName: Optional[str]
|
||
PipetteHeight: Optional[int]
|
||
materialEnum: Optional[int]
|
||
|
||
|
||
class WorkTablets(TypedDict):
|
||
Number: int
|
||
Code: str
|
||
Material: Dict[str, Any]
|
||
|
||
|
||
class MatrixInfo(TypedDict):
|
||
MatrixId: str
|
||
MatrixName: str
|
||
MatrixCount: int
|
||
WorkTablets: list[WorkTablets]
|
||
|
||
|
||
class PRCXI9300Deck(Deck):
|
||
"""PRCXI 9300 的专用 Deck 类,继承自 Deck。
|
||
|
||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||
"""
|
||
|
||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float):
|
||
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 的工作台布局和槽位信息。
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
name: str,
|
||
size_x: float,
|
||
size_y: float,
|
||
size_z: float,
|
||
category: str,
|
||
ordering: collections.OrderedDict,
|
||
model: Optional[str] = None,
|
||
):
|
||
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
|
||
self._unilabos_state = {}
|
||
|
||
def load_state(self, state: Dict[str, Any]) -> None:
|
||
"""从给定的状态加载工作台信息。"""
|
||
super().load_state(state)
|
||
self._unilabos_state = state
|
||
|
||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||
data = super().serialize_state()
|
||
data.update(self._unilabos_state)
|
||
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):
|
||
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)
|
||
self._unilabos_state = {}
|
||
|
||
def load_state(self, state: Dict[str, Any]) -> None:
|
||
"""从给定的状态加载工作台信息。"""
|
||
# super().load_state(state)
|
||
self._unilabos_state = state
|
||
|
||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||
data = super().serialize_state()
|
||
data.update(self._unilabos_state)
|
||
return data
|
||
|
||
|
||
class PRCXI9300Handler(LiquidHandlerAbstract):
|
||
support_touch_tip = False
|
||
|
||
@property
|
||
def reset_ok(self) -> bool:
|
||
"""检查设备是否已重置成功。"""
|
||
if self._unilabos_backend.debug:
|
||
return True
|
||
return self._unilabos_backend.is_reset_ok
|
||
|
||
def __init__(
|
||
self,
|
||
deck: Deck,
|
||
host: str,
|
||
port: int,
|
||
timeout: float,
|
||
channel_num=8,
|
||
axis="Left",
|
||
setup=True,
|
||
debug=False,
|
||
simulator=False,
|
||
matrix_id="",
|
||
is_9320=False,
|
||
):
|
||
tablets_info = []
|
||
count = 0
|
||
for child in deck.children:
|
||
if "Material" in child._unilabos_state:
|
||
count += 1
|
||
tablets_info.append(
|
||
WorkTablets(Number=count, Code=f"T{count}", Material=child._unilabos_state["Material"])
|
||
)
|
||
if is_9320:
|
||
print("当前设备是9320")
|
||
self._unilabos_backend = PRCXI9300Backend(
|
||
tablets_info, host, port, timeout, channel_num, axis, setup, debug, matrix_id, is_9320
|
||
)
|
||
super().__init__(backend=self._unilabos_backend, deck=deck, simulator=simulator, channel_num=channel_num)
|
||
|
||
def set_liquid(self, wells: list[Well], liquid_names: list[str], volumes: list[float]):
|
||
return super().set_liquid(wells, liquid_names, volumes)
|
||
|
||
def set_group(self, group_name: str, wells: List[Well], volumes: List[float]):
|
||
return super().set_group(group_name, wells, volumes)
|
||
|
||
async def transfer_group(self, source_group_name: str, target_group_name: str, unit_volume: float):
|
||
return await super().transfer_group(source_group_name, target_group_name, unit_volume)
|
||
|
||
async def create_protocol(
|
||
self,
|
||
protocol_name: str = "",
|
||
protocol_description: str = "",
|
||
protocol_version: str = "",
|
||
protocol_author: str = "",
|
||
protocol_date: str = "",
|
||
protocol_type: str = "",
|
||
none_keys: List[str] = [],
|
||
):
|
||
self._unilabos_backend.create_protocol(protocol_name)
|
||
|
||
async def run_protocol(self):
|
||
return self._unilabos_backend.run_protocol()
|
||
|
||
async def remove_liquid(
|
||
self,
|
||
vols: List[float],
|
||
sources: Sequence[Container],
|
||
waste_liquid: Optional[Container] = None,
|
||
*,
|
||
use_channels: Optional[List[int]] = None,
|
||
flow_rates: Optional[List[Optional[float]]] = None,
|
||
offsets: Optional[List[Coordinate]] = None,
|
||
liquid_height: Optional[List[Optional[float]]] = None,
|
||
blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
||
spread: Optional[Literal["wide", "tight", "custom"]] = "wide",
|
||
delays: Optional[List[int]] = None,
|
||
is_96_well: Optional[bool] = False,
|
||
top: Optional[List[float]] = None,
|
||
none_keys: List[str] = [],
|
||
):
|
||
return await super().remove_liquid(
|
||
vols,
|
||
sources,
|
||
waste_liquid,
|
||
use_channels=use_channels,
|
||
flow_rates=flow_rates,
|
||
offsets=offsets,
|
||
liquid_height=liquid_height,
|
||
blow_out_air_volume=blow_out_air_volume,
|
||
spread=spread,
|
||
delays=delays,
|
||
is_96_well=is_96_well,
|
||
top=top,
|
||
none_keys=none_keys,
|
||
)
|
||
|
||
async def add_liquid(
|
||
self,
|
||
asp_vols: Union[List[float], float],
|
||
dis_vols: Union[List[float], float],
|
||
reagent_sources: Sequence[Container],
|
||
targets: Sequence[Container],
|
||
*,
|
||
use_channels: Optional[List[int]] = None,
|
||
flow_rates: Optional[List[Optional[float]]] = None,
|
||
offsets: Optional[List[Coordinate]] = None,
|
||
liquid_height: Optional[List[Optional[float]]] = None,
|
||
blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
||
spread: Optional[Literal["wide", "tight", "custom"]] = "wide",
|
||
is_96_well: bool = False,
|
||
delays: Optional[List[int]] = None,
|
||
mix_time: Optional[int] = None,
|
||
mix_vol: Optional[int] = None,
|
||
mix_rate: Optional[int] = None,
|
||
mix_liquid_height: Optional[float] = None,
|
||
none_keys: List[str] = [],
|
||
):
|
||
return await super().add_liquid(
|
||
asp_vols,
|
||
dis_vols,
|
||
reagent_sources,
|
||
targets,
|
||
use_channels=use_channels,
|
||
flow_rates=flow_rates,
|
||
offsets=offsets,
|
||
liquid_height=liquid_height,
|
||
blow_out_air_volume=blow_out_air_volume,
|
||
spread=spread,
|
||
is_96_well=is_96_well,
|
||
delays=delays,
|
||
mix_time=mix_time,
|
||
mix_vol=mix_vol,
|
||
mix_rate=mix_rate,
|
||
mix_liquid_height=mix_liquid_height,
|
||
none_keys=none_keys,
|
||
)
|
||
|
||
async def transfer_liquid(
|
||
self,
|
||
sources: Sequence[Container],
|
||
targets: Sequence[Container],
|
||
tip_racks: Sequence[TipRack],
|
||
*,
|
||
use_channels: Optional[List[int]] = None,
|
||
asp_vols: Union[List[float], float],
|
||
dis_vols: Union[List[float], float],
|
||
asp_flow_rates: Optional[List[Optional[float]]] = None,
|
||
dis_flow_rates: Optional[List[Optional[float]]] = None,
|
||
offsets: Optional[List[Coordinate]] = None,
|
||
touch_tip: bool = False,
|
||
liquid_height: Optional[List[Optional[float]]] = None,
|
||
blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
||
spread: Literal["wide", "tight", "custom"] = "wide",
|
||
is_96_well: bool = False,
|
||
mix_stage: Optional[Literal["none", "before", "after", "both"]] = "none",
|
||
mix_times: Optional[List[int]] = None,
|
||
mix_vol: Optional[int] = None,
|
||
mix_rate: Optional[int] = None,
|
||
mix_liquid_height: Optional[float] = None,
|
||
delays: Optional[List[int]] = None,
|
||
none_keys: List[str] = [],
|
||
):
|
||
return await super().transfer_liquid(
|
||
sources,
|
||
targets,
|
||
tip_racks,
|
||
use_channels=use_channels,
|
||
asp_vols=asp_vols,
|
||
dis_vols=dis_vols,
|
||
asp_flow_rates=asp_flow_rates,
|
||
dis_flow_rates=dis_flow_rates,
|
||
offsets=offsets,
|
||
touch_tip=touch_tip,
|
||
liquid_height=liquid_height,
|
||
blow_out_air_volume=blow_out_air_volume,
|
||
spread=spread,
|
||
is_96_well=is_96_well,
|
||
mix_stage=mix_stage,
|
||
mix_times=mix_times,
|
||
mix_vol=mix_vol,
|
||
mix_rate=mix_rate,
|
||
mix_liquid_height=mix_liquid_height,
|
||
delays=delays,
|
||
none_keys=none_keys,
|
||
)
|
||
|
||
async def custom_delay(self, seconds=0, msg=None):
|
||
return await super().custom_delay(seconds, msg)
|
||
|
||
async def touch_tip(self, targets: Sequence[Container]):
|
||
return await super().touch_tip(targets)
|
||
|
||
async def mix(
|
||
self,
|
||
targets: Sequence[Container],
|
||
mix_time: int = None,
|
||
mix_vol: Optional[int] = None,
|
||
height_to_bottom: Optional[float] = None,
|
||
offsets: Optional[Coordinate] = None,
|
||
mix_rate: Optional[float] = None,
|
||
none_keys: List[str] = [],
|
||
):
|
||
return await self._unilabos_backend.mix(
|
||
targets, mix_time, mix_vol, height_to_bottom, offsets, mix_rate, none_keys
|
||
)
|
||
|
||
def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]:
|
||
return super().iter_tips(tip_racks)
|
||
|
||
async def pick_up_tips(
|
||
self,
|
||
tip_spots: List[TipSpot],
|
||
use_channels: Optional[List[int]] = None,
|
||
offsets: Optional[List[Coordinate]] = None,
|
||
**backend_kwargs,
|
||
):
|
||
return await super().pick_up_tips(tip_spots, use_channels, offsets, **backend_kwargs)
|
||
|
||
async def aspirate(
|
||
self,
|
||
resources: Sequence[Container],
|
||
vols: List[float],
|
||
use_channels: Optional[List[int]] = None,
|
||
flow_rates: Optional[List[Optional[float]]] = None,
|
||
offsets: Optional[List[Coordinate]] = None,
|
||
liquid_height: Optional[List[Optional[float]]] = None,
|
||
blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
||
spread: Literal["wide", "tight", "custom"] = "wide",
|
||
**backend_kwargs,
|
||
):
|
||
|
||
return await super().aspirate(
|
||
resources,
|
||
vols,
|
||
use_channels,
|
||
flow_rates,
|
||
offsets,
|
||
liquid_height,
|
||
blow_out_air_volume,
|
||
spread,
|
||
**backend_kwargs,
|
||
)
|
||
|
||
async def drop_tips(
|
||
self,
|
||
tip_spots: Sequence[Union[TipSpot, Trash]],
|
||
use_channels: Optional[List[int]] = None,
|
||
offsets: Optional[List[Coordinate]] = None,
|
||
allow_nonzero_volume: bool = False,
|
||
**backend_kwargs,
|
||
):
|
||
return await super().drop_tips(tip_spots, use_channels, offsets, allow_nonzero_volume, **backend_kwargs)
|
||
|
||
async def dispense(
|
||
self,
|
||
resources: Sequence[Container],
|
||
vols: List[float],
|
||
use_channels: Optional[List[int]] = None,
|
||
flow_rates: Optional[List[Optional[float]]] = None,
|
||
offsets: Optional[List[Coordinate]] = None,
|
||
liquid_height: Optional[List[Optional[float]]] = None,
|
||
blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
||
spread: Literal["wide", "tight", "custom"] = "wide",
|
||
**backend_kwargs,
|
||
):
|
||
return await super().dispense(
|
||
resources,
|
||
vols,
|
||
use_channels,
|
||
flow_rates,
|
||
offsets,
|
||
liquid_height,
|
||
blow_out_air_volume,
|
||
spread,
|
||
**backend_kwargs,
|
||
)
|
||
|
||
async def discard_tips(
|
||
self,
|
||
use_channels: Optional[List[int]] = None,
|
||
allow_nonzero_volume: bool = True,
|
||
offsets: Optional[List[Coordinate]] = None,
|
||
**backend_kwargs,
|
||
):
|
||
return await super().discard_tips(use_channels, allow_nonzero_volume, offsets, **backend_kwargs)
|
||
|
||
def set_tiprack(self, tip_racks: Sequence[TipRack]):
|
||
super().set_tiprack(tip_racks)
|
||
|
||
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。
|
||
|
||
该类提供了与 PRCXI 9300 设备进行通信的基本方法,包括方案管理、自动化控制、运行状态查询等。
|
||
"""
|
||
|
||
_num_channels = 8 # 默认通道数为 8
|
||
_is_reset_ok = False
|
||
|
||
@property
|
||
def is_reset_ok(self) -> bool:
|
||
self._is_reset_ok = self.api_client.get_reset_status()
|
||
return self._is_reset_ok
|
||
|
||
matrix_info: MatrixInfo
|
||
protocol_name: str
|
||
steps_todo_list = []
|
||
|
||
def __init__(
|
||
self,
|
||
tablets_info: list[WorkTablets],
|
||
host: str = "127.0.0.1",
|
||
port: int = 9999,
|
||
timeout: float = 10.0,
|
||
channel_num: int = 8,
|
||
axis: str = "Left",
|
||
setup=True,
|
||
debug=False,
|
||
matrix_id="",
|
||
is_9320=False,
|
||
) -> None:
|
||
super().__init__()
|
||
self.tablets_info = tablets_info
|
||
self.matrix_id = matrix_id
|
||
self.api_client = PRCXI9300Api(host, port, timeout, axis, debug, is_9320)
|
||
self.host, self.port, self.timeout = host, port, timeout
|
||
self._num_channels = channel_num
|
||
self._execute_setup = setup
|
||
self.debug = debug
|
||
|
||
def create_protocol(self, protocol_name):
|
||
self.protocol_name = protocol_name
|
||
self.steps_todo_list = []
|
||
|
||
def run_protocol(self):
|
||
assert self.is_reset_ok, "PRCXI9300Backend is not reset successfully. Please call setup() first."
|
||
run_time = time.time()
|
||
self.matrix_info = MatrixInfo(
|
||
MatrixId=f"{int(run_time)}",
|
||
MatrixName=f"protocol_{run_time}",
|
||
MatrixCount=len(self.tablets_info),
|
||
WorkTablets=self.tablets_info,
|
||
)
|
||
# print(json.dumps(self.matrix_info, indent=2))
|
||
if not len(self.matrix_id):
|
||
res = self.api_client.add_WorkTablet_Matrix(self.matrix_info)
|
||
assert res["Success"], f"Failed to create matrix: {res.get('Message', 'Unknown error')}"
|
||
print(f"PRCXI9300Backend created matrix with ID: {self.matrix_info['MatrixId']}, result: {res}")
|
||
solution_id = self.api_client.add_solution(
|
||
f"protocol_{run_time}", self.matrix_info["MatrixId"], self.steps_todo_list
|
||
)
|
||
else:
|
||
print(f"PRCXI9300Backend using predefined worktable {self.matrix_id}, skipping matrix creation.")
|
||
solution_id = self.api_client.add_solution(f"protocol_{run_time}", self.matrix_id, self.steps_todo_list)
|
||
print(f"PRCXI9300Backend created solution with ID: {solution_id}")
|
||
self.api_client.load_solution(solution_id)
|
||
print(json.dumps(self.steps_todo_list, indent=2))
|
||
return self.api_client.start()
|
||
|
||
@classmethod
|
||
def check_channels(cls, use_channels: List[int]) -> List[int]:
|
||
"""检查通道是否符合要求,PRCXI9300Backend 只支持所有 8 个通道。"""
|
||
if use_channels != [0, 1, 2, 3, 4, 5, 6, 7]:
|
||
print("PRCXI9300Backend only supports all 8 channels, using default [0, 1, 2, 3, 4, 5, 6, 7].")
|
||
return [0, 1, 2, 3, 4, 5, 6, 7]
|
||
return use_channels
|
||
|
||
async def setup(self):
|
||
await super().setup()
|
||
try:
|
||
if self._execute_setup:
|
||
self.api_client.call("IAutomation", "Reset")
|
||
while not self.is_reset_ok:
|
||
print("Waiting for PRCXI9300 to reset...")
|
||
await asyncio.sleep(1)
|
||
print("PRCXI9300 reset successfully.")
|
||
except ConnectionRefusedError as e:
|
||
raise RuntimeError(
|
||
f"Failed to connect to PRCXI9300 API at {self.host}:{self.port}. "
|
||
"Please ensure the PRCXI9300 service is running."
|
||
) from e
|
||
|
||
async def stop(self):
|
||
self.api_client.call("IAutomation", "Stop")
|
||
|
||
async def pick_up_tips(self, ops: List[Pickup], use_channels: List[int] = None):
|
||
"""Pick up tips from the specified resource."""
|
||
|
||
plate_indexes = []
|
||
for op in ops:
|
||
plate = op.resource.parent
|
||
deck = plate.parent
|
||
plate_index = deck.children.index(plate)
|
||
# print(f"Plate index: {plate_index}, Plate name: {plate.name}")
|
||
# print(f"Number of children in deck: {len(deck.children)}")
|
||
|
||
plate_indexes.append(plate_index)
|
||
|
||
if len(set(plate_indexes)) != 1:
|
||
raise ValueError("All pickups must be from the same plate. Found different plates: " + str(plate_indexes))
|
||
|
||
tip_columns = []
|
||
for op in ops:
|
||
tipspot = op.resource
|
||
tipspot_index = tipspot.parent.children.index(tipspot)
|
||
tip_columns.append(tipspot_index // 8)
|
||
if len(set(tip_columns)) != 1:
|
||
raise ValueError("All pickups must be from the same tip column. Found different columns: " + str(tip_columns))
|
||
PlateNo = plate_indexes[0] + 1
|
||
hole_col = tip_columns[0] + 1
|
||
hole_row = 1
|
||
if self._num_channels == 1:
|
||
hole_row = tipspot_index % 8 + 1
|
||
|
||
step = self.api_client.Load(
|
||
dosage=0,
|
||
plate_no=PlateNo,
|
||
is_whole_plate=False,
|
||
hole_row=hole_row,
|
||
hole_col=hole_col,
|
||
blending_times=0,
|
||
balance_height=0,
|
||
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
|
||
hole_numbers=f"{(hole_col - 1) * 8 + hole_row}" if self._num_channels == 1 else "1,2,3,4,5",
|
||
)
|
||
self.steps_todo_list.append(step)
|
||
|
||
async def drop_tips(self, ops: List[Drop], use_channels: List[int] = None):
|
||
"""Pick up tips from the specified resource."""
|
||
|
||
# 检查trash #
|
||
if ops[0].resource.name == "trash":
|
||
|
||
PlateNo = ops[0].resource.parent.children.index(ops[0].resource) + 1
|
||
|
||
step = self.api_client.UnLoad(
|
||
dosage=0,
|
||
plate_no=PlateNo,
|
||
is_whole_plate=False,
|
||
hole_row=1,
|
||
hole_col=3,
|
||
blending_times=0,
|
||
balance_height=0,
|
||
plate_or_hole=f"H{1}-8,T{PlateNo}",
|
||
hole_numbers="1,2,3,4,5,6,7,8",
|
||
)
|
||
self.steps_todo_list.append(step)
|
||
return
|
||
# print(ops[0].resource.parent.children.index(ops[0].resource))
|
||
|
||
plate_indexes = []
|
||
for op in ops:
|
||
plate = op.resource.parent
|
||
deck = plate.parent
|
||
plate_index = deck.children.index(plate)
|
||
plate_indexes.append(plate_index)
|
||
if len(set(plate_indexes)) != 1:
|
||
raise ValueError(
|
||
"All drop_tips must be from the same plate. Found different plates: " + str(plate_indexes)
|
||
)
|
||
|
||
tip_columns = []
|
||
for op in ops:
|
||
tipspot = op.resource
|
||
tipspot_index = tipspot.parent.children.index(tipspot)
|
||
tip_columns.append(tipspot_index // 8)
|
||
if len(set(tip_columns)) != 1:
|
||
raise ValueError(
|
||
"All drop_tips must be from the same tip column. Found different columns: " + str(tip_columns)
|
||
)
|
||
|
||
PlateNo = plate_indexes[0] + 1
|
||
hole_col = tip_columns[0] + 1
|
||
|
||
if self.channel_num == 1:
|
||
hole_row = tipspot_index % 8 + 1
|
||
|
||
step = self.api_client.UnLoad(
|
||
dosage=0,
|
||
plate_no=PlateNo,
|
||
is_whole_plate=False,
|
||
hole_row=hole_row,
|
||
hole_col=hole_col,
|
||
blending_times=0,
|
||
balance_height=0,
|
||
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
|
||
hole_numbers="1,2,3,4,5,6,7,8",
|
||
)
|
||
self.steps_todo_list.append(step)
|
||
|
||
async def mix(
|
||
self,
|
||
targets: Sequence[Container],
|
||
mix_time: int = None,
|
||
mix_vol: Optional[int] = None,
|
||
height_to_bottom: Optional[float] = None,
|
||
offsets: Optional[Coordinate] = None,
|
||
mix_rate: Optional[float] = None,
|
||
none_keys: List[str] = [],
|
||
):
|
||
"""Mix liquid in the specified resources."""
|
||
|
||
plate_indexes = []
|
||
for op in targets:
|
||
deck = op.parent.parent
|
||
plate = op.parent
|
||
plate_index = deck.children.index(plate)
|
||
plate_indexes.append(plate_index)
|
||
|
||
if len(set(plate_indexes)) != 1:
|
||
raise ValueError("All pickups must be from the same plate. Found different plates: " + str(plate_indexes))
|
||
|
||
tip_columns = []
|
||
for op in targets:
|
||
tipspot_index = op.parent.children.index(op)
|
||
tip_columns.append(tipspot_index // 8)
|
||
|
||
if len(set(tip_columns)) != 1:
|
||
raise ValueError(
|
||
"All pickups must be from the same tip column. Found different columns: " + str(tip_columns)
|
||
)
|
||
|
||
PlateNo = plate_indexes[0] + 1
|
||
hole_col = tip_columns[0] + 1
|
||
hole_row = 1
|
||
if self.num_channels == 1:
|
||
hole_row = tipspot_index % 8 + 1
|
||
|
||
assert mix_time > 0
|
||
step = self.api_client.Blending(
|
||
dosage=mix_vol,
|
||
plate_no=PlateNo,
|
||
is_whole_plate=False,
|
||
hole_row=hole_row,
|
||
hole_col=hole_col,
|
||
blending_times=mix_time,
|
||
balance_height=0,
|
||
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
|
||
hole_numbers="1,2,3,4,5,6,7,8",
|
||
)
|
||
self.steps_todo_list.append(step)
|
||
|
||
async def aspirate(self, ops: List[SingleChannelAspiration], use_channels: List[int] = None):
|
||
"""Aspirate liquid from the specified resources."""
|
||
|
||
plate_indexes = []
|
||
for op in ops:
|
||
plate = op.resource.parent
|
||
deck = plate.parent
|
||
plate_index = deck.children.index(plate)
|
||
plate_indexes.append(plate_index)
|
||
|
||
if len(set(plate_indexes)) != 1:
|
||
raise ValueError("All pickups must be from the same plate. Found different plates: " + str(plate_indexes))
|
||
|
||
tip_columns = []
|
||
for op in ops:
|
||
tipspot = op.resource
|
||
tipspot_index = tipspot.parent.children.index(tipspot)
|
||
tip_columns.append(tipspot_index // 8)
|
||
|
||
if len(set(tip_columns)) != 1:
|
||
raise ValueError(
|
||
"All pickups must be from the same tip column. Found different columns: " + str(tip_columns)
|
||
)
|
||
|
||
volumes = [op.volume for op in ops]
|
||
if len(set(volumes)) != 1:
|
||
raise ValueError("All aspirate volumes must be the same. Found different volumes: " + str(volumes))
|
||
|
||
PlateNo = plate_indexes[0] + 1
|
||
hole_col = tip_columns[0] + 1
|
||
hole_row = 1
|
||
if self.num_channels == 1:
|
||
hole_row = tipspot_index % 8 + 1
|
||
|
||
step = self.api_client.Imbibing(
|
||
dosage=int(volumes[0]),
|
||
plate_no=PlateNo,
|
||
is_whole_plate=False,
|
||
hole_row=hole_row,
|
||
hole_col=hole_col,
|
||
blending_times=0,
|
||
balance_height=0,
|
||
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
|
||
hole_numbers="1,2,3,4,5,6,7,8",
|
||
)
|
||
self.steps_todo_list.append(step)
|
||
|
||
async def dispense(self, ops: List[SingleChannelDispense], use_channels: List[int] = None):
|
||
"""Dispense liquid into the specified resources."""
|
||
|
||
plate_indexes = []
|
||
for op in ops:
|
||
plate = op.resource.parent
|
||
deck = plate.parent
|
||
plate_index = deck.children.index(plate)
|
||
plate_indexes.append(plate_index)
|
||
|
||
if len(set(plate_indexes)) != 1:
|
||
raise ValueError("All dispense must be from the same plate. Found different plates: " + str(plate_indexes))
|
||
|
||
tip_columns = []
|
||
for op in ops:
|
||
tipspot = op.resource
|
||
tipspot_index = tipspot.parent.children.index(tipspot)
|
||
tip_columns.append(tipspot_index // 8)
|
||
|
||
if len(set(tip_columns)) != 1:
|
||
raise ValueError(
|
||
"All dispense must be from the same tip column. Found different columns: " + str(tip_columns)
|
||
)
|
||
|
||
volumes = [op.volume for op in ops]
|
||
if len(set(volumes)) != 1:
|
||
raise ValueError("All dispense volumes must be the same. Found different volumes: " + str(volumes))
|
||
|
||
PlateNo = plate_indexes[0] + 1
|
||
hole_col = tip_columns[0] + 1
|
||
|
||
hole_row = 1
|
||
if self.num_channels == 1:
|
||
hole_row = tipspot_index % 8 + 1
|
||
|
||
step = self.api_client.Tapping(
|
||
dosage=int(volumes[0]),
|
||
plate_no=PlateNo,
|
||
is_whole_plate=False,
|
||
hole_row=hole_row,
|
||
hole_col=hole_col,
|
||
blending_times=0,
|
||
balance_height=0,
|
||
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
|
||
hole_numbers="1,2,3,4,5,6,7,8",
|
||
)
|
||
self.steps_todo_list.append(step)
|
||
|
||
async def pick_up_tips96(self, pickup: PickupTipRack):
|
||
raise NotImplementedError("The PRCXI backend does not support the 96 head.")
|
||
|
||
async def drop_tips96(self, drop: DropTipRack):
|
||
raise NotImplementedError("The PRCXI backend does not support the 96 head.")
|
||
|
||
async def aspirate96(self, aspiration: Union[MultiHeadAspirationPlate, MultiHeadAspirationContainer]):
|
||
raise NotImplementedError("The Opentrons backend does not support the 96 head.")
|
||
|
||
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer]):
|
||
raise NotImplementedError("The Opentrons backend does not support the 96 head.")
|
||
|
||
async def pick_up_resource(self, pickup: ResourcePickup):
|
||
raise NotImplementedError("The Opentrons backend does not support the robotic arm.")
|
||
|
||
async def move_picked_up_resource(self, move: ResourceMove):
|
||
raise NotImplementedError("The Opentrons backend does not support the robotic arm.")
|
||
|
||
async def drop_resource(self, drop: ResourceDrop):
|
||
raise NotImplementedError("The Opentrons backend does not support the robotic arm.")
|
||
|
||
def can_pick_up_tip(self, channel_idx: int, tip: Tip) -> bool:
|
||
return True # PRCXI9300Backend does not have tip compatibility issues
|
||
|
||
def serialize(self) -> dict:
|
||
raise NotImplementedError()
|
||
|
||
@property
|
||
def num_channels(self) -> int:
|
||
return self._num_channels
|
||
|
||
|
||
class PRCXI9300Api:
|
||
def __init__(
|
||
self,
|
||
host: str = "127.0.0.1",
|
||
port: int = 9999,
|
||
timeout: float = 10.0,
|
||
axis="Left",
|
||
debug: bool = False,
|
||
is_9320: bool = False,
|
||
) -> None:
|
||
self.host, self.port, self.timeout = host, port, timeout
|
||
self.debug = debug
|
||
self.axis = axis
|
||
self.is_9320 = is_9320
|
||
|
||
@staticmethod
|
||
def _len_prefix(n: int) -> bytes:
|
||
return bytes.fromhex(format(n, "016x"))
|
||
|
||
def _raw_request(self, payload: str) -> str:
|
||
if self.debug:
|
||
return " "
|
||
with contextlib.closing(socket.socket()) as sock:
|
||
sock.settimeout(self.timeout)
|
||
sock.connect((self.host, self.port))
|
||
data = payload.encode()
|
||
sock.sendall(self._len_prefix(len(data)) + data)
|
||
|
||
chunks, first = [], True
|
||
while True:
|
||
chunk = sock.recv(4096)
|
||
if not chunk:
|
||
break
|
||
if first:
|
||
chunk, first = chunk[8:], False
|
||
chunks.append(chunk)
|
||
return b"".join(chunks).decode()
|
||
|
||
# ---------------------------------------------------- 方案相关(ISolution)
|
||
def list_solutions(self) -> List[Dict[str, Any]]:
|
||
"""GetSolutionList"""
|
||
return self.call("ISolution", "GetSolutionList")
|
||
|
||
def load_solution(self, solution_id: str) -> bool:
|
||
"""LoadSolution"""
|
||
return self.call("ISolution", "LoadSolution", [solution_id])
|
||
|
||
def add_solution(self, name: str, matrix_id: str, steps: List[Dict[str, Any]]) -> str:
|
||
"""AddSolution → 返回新方案 GUID"""
|
||
return self.call("ISolution", "AddSolution", [name, matrix_id, steps])
|
||
|
||
# ---------------------------------------------------- 自动化控制(IAutomation)
|
||
def start(self) -> bool:
|
||
return self.call("IAutomation", "Start")
|
||
|
||
def call(self, service: str, method: str, params: Optional[list] = None) -> Any:
|
||
payload = json.dumps(
|
||
{"ServiceName": service, "MethodName": method, "Paramters": params or []}, separators=(",", ":")
|
||
)
|
||
resp = json.loads(self._raw_request(payload))
|
||
if not resp.get("Success", False):
|
||
raise PRCXIError(resp.get("Msg", "Unknown error"))
|
||
data = resp.get("Data")
|
||
try:
|
||
return json.loads(data)
|
||
except (TypeError, json.JSONDecodeError):
|
||
return data
|
||
|
||
def pause(self) -> bool:
|
||
"""Pause"""
|
||
return self.call("IAutomation", "Pause")
|
||
|
||
def resume(self) -> bool:
|
||
"""Resume"""
|
||
return self.call("IAutomation", "Resume")
|
||
|
||
def get_error_code(self) -> Optional[str]:
|
||
"""GetErrorCode"""
|
||
return self.call("IAutomation", "GetErrorCode")
|
||
|
||
def get_reset_status(self) -> bool:
|
||
"""GetErrorCode"""
|
||
if self.debug:
|
||
return True
|
||
res = self.call("IAutomation", "GetResetStatus")
|
||
return not res
|
||
|
||
def clear_error_code(self) -> bool:
|
||
"""RemoveErrorCodet"""
|
||
return self.call("IAutomation", "RemoveErrorCodet")
|
||
|
||
# ---------------------------------------------------- 运行状态(IMachineState)
|
||
def step_state_list(self) -> List[Dict[str, Any]]:
|
||
"""GetStepStateList"""
|
||
return self.call("IMachineState", "GetStepStateList")
|
||
|
||
def step_status(self, seq_num: int) -> Dict[str, Any]:
|
||
"""GetStepStatus"""
|
||
return self.call("IMachineState", "GetStepStatus", [seq_num])
|
||
|
||
def step_state(self, seq_num: int) -> Dict[str, Any]:
|
||
"""GetStepState"""
|
||
return self.call("IMachineState", "GetStepState", [seq_num])
|
||
|
||
def axis_location(self, axis_num: int = 1) -> Dict[str, Any]:
|
||
"""GetLocation"""
|
||
return self.call("IMachineState", "GetLocation", [axis_num])
|
||
|
||
# ---------------------------------------------------- 版位矩阵(IMatrix)
|
||
def get_all_materials(self) -> Dict[str, Any]:
|
||
"""GetStepState"""
|
||
return self.call("IMatrix", "GetAllMaterial", [])
|
||
|
||
def list_matrices(self) -> List[Dict[str, Any]]:
|
||
"""GetWorkTabletMatrices"""
|
||
return self.call("IMatrix", "GetWorkTabletMatrices")
|
||
|
||
def matrix_by_id(self, matrix_id: str) -> Dict[str, Any]:
|
||
"""GetWorkTabletMatrixById"""
|
||
return self.call("IMatrix", "GetWorkTabletMatrixById", [matrix_id])
|
||
|
||
def add_WorkTablet_Matrix(self, matrix: MatrixInfo):
|
||
return self.call("IMatrix", "AddWorkTabletMatrix2" if self.is_9320 else "AddWorkTabletMatrix", [matrix])
|
||
|
||
def Load(
|
||
self,
|
||
dosage: int,
|
||
plate_no: int,
|
||
is_whole_plate: bool,
|
||
hole_row: int,
|
||
hole_col: int,
|
||
blending_times: int,
|
||
balance_height: int,
|
||
plate_or_hole: str,
|
||
hole_numbers: str,
|
||
assist_fun1: str = "",
|
||
assist_fun2: str = "",
|
||
assist_fun3: str = "",
|
||
assist_fun4: str = "",
|
||
assist_fun5: str = "",
|
||
liquid_method: str = "NormalDispense",
|
||
) -> Dict[str, Any]:
|
||
return {
|
||
"StepAxis": self.axis,
|
||
"Function": "Load",
|
||
"DosageNum": dosage,
|
||
"PlateNo": plate_no,
|
||
"IsWholePlate": is_whole_plate,
|
||
"HoleRow": hole_row,
|
||
"HoleCol": hole_col,
|
||
"BlendingTimes": blending_times,
|
||
"BalanceHeight": balance_height,
|
||
"PlateOrHoleNum": plate_or_hole,
|
||
"AssistFun1": assist_fun1,
|
||
"AssistFun2": assist_fun2,
|
||
"AssistFun3": assist_fun3,
|
||
"AssistFun4": assist_fun4,
|
||
"AssistFun5": assist_fun5,
|
||
"HoleNumbers": hole_numbers,
|
||
"LiquidDispensingMethod": liquid_method,
|
||
}
|
||
|
||
def Imbibing(
|
||
self,
|
||
dosage: int,
|
||
plate_no: int,
|
||
is_whole_plate: bool,
|
||
hole_row: int,
|
||
hole_col: int,
|
||
blending_times: int,
|
||
balance_height: int,
|
||
plate_or_hole: str,
|
||
hole_numbers: str,
|
||
assist_fun1: str = "",
|
||
assist_fun2: str = "",
|
||
assist_fun3: str = "",
|
||
assist_fun4: str = "",
|
||
assist_fun5: str = "",
|
||
liquid_method: str = "NormalDispense",
|
||
) -> Dict[str, Any]:
|
||
return {
|
||
"StepAxis": self.axis,
|
||
"Function": "Imbibing",
|
||
"DosageNum": dosage,
|
||
"PlateNo": plate_no,
|
||
"IsWholePlate": is_whole_plate,
|
||
"HoleRow": hole_row,
|
||
"HoleCol": hole_col,
|
||
"BlendingTimes": blending_times,
|
||
"BalanceHeight": balance_height,
|
||
"PlateOrHoleNum": plate_or_hole,
|
||
"AssistFun1": assist_fun1,
|
||
"AssistFun2": assist_fun2,
|
||
"AssistFun3": assist_fun3,
|
||
"AssistFun4": assist_fun4,
|
||
"AssistFun5": assist_fun5,
|
||
"HoleNumbers": hole_numbers,
|
||
"LiquidDispensingMethod": liquid_method,
|
||
}
|
||
|
||
def Tapping(
|
||
self,
|
||
dosage: int,
|
||
plate_no: int,
|
||
is_whole_plate: bool,
|
||
hole_row: int,
|
||
hole_col: int,
|
||
blending_times: int,
|
||
balance_height: int,
|
||
plate_or_hole: str,
|
||
hole_numbers: str,
|
||
assist_fun1: str = "",
|
||
assist_fun2: str = "",
|
||
assist_fun3: str = "",
|
||
assist_fun4: str = "",
|
||
assist_fun5: str = "",
|
||
liquid_method: str = "NormalDispense",
|
||
) -> Dict[str, Any]:
|
||
return {
|
||
"StepAxis": self.axis,
|
||
"Function": "Tapping",
|
||
"DosageNum": dosage,
|
||
"PlateNo": plate_no,
|
||
"IsWholePlate": is_whole_plate,
|
||
"HoleRow": hole_row,
|
||
"HoleCol": hole_col,
|
||
"BlendingTimes": blending_times,
|
||
"BalanceHeight": balance_height,
|
||
"PlateOrHoleNum": plate_or_hole,
|
||
"AssistFun1": assist_fun1,
|
||
"AssistFun2": assist_fun2,
|
||
"AssistFun3": assist_fun3,
|
||
"AssistFun4": assist_fun4,
|
||
"AssistFun5": assist_fun5,
|
||
"HoleNumbers": hole_numbers,
|
||
"LiquidDispensingMethod": liquid_method,
|
||
}
|
||
|
||
def Blending(
|
||
self,
|
||
dosage: int,
|
||
plate_no: int,
|
||
is_whole_plate: bool,
|
||
hole_row: int,
|
||
hole_col: int,
|
||
blending_times: int,
|
||
balance_height: int,
|
||
plate_or_hole: str,
|
||
hole_numbers: str,
|
||
assist_fun1: str = "",
|
||
assist_fun2: str = "",
|
||
assist_fun3: str = "",
|
||
assist_fun4: str = "",
|
||
assist_fun5: str = "",
|
||
liquid_method: str = "NormalDispense",
|
||
) -> Dict[str, Any]:
|
||
return {
|
||
"StepAxis": self.axis,
|
||
"Function": "Blending",
|
||
"DosageNum": dosage,
|
||
"PlateNo": plate_no,
|
||
"IsWholePlate": is_whole_plate,
|
||
"HoleRow": hole_row,
|
||
"HoleCol": hole_col,
|
||
"BlendingTimes": blending_times,
|
||
"BalanceHeight": balance_height,
|
||
"PlateOrHoleNum": plate_or_hole,
|
||
"AssistFun1": assist_fun1,
|
||
"AssistFun2": assist_fun2,
|
||
"AssistFun3": assist_fun3,
|
||
"AssistFun4": assist_fun4,
|
||
"AssistFun5": assist_fun5,
|
||
"HoleNumbers": hole_numbers,
|
||
"LiquidDispensingMethod": liquid_method,
|
||
}
|
||
|
||
def UnLoad(
|
||
self,
|
||
dosage: int,
|
||
plate_no: int,
|
||
is_whole_plate: bool,
|
||
hole_row: int,
|
||
hole_col: int,
|
||
blending_times: int,
|
||
balance_height: int,
|
||
plate_or_hole: str,
|
||
hole_numbers: str,
|
||
assist_fun1: str = "",
|
||
assist_fun2: str = "",
|
||
assist_fun3: str = "",
|
||
assist_fun4: str = "",
|
||
assist_fun5: str = "",
|
||
liquid_method: str = "NormalDispense",
|
||
) -> Dict[str, Any]:
|
||
return {
|
||
"StepAxis": self.axis,
|
||
"Function": "UnLoad",
|
||
"DosageNum": dosage,
|
||
"PlateNo": plate_no,
|
||
"IsWholePlate": is_whole_plate,
|
||
"HoleRow": hole_row,
|
||
"HoleCol": hole_col,
|
||
"BlendingTimes": blending_times,
|
||
"BalanceHeight": balance_height,
|
||
"PlateOrHoleNum": plate_or_hole,
|
||
"AssistFun1": assist_fun1,
|
||
"AssistFun2": assist_fun2,
|
||
"AssistFun3": assist_fun3,
|
||
"AssistFun4": assist_fun4,
|
||
"AssistFun5": assist_fun5,
|
||
"HoleNumbers": hole_numbers,
|
||
"LiquidDispensingMethod": liquid_method,
|
||
}
|
||
|
||
class DefaultLayout:
|
||
|
||
def __init__(self, product_name: str = "PRCXI9300"):
|
||
self.labresource = {}
|
||
if product_name not in ["PRCXI9300", "PRCXI9320"]:
|
||
raise ValueError(f"Unsupported product_name: {product_name}. Only 'PRCXI9300' and 'PRCXI9320' are supported.")
|
||
|
||
if product_name == "PRCXI9300":
|
||
self.rows = 2
|
||
self.columns = 3
|
||
self.layout = [1, 2, 3, 4, 5, 6]
|
||
self.trash_slot = 3
|
||
self.waste_liquid_slot = 6
|
||
|
||
elif product_name == "PRCXI9320":
|
||
self.rows = 3
|
||
self.columns = 4
|
||
self.layout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||
self.trash_slot = 16
|
||
self.waste_liquid_slot = 12
|
||
self.default_layout = {"MatrixId":f"{time.time()}","MatrixName":f"{time.time()}","MatrixCount":16,"WorkTablets":
|
||
[{"Number": 1, "Code": "T1", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 2, "Code": "T2", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 3, "Code": "T3", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 4, "Code": "T4", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 5, "Code": "T5", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 6, "Code": "T6", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 7, "Code": "T7", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 8, "Code": "T8", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 9, "Code": "T9", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 10, "Code": "T10", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 11, "Code": "T11", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 12, "Code": "T12", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}}, # 这个设置成废液槽,用储液槽表示
|
||
{"Number": 13, "Code": "T13", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 14, "Code": "T14", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 15, "Code": "T15", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
||
{"Number": 16, "Code": "T16", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}} # 这个设置成垃圾桶,用储液槽表示
|
||
]
|
||
}
|
||
|
||
def get_layout(self) -> Dict[str, Any]:
|
||
return {
|
||
"rows": self.rows,
|
||
"columns": self.columns,
|
||
"layout": self.layout,
|
||
"trash_slot": self.trash_slot,
|
||
"waste_liquid_slot": self.waste_liquid_slot
|
||
}
|
||
|
||
def get_trash_slot(self) -> int:
|
||
return self.trash_slot
|
||
|
||
def get_waste_liquid_slot(self) -> int:
|
||
return self.waste_liquid_slot
|
||
|
||
def add_lab_resource(self, material_info):
|
||
self.labresource = material_info
|
||
|
||
def recommend_layout(self, needs: List[Tuple[str, str, int]]) -> Dict[str, Any]:
|
||
layout_list = []
|
||
for reagent_name, material_name, count in needs:
|
||
|
||
if material_name not in self.labresource:
|
||
raise ValueError(f"Material {reagent_name} not found in lab resources.")
|
||
|
||
# 预留位置12和16不动
|
||
reserved_positions = {12, 16}
|
||
available_positions = [i for i in range(1, 17) if i not in reserved_positions]
|
||
|
||
# 计算总需求
|
||
total_needed = sum(count for _, _, count in needs)
|
||
if total_needed > len(available_positions):
|
||
raise ValueError(f"需要 {total_needed} 个位置,但只有 {len(available_positions)} 个可用位置(排除位置12和16)")
|
||
|
||
# 依次分配位置
|
||
current_pos = 0
|
||
for reagent_name, material_name, count in needs:
|
||
|
||
material_uuid = self.labresource[material_name]['uuid']
|
||
material_enum = self.labresource[material_name]['materialEnum']
|
||
|
||
for _ in range(count):
|
||
if current_pos >= len(available_positions):
|
||
raise ValueError("位置不足,无法分配更多物料")
|
||
|
||
position = available_positions[current_pos]
|
||
# 找到对应的tablet并更新
|
||
for tablet in self.default_layout['WorkTablets']:
|
||
if tablet['Number'] == position:
|
||
tablet['Material']['uuid'] = material_uuid
|
||
tablet['Material']['materialEnum'] = material_enum
|
||
layout_list.append(dict(reagent_name=reagent_name, material_name=material_name, positions=position))
|
||
break
|
||
current_pos += 1
|
||
return self.default_layout, layout_list
|
||
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# Example usage
|
||
# 1. 用导出的json,给每个T1 T2板子设定相应的物料,如果是孔板和枪头盒,要对应区分
|
||
# 2. backend需要支持num channel为1的情况
|
||
# 3. 设计一个单点动作流程,可以跑
|
||
# 4.
|
||
|
||
# deck = PRCXI9300Deck(name="PRCXI_Deck_9300", size_x=100, size_y=100, size_z=100)
|
||
|
||
# from pylabrobot.resources.opentrons.tip_racks import opentrons_96_tiprack_300ul,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:
|
||
# 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_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)
|
||
# return new_plate
|
||
|
||
# def get_tip_rack(name: str) -> PRCXI9300Container:
|
||
# tip_racks = opentrons_96_tiprack_300ul("name").serialize()
|
||
# tip_rack = PRCXI9300Container(name=name, size_x=50, size_y=50, size_z=10, category="tip_rack",
|
||
# ordering=tip_racks["ordering"])
|
||
# 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)
|
||
# return new_tip_rack
|
||
|
||
# plate1 = get_tip_rack("RackT1")
|
||
# plate1.load_state({
|
||
# "Material": {
|
||
# "uuid": "076250742950465b9d6ea29a225dfb00",
|
||
# "Code": "ZX-001-300",
|
||
# "Name": "300μL Tip头"
|
||
# }
|
||
# })
|
||
|
||
# plate2 = get_well_container("PlateT2")
|
||
# plate2.load_state({
|
||
# "Material": {
|
||
# "uuid": "57b1e4711e9e4a32b529f3132fc5931f",
|
||
# "Code": "ZX-019-2.2",
|
||
# "Name": "96深孔板"
|
||
# }
|
||
# })
|
||
|
||
# plate3 = PRCXI9300Trash("trash", size_x=50, size_y=100, size_z=10, category="trash")
|
||
# plate3.load_state({
|
||
# "Material": {
|
||
# "uuid": "730067cf07ae43849ddf4034299030e9"
|
||
# }
|
||
# })
|
||
|
||
# plate4 = get_well_container("PlateT4")
|
||
# plate4.load_state({
|
||
# "Material": {
|
||
# "uuid": "57b1e4711e9e4a32b529f3132fc5931f",
|
||
# "Code": "ZX-019-2.2",
|
||
# "Name": "96深孔板"
|
||
# }
|
||
# })
|
||
|
||
# plate5 = get_well_container("PlateT5")
|
||
# plate5.load_state({
|
||
# "Material": {
|
||
# "uuid": "57b1e4711e9e4a32b529f3132fc5931f",
|
||
# "Code": "ZX-019-2.2",
|
||
# "Name": "96深孔板"
|
||
# }
|
||
# })
|
||
# plate6 = get_well_container("PlateT6")
|
||
|
||
# plate6.load_state({
|
||
# "Material": {
|
||
# "uuid": "57b1e4711e9e4a32b529f3132fc5931f",
|
||
# "Code": "ZX-019-2.2",
|
||
# "Name": "96深孔板"
|
||
# }
|
||
# })
|
||
|
||
# deck.assign_child_resource(plate1, location=Coordinate(0, 0, 0))
|
||
# deck.assign_child_resource(plate2, location=Coordinate(0, 0, 0))
|
||
# deck.assign_child_resource(plate3, location=Coordinate(0, 0, 0))
|
||
# deck.assign_child_resource(plate4, location=Coordinate(0, 0, 0))
|
||
# deck.assign_child_resource(plate5, location=Coordinate(0, 0, 0))
|
||
# deck.assign_child_resource(plate6, location=Coordinate(0, 0, 0))
|
||
|
||
# # # plate_2_liquids = [[('water', 500)]]*96
|
||
|
||
# # # plate2.set_well_liquids(plate_2_liquids)
|
||
|
||
|
||
|
||
|
||
# handler = PRCXI9300Handler(deck=deck, host="10.181.214.132", port=9999,
|
||
# timeout=10.0, setup=False, debug=False,
|
||
# simulator=True,
|
||
# matrix_id="71593",
|
||
# channel_num=8, axis="Left") # Initialize the handler with the deck and host settings
|
||
|
||
# plate_2_liquids = handler.set_group("water", plate2.children[:8], [200]*8)
|
||
|
||
# plate5_liquids = handler.set_group("master_mix", plate5.children[:8], [100]*8)
|
||
|
||
# handler.set_tiprack([plate1])
|
||
# asyncio.run(handler.setup()) # Initialize the handler and setup the connection
|
||
# from pylabrobot.resources import set_volume_tracking
|
||
# from pylabrobot.resources import set_tip_tracking
|
||
# set_volume_tracking(enabled=True)
|
||
# from unilabos.resources.graphio import *
|
||
# # A = tree_to_list([resource_plr_to_ulab(deck)])
|
||
# # with open("deck_9300_new.json", "w", encoding="utf-8") as f:
|
||
# # json.dump(A, f, indent=4, ensure_ascii=False)
|
||
# asyncio.run(handler.create_protocol(protocol_name="Test Protocol")) # Initialize the backend and setup the connection
|
||
# asyncio.run(handler.transfer_group("water", "master_mix", 100)) # Reset tip tracking
|
||
|
||
# asyncio.run(handler.pick_up_tips(plate1.children[:8],[0,1,2,3,4,5,6,7]))
|
||
# print(plate1.children[:8])
|
||
# asyncio.run(handler.aspirate(plate2.children[:8],[50]*8, [0,1,2,3,4,5,6,7]))
|
||
# print(plate2.children[:8])
|
||
# asyncio.run(handler.dispense(plate5.children[:8],[50]*8,[0,1,2,3,4,5,6,7]))
|
||
# print(plate5.children[:8])
|
||
|
||
# #asyncio.run(handler.drop_tips(tip_rack.children[8:16],[0,1,2,3,4,5,6,7]))
|
||
# asyncio.run(handler.discard_tips([0,1,2,3,4,5,6,7]))
|
||
|
||
# asyncio.run(handler.mix(well_containers.children[:8
|
||
# ], mix_time=3, mix_vol=50, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
|
||
# #print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
|
||
# asyncio.run(handler.add_liquid(
|
||
# asp_vols=[100]*16,
|
||
# dis_vols=[100]*16,
|
||
# reagent_sources=plate2.children[:16],
|
||
# targets=plate5.children[:16],
|
||
# use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
|
||
# flow_rates=[None] * 32,
|
||
# offsets=[Coordinate(0, 0, 0)] * 32,
|
||
# liquid_height=[None] * 16,
|
||
# blow_out_air_volume=[None] * 16,
|
||
# delays=None,
|
||
# mix_time=3,
|
||
# mix_vol=50,
|
||
# spread="wide",
|
||
# ))
|
||
# asyncio.run(handler.run_protocol()) # Run the protocol
|
||
# asyncio.run(handler.remove_liquid(
|
||
# vols=[100]*16,
|
||
# sources=plate2.children[-16:],
|
||
# waste_liquid=plate5.children[:16], # 这个有些奇怪,但是好像也只能这么写
|
||
# use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
|
||
# flow_rates=[None] * 32,
|
||
# offsets=[Coordinate(0, 0, 0)] * 32,
|
||
# liquid_height=[None] * 32,
|
||
# blow_out_air_volume=[None] * 32,
|
||
# spread="wide",
|
||
# ))
|
||
|
||
# acid = [20]*8+[40]*8+[60]*8+[80]*8+[100]*8+[120]*8+[140]*8+[160]*8+[180]*8+[200]*8+[220]*8+[240]*8
|
||
# alkaline = acid[::-1] # Reverse the acid list for alkaline
|
||
# asyncio.run(handler.transfer_liquid(
|
||
# asp_vols=acid,
|
||
# dis_vols=acid,
|
||
# tip_racks=[plate1],
|
||
# sources=plate2.children[:],
|
||
# targets=plate5.children[:],
|
||
# use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
|
||
# offsets=[Coordinate(0, 0, 0)] * 32,
|
||
# asp_flow_rates=[None] * 16,
|
||
# dis_flow_rates=[None] * 16,
|
||
# liquid_height=[None] * 32,
|
||
# blow_out_air_volume=[None] * 32,
|
||
# mix_times=3,
|
||
# mix_vol=50,
|
||
# spread="wide",
|
||
# ))
|
||
# asyncio.run(handler.run_protocol()) # Run the protocol
|
||
# # input("Running protocol...")
|
||
# # input("Press Enter to continue...") # Wait for user input before proceeding
|
||
# # print("PRCXI9300Handler initialized with deck and host settings.")
|
||
|
||
|
||
|
||
### 9320 ###
|
||
|
||
|
||
deck = PRCXI9300Deck(name="PRCXI_Deck", size_x=100, size_y=100, size_z=100)
|
||
|
||
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:
|
||
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_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)
|
||
return new_plate
|
||
|
||
def get_tip_rack(name: str, child_prefix: str="tip") -> 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({
|
||
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)
|
||
return new_tip_rack
|
||
|
||
plate1 = get_tip_rack("RackT1")
|
||
plate1.load_state(
|
||
{"Material": {"uuid": "068b3815e36b4a72a59bae017011b29f", "Code": "ZX-001-10+", "Name": "10μL加长 Tip头"}}
|
||
)
|
||
plate2 = get_well_container("PlateT2")
|
||
plate2.load_state(
|
||
{"Material": {"uuid": "b05b3b2aafd94ec38ea0cd3215ecea8f", "Code": "ZX-78-096", "Name": "细菌培养皿"}}
|
||
)
|
||
plate3 = get_well_container("PlateT3")
|
||
plate3.load_state(
|
||
{
|
||
"Material": {
|
||
"uuid": "04211a2dc93547fe9bf6121eac533650",
|
||
}
|
||
}
|
||
)
|
||
plate4 = get_well_container("PlateT4")
|
||
plate4.load_state(
|
||
{"Material": {"uuid": "b05b3b2aafd94ec38ea0cd3215ecea8f", "Code": "ZX-78-096", "Name": "细菌培养皿"}}
|
||
)
|
||
|
||
plate5 = get_tip_rack("RackT5")
|
||
plate5.load_state(
|
||
{
|
||
"Material": {
|
||
"uuid": "076250742950465b9d6ea29a225dfb00",
|
||
"Code": "ZX-001-300",
|
||
"SupplyType": 1,
|
||
"Name": "300μL Tip头",
|
||
}
|
||
}
|
||
)
|
||
plate6 = get_well_container("PlateT6")
|
||
plate6.load_state(
|
||
{
|
||
"Material": {
|
||
"uuid": "e146697c395e4eabb3d6b74f0dd6aaf7",
|
||
"Code": "1",
|
||
"SupplyType": 1,
|
||
"Name": "ep适配器",
|
||
"SummaryName": "ep适配器",
|
||
}
|
||
}
|
||
)
|
||
plate7 = PRCXI9300Container(
|
||
name="plateT7", size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict()
|
||
)
|
||
plate7.load_state({"Material": {"uuid": "04211a2dc93547fe9bf6121eac533650"}})
|
||
plate8 = get_tip_rack("PlateT8")
|
||
plate8.load_state({"Material": {"uuid": "04211a2dc93547fe9bf6121eac533650"}})
|
||
plate9 = get_well_container("PlateT9")
|
||
plate9.load_state(
|
||
{
|
||
"Material": {
|
||
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
|
||
"Code": "ZX-58-0001",
|
||
"SupplyType": 2,
|
||
"Name": "全裙边 PCR适配器",
|
||
"SummaryName": "全裙边 PCR适配器",
|
||
}
|
||
}
|
||
)
|
||
plate10 = get_well_container("PlateT10")
|
||
plate10.load_state(
|
||
{
|
||
"Material": {
|
||
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
|
||
"Code": "ZX-58-0001",
|
||
"SupplyType": 2,
|
||
"Name": "全裙边 PCR适配器",
|
||
"SummaryName": "全裙边 PCR适配器",
|
||
}
|
||
}
|
||
)
|
||
plate11 = get_well_container("PlateT11")
|
||
plate11.load_state(
|
||
{
|
||
"Material": {
|
||
"uuid": "04211a2dc93547fe9bf6121eac533650",
|
||
}
|
||
}
|
||
)
|
||
plate12 = get_well_container("PlateT12")
|
||
plate12.load_state({"Material": {"uuid": "04211a2dc93547fe9bf6121eac533650"}})
|
||
plate13 = get_well_container("PlateT13")
|
||
plate13.load_state(
|
||
{
|
||
"Material": {
|
||
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
|
||
"Code": "ZX-58-0001",
|
||
"SupplyType": 2,
|
||
"Name": "全裙边 PCR适配器",
|
||
"SummaryName": "全裙边 PCR适配器",
|
||
}
|
||
}
|
||
),
|
||
plate14 = get_well_container("PlateT14")
|
||
plate14.load_state(
|
||
{
|
||
"Material": {
|
||
"uuid": "4a043a07c65a4f9bb97745e1f129b165",
|
||
"Code": "ZX-58-0001",
|
||
"SupplyType": 2,
|
||
"Name": "全裙边 PCR适配器",
|
||
"SummaryName": "全裙边 PCR适配器",
|
||
}
|
||
}
|
||
),
|
||
plate15 = get_well_container("PlateT15")
|
||
plate15.load_state({"Material": {"uuid": "04211a2dc93547fe9bf6121eac533650"}})
|
||
|
||
trash = PRCXI9300Trash(name="trash", size_x=50, size_y=50, size_z=10, category="trash")
|
||
trash.load_state({"Material": {"uuid": "730067cf07ae43849ddf4034299030e9"}})
|
||
|
||
# container_for_nothing = PRCXI9300Container(name="container_for_nothing", size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict())
|
||
|
||
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(
|
||
name="container_for_nothin3",
|
||
size_x=50,
|
||
size_y=50,
|
||
size_z=10,
|
||
category="plate",
|
||
ordering=collections.OrderedDict(),
|
||
),
|
||
location=Coordinate(0, 0, 0),
|
||
)
|
||
deck.assign_child_resource(plate4, location=Coordinate(0, 0, 0))
|
||
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(
|
||
name="container_for_nothing7",
|
||
size_x=50,
|
||
size_y=50,
|
||
size_z=10,
|
||
category="plate",
|
||
ordering=collections.OrderedDict(),
|
||
),
|
||
location=Coordinate(0, 0, 0),
|
||
)
|
||
deck.assign_child_resource(
|
||
PRCXI9300Container(
|
||
name="container_for_nothing8",
|
||
size_x=50,
|
||
size_y=50,
|
||
size_z=10,
|
||
category="plate",
|
||
ordering=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(
|
||
name="container_for_nothing11",
|
||
size_x=50,
|
||
size_y=50,
|
||
size_z=10,
|
||
category="plate",
|
||
ordering=collections.OrderedDict(),
|
||
),
|
||
location=Coordinate(0, 0, 0),
|
||
)
|
||
deck.assign_child_resource(
|
||
PRCXI9300Container(
|
||
name="container_for_nothing12",
|
||
size_x=50,
|
||
size_y=50,
|
||
size_z=10,
|
||
category="plate",
|
||
ordering=collections.OrderedDict(),
|
||
),
|
||
location=Coordinate(0, 0, 0),
|
||
)
|
||
deck.assign_child_resource(plate13, location=Coordinate(0, 0, 0))
|
||
deck.assign_child_resource(plate14, location=Coordinate(0, 0, 0))
|
||
deck.assign_child_resource(plate15, location=Coordinate(0, 0, 0))
|
||
deck.assign_child_resource(trash, location=Coordinate(0, 0, 0))
|
||
|
||
from unilabos.resources.graphio import tree_to_list, resource_plr_to_ulab
|
||
|
||
A = tree_to_list([resource_plr_to_ulab(deck)])
|
||
with open("deck.json", "w", encoding="utf-8") as f:
|
||
json.dump(A, f, indent=4, ensure_ascii=False)
|
||
|
||
handler = PRCXI9300Handler(
|
||
deck=deck,
|
||
host="192.168.0.121",
|
||
port=9999,
|
||
timeout=10.0,
|
||
setup=True,
|
||
debug=False,
|
||
matrix_id="5de524d0-3f95-406c-86dd-f83626ebc7cb",
|
||
channel_num=1,
|
||
axis="Right",
|
||
simulator=False,
|
||
is_9320=True,
|
||
)
|
||
backend: PRCXI9300Backend = handler.backend
|
||
from pylabrobot.resources import set_volume_tracking
|
||
set_volume_tracking(enabled=True)
|
||
# res = backend.api_client.get_all_materials()
|
||
asyncio.run(handler.setup()) # Initialize the handler and setup the connection
|
||
handler.set_tiprack([plate1, plate5]) # Set the tip rack for the handler
|
||
handler.set_liquid([plate9.get_well("H12")], ["water"], [5])
|
||
asyncio.run(handler.create_protocol(protocol_name="Test Protocol"))
|
||
asyncio.run(handler.pick_up_tips([plate5.get_item("C5")], [0]))
|
||
asyncio.run(handler.aspirate([plate9.get_item("H12")], [5], [0]))
|
||
|
||
for well in plate13.get_all_items():
|
||
# well_pos = well.name.split("_")[1] # 走一行
|
||
# if well_pos.startswith("A"):
|
||
if well.name.startswith("PlateT13"): # 走整个Plate
|
||
asyncio.run(handler.dispense([well], [0.01], [0]))
|
||
|
||
# asyncio.run(handler.dispense([plate10.get_item("H12")], [1], [0]))
|
||
# asyncio.run(handler.dispense([plate13.get_item("A1")], [1], [0]))
|
||
# asyncio.run(handler.dispense([plate14.get_item("C5")], [1], [0]))
|
||
asyncio.run(handler.mix([plate10.get_item("H12")], mix_time=3, mix_vol=5))
|
||
asyncio.run(handler.discard_tips([0]))
|
||
asyncio.run(handler.run_protocol())
|
||
time.sleep(5)
|
||
os._exit(0)
|
||
# 第一种情景:一个孔往多个孔加液
|
||
# plate_2_liquids = handler.set_group("water", [plate2.children[0]], [300])
|
||
# plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100]*23)
|
||
# 第二个情景:多个孔往多个孔加液(但是个数得对应)
|
||
plate_2_liquids = handler.set_group("water", plate2.children[:23], [300]*23)
|
||
plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100]*23)
|
||
|
||
# plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8
|
||
|
||
# plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8
|
||
|
||
# A = tree_to_list([resource_plr_to_ulab(deck)])
|
||
# # with open("deck.json", "w", encoding="utf-8") as f:
|
||
# # json.dump(A, f, indent=4, ensure_ascii=False)
|
||
|
||
# print(plate11.get_well(0).tracker.get_used_volume())
|
||
# Initialize the backend and setup the connection
|
||
asyncio.run(handler.transfer_group("water", "master_mix", 10)) # Reset tip tracking
|
||
|
||
|
||
# asyncio.run(handler.pick_up_tips([plate8.children[8]],[0]))
|
||
# print(plate8.children[8])
|
||
# asyncio.run(handler.run_protocol())
|
||
# asyncio.run(handler.aspirate([plate11.children[0]],[10], [0]))
|
||
# print(plate11.children[0])
|
||
# # asyncio.run(handler.run_protocol())
|
||
# asyncio.run(handler.dispense([plate1.children[0]],[10],[0]))
|
||
# print(plate1.children[0])
|
||
# asyncio.run(handler.run_protocol())
|
||
# asyncio.run(handler.mix([plate1.children[0]], mix_time=3, mix_vol=5, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
|
||
# print(plate1.children[0])
|
||
# asyncio.run(handler.discard_tips([0]))
|
||
|
||
# asyncio.run(handler.add_liquid(
|
||
# asp_vols=[10]*7,
|
||
# dis_vols=[10]*7,
|
||
# reagent_sources=plate11.children[:7],
|
||
# targets=plate1.children[2:9],
|
||
# use_channels=[0],
|
||
# flow_rates=[None] * 7,
|
||
# offsets=[Coordinate(0, 0, 0)] * 7,
|
||
# liquid_height=[None] * 7,
|
||
# blow_out_air_volume=[None] * 2,
|
||
# delays=None,
|
||
# mix_time=3,
|
||
# mix_vol=5,
|
||
# spread="custom",
|
||
# ))
|
||
|
||
# asyncio.run(handler.run_protocol()) # Run the protocol
|
||
|
||
|
||
|
||
|
||
# # # asyncio.run(handler.transfer_liquid(
|
||
# # # asp_vols=[10]*2,
|
||
# # # dis_vols=[10]*2,
|
||
# # # sources=plate11.children[:2],
|
||
# # # targets=plate11.children[-2:],
|
||
# # # use_channels=[0],
|
||
# # # offsets=[Coordinate(0, 0, 0)] * 4,
|
||
# # # liquid_height=[None] * 2,
|
||
# # # blow_out_air_volume=[None] * 2,
|
||
# # # delays=None,
|
||
# # # mix_times=3,
|
||
# # # mix_vol=5,
|
||
# # # spread="wide",
|
||
# # # tip_racks=[plate8]
|
||
# # # ))
|
||
|
||
# # # asyncio.run(handler.remove_liquid(
|
||
# # # vols=[10]*2,
|
||
# # # sources=plate11.children[:2],
|
||
# # # waste_liquid=plate11.children[43],
|
||
# # # use_channels=[0],
|
||
# # # offsets=[Coordinate(0, 0, 0)] * 4,
|
||
# # # liquid_height=[None] * 2,
|
||
# # # blow_out_air_volume=[None] * 2,
|
||
# # # delays=None,
|
||
# # # spread="wide"
|
||
# # # ))
|
||
# # asyncio.run(handler.run_protocol())
|
||
|
||
# # # asyncio.run(handler.discard_tips())
|
||
# # # asyncio.run(handler.mix(well_containers.children[:8
|
||
# # # ], mix_time=3, mix_vol=50, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
|
||
# # #print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
|
||
|
||
|
||
# # # asyncio.run(handler.remove_liquid(
|
||
# # # vols=[100]*16,
|
||
# # # sources=well_containers.children[-16:],
|
||
# # # waste_liquid=well_containers.children[:16], # 这个有些奇怪,但是好像也只能这么写
|
||
# # # use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
|
||
# # # flow_rates=[None] * 32,
|
||
# # # offsets=[Coordinate(0, 0, 0)] * 32,
|
||
# # # liquid_height=[None] * 32,
|
||
# # # blow_out_air_volume=[None] * 32,
|
||
# # # spread="wide",
|
||
# # # ))
|
||
# # # asyncio.run(handler.transfer_liquid(
|
||
# # # asp_vols=[100]*16,
|
||
# # # dis_vols=[100]*16,
|
||
# # # tip_racks=[tip_rack],
|
||
# # # sources=well_containers.children[-16:],
|
||
# # # targets=well_containers.children[:16],
|
||
# # # use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
|
||
# # # offsets=[Coordinate(0, 0, 0)] * 32,
|
||
# # # asp_flow_rates=[None] * 16,
|
||
# # # dis_flow_rates=[None] * 16,
|
||
# # # liquid_height=[None] * 32,
|
||
# # # blow_out_air_volume=[None] * 32,
|
||
# # # mix_times=3,
|
||
# # # mix_vol=50,
|
||
# # # spread="wide",
|
||
# # # ))
|
||
# # print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
|
||
# # # input("pick_up_tips add step")
|
||
#asyncio.run(handler.run_protocol()) # Run the protocol
|
||
# # # input("Running protocol...")
|
||
# # # input("Press Enter to continue...") # Wait for user input before proceeding
|
||
# # # print("PRCXI9300Handler initialized with deck and host settings.")
|
||
|
||
|
||
# 一些推荐版位组合的测试样例:
|
||
|
||
# 一些推荐版位组合的测试样例:
|
||
|
||
|
||
|
||
with open("prcxi_material.json", "r") as f:
|
||
material_info = json.load(f)
|
||
|
||
layout = DefaultLayout("PRCXI9320")
|
||
layout.add_lab_resource(material_info)
|
||
MatrixLayout_1, dict_1 = layout.recommend_layout([
|
||
("reagent_1", "96 细胞培养皿", 3),
|
||
("reagent_2", "12道储液槽", 1),
|
||
("reagent_3", "200μL Tip头", 7),
|
||
("reagent_4", "10μL加长 Tip头", 1),
|
||
])
|
||
print(dict_1)
|
||
MatrixLayout_2, dict_2 = layout.recommend_layout([
|
||
("reagent_1", "96深孔板", 4),
|
||
("reagent_2", "12道储液槽", 1),
|
||
("reagent_3", "200μL Tip头", 1),
|
||
("reagent_4", "10μL加长 Tip头", 1),
|
||
])
|
||
|
||
# with open("prcxi_material.json", "r") as f:
|
||
# material_info = json.load(f)
|
||
|
||
# layout = DefaultLayout("PRCXI9320")
|
||
# layout.add_lab_resource(material_info)
|
||
# MatrixLayout_1, dict_1 = layout.recommend_layout([
|
||
# ("reagent_1", "96 细胞培养皿", 3),
|
||
# ("reagent_2", "12道储液槽", 1),
|
||
# ("reagent_3", "200μL Tip头", 7),
|
||
# ("reagent_4", "10μL加长 Tip头", 1),
|
||
# ])
|
||
# print(dict_1)
|
||
# MatrixLayout_2, dict_2 = layout.recommend_layout([
|
||
# ("reagent_1", "96深孔板", 4),
|
||
# ("reagent_2", "12道储液槽", 1),
|
||
# ("reagent_3", "200μL Tip头", 1),
|
||
# ("reagent_4", "10μL加长 Tip头", 1),
|
||
# ])
|